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; + }, + ], +];