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/.github/workflows/deployment.yml b/.github/workflows/deployment.yml
index c832f39d4..814200757 100644
--- a/.github/workflows/deployment.yml
+++ b/.github/workflows/deployment.yml
@@ -60,6 +60,16 @@ jobs:
name: manifest
path: vendor/wp-cli/wp-cli/manifest.json
+ # 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
@@ -118,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
new file mode 100644
index 000000000..828808265
--- /dev/null
+++ b/features/dependency-isolation.feature
@@ -0,0 +1,98 @@
+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. The Phar therefore ships Composer's dependency tree
+ # under the `WP_CLI\Vendor` prefix instead, see utils/prefix-dependencies.php.
+ #
+ # 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 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
+ # 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:
+ """
+
.
+
+ */third_party/*
+
@@ -46,16 +49,22 @@
#############################################################################
-->
-
*/utils/get-package-require-from-composer\.php$
*/utils/make-phar\.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/prefix-dependencies\.php$
+ */utils/scoper/prefixed-packages\.php$
+ */utils/scoper/scoper\.inc\.php$
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index a1f53a4d5..0f888c81e 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -3,6 +3,13 @@ parameters:
paths:
- php
- utils
+ excludePaths:
+ analyse:
+ # 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 8aee6a586..adb3529bc 100644
--- a/utils/make-phar.php
+++ b/utils/make-phar.php
@@ -38,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'] ) ) {
@@ -54,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 );
@@ -62,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 );
}
@@ -200,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' )
@@ -216,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
@@ -235,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' );
+ }
}
}
@@ -264,6 +313,32 @@ 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();
$finder
@@ -304,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..06d5a5725
--- /dev/null
+++ b/utils/prefix-dependencies.php
@@ -0,0 +1,437 @@
+= 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 ) ) );
+
+/*
+ * 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',
+ '--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/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..2e14152ee
--- /dev/null
+++ b/utils/scoper/composer.json
@@ -0,0 +1,18 @@
+{
+ "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": {
+ "php": ">=8.2",
+ "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
new file mode 100644
index 000000000..c34069fda
--- /dev/null
+++ b/utils/scoper/composer.lock
@@ -0,0 +1,1869 @@
+{
+ "_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": "f82afeff84c771d44a995ead31c23fbd",
+ "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.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theofidry/filesystem.git",
+ "reference": "3e1f9cac40f807b7c4196013ab77cc1b9416e3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theofidry/filesystem/zipball/3e1f9cac40f807b7c4196013ab77cc1b9416e3e5",
+ "reference": "3e1f9cac40f807b7c4196013ab77cc1b9416e3e5",
+ "shasum": ""
+ },
+ "require": {
+ "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": "^10.3",
+ "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.2.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theofidry",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-13T22:58:51+00:00"
+ },
+ {
+ "name": "humbug/php-scoper",
+ "version": "0.18.19",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/humbug/php-scoper.git",
+ "reference": "518d551ad5d6996c69faf6b49ed692b5ed816bdc"
+ },
+ "dist": {
+ "type": "zip",
+ "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": "dev-master",
+ "nikic/php-parser": "^5.0",
+ "php": "^8.2",
+ "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"
+ },
+ "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.4"
+ },
+ "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.19"
+ },
+ "time": "2026-03-02T11:06:51+00:00"
+ },
+ {
+ "name": "jetbrains/phpstorm-stubs",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/JetBrains/phpstorm-stubs",
+ "reference": "748ab87d16253a5b5d648b5fe4dae1ff4152bb03"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/748ab87d16253a5b5d648b5fe4dae1ff4152bb03",
+ "reference": "748ab87d16253a5b5d648b5fe4dae1ff4152bb03",
+ "shasum": ""
+ },
+ "require-dev": {
+ "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": [
+ "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"
+ ],
+ "time": "2026-08-22T14:29:17+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-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",
+ "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": "v7.4.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "e394af32256bf9e7bf80849d95e589167c10097b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b",
+ "reference": "e394af32256bf9e7bf80849d95e589167c10097b",
+ "shasum": ""
+ },
+ "require": {
+ "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.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": "^6.4|^7.0|^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/v7.4.15"
+ },
+ "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:33:02+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": "dev",
+ "stability-flags": {},
+ "prefer-stable": true,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=8.2"
+ },
+ "platform-dev": {},
+ "platform-overrides": {
+ "php": "8.2.0"
+ },
+ "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
new file mode 100644
index 000000000..1930a5302
--- /dev/null
+++ b/utils/scoper/scoper.inc.php
@@ -0,0 +1,106 @@
+ WP_CLI_VENDOR_PREFIX,
+
+ 'finders' => [
+ Finder::create()
+ ->files()
+ ->ignoreVCS( true )
+ ->name( '*.php' )
+ ->exclude( [ 'test', 'tests', 'Test', 'Tests' ] )
+ ->in( array_column( $packages, 'path' ) ),
+
+ // 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',
+ ]
+ ),
+ ],
+
+ // 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' => [
+ /*
+ * 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 ( $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;
+ },
+ ],
+];