Skip to content

Prefix bundled dependencies to prevent conflicts with site code - #1110

Draft
swissspidy wants to merge 6 commits into
mainfrom
claude/wp-cli-issue-5920-sf6uat
Draft

Prefix bundled dependencies to prevent conflicts with site code#1110
swissspidy wants to merge 6 commits into
mainfrom
claude/wp-cli-issue-5920-sf6uat

Conversation

@swissspidy

Copy link
Copy Markdown
Member

Summary

This change implements namespace prefixing for WP-CLI's bundled third-party dependencies to prevent conflicts when the Phar runs against WordPress sites that ship their own versions of those libraries. Since WP-CLI's autoloader is registered before WordPress boots, unprefixed classes from the Phar take precedence over the site's own dependencies, causing fatal errors when interface signatures differ (e.g., psr/log v1 vs v3).

Key Changes

  • New utils/scope-dependencies.php script: Orchestrates the prefixing workflow using php-scoper. The script:

    • Installs an isolated php-scoper toolchain (requires PHP 8.2+)
    • Prefixes the composer/composer dependency tree under the WP_CLI\Vendor namespace
    • Rewrites Composer's autoload maps from PSR-4/PSR-0 to classmap format to advertise prefixed class names
    • Validates that the regenerated autoloader contains no unprefixed dependencies
    • Runs between composer install and utils/make-phar.php
  • New utils/scoper/scoper.inc.php configuration: Defines php-scoper behavior:

    • Prefixes vendor directories: composer, justinrainbow, marc-mabe, psr, react, seld, symfony
    • Keeps the Composer\ namespace unprefixed so third-party Composer plugins continue working
    • Includes all PHP files (including test fixtures) to prevent unprefixed names from leaking into the classmap
    • Patches string literals that reference Composer\ classes to avoid broken dynamic class resolution
  • New utils/scoper/composer.json: Isolated toolchain configuration requiring php-scoper ^0.18, kept separate because php-scoper needs PHP 8.2+ while WP-CLI targets PHP 7.2.24

  • Updated utils/make-phar.php: Adapts to work with both prefixed and unprefixed Symfony Finder class depending on whether scope-dependencies.php has already run

  • New features/dependency-isolation.feature: Behat scenarios validating that:

    • Sites providing their own psr/log v3 don't fatal due to interface incompatibility
    • Sites providing their own Symfony Console don't conflict with the bundled version
    • Package management still works against the prefixed dependency tree
  • Updated .github/workflows/deployment.yml: Adds prefixing step to the Phar build pipeline

Notable Implementation Details

  • The script validates that no unprefixed dependencies leak into the regenerated autoloader by scanning generated PHP files for unescaped namespace references
  • Composer's autoload rules are rewritten to classmap format because prefixed files no longer satisfy their original PSR-4 declarations
  • The --classmap-authoritative flag ensures the ClassLoader consults only the classmap, preventing any leftover PSR-4 rules from resurrecting unprefixed names
  • Test fixtures are intentionally included in prefixing to prevent them from surviving the merge with original namespaces intact

https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi

claude added 2 commits August 10, 2026 14:29
WP-CLI registers its autoloader before WordPress boots, so for any class
shipped both by the Phar and by the site, the Phar's copy wins and is
imposed on the site. A site using monolog/monolog against psr/log v3 gets
the Phar's psr/log 1.1.4 instead and fatals on the incompatible
LoggerInterface signature.

Moving wp-cli/package-command to require-dev fixed this for Composer-based
installations, but the Phar is still built with dev dependencies, so it
continues to ship composer/composer and its tree unprefixed:
symfony/console v5.4.47, psr/log 1.1.4, react/promise, seld/*.

Prefix that tree with php-scoper, with two constraints:

* The `Composer\` namespace itself is left alone. Third-party Composer
  plugins are compiled against the real `Composer\Plugin\PluginInterface`,
  so prefixing it would break `wp package install` for any package
  shipping one. References from inside `Composer\` to the prefixed
  vendors are still rewritten, so Composer keeps using its own psr/log.
* Nothing reachable from WP-CLI's public API is touched: php-cli-tools
  (`Utils\make_progress_bar()`), Requests (`Utils\http_request()`, plus
  RequestsLibrary deliberately sharing the library with Core), and every
  wp-cli/* package.

php-scoper needs PHP 8.2 while WP-CLI still targets 7.2.24, so the
toolchain lives in utils/scoper with its own composer.json.

Two things this turned up that are easy to get wrong:

* php-scoper rewrites source files but not Composer's generated autoload
  maps. A scoped tree with a stale autoloader still advertises `Psr\Log\`
  and the conflict survives with nothing to show for it, so the
  autoloader is regenerated and then asserted on.
* Excluding a namespace does not stop php-scoper prefixing string
  literals naming classes inside it. Composer compares `$class` against
  'Composer\Package\CompletePackage', which the prefix silently breaks;
  a patcher restores those.

Refs wp-cli/wp-cli#5920

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi
`utils/scope-dependencies.php` prefixes symfony/finder along with the rest
of the Composer tree, but `utils/make-phar.php` builds the Phar with that
same Finder. Once prefixing has run, `Symfony\Component\Finder\Finder` no
longer exists and the build dies before writing anything.

Resolve the class name at runtime instead, so the build works whether or
not the dependencies have been prefixed yet.

Also drop `@require-mysql` from the isolation scenarios: they only need a
WordPress installation, which the Behat suite can provide on SQLite too.

Refs wp-cli/wp-cli#5920

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

鈿欙笍 Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f253cc49-5456-40e1-8fad-9ab4b9815dc2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 馃攳 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

鉂わ笍 Share

Comment @coderabbitai help to get the list of available commands.

claude added 4 commits August 10, 2026 15:59
The static analysis config lints everything under `utils`, so the new
build scripts need the same treatment the existing ones already get.

* Exempt them from the two WordPress-context sniffs `make-phar.php` is
  already exempt from; they are procedural stand-alone scripts that never
  run inside WordPress.
* Exclude `utils/scoper/scoper.inc.php` from PHPStan. It is an isolated
  toolchain with its own composer.json, so the classes it references are
  not installed in this project's vendor directory.
* Swap `str_contains()` for `strpos()`. The script refuses to run below
  PHP 8.2, but phpcs checks this repository against a 7.2 baseline and
  flags the newer function.

Refs wp-cli/wp-cli#5920

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi
The mu-plugins used a `\Stringable|string` union type, which is a parse
error on the PHP 7.2 to 7.4 jobs in the matrix, so the scenarios failed
with "syntax error, unexpected '|'" rather than exercising anything.

Narrowing an untyped parameter to `string` is the same contravariance
violation and parses on every version the suite runs, so the scenarios
still distinguish a prefixed tree from an unprefixed one: declaring the
class against an unprefixed `psr/log` v1 remains a fatal error, and
succeeds once the bundled copy is prefixed.

Also drop `--format=count` from `wp package list`, which does not offer
that format and made the step fail on argument parsing.

Refs wp-cli/wp-cli#5920

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi
Three separate code quality failures on the new build scripts:

* PHPStan runs at level 9, where every offset read off `json_decode()`
  output is `mixed`. Narrow the decoded `installed.json` explicitly and
  build the packages list in a local variable instead of writing back
  through nested offsets.
* Align the scoper config's array arrows on the longest key.
* `marc-mabe` is a vendor name, not a misspelling of "maybe"; mark those
  two lines with the `spellchecker:disable-line` annotation the repo's
  `.typos.toml` already recognises.

Re-ran the prefixing end to end against a scratch tree after the PHPStan
refactor: 30 packages rewritten, autoloader regenerated, verification
still passes.

Refs wp-cli/wp-cli#5920

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants