Summary
Every split package declares fewer dependencies than its production code uses. The classes resolve inside the monorepo because the root pulls everything in together, but each packages/* directory is published as a standalone Composer package, and there the metadata is simply wrong. A consumer installing phpdocumentor/filesystem on its own gets a FlySystemAdapter whose League\Flysystem and Flyfinder classes are not required by anything.
I ran composer-require-checker against each split package in isolation — package directory copied out, its own composer install, autoload-dev and require-dev removed so only production code is scanned. Unknown symbols per package:
| Package |
unknown symbols |
| phpdocumentor/guides |
26 |
| phpdocumentor/guides-restructured-text |
28 |
| phpdocumentor/filesystem |
16 |
| phpdocumentor/guides-cli |
16 |
| phpdocumentor/guides-graphs |
12 |
| phpdocumentor/guides-code |
11 |
| phpdocumentor/guides-theme-rst |
11 |
| phpdocumentor/guides-markdown |
10 |
| phpdocumentor/dev-server |
8 |
| phpdocumentor/guides-theme-bootstrap |
6 |
psr/log is already handled separately in #1354 and no longer appears in these numbers.
Straightforward part
Most of it is a plain missing require for a library the code uses directly and that is already installed transitively. Nothing optional, no semantics involved:
| Package |
missing |
| filesystem |
league/flysystem, phpdocumentor/flyfinder |
| guides |
psr/container, psr/clock, league/uri-interfaces, symfony/http-client-contracts |
| guides-cli |
psr/container, psr/clock, psr/event-dispatcher, doctrine/deprecations, league/tactician, phpdocumentor/flyfinder, phpdocumentor/filesystem, webmozart/assert |
| guides-code |
twig/twig |
| guides-graphs |
symfony/http-client-contracts, webmozart/assert |
| guides-markdown |
symfony/string |
| guides-restructured-text |
symfony/string, phpdocumentor/flyfinder |
| guides-theme-rst |
twig/twig, symfony/polyfill-mbstring |
| dev-server |
guzzlehttp/psr7, ratchet/rfc6455, react/event-loop, psr/event-dispatcher, symfony/event-dispatcher-contracts |
Four things that need a decision, not a declaration
symfony/config and symfony/dependency-injection are used by the DependencyInjection classes of guides, guides-code, guides-graphs, guides-markdown, guides-restructured-text, guides-theme-bootstrap and guides-theme-rst. Declaring them as require forces every consumer to install Symfony's container even when wiring the library by hand. That is a product decision: hard requirement, suggest, or the extension classes move somewhere optional.
A dependency cycle. guides-restructured-text/src references phpDocumentor\Guides\Bootstrap\* in TabDirective.php, TabsNode.php and TabNode.php, while guides-theme-bootstrap requires guides-restructured-text. Declaring this one would close a ring, so it needs a code change rather than metadata.
Test support in production src. packages/guides/src/DependencyInjection/TestExtension.php uses Monolog\Logger, Monolog\Handler\TestHandler and Symfony\Component\Clock\MockClock. It is test infrastructure whose only user is tests/ApplicationTestCase.php, and it reached this package through a package merge rather than a decision. Moving it out of the shipped source, rather than making Monolog a production dependency, is #1357.
PHP extensions. ext-filter, ext-ctype and ext-mbstring are used but never declared.
Symbols like UnknownSymbol, template and templateArray come from docblock generics and belong in composer-require-checker.json, not in require.
One that reads worse than it is
guides-theme-rst calls mb_str_pad() in RstTheme/Twig/RstExtension.php:154, which PHP provides only from 8.3 while the package declares php: ^8.1. It is not broken: symfony/polyfill-mbstring has supplied the function since v1.28 and arrives through symfony/string and twig/twig. Verified by installing the package standalone in a PHP 8.1 container, where mb_str_pad() returns the padded string. So this is the same undeclared-dependency case as the rest and not a version-floor decision; #1356 declares the polyfill.
Why CI does not catch this
The repository already ships composer-require-checker.json and pins the tool in .phive/phars.xml, and phpDocumentor has a reusable workflow for exactly this — phpDocumentor/.github/.github/workflows/dependency-analysis.yml, which runs composer-require-checker check --config-file=$(pwd)/composer-require-checker.json. Wiring that in as-is would not help, for two reasons I measured:
The workflow checks the root composer.json, and this root has no production autoload at all, only autoload-dev pointing at test directories. Running the checker there reports There were no symbols found, please check your configuration. — a green no-op. Catching the gaps above needs the check to run per package, over packages/*/composer.json.
The pinned composer-require-checker 3.5.1 aborts on symfony/config v8 with Syntax error, unexpected '(', expecting T_VARIABLE in Definition/Builder/NodeBuilder.php, and --ignore-parse-errors does not help. It hits guides-cli, the only package declaring symfony/config directly, and the abort produces empty output that reads like a pass. The numbers in this issue were produced with composer-require-checker 4.24.0.
Expected output
Each published package declares the dependencies its own source code uses, and CI fails when that stops being true.
#1356 implements the straightforward table above and #1357 moves TestExtension. The remaining decision items are left here for maintainers.
Assisted by claude-code:claude-fable-5 — Session
Summary
Every split package declares fewer dependencies than its production code uses. The classes resolve inside the monorepo because the root pulls everything in together, but each
packages/*directory is published as a standalone Composer package, and there the metadata is simply wrong. A consumer installingphpdocumentor/filesystemon its own gets aFlySystemAdapterwhoseLeague\FlysystemandFlyfinderclasses are not required by anything.I ran
composer-require-checkeragainst each split package in isolation — package directory copied out, its owncomposer install,autoload-devandrequire-devremoved so only production code is scanned. Unknown symbols per package:psr/logis already handled separately in #1354 and no longer appears in these numbers.Straightforward part
Most of it is a plain missing
requirefor a library the code uses directly and that is already installed transitively. Nothing optional, no semantics involved:league/flysystem,phpdocumentor/flyfinderpsr/container,psr/clock,league/uri-interfaces,symfony/http-client-contractspsr/container,psr/clock,psr/event-dispatcher,doctrine/deprecations,league/tactician,phpdocumentor/flyfinder,phpdocumentor/filesystem,webmozart/asserttwig/twigsymfony/http-client-contracts,webmozart/assertsymfony/stringsymfony/string,phpdocumentor/flyfindertwig/twig,symfony/polyfill-mbstringguzzlehttp/psr7,ratchet/rfc6455,react/event-loop,psr/event-dispatcher,symfony/event-dispatcher-contractsFour things that need a decision, not a declaration
symfony/configandsymfony/dependency-injectionare used by theDependencyInjectionclasses of guides, guides-code, guides-graphs, guides-markdown, guides-restructured-text, guides-theme-bootstrap and guides-theme-rst. Declaring them asrequireforces every consumer to install Symfony's container even when wiring the library by hand. That is a product decision: hard requirement,suggest, or the extension classes move somewhere optional.A dependency cycle.
guides-restructured-text/srcreferencesphpDocumentor\Guides\Bootstrap\*inTabDirective.php,TabsNode.phpandTabNode.php, whileguides-theme-bootstraprequiresguides-restructured-text. Declaring this one would close a ring, so it needs a code change rather than metadata.Test support in production
src.packages/guides/src/DependencyInjection/TestExtension.phpusesMonolog\Logger,Monolog\Handler\TestHandlerandSymfony\Component\Clock\MockClock. It is test infrastructure whose only user istests/ApplicationTestCase.php, and it reached this package through a package merge rather than a decision. Moving it out of the shipped source, rather than making Monolog a production dependency, is #1357.PHP extensions.
ext-filter,ext-ctypeandext-mbstringare used but never declared.Symbols like
UnknownSymbol,templateandtemplateArraycome from docblock generics and belong incomposer-require-checker.json, not inrequire.One that reads worse than it is
guides-theme-rstcallsmb_str_pad()inRstTheme/Twig/RstExtension.php:154, which PHP provides only from 8.3 while the package declaresphp: ^8.1. It is not broken:symfony/polyfill-mbstringhas supplied the function since v1.28 and arrives throughsymfony/stringandtwig/twig. Verified by installing the package standalone in a PHP 8.1 container, wheremb_str_pad()returns the padded string. So this is the same undeclared-dependency case as the rest and not a version-floor decision; #1356 declares the polyfill.Why CI does not catch this
The repository already ships
composer-require-checker.jsonand pins the tool in.phive/phars.xml, and phpDocumentor has a reusable workflow for exactly this —phpDocumentor/.github/.github/workflows/dependency-analysis.yml, which runscomposer-require-checker check --config-file=$(pwd)/composer-require-checker.json. Wiring that in as-is would not help, for two reasons I measured:The workflow checks the root
composer.json, and this root has no productionautoloadat all, onlyautoload-devpointing at test directories. Running the checker there reportsThere were no symbols found, please check your configuration.— a green no-op. Catching the gaps above needs the check to run per package, overpackages/*/composer.json.The pinned
composer-require-checker3.5.1 aborts onsymfony/configv8 withSyntax error, unexpected '(', expecting T_VARIABLEinDefinition/Builder/NodeBuilder.php, and--ignore-parse-errorsdoes not help. It hitsguides-cli, the only package declaringsymfony/configdirectly, and the abort produces empty output that reads like a pass. The numbers in this issue were produced withcomposer-require-checker4.24.0.Expected output
Each published package declares the dependencies its own source code uses, and CI fails when that stops being true.
#1356 implements the straightforward table above and #1357 moves
TestExtension. The remaining decision items are left here for maintainers.Assisted by claude-code:claude-fable-5 — Session