Skip to content

Split packages do not declare the dependencies their code uses #1355

Description

@CybotTM

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions