From 66464aa18ec4b3ca9905e7ff6c1f155437410fdf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 14:29:14 +0000 Subject: [PATCH 1/9] Prefix the Phar's Composer dependency tree 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 https://github.com/wp-cli/wp-cli/issues/5920 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi --- .github/workflows/deployment.yml | 6 + features/dependency-isolation.feature | 100 +++++++++ utils/scope-dependencies.php | 294 ++++++++++++++++++++++++++ utils/scoper/.gitignore | 1 + utils/scoper/composer.json | 13 ++ utils/scoper/scoper.inc.php | 133 ++++++++++++ 6 files changed, 547 insertions(+) create mode 100644 features/dependency-isolation.feature create mode 100644 utils/scope-dependencies.php create mode 100644 utils/scoper/.gitignore create mode 100644 utils/scoper/composer.json create mode 100644 utils/scoper/scoper.inc.php diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index f69e3a2b3..b5e00481f 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -60,6 +60,12 @@ jobs: name: manifest path: vendor/wp-cli/wp-cli/manifest.json + # Prefixes the composer/composer dependency tree so the Phar stops + # imposing its own psr/log, Symfony and React versions on the site it + # runs against. See https://github.com/wp-cli/wp-cli/issues/5920 + - name: Prefix bundled dependencies + run: php utils/scope-dependencies.php + - name: Build the Phar file run: php -dphar.readonly=0 utils/make-phar.php wp-cli.phar --version=$CLI_VERSION diff --git a/features/dependency-isolation.feature b/features/dependency-isolation.feature new file mode 100644 index 000000000..5012491d5 --- /dev/null +++ b/features/dependency-isolation.feature @@ -0,0 +1,100 @@ +Feature: Bundled dependencies do not conflict with the site's own + + # WP-CLI's autoloader is registered 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. Prefixing the `composer/composer` dependency tree stops + # the Phar from claiming those names at all. + # + # These scenarios run against the built Phar, which is the only artifact the + # prefixing applies to; a Composer-based installation resolves its own + # dependency versions and has no conflict to avoid. + # + # See https://github.com/wp-cli/wp-cli/issues/5920 + + @require-mysql + Scenario: A site providing its own psr/log is not broken by the bundled one + Given a WP installation + # Stands in for a site that ships psr/log v3 through its own vendor + # directory, as anything depending on monolog/monolog does. The typed + # signatures are incompatible with the psr/log v1 that composer/composer + # resolves to under the Phar's PHP 7.2 platform requirement, so whichever + # copy of the interface loads first decides whether this fatals. + And a wp-content/mu-plugins/site-logger.php file: + """ + ] [--quiet] + * + * @see https://github.com/wp-cli/wp-cli/issues/5920 + */ + +declare( strict_types=1 ); + +define( 'WP_CLI_BUNDLE_ROOT', rtrim( dirname( __DIR__ ), '/' ) ); + +/** + * Vendor directories handed to php-scoper. Keep in sync with the finders in + * `utils/scoper/scoper.inc.php`. + */ +const SCOPED_VENDOR_DIRS = [ + 'composer', + 'justinrainbow', + 'marc-mabe', + 'psr', + 'react', + 'seld', + 'symfony', +]; + +$options = getopt( '', [ 'vendor-dir::', 'quiet' ] ); +$be_quiet = isset( $options['quiet'] ); +$vendor_dir = isset( $options['vendor-dir'] ) && is_string( $options['vendor-dir'] ) + ? rtrim( $options['vendor-dir'], '/' ) + : WP_CLI_BUNDLE_ROOT . '/vendor'; + +$scoper_dir = WP_CLI_BUNDLE_ROOT . '/utils/scoper'; + +/** + * Write a progress line unless running quietly. + */ +function report( string $message ): void { + if ( ! $GLOBALS['be_quiet'] ) { + fwrite( STDOUT, $message . PHP_EOL ); + } +} + +/** + * Run a command, returning its exit code. + * + * @param array $command + */ +function run( array $command, ?string $cwd = null ): int { + $cwd_prefix = null !== $cwd ? sprintf( 'cd %s && ', escapeshellarg( $cwd ) ) : ''; + $escaped = implode( ' ', array_map( 'escapeshellarg', $command ) ); + + passthru( $cwd_prefix . $escaped, $exit_code ); + + return $exit_code; +} + +/** + * Fail with a message. + */ +function fail( string $message ): void { + fwrite( STDERR, 'Error: ' . $message . PHP_EOL ); + exit( 1 ); +} + +if ( ! is_dir( $vendor_dir ) ) { + fail( sprintf( "Vendor directory '%s' does not exist. Run `composer install` first.", $vendor_dir ) ); +} + +// php-scoper needs PHP 8.2+, which is why it lives in its own composer.json +// rather than in the bundle's (that one still has to resolve against PHP 7.2.24). +if ( PHP_VERSION_ID < 80200 ) { + fail( sprintf( 'php-scoper requires PHP 8.2 or newer, but this is PHP %s.', PHP_VERSION ) ); +} + +// --- 1. Make sure the isolated toolchain is installed. ---------------------- + +if ( ! file_exists( $scoper_dir . '/vendor/bin/php-scoper' ) ) { + report( 'Installing the php-scoper toolchain...' ); + if ( 0 !== run( [ 'composer', 'install', '--no-interaction', '--prefer-dist', '--quiet' ], $scoper_dir ) ) { + fail( 'Failed to install the php-scoper toolchain.' ); + } +} + +// --- 2. Prefix the dependency tree. ----------------------------------------- + +$output_dir = $vendor_dir . '/../build/scoped-vendor'; + +if ( is_dir( $output_dir ) ) { + run( [ 'rm', '-rf', $output_dir ] ); +} + +report( 'Prefixing third-party dependencies...' ); + +putenv( 'WP_CLI_SCOPER_VENDOR_DIR=' . $vendor_dir ); + +$scoper_exit = run( + [ + $scoper_dir . '/vendor/bin/php-scoper', + 'add-prefix', + '--config=' . $scoper_dir . '/scoper.inc.php', + '--output-dir=' . $output_dir, + '--force', + '--no-interaction', + $be_quiet ? '--quiet' : '--no-ansi', + ] +); + +if ( 0 !== $scoper_exit ) { + fail( 'php-scoper failed.' ); +} + +// --- 3. Swap the prefixed tree into vendor/. -------------------------------- + +foreach ( SCOPED_VENDOR_DIRS as $dir ) { + $scoped = $output_dir . '/' . $dir; + $target = $vendor_dir . '/' . $dir; + + if ( ! is_dir( $scoped ) ) { + continue; + } + + report( sprintf( ' Replacing vendor/%s', $dir ) ); + + // Composer's autoloader machinery lives alongside the composer/* packages + // in vendor/composer and is regenerated below, so only the package + // subdirectories are replaced wholesale. + if ( 0 !== run( [ 'cp', '-a', $scoped . '/.', $target . '/' ] ) ) { + fail( sprintf( "Failed to copy the prefixed '%s' into place.", $dir ) ); + } +} + +// --- 4. Teach Composer about the new class names. --------------------------- + +/* + * The prefixed files no longer satisfy their packages' PSR-4 rules: the classes + * in vendor/psr/log now declare WP_CLI\Vendor\Psr\Log\*, while psr/log's + * composer.json still maps Psr\Log\ to that directory. Left alone, a dump would + * both re-advertise the unprefixed prefix and skip the prefixed classes as + * "not compliant with PSR-4". + * + * Rewriting the affected packages' autoload rules to a classmap sidesteps both + * problems: Composer scans the directories and records whatever class names the + * files actually declare. + */ +$installed_json = $vendor_dir . '/composer/installed.json'; + +if ( ! file_exists( $installed_json ) ) { + fail( sprintf( "Could not find '%s'.", $installed_json ) ); +} + +$installed = json_decode( (string) file_get_contents( $installed_json ), true ); + +if ( ! is_array( $installed ) || ! isset( $installed['packages'] ) ) { + fail( sprintf( "Could not decode '%s'.", $installed_json ) ); +} + +$patched = 0; + +foreach ( $installed['packages'] as $index => $package ) { + $name = $package['name'] ?? ''; + + if ( '' === $name ) { + continue; + } + + $vendor_name = explode( '/', $name )[0]; + + if ( ! in_array( $vendor_name, SCOPED_VENDOR_DIRS, true ) ) { + continue; + } + + if ( ! isset( $package['autoload'] ) || ! is_array( $package['autoload'] ) ) { + continue; + } + + $roots = []; + + foreach ( [ 'psr-4', 'psr-0' ] as $standard ) { + foreach ( (array) ( $package['autoload'][ $standard ] ?? [] ) as $paths ) { + foreach ( (array) $paths as $path ) { + $roots[] = '' === $path ? '.' : $path; + } + } + } + + foreach ( (array) ( $package['autoload']['classmap'] ?? [] ) as $path ) { + $roots[] = $path; + } + + if ( ! $roots ) { + continue; + } + + $installed['packages'][ $index ]['autoload'] = [ 'classmap' => array_values( array_unique( $roots ) ) ]; + ++$patched; +} + +report( sprintf( 'Rewrote autoload rules for %d prefixed package(s).', $patched ) ); + +$encoded = json_encode( $installed, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); + +if ( false === $encoded || false === file_put_contents( $installed_json, $encoded ) ) { + fail( sprintf( "Failed to write '%s'.", $installed_json ) ); +} + +// --- 5. Regenerate the autoloader. ------------------------------------------ + +report( 'Regenerating the Composer autoloader...' ); + +/* + * --classmap-authoritative makes the ClassLoader consult only the classmap, so + * no leftover PSR-4 rule can resurrect an unprefixed name. Everything the Phar + * runs is inside the Phar, so there is nothing to discover at runtime. + */ +// Derived from the vendor directory rather than assumed, so the script can be +// pointed at a scratch tree for testing. +$composer_root = dirname( $vendor_dir ); + +if ( 0 !== run( [ 'composer', 'dump-autoload', '--classmap-authoritative', '--no-interaction' ], $composer_root ) ) { + fail( 'Failed to regenerate the Composer autoloader.' ); +} + +run( [ 'rm', '-rf', dirname( $output_dir ) ] ); + +// --- 6. Verify the autoloader no longer claims the unprefixed names. -------- + +/* + * The failure mode this guards against is silent: php-scoper rewrites source + * files but not Composer's generated maps, so a tree that looks scoped can + * still resolve `Psr\Log\LoggerInterface` to the bundled copy and reintroduce + * the conflict with nothing in the build output to show for it. + */ +$autoload_files = array_filter( + [ + $vendor_dir . '/composer/autoload_classmap.php', + $vendor_dir . '/composer/autoload_psr4.php', + $vendor_dir . '/composer/autoload_static.php', + ], + 'file_exists' +); + +$must_not_appear = [ + 'Psr\\Log\\', + 'Symfony\\Component\\Console\\', + 'React\\Promise\\', + 'Seld\\JsonLint\\', +]; + +$leaked = []; + +foreach ( $autoload_files as $file ) { + $contents = (string) file_get_contents( $file ); + + foreach ( $must_not_appear as $symbol ) { + // Written as it appears in the generated PHP source, where each + // namespace separator is escaped. + $needle = str_replace( '\\', '\\\\', $symbol ); + $prefixed = 'WP_CLI\\\\Vendor\\\\' . $needle; + $occurring = substr_count( $contents, $needle ) - substr_count( $contents, $prefixed ); + + if ( $occurring > 0 ) { + $leaked[] = sprintf( ' %s advertises %s (%d time(s))', basename( $file ), $symbol, $occurring ); + } + } +} + +if ( $leaked ) { + fail( + "The regenerated autoloader still advertises unprefixed dependencies:\n" + . implode( "\n", $leaked ) + . "\nThe Phar would keep imposing these on the site. See https://github.com/wp-cli/wp-cli/issues/5920" + ); +} + +$classmap = $vendor_dir . '/composer/autoload_classmap.php'; + +if ( file_exists( $classmap ) && ! str_contains( (string) file_get_contents( $classmap ), 'WP_CLI\\\\Vendor\\\\' ) ) { + fail( 'The regenerated classmap contains no prefixed classes at all; the prefixing step did not take effect.' ); +} + +report( 'Verified: the autoloader advertises only prefixed dependencies.' ); +report( 'Done.' ); diff --git a/utils/scoper/.gitignore b/utils/scoper/.gitignore new file mode 100644 index 000000000..57872d0f1 --- /dev/null +++ b/utils/scoper/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/utils/scoper/composer.json b/utils/scoper/composer.json new file mode 100644 index 000000000..4db272d65 --- /dev/null +++ b/utils/scoper/composer.json @@ -0,0 +1,13 @@ +{ + "name": "wp-cli/phar-scoper-toolchain", + "description": "Isolated toolchain used to prefix the Phar's third-party dependencies. Kept out of the bundle's own composer.json because php-scoper requires PHP 8.2+, while WP-CLI still targets PHP 7.2.24.", + "license": "MIT", + "type": "project", + "require": { + "humbug/php-scoper": "^0.18" + }, + "config": { + "sort-packages": true, + "lock": false + } +} diff --git a/utils/scoper/scoper.inc.php b/utils/scoper/scoper.inc.php new file mode 100644 index 000000000..d584fcb22 --- /dev/null +++ b/utils/scoper/scoper.inc.php @@ -0,0 +1,133 @@ + 'WP_CLI\\Vendor', + 'finders' => [ + /* + * Deliberately without exclusions. The prefixed output is merged back + * over `vendor/` rather than replacing it, because php-scoper only + * emits the PHP files it processed and the directories also hold + * assets the Phar needs (certificate bundles, templates, stubs). + * Any PHP file skipped here would therefore survive the merge with its + * original namespace intact and be picked up by the regenerated + * classmap -- which is exactly the unprefixed name the Phar is not + * supposed to advertise any more. Test fixtures are the usual culprit: + * `Psr\Log\Test\TestLogger` implements the very interface at issue. + */ + $finder_class::create() + ->files() + ->ignoreVCS( true ) + ->name( '*.php' ) + ->in( $scoped_paths ), + ], + + /* + * Left unprefixed so third-party Composer plugins keep implementing the + * real interfaces. References from these files to the prefixed vendors are + * still rewritten by php-scoper. + */ + 'exclude-namespaces' => [ + 'Composer', + ], + + 'exclude-classes' => [], + 'exclude-functions' => [], + 'exclude-constants' => [], + + 'patchers' => [ + /* + * Excluding a namespace stops php-scoper prefixing its declarations, + * but not string literals that name classes inside it. Composer passes + * plenty of class names around as strings -- `ArrayLoader::load()` + * defaults `$class` to 'Composer\Package\CompletePackage' and compares + * against it -- and prefixing those strings points them at classes + * that do not exist, because `Composer\` itself was left alone. + * + * Left unpatched this is quiet rather than fatal: Composer emits a + * spurious "The $class arg is deprecated" notice and carries on, while + * the same mismatch in a `new $class` path would be a hard failure. + */ + static function ( string $file_path, string $prefix, string $contents ): string { + return str_replace( + [ + $prefix . '\\Composer\\', + $prefix . '\\\\Composer\\\\', + ], + [ + 'Composer\\', + 'Composer\\\\', + ], + $contents + ); + }, + ], +]; From b4576506dc6cd69cd61ccdb8e5e1c99eab642b8f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 15:08:41 +0000 Subject: [PATCH 2/9] Resolve Symfony Finder in make-phar.php at runtime `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 https://github.com/wp-cli/wp-cli/issues/5920 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi --- features/dependency-isolation.feature | 2 -- utils/make-phar.php | 21 +++++++++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/features/dependency-isolation.feature b/features/dependency-isolation.feature index 5012491d5..2b1efc1a8 100644 --- a/features/dependency-isolation.feature +++ b/features/dependency-isolation.feature @@ -11,7 +11,6 @@ Feature: Bundled dependencies do not conflict with the site's own # # See https://github.com/wp-cli/wp-cli/issues/5920 - @require-mysql Scenario: A site providing its own psr/log is not broken by the bundled one Given a WP installation # Stands in for a site that ships psr/log v3 through its own vendor @@ -55,7 +54,6 @@ Feature: Bundled dependencies do not conflict with the site's own """ And the return code should be 0 - @require-mysql Scenario: A site providing its own Symfony Console is not broken by the bundled one Given a WP installation And a wp-content/mu-plugins/site-console.php file: diff --git a/utils/make-phar.php b/utils/make-phar.php index 8aee6a586..dec33d651 100644 --- a/utils/make-phar.php +++ b/utils/make-phar.php @@ -18,7 +18,6 @@ require WP_CLI_VENDOR_DIR . '/autoload.php'; require WP_CLI_ROOT . '/php/utils.php'; -use Symfony\Component\Finder\Finder; use WP_CLI\Utils; use WP_CLI\Configurator; @@ -190,7 +189,21 @@ function get_composer_versions( $current_version ) { $phar->startBuffering(); // PHP files -$finder = new Finder(); +/* + * `utils/scope-dependencies.php` prefixes symfony/finder along with the rest + * of the Composer tree, so the class this build script itself relies on moves + * depending on whether prefixing has already run. + */ +$finder_class = class_exists( 'Symfony\\Component\\Finder\\Finder' ) + ? 'Symfony\\Component\\Finder\\Finder' + : 'WP_CLI\\Vendor\\Symfony\\Component\\Finder\\Finder'; + +if ( ! class_exists( $finder_class ) ) { + fwrite( STDERR, 'Missing Symfony Finder; run `composer install` first.' . PHP_EOL ); + exit( 1 ); +} + +$finder = new $finder_class(); $finder ->files() ->ignoreVCS( true ) @@ -265,7 +278,7 @@ function get_composer_versions( $current_version ) { } // other files -$finder = new Finder(); +$finder = new $finder_class(); $finder ->files() ->ignoreVCS( true ) @@ -280,7 +293,7 @@ function get_composer_versions( $current_version ) { if ( 'cli' !== BUILD ) { // Include base project files, because the autoloader will load them if ( WP_CLI_BASE_PATH !== WP_CLI_BUNDLE_ROOT && is_dir( WP_CLI_BASE_PATH . '/src' ) ) { - $finder = new Finder(); + $finder = new $finder_class(); $finder ->files() ->ignoreVCS( true ) From 8d503851d7063efa9ce506541699a66e1b601290 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 15:59:30 +0000 Subject: [PATCH 3/9] Keep the scoping build scripts within the code quality checks 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 https://github.com/wp-cli/wp-cli/issues/5920 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi --- phpcs.xml.dist | 4 ++++ phpstan.neon.dist | 5 +++++ utils/scope-dependencies.php | 5 ++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index eb99f8845..d22045599 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -52,10 +52,14 @@ */utils/get-package-require-from-composer\.php$ */utils/make-phar\.php$ + */utils/scope-dependencies\.php$ + */utils/scoper/scoper\.inc\.php$ */utils/get-package-require-from-composer\.php$ */utils/make-phar\.php$ + */utils/scope-dependencies\.php$ + */utils/scoper/scoper\.inc\.php$ diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a1f53a4d5..10e149d75 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -3,6 +3,11 @@ parameters: paths: - php - utils + excludePaths: + analyse: + # Isolated toolchain with its own composer.json; its dependencies are not + # installed in this project's vendor directory. + - utils/scoper/scoper.inc.php scanDirectories: - vendor/wp-cli/wp-cli scanFiles: diff --git a/utils/scope-dependencies.php b/utils/scope-dependencies.php index de3cb8fc4..f2b7fb904 100644 --- a/utils/scope-dependencies.php +++ b/utils/scope-dependencies.php @@ -286,7 +286,10 @@ function fail( string $message ): void { $classmap = $vendor_dir . '/composer/autoload_classmap.php'; -if ( file_exists( $classmap ) && ! str_contains( (string) file_get_contents( $classmap ), 'WP_CLI\\\\Vendor\\\\' ) ) { +// strpos() rather than str_contains() so the file still parses under the 7.2 +// baseline phpcs checks this repository against, even though the script itself +// refuses to run on anything below PHP 8.2. +if ( file_exists( $classmap ) && false === strpos( (string) file_get_contents( $classmap ), 'WP_CLI\\\\Vendor\\\\' ) ) { fail( 'The regenerated classmap contains no prefixed classes at all; the prefixing step did not take effect.' ); } From d29611844265d7b913f7d8d0be915048a111c620 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 16:11:37 +0000 Subject: [PATCH 4/9] Fix the dependency isolation scenarios failing on PHP 7.x 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 https://github.com/wp-cli/wp-cli/issues/5920 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi --- features/dependency-isolation.feature | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/features/dependency-isolation.feature b/features/dependency-isolation.feature index 2b1efc1a8..2129ce9dd 100644 --- a/features/dependency-isolation.feature +++ b/features/dependency-isolation.feature @@ -31,14 +31,14 @@ Feature: Bundled dependencies do not conflict with the site's own eval( 'namespace Psr\Log; interface LoggerInterface { - public function emergency( \Stringable|string $message, array $context = [] ): void; + public function emergency( string $message, array $context = [] ): void; }' ); } ); final class Site_Logger implements \Psr\Log\LoggerInterface { - public function emergency( \Stringable|string $message, array $context = [] ): void { + public function emergency( string $message, array $context = [] ): void { } } """ @@ -69,14 +69,14 @@ Feature: Bundled dependencies do not conflict with the site's own eval( 'namespace Symfony\Component\Console\Output; interface OutputInterface { - public function writeln( \Stringable|string $messages, int $options = 0 ): void; + public function writeln( string $messages, int $options = 0 ): void; }' ); } ); final class Site_Output implements \Symfony\Component\Console\Output\OutputInterface { - public function writeln( \Stringable|string $messages, int $options = 0 ): void { + public function writeln( string $messages, int $options = 0 ): void { } } """ @@ -93,6 +93,6 @@ Feature: Bundled dependencies do not conflict with the site's own # strings, which prefixing of static `use` statements does not cover. Given an empty directory - When I run `wp package list --format=count` + When I run `wp package list` Then STDERR should be empty And the return code should be 0 From 00a9caf70d75a708e2409b7ebdd8a10f4b0e62ac Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 16:21:18 +0000 Subject: [PATCH 5/9] Satisfy PHPCS, PHPStan and the spell checker 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 https://github.com/wp-cli/wp-cli/issues/5920 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi --- utils/scope-dependencies.php | 53 +++++++++++++++++++++++++----------- utils/scoper/scoper.inc.php | 14 +++++----- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/utils/scope-dependencies.php b/utils/scope-dependencies.php index f2b7fb904..d860cca47 100644 --- a/utils/scope-dependencies.php +++ b/utils/scope-dependencies.php @@ -28,7 +28,7 @@ const SCOPED_VENDOR_DIRS = [ 'composer', 'justinrainbow', - 'marc-mabe', + 'marc-mabe', // spellchecker:disable-line 'psr', 'react', 'seld', @@ -160,22 +160,25 @@ function fail( string $message ): void { fail( sprintf( "Could not find '%s'.", $installed_json ) ); } -$installed = json_decode( (string) file_get_contents( $installed_json ), true ); +$decoded = json_decode( (string) file_get_contents( $installed_json ), true ); -if ( ! is_array( $installed ) || ! isset( $installed['packages'] ) ) { +if ( ! is_array( $decoded ) || ! isset( $decoded['packages'] ) || ! is_array( $decoded['packages'] ) ) { fail( sprintf( "Could not decode '%s'.", $installed_json ) ); } -$patched = 0; - -foreach ( $installed['packages'] as $index => $package ) { - $name = $package['name'] ?? ''; +/** + * @var array $decoded + * @var array $packages + */ +$packages = $decoded['packages']; +$patched = 0; - if ( '' === $name ) { +foreach ( $packages as $index => $package ) { + if ( ! is_array( $package ) || ! isset( $package['name'] ) || ! is_string( $package['name'] ) ) { continue; } - $vendor_name = explode( '/', $name )[0]; + $vendor_name = explode( '/', $package['name'] )[0]; if ( ! in_array( $vendor_name, SCOPED_VENDOR_DIRS, true ) ) { continue; @@ -185,31 +188,49 @@ function fail( string $message ): void { continue; } - $roots = []; + $autoload = $package['autoload']; + $roots = []; foreach ( [ 'psr-4', 'psr-0' ] as $standard ) { - foreach ( (array) ( $package['autoload'][ $standard ] ?? [] ) as $paths ) { + $rules = $autoload[ $standard ] ?? []; + + if ( ! is_array( $rules ) ) { + continue; + } + + foreach ( $rules as $paths ) { foreach ( (array) $paths as $path ) { - $roots[] = '' === $path ? '.' : $path; + if ( is_string( $path ) ) { + $roots[] = '' === $path ? '.' : $path; + } } } } - foreach ( (array) ( $package['autoload']['classmap'] ?? [] ) as $path ) { - $roots[] = $path; + $classmap_rules = $autoload['classmap'] ?? []; + + if ( is_array( $classmap_rules ) ) { + foreach ( $classmap_rules as $path ) { + if ( is_string( $path ) ) { + $roots[] = $path; + } + } } if ( ! $roots ) { continue; } - $installed['packages'][ $index ]['autoload'] = [ 'classmap' => array_values( array_unique( $roots ) ) ]; + $package['autoload'] = [ 'classmap' => array_values( array_unique( $roots ) ) ]; + $packages[ $index ] = $package; ++$patched; } +$decoded['packages'] = $packages; + report( sprintf( 'Rewrote autoload rules for %d prefixed package(s).', $patched ) ); -$encoded = json_encode( $installed, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); +$encoded = json_encode( $decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); if ( false === $encoded || false === file_put_contents( $installed_json, $encoded ) ) { fail( sprintf( "Failed to write '%s'.", $installed_json ) ); diff --git a/utils/scoper/scoper.inc.php b/utils/scoper/scoper.inc.php index d584fcb22..fce325037 100644 --- a/utils/scoper/scoper.inc.php +++ b/utils/scoper/scoper.inc.php @@ -59,7 +59,7 @@ static function ( $relative ) use ( $vendor_dir ) { [ 'composer', 'justinrainbow', - 'marc-mabe', + 'marc-mabe', // spellchecker:disable-line 'psr', 'react', 'seld', @@ -70,8 +70,8 @@ static function ( $relative ) use ( $vendor_dir ) { ); return [ - 'prefix' => 'WP_CLI\\Vendor', - 'finders' => [ + 'prefix' => 'WP_CLI\\Vendor', + 'finders' => [ /* * Deliberately without exclusions. The prefixed output is merged back * over `vendor/` rather than replacing it, because php-scoper only @@ -99,11 +99,11 @@ static function ( $relative ) use ( $vendor_dir ) { 'Composer', ], - 'exclude-classes' => [], - 'exclude-functions' => [], - 'exclude-constants' => [], + 'exclude-classes' => [], + 'exclude-functions' => [], + 'exclude-constants' => [], - 'patchers' => [ + 'patchers' => [ /* * Excluding a namespace stops php-scoper prefixing its declarations, * but not string literals that name classes inside it. Composer passes From 6bd71ebe223215a1ed8d95e26825eacabe0bdd88 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 16:25:40 +0000 Subject: [PATCH 6/9] Align the assignments PHPCS flagged in scope-dependencies.php Refs https://github.com/wp-cli/wp-cli/issues/5920 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RHtjyXkZh8X16sBgmidSQi --- utils/scope-dependencies.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/scope-dependencies.php b/utils/scope-dependencies.php index d860cca47..dac204e37 100644 --- a/utils/scope-dependencies.php +++ b/utils/scope-dependencies.php @@ -221,8 +221,8 @@ function fail( string $message ): void { continue; } - $package['autoload'] = [ 'classmap' => array_values( array_unique( $roots ) ) ]; - $packages[ $index ] = $package; + $package['autoload'] = [ 'classmap' => array_values( array_unique( $roots ) ) ]; + $packages[ $index ] = $package; ++$patched; } From 2519e02c259c0a89822a899567a82fbe04b85d34 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 16:26:49 +0000 Subject: [PATCH 7/9] Prefix the Phar's third-party dependencies into third_party/ Replaces the earlier approach on this branch, which rewrote vendor/ in place from a separate build step. WP-CLI registers its autoloader before WordPress boots, so for any class shipped both by the Phar and by the site's own vendor/ the Phar's copy wins. A site depending on psr/log v3 therefore loads the Phar's psr/log v1 and fatals on the incompatible LoggerInterface signature. Composer's dependency tree (composer/composer and everything it pulls in, resolved from installed.json) is now rewritten under the WP_CLI\Vendor namespace into third_party/, which gets its own classmap autoloader that php/boot-phar.php registers. vendor/ is left untouched; utils/make-phar.php bundles third_party/ instead of the original packages and drops them from the main autoload maps, so the Phar stops advertising the unprefixed names. A Composer-based installation of the bundle is unaffected. The prefixing runs as Composer's post-install-cmd/post-update-cmd hook (`composer prefix-dependencies`) rather than as a separate build step. The php-scoper toolchain needs PHP 8.2+ while the bundle targets 7.2, so it lives in utils/scoper/composer.json with a committed lock file; on an older PHP the hook skips with a notice and a Phar built from that checkout bundles the unprefixed tree. CI that must test on older PHP installs the toolchain with a recent interpreter first and hands it over through WP_CLI_SCOPER_PHP, as the deployment test matrix now does. The build job refuses to build a Phar without third_party/. `Composer\` itself stays unprefixed so that third-party Composer plugins run by `wp package install` keep implementing the real interfaces, and the Symfony polyfills are copied verbatim since they only work under their global names. The hook verifies that the generated classmap advertises nothing else under an original name, and that no bundled WP-CLI package depends on a prefixed namespace. See https://github.com/wp-cli/wp-cli/issues/5920 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BrV8jGVsfiypo6buRmZfug --- .github/workflows/deployment.yml | 35 +- .gitignore | 1 + composer.json | 8 +- features/dependency-isolation.feature | 52 +- features/make-phar.feature | 14 + php/boot-phar.php | 8 + phpcs.xml.dist | 11 +- phpstan.neon.dist | 6 +- utils/make-phar.php | 229 ++-- utils/prefix-dependencies.php | 429 ++++++ utils/scope-dependencies.php | 318 ----- utils/scoper/composer.json | 10 +- utils/scoper/composer.lock | 1781 +++++++++++++++++++++++++ utils/scoper/prefixed-packages.php | 158 +++ utils/scoper/scoper.inc.php | 181 ++- 15 files changed, 2696 insertions(+), 545 deletions(-) create mode 100644 utils/prefix-dependencies.php delete mode 100644 utils/scope-dependencies.php create mode 100644 utils/scoper/composer.lock create mode 100644 utils/scoper/prefixed-packages.php diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index f97c81689..814200757 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -60,11 +60,15 @@ jobs: name: manifest path: vendor/wp-cli/wp-cli/manifest.json - # Prefixes the composer/composer dependency tree so the Phar stops - # imposing its own psr/log, Symfony and React versions on the site it - # runs against. See https://github.com/wp-cli/wp-cli/issues/5920 - - name: Prefix bundled dependencies - run: php utils/scope-dependencies.php + # third_party/ is generated by the post-install-cmd hook (utils/prefix-dependencies.php). + # Without it the Phar would bundle unprefixed dependencies and impose them on the site, + # see https://github.com/wp-cli/wp-cli/issues/5920 + - name: Verify the third-party dependencies were prefixed + run: | + if [ ! -f third_party/vendor/autoload.php ]; then + echo '::error::third_party/ is missing: `composer install` did not prefix the dependencies.' + exit 1 + fi - name: Build the Phar file run: php -dphar.readonly=0 utils/make-phar.php wp-cli.phar --version=$CLI_VERSION @@ -124,6 +128,27 @@ jobs: sudo apt-get update sudo apt-get install ghostscript -y + # The `composer install` below prefixes the bundled dependencies through + # php-scoper, which needs PHP 8.2+, while this matrix goes down to 7.2. + # Install the toolchain with a recent PHP first and hand that interpreter + # to utils/prefix-dependencies.php, then switch to the matrix version. + - name: Set up PHP for the php-scoper toolchain + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + with: + php-version: 'latest' + coverage: none + tools: composer + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install the php-scoper toolchain + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # v4 + with: + working-directory: utils/scoper + + - name: Remember the PHP interpreter for php-scoper + run: echo "WP_CLI_SCOPER_PHP=$(php -r 'echo PHP_BINARY;')" >> "$GITHUB_ENV" + - name: Set up PHP environment uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 with: diff --git a/.gitignore b/.gitignore index a0d6a5710..a827b2613 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ PHAR_BUILD_VERSION /cache /packages /vendor +/third_party /*.phar .*.swp *.log diff --git a/composer.json b/composer.json index 2f065f236..366c927b5 100644 --- a/composer.json +++ b/composer.json @@ -74,9 +74,12 @@ "minimum-stability": "dev", "prefer-stable": true, "scripts": { + "post-install-cmd": "@prefix-dependencies", + "post-update-cmd": "@prefix-dependencies", + "prefix-dependencies": "php utils/prefix-dependencies.php", "behat": "run-behat-tests", "behat-rerun": "rerun-behat-tests", - "lint": "run-linter-tests", + "lint": "run-linter-tests --exclude third_party --exclude utils/scoper/vendor", "lint-gherkin": "run-gherkin-lint-tests", "phpcs": "run-phpcs-tests", "phpstan": "run-phpstan-tests", @@ -93,6 +96,9 @@ "@behat" ] }, + "scripts-descriptions": { + "prefix-dependencies": "Prefix the namespaces of the Phar's third-party dependencies into third_party/ (needs PHP 8.2+; runs on install and update)." + }, "support": { "issues": "https://github.com/wp-cli/wp-cli-bundle/issues", "source": "https://github.com/wp-cli/wp-cli-bundle", diff --git a/features/dependency-isolation.feature b/features/dependency-isolation.feature index 2129ce9dd..828808265 100644 --- a/features/dependency-isolation.feature +++ b/features/dependency-isolation.feature @@ -2,17 +2,21 @@ Feature: Bundled dependencies do not conflict with the site's own # WP-CLI's autoloader is registered 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. Prefixing the `composer/composer` dependency tree stops - # the Phar from claiming those names at all. + # imposed on the site. The Phar therefore ships Composer's dependency tree + # under the `WP_CLI\Vendor` prefix instead, see utils/prefix-dependencies.php. # - # These scenarios run against the built Phar, which is the only artifact the - # prefixing applies to; a Composer-based installation resolves its own - # dependency versions and has no conflict to avoid. + # Prefixing happens on `composer install` and needs PHP 8.2+, which is why + # these scenarios build their own Phar from the checkout and only run where + # that tree exists. A Composer-based installation resolves its own dependency + # versions and has no conflict to avoid. # # See https://github.com/wp-cli/wp-cli/issues/5920 + @require-php-8.2 Scenario: A site providing its own psr/log is not broken by the bundled one - Given a WP installation + Given an empty directory + And a new Phar with the same version + And a WP installation # Stands in for a site that ships psr/log v3 through its own vendor # directory, as anything depending on monolog/monolog does. The typed # signatures are incompatible with the psr/log v1 that composer/composer @@ -43,19 +47,15 @@ Feature: Bundled dependencies do not conflict with the site's own } """ - When I try `wp option get siteurl` - Then STDERR should not contain: - """ - must be compatible with - """ - And STDERR should not contain: - """ - critical error - """ - And the return code should be 0 + When I run `php {PHAR_PATH} option get siteurl` + Then STDOUT should not be empty + And STDERR should be empty + @require-php-8.2 Scenario: A site providing its own Symfony Console is not broken by the bundled one - Given a WP installation + Given an empty directory + And a new Phar with the same version + And a WP installation And a wp-content/mu-plugins/site-console.php file: """ . + + */third_party/* + @@ -46,19 +49,21 @@ ############################################################################# --> - */utils/get-package-require-from-composer\.php$ */utils/make-phar\.php$ - */utils/scope-dependencies\.php$ + */utils/prefix-dependencies\.php$ + */utils/scoper/prefixed-packages\.php$ */utils/scoper/scoper\.inc\.php$ */utils/get-package-require-from-composer\.php$ */utils/make-phar\.php$ - */utils/scope-dependencies\.php$ + */utils/prefix-dependencies\.php$ + */utils/scoper/prefixed-packages\.php$ */utils/scoper/scoper\.inc\.php$ diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 10e149d75..0f888c81e 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -5,9 +5,11 @@ parameters: - utils excludePaths: analyse: - # Isolated toolchain with its own composer.json; its dependencies are not - # installed in this project's vendor directory. + # php-scoper configuration; its classes only exist in the isolated toolchain. - utils/scoper/scoper.inc.php + analyseAndScan: + # The isolated php-scoper toolchain. + - utils/scoper/vendor scanDirectories: - vendor/wp-cli/wp-cli scanFiles: diff --git a/utils/make-phar.php b/utils/make-phar.php index dec33d651..adb3529bc 100644 --- a/utils/make-phar.php +++ b/utils/make-phar.php @@ -18,6 +18,7 @@ require WP_CLI_VENDOR_DIR . '/autoload.php'; require WP_CLI_ROOT . '/php/utils.php'; +use Symfony\Component\Finder\Finder; use WP_CLI\Utils; use WP_CLI\Configurator; @@ -37,6 +38,19 @@ define( 'BUILD', isset( $runtime_config['build'] ) ? $runtime_config['build'] : '' ); +/* + * `composer install` rewrites Composer's dependency tree under the + * `WP_CLI\Vendor` namespace into third_party/ (see utils/prefix-dependencies.php). + * When that tree exists, it replaces the original packages in the Phar. The + * minimal "cli" build has no use for it. + */ +define( 'WP_CLI_THIRD_PARTY_DIR', WP_CLI_BUNDLE_ROOT . '/third_party' ); +define( 'BUNDLE_PREFIXED_DEPENDENCIES', 'cli' !== BUILD && file_exists( WP_CLI_THIRD_PARTY_DIR . '/vendor/autoload.php' ) ); + +if ( 'cli' !== BUILD && ! BUNDLE_PREFIXED_DEPENDENCIES && ! BE_QUIET ) { + echo 'Warning: third_party/ is missing, bundling unprefixed third-party dependencies. Run `composer prefix-dependencies` (PHP 8.2+) first.' . PHP_EOL; +} + $current_version = trim( (string) file_get_contents( WP_CLI_ROOT . '/VERSION' ) ); if ( isset( $runtime_config['version'] ) ) { @@ -53,6 +67,76 @@ $current_version = $new_version; } +/** + * Patterns matching the lines to drop from vendor/composer/autoload_*.php. + * + * @return string[] + */ +function get_autoload_strip_patterns() { + static $strip_res = null; + + if ( null !== $strip_res ) { + return $strip_res; + } + + if ( 'cli' === BUILD ) { + $strips = [ + '\/(?:behat|composer|gherkin)\/src\/', + '\/behat\/', + '\/phpunit\/', + '\/phpstan\/', + '\/phpspec\/', + '\/sebastian\/', + '\/php-parallel-lint\/', + '\/nb\/oxymel\/', + '-command\/src\/', + '\/wp-cli\/[^\n]+?-command\/', + '\/symfony\/(?:config|console|debug|dependency-injection|event-dispatcher|filesystem|translation|yaml)', + '\/(?:dealerdirect|myclabs|squizlabs|wimg)\/', + '\/yoast\/', + ]; + } else { + $strips = [ + '\/(?:behat|gherkin)\/src\/', + '\/behat\/', + '\/phpunit\/', + '\/phpstan\/', + '\/phpspec\/', + '\/sebastian\/', + '\/php-parallel-lint\/', + '\/symfony\/(?:config|debug|dependency-injection|event-dispatcher|translation|yaml)', + '\/composer\/spdx-licenses\/', + '\/Composer\/(?:Command\/|Compiler\.php|Console\/|Downloader\/Pear|Installer\/Pear|Question\/|Repository\/Pear|SelfUpdate\/)', + '\/(?:dealerdirect|myclabs|squizlabs|wimg)\/', + '\/yoast\/', + ]; + + if ( BUNDLE_PREFIXED_DEPENDENCIES ) { + // The prefixed tree brings its own autoloader. Dropping the original + // packages from this one is what stops the Phar from advertising + // their unprefixed names. + foreach ( (array) glob( WP_CLI_THIRD_PARTY_DIR . '/*/*', GLOB_ONLYDIR ) as $package_dir ) { + $package = substr( (string) $package_dir, strlen( WP_CLI_THIRD_PARTY_DIR ) + 1 ); + + if ( 0 === strpos( $package, 'vendor/' ) ) { + continue; + } + + $strips[] = '\/' . preg_quote( $package, '/' ) . "(?=[\/'])"; + } + } + } + + $strip_res = array_map( + static function ( $v ) { + return '/^[^,\n]+?' . $v . '[^,\n]+?, *\n/m'; + }, + $strips + ); + + return $strip_res; +} + function add_file( $phar, $path ) { $key = str_replace( WP_CLI_BASE_PATH, '', $path ); @@ -61,50 +145,9 @@ function add_file( $phar, $path ) { } $basename = basename( $path ); - if ( 0 === strpos( $basename, 'autoload_' ) && preg_match( '/(?:classmap|files|namespaces|psr4|static)\.php$/', $basename ) ) { + if ( dirname( (string) $path ) === WP_CLI_VENDOR_DIR . '/composer' && 0 === strpos( $basename, 'autoload_' ) && preg_match( '/(?:classmap|files|namespaces|psr4|static)\.php$/', $basename ) ) { // Strip autoload maps of unused stuff. - static $strip_res = null; - if ( null === $strip_res ) { - if ( 'cli' === BUILD ) { - $strips = [ - '\/(?:behat|composer|gherkin)\/src\/', - '\/behat\/', - '\/phpunit\/', - '\/phpstan\/', - '\/phpspec\/', - '\/sebastian\/', - '\/php-parallel-lint\/', - '\/nb\/oxymel\/', - '-command\/src\/', - '\/wp-cli\/[^\n]+?-command\/', - '\/symfony\/(?:config|console|debug|dependency-injection|event-dispatcher|filesystem|translation|yaml)', - '\/(?:dealerdirect|myclabs|squizlabs|wimg)\/', - '\/yoast\/', - ]; - } else { - $strips = [ - '\/(?:behat|gherkin)\/src\/', - '\/behat\/', - '\/phpunit\/', - '\/phpstan\/', - '\/phpspec\/', - '\/sebastian\/', - '\/php-parallel-lint\/', - '\/symfony\/(?:config|debug|dependency-injection|event-dispatcher|translation|yaml)', - '\/composer\/spdx-licenses\/', - '\/Composer\/(?:Command\/|Compiler\.php|Console\/|Downloader\/Pear|Installer\/Pear|Question\/|Repository\/Pear|SelfUpdate\/)', - '\/(?:dealerdirect|myclabs|squizlabs|wimg)\/', - '\/yoast\/', - ]; - } - $strip_res = array_map( - static function ( $v ) { - return '/^[^,\n]+?' . $v . '[^,\n]+?, *\n/m'; - }, - $strips - ); - } - $phar[ $key ] = preg_replace( $strip_res, '', (string) file_get_contents( $path ) ); + $phar[ $key ] = preg_replace( get_autoload_strip_patterns(), '', (string) file_get_contents( $path ) ); } else { $phar[ $key ] = (string) file_get_contents( $path ); } @@ -189,21 +232,7 @@ function get_composer_versions( $current_version ) { $phar->startBuffering(); // PHP files -/* - * `utils/scope-dependencies.php` prefixes symfony/finder along with the rest - * of the Composer tree, so the class this build script itself relies on moves - * depending on whether prefixing has already run. - */ -$finder_class = class_exists( 'Symfony\\Component\\Finder\\Finder' ) - ? 'Symfony\\Component\\Finder\\Finder' - : 'WP_CLI\\Vendor\\Symfony\\Component\\Finder\\Finder'; - -if ( ! class_exists( $finder_class ) ) { - fwrite( STDERR, 'Missing Symfony Finder; run `composer install` first.' . PHP_EOL ); - exit( 1 ); -} - -$finder = new $finder_class(); +$finder = new Finder(); $finder ->files() ->ignoreVCS( true ) @@ -213,8 +242,6 @@ function get_composer_versions( $current_version ) { ->in( WP_CLI_VENDOR_DIR . '/mustache/mustache' ) ->in( WP_CLI_VENDOR_DIR . '/eftec/bladeone' ) ->in( WP_CLI_ROOT . '/bundle/rmccue/requests' ) - ->in( WP_CLI_VENDOR_DIR . '/composer' ) - ->in( WP_CLI_VENDOR_DIR . '/symfony' ) ->notName( 'behat-tags.php' ) ->notPath( '#(?:[^/]+-command|php-cli-tools)/vendor/#' ) // For running locally, in case have composer installed or symlinked them. ->exclude( 'config' ) @@ -229,9 +256,14 @@ function get_composer_versions( $current_version ) { ->exclude( 'tests' ) ->exclude( 'Test' ) ->exclude( 'Tests' ); -if ( is_dir( WP_CLI_VENDOR_DIR . '/react' ) ) { +if ( ! BUNDLE_PREFIXED_DEPENDENCIES ) { $finder - ->in( WP_CLI_VENDOR_DIR . '/react' ); + ->in( WP_CLI_VENDOR_DIR . '/composer' ) + ->in( WP_CLI_VENDOR_DIR . '/symfony' ); + if ( is_dir( WP_CLI_VENDOR_DIR . '/react' ) ) { + $finder + ->in( WP_CLI_VENDOR_DIR . '/react' ); + } } if ( 'cli' === BUILD ) { $finder @@ -248,28 +280,32 @@ function get_composer_versions( $current_version ) { $finder ->in( WP_CLI_VENDOR_DIR . '/wp-cli' ) ->in( WP_CLI_VENDOR_DIR . '/nb/oxymel' ) - ->in( WP_CLI_VENDOR_DIR . '/psr' ) - ->in( WP_CLI_VENDOR_DIR . '/seld' ) - ->in( WP_CLI_VENDOR_DIR . '/justinrainbow/json-schema' ) ->in( WP_CLI_VENDOR_DIR . '/gettext' ) ->in( WP_CLI_VENDOR_DIR . '/mck89' ) ->exclude( 'demo' ) ->exclude( 'wp-cli-tests' ) - ->exclude( 'nb/oxymel/OxymelTest.php' ) - ->exclude( 'composer/spdx-licenses' ) - ->exclude( 'composer/composer/src/Composer/Command' ) - ->exclude( 'composer/composer/src/Composer/Compiler.php' ) - ->exclude( 'composer/composer/src/Composer/Console' ) - ->exclude( 'composer/composer/src/Composer/Downloader/PearPackageExtractor.php' ) // Assuming Pear installation isn't supported by wp-cli. - ->exclude( 'composer/composer/src/Composer/Installer/PearBinaryInstaller.php' ) - ->exclude( 'composer/composer/src/Composer/Installer/PearInstaller.php' ) - ->exclude( 'composer/composer/src/Composer/Question' ) - ->exclude( 'composer/composer/src/Composer/Repository/Pear' ) - ->exclude( 'composer/composer/src/Composer/SelfUpdate' ); - - // required by justinrainbow/json-schema v6+. - if ( is_dir( WP_CLI_VENDOR_DIR . '/marc-mabe/php-enum' ) ) { - $finder->in( WP_CLI_VENDOR_DIR . '/marc-mabe/php-enum' ); + ->exclude( 'nb/oxymel/OxymelTest.php' ); + + if ( ! BUNDLE_PREFIXED_DEPENDENCIES ) { + $finder + ->in( WP_CLI_VENDOR_DIR . '/psr' ) + ->in( WP_CLI_VENDOR_DIR . '/seld' ) + ->in( WP_CLI_VENDOR_DIR . '/justinrainbow/json-schema' ) + ->exclude( 'composer/spdx-licenses' ) + ->exclude( 'composer/composer/src/Composer/Command' ) + ->exclude( 'composer/composer/src/Composer/Compiler.php' ) + ->exclude( 'composer/composer/src/Composer/Console' ) + ->exclude( 'composer/composer/src/Composer/Downloader/PearPackageExtractor.php' ) // Assuming Pear installation isn't supported by wp-cli. + ->exclude( 'composer/composer/src/Composer/Installer/PearBinaryInstaller.php' ) + ->exclude( 'composer/composer/src/Composer/Installer/PearInstaller.php' ) + ->exclude( 'composer/composer/src/Composer/Question' ) + ->exclude( 'composer/composer/src/Composer/Repository/Pear' ) + ->exclude( 'composer/composer/src/Composer/SelfUpdate' ); + + // required by justinrainbow/json-schema v6+. + if ( is_dir( WP_CLI_VENDOR_DIR . '/marc-mabe/php-enum' ) ) { + $finder->in( WP_CLI_VENDOR_DIR . '/marc-mabe/php-enum' ); + } } } @@ -277,8 +313,34 @@ function get_composer_versions( $current_version ) { add_file( $phar, $file ); } +if ( BUNDLE_PREFIXED_DEPENDENCIES ) { + // Composer's autoloader machinery, minus the packages now in third_party/. + $finder = new Finder(); + $finder + ->files() + ->name( '*.php' ) + ->depth( '== 0' ) + ->in( WP_CLI_VENDOR_DIR . '/composer' ); + + foreach ( $finder as $file ) { + add_file( $phar, $file ); + } + + // The prefixed dependency tree, including its own autoloader. + $finder = new Finder(); + $finder + ->files() + ->ignoreVCS( true ) + ->notName( [ 'composer.json', 'composer.lock', 'installed.json' ] ) + ->in( WP_CLI_THIRD_PARTY_DIR ); + + foreach ( $finder as $file ) { + add_file( $phar, $file ); + } +} + // other files -$finder = new $finder_class(); +$finder = new Finder(); $finder ->files() ->ignoreVCS( true ) @@ -293,7 +355,7 @@ function get_composer_versions( $current_version ) { if ( 'cli' !== BUILD ) { // Include base project files, because the autoloader will load them if ( WP_CLI_BASE_PATH !== WP_CLI_BUNDLE_ROOT && is_dir( WP_CLI_BASE_PATH . '/src' ) ) { - $finder = new $finder_class(); + $finder = new Finder(); $finder ->files() ->ignoreVCS( true ) @@ -317,7 +379,8 @@ function get_composer_versions( $current_version ) { } add_file( $phar, WP_CLI_VENDOR_DIR . '/autoload.php' ); -if ( 'cli' !== BUILD ) { +if ( 'cli' !== BUILD && ! BUNDLE_PREFIXED_DEPENDENCIES ) { + // With prefixed dependencies, both come from third_party/. add_file( $phar, WP_CLI_VENDOR_DIR . '/composer/composer/LICENSE' ); add_file( $phar, WP_CLI_VENDOR_DIR . '/composer/composer/res/composer-schema.json' ); } diff --git a/utils/prefix-dependencies.php b/utils/prefix-dependencies.php new file mode 100644 index 000000000..3ffcaca4d --- /dev/null +++ b/utils/prefix-dependencies.php @@ -0,0 +1,429 @@ += 70400 ? $command : implode( ' ', array_map( 'escapeshellarg', $command ) ); + + // @phpstan-ignore argument.type (the stubs describe PHP 7.2, where proc_open() only takes a string) + $process = proc_open( $command_line, [ STDIN, STDOUT, STDERR ], $pipes ); + + return is_resource( $process ) ? proc_close( $process ) : 1; +} + +/** + * @param string $dir + * @return void + */ +function remove_directory( $dir ) { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ( $iterator as $entry ) { + /** + * @var SplFileInfo $entry + */ + if ( $entry->isDir() && ! $entry->isLink() ) { + rmdir( $entry->getPathname() ); + } else { + unlink( $entry->getPathname() ); + } + } + + rmdir( $dir ); +} + +/** + * The Composer binary running this hook, if any. + * + * @return string[] Command prefix. + */ +function composer_command() { + $binary = getenv( 'COMPOSER_BINARY' ); + + return is_string( $binary ) && '' !== $binary + ? [ PHP_BINARY, $binary ] + : [ 'composer' ]; +} + +// --- Which PHP runs php-scoper? --------------------------------------------- + +$scoper_php = getenv( 'WP_CLI_SCOPER_PHP' ); + +if ( ! is_string( $scoper_php ) || '' === $scoper_php ) { + $scoper_php = PHP_BINARY; + $scoper_php_version = PHP_VERSION; +} else { + $scoper_php_version = trim( (string) shell_exec( escapeshellarg( $scoper_php ) . ' -r ' . escapeshellarg( 'echo PHP_VERSION;' ) ) ); + + if ( ! preg_match( '/^\d+\.\d+\.\d+/', $scoper_php_version ) ) { + fail( "WP_CLI_SCOPER_PHP does not point to a working PHP interpreter: '{$scoper_php}'." ); + } +} + +if ( version_compare( $scoper_php_version, WP_CLI_SCOPER_MIN_PHP, '<' ) ) { + skip( + sprintf( + 'Skipping the prefixing of third-party dependencies: php-scoper needs PHP %s or newer, but %s is PHP %s.', + WP_CLI_SCOPER_MIN_PHP, + $scoper_php, + $scoper_php_version + ) + ); +} + +// --- What gets prefixed? ---------------------------------------------------- + +try { + $installed = wp_cli_installed_packages( WP_CLI_BUNDLE_VENDOR_DIR ); + $packages = wp_cli_prefixed_packages( WP_CLI_BUNDLE_VENDOR_DIR ); +} catch ( RuntimeException $exception ) { + fail( $exception->getMessage() ); +} + +if ( ! $packages ) { + skip( WP_CLI_PREFIXED_ROOT_PACKAGE . ' is not installed (a `--no-dev` install?), so there is nothing to prefix.' ); +} + +/* + * Bundled WP-CLI code is not prefixed, so anything it requires from the + * prefixed tree has to be reachable under its original name. Today that is + * only `Composer\Semver`, which stays unprefixed anyway; catch a command that + * starts depending on, say, symfony/process before it breaks in the Phar. + */ +$consumers = []; + +foreach ( $installed as $name => $package ) { + if ( 0 === strpos( $name, 'wp-cli/' ) && 'wp-cli/wp-cli-tests' !== $name ) { + $consumers[ $name ] = $package['require']; + } +} + +$bundle_composer_json = json_decode( (string) file_get_contents( WP_CLI_BUNDLE_ROOT . '/composer.json' ), true ); + +if ( is_array( $bundle_composer_json ) && isset( $bundle_composer_json['require'] ) && is_array( $bundle_composer_json['require'] ) ) { + $consumers['wp-cli/wp-cli-bundle'] = array_map( 'strval', array_keys( $bundle_composer_json['require'] ) ); +} + +$unreachable = []; + +foreach ( $consumers as $consumer => $requirements ) { + $queue = $requirements; + $visited = []; + + while ( $queue ) { + $name = array_shift( $queue ); + + if ( isset( $visited[ $name ] ) || ! isset( $installed[ $name ] ) ) { + continue; + } + + $visited[ $name ] = true; + + if ( ! isset( $packages[ $name ] ) ) { + $queue = array_merge( $queue, $installed[ $name ]['require'] ); + continue; + } + + $namespaces = []; + + foreach ( [ 'psr-4', 'psr-0' ] as $standard ) { + if ( isset( $packages[ $name ]['autoload'][ $standard ] ) && is_array( $packages[ $name ]['autoload'][ $standard ] ) ) { + $namespaces = array_merge( $namespaces, array_map( 'strval', array_keys( $packages[ $name ]['autoload'][ $standard ] ) ) ); + } + } + + foreach ( $namespaces as $namespace ) { + if ( ! wp_cli_is_unprefixed_namespace( rtrim( $namespace, '\\' ) ) ) { + $unreachable[] = "{$consumer} requires {$name}, whose namespace {$namespace} gets prefixed"; + } + } + } +} + +if ( $unreachable ) { + fail( + "Bundled WP-CLI code depends on packages that are prefixed for the Phar and would not find them:\n " + . implode( "\n ", array_unique( $unreachable ) ) + . "\nEither drop the dependency or add its namespace to WP_CLI_UNPREFIXED_NAMESPACES in utils/scoper/prefixed-packages.php." + ); +} + +// --- Anything to do? -------------------------------------------------------- + +$stamp = sha1( + implode( + "\n", + array_map( + static function ( $file ) { + return sha1( (string) file_get_contents( $file ) ); + }, + [ + WP_CLI_BUNDLE_VENDOR_DIR . '/composer/installed.json', + WP_CLI_SCOPER_DIR . '/composer.lock', + WP_CLI_SCOPER_DIR . '/scoper.inc.php', + WP_CLI_SCOPER_DIR . '/prefixed-packages.php', + __FILE__, + ] + ) + ) +); + +if ( ! $force && file_exists( WP_CLI_THIRD_PARTY_STAMP ) && trim( (string) file_get_contents( WP_CLI_THIRD_PARTY_STAMP ) ) === $stamp ) { + report( 'Prefixed third-party dependencies in third_party/ are up to date.' ); + exit( 0 ); +} + +// --- Toolchain -------------------------------------------------------------- + +report( 'Installing the php-scoper toolchain into utils/scoper/vendor/...' ); + +$composer = composer_command(); +// Composer checks the platform of the interpreter running it, so the +// toolchain must be installed with the PHP that will run php-scoper. +$composer[0] = PHP_BINARY === $scoper_php ? $composer[0] : $scoper_php; + +if ( 0 !== run( array_merge( $composer, [ 'install', '--working-dir=' . WP_CLI_SCOPER_DIR, '--no-interaction', '--no-progress', '--no-plugins' ] ) ) ) { + fail( 'Failed to install the php-scoper toolchain.' ); +} + +// --- Prefix ----------------------------------------------------------------- + +if ( is_dir( WP_CLI_THIRD_PARTY_DIR ) ) { + remove_directory( WP_CLI_THIRD_PARTY_DIR ); +} + +report( sprintf( 'Prefixing %d packages into third_party/...', count( $packages ) ) ); + +$scoper_exit = run( + [ + $scoper_php, + WP_CLI_SCOPER_DIR . '/vendor/bin/php-scoper', + 'add-prefix', + '--config=' . WP_CLI_SCOPER_DIR . '/scoper.inc.php', + '--output-dir=' . WP_CLI_THIRD_PARTY_DIR, + '--force', + '--stop-on-failure', + '--no-interaction', + '--no-ansi', + '--quiet', + ] +); + +if ( 0 !== $scoper_exit ) { + fail( 'php-scoper failed.' ); +} + +// --- Autoloader ------------------------------------------------------------- + +/* + * The prefixed files no longer satisfy their packages' PSR-4 rules (the files + * in third_party/psr/log now declare `WP_CLI\Vendor\Psr\Log\*`), so those + * rules become classmap rules over the same directories: Composer then records + * whatever names the files actually declare. `files` rules are kept as they + * are; they load the polyfills and the namespaced function files. + */ +$classmap = []; +$files = []; + +foreach ( $packages as $name => $package ) { + $prefixed_path = WP_CLI_THIRD_PARTY_DIR . '/' . $package['relative']; + + if ( ! is_dir( $prefixed_path ) ) { + fail( "php-scoper did not produce '{$prefixed_path}'." ); + } + + foreach ( [ 'psr-4', 'psr-0', 'classmap', 'files' ] as $rule ) { + if ( ! isset( $package['autoload'][ $rule ] ) || ! is_array( $package['autoload'][ $rule ] ) ) { + continue; + } + + foreach ( $package['autoload'][ $rule ] as $paths ) { + foreach ( (array) $paths as $path ) { + if ( ! is_string( $path ) ) { + continue; + } + + $relative = trim( $package['relative'] . '/' . wp_cli_normalize_path( $path ), '/' ); + + // Skipped by the finders, like a `Tests/` directory. + if ( ! file_exists( WP_CLI_THIRD_PARTY_DIR . '/' . $relative ) ) { + continue; + } + + if ( 'files' === $rule ) { + $files[] = $relative; + } else { + $classmap[] = $relative; + } + } + } + } +} + +$third_party_composer_json = [ + 'name' => 'wp-cli/wp-cli-bundle-third-party', + 'description' => 'Autoloader for the prefixed dependencies bundled into the Phar. Generated by utils/prefix-dependencies.php.', + 'autoload' => [ + 'classmap' => array_values( array_unique( $classmap ) ), + 'files' => array_values( array_unique( $files ) ), + ], + 'config' => [ + 'autoloader-suffix' => 'WpCliBundleThirdParty', + 'classmap-authoritative' => true, + 'optimize-autoloader' => true, + 'platform-check' => false, + ], +]; + +if ( false === file_put_contents( WP_CLI_THIRD_PARTY_DIR . '/composer.json', json_encode( $third_party_composer_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) . "\n" ) ) { + fail( 'Failed to write third_party/composer.json.' ); +} + +report( 'Generating the third_party/ autoloader...' ); + +/* + * `install` rather than `dump-autoload`: only a real install writes the + * `InstalledVersions.php` that the generated classmap points at, and this + * autoloader has to stand on its own, as it is registered before the + * bundle's main one. + */ +if ( 0 !== run( array_merge( composer_command(), [ 'install', '--working-dir=' . WP_CLI_THIRD_PARTY_DIR, '--no-interaction', '--no-plugins', '--no-progress' ] ) ) ) { + fail( 'Failed to generate the third_party/ autoloader.' ); +} + +// --- Verify ----------------------------------------------------------------- + +/* + * The failure mode this guards against is silent: a class php-scoper left + * alone still ends up in the classmap under its original name, and the Phar + * would keep imposing it on the site with nothing in the build output to + * show for it. + */ +$class_map = require WP_CLI_THIRD_PARTY_DIR . '/vendor/composer/autoload_classmap.php'; + +if ( ! is_array( $class_map ) || ! $class_map ) { + fail( 'The third_party/ classmap is empty.' ); +} + +$leaked = []; +$prefixed = 0; + +foreach ( $class_map as $class => $file ) { + $class = (string) $class; + + if ( 0 === strpos( $class, WP_CLI_VENDOR_PREFIX . '\\' ) ) { + ++$prefixed; + continue; + } + + if ( wp_cli_is_unprefixed_namespace( $class ) ) { + continue; + } + + // Global classes are only expected from the polyfills, which guard them. + if ( false === strpos( $class, '\\' ) && is_string( $file ) && preg_match( '#/symfony/polyfill-[^/]+/#', str_replace( '\\', '/', $file ) ) ) { + continue; + } + + $leaked[] = $class; +} + +if ( $leaked ) { + fail( + "The third_party/ autoloader advertises classes under their original names:\n " + . implode( "\n ", $leaked ) + . "\nThe Phar would keep imposing these on the site, see https://github.com/wp-cli/wp-cli/issues/5920" + ); +} + +if ( 0 === $prefixed ) { + fail( 'The third_party/ classmap contains no prefixed classes at all.' ); +} + +file_put_contents( WP_CLI_THIRD_PARTY_STAMP, $stamp . "\n" ); + +report( sprintf( 'Prefixed %d packages (%d classes) into third_party/.', count( $packages ), $prefixed ) ); diff --git a/utils/scope-dependencies.php b/utils/scope-dependencies.php deleted file mode 100644 index dac204e37..000000000 --- a/utils/scope-dependencies.php +++ /dev/null @@ -1,318 +0,0 @@ -] [--quiet] - * - * @see https://github.com/wp-cli/wp-cli/issues/5920 - */ - -declare( strict_types=1 ); - -define( 'WP_CLI_BUNDLE_ROOT', rtrim( dirname( __DIR__ ), '/' ) ); - -/** - * Vendor directories handed to php-scoper. Keep in sync with the finders in - * `utils/scoper/scoper.inc.php`. - */ -const SCOPED_VENDOR_DIRS = [ - 'composer', - 'justinrainbow', - 'marc-mabe', // spellchecker:disable-line - 'psr', - 'react', - 'seld', - 'symfony', -]; - -$options = getopt( '', [ 'vendor-dir::', 'quiet' ] ); -$be_quiet = isset( $options['quiet'] ); -$vendor_dir = isset( $options['vendor-dir'] ) && is_string( $options['vendor-dir'] ) - ? rtrim( $options['vendor-dir'], '/' ) - : WP_CLI_BUNDLE_ROOT . '/vendor'; - -$scoper_dir = WP_CLI_BUNDLE_ROOT . '/utils/scoper'; - -/** - * Write a progress line unless running quietly. - */ -function report( string $message ): void { - if ( ! $GLOBALS['be_quiet'] ) { - fwrite( STDOUT, $message . PHP_EOL ); - } -} - -/** - * Run a command, returning its exit code. - * - * @param array $command - */ -function run( array $command, ?string $cwd = null ): int { - $cwd_prefix = null !== $cwd ? sprintf( 'cd %s && ', escapeshellarg( $cwd ) ) : ''; - $escaped = implode( ' ', array_map( 'escapeshellarg', $command ) ); - - passthru( $cwd_prefix . $escaped, $exit_code ); - - return $exit_code; -} - -/** - * Fail with a message. - */ -function fail( string $message ): void { - fwrite( STDERR, 'Error: ' . $message . PHP_EOL ); - exit( 1 ); -} - -if ( ! is_dir( $vendor_dir ) ) { - fail( sprintf( "Vendor directory '%s' does not exist. Run `composer install` first.", $vendor_dir ) ); -} - -// php-scoper needs PHP 8.2+, which is why it lives in its own composer.json -// rather than in the bundle's (that one still has to resolve against PHP 7.2.24). -if ( PHP_VERSION_ID < 80200 ) { - fail( sprintf( 'php-scoper requires PHP 8.2 or newer, but this is PHP %s.', PHP_VERSION ) ); -} - -// --- 1. Make sure the isolated toolchain is installed. ---------------------- - -if ( ! file_exists( $scoper_dir . '/vendor/bin/php-scoper' ) ) { - report( 'Installing the php-scoper toolchain...' ); - if ( 0 !== run( [ 'composer', 'install', '--no-interaction', '--prefer-dist', '--quiet' ], $scoper_dir ) ) { - fail( 'Failed to install the php-scoper toolchain.' ); - } -} - -// --- 2. Prefix the dependency tree. ----------------------------------------- - -$output_dir = $vendor_dir . '/../build/scoped-vendor'; - -if ( is_dir( $output_dir ) ) { - run( [ 'rm', '-rf', $output_dir ] ); -} - -report( 'Prefixing third-party dependencies...' ); - -putenv( 'WP_CLI_SCOPER_VENDOR_DIR=' . $vendor_dir ); - -$scoper_exit = run( - [ - $scoper_dir . '/vendor/bin/php-scoper', - 'add-prefix', - '--config=' . $scoper_dir . '/scoper.inc.php', - '--output-dir=' . $output_dir, - '--force', - '--no-interaction', - $be_quiet ? '--quiet' : '--no-ansi', - ] -); - -if ( 0 !== $scoper_exit ) { - fail( 'php-scoper failed.' ); -} - -// --- 3. Swap the prefixed tree into vendor/. -------------------------------- - -foreach ( SCOPED_VENDOR_DIRS as $dir ) { - $scoped = $output_dir . '/' . $dir; - $target = $vendor_dir . '/' . $dir; - - if ( ! is_dir( $scoped ) ) { - continue; - } - - report( sprintf( ' Replacing vendor/%s', $dir ) ); - - // Composer's autoloader machinery lives alongside the composer/* packages - // in vendor/composer and is regenerated below, so only the package - // subdirectories are replaced wholesale. - if ( 0 !== run( [ 'cp', '-a', $scoped . '/.', $target . '/' ] ) ) { - fail( sprintf( "Failed to copy the prefixed '%s' into place.", $dir ) ); - } -} - -// --- 4. Teach Composer about the new class names. --------------------------- - -/* - * The prefixed files no longer satisfy their packages' PSR-4 rules: the classes - * in vendor/psr/log now declare WP_CLI\Vendor\Psr\Log\*, while psr/log's - * composer.json still maps Psr\Log\ to that directory. Left alone, a dump would - * both re-advertise the unprefixed prefix and skip the prefixed classes as - * "not compliant with PSR-4". - * - * Rewriting the affected packages' autoload rules to a classmap sidesteps both - * problems: Composer scans the directories and records whatever class names the - * files actually declare. - */ -$installed_json = $vendor_dir . '/composer/installed.json'; - -if ( ! file_exists( $installed_json ) ) { - fail( sprintf( "Could not find '%s'.", $installed_json ) ); -} - -$decoded = json_decode( (string) file_get_contents( $installed_json ), true ); - -if ( ! is_array( $decoded ) || ! isset( $decoded['packages'] ) || ! is_array( $decoded['packages'] ) ) { - fail( sprintf( "Could not decode '%s'.", $installed_json ) ); -} - -/** - * @var array $decoded - * @var array $packages - */ -$packages = $decoded['packages']; -$patched = 0; - -foreach ( $packages as $index => $package ) { - if ( ! is_array( $package ) || ! isset( $package['name'] ) || ! is_string( $package['name'] ) ) { - continue; - } - - $vendor_name = explode( '/', $package['name'] )[0]; - - if ( ! in_array( $vendor_name, SCOPED_VENDOR_DIRS, true ) ) { - continue; - } - - if ( ! isset( $package['autoload'] ) || ! is_array( $package['autoload'] ) ) { - continue; - } - - $autoload = $package['autoload']; - $roots = []; - - foreach ( [ 'psr-4', 'psr-0' ] as $standard ) { - $rules = $autoload[ $standard ] ?? []; - - if ( ! is_array( $rules ) ) { - continue; - } - - foreach ( $rules as $paths ) { - foreach ( (array) $paths as $path ) { - if ( is_string( $path ) ) { - $roots[] = '' === $path ? '.' : $path; - } - } - } - } - - $classmap_rules = $autoload['classmap'] ?? []; - - if ( is_array( $classmap_rules ) ) { - foreach ( $classmap_rules as $path ) { - if ( is_string( $path ) ) { - $roots[] = $path; - } - } - } - - if ( ! $roots ) { - continue; - } - - $package['autoload'] = [ 'classmap' => array_values( array_unique( $roots ) ) ]; - $packages[ $index ] = $package; - ++$patched; -} - -$decoded['packages'] = $packages; - -report( sprintf( 'Rewrote autoload rules for %d prefixed package(s).', $patched ) ); - -$encoded = json_encode( $decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); - -if ( false === $encoded || false === file_put_contents( $installed_json, $encoded ) ) { - fail( sprintf( "Failed to write '%s'.", $installed_json ) ); -} - -// --- 5. Regenerate the autoloader. ------------------------------------------ - -report( 'Regenerating the Composer autoloader...' ); - -/* - * --classmap-authoritative makes the ClassLoader consult only the classmap, so - * no leftover PSR-4 rule can resurrect an unprefixed name. Everything the Phar - * runs is inside the Phar, so there is nothing to discover at runtime. - */ -// Derived from the vendor directory rather than assumed, so the script can be -// pointed at a scratch tree for testing. -$composer_root = dirname( $vendor_dir ); - -if ( 0 !== run( [ 'composer', 'dump-autoload', '--classmap-authoritative', '--no-interaction' ], $composer_root ) ) { - fail( 'Failed to regenerate the Composer autoloader.' ); -} - -run( [ 'rm', '-rf', dirname( $output_dir ) ] ); - -// --- 6. Verify the autoloader no longer claims the unprefixed names. -------- - -/* - * The failure mode this guards against is silent: php-scoper rewrites source - * files but not Composer's generated maps, so a tree that looks scoped can - * still resolve `Psr\Log\LoggerInterface` to the bundled copy and reintroduce - * the conflict with nothing in the build output to show for it. - */ -$autoload_files = array_filter( - [ - $vendor_dir . '/composer/autoload_classmap.php', - $vendor_dir . '/composer/autoload_psr4.php', - $vendor_dir . '/composer/autoload_static.php', - ], - 'file_exists' -); - -$must_not_appear = [ - 'Psr\\Log\\', - 'Symfony\\Component\\Console\\', - 'React\\Promise\\', - 'Seld\\JsonLint\\', -]; - -$leaked = []; - -foreach ( $autoload_files as $file ) { - $contents = (string) file_get_contents( $file ); - - foreach ( $must_not_appear as $symbol ) { - // Written as it appears in the generated PHP source, where each - // namespace separator is escaped. - $needle = str_replace( '\\', '\\\\', $symbol ); - $prefixed = 'WP_CLI\\\\Vendor\\\\' . $needle; - $occurring = substr_count( $contents, $needle ) - substr_count( $contents, $prefixed ); - - if ( $occurring > 0 ) { - $leaked[] = sprintf( ' %s advertises %s (%d time(s))', basename( $file ), $symbol, $occurring ); - } - } -} - -if ( $leaked ) { - fail( - "The regenerated autoloader still advertises unprefixed dependencies:\n" - . implode( "\n", $leaked ) - . "\nThe Phar would keep imposing these on the site. See https://github.com/wp-cli/wp-cli/issues/5920" - ); -} - -$classmap = $vendor_dir . '/composer/autoload_classmap.php'; - -// strpos() rather than str_contains() so the file still parses under the 7.2 -// baseline phpcs checks this repository against, even though the script itself -// refuses to run on anything below PHP 8.2. -if ( file_exists( $classmap ) && false === strpos( (string) file_get_contents( $classmap ), 'WP_CLI\\\\Vendor\\\\' ) ) { - fail( 'The regenerated classmap contains no prefixed classes at all; the prefixing step did not take effect.' ); -} - -report( 'Verified: the autoloader advertises only prefixed dependencies.' ); -report( 'Done.' ); diff --git a/utils/scoper/composer.json b/utils/scoper/composer.json index 4db272d65..470a832ab 100644 --- a/utils/scoper/composer.json +++ b/utils/scoper/composer.json @@ -1,13 +1,13 @@ { - "name": "wp-cli/phar-scoper-toolchain", - "description": "Isolated toolchain used to prefix the Phar's third-party dependencies. Kept out of the bundle's own composer.json because php-scoper requires PHP 8.2+, while WP-CLI still targets PHP 7.2.24.", + "name": "wp-cli/php-scoper-toolchain", + "description": "Isolated php-scoper toolchain driven by utils/prefix-dependencies.php. Kept out of the bundle's own composer.json because php-scoper needs PHP 8.2+, while the bundle still targets PHP 7.2.", "license": "MIT", "type": "project", "require": { - "humbug/php-scoper": "^0.18" + "php": ">=8.2", + "humbug/php-scoper": "^0.18.18" }, "config": { - "sort-packages": true, - "lock": false + "sort-packages": true } } diff --git a/utils/scoper/composer.lock b/utils/scoper/composer.lock new file mode 100644 index 000000000..3b387305c --- /dev/null +++ b/utils/scoper/composer.lock @@ -0,0 +1,1781 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "508ea839e1f9a57931be42a92569e378", + "packages": [ + { + "name": "fidry/console", + "version": "0.6.11", + "source": { + "type": "git", + "url": "https://github.com/theofidry/console.git", + "reference": "bea8316beae874fc5b8be679d67dd3169c7e205f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/console/zipball/bea8316beae874fc5b8be679d67dd3169c7e205f", + "reference": "bea8316beae874fc5b8be679d67dd3169c7e205f", + "shasum": "" + }, + "require": { + "php": "^8.2", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/console": "^6.4 || ^7.2", + "symfony/deprecation-contracts": "^3.4", + "symfony/event-dispatcher-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php84": "^1.31", + "symfony/service-contracts": "^2.5 || ^3.0", + "thecodingmachine/safe": "^2.0 || ^3.0", + "webmozart/assert": "^1.11" + }, + "conflict": { + "symfony/dependency-injection": "<6.4.0 || >=7.0.0 <7.2.0", + "symfony/framework-bundle": "<6.4.0 || >=7.0.0 <7.2.0", + "symfony/http-kernel": "<6.4.0 || >=7.0.0 <7.2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "composer/semver": "^3.3.2", + "ergebnis/composer-normalize": "^2.33", + "fidry/makefile": "^0.2.1 || ^1.0.0", + "infection/infection": "^0.28", + "phpunit/phpunit": "^10.2", + "symfony/dependency-injection": "^6.4 || ^7.2", + "symfony/flex": "^2.4.0", + "symfony/framework-bundle": "^6.4 || ^7.2", + "symfony/http-kernel": "^6.4 || ^7.2", + "symfony/yaml": "^6.4 || ^7.2" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fidry\\Console\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Library to create CLI applications", + "keywords": [ + "cli", + "console", + "symfony" + ], + "support": { + "issues": "https://github.com/theofidry/console/issues", + "source": "https://github.com/theofidry/console/tree/0.6.11" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-02-14T11:06:15+00:00" + }, + { + "name": "fidry/filesystem", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/filesystem.git", + "reference": "d0d9e8dfa43f7663da153c306b0d5bc24846ad8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/filesystem/zipball/d0d9e8dfa43f7663da153c306b0d5bc24846ad8e", + "reference": "d0d9e8dfa43f7663da153c306b0d5bc24846ad8e", + "shasum": "" + }, + "require": { + "php": "^8.3", + "symfony/filesystem": "^6.4 || ^7.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "ergebnis/composer-normalize": "^2.28", + "infection/infection": ">=0.26", + "phpunit/phpunit": "^12", + "symfony/finder": "^6.4 || ^7.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fidry\\FileSystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Symfony Filesystem with a few more utilities.", + "keywords": [ + "filesystem" + ], + "support": { + "issues": "https://github.com/theofidry/filesystem/issues", + "source": "https://github.com/theofidry/filesystem/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-02-13T23:05:19+00:00" + }, + { + "name": "humbug/php-scoper", + "version": "0.18.18", + "source": { + "type": "git", + "url": "https://github.com/humbug/php-scoper.git", + "reference": "dd55d01a937602c9473cfbe0ecab9521cb9740aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/humbug/php-scoper/zipball/dd55d01a937602c9473cfbe0ecab9521cb9740aa", + "reference": "dd55d01a937602c9473cfbe0ecab9521cb9740aa", + "shasum": "" + }, + "require": { + "fidry/console": "^0.6.10", + "fidry/filesystem": "^1.1", + "jetbrains/phpstorm-stubs": "^2024.1", + "nikic/php-parser": "^5.0", + "php": "^8.2", + "symfony/console": "^6.4 || ^7.0", + "symfony/filesystem": "^6.4 || ^7.0", + "symfony/finder": "^6.4 || ^7.0", + "symfony/var-dumper": "^7.1", + "thecodingmachine/safe": "^3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.1", + "ergebnis/composer-normalize": "^2.28", + "fidry/makefile": "^1.0", + "humbug/box": "^4.6.2", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0", + "symfony/yaml": "^6.4 || ^7.0" + }, + "bin": [ + "bin/php-scoper" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Humbug\\PhpScoper\\": "src/" + }, + "classmap": [ + "vendor-hotfix/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + }, + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com" + } + ], + "description": "Prefixes all PHP namespaces in a file or directory.", + "support": { + "issues": "https://github.com/humbug/php-scoper/issues", + "source": "https://github.com/humbug/php-scoper/tree/0.18.18" + }, + "time": "2025-10-15T15:29:47+00:00" + }, + { + "name": "jetbrains/phpstorm-stubs", + "version": "v2024.3", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-stubs.git", + "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", + "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", + "shasum": "" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "v3.64.0", + "nikic/php-parser": "v5.3.1", + "phpdocumentor/reflection-docblock": "5.6.0", + "phpunit/phpunit": "11.4.3" + }, + "type": "library", + "autoload": { + "files": [ + "PhpStormStubsMap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "PHP runtime & extensions header files for PhpStorm", + "homepage": "https://www.jetbrains.com/phpstorm", + "keywords": [ + "autocomplete", + "code", + "inference", + "inspection", + "jetbrains", + "phpstorm", + "stubs", + "type" + ], + "support": { + "source": "https://github.com/JetBrains/phpstorm-stubs/tree/v2024.3" + }, + "time": "2024-12-14T08:03:12+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.8.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" + }, + "time": "2026-07-04T14:30:18+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/23d6f88a29f6d0eac45bd77d70307adf83ba7ab0", + "reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-25T14:18:37+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "90d412aa5277c6819db39e7605aa46b1019e3232" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/90d412aa5277c6819db39e7605aa46b1019e3232", + "reference": "90d412aa5277c6819db39e7605aa46b1019e3232", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-23T10:03:40+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/5ce28827081f6d1f0c32eaf3882750f19cb5bbe6", + "reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T12:09:28+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-28T08:25:59+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.42.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-07T06:33:24+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-27T06:59:30+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-27T15:39:01+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-28T07:35:25+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "e088da50b813f32473a76871616cbb8fa54653a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e088da50b813f32473a76871616cbb8fa54653a8", + "reference": "e088da50b813f32473a76871616cbb8fa54653a8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-30T20:10:52+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^7.2 || ^8.0" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.12.1" + }, + "time": "2025-10-29T15:56:20+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/utils/scoper/prefixed-packages.php b/utils/scoper/prefixed-packages.php new file mode 100644 index 000000000..325ed45ac --- /dev/null +++ b/utils/scoper/prefixed-packages.php @@ -0,0 +1,158 @@ +}> Keyed by package name. + * @throws RuntimeException If `installed.json` cannot be read. + */ +function wp_cli_installed_packages( $vendor_dir ) { + $installed_json = $vendor_dir . '/composer/installed.json'; + $installed = file_exists( $installed_json ) + ? json_decode( (string) file_get_contents( $installed_json ), true ) + : null; + + if ( ! is_array( $installed ) || ! isset( $installed['packages'] ) || ! is_array( $installed['packages'] ) ) { + throw new RuntimeException( "Could not read '{$installed_json}'. Run `composer install` first." ); + } + + $packages = []; + + foreach ( $installed['packages'] as $package ) { + if ( ! is_array( $package ) || ! isset( $package['name'] ) || ! is_string( $package['name'] ) ) { + continue; + } + + // Metapackages have no install path and nothing to prefix. + if ( ! isset( $package['install-path'] ) || ! is_string( $package['install-path'] ) ) { + continue; + } + + // `install-path` is relative to `vendor/composer/`. + $relative = wp_cli_normalize_path( 'composer/' . $package['install-path'] ); + $require = isset( $package['require'] ) && is_array( $package['require'] ) + ? array_map( 'strval', array_keys( $package['require'] ) ) + : []; + $autoload = isset( $package['autoload'] ) && is_array( $package['autoload'] ) + ? $package['autoload'] + : []; + + $packages[ $package['name'] ] = [ + 'path' => $vendor_dir . '/' . $relative, + 'relative' => $relative, + 'require' => $require, + 'autoload' => $autoload, + ]; + } + + return $packages; +} + +/** + * Resolves `composer/composer` and its transitive dependencies. + * + * Platform packages (`php`, `ext-*`, `composer-plugin-api`) never appear in + * `installed.json` and drop out on their own. + * + * @param string $vendor_dir Absolute path to the bundle's vendor directory. + * @return array}> Keyed and sorted by package name; empty if `composer/composer` is not installed. + */ +function wp_cli_prefixed_packages( $vendor_dir ) { + $installed = wp_cli_installed_packages( $vendor_dir ); + $prefixed = []; + $queue = [ WP_CLI_PREFIXED_ROOT_PACKAGE ]; + + while ( $queue ) { + $name = array_shift( $queue ); + + if ( isset( $prefixed[ $name ] ) || ! isset( $installed[ $name ] ) ) { + continue; + } + + $prefixed[ $name ] = $installed[ $name ]; + $queue = array_merge( $queue, $installed[ $name ]['require'] ); + } + + ksort( $prefixed ); + + return $prefixed; +} + +/** + * Whether a class or namespace name is one the Phar deliberately keeps unprefixed. + * + * @param string $name Fully qualified name without a leading backslash. + * @return bool + */ +function wp_cli_is_unprefixed_namespace( $name ) { + foreach ( WP_CLI_UNPREFIXED_NAMESPACES as $namespace ) { + if ( $name === $namespace || 0 === strpos( $name, $namespace . '\\' ) ) { + return true; + } + } + + return false; +} + +/** + * Collapses `.` and `..` segments in a relative, forward-slash path. + * + * @param string $path + * @return string + */ +function wp_cli_normalize_path( $path ) { + $normalized = []; + + foreach ( explode( '/', str_replace( '\\', '/', $path ) ) as $segment ) { + if ( '' === $segment || '.' === $segment ) { + continue; + } + + if ( '..' === $segment ) { + array_pop( $normalized ); + continue; + } + + $normalized[] = $segment; + } + + return implode( '/', $normalized ); +} diff --git a/utils/scoper/scoper.inc.php b/utils/scoper/scoper.inc.php index fce325037..19de78b30 100644 --- a/utils/scoper/scoper.inc.php +++ b/utils/scoper/scoper.inc.php @@ -1,133 +1,110 @@ 'WP_CLI\\Vendor', - 'finders' => [ - /* - * Deliberately without exclusions. The prefixed output is merged back - * over `vendor/` rather than replacing it, because php-scoper only - * emits the PHP files it processed and the directories also hold - * assets the Phar needs (certificate bundles, templates, stubs). - * Any PHP file skipped here would therefore survive the merge with its - * original namespace intact and be picked up by the regenerated - * classmap -- which is exactly the unprefixed name the Phar is not - * supposed to advertise any more. Test fixtures are the usual culprit: - * `Psr\Log\Test\TestLogger` implements the very interface at issue. - */ - $finder_class::create() + 'prefix' => WP_CLI_VENDOR_PREFIX, + + 'finders' => [ + Finder::create() ->files() ->ignoreVCS( true ) ->name( '*.php' ) - ->in( $scoped_paths ), - ], + ->exclude( [ 'test', 'tests', 'Test', 'Tests' ] ) + // Parts of Composer the Phar never needed. Whatever is left out here + // also stays out of the generated classmap. + ->notPath( '#^src/Composer/(?:Command|Console|Question|SelfUpdate|Installer/Pear|Repository/Pear)/#' ) + ->notPath( '#^src/Composer/(?:Compiler|Downloader/PearPackageExtractor|Installer/PearBinaryInstaller|Installer/PearInstaller)\.php$#' ) + ->in( array_column( $packages, 'path' ) ), - /* - * Left unprefixed so third-party Composer plugins keep implementing the - * real interfaces. References from these files to the prefixed vendors are - * still rewritten by php-scoper. - */ - 'exclude-namespaces' => [ - 'Composer', + // Non-PHP files Composer reads at runtime; copied unchanged. + Finder::create() + ->append( + [ + $vendor_dir . '/composer/composer/res/composer-schema.json', + $vendor_dir . '/composer/composer/LICENSE', + ] + ), ], - 'exclude-classes' => [], - 'exclude-functions' => [], - 'exclude-constants' => [], + // See WP_CLI_UNPREFIXED_NAMESPACES for why these keep their names. + 'exclude-namespaces' => WP_CLI_UNPREFIXED_NAMESPACES, + 'exclude-files' => array_filter( $verbatim_files, 'is_string' ), + 'exclude-functions' => [ 'trigger_deprecation' ], + 'exclude-constants' => [ '/^SYMFONY_[\p{L}_]+$/' ], + + // Nothing in the tree is meant to be reachable under a global name; the + // polyfills above are the one exception and are handled explicitly. + 'expose-global-constants' => false, + 'expose-global-classes' => false, + 'expose-global-functions' => false, - 'patchers' => [ + 'patchers' => [ /* - * Excluding a namespace stops php-scoper prefixing its declarations, - * but not string literals that name classes inside it. Composer passes - * plenty of class names around as strings -- `ArrayLoader::load()` - * defaults `$class` to 'Composer\Package\CompletePackage' and compares - * against it -- and prefixing those strings points them at classes - * that do not exist, because `Composer\` itself was left alone. - * - * Left unpatched this is quiet rather than fatal: Composer emits a - * spurious "The $class arg is deprecated" notice and carries on, while - * the same mismatch in a `new $class` path would be a hard failure. + * Excluding a namespace keeps php-scoper away from its declarations + * and references, but not reliably from string literals naming its + * classes: `ArrayLoader::load()` defaults its $class parameter to + * 'Composer\Package\CompletePackage' and that string does get + * prefixed, pointing at a class that does not exist because + * `Composer\` itself was left alone. */ - static function ( string $file_path, string $prefix, string $contents ): string { - return str_replace( - [ - $prefix . '\\Composer\\', - $prefix . '\\\\Composer\\\\', - ], - [ - 'Composer\\', - 'Composer\\\\', - ], - $contents - ); + static function ( $file_path, $prefix, $contents ) { + foreach ( WP_CLI_UNPREFIXED_NAMESPACES as $namespace ) { + $contents = str_replace( + [ + $prefix . '\\' . $namespace . '\\', + str_replace( '\\', '\\\\', $prefix . '\\' . $namespace . '\\' ), + ], + [ + $namespace . '\\', + str_replace( '\\', '\\\\', $namespace . '\\' ), + ], + $contents + ); + } + + return $contents; }, ], ]; From 042f7d6cde4367116b1035c79da6789b4936ff35 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 16:36:33 +0000 Subject: [PATCH 8/9] Make the php-scoper toolchain install on PHP 8.2 and run on PHP 8.5 The toolchain lock file had been resolved against PHP 8.4, so it pinned fidry/filesystem 1.3 and symfony/string 8.1, which refuse to install on the PHP 8.2 and 8.3 legs of the test matrix. Lock against a PHP 8.2 platform. On PHP 8.5, php-scoper 0.18.18 died while parsing the first file: its own UseStmtCollection used null as an array key, and its error handler turned the resulting deprecation into an exception. Bump to 0.18.19, which fixes both, and keep deprecations out of the php-scoper run so that a future PHP cannot fail the build the same way. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BrV8jGVsfiypo6buRmZfug --- utils/prefix-dependencies.php | 8 ++ utils/scoper/composer.json | 7 +- utils/scoper/composer.lock | 188 +++++++++++++++++++++++++--------- 3 files changed, 152 insertions(+), 51 deletions(-) diff --git a/utils/prefix-dependencies.php b/utils/prefix-dependencies.php index 3ffcaca4d..06d5a5725 100644 --- a/utils/prefix-dependencies.php +++ b/utils/prefix-dependencies.php @@ -278,9 +278,17 @@ static function ( $file ) { report( sprintf( 'Prefixing %d packages into third_party/...', count( $packages ) ) ); +/* + * A deprecation that a newer PHP raises inside the toolchain is noise at best + * and, up to php-scoper 0.18.18, fatal: it turned every diagnostic into an + * exception (PHP 8.5, "Using null as an array offset"). The prefixed output + * is unaffected either way, so keep deprecations out of the run. + */ $scoper_exit = run( [ $scoper_php, + '-d', + 'error_reporting=' . ( E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED ), WP_CLI_SCOPER_DIR . '/vendor/bin/php-scoper', 'add-prefix', '--config=' . WP_CLI_SCOPER_DIR . '/scoper.inc.php', diff --git a/utils/scoper/composer.json b/utils/scoper/composer.json index 470a832ab..2e14152ee 100644 --- a/utils/scoper/composer.json +++ b/utils/scoper/composer.json @@ -5,9 +5,14 @@ "type": "project", "require": { "php": ">=8.2", - "humbug/php-scoper": "^0.18.18" + "humbug/php-scoper": "^0.18.19" }, + "minimum-stability": "dev", + "prefer-stable": true, "config": { + "platform": { + "php": "8.2.0" + }, "sort-packages": true } } diff --git a/utils/scoper/composer.lock b/utils/scoper/composer.lock index 3b387305c..c34069fda 100644 --- a/utils/scoper/composer.lock +++ b/utils/scoper/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "508ea839e1f9a57931be42a92569e378", + "content-hash": "f82afeff84c771d44a995ead31c23fbd", "packages": [ { "name": "fidry/console", @@ -94,27 +94,27 @@ }, { "name": "fidry/filesystem", - "version": "1.3.0", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theofidry/filesystem.git", - "reference": "d0d9e8dfa43f7663da153c306b0d5bc24846ad8e" + "reference": "3e1f9cac40f807b7c4196013ab77cc1b9416e3e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/filesystem/zipball/d0d9e8dfa43f7663da153c306b0d5bc24846ad8e", - "reference": "d0d9e8dfa43f7663da153c306b0d5bc24846ad8e", + "url": "https://api.github.com/repos/theofidry/filesystem/zipball/3e1f9cac40f807b7c4196013ab77cc1b9416e3e5", + "reference": "3e1f9cac40f807b7c4196013ab77cc1b9416e3e5", "shasum": "" }, "require": { - "php": "^8.3", + "php": "^8.1", "symfony/filesystem": "^6.4 || ^7.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4", "ergebnis/composer-normalize": "^2.28", "infection/infection": ">=0.26", - "phpunit/phpunit": "^12", + "phpunit/phpunit": "^10.3", "symfony/finder": "^6.4 || ^7.0" }, "type": "library", @@ -148,7 +148,7 @@ ], "support": { "issues": "https://github.com/theofidry/filesystem/issues", - "source": "https://github.com/theofidry/filesystem/tree/1.3.0" + "source": "https://github.com/theofidry/filesystem/tree/1.2.3" }, "funding": [ { @@ -156,31 +156,33 @@ "type": "github" } ], - "time": "2025-02-13T23:05:19+00:00" + "time": "2025-02-13T22:58:51+00:00" }, { "name": "humbug/php-scoper", - "version": "0.18.18", + "version": "0.18.19", "source": { "type": "git", "url": "https://github.com/humbug/php-scoper.git", - "reference": "dd55d01a937602c9473cfbe0ecab9521cb9740aa" + "reference": "518d551ad5d6996c69faf6b49ed692b5ed816bdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/humbug/php-scoper/zipball/dd55d01a937602c9473cfbe0ecab9521cb9740aa", - "reference": "dd55d01a937602c9473cfbe0ecab9521cb9740aa", + "url": "https://api.github.com/repos/humbug/php-scoper/zipball/518d551ad5d6996c69faf6b49ed692b5ed816bdc", + "reference": "518d551ad5d6996c69faf6b49ed692b5ed816bdc", "shasum": "" }, "require": { "fidry/console": "^0.6.10", "fidry/filesystem": "^1.1", - "jetbrains/phpstorm-stubs": "^2024.1", + "jetbrains/phpstorm-stubs": "dev-master", "nikic/php-parser": "^5.0", "php": "^8.2", - "symfony/console": "^6.4 || ^7.0", - "symfony/filesystem": "^6.4 || ^7.0", - "symfony/finder": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.4", + "symfony/filesystem": "^6.4 || ^7.4", + "symfony/finder": "^6.4 || ^7.4", + "symfony/polyfill-iconv": "^1.33", + "symfony/polyfill-mbstring": "^1.33", "symfony/var-dumper": "^7.1", "thecodingmachine/safe": "^3.0" }, @@ -191,7 +193,7 @@ "humbug/box": "^4.6.2", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^10.0 || ^11.0", - "symfony/yaml": "^6.4 || ^7.0" + "symfony/yaml": "^6.4 || ^7.4" }, "bin": [ "bin/php-scoper" @@ -238,30 +240,31 @@ "description": "Prefixes all PHP namespaces in a file or directory.", "support": { "issues": "https://github.com/humbug/php-scoper/issues", - "source": "https://github.com/humbug/php-scoper/tree/0.18.18" + "source": "https://github.com/humbug/php-scoper/tree/0.18.19" }, - "time": "2025-10-15T15:29:47+00:00" + "time": "2026-03-02T11:06:51+00:00" }, { "name": "jetbrains/phpstorm-stubs", - "version": "v2024.3", + "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c" + "url": "https://github.com/JetBrains/phpstorm-stubs", + "reference": "748ab87d16253a5b5d648b5fe4dae1ff4152bb03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", - "reference": "0e82bdfe850c71857ee4ee3501ed82a9fc5d043c", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/748ab87d16253a5b5d648b5fe4dae1ff4152bb03", + "reference": "748ab87d16253a5b5d648b5fe4dae1ff4152bb03", "shasum": "" }, "require-dev": { - "friendsofphp/php-cs-fixer": "v3.64.0", - "nikic/php-parser": "v5.3.1", - "phpdocumentor/reflection-docblock": "5.6.0", - "phpunit/phpunit": "11.4.3" + "friendsofphp/php-cs-fixer": "^v3.86", + "nikic/php-parser": "^v5.6", + "phpdocumentor/reflection-docblock": "^6.0", + "phpunit/phpunit": "^13.2" }, + "default-branch": true, "type": "library", "autoload": { "files": [ @@ -284,10 +287,7 @@ "stubs", "type" ], - "support": { - "source": "https://github.com/JetBrains/phpstorm-stubs/tree/v2024.3" - }, - "time": "2024-12-14T08:03:12+00:00" + "time": "2026-08-22T14:29:17+00:00" }, { "name": "nikic/php-parser", @@ -969,6 +969,90 @@ ], "time": "2026-04-10T16:19:22+00:00" }, + { + "name": "symfony/polyfill-iconv", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "2c5729fd241b4b22f6e4b436bc3354a4f262df57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/2c5729fd241b4b22f6e4b436bc3354a4f262df57", + "reference": "2c5729fd241b4b22f6e4b436bc3354a4f262df57", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-iconv": "*" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Iconv\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Iconv extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "iconv", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, { "name": "symfony/polyfill-intl-grapheme", "version": "v1.41.0", @@ -1390,34 +1474,35 @@ }, { "name": "symfony/string", - "version": "v8.1.2", + "version": "v7.4.15", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" + "reference": "e394af32256bf9e7bf80849d95e589167c10097b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", - "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b", + "reference": "e394af32256bf9e7bf80849d95e589167c10097b", "shasum": "" }, "require": { - "php": ">=8.4.1", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-intl-grapheme": "^1.33", - "symfony/polyfill-intl-normalizer": "^1.0", - "symfony/polyfill-mbstring": "^1.0" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.4|^8.0", - "symfony/http-client": "^7.4|^8.0", - "symfony/intl": "^7.4|^8.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^7.4|^8.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -1456,7 +1541,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.1.2" + "source": "https://github.com/symfony/string/tree/v7.4.15" }, "funding": [ { @@ -1476,7 +1561,7 @@ "type": "tidelift" } ], - "time": "2026-07-28T07:35:25+00:00" + "time": "2026-07-28T07:33:02+00:00" }, { "name": "symfony/var-dumper", @@ -1769,13 +1854,16 @@ ], "packages-dev": [], "aliases": [], - "minimum-stability": "stable", + "minimum-stability": "dev", "stability-flags": {}, - "prefer-stable": false, + "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=8.2" }, "platform-dev": {}, + "platform-overrides": { + "php": "8.2.0" + }, "plugin-api-version": "2.6.0" } From 07b71946a7b01f1d30f79647559cac60cc59e010 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 16:55:14 +0000 Subject: [PATCH 9/9] Ship all of composer/composer and keep the linter out of generated trees `wp package install` fataled with "Class Composer\Console\GithubActionError not found" whenever dependency resolution failed: Composer\Installer instantiates that class on every failure path. The scoper configuration had carried over make-phar.php's exclusions of Composer's Command, Console and Pear code, but those exclusions never actually applied on main: Finder's exclude() takes paths relative to the in() roots, and the ones listed are relative to vendor/ instead, so main's Phar has always shipped those files. Drop the exclusions so the prefixed tree matches what the Phar really contained before. The reusable code-quality workflow runs parallel-lint over the whole checkout with --show-deprecated, which now walks the php-scoper toolchain (whose IDE stubs redeclare PHP's own functions) and third_party/. Pass both through its parallel-lint-excludes input. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BrV8jGVsfiypo6buRmZfug --- .github/workflows/code-quality.yml | 6 ++++++ utils/scoper/scoper.inc.php | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index e9fe57761..944188594 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -12,3 +12,9 @@ on: jobs: code-quality: uses: wp-cli/.github/.github/workflows/reusable-code-quality.yml@main + with: + # Generated by the post-install-cmd hook (utils/prefix-dependencies.php): + # prefixed copies of third-party packages, and the php-scoper toolchain. + parallel-lint-excludes: | + third_party + utils/scoper/vendor diff --git a/utils/scoper/scoper.inc.php b/utils/scoper/scoper.inc.php index 19de78b30..1930a5302 100644 --- a/utils/scoper/scoper.inc.php +++ b/utils/scoper/scoper.inc.php @@ -52,10 +52,6 @@ ->ignoreVCS( true ) ->name( '*.php' ) ->exclude( [ 'test', 'tests', 'Test', 'Tests' ] ) - // Parts of Composer the Phar never needed. Whatever is left out here - // also stays out of the generated classmap. - ->notPath( '#^src/Composer/(?:Command|Console|Question|SelfUpdate|Installer/Pear|Repository/Pear)/#' ) - ->notPath( '#^src/Composer/(?:Compiler|Downloader/PearPackageExtractor|Installer/PearBinaryInstaller|Installer/PearInstaller)\.php$#' ) ->in( array_column( $packages, 'path' ) ), // Non-PHP files Composer reads at runtime; copied unchanged.