diff --git a/Makefile b/Makefile index 617c3d657..5ad0d2549 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,11 @@ INFECTION_SRC := $(shell find src tests) phpunit.xml.dist PHP_CS_FIXER_BIN = vendor-bin/php-cs-fixer/vendor/bin/php-cs-fixer PHP_CS_FIXER = $(PHP_CS_FIXER_BIN) +PHPBENCH_BIN = vendor-bin/phpbench/vendor/bin/phpbench +PHPBENCH = $(PHPBENCH_BIN) +PHPBENCH_WITH_COMPACTORS_VENDOR_DIR = fixtures/bench/with-compactors/vendor +PHPBENCH_WITHOUT_COMPACTORS_VENDOR_DIR = fixtures/bench/without-compactors/vendor + WEBSITE_SRC := mkdocs.yaml $(shell find doc) # This is defined in mkdocs.yaml#site_dir WEBSITE_OUTPUT = dist/website @@ -249,6 +254,15 @@ _infection: $(INFECTION_BIN) $(COVERAGE_XML_DIR) $(COVERAGE_JUNIT) vendor _infection_ci: $(INFECTION_BIN) $(COVERAGE_XML_DIR) $(COVERAGE_JUNIT) vendor $(INFECTION_CI) +.PHONY: phpbench +phpbench: ## Runs PHPBench +phpbench: $(PHPBENCH_BIN) $(PHPBENCH_WITH_COMPACTORS_VENDOR_DIR) $(PHPBENCH_WITHOUT_COMPACTORS_VENDOR_DIR) + $(MAKE) _phpbench + +.PHONY: _phpbench +_phpbench: + $(PHPBENCH) run tests/Benchmark --report=default + #--------------------------------------------------------------------------- @@ -370,6 +384,18 @@ vendor-bin/php-cs-fixer/composer.lock: vendor-bin/php-cs-fixer/composer.json @echo "$(ERROR_COLOR)$(@) is not up to date. You may want to run the following command:$(NO_COLOR)" @echo "$$ composer bin php-cs-fixer update --lock && touch -c $(@)" +.PHONY: phpbench_install +phpbench_install: $(PHPBENCH_BIN) + +$(PHPBENCH_BIN): vendor-bin/phpbench/vendor + touch -c $@ +vendor-bin/phpbench/vendor: vendor-bin/phpbench/composer.lock $(COMPOSER_BIN_PLUGIN_VENDOR) + composer bin phpbench install + touch -c $@ +vendor-bin/phpbench/composer.lock: vendor-bin/phpbench/composer.json + @echo "$(ERROR_COLOR)$(@) is not up to date. You may want to run the following command:$(NO_COLOR)" + @echo "$$ composer bin phpbench update --lock && touch -c $(@)" + .PHONY: infection_install infection_install: $(INFECTION_BIN) @@ -457,6 +483,14 @@ vendor-bin/requirement-checker/composer.lock: vendor-bin/requirement-checker/com @echo "$(ERROR_COLOR)$(@) is not up to date. You may want to run the following command:$(NO_COLOR)" @echo "$$ composer bin requirement-checker update --lock && touch -c $(@)" +$(PHPBENCH_WITH_COMPACTORS_VENDOR_DIR): + composer install --working-dir=$$(dirname $@) + touch -c $@ + +$(PHPBENCH_WITHOUT_COMPACTORS_VENDOR_DIR): + composer install --working-dir=$$(dirname $@) + touch -c $@ + dist: mkdir -p dist touch dist/.gitkeep diff --git a/fixtures/bench/with-compactors/.gitignore b/fixtures/bench/with-compactors/.gitignore new file mode 100644 index 000000000..2a37274e3 --- /dev/null +++ b/fixtures/bench/with-compactors/.gitignore @@ -0,0 +1,2 @@ +/.box_dump/ +/vendor/ diff --git a/fixtures/bench/with-compactors/box.json.dist b/fixtures/bench/with-compactors/box.json.dist new file mode 100644 index 000000000..9904ada17 --- /dev/null +++ b/fixtures/bench/with-compactors/box.json.dist @@ -0,0 +1,12 @@ +{ + "$schema": "../../../res/schema.json", + + "main": "box.php", + "output": "../../../dist/bench/box.phar", + + "directories-bin": ["../../../res/requirement-checker"], + "force-autodiscovery": true, + + "dump-autoload": false, + "datetime": "release-date" +} diff --git a/fixtures/bench/with-compactors/box.php b/fixtures/bench/with-compactors/box.php new file mode 100755 index 000000000..98d6c6c43 --- /dev/null +++ b/fixtures/bench/with-compactors/box.php @@ -0,0 +1,60 @@ +#!/usr/bin/env php + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Fidry\Console\Application\ApplicationRunner; +use Fidry\Console\IO; +use KevinGH\Box\Console\Application; +use KevinGH\Box\Console\OutputFormatterConfigurator; +use RuntimeException; +use function file_exists; +use function in_array; +use const PHP_EOL; +use const PHP_SAPI; + +// See https://github.com/easysoft/phpmicro for the micro SAPI. +if (false === in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed', 'micro'], true)) { + echo PHP_EOL.'Box may only be invoked from a command line, got "'.PHP_SAPI.'"'.PHP_EOL; + + exit(1); +} + +(static function (): void { + if (file_exists($autoload = __DIR__.'/../../../autoload.php')) { + // Is installed via Composer + include_once $autoload; + + return; + } + + if (file_exists($autoload = __DIR__.'/../vendor/autoload.php')) { + // Is installed locally + include_once $autoload; + + return; + } + + throw new RuntimeException('Unable to find the Composer autoloader.'); +})(); + +register_aliases(); +register_error_handler(); + +$io = IO::createDefault(); +OutputFormatterConfigurator::configure($io); + +$runner = new ApplicationRunner(new Application()); +$runner->run($io); diff --git a/fixtures/bench/with-compactors/composer.json b/fixtures/bench/with-compactors/composer.json new file mode 100644 index 000000000..d4b8722c6 --- /dev/null +++ b/fixtures/bench/with-compactors/composer.json @@ -0,0 +1,114 @@ +{ + "name": "humbug/box", + "description": "Fast, zero config application bundler with PHARs.", + "license": "MIT", + "keywords": [ + "phar" + ], + "authors": [ + { + "name": "Kevin Herrera", + "email": "kevin@herrera.io", + "homepage": "http://kevin.herrera.io" + }, + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + } + ], + "require": { + "php": "^8.2", + "ext-iconv": "*", + "ext-mbstring": "*", + "ext-phar": "*", + "composer-plugin-api": "^2.2", + "amphp/parallel-functions": "^1.1", + "composer/semver": "^3.3.2", + "composer/xdebug-handler": "^3.0.3", + "fidry/console": "^0.6.0", + "fidry/filesystem": "^1.1", + "humbug/php-scoper": "^0.18.6", + "justinrainbow/json-schema": "^5.2.12", + "laravel/serializable-closure": "^1.2.2", + "nikic/iter": "^2.2", + "nikic/php-parser": "^4.15.2", + "phpdocumentor/reflection-docblock": "^5.3", + "phpdocumentor/type-resolver": "^1.7", + "psr/log": "^3.0", + "sebastian/diff": "^4.0", + "seld/jsonlint": "^1.9", + "seld/phar-utils": "^1.2", + "symfony/filesystem": "^6.1.5", + "symfony/finder": "^6.1.3", + "symfony/polyfill-iconv": "^1.28", + "symfony/polyfill-mbstring": "^1.28", + "symfony/process": "^6.1.3", + "symfony/var-dumper": "^6.1.6", + "webmozart/assert": "^1.11" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ergebnis/composer-normalize": "^2.29", + "fidry/makefile": "^1.0.1", + "mikey179/vfsstream": "^1.6.11", + "phpspec/prophecy": "^1.17", + "phpspec/prophecy-phpunit": "^2.0.2", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^6.1.6", + "symfony/yaml": "^6.2", + "webmozarts/strict-phpunit": "^7.6" + }, + "replace": { + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*" + }, + "suggest": { + "ext-openssl": "To accelerate private key generation." + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "KevinGH\\Box\\": "src" + }, + "files": [ + "src/functions.php" + ], + "exclude-from-classmap": [ + "/Test/", + "vendor/humbug/php-scoper/vendor-hotfix" + ] + }, + "autoload-dev": { + "psr-4": { + "KevinGH\\Box\\": [ + "fixtures", + "tests" + ] + } + }, + "bin": [ + "bin/box" + ], + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true, + "composer/package-versions-deprecated": false, + "ergebnis/composer-normalize": true + }, + "platform": { + "php": "8.2" + }, + "platform-check": false, + "sort-packages": true + }, + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "4.x-dev" + } + } +} diff --git a/fixtures/bench/with-compactors/composer.lock b/fixtures/bench/with-compactors/composer.lock new file mode 100644 index 000000000..7dc128640 --- /dev/null +++ b/fixtures/bench/with-compactors/composer.lock @@ -0,0 +1,5770 @@ +{ + "_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": "3cba203db0b518d2e4a789aca01b9887", + "packages": [ + { + "name": "amphp/amp", + "version": "v2.6.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", + "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^7 | ^8 | ^9", + "psalm/phar": "^3.11@dev", + "react/promise": "^2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "files": [ + "lib/functions.php", + "lib/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.6.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2022-02-20T17:52:18+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v1.8.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.4", + "friendsofphp/php-cs-fixer": "^2.3", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^6 || ^7 || ^8", + "psalm/phar": "^3.11.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "http://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2021-03-30T17:13:30+00:00" + }, + { + "name": "amphp/parallel", + "version": "v1.4.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/parallel.git", + "reference": "3aac213ba7858566fd83d38ccb85b91b2d652cb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parallel/zipball/3aac213ba7858566fd83d38ccb85b91b2d652cb0", + "reference": "3aac213ba7858566fd83d38ccb85b91b2d652cb0", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "amphp/byte-stream": "^1.6.1", + "amphp/parser": "^1", + "amphp/process": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^1.0.1", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.1", + "phpunit/phpunit": "^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Context/functions.php", + "lib/Sync/functions.php", + "lib/Worker/functions.php" + ], + "psr-4": { + "Amp\\Parallel\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", + "keywords": [ + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" + ], + "support": { + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v1.4.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2023-03-23T08:04:23+00:00" + }, + { + "name": "amphp/parallel-functions", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/parallel-functions.git", + "reference": "04e92fcacfc921a56dfe12c23b3265e62593a7cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parallel-functions/zipball/04e92fcacfc921a56dfe12c23b3265e62593a7cb", + "reference": "04e92fcacfc921a56dfe12c23b3265e62593a7cb", + "shasum": "" + }, + "require": { + "amphp/amp": "^2.0.3", + "amphp/parallel": "^1.4", + "amphp/serialization": "^1.0", + "laravel/serializable-closure": "^1.0", + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "v2.x-dev", + "amphp/phpunit-util": "^2.0", + "phpunit/phpunit": "^9.5.11" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\ParallelFunctions\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Parallel processing made simple.", + "support": { + "issues": "https://github.com/amphp/parallel-functions/issues", + "source": "https://github.com/amphp/parallel-functions/tree/v1.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2022-02-03T19:32:41+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "ff1de4144726c5dad5fab97f66692ebe8de3e151" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/ff1de4144726c5dad5fab97f66692ebe8de3e151", + "reference": "ff1de4144726c5dad5fab97f66692ebe8de3e151", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2022-12-30T18:08:47+00:00" + }, + { + "name": "amphp/process", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "76e9495fd6818b43a20167cb11d8a67f7744ee0f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/76e9495fd6818b43a20167cb11d8a67f7744ee0f", + "reference": "76e9495fd6818b43a20167cb11d8a67f7744ee0f", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "amphp/byte-stream": "^1.4", + "php": ">=7" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "phpunit/phpunit": "^6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous process manager.", + "homepage": "https://github.com/amphp/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2022-07-06T23:50:12+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/sync", + "version": "v1.4.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "85ab06764f4f36d63b1356b466df6111cf4b89cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/85ab06764f4f36d63b1356b466df6111cf4b89cf", + "reference": "85ab06764f4f36d63b1356b466df6111cf4b89cf", + "shasum": "" + }, + "require": { + "amphp/amp": "^2.2", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.1", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/ConcurrentIterator/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Mutex, Semaphore, and other synchronization tools for Amp.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v1.4.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2021-10-25T18:29:10+00:00" + }, + { + "name": "composer/pcre", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-10-11T07:11:09+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-08-31T09:50:34+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T21:32:43+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.2" + }, + "time": "2023-09-27T20:04:15+00:00" + }, + { + "name": "fidry/console", + "version": "0.6.8", + "source": { + "type": "git", + "url": "https://github.com/theofidry/console.git", + "reference": "f77b0abe54fd6fea6ce8733c98817d82c07b6b8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/console/zipball/f77b0abe54fd6fea6ce8733c98817d82c07b6b8e", + "reference": "f77b0abe54fd6fea6ce8733c98817d82c07b6b8e", + "shasum": "" + }, + "require": { + "php": "^8.2", + "psr/log": "^3.0", + "symfony/console": "^6.3", + "symfony/event-dispatcher-contracts": "^2.5 || ^3.0", + "symfony/service-contracts": "^2.5 || ^3.0", + "thecodingmachine/safe": "^2.0", + "webmozart/assert": "^1.11" + }, + "conflict": { + "symfony/dependency-injection": "<6.3.0", + "symfony/framework-bundle": "<6.3.0", + "symfony/http-kernel": "<6.3.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.27", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^10.2", + "symfony/dependency-injection": "^6.3", + "symfony/flex": "^2.4.0", + "symfony/framework-bundle": "^6.3", + "symfony/http-kernel": "^6.3", + "symfony/yaml": "^6.3" + }, + "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.8" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2023-11-18T22:57:23+00:00" + }, + { + "name": "fidry/filesystem", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/filesystem.git", + "reference": "1dd372ab3eb8b84ffe9578bff576b00c9a44ee46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/filesystem/zipball/1dd372ab3eb8b84ffe9578bff576b00c9a44ee46", + "reference": "1dd372ab3eb8b84ffe9578bff576b00c9a44ee46", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/filesystem": "^6.3", + "thecodingmachine/safe": "^2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "ergebnis/composer-normalize": "^2.28", + "infection/infection": "^0.26", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^10.3", + "symfony/finder": "^6.3", + "symfony/phpunit-bridge": "^6.2" + }, + "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.1.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2023-10-07T07:32:54+00:00" + }, + { + "name": "humbug/php-scoper", + "version": "0.18.9", + "source": { + "type": "git", + "url": "https://github.com/humbug/php-scoper.git", + "reference": "dd0a6721c38a26e18c1e36c0a718b6262350dd96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/humbug/php-scoper/zipball/dd0a6721c38a26e18c1e36c0a718b6262350dd96", + "reference": "dd0a6721c38a26e18c1e36c0a718b6262350dd96", + "shasum": "" + }, + "require": { + "fidry/console": "^0.6.6", + "fidry/filesystem": "^1.1", + "jetbrains/phpstorm-stubs": "^v2022.2", + "nikic/php-parser": "^4.12", + "php": "^8.2", + "symfony/console": "^5.2 || ^6.0", + "symfony/filesystem": "^5.2 || ^6.0", + "symfony/finder": "^5.2 || ^6.0", + "thecodingmachine/safe": "^2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.1", + "ergebnis/composer-normalize": "^2.28", + "fidry/makefile": "^1.0", + "humbug/box": "^4.5.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.0", + "symfony/yaml": "^6.1" + }, + "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.9" + }, + "time": "2023-11-21T21:00:48+00:00" + }, + { + "name": "jetbrains/phpstorm-stubs", + "version": "v2022.3", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-stubs.git", + "reference": "6b568c153cea002dc6fad96285c3063d07cab18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/6b568c153cea002dc6fad96285c3063d07cab18d", + "reference": "6b568c153cea002dc6fad96285c3063d07cab18d", + "shasum": "" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "@stable", + "nikic/php-parser": "@stable", + "php": "^8.0", + "phpdocumentor/reflection-docblock": "@stable", + "phpunit/phpunit": "@stable" + }, + "type": "library", + "autoload": { + "files": [ + "PhpStormStubsMap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "PHP runtime & extensions header files for PhpStorm", + "homepage": "https://www.jetbrains.com/phpstorm", + "keywords": [ + "autocomplete", + "code", + "inference", + "inspection", + "jetbrains", + "phpstorm", + "stubs", + "type" + ], + "support": { + "source": "https://github.com/JetBrains/phpstorm-stubs/tree/v2022.3" + }, + "time": "2022-10-17T09:21:37+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "v5.2.13", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13" + }, + "time": "2023-09-26T02:20:38+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v1.3.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "3dbf8a8e914634c48d389c1234552666b3d43754" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3dbf8a8e914634c48d389c1234552666b3d43754", + "reference": "3dbf8a8e914634c48d389c1234552666b3d43754", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "nesbot/carbon": "^2.61", + "pestphp/pest": "^1.21.3", + "phpstan/phpstan": "^1.8.2", + "symfony/var-dumper": "^5.4.11" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2023-11-08T14:08:06+00:00" + }, + { + "name": "nikic/iter", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/iter.git", + "reference": "d9f88bc04b5b453914373e70c041353d8e67c3f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/iter/zipball/d9f88bc04b5b453914373e70c041353d8e67c3f5", + "reference": "d9f88bc04b5b453914373e70c041353d8e67c3f5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "vimeo/psalm": "^4.18 || ^5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/iter.func.php", + "src/iter.php", + "src/iter.rewindable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Iteration primitives using generators", + "keywords": [ + "functional", + "generator", + "iterator" + ], + "support": { + "issues": "https://github.com/nikic/iter/issues", + "source": "https://github.com/nikic/iter/tree/v2.3.0" + }, + "time": "2023-07-25T19:55:40+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.17.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-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/v4.17.1" + }, + "time": "2023-08-13T19:53:39+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + }, + "time": "2021-10-19T17:43:47+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", + "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.3" + }, + "time": "2023-08-12T11:01:26+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.24.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "12f01d214f1c73b9c91fdb3b1c415e4c70652083" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/12f01d214f1c73b9c91fdb3b1c415e4c70652083", + "reference": "12f01d214f1c73b9c91fdb3b1c415e4c70652083", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.3" + }, + "time": "2023-11-18T20:15:32+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.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "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.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-05-07T05:35:17+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "594fd6462aad8ecee0b45ca5045acea4776667f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/594fd6462aad8ecee0b45ca5045acea4776667f1", + "reference": "594fd6462aad8ecee0b45ca5045acea4776667f1", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2023-05-11T13:16:46+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phar" + ], + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1" + }, + "time": "2022-08-31T10:31:18+00:00" + }, + { + "name": "symfony/console", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0d14a9f6d04d4ac38a8cea1171f4554e325dae92", + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.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/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-10-31T08:09:35+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "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.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "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.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "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/v6.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-06-01T08:30:39+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/a1b31d88c0e998168ca7792f222cbecee47428c4", + "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.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/v6.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-09-26T12:56:25+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-iconv", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "6de50471469b8c9afc38164452ab2b6170ee71c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/6de50471469b8c9afc38164452ab2b6170ee71c1", + "reference": "6de50471469b8c9afc38164452ab2b6170ee71c1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-iconv": "*" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "875e90aeea2777b6f135677f618529449334a612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-28T09:04:16+00:00" + }, + { + "name": "symfony/process", + "version": "v6.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54", + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "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": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-07T10:39:22+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/b3313c2dbffaf71c8de2934e2ea56ed2291a3838", + "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "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.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-30T20:28:31+00:00" + }, + { + "name": "symfony/string", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "13880a87790c76ef994c91e87efb96134522577a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/13880a87790c76ef994c91e87efb96134522577a", + "reference": "13880a87790c76ef994c91e87efb96134522577a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/intl": "^6.2", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.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/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-09T08:28:21+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "81acabba9046550e89634876ca64bfcd3c06aa0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/81acabba9046550e89634876ca64bfcd3c06aa0a", + "reference": "81acabba9046550e89634876ca64bfcd3c06aa0a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "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/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-08T10:42:36+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.2", + "thecodingmachine/phpstan-strict-rules": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "files": [ + "deprecated/apc.php", + "deprecated/array.php", + "deprecated/datetime.php", + "deprecated/libevent.php", + "deprecated/misc.php", + "deprecated/password.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "deprecated/strings.php", + "lib/special_cases.php", + "deprecated/mysqli.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/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/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/", + "deprecated/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/v2.5.0" + }, + "time": "2023-04-05T11:54:14+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "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.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "packages-dev": [ + { + "name": "bamarni/composer-bin-plugin", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/bamarni/composer-bin-plugin.git", + "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880", + "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "ext-json": "*", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Bamarni\\Composer\\Bin\\BamarniBinPlugin" + }, + "autoload": { + "psr-4": { + "Bamarni\\Composer\\Bin\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "No conflicts for your bin dependencies", + "keywords": [ + "composer", + "conflict", + "dependency", + "executable", + "isolation", + "tool" + ], + "support": { + "issues": "https://github.com/bamarni/composer-bin-plugin/issues", + "source": "https://github.com/bamarni/composer-bin-plugin/tree/1.8.2" + }, + "time": "2022-10-31T08:38:03+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "ergebnis/composer-normalize", + "version": "2.39.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/composer-normalize.git", + "reference": "a878360bc8cb5cb440b9381f72b0aaa125f937c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/a878360bc8cb5cb440b9381f72b0aaa125f937c7", + "reference": "a878360bc8cb5cb440b9381f72b0aaa125f937c7", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "ergebnis/json": "^1.1.0", + "ergebnis/json-normalizer": "^4.3.0", + "ergebnis/json-printer": "^3.4.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12", + "localheinz/diff": "^1.1.1", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "composer/composer": "^2.6.5", + "ergebnis/license": "^2.2.0", + "ergebnis/php-cs-fixer-config": "~6.7.0", + "ergebnis/phpunit-slow-test-detector": "^2.3.0", + "fakerphp/faker": "^1.23.0", + "infection/infection": "~0.27.4", + "phpunit/phpunit": "^10.4.1", + "psalm/plugin-phpunit": "~0.18.4", + "rector/rector": "~0.18.5", + "symfony/filesystem": "^6.0.13", + "vimeo/psalm": "^5.15.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Ergebnis\\Composer\\Normalize\\NormalizePlugin", + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + }, + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Ergebnis\\Composer\\Normalize\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a composer plugin for normalizing composer.json.", + "homepage": "https://github.com/ergebnis/composer-normalize", + "keywords": [ + "composer", + "normalize", + "normalizer", + "plugin" + ], + "support": { + "issues": "https://github.com/ergebnis/composer-normalize/issues", + "security": "https://github.com/ergebnis/composer-normalize/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/composer-normalize" + }, + "time": "2023-10-10T15:43:27+00:00" + }, + { + "name": "ergebnis/json", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json.git", + "reference": "9f2b9086c43b189d7044a5b6215a931fb6e9125d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json/zipball/9f2b9086c43b189d7044a5b6215a931fb6e9125d", + "reference": "9f2b9086c43b189d7044a5b6215a931fb6e9125d", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.29.0", + "ergebnis/data-provider": "^3.0.0", + "ergebnis/license": "^2.2.0", + "ergebnis/php-cs-fixer-config": "^6.6.0", + "ergebnis/phpunit-slow-test-detector": "^2.3.0", + "fakerphp/faker": "^1.23.0", + "infection/infection": "~0.27.4", + "phpunit/phpunit": "^10.4.1", + "psalm/plugin-phpunit": "~0.18.4", + "rector/rector": "~0.18.5", + "vimeo/psalm": "^5.15.0" + }, + "type": "library", + "extra": { + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a Json value object for representing a valid JSON string.", + "homepage": "https://github.com/ergebnis/json", + "keywords": [ + "json" + ], + "support": { + "issues": "https://github.com/ergebnis/json/issues", + "security": "https://github.com/ergebnis/json/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json" + }, + "time": "2023-10-10T07:57:48+00:00" + }, + { + "name": "ergebnis/json-normalizer", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-normalizer.git", + "reference": "716fa0a5dcc75fbcb2c1c2e0542b2f56732460bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-normalizer/zipball/716fa0a5dcc75fbcb2c1c2e0542b2f56732460bd", + "reference": "716fa0a5dcc75fbcb2c1c2e0542b2f56732460bd", + "shasum": "" + }, + "require": { + "ergebnis/json": "^1.1.0", + "ergebnis/json-pointer": "^3.2.0", + "ergebnis/json-printer": "^3.4.0", + "ergebnis/json-schema-validator": "^4.1.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "composer/semver": "^3.4.0", + "ergebnis/data-provider": "^3.0.0", + "ergebnis/license": "^2.2.0", + "ergebnis/php-cs-fixer-config": "~6.7.0", + "ergebnis/phpunit-slow-test-detector": "^2.3.0", + "fakerphp/faker": "^1.23.0", + "infection/infection": "~0.27.4", + "phpunit/phpunit": "^10.4.1", + "psalm/plugin-phpunit": "~0.18.4", + "rector/rector": "~0.18.5", + "symfony/filesystem": "^6.3.1", + "symfony/finder": "^6.3.5", + "vimeo/psalm": "^5.15.0" + }, + "suggest": { + "composer/semver": "If you want to use ComposerJsonNormalizer or VersionConstraintNormalizer" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Normalizer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides generic and vendor-specific normalizers for normalizing JSON documents.", + "homepage": "https://github.com/ergebnis/json-normalizer", + "keywords": [ + "json", + "normalizer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-normalizer/issues", + "security": "https://github.com/ergebnis/json-normalizer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-normalizer" + }, + "time": "2023-10-10T15:15:03+00:00" + }, + { + "name": "ergebnis/json-pointer", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-pointer.git", + "reference": "8e517faefc06b7c761eaa041febef51a9375819a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-pointer/zipball/8e517faefc06b7c761eaa041febef51a9375819a", + "reference": "8e517faefc06b7c761eaa041febef51a9375819a", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.29.0", + "ergebnis/data-provider": "^3.0.0", + "ergebnis/license": "^2.2.0", + "ergebnis/php-cs-fixer-config": "~6.7.0", + "ergebnis/phpunit-slow-test-detector": "^2.3.0", + "fakerphp/faker": "^1.23.0", + "infection/infection": "~0.27.4", + "phpunit/phpunit": "^10.4.1", + "psalm/plugin-phpunit": "~0.18.4", + "rector/rector": "~0.18.5", + "vimeo/psalm": "^5.15.0" + }, + "type": "library", + "extra": { + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Pointer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides JSON pointer as a value object.", + "homepage": "https://github.com/ergebnis/json-pointer", + "keywords": [ + "RFC6901", + "json", + "pointer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-pointer/issues", + "security": "https://github.com/ergebnis/json-pointer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-pointer" + }, + "time": "2023-10-10T14:41:06+00:00" + }, + { + "name": "ergebnis/json-printer", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-printer.git", + "reference": "05841593d72499de4f7ce4034a237c77e470558f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-printer/zipball/05841593d72499de4f7ce4034a237c77e470558f", + "reference": "05841593d72499de4f7ce4034a237c77e470558f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "ergebnis/license": "^2.2.0", + "ergebnis/php-cs-fixer-config": "^6.6.0", + "ergebnis/phpunit-slow-test-detector": "^2.3.0", + "fakerphp/faker": "^1.23.0", + "infection/infection": "~0.27.3", + "phpunit/phpunit": "^10.4.1", + "psalm/plugin-phpunit": "~0.18.4", + "rector/rector": "~0.18.5", + "vimeo/psalm": "^5.15.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Printer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a JSON printer, allowing for flexible indentation.", + "homepage": "https://github.com/ergebnis/json-printer", + "keywords": [ + "formatter", + "json", + "printer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-printer/issues", + "security": "https://github.com/ergebnis/json-printer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-printer" + }, + "time": "2023-10-10T07:42:48+00:00" + }, + { + "name": "ergebnis/json-schema-validator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-schema-validator.git", + "reference": "d568ed85d1cdc2e49d650c2fc234dc2516f3f25b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-schema-validator/zipball/d568ed85d1cdc2e49d650c2fc234dc2516f3f25b", + "reference": "d568ed85d1cdc2e49d650c2fc234dc2516f3f25b", + "shasum": "" + }, + "require": { + "ergebnis/json": "^1.0.1", + "ergebnis/json-pointer": "^3.2.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.21.0", + "ergebnis/data-provider": "^3.0.0", + "ergebnis/license": "^2.2.0", + "ergebnis/php-cs-fixer-config": "~6.6.0", + "ergebnis/phpunit-slow-test-detector": "^2.3.0", + "fakerphp/faker": "^1.23.0", + "infection/infection": "~0.27.4", + "phpunit/phpunit": "^10.4.1", + "psalm/plugin-phpunit": "~0.18.4", + "rector/rector": "~0.18.5", + "vimeo/psalm": "^5.15.0" + }, + "type": "library", + "extra": { + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\SchemaValidator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a JSON schema validator, building on top of justinrainbow/json-schema.", + "homepage": "https://github.com/ergebnis/json-schema-validator", + "keywords": [ + "json", + "schema", + "validator" + ], + "support": { + "issues": "https://github.com/ergebnis/json-schema-validator/issues", + "security": "https://github.com/ergebnis/json-schema-validator/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-schema-validator" + }, + "time": "2023-10-10T14:16:57+00:00" + }, + { + "name": "fidry/makefile", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/theofidry/makefile.git", + "reference": "be5a048dcc5648d04e924facce0f85b406d731e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/makefile/zipball/be5a048dcc5648d04e924facce0f85b406d731e4", + "reference": "be5a048dcc5648d04e924facce0f85b406d731e4", + "shasum": "" + }, + "require": { + "php": "^8.1", + "thecodingmachine/safe": "^2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "ergebnis/composer-normalize": "^2.28", + "infection/infection": "^0.26", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fidry\\Makefile\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Utility to parse a Makefile and implement some convention rules.", + "keywords": [ + "Makefile" + ], + "support": { + "issues": "https://github.com/theofidry/makefile/issues", + "source": "https://github.com/theofidry/makefile/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2023-10-13T23:49:14+00:00" + }, + { + "name": "localheinz/diff", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/localheinz/diff.git", + "reference": "851bb20ea8358c86f677f5f111c4ab031b1c764c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/localheinz/diff/zipball/851bb20ea8358c86f677f5f111c4ab031b1c764c", + "reference": "851bb20ea8358c86f677f5f111c4ab031b1c764c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Fork of sebastian/diff for use with ergebnis/composer-normalize", + "homepage": "https://github.com/localheinz/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "source": "https://github.com/localheinz/diff/tree/main" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-06T04:49:32+00:00" + }, + { + "name": "mikey179/vfsstream", + "version": "v1.6.11", + "source": { + "type": "git", + "url": "https://github.com/bovigo/vfsStream.git", + "reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f", + "reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.5|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "support": { + "issues": "https://github.com/bovigo/vfsStream/issues", + "source": "https://github.com/bovigo/vfsStream/tree/master", + "wiki": "https://github.com/bovigo/vfsStream/wiki" + }, + "time": "2022-02-23T02:02:42+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "15873c65b207b07765dbc3c95d20fdf4a320cbe2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/15873c65b207b07765dbc3c95d20fdf4a320cbe2", + "reference": "15873c65b207b07765dbc3c95d20fdf4a320cbe2", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2 || ^2.0", + "php": "^7.2 || 8.0.* || 8.1.* || 8.2.*", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0 || ^7.0", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.17.0" + }, + "time": "2023-02-02T15:41:36+00:00" + }, + { + "name": "phpspec/prophecy-phpunit", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy-phpunit.git", + "reference": "9f26c224a2fa335f33e6666cc078fbf388255e87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy-phpunit/zipball/9f26c224a2fa335f33e6666cc078fbf388255e87", + "reference": "9f26c224a2fa335f33e6666cc078fbf388255e87", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8", + "phpspec/prophecy": "^1.3", + "phpunit/phpunit": "^9.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\PhpUnit\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + } + ], + "description": "Integrating the Prophecy mocking library in PHPUnit test cases", + "homepage": "http://phpspec.net", + "keywords": [ + "phpunit", + "prophecy" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy-phpunit/issues", + "source": "https://github.com/phpspec/prophecy-phpunit/tree/v2.0.2" + }, + "time": "2023-04-18T11:58:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.29", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.15", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-09-19T04:57:46+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.13", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", + "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.28", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2023-09-19T05:39:22+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:03:37+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bde739e7565280bda77be70044ac1047bc007e34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-02T09:26:13+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "symfony/phpunit-bridge", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/phpunit-bridge.git", + "reference": "45610900872a35b77db7698651f36129906041ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/45610900872a35b77db7698651f36129906041ea", + "reference": "45610900872a35b77db7698651f36129906041ea", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "conflict": { + "phpunit/phpunit": "<7.5|9.1.2" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/error-handler": "^5.4|^6.0", + "symfony/polyfill-php81": "^1.27" + }, + "bin": [ + "bin/simple-phpunit" + ], + "type": "symfony-bridge", + "extra": { + "thanks": { + "name": "phpunit/phpunit", + "url": "https://github.com/sebastianbergmann/phpunit" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Bridge\\PhpUnit\\": "" + }, + "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 utilities for PHPUnit, especially user deprecation notices management", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/phpunit-bridge/tree/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-10-31T08:07:48+00:00" + }, + { + "name": "symfony/yaml", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "3493af8a8dad7fa91c77fa473ba23ecd95334a92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/3493af8a8dad7fa91c77fa473ba23ecd95334a92", + "reference": "3493af8a8dad7fa91c77fa473ba23ecd95334a92", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "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": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-06T10:58:05+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + }, + { + "name": "webmozarts/strict-phpunit", + "version": "7.7.7", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/strict-phpunit.git", + "reference": "bec31848d596536af8119a37de61ba23e0599377" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/strict-phpunit/zipball/bec31848d596536af8119a37de61ba23e0599377", + "reference": "bec31848d596536af8119a37de61ba23e0599377", + "shasum": "" + }, + "require": { + "php": ">=7.4.0", + "phpunit/phpunit": "^9.4.3" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.28", + "infection/infection": "^0.26.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozarts\\StrictPHPUnit\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bernhard.schussek@webmozarts.com" + } + ], + "description": "Enables type-safe comparisons of objects in PHPUnit", + "support": { + "issues": "https://github.com/webmozarts/strict-phpunit/issues", + "source": "https://github.com/webmozarts/strict-phpunit/tree/7.7.7" + }, + "time": "2023-03-29T20:04:11+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2", + "ext-iconv": "*", + "ext-mbstring": "*", + "ext-phar": "*", + "composer-plugin-api": "^2.2" + }, + "platform-dev": [], + "platform-overrides": { + "php": "8.2" + }, + "plugin-api-version": "2.6.0" +} diff --git a/fixtures/bench/with-compactors/scoper.inc.php b/fixtures/bench/with-compactors/scoper.inc.php new file mode 100644 index 000000000..86dff528b --- /dev/null +++ b/fixtures/bench/with-compactors/scoper.inc.php @@ -0,0 +1,36 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +use Isolated\Symfony\Component\Finder\Finder as IsolatedFinder; + +return [ + 'exclude-namespaces' => [ + 'Symfony\Polyfill' + ], + 'exclude-classes' => [ + IsolatedFinder::class, + ], + 'exclude-constants' => [ + // Symfony global constants + '/^SYMFONY\_[\p{L}_]+$/', + ], + 'expose-classes' => [ + \Composer\Autoload\ClassLoader::class, + + \KevinGH\Box\Compactor\Compactor::class, + \KevinGH\Box\Compactor\Json::class, + \KevinGH\Box\Compactor\Php::class, + \KevinGH\Box\Compactor\PhpScoper::class, + ], +]; diff --git a/fixtures/bench/with-compactors/src/Amp/FailureCollector.php b/fixtures/bench/with-compactors/src/Amp/FailureCollector.php new file mode 100644 index 000000000..ba8f3efd1 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Amp/FailureCollector.php @@ -0,0 +1,39 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Amp; + +use Amp\MultiReasonException; +use KevinGH\Box\NotInstantiable; +use Throwable; +use function array_map; +use function array_unique; + +final class FailureCollector +{ + use NotInstantiable; + + /** + * @return list + */ + public static function collectReasons(MultiReasonException $exception): array + { + return array_unique( + array_map( + static fn (Throwable $throwable) => $throwable->getMessage(), + $exception->getReasons(), + ), + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Annotation/CompactedFormatter.php b/fixtures/bench/with-compactors/src/Annotation/CompactedFormatter.php new file mode 100644 index 000000000..3aeef0cb4 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Annotation/CompactedFormatter.php @@ -0,0 +1,42 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Annotation; + +use phpDocumentor\Reflection\DocBlock\Tag; +use phpDocumentor\Reflection\DocBlock\Tags\Formatter; +use phpDocumentor\Reflection\DocBlock\Tags\Generic; +use function array_map; +use function explode; +use function implode; + +final class CompactedFormatter implements Formatter +{ + public function format(Tag $tag): string + { + if (!$tag instanceof Generic) { + return trim('@'.$tag->getName()); + } + + $description = (string) $tag; + + if (!str_starts_with($description, '(')) { + return trim('@'.$tag->getName()); + } + + $description = implode('', array_map('trim', explode("\n", (string) $tag))); + + return trim('@'.$tag->getName().$description); + } +} diff --git a/fixtures/bench/with-compactors/src/Annotation/DocblockAnnotationParser.php b/fixtures/bench/with-compactors/src/Annotation/DocblockAnnotationParser.php new file mode 100644 index 000000000..b53fb1cce --- /dev/null +++ b/fixtures/bench/with-compactors/src/Annotation/DocblockAnnotationParser.php @@ -0,0 +1,94 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Annotation; + +use InvalidArgumentException; +use phpDocumentor\Reflection\DocBlock; +use phpDocumentor\Reflection\DocBlock\Tag; +use phpDocumentor\Reflection\DocBlock\Tags\Formatter; +use phpDocumentor\Reflection\DocBlockFactoryInterface; +use function array_filter; +use function array_flip; +use function array_key_exists; +use function array_map; +use function array_values; + +/** + * @private + */ +final class DocblockAnnotationParser +{ + /** + * @var array + */ + private array $ignoredAnnotationsAsKeys; + + /** + * @param string[] $ignoredAnnotations + */ + public function __construct( + private DocBlockFactoryInterface $factory, + private Formatter $tagsFormatter, + array $ignoredAnnotations, + ) { + $this->ignoredAnnotationsAsKeys = array_flip($ignoredAnnotations); + } + + /** + * @return string[] Parsed compacted annotations parsed from the docblock + */ + public function parse(string $docblock): array + { + $doc = $this->createDocBlock($docblock); + + $tags = self::extractTags($doc, $this->ignoredAnnotationsAsKeys); + + return array_map( + fn (Tag $tag) => $tag->render($this->tagsFormatter), + $tags, + ); + } + + private function createDocBlock(string $docblock): DocBlock + { + try { + return $this->factory->create($docblock); + } catch (InvalidArgumentException $invalidDocBlock) { + throw new MalformedTagException( + 'The annotations could not be parsed.', + 0, + $invalidDocBlock, + ); + } + } + + /** + * @param array $ignoredAnnotations + * + * @return list + */ + private static function extractTags(DocBlock $docBlock, array $ignoredAnnotations): array + { + return array_values( + array_filter( + $docBlock->getTags(), + static fn (Tag $tag) => !array_key_exists( + mb_strtolower($tag->getName()), + $ignoredAnnotations, + ), + ), + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Annotation/MalformedTagException.php b/fixtures/bench/with-compactors/src/Annotation/MalformedTagException.php new file mode 100644 index 000000000..29f0898c7 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Annotation/MalformedTagException.php @@ -0,0 +1,24 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Annotation; + +use UnexpectedValueException; + +/** + * @private + */ +final class MalformedTagException extends UnexpectedValueException +{ +} diff --git a/fixtures/bench/with-compactors/src/Box.php b/fixtures/bench/with-compactors/src/Box.php new file mode 100644 index 000000000..39d3fb747 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Box.php @@ -0,0 +1,524 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Amp\MultiReasonException; +use BadMethodCallException; +use Countable; +use DateTimeImmutable; +use Fidry\FileSystem\FS; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\Compactor\Compactors; +use KevinGH\Box\Compactor\PhpScoper; +use KevinGH\Box\Compactor\Placeholder; +use KevinGH\Box\Phar\CompressionAlgorithm; +use KevinGH\Box\Phar\SigningAlgorithm; +use KevinGH\Box\PhpScoper\NullScoper; +use KevinGH\Box\PhpScoper\Scoper; +use Phar; +use RecursiveDirectoryIterator; +use RuntimeException; +use Seld\PharUtils\Timestamps; +use SplFileInfo; +use Webmozart\Assert\Assert; +use function Amp\ParallelFunctions\parallelMap; +use function Amp\Promise\wait; +use function array_filter; +use function array_map; +use function array_unshift; +use function chdir; +use function dirname; +use function extension_loaded; +use function file_exists; +use function getcwd; +use function is_object; +use function openssl_pkey_export; +use function openssl_pkey_get_details; +use function openssl_pkey_get_private; +use function sprintf; + +/** + * Box is a utility class to generate a PHAR. + * + * @private + */ +final class Box implements Countable +{ + private Compactors $compactors; + private Placeholder $placeholderCompactor; + private MapFile $mapFile; + private Scoper $scoper; + private bool $buffering = false; + + /** + * @var array Relative file path as key and file contents as value + */ + private array $bufferedFiles = []; + + private function __construct( + private Phar $phar, + private readonly string $pharFilePath, + private readonly bool $enableParallelization, + ) { + $this->compactors = new Compactors(); + $this->placeholderCompactor = new Placeholder([]); + $this->mapFile = new MapFile(getcwd(), []); + $this->scoper = new NullScoper(); + } + + /** + * Creates a new PHAR and Box instance. + * + * @param string $pharFilePath The PHAR file name + * @param int $pharFlags Flags to pass to the Phar parent class RecursiveDirectoryIterator + * @param string $pharAlias Alias with which the Phar archive should be referred to in calls to stream functionality + * + * @see RecursiveDirectoryIterator + */ + public static function create( + string $pharFilePath, + int $pharFlags = 0, + ?string $pharAlias = null, + bool $enableParallelization = false, + ): self { + // Ensure the parent directory of the PHAR file exists as `new \Phar()` does not create it and would fail + // otherwise. + FS::mkdir(dirname($pharFilePath)); + + return new self( + new Phar($pharFilePath, $pharFlags, $pharAlias), + $pharFilePath, + $enableParallelization, + ); + } + + public function startBuffering(): void + { + Assert::false($this->buffering, 'The buffering must be ended before starting it again'); + + $this->buffering = true; + + $this->phar->startBuffering(); + } + + /** + * @param callable(SymbolsRegistry, string): void $dumpAutoload + */ + public function endBuffering(?callable $dumpAutoload): void + { + Assert::true($this->buffering, 'The buffering must be started before ending it'); + + $dumpAutoload ??= static fn () => null; + $cwd = getcwd(); + + $tmp = FS::makeTmpDir('box', self::class); + chdir($tmp); + + if ([] === $this->bufferedFiles) { + $this->bufferedFiles = [ + '.box_empty' => 'A PHAR cannot be empty so Box adds this file to ensure the PHAR is created still.', + ]; + } + + try { + foreach ($this->bufferedFiles as $file => $contents) { + FS::dumpFile($file, $contents); + } + + if (null !== $dumpAutoload) { + $dumpAutoload( + $this->scoper->getSymbolsRegistry(), + $this->scoper->getPrefix(), + $this->scoper->getExcludedFilePaths(), + ); + } + + chdir($cwd); + + $this->phar->buildFromDirectory($tmp); + } finally { + FS::remove($tmp); + } + + $this->buffering = false; + + $this->phar->stopBuffering(); + } + + /** + * @param non-empty-string $normalizedVendorDir Normalized path ("/" path separator and no trailing "/") to the Composer vendor directory + */ + public function removeComposerArtefacts(string $normalizedVendorDir): void + { + Assert::false($this->buffering, 'The buffering must have ended before removing the Composer artefacts'); + + $composerFiles = [ + 'composer.json', + 'composer.lock', + $normalizedVendorDir.'/composer/installed.json', + ]; + + $this->phar->startBuffering(); + + foreach ($composerFiles as $composerFile) { + $localComposerFile = ($this->mapFile)($composerFile); + + $pharFilePath = sprintf( + 'phar://%s/%s', + $this->phar->getPath(), + $localComposerFile, + ); + + if (file_exists($pharFilePath)) { + $this->phar->delete($localComposerFile); + } + } + + $this->phar->stopBuffering(); + } + + public function compress(CompressionAlgorithm $compressionAlgorithm): ?string + { + Assert::false($this->buffering, 'Cannot compress files while buffering.'); + + $extensionRequired = $compressionAlgorithm->getRequiredExtension(); + + if (null !== $extensionRequired && false === extension_loaded($extensionRequired)) { + throw new RuntimeException( + sprintf( + 'Cannot compress the PHAR with the compression algorithm "%s": the extension "%s" is required but appear to not be loaded', + $compressionAlgorithm->name, + $extensionRequired, + ), + ); + } + + try { + if (CompressionAlgorithm::NONE === $compressionAlgorithm) { + $this->phar->decompressFiles(); + } else { + $this->phar->compressFiles($compressionAlgorithm->value); + } + } catch (BadMethodCallException $exception) { + $exceptionMessage = 'unable to create temporary file' !== $exception->getMessage() + ? 'Could not compress the PHAR: '.$exception->getMessage() + : sprintf( + 'Could not compress the PHAR: the compression requires too many file descriptors to be opened (%s). Check your system limits or install the posix extension to allow Box to automatically configure it during the compression', + $this->phar->count(), + ); + + throw new RuntimeException($exceptionMessage, $exception->getCode(), $exception); + } + + return $extensionRequired; + } + + public function registerCompactors(Compactors $compactors): void + { + $compactorsArray = $compactors->toArray(); + + foreach ($compactorsArray as $index => $compactor) { + if ($compactor instanceof PhpScoper) { + $this->scoper = $compactor->getScoper(); + + continue; + } + + if ($compactor instanceof Placeholder) { + // Removes the known Placeholder compactors in favour of the Box one + unset($compactorsArray[$index]); + } + } + + array_unshift($compactorsArray, $this->placeholderCompactor); + + $this->compactors = new Compactors(...$compactorsArray); + } + + /** + * @param scalar[] $placeholders + */ + public function registerPlaceholders(array $placeholders): void + { + $message = 'Expected value "%s" to be a scalar or stringable object.'; + + foreach ($placeholders as $index => $placeholder) { + if (is_object($placeholder)) { + Assert::methodExists($placeholder, '__toString', $message); + + $placeholders[$index] = (string) $placeholder; + + break; + } + + Assert::scalar($placeholder, $message); + } + + $this->placeholderCompactor = new Placeholder($placeholders); + + $this->registerCompactors($this->compactors); + } + + public function registerFileMapping(MapFile $fileMapper): void + { + $this->mapFile = $fileMapper; + } + + public function registerStub(string $file): void + { + $contents = $this->placeholderCompactor->compact( + $file, + FS::getFileContents($file), + ); + + $this->phar->setStub($contents); + } + + /** + * @param array $files + * + * @throws MultiReasonException + */ + public function addFiles(array $files, bool $binary): void + { + Assert::true($this->buffering, 'Cannot add files if the buffering has not started.'); + + $files = array_map('strval', $files); + + if ($binary) { + foreach ($files as $file) { + $this->addFile($file, null, true); + } + + return; + } + + foreach ($this->processContents($files) as [$file, $contents]) { + $this->bufferedFiles[$file] = $contents; + } + } + + /** + * Adds the a file to the PHAR. The contents will first be compacted and have its placeholders + * replaced. + * + * @param null|string $contents If null the content of the file will be used + * @param bool $binary When true means the file content shouldn't be processed + * + * @return string File local path + */ + public function addFile(string $file, ?string $contents = null, bool $binary = false): string + { + Assert::true($this->buffering, 'Cannot add files if the buffering has not started.'); + + if (null === $contents) { + $contents = FS::getFileContents($file); + } + + $local = ($this->mapFile)($file); + + $this->bufferedFiles[$local] = $binary ? $contents : $this->compactors->compact($local, $contents); + + return $local; + } + + /** + * @internal + */ + public function getPhar(): Phar + { + return $this->phar; + } + + public function setAlias(string $alias): void + { + $aliasWasAdded = $this->phar->setAlias($alias); + + Assert::true( + $aliasWasAdded, + sprintf( + 'The alias "%s" is invalid. See Phar::setAlias() documentation for more information.', + $alias, + ), + ); + } + + public function setStub(string $stub): void + { + $this->phar->setStub($stub); + } + + public function setDefaultStub(string $main): void + { + $this->phar->setDefaultStub($main); + } + + public function setMetadata(mixed $metadata): void + { + $this->phar->setMetadata($metadata); + } + + public function extractTo(string $directory, bool $overwrite = false): void + { + $this->phar->extractTo($directory, overwrite: $overwrite); + } + + public function sign( + SigningAlgorithm $signingAlgorithm, + ?DateTimeImmutable $timestamp = null, + ): void { + if (null === $timestamp) { + $this->phar->setSignatureAlgorithm($signingAlgorithm->value); + + return; + } + + $phar = $this->phar; + $phar->__destruct(); + unset($this->phar); + + $util = new Timestamps($this->pharFilePath); + $util->updateTimestamps($timestamp); + $util->save( + $this->pharFilePath, + $signingAlgorithm->value, + ); + + $this->phar = new Phar($this->pharFilePath); + } + + /** + * Signs the PHAR using a private key file. + * + * @param string $file the private key file name + * @param null|string $password the private key password + */ + public function signUsingFile(string $file, ?string $password = null): void + { + $this->signUsingKey(FS::getFileContents($file), $password); + } + + /** + * Signs the PHAR using a private key. + * + * @param string $key The private key + * @param null|string $password The private key password + */ + public function signUsingKey(string $key, ?string $password): void + { + $pubKey = $this->pharFilePath.'.pubkey'; + + Assert::writable(dirname($pubKey)); + Assert::true(extension_loaded('openssl')); + + if (file_exists($pubKey)) { + Assert::file( + $pubKey, + 'Cannot create public key: %s already exists and is not a file.', + ); + } + + $resource = openssl_pkey_get_private($key, (string) $password); + + Assert::notSame(false, $resource, 'Could not retrieve the private key, check that the password is correct.'); + + openssl_pkey_export($resource, $private); + + $details = openssl_pkey_get_details($resource); + + $this->phar->setSignatureAlgorithm(Phar::OPENSSL, $private); + + FS::dumpFile($pubKey, $details['key']); + } + + /** + * @param string[] $files + * + * @throws MultiReasonException + * + * @return array array of tuples where the first element is the local file path (path inside the PHAR) and the + * second element is the processed contents + */ + private function processContents(array $files): array + { + $mapFile = $this->mapFile; + $compactors = $this->compactors; + $cwd = getcwd(); + $enableParallelization = $this->enableParallelization; + + $processFile = static function (string $file) use ($cwd, $mapFile, $compactors, $enableParallelization): array { + chdir($cwd); + + // Keep the fully qualified call here since this function may be executed without the right autoloading + // mechanism + \KevinGH\Box\register_aliases(); + if ($enableParallelization) { + \KevinGH\Box\register_error_handler(); + } + + $contents = \Fidry\FileSystem\FS::getFileContents($file); + + $local = $mapFile($file); + + $processedContents = $compactors->compact($local, $contents); + + return [$local, $processedContents, $compactors->getScoperSymbolsRegistry()]; + }; + + if ($this->scoper instanceof NullScoper || !$enableParallelization) { + return array_map($processFile, $files); + } + + // In the case of parallel processing, an issue is caused due to the statefulness nature of the PhpScoper + // symbols registry. + // + // Indeed, the PhpScoper symbols registry stores the records of exposed/excluded classes and functions. If nothing is done, + // then the symbols registry retrieved in the end will here will be "blank" since the updated symbols registries are the ones + // from the workers used for the parallel processing. + // + // In order to avoid that, the symbols registries will be returned as a result as well in order to be able to merge + // all the symbols registries into one. + // + // This process is allowed thanks to the nature of the state of the symbols registries: having redundant classes or + // functions registered can easily be deal with so merging all those different states is actually + // straightforward. + $tuples = wait(parallelMap($files, $processFile)); + + if ([] === $tuples) { + return []; + } + + $filesWithContents = []; + $symbolRegistries = []; + + foreach ($tuples as [$local, $processedContents, $symbolRegistry]) { + $filesWithContents[] = [$local, $processedContents]; + $symbolRegistries[] = $symbolRegistry; + } + + $this->compactors->registerSymbolsRegistry( + SymbolsRegistry::createFromRegistries(array_filter($symbolRegistries)), + ); + + return $filesWithContents; + } + + public function count(): int + { + Assert::false($this->buffering, 'Cannot count the number of files in the PHAR when buffering'); + + return $this->phar->count(); + } +} diff --git a/fixtures/bench/with-compactors/src/Compactor/BaseCompactor.php b/fixtures/bench/with-compactors/src/Compactor/BaseCompactor.php new file mode 100644 index 000000000..74ab1e476 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Compactor/BaseCompactor.php @@ -0,0 +1,36 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +/** + * Base compactor class providing a slightly simpler API to compact the content only if the file is supported. + * + * @private + */ +abstract class BaseCompactor implements Compactor +{ + public function compact(string $file, string $contents): string + { + if ($this->supports($file)) { + return $this->compactContent($contents); + } + + return $contents; + } + + abstract protected function compactContent(string $contents): string; + + abstract protected function supports(string $file): bool; +} diff --git a/fixtures/bench/with-compactors/src/Compactor/Compactor.php b/fixtures/bench/with-compactors/src/Compactor/Compactor.php new file mode 100644 index 000000000..48f475aa5 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Compactor/Compactor.php @@ -0,0 +1,34 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +/** + * A compactor is a class called to process a file contents before adding it to the PHAR. This make it possible to for + * example strip down the file from useless phpdoc. + * + * @private + */ +interface Compactor +{ + /** + * Compacts the file contents. + * + * @param string $file The file name + * @param string $contents The file contents + * + * @return string The compacted contents + */ + public function compact(string $file, string $contents): string; +} diff --git a/fixtures/bench/with-compactors/src/Compactor/Compactors.php b/fixtures/bench/with-compactors/src/Compactor/Compactors.php new file mode 100644 index 000000000..f96bc2e60 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Compactor/Compactors.php @@ -0,0 +1,86 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +use Countable; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\PhpScoper\Scoper; +use function array_reduce; +use function count; + +/** + * @private + */ +final class Compactors implements Compactor, Countable +{ + /** + * @var Compactor[] + */ + private array $compactors; + + private ?PhpScoper $scoperCompactor = null; + + public function __construct(Compactor ...$compactors) + { + $this->compactors = $compactors; + + foreach ($compactors as $compactor) { + if ($compactor instanceof PhpScoper) { + $this->scoperCompactor = $compactor; + + // We do not expect more than one Scoper Compactor. If there is more than + // one then the latter is ignored. + break; + } + } + } + + public function compact(string $file, string $contents): string + { + return array_reduce( + $this->compactors, + static fn (string $contents, Compactor $compactor): string => $compactor->compact($file, $contents), + $contents, + ); + } + + public function getScoper(): ?Scoper + { + return $this->scoperCompactor?->getScoper(); + } + + public function getScoperSymbolsRegistry(): ?SymbolsRegistry + { + return $this->scoperCompactor?->getScoper()->getSymbolsRegistry(); + } + + public function registerSymbolsRegistry(SymbolsRegistry $symbolsRegistry): void + { + $this->scoperCompactor?->getScoper()->changeSymbolsRegistry($symbolsRegistry); + } + + /** + * @return Compactor[] + */ + public function toArray(): array + { + return $this->compactors; + } + + public function count(): int + { + return count($this->compactors); + } +} diff --git a/fixtures/bench/with-compactors/src/Compactor/FileExtensionCompactor.php b/fixtures/bench/with-compactors/src/Compactor/FileExtensionCompactor.php new file mode 100644 index 000000000..4c62dbb8d --- /dev/null +++ b/fixtures/bench/with-compactors/src/Compactor/FileExtensionCompactor.php @@ -0,0 +1,55 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +use Webmozart\Assert\Assert; +use function in_array; +use function pathinfo; +use const PATHINFO_EXTENSION; + +/** + * An abstract compactor class that handles matching supported file by their types. + * + * @private + */ +abstract class FileExtensionCompactor extends BaseCompactor +{ + /** + * @var string[] + */ + private array $extensions; + + /** + * @param string[] $extensions the list of supported file extensions + */ + public function __construct(array $extensions) + { + Assert::allString($extensions); + + $this->extensions = $extensions; + } + + protected function supports(string $file): bool + { + return in_array( + pathinfo( + $file, + PATHINFO_EXTENSION, + ), + $this->extensions, + true, + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Compactor/Json.php b/fixtures/bench/with-compactors/src/Compactor/Json.php new file mode 100644 index 000000000..600594c69 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Compactor/Json.php @@ -0,0 +1,45 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +use function json_decode; +use function json_encode; +use function json_last_error; +use const JSON_ERROR_NONE; +use const JSON_THROW_ON_ERROR; + +/** + * Compacts JSON files by re-encoding without pretty print. + * + * @private + */ +final class Json extends FileExtensionCompactor +{ + public function __construct(array $extensions = ['json', 'lock']) + { + parent::__construct($extensions); + } + + protected function compactContent(string $contents): string + { + $decodedContents = json_decode($contents, false); + + if (JSON_ERROR_NONE !== json_last_error()) { + return $contents; + } + + return json_encode($decodedContents, JSON_THROW_ON_ERROR); + } +} diff --git a/fixtures/bench/with-compactors/src/Compactor/NullCompactor.php b/fixtures/bench/with-compactors/src/Compactor/NullCompactor.php new file mode 100644 index 000000000..a02a7c334 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Compactor/NullCompactor.php @@ -0,0 +1,26 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +/** + * @private + */ +final class NullCompactor implements Compactor +{ + public function compact(string $file, string $contents): string + { + return $contents; + } +} diff --git a/fixtures/bench/with-compactors/src/Compactor/Php.php b/fixtures/bench/with-compactors/src/Compactor/Php.php new file mode 100644 index 000000000..6f0d1e077 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Compactor/Php.php @@ -0,0 +1,264 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +use KevinGH\Box\Annotation\CompactedFormatter; +use KevinGH\Box\Annotation\DocblockAnnotationParser; +use phpDocumentor\Reflection\DocBlockFactory; +use PhpToken; +use RuntimeException; +use Webmozart\Assert\Assert; +use function array_pop; +use function array_slice; +use function array_splice; +use function count; +use function is_int; +use function ltrim; +use function preg_replace; +use function str_repeat; +use const T_COMMENT; +use const T_DOC_COMMENT; +use const T_WHITESPACE; + +/** + * A PHP source code compactor copied from Composer. + * + * @see https://github.com/composer/composer/blob/a8df30c09be550bffc37ba540fb7c7f0383c3944/src/Composer/Compiler.php#L214 + * + * @author Kevin Herrera + * @author Fabien Potencier + * @author Jordi Boggiano + * @author Théo Fidry + * @author Juliette Reinders Folmer + * @author Alessandro Chitolina + * + * @private + */ +final class Php extends FileExtensionCompactor +{ + /** + * @param list $ignoredAnnotations + */ + public static function create(array $ignoredAnnotations): self + { + return new self( + new DocblockAnnotationParser( + DocBlockFactory::createInstance(), + new CompactedFormatter(), + $ignoredAnnotations, + ), + ); + } + + public function __construct( + private ?DocblockAnnotationParser $annotationParser, + array $extensions = ['php'], + ) { + parent::__construct($extensions); + } + + protected function compactContent(string $contents): string + { + $output = ''; + $tokens = PhpToken::tokenize($contents); + $tokenCount = count($tokens); + + for ($index = 0; $index < $tokenCount; ++$index) { + $token = $tokens[$index]; + $tokenText = $token->text; + + if ($token->is([T_COMMENT, T_DOC_COMMENT])) { + if (str_starts_with($tokenText, '#[')) { + // This is, in all likelihood, the start of a PHP >= 8.0 attribute. + // Note: $tokens may be updated by reference as well! + $retokenized = $this->retokenizeAttribute($tokens, $index); + + if (null !== $retokenized) { + array_splice($tokens, $index, 1, $retokenized); + $tokenCount = count($tokens); + } + + $attributeCloser = self::findAttributeCloser($tokens, $index); + + if (is_int($attributeCloser)) { + $output .= '#['; + } else { + // Turns out this was not an attribute. Treat it as a plain comment. + $output .= str_repeat("\n", mb_substr_count($tokenText, "\n")); + } + } elseif (str_contains($tokenText, '@')) { + try { + $output .= $this->compactAnnotations($tokenText); + } catch (RuntimeException) { + $output .= $tokenText; + } + } else { + $output .= str_repeat("\n", mb_substr_count($tokenText, "\n")); + } + } elseif ($token->is(T_WHITESPACE)) { + $whitespace = $tokenText; + $previousIndex = ($index - 1); + + // Handle whitespace potentially being split into two tokens after attribute retokenization. + $nextToken = $tokens[$index + 1] ?? null; + + if (null !== $nextToken + && $nextToken->is(T_WHITESPACE) + ) { + $whitespace .= $nextToken->text; + ++$index; + } + + // reduce wide spaces + $whitespace = preg_replace('{[ \t]+}', ' ', $whitespace); + + // normalize newlines to \n + $whitespace = preg_replace('{(?:\r\n|\r|\n)}', "\n", $whitespace); + + // If the new line was split off from the whitespace token due to it being included in + // the previous (comment) token (PHP < 8), remove leading spaces. + + $previousToken = $tokens[$previousIndex]; + + if ($previousToken->is(T_COMMENT) + && str_contains($previousToken->text, "\n") + ) { + $whitespace = ltrim($whitespace, ' '); + } + + // trim leading spaces + $whitespace = preg_replace('{\n +}', "\n", $whitespace); + + $output .= $whitespace; + } else { + $output .= $tokenText; + } + } + + return $output; + } + + private function compactAnnotations(string $docblock): string + { + if (null === $this->annotationParser) { + return $docblock; + } + + $breaksNbr = mb_substr_count($docblock, "\n"); + + $annotations = $this->annotationParser->parse($docblock); + + if ([] === $annotations) { + return str_repeat("\n", $breaksNbr); + } + + $compactedDocblock = '/**'; + + foreach ($annotations as $annotation) { + $compactedDocblock .= "\n".$annotation; + } + + $breaksNbr -= count($annotations); + + if ($breaksNbr > 0) { + $compactedDocblock .= str_repeat("\n", $breaksNbr - 1); + $compactedDocblock .= "\n*/"; + } else { + // A space is required here to avoid having /***/ + $compactedDocblock .= ' */'; + } + + return $compactedDocblock; + } + + /** + * @param list $tokens + */ + private static function findAttributeCloser(array $tokens, int $opener): ?int + { + $tokenCount = count($tokens); + $brackets = [$opener]; + $closer = null; + + for ($i = ($opener + 1); $i < $tokenCount; ++$i) { + $tokenText = $tokens[$i]->text; + + // Allow for short arrays within attributes. + if ('[' === $tokenText) { + $brackets[] = $i; + + continue; + } + + if (']' === $tokenText) { + array_pop($brackets); + + if (0 === count($brackets)) { + $closer = $i; + break; + } + } + } + + return $closer; + } + + /** + * @param non-empty-list $tokens + */ + private function retokenizeAttribute(array &$tokens, int $opener): ?array + { + Assert::keyExists($tokens, $opener); + + $token = $tokens[$opener]; + $attributeBody = mb_substr($token->text, 2); + $subTokens = PhpToken::tokenize('text; + } + + $subTokens = PhpToken::tokenize(' + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +use KevinGH\Box\PhpScoper\Scoper; +use Throwable; + +/** + * @private + */ +final class PhpScoper implements Compactor +{ + public function __construct(private Scoper $scoper) + { + } + + public function compact(string $file, string $contents): string + { + try { + return $this->scoper->scope($file, $contents); + } catch (Throwable) { + return $contents; + } + } + + public function getScoper(): Scoper + { + return $this->scoper; + } +} diff --git a/fixtures/bench/with-compactors/src/Compactor/Placeholder.php b/fixtures/bench/with-compactors/src/Compactor/Placeholder.php new file mode 100644 index 000000000..66f66835e --- /dev/null +++ b/fixtures/bench/with-compactors/src/Compactor/Placeholder.php @@ -0,0 +1,46 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +use Webmozart\Assert\Assert; +use function array_keys; +use function str_replace; + +final class Placeholder implements Compactor +{ + /** + * @var scalar[] + */ + private array $placeholders; + + /** + * @param scalar[] $placeholders + */ + public function __construct(array $placeholders) + { + Assert::allScalar($placeholders); + + $this->placeholders = $placeholders; + } + + public function compact(string $file, string $contents): string + { + return str_replace( + array_keys($this->placeholders), + $this->placeholders, + $contents, + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Composer/AutoloadDumper.php b/fixtures/bench/with-compactors/src/Composer/AutoloadDumper.php new file mode 100644 index 000000000..6a4133c88 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Composer/AutoloadDumper.php @@ -0,0 +1,116 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use Humbug\PhpScoper\Autoload\ScoperAutoloadGenerator; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\NotInstantiable; +use UnexpectedValueException; +use function array_map; +use function explode; +use function implode; +use function preg_match; +use function preg_replace; +use function str_replace; +use const PHP_EOL; + +final class AutoloadDumper +{ + use NotInstantiable; + + public static function generateAutoloadStatements( + SymbolsRegistry $symbolsRegistry, + array $excludedComposerAutoloadFileHashes, + string $autoloadContents, + ): string { + if (0 === $symbolsRegistry->count()) { + return $autoloadContents; + } + + $autoloadContents = self::extractInlinedAutoloadContents($autoloadContents); + $scoperStatements = self::getOriginalScoperAutoloaderContents( + $symbolsRegistry, + $excludedComposerAutoloadFileHashes, + ); + + $indentedAutoloadContents = self::fixInlinedAutoloadIndent( + $autoloadContents, + self::getLoaderStatementIndent($scoperStatements), + ); + + $mergedAutoloadContents = preg_replace( + '/(\s*\\$loader \= .*autoload\.php.*)/', + $indentedAutoloadContents, + $scoperStatements, + ); + + return self::cleanupDuplicateLineReturns($mergedAutoloadContents); + } + + private static function extractInlinedAutoloadContents(string $autoloadContents): string + { + $autoloadContents = str_replace('dump(); + + return preg_replace( + '/scoper\-autoload\.php \@generated by PhpScoper/', + '@generated by Humbug Box', + $scoperStatements, + ); + } + + private static function getLoaderStatementIndent(string $scoperStatements): string + { + if (1 !== preg_match('/(? *)\\$loader \= .*autoload\.php.*/', $scoperStatements, $matches)) { + throw new UnexpectedValueException('Could not process the scoper autoloader statements'); + } + + return $matches['indent']; + } + + private static function fixInlinedAutoloadIndent(string $autoloadContents, string $indent): string + { + $lines = explode(PHP_EOL, $autoloadContents); + + $indentedLines = array_map( + static fn (string $line) => '' === $line ? $line : $indent.$line, + $lines, + ); + + return implode(PHP_EOL, $indentedLines); + } + + private static function cleanupDuplicateLineReturns(string $value): string + { + return preg_replace( + '/\n{2,}/m', + PHP_EOL.PHP_EOL, + $value, + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Composer/CompilerPsrLogger.php b/fixtures/bench/with-compactors/src/Composer/CompilerPsrLogger.php new file mode 100644 index 000000000..fdd52fdca --- /dev/null +++ b/fixtures/bench/with-compactors/src/Composer/CompilerPsrLogger.php @@ -0,0 +1,71 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use KevinGH\Box\Console\Logger\CompilerLogger; +use Psr\Log\AbstractLogger; +use Psr\Log\LogLevel; +use Stringable; +use Symfony\Component\Console\Output\OutputInterface; +use function array_key_exists; + +final class CompilerPsrLogger extends AbstractLogger +{ + public function __construct( + private CompilerLogger $decoratedLogger, + ) { + } + + public function log($level, Stringable|string $message, array $context = []): void + { + $verbosity = self::getVerbosity($level); + $output = self::getOutput($context); + + if (null === $output) { + $this->decoratedLogger->log( + CompilerLogger::CHEVRON_PREFIX, + $message, + $verbosity, + ); + } else { + $this->decoratedLogger->getIO()->writeln( + $output, + $verbosity, + ); + } + } + + private static function getVerbosity(string $level): int + { + return match ($level) { + LogLevel::INFO => OutputInterface::VERBOSITY_VERBOSE, + LogLevel::DEBUG => OutputInterface::VERBOSITY_VERY_VERBOSE, + default => OutputInterface::OUTPUT_NORMAL, + }; + } + + private static function getOutput(array $context): ?string + { + $knownKeys = ['stdout', 'stderr']; + + foreach ($knownKeys as $knownKey) { + if (array_key_exists($knownKey, $context)) { + return $context[$knownKey]; + } + } + + return null; + } +} diff --git a/fixtures/bench/with-compactors/src/Composer/ComposerConfiguration.php b/fixtures/bench/with-compactors/src/Composer/ComposerConfiguration.php new file mode 100644 index 000000000..0529f6ee9 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Composer/ComposerConfiguration.php @@ -0,0 +1,107 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use Symfony\Component\Filesystem\Path; +use function array_column; +use function array_filter; +use function array_key_exists; +use function array_map; +use function realpath; +use const DIRECTORY_SEPARATOR; + +/** + * @private + */ +final class ComposerConfiguration +{ + /** + * Attempts to locate the `composer.json` and `composer.lock` files in the provided base-path in order to collect + * all the dev packages. + * + * @return string[] Dev package paths + */ + public static function retrieveDevPackages( + string $basePath, + ?array $composerJsonDecodedContents, + ?array $composerLockDecodedContents, + bool $excludeDevPackages, + ): array { + if (null === $composerJsonDecodedContents + || null === $composerLockDecodedContents + || false === $excludeDevPackages + ) { + return []; + } + + return self::getDevPackagePaths( + $basePath, + $composerJsonDecodedContents, + $composerLockDecodedContents, + ); + } + + /** + * @return string[] Dev packages paths + */ + private static function getDevPackagePaths( + string $basePath, + array $composerJsonDecodedContents, + array $composerLockDecodedContents, + ): array { + $vendorDir = Path::makeAbsolute( + self::retrieveVendorDir($composerJsonDecodedContents), + $basePath, + ); + + $packageNames = self::retrieveDevPackageNames($composerLockDecodedContents); + + return array_filter( + array_map( + static function (string $packageName) use ($vendorDir): ?string { + $realPath = realpath($vendorDir.DIRECTORY_SEPARATOR.$packageName); + + return false !== $realPath ? $realPath : null; + }, + $packageNames, + ), + ); + } + + public static function retrieveVendorDir(array $composerJsonDecodedContents): string + { + if (false === array_key_exists('config', $composerJsonDecodedContents)) { + return 'vendor'; + } + + if (false === array_key_exists('vendor-dir', $composerJsonDecodedContents['config'])) { + return 'vendor'; + } + + return $composerJsonDecodedContents['config']['vendor-dir']; + } + + /** + * @return string[] Names of the dev packages + */ + private static function retrieveDevPackageNames(array $composerLockDecodedContents): array + { + if (false === array_key_exists('packages-dev', $composerLockDecodedContents)) { + return []; + } + + return array_column($composerLockDecodedContents['packages-dev'], 'name'); + } +} diff --git a/fixtures/bench/with-compactors/src/Composer/ComposerFile.php b/fixtures/bench/with-compactors/src/Composer/ComposerFile.php new file mode 100644 index 000000000..1d362361b --- /dev/null +++ b/fixtures/bench/with-compactors/src/Composer/ComposerFile.php @@ -0,0 +1,50 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use Webmozart\Assert\Assert; + +final class ComposerFile +{ + private ?string $path; + private array $contents; + + public static function createEmpty(): self + { + return new self(null, []); + } + + public function __construct(?string $path, array $contents) + { + Assert::nullOrNotEmpty($path); + + if (null === $path) { + Assert::same([], $contents); + } + + $this->path = $path; + $this->contents = $contents; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function getDecodedContents(): array + { + return $this->contents; + } +} diff --git a/fixtures/bench/with-compactors/src/Composer/ComposerFiles.php b/fixtures/bench/with-compactors/src/Composer/ComposerFiles.php new file mode 100644 index 000000000..79a36ab14 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Composer/ComposerFiles.php @@ -0,0 +1,68 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use function array_filter; +use function array_map; +use function array_values; + +final class ComposerFiles +{ + public static function createEmpty(): self + { + return new self( + ComposerFile::createEmpty(), + ComposerFile::createEmpty(), + ComposerFile::createEmpty(), + ); + } + + public function __construct( + private readonly ComposerFile $composerJson, + private readonly ComposerFile $composerLock, + private readonly ComposerFile $installedJson, + ) { + } + + public function getComposerJson(): ComposerFile + { + return $this->composerJson; + } + + public function getComposerLock(): ComposerFile + { + return $this->composerLock; + } + + public function getInstalledJson(): ComposerFile + { + return $this->installedJson; + } + + /** + * @return list + */ + public function getPaths(): array + { + return array_values( + array_filter( + array_map( + static fn (ComposerFile $file): ?string => $file->getPath(), + [$this->composerJson, $this->composerLock, $this->installedJson], + ), + ), + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Composer/ComposerOrchestrator.php b/fixtures/bench/with-compactors/src/Composer/ComposerOrchestrator.php new file mode 100644 index 000000000..02c7f74ed --- /dev/null +++ b/fixtures/bench/with-compactors/src/Composer/ComposerOrchestrator.php @@ -0,0 +1,186 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use Composer\Semver\Semver; +use Fidry\Console\IO; +use Fidry\FileSystem\FileSystem; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\NotInstantiable; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use RuntimeException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use function sprintf; +use function trim; +use const PHP_EOL; + +/** + * @private + */ +final class ComposerOrchestrator +{ + use NotInstantiable; + + public const SUPPORTED_VERSION_CONSTRAINTS = '^2.2.0'; + + private string $detectedVersion; + + public static function create(): self + { + return new self( + ComposerProcessFactory::create(io: IO::createNull()), + new NullLogger(), + new FileSystem(), + ); + } + + public function __construct( + private ComposerProcessFactory $processFactory, + private LoggerInterface $logger, + private FileSystem $fileSystem, + ) { + } + + /** + * @throws UndetectableComposerVersion + */ + public function getVersion(): string + { + if (isset($this->detectedVersion)) { + return $this->detectedVersion; + } + + $getVersionProcess = $this->processFactory->getVersionProcess(); + + $this->logger->info($getVersionProcess->getCommandLine()); + + $getVersionProcess->run(); + + if (false === $getVersionProcess->isSuccessful()) { + throw UndetectableComposerVersion::forFailedProcess($getVersionProcess); + } + + $output = $getVersionProcess->getOutput(); + + if (1 !== preg_match('/Composer version (\S+?) /', $output, $match)) { + throw UndetectableComposerVersion::forOutput( + $getVersionProcess, + $output, + ); + } + + $this->detectedVersion = $match[1]; + + return $this->detectedVersion; + } + + /** + * @throws UndetectableComposerVersion + * @throws IncompatibleComposerVersion + */ + public function checkVersion(): void + { + $version = $this->getVersion(); + + $this->logger->info( + sprintf( + 'Version detected: %s (Box requires %s)', + $version, + self::SUPPORTED_VERSION_CONSTRAINTS, + ), + ); + + if (!Semver::satisfies($version, self::SUPPORTED_VERSION_CONSTRAINTS)) { + throw IncompatibleComposerVersion::create($version, self::SUPPORTED_VERSION_CONSTRAINTS); + } + } + + public function dumpAutoload( + SymbolsRegistry $symbolsRegistry, + string $prefix, + bool $excludeDevFiles, + array $excludedComposerAutoloadFileHashes, + ): void { + $this->dumpAutoloader(true === $excludeDevFiles); + + if ('' === $prefix) { + return; + } + + $autoloadFile = $this->getVendorDir().'/autoload.php'; + + $autoloadContents = AutoloadDumper::generateAutoloadStatements( + $symbolsRegistry, + $excludedComposerAutoloadFileHashes, + $this->fileSystem->getFileContents($autoloadFile), + ); + + $this->fileSystem->dumpFile($autoloadFile, $autoloadContents); + } + + public function getVendorDir(): string + { + $vendorDirProcess = $this->processFactory->getVendorDirProcess(); + + $this->logger->info($vendorDirProcess->getCommandLine()); + + $vendorDirProcess->run(); + + if (false === $vendorDirProcess->isSuccessful()) { + throw new RuntimeException( + 'Could not retrieve the vendor dir.', + 0, + new ProcessFailedException($vendorDirProcess), + ); + } + + return trim($vendorDirProcess->getOutput()); + } + + private function dumpAutoloader(bool $noDev): void + { + $dumpAutoloadProcess = $this->processFactory->getDumpAutoloaderProcess($noDev); + + $this->logger->info($dumpAutoloadProcess->getCommandLine()); + + $dumpAutoloadProcess->run(); + + if (false === $dumpAutoloadProcess->isSuccessful()) { + throw new RuntimeException( + 'Could not dump the autoloader.', + 0, + new ProcessFailedException($dumpAutoloadProcess), + ); + } + + $output = $dumpAutoloadProcess->getOutput(); + $errorOutput = $dumpAutoloadProcess->getErrorOutput(); + + if ('' !== $output) { + $this->logger->info( + 'STDOUT output:'.PHP_EOL.$output, + ['stdout' => $output], + ); + } + + if ('' !== $errorOutput) { + $this->logger->info( + 'STDERR output:'.PHP_EOL.$errorOutput, + ['stderr' => $errorOutput], + ); + } + } +} diff --git a/fixtures/bench/with-compactors/src/Composer/ComposerProcessFactory.php b/fixtures/bench/with-compactors/src/Composer/ComposerProcessFactory.php new file mode 100644 index 000000000..6584ce950 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Composer/ComposerProcessFactory.php @@ -0,0 +1,164 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use Closure; +use Fidry\Console\IO; +use KevinGH\Box\Constants; +use RuntimeException; +use Symfony\Component\Process\ExecutableFinder; +use Symfony\Component\Process\Process; + +/** + * @final + * @private + */ +class ComposerProcessFactory +{ + private string $composerExecutable; + + public static function create( + ?string $composerExecutable = null, + ?IO $io = null, + ): self { + $io ??= IO::createNull(); + + return new self( + null === $composerExecutable + ? self::retrieveComposerExecutable(...) + : static fn () => $composerExecutable, + self::retrieveSubProcessVerbosity($io), + $io->isDecorated(), + self::getDefaultEnvVars(), + ); + } + + /** + * @param Closure():string $composerExecutableFactory + */ + public function __construct( + private Closure $composerExecutableFactory, + private ?string $verbosity, + private bool $ansi, + private array $defaultEnvironmentVariables, + ) { + } + + public function getVersionProcess(): Process + { + return $this->createProcess( + [ + $this->getComposerExecutable(), + '--version', + // Never use ANSI support here as we want to parse the raw output. + '--no-ansi', + ], + // Ensure that even if this command gets executed within the app with --quiet it still + // works. + ['SHELL_VERBOSITY' => 0], + ); + } + + public function getDumpAutoloaderProcess(bool $noDev): Process + { + $composerCommand = [$this->getComposerExecutable(), 'dump-autoload', '--classmap-authoritative']; + + if (true === $noDev) { + $composerCommand[] = '--no-dev'; + } + + if (null !== $this->verbosity) { + $composerCommand[] = $this->verbosity; + } + + if ($this->ansi) { + $composerCommand[] = '--ansi'; + } + + return $this->createProcess($composerCommand); + } + + public function getVendorDirProcess(): Process + { + return $this->createProcess( + [ + $this->getComposerExecutable(), + 'config', + 'vendor-dir', + // Never use ANSI support here as we want to parse the raw output. + '--no-ansi', + ], + // Ensure that even if this command gets executed within the app with --quiet it still + // works. + ['SHELL_VERBOSITY' => 0], + ); + } + + private function createProcess(array $command, array $environmentVariables = []): Process + { + return new Process( + $command, + env: [ + ...$this->defaultEnvironmentVariables, + ...$environmentVariables, + ], + ); + } + + private function getComposerExecutable(): string + { + if (!isset($this->composerExecutable)) { + $this->composerExecutable = ($this->composerExecutableFactory)(); + } + + return $this->composerExecutable; + } + + private static function retrieveSubProcessVerbosity(IO $io): ?string + { + if ($io->isDebug()) { + return '-vvv'; + } + + if ($io->isVeryVerbose()) { + return '-v'; + } + + return null; + } + + private static function getDefaultEnvVars(): array + { + $vars = ['COMPOSER_ORIGINAL_INIS' => '']; + + if ('1' === (string) getenv(Constants::ALLOW_XDEBUG)) { + $vars['COMPOSER_ALLOW_XDEBUG'] = '1'; + } + + return $vars; + } + + private static function retrieveComposerExecutable(): string + { + $executableFinder = new ExecutableFinder(); + $executableFinder->addSuffix('.phar'); + + if (null === $composer = $executableFinder->find('composer')) { + throw new RuntimeException('Could not find a Composer executable.'); + } + + return $composer; + } +} diff --git a/fixtures/bench/with-compactors/src/Composer/IncompatibleComposerVersion.php b/fixtures/bench/with-compactors/src/Composer/IncompatibleComposerVersion.php new file mode 100644 index 000000000..b893111eb --- /dev/null +++ b/fixtures/bench/with-compactors/src/Composer/IncompatibleComposerVersion.php @@ -0,0 +1,32 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use RuntimeException; +use function sprintf; + +final class IncompatibleComposerVersion extends RuntimeException +{ + public static function create(string $version, string $constraints): self + { + return new self( + sprintf( + 'The Composer version "%s" does not satisfy the constraint "%s".', + $version, + $constraints, + ) + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Composer/UndetectableComposerVersion.php b/fixtures/bench/with-compactors/src/Composer/UndetectableComposerVersion.php new file mode 100644 index 000000000..f2c6115b7 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Composer/UndetectableComposerVersion.php @@ -0,0 +1,65 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use JetBrains\PhpStorm\Pure; +use RuntimeException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; +use Throwable; +use function implode; +use function sprintf; +use const PHP_EOL; + +final class UndetectableComposerVersion extends RuntimeException +{ + #[Pure] + public function __construct( + string $message, + public readonly ?Process $process = null, + int $code = 0, + ?Throwable $previous = null, + ) { + parent::__construct($message, $code, $previous); + } + + public static function forFailedProcess(Process $process): self + { + $previous = new ProcessFailedException($process); + + return new self( + sprintf( + 'Could not detect the Composer version: %s', + $previous->getMessage(), + ), + $process, + previous: $previous, + ); + } + + public static function forOutput(Process $process, string $normalizedOutput): self + { + return new self( + implode( + PHP_EOL, + [ + 'Could not determine the Composer version from the following output:', + $normalizedOutput, + ], + ), + $process, + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Configuration/Configuration.php b/fixtures/bench/with-compactors/src/Configuration/Configuration.php new file mode 100644 index 000000000..ddc5b051c --- /dev/null +++ b/fixtures/bench/with-compactors/src/Configuration/Configuration.php @@ -0,0 +1,2738 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Configuration; + +use Closure; +use DateTimeImmutable; +use DateTimeZone; +use Fidry\FileSystem\FS; +use Humbug\PhpScoper\Configuration\Configuration as PhpScoperConfiguration; +use InvalidArgumentException; +use KevinGH\Box\Compactor\Compactor; +use KevinGH\Box\Compactor\Compactors; +use KevinGH\Box\Compactor\Php as PhpCompactor; +use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor; +use KevinGH\Box\Composer\ComposerConfiguration; +use KevinGH\Box\Composer\ComposerFile; +use KevinGH\Box\Composer\ComposerFiles; +use KevinGH\Box\Json\Json; +use KevinGH\Box\MapFile; +use KevinGH\Box\Phar\CompressionAlgorithm; +use KevinGH\Box\Phar\SigningAlgorithm; +use KevinGH\Box\PhpScoper\ConfigurationFactory as PhpScoperConfigurationFactory; +use KevinGH\Box\PhpScoper\SerializableScoper; +use Phar; +use RuntimeException; +use Seld\JsonLint\ParsingException; +use SplFileInfo; +use stdClass; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; +use Webmozart\Assert\Assert; +use function array_diff; +use function array_filter; +use function array_flip; +use function array_key_exists; +use function array_keys; +use function array_map; +use function array_merge; +use function array_unique; +use function array_values; +use function array_walk; +use function constant; +use function current; +use function dirname; +use function explode; +use function file_exists; +use function getcwd; +use function implode; +use function in_array; +use function intval; +use function is_array; +use function is_bool; +use function is_file; +use function is_link; +use function is_object; +use function is_readable; +use function is_string; +use function iter\map; +use function iter\toArray; +use function iter\values; +use function KevinGH\Box\get_box_version; +use function KevinGH\Box\unique_id; +use function krsort; +use function preg_match; +use function preg_replace; +use function property_exists; +use function realpath; +use function sprintf; +use function str_starts_with; +use function trigger_error; +use function trim; +use const E_USER_DEPRECATED; + +/** + * @private + */ +final class Configuration +{ + private const DEFAULT_OUTPUT_FALLBACK = 'test.phar'; + private const DEFAULT_MAIN_SCRIPT = 'index.php'; + private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s T'; + private const DEFAULT_REPLACEMENT_SIGIL = '@'; + private const DEFAULT_SHEBANG = '#!/usr/bin/env php'; + private const DEFAULT_BANNER = <<<'BANNER' + Generated by Humbug Box %s. + + @link https://github.com/humbug/box + BANNER; + private const FILES_SETTINGS = [ + 'directories', + 'finder', + ]; + private const PHP_SCOPER_CONFIG = 'scoper.inc.php'; + private const DEFAULT_SIGNING_ALGORITHM = SigningAlgorithm::SHA512; + private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-'; + + private const DEFAULT_IGNORED_ANNOTATIONS = [ + 'abstract', + 'access', + 'annotation', + 'api', + 'attribute', + 'attributes', + 'author', + 'category', + 'code', + 'codecoverageignore', + 'codecoverageignoreend', + 'codecoverageignorestart', + 'copyright', + 'deprec', + 'deprecated', + 'endcode', + 'example', + 'exception', + 'filesource', + 'final', + 'fixme', + 'global', + 'ignore', + 'ingroup', + 'inheritdoc', + 'internal', + 'license', + 'link', + 'magic', + 'method', + 'name', + 'override', + 'package', + 'package_version', + 'param', + 'private', + 'property', + 'required', + 'return', + 'see', + 'since', + 'static', + 'staticvar', + 'subpackage', + 'suppresswarnings', + 'target', + 'throw', + 'throws', + 'todo', + 'tutorial', + 'usedby', + 'uses', + 'var', + 'version', + ]; + + private const ALGORITHM_KEY = 'algorithm'; + private const ALIAS_KEY = 'alias'; + private const ANNOTATIONS_KEY = 'annotations'; + private const IGNORED_ANNOTATIONS_KEY = 'ignore'; + private const AUTO_DISCOVERY_KEY = 'force-autodiscovery'; + private const BANNER_KEY = 'banner'; + private const BANNER_FILE_KEY = 'banner-file'; + private const BASE_PATH_KEY = 'base-path'; + private const BLACKLIST_KEY = 'blacklist'; + private const CHECK_REQUIREMENTS_KEY = 'check-requirements'; + private const CHMOD_KEY = 'chmod'; + private const COMPACTORS_KEY = 'compactors'; + private const COMPRESSION_KEY = 'compression'; + private const DATETIME_KEY = 'datetime'; + private const DATETIME_FORMAT_KEY = 'datetime-format'; + private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format'; + private const DIRECTORIES_KEY = 'directories'; + private const DIRECTORIES_BIN_KEY = 'directories-bin'; + private const DUMP_AUTOLOAD_KEY = 'dump-autoload'; + private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files'; + private const EXCLUDE_DEV_FILES_KEY = 'exclude-dev-files'; + private const FILES_KEY = 'files'; + private const FILES_BIN_KEY = 'files-bin'; + private const FINDER_KEY = 'finder'; + private const FINDER_BIN_KEY = 'finder-bin'; + private const GIT_KEY = 'git'; + private const GIT_COMMIT_KEY = 'git-commit'; + private const GIT_COMMIT_SHORT_KEY = 'git-commit-short'; + private const GIT_TAG_KEY = 'git-tag'; + private const GIT_VERSION_KEY = 'git-version'; + private const INTERCEPT_KEY = 'intercept'; + private const KEY_KEY = 'key'; + private const KEY_PASS_KEY = 'key-pass'; + private const MAIN_KEY = 'main'; + private const MAP_KEY = 'map'; + private const METADATA_KEY = 'metadata'; + private const OUTPUT_KEY = 'output'; + private const PHP_SCOPER_KEY = 'php-scoper'; + private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil'; + private const REPLACEMENTS_KEY = 'replacements'; + private const SHEBANG_KEY = 'shebang'; + private const STUB_KEY = 'stub'; + private const TIMESTAMP = 'timestamp'; + + private ?string $mainScriptPath; + private ?string $mainScriptContents; + private ?string $composerBin = null; + + public static function create(?string $file, stdClass $raw): self + { + $logger = new ConfigurationLogger(); + + $basePath = self::retrieveBasePath($file, $raw, $logger); + + $composerFiles = self::retrieveComposerFiles($basePath); + + $dumpAutoload = self::retrieveDumpAutoload($raw, $composerFiles, $logger); + + $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw, $logger); + + $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles->getComposerJson()->getDecodedContents(), $logger); + $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath); + + [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger); + + $stubPath = self::retrieveStubPath($raw, $basePath, $logger); + $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger); + + $alias = self::retrieveAlias($raw, null !== $stubPath, $logger); + + $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger); + + $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger); + $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger); + + if (null !== $stubBannerPath) { + $stubBannerContents = FS::getFileContents($stubBannerPath); + } + + $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents); + + if (null !== $stubBannerPath && self::getDefaultBanner() === $stubBannerContents) { + self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY); + } + + $isInterceptsFileFunctions = self::retrieveInterceptsFileFunctions($raw, $isStubGenerated, $logger); + + $checkRequirements = self::retrieveCheckRequirements( + $raw, + null !== $composerFiles->getComposerJson()->getPath(), + null !== $composerFiles->getComposerLock()->getPath(), + false === $isStubGenerated && null === $stubPath, + $logger, + ); + + $excludeDevPackages = self::retrieveExcludeDevFiles($raw, $dumpAutoload, $logger); + + $devPackages = ComposerConfiguration::retrieveDevPackages( + $basePath, + $composerFiles->getComposerJson()->getDecodedContents(), + $composerFiles->getComposerLock()->getDecodedContents(), + $excludeDevPackages, + ); + + /** + * @var string[] $excludedPaths + * @var Closure $blacklistFilter + */ + [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter( + $raw, + $basePath, + $logger, + $tmpOutputPath, + $outputPath, + $mainScriptPath, + ); + // Excluded paths above is a bit misleading since including a file directly has precedence over the blacklist. + // If you consider the following: + // + // { + // "files": ["file1"], + // "blacklist": ["file1"], + // } + // + // In the end the file "file1" _will_ be included: blacklist are here to help out to exclude files for finders + // and directories but the user should always have the possibility to force his way to include a file. + // + // The exception however, is for the following which is essential for the good functioning of Box + $alwaysExcludedPaths = array_map( + static fn (string $excludedPath): string => self::normalizePath($excludedPath, $basePath), + array_filter([$tmpOutputPath, $outputPath, $mainScriptPath]), + ); + + $autodiscoverFiles = self::autodiscoverFiles($file, $raw); + $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger); + + $filesAggregate = self::collectFiles( + $raw, + $basePath, + $mainScriptPath, + $blacklistFilter, + $excludedPaths, + $alwaysExcludedPaths, + $devPackages, + $composerFiles, + $autodiscoverFiles, + $forceFilesAutodiscovery, + $logger, + ); + $binaryFilesAggregate = self::collectBinaryFiles( + $raw, + $basePath, + $blacklistFilter, + $excludedPaths, + $alwaysExcludedPaths, + $devPackages, + $logger, + ); + + $compactors = self::retrieveCompactors($raw, $basePath, $logger); + $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger); + + $fileMode = self::retrieveFileMode($raw, $logger); + + $map = self::retrieveMap($raw, $logger); + $fileMapper = new MapFile($basePath, $map); + + $metadata = self::retrieveMetadata($raw, $logger); + + $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger); + $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger); + $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger); + $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger); + + $replacements = self::retrieveReplacements($raw, $file, $basePath, $logger); + + $timestamp = self::retrieveTimestamp($raw, $signingAlgorithm, $logger); + + return new self( + $file, + $alias, + $basePath, + $composerFiles->getComposerJson(), + $composerFiles->getComposerLock(), + $filesAggregate, + $binaryFilesAggregate, + $autodiscoverFiles || $forceFilesAutodiscovery, + $dumpAutoload, + $excludeComposerFiles, + $excludeDevPackages, + $compactors, + $compressionAlgorithm, + $fileMode, + $mainScriptPath, + $mainScriptContents, + $fileMapper, + $metadata, + $tmpOutputPath, + $outputPath, + $privateKeyPassphrase, + $privateKeyPath, + $promptForPrivateKey, + $replacements, + $shebang, + $signingAlgorithm, + $stubBannerContents, + $stubBannerPath, + $stubPath, + $isInterceptsFileFunctions, + $isStubGenerated, + $timestamp, + $checkRequirements, + $logger->getWarnings(), + $logger->getRecommendations(), + ); + } + + /** + * @param string $basePath Utility to private the base path used and be able to retrieve a + * path relative to it (the base path) + * @param array $composerJson The first element is the path to the `composer.json` file as a + * string and the second element its decoded contents as an + * associative array. + * @param array $composerLock The first element is the path to the `composer.lock` file as a + * string and the second element its decoded contents as an + * associative array. + * @param SplFileInfo[] $files List of files + * @param SplFileInfo[] $binaryFiles List of binary files + * @param bool $dumpAutoload Whether the Composer autoloader should be dumped + * @param bool $excludeComposerFiles Whether the Composer files composer.json, composer.lock and + * installed.json should be removed from the PHAR + * @param CompressionAlgorithm $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants + * @param null|int $fileMode File mode in octal form + * @param string $mainScriptPath The main script file path + * @param string $mainScriptContents The processed content of the main script file + * @param MapFile $fileMapper Utility to map the files from outside and inside the PHAR + * @param mixed $metadata The PHAR Metadata + * @param bool $promptForPrivateKey If the user should be prompted for the private key passphrase + * @param array $processedReplacements The processed list of replacement placeholders and their values + * @param null|non-empty-string $shebang The shebang line + * @param SigningAlgorithm $signingAlgorithm The PHAR siging algorithm. See \Phar constants + * @param null|string $stubBannerContents The stub banner comment + * @param null|string $stubBannerPath The path to the stub banner comment file + * @param null|string $stubPath The PHAR stub file path + * @param bool $isInterceptFileFuncs Whether Phar::interceptFileFuncs() should be used + * @param bool $isStubGenerated Whether if the PHAR stub should be generated + * @param null|DateTimeImmutable $timestamp Timestamp at which the PHAR will be set to. + * @param bool $checkRequirements Whether the PHAR will check the application requirements before + * running + * @param string[] $warnings + * @param string[] $recommendations + */ + private function __construct( + private ?string $file, + private string $alias, + private string $basePath, + private ComposerFile $composerJson, + private ComposerFile $composerLock, + private array $files, + private array $binaryFiles, + private bool $autodiscoveredFiles, + private bool $dumpAutoload, + private bool $excludeComposerFiles, + private bool $excludeDevFiles, + private Compactors|array $compactors, + private CompressionAlgorithm $compressionAlgorithm, + private int|string|null $fileMode, + ?string $mainScriptPath, + ?string $mainScriptContents, + private MapFile $fileMapper, + private mixed $metadata, + private string $tmpOutputPath, + private string $outputPath, + private ?string $privateKeyPassphrase, + private ?string $privateKeyPath, + private bool $promptForPrivateKey, + private array $processedReplacements, + private ?string $shebang, + private SigningAlgorithm $signingAlgorithm, + private ?string $stubBannerContents, + private ?string $stubBannerPath, + private ?string $stubPath, + private bool $isInterceptFileFuncs, + private bool $isStubGenerated, + private ?DateTimeImmutable $timestamp, + private bool $checkRequirements, + private array $warnings, + private array $recommendations, + ) { + if (null === $mainScriptPath) { + Assert::null($mainScriptContents); + } else { + Assert::notNull($mainScriptContents); + } + + $this->mainScriptPath = $mainScriptPath; + $this->mainScriptContents = $mainScriptContents; + } + + public function setComposerBin(?string $composerBin): void + { + $this->composerBin = $composerBin; + } + + public function getComposerBin(): ?string + { + return $this->composerBin; + } + + public function export(): string + { + return ExportableConfiguration::create($this)->export(); + } + + public function getConfigurationFile(): ?string + { + return $this->file; + } + + public function getAlias(): string + { + return $this->alias; + } + + public function getBasePath(): string + { + return $this->basePath; + } + + public function getComposerJson(): ?string + { + return $this->composerJson->getPath(); + } + + public function getDecodedComposerJsonContents(): ?array + { + return null === $this->composerJson->getPath() ? null : $this->composerJson->getDecodedContents(); + } + + public function getComposerLock(): ?string + { + return $this->composerLock->getPath(); + } + + public function getDecodedComposerLockContents(): ?array + { + return null === $this->composerLock->getPath() ? null : $this->composerLock->getDecodedContents(); + } + + /** + * @return SplFileInfo[] + */ + public function getFiles(): array + { + return $this->files; + } + + /** + * @return SplFileInfo[] + */ + public function getBinaryFiles(): array + { + return $this->binaryFiles; + } + + public function hasAutodiscoveredFiles(): bool + { + return $this->autodiscoveredFiles; + } + + public function dumpAutoload(): bool + { + return $this->dumpAutoload; + } + + public function excludeComposerFiles(): bool + { + return $this->excludeComposerFiles; + } + + public function excludeDevFiles(): bool + { + return $this->excludeDevFiles; + } + + public function getCompactors(): Compactors + { + return $this->compactors; + } + + public function getCompressionAlgorithm(): CompressionAlgorithm + { + return $this->compressionAlgorithm; + } + + public function getFileMode(): ?int + { + return $this->fileMode; + } + + public function hasMainScript(): bool + { + return null !== $this->mainScriptPath; + } + + public function getMainScriptPath(): string + { + Assert::notNull( + $this->mainScriptPath, + 'Cannot retrieve the main script path: no main script configured.', + ); + + return $this->mainScriptPath; + } + + public function getMainScriptContents(): string + { + Assert::notNull( + $this->mainScriptPath, + 'Cannot retrieve the main script contents: no main script configured.', + ); + + return $this->mainScriptContents; + } + + public function checkRequirements(): bool + { + return $this->checkRequirements; + } + + public function getTmpOutputPath(): string + { + return $this->tmpOutputPath; + } + + public function getOutputPath(): string + { + return $this->outputPath; + } + + public function getFileMapper(): MapFile + { + return $this->fileMapper; + } + + public function getMetadata(): mixed + { + return $this->metadata; + } + + public function getPrivateKeyPassphrase(): ?string + { + return $this->privateKeyPassphrase; + } + + public function getPrivateKeyPath(): ?string + { + return $this->privateKeyPath; + } + + /** + * @deprecated Use promptForPrivateKey() instead + */ + public function isPrivateKeyPrompt(): bool + { + return $this->promptForPrivateKey; + } + + public function promptForPrivateKey(): bool + { + return $this->promptForPrivateKey; + } + + /** + * @return scalar[] + */ + public function getReplacements(): array + { + return $this->processedReplacements; + } + + public function getShebang(): ?string + { + return $this->shebang; + } + + public function getSigningAlgorithm(): SigningAlgorithm + { + return $this->signingAlgorithm; + } + + public function getStubBannerContents(): ?string + { + return $this->stubBannerContents; + } + + public function getStubBannerPath(): ?string + { + return $this->stubBannerPath; + } + + public function getStubPath(): ?string + { + return $this->stubPath; + } + + public function isInterceptFileFuncs(): bool + { + return $this->isInterceptFileFuncs; + } + + public function isStubGenerated(): bool + { + return $this->isStubGenerated; + } + + public function getTimestamp(): ?DateTimeImmutable + { + return $this->timestamp; + } + + /** + * @return string[] + */ + public function getWarnings(): array + { + return $this->warnings; + } + + /** + * @return string[] + */ + public function getRecommendations(): array + { + return $this->recommendations; + } + + private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string + { + self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY); + + if (false === isset($raw->{self::ALIAS_KEY})) { + return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar'; + } + + $alias = trim($raw->{self::ALIAS_KEY}); + + Assert::notEmpty($alias, 'A PHAR alias cannot be empty when provided.'); + + if ($userStubUsed) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but is ignored since a custom stub path is used', + self::ALIAS_KEY, + ), + ); + } + + return $alias; + } + + private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string + { + if (null === $file) { + return getcwd(); + } + + if (false === isset($raw->{self::BASE_PATH_KEY})) { + return realpath(dirname($file)); + } + + $basePath = trim($raw->{self::BASE_PATH_KEY}); + + Assert::directory( + $basePath, + 'The base path %s is not a directory or does not exist.', + ); + + $basePath = realpath($basePath); + $defaultPath = realpath(dirname($file)); + + if ($basePath === $defaultPath) { + self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY); + } + + return $basePath; + } + + /** + * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting. + */ + private static function autodiscoverFiles(?string $file, stdClass $raw): bool + { + if (null === $file) { + return true; + } + + $associativeRaw = (array) $raw; + + return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw)); + } + + private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool + { + self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false); + + return $raw->{self::AUTO_DISCOVERY_KEY} ?? false; + } + + private static function retrieveBlacklistFilter( + stdClass $raw, + string $basePath, + ConfigurationLogger $logger, + ?string ...$excludedPaths, + ): array { + $blacklist = array_flip( + self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths), + ); + + $blacklistFilter = static function (SplFileInfo $file) use ($blacklist): ?bool { + if ($file->isLink()) { + return false; + } + + if (false === $file->getRealPath()) { + return false; + } + + if (array_key_exists($file->getRealPath(), $blacklist)) { + return false; + } + + return null; + }; + + return [array_keys($blacklist), $blacklistFilter]; + } + + /** + * @param null[]|string[] $excludedPaths + * + * @return string[] + */ + private static function retrieveBlacklist( + stdClass $raw, + string $basePath, + ConfigurationLogger $logger, + ?string ...$excludedPaths, + ): array { + self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []); + + $normalizedBlacklist = array_map( + static fn (string $excludedPath): string => self::normalizePath($excludedPath, $basePath), + array_filter($excludedPaths), + ); + + /** @var string[] $blacklist */ + $blacklist = $raw->{self::BLACKLIST_KEY} ?? []; + + foreach ($blacklist as $file) { + $normalizedBlacklist[] = self::normalizePath($file, $basePath); + $normalizedBlacklist[] = Path::canonicalize(Path::makeRelative(trim($file), $basePath)); + } + + return array_unique($normalizedBlacklist); + } + + /** + * @param string[] $excludedPaths + * @param string[] $alwaysExcludedPaths + * @param string[] $devPackages + * + * @return SplFileInfo[] + */ + private static function collectFiles( + stdClass $raw, + string $basePath, + ?string $mainScriptPath, + Closure $blacklistFilter, + array $excludedPaths, + array $alwaysExcludedPaths, + array $devPackages, + ComposerFiles $composerFiles, + bool $autodiscoverFiles, + bool $forceFilesAutodiscovery, + ConfigurationLogger $logger, + ): array { + $files = [self::retrieveFiles($raw, self::FILES_KEY, $basePath, $composerFiles, $alwaysExcludedPaths, $logger)]; + + if ($autodiscoverFiles || $forceFilesAutodiscovery) { + [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude( + $basePath, + $composerFiles->getComposerJson()->getDecodedContents(), + $devPackages, + $composerFiles->getPaths(), + $excludedPaths, + ); + + $files[] = self::wrapInSplFileInfo($filesToAppend); + + $files[] = self::retrieveAllFiles( + $basePath, + $directories, + $mainScriptPath, + $blacklistFilter, + $excludedPaths, + $devPackages, + ); + } + + if (false === $autodiscoverFiles) { + $files[] = self::retrieveDirectories( + $raw, + self::DIRECTORIES_KEY, + $basePath, + $blacklistFilter, + $excludedPaths, + $logger, + ); + + $filesFromFinders = self::retrieveFilesFromFinders( + $raw, + self::FINDER_KEY, + $basePath, + $blacklistFilter, + $devPackages, + $logger, + ); + + foreach ($filesFromFinders as $filesFromFinder) { + // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files + $files[] = $filesFromFinder; + } + + $files[] = self::wrapInSplFileInfo($composerFiles->getPaths()); + } + + return self::retrieveFilesAggregate(...$files); + } + + /** + * @param string[] $excludedPaths + * @param string[] $alwaysExcludedPaths + * @param string[] $devPackages + * + * @return SplFileInfo[] + */ + private static function collectBinaryFiles( + stdClass $raw, + string $basePath, + Closure $blacklistFilter, + array $excludedPaths, + array $alwaysExcludedPaths, + array $devPackages, + ConfigurationLogger $logger, + ): array { + $binaryFiles = self::retrieveFiles($raw, self::FILES_BIN_KEY, $basePath, ComposerFiles::createEmpty(), $alwaysExcludedPaths, $logger); + + $binaryDirectories = self::retrieveDirectories( + $raw, + self::DIRECTORIES_BIN_KEY, + $basePath, + $blacklistFilter, + $excludedPaths, + $logger, + ); + + $binaryFilesFromFinders = self::retrieveFilesFromFinders( + $raw, + self::FINDER_BIN_KEY, + $basePath, + $blacklistFilter, + $devPackages, + $logger, + ); + + return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders); + } + + /** + * @param string[] $excludedFiles + * + * @return SplFileInfo[] + */ + private static function retrieveFiles( + stdClass $raw, + string $key, + string $basePath, + ComposerFiles $composerFiles, + array $excludedFiles, + ConfigurationLogger $logger, + ): array { + self::checkIfDefaultValue($logger, $raw, $key, []); + + $excludedFiles = array_flip($excludedFiles); + $files = array_filter([ + $composerFiles->getComposerJson()->getPath(), + $composerFiles->getComposerLock()->getPath(), + ]); + + if (false === isset($raw->{$key})) { + return self::wrapInSplFileInfo($files); + } + + if ([] === (array) $raw->{$key}) { + return self::wrapInSplFileInfo($files); + } + + $files = array_merge((array) $raw->{$key}, $files); + + Assert::allString($files); + + $normalizePath = static function (string $file) use ($basePath, $key, $excludedFiles): ?SplFileInfo { + $file = self::normalizePath($file, $basePath); + + Assert::false( + is_link($file), + sprintf( + 'Cannot add the link "%s": links are not supported.', + $file, + ), + ); + + Assert::file( + $file, + sprintf( + '"%s" must contain a list of existing files. Could not find %%s.', + $key, + ), + ); + + return array_key_exists($file, $excludedFiles) ? null : new SplFileInfo($file); + }; + + return array_filter(array_map($normalizePath, $files)); + } + + /** + * @param string $key Config property name + * @param string[] $excludedPaths + * + * @return iterable&(SplFileInfo[]&Finder) + */ + private static function retrieveDirectories( + stdClass $raw, + string $key, + string $basePath, + Closure $blacklistFilter, + array $excludedPaths, + ConfigurationLogger $logger, + ): iterable { + $directories = self::retrieveDirectoryPaths($raw, $key, $basePath, $logger); + + if ([] !== $directories) { + $finder = Finder::create() + ->files() + ->filter($blacklistFilter) + ->ignoreVCS(true) + ->in($directories); + + foreach ($excludedPaths as $excludedPath) { + $finder->notPath($excludedPath); + } + + return $finder; + } + + return []; + } + + /** + * @param string[] $devPackages + * + * @return iterable[]|SplFileInfo[][] + */ + private static function retrieveFilesFromFinders( + stdClass $raw, + string $key, + string $basePath, + Closure $blacklistFilter, + array $devPackages, + ConfigurationLogger $logger, + ): array { + self::checkIfDefaultValue($logger, $raw, $key, []); + + if (false === isset($raw->{$key})) { + return []; + } + + $finder = $raw->{$key}; + + return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages); + } + + /** + * @param iterable[]|SplFileInfo[][] $fileIterators + * + * @return SplFileInfo[] + */ + private static function retrieveFilesAggregate(iterable ...$fileIterators): array + { + $files = []; + + foreach ($fileIterators as $fileIterator) { + foreach ($fileIterator as $file) { + $files[(string) $file] = $file; + } + } + + return array_values($files); + } + + /** + * @param string[] $devPackages + * + * @return Finder[]|SplFileInfo[][] + */ + private static function processFinders( + array $findersConfig, + string $basePath, + Closure $blacklistFilter, + array $devPackages, + ): array { + $processFinderConfig = static fn (stdClass $config) => self::processFinder($config, $basePath, $blacklistFilter, $devPackages); + + return array_map($processFinderConfig, $findersConfig); + } + + /** + * @param string[] $devPackages + * + * @return Finder|SplFileInfo[] + */ + private static function processFinder( + stdClass $config, + string $basePath, + Closure $blacklistFilter, + array $devPackages, + ): Finder { + $finder = Finder::create() + ->files() + ->filter($blacklistFilter) + ->filter( + static function (SplFileInfo $fileInfo) use ($devPackages): bool { + foreach ($devPackages as $devPackage) { + if ($devPackage === Path::getLongestCommonBasePath($devPackage, $fileInfo->getRealPath())) { + // File belongs to the dev package + return false; + } + } + + return true; + }, + ) + ->ignoreVCS(true); + + $normalizedConfig = (static function (array $config, Finder $finder): array { + $normalizedConfig = []; + + foreach ($config as $method => $arguments) { + $method = trim($method); + $arguments = (array) $arguments; + + Assert::methodExists($finder, $method); + + $normalizedConfig[$method] = $arguments; + } + + krsort($normalizedConfig); + + return $normalizedConfig; + })((array) $config, $finder); + + $createNormalizedDirectories = static function (string $directory) use ($basePath): ?string { + $directory = self::normalizePath($directory, $basePath); + + Assert::false( + is_link($directory), + sprintf( + 'Cannot append the link "%s" to the Finder: links are not supported.', + $directory, + ), + ); + + Assert::directory($directory); + + return $directory; + }; + + $normalizeFileOrDirectory = static function (?string &$fileOrDirectory) use ($basePath, $blacklistFilter): void { + if (null === $fileOrDirectory) { + return; + } + + $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath); + + Assert::false( + is_link($fileOrDirectory), + sprintf( + 'Cannot append the link "%s" to the Finder: links are not supported.', + $fileOrDirectory, + ), + ); + + Assert::true( + file_exists($fileOrDirectory), + sprintf( + 'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).', + $fileOrDirectory, + ), + ); + + if (false === is_file($fileOrDirectory)) { + Assert::directory($fileOrDirectory); + } else { + Assert::file($fileOrDirectory); + } + + if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) { + $fileOrDirectory = null; + } + }; + + foreach ($normalizedConfig as $method => $arguments) { + if ('in' === $method) { + $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments); + } + + if ('exclude' === $method) { + $arguments = array_unique(array_map('trim', $arguments)); + } + + if ('append' === $method) { + array_walk($arguments, $normalizeFileOrDirectory); + + $arguments = [array_filter($arguments)]; + } + + foreach ($arguments as $argument) { + $finder->{$method}($argument); + } + } + + return $finder; + } + + /** + * @param string[] $devPackages + * @param string[] $filesToAppend + * + * @return string[][] + */ + private static function retrieveAllDirectoriesToInclude( + string $basePath, + ?array $decodedJsonContents, + array $devPackages, + array $filesToAppend, + array $excludedPaths, + ): array { + $toString = static fn (string|SplFileInfo $file): string => (string) $file; + + if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) { + $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath); + } else { + $vendorDir = self::normalizePath('vendor', $basePath); + } + + if (file_exists($vendorDir)) { + // Note that some files may not exist. For example installed.json does not exist at all if no dependencies + // are included in composer.json. + $requiredComposerFiles = [ + 'installed.json', + 'installed.php', + 'InstalledVersions.php', + ]; + + foreach ($requiredComposerFiles as $requiredComposerFile) { + $normalizePath = self::normalizePath($vendorDir.'/composer/'.$requiredComposerFile, $basePath); + + if (file_exists($normalizePath)) { + $filesToAppend[] = $normalizePath; + } + } + + $vendorPackages = toArray(values(map( + $toString, + Finder::create() + ->in($vendorDir) + ->directories() + ->depth(1) + ->ignoreUnreadableDirs() + ->filter( + static function (SplFileInfo $fileInfo): ?bool { + if ($fileInfo->isLink()) { + return false; + } + + return null; + }, + ), + ))); + + $vendorPackages = array_diff($vendorPackages, $devPackages); + + if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) { + $files = toArray(values(map( + $toString, + Finder::create() + ->in($basePath) + ->files() + ->depth(0), + ))); + + $directories = toArray(values(map( + $toString, + Finder::create() + ->in($basePath) + ->notPath('vendor') + ->directories() + ->depth(0), + ))); + + return [ + array_merge( + array_diff($files, $excludedPaths), + $filesToAppend, + ), + array_merge( + array_diff($directories, $excludedPaths), + $vendorPackages, + ), + ]; + } + + $paths = $vendorPackages; + } else { + $paths = []; + } + + $autoload = $decodedJsonContents['autoload'] ?? []; + + if (array_key_exists('psr-4', $autoload)) { + foreach ($autoload['psr-4'] as $path) { + /** @var string|string[] $path */ + $composerPaths = (array) $path; + + foreach ($composerPaths as $composerPath) { + $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath; + } + } + } + + if (array_key_exists('psr-0', $autoload)) { + foreach ($autoload['psr-0'] as $path) { + /** @var string|string[] $path */ + $composerPaths = (array) $path; + + foreach ($composerPaths as $composerPath) { + $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath; + } + } + } + + if (array_key_exists('classmap', $autoload)) { + foreach ($autoload['classmap'] as $path) { + // @var string $path + $paths[] = $path; + } + } + + $normalizePath = static fn (string $path): string => Path::isAbsolute($path) + ? Path::canonicalize($path) + : self::normalizePath(trim($path, '/ '), $basePath); + + if (array_key_exists('files', $autoload)) { + foreach ($autoload['files'] as $path) { + /** @var string $path */ + $path = $normalizePath($path); + + Assert::file($path); + Assert::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.'); + + $filesToAppend[] = $path; + } + } + + $files = $filesToAppend; + $directories = []; + + foreach ($paths as $path) { + $path = $normalizePath($path); + + Assert::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.'); + Assert::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.'); + + if (is_file($path)) { + $files[] = $path; + } else { + $directories[] = $path; + } + } + + [$files, $directories] = [ + array_unique($files), + array_unique($directories), + ]; + + return [ + array_diff($files, $excludedPaths), + array_diff($directories, $excludedPaths), + ]; + } + + /** + * @param string[] $directories + * @param string[] $excludedPaths + * @param string[] $devPackages + * + * @return Finder|SplFileInfo[] + */ + private static function retrieveAllFiles( + string $basePath, + array $directories, + ?string $mainScriptPath, + Closure $blacklistFilter, + array $excludedPaths, + array $devPackages, + ): iterable { + if ([] === $directories) { + return []; + } + + $relativeDevPackages = array_map( + static fn (string $packagePath): string => Path::makeRelative($packagePath, $basePath), + $devPackages, + ); + + $finder = Finder::create() + ->files() + ->filter($blacklistFilter) + ->exclude($relativeDevPackages) + ->ignoreVCS(true) + ->ignoreDotFiles(true) + // Remove build files + ->notName('composer.json') + ->notName('composer.lock') + ->notName('Makefile') + ->notName('Vagrantfile') + ->notName('phpstan*.neon*') + ->notName('infection*.json*') + ->notName('humbug*.json*') + ->notName('easy-coding-standard.neon*') + ->notName('phpbench.json*') + ->notName('phpcs.xml*') + ->notName('psalm.xml*') + ->notName('scoper.inc*') + ->notName('box*.json*') + ->notName('phpdoc*.xml*') + ->notName('codecov.yml*') + ->notName('Dockerfile') + ->exclude('build') + ->exclude('dist') + ->exclude('example') + ->exclude('examples') + // Remove documentation + ->notName('*.md') + ->notName('*.rst') + ->notName('/^readme((?!\.php)(\..*+))?$/i') + ->notName('/^upgrade((?!\.php)(\..*+))?$/i') + ->notName('/^contributing((?!\.php)(\..*+))?$/i') + ->notName('/^changelog((?!\.php)(\..*+))?$/i') + ->notName('/^authors?((?!\.php)(\..*+))?$/i') + ->notName('/^conduct((?!\.php)(\..*+))?$/i') + ->notName('/^todo((?!\.php)(\..*+))?$/i') + ->exclude('doc') + ->exclude('docs') + ->exclude('documentation') + // Remove backup files + ->notName('*~') + ->notName('*.back') + ->notName('*.swp') + // Remove tests + ->exclude('tests') + ->exclude('Tests') + ->notName('/phpunit.*\.xml(.dist)?/') + ->notName('/behat.*\.yml(.dist)?/') + ->exclude('spec') + ->exclude('specs') + ->exclude('features') + // Remove CI config + ->exclude('travis') + ->notName('travis.yml') + ->notName('appveyor.yml') + ->notName('build.xml*'); + + if (null !== $mainScriptPath) { + $finder->notPath(Path::makeRelative($mainScriptPath, $basePath)); + } + + $finder->in($directories); + + $excludedPaths = array_unique( + array_filter( + array_map( + static fn (string $path): string => Path::makeRelative($path, $basePath), + $excludedPaths, + ), + static fn (string $path): bool => !str_starts_with($path, '..'), + ), + ); + + foreach ($excludedPaths as $excludedPath) { + $finder->notPath($excludedPath); + } + + return $finder; + } + + /** + * @param string $key Config property name + * + * @return string[] + */ + private static function retrieveDirectoryPaths( + stdClass $raw, + string $key, + string $basePath, + ConfigurationLogger $logger, + ): array { + self::checkIfDefaultValue($logger, $raw, $key, []); + + if (false === isset($raw->{$key})) { + return []; + } + + $directories = $raw->{$key}; + + $normalizeDirectory = static function (string $directory) use ($basePath, $key): string { + $directory = self::normalizePath($directory, $basePath); + + Assert::false( + is_link($directory), + sprintf( + 'Cannot add the link "%s": links are not supported.', + $directory, + ), + ); + + Assert::directory( + $directory, + sprintf( + '"%s" must contain a list of existing directories. Could not find %%s.', + $key, + ), + ); + + return $directory; + }; + + return array_map($normalizeDirectory, $directories); + } + + private static function normalizePath(string $file, string $basePath): string + { + return Path::makeAbsolute(trim($file), $basePath); + } + + /** + * @param string[] $files + * + * @return SplFileInfo[] + */ + private static function wrapInSplFileInfo(array $files): array + { + return array_map( + static fn (string $file): SplFileInfo => new SplFileInfo($file), + $files, + ); + } + + private static function retrieveDumpAutoload(stdClass $raw, ComposerFiles $composerFiles, ConfigurationLogger $logger): bool + { + self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, null); + + $canDumpAutoload = ( + null !== $composerFiles->getComposerJson()->getPath() + && ( + // The composer.lock and installed.json are optional (e.g. if there is no dependencies installed) + // but when one is present, the other must be as well otherwise the dumped autoloader will be broken + ( + null === $composerFiles->getComposerLock()->getPath() + && null === $composerFiles->getInstalledJson()->getPath() + ) + || ( + null !== $composerFiles->getComposerLock()->getPath() + && null !== $composerFiles->getInstalledJson()->getPath() + ) + || ( + null === $composerFiles->getComposerLock()->getPath() + && null !== $composerFiles->getInstalledJson()->getPath() + && [] === $composerFiles->getInstalledJson()->getDecodedContents() + ) + ) + ); + + if ($canDumpAutoload) { + self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, true); + } + + if (false === property_exists($raw, self::DUMP_AUTOLOAD_KEY)) { + return $canDumpAutoload; + } + + $dumpAutoload = $raw->{self::DUMP_AUTOLOAD_KEY} ?? true; + + if (false === $canDumpAutoload && $dumpAutoload) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but has been ignored because the composer.json, composer.lock' + .' and vendor/composer/installed.json files are necessary but could not be found.', + self::DUMP_AUTOLOAD_KEY, + ), + ); + + return false; + } + + return $canDumpAutoload && false !== $dumpAutoload; + } + + private static function retrieveExcludeDevFiles(stdClass $raw, bool $dumpAutoload, ConfigurationLogger $logger): bool + { + self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_DEV_FILES_KEY, $dumpAutoload); + + if (false === property_exists($raw, self::EXCLUDE_DEV_FILES_KEY)) { + return $dumpAutoload; + } + + $excludeDevFiles = $raw->{self::EXCLUDE_DEV_FILES_KEY} ?? $dumpAutoload; + + if (true === $excludeDevFiles && false === $dumpAutoload) { + $logger->addWarning(sprintf( + 'The "%s" setting has been set but has been ignored because the Composer autoloader is not dumped', + self::EXCLUDE_DEV_FILES_KEY, + )); + + return false; + } + + return $excludeDevFiles; + } + + private static function retrieveExcludeComposerFiles(stdClass $raw, ConfigurationLogger $logger): bool + { + self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_COMPOSER_FILES_KEY, true); + + return $raw->{self::EXCLUDE_COMPOSER_FILES_KEY} ?? true; + } + + private static function retrieveCompactors(stdClass $raw, string $basePath, ConfigurationLogger $logger): Compactors + { + self::checkIfDefaultValue($logger, $raw, self::COMPACTORS_KEY, []); + + $compactorClasses = array_unique((array) ($raw->{self::COMPACTORS_KEY} ?? [])); + + // Needs to do this check before returning the compactors in order to properly inform the users about + // possible misconfiguration + $ignoredAnnotations = self::retrievePhpCompactorIgnoredAnnotations($raw, $compactorClasses, $logger); + + if (false === isset($raw->{self::COMPACTORS_KEY})) { + return new Compactors(); + } + + $compactors = new Compactors( + ...self::createCompactors( + $raw, + $basePath, + $compactorClasses, + $ignoredAnnotations, + $logger, + ), + ); + + self::checkCompactorsOrder($logger, $compactors); + + return $compactors; + } + + /** + * @param string[] $compactorClasses + * @param string[]|null $ignoredAnnotations + * + * @return Compactor[] + */ + private static function createCompactors( + stdClass $raw, + string $basePath, + array $compactorClasses, + ?array $ignoredAnnotations, + ConfigurationLogger $logger, + ): array { + return array_map( + static function (string $class) use ($raw, $basePath, $logger, $ignoredAnnotations): Compactor { + Assert::classExists($class, 'The compactor class %s does not exist.'); + Assert::isAOf($class, Compactor::class, sprintf('The class "%s" is not a compactor class.', $class)); + + if (in_array($class, [PhpCompactor::class, 'KevinGH\Box\Compactor\Php'], true)) { + return self::createPhpCompactor($ignoredAnnotations); + } + + if (in_array($class, [PhpScoperCompactor::class, 'KevinGH\Box\Compactor\PhpScoper'], true)) { + return self::createPhpScoperCompactor($raw, $basePath, $logger); + } + + return new $class(); + }, + $compactorClasses, + ); + } + + private static function checkCompactorsOrder(ConfigurationLogger $logger, Compactors $compactors): void + { + $scoperCompactor = false; + + foreach ($compactors->toArray() as $compactor) { + if ($compactor instanceof PhpScoperCompactor) { + $scoperCompactor = true; + } + + if ($compactor instanceof PhpCompactor) { + if (true === $scoperCompactor) { + $logger->addRecommendation( + 'The PHP compactor has been registered after the PhpScoper compactor. It is ' + .'recommended to register the PHP compactor before for a clearer code and faster processing.', + ); + } + + break; + } + } + } + + private static function retrieveCompressionAlgorithm(stdClass $raw, ConfigurationLogger $logger): CompressionAlgorithm + { + self::checkIfDefaultValue($logger, $raw, self::COMPRESSION_KEY, 'NONE'); + + if (false === isset($raw->{self::COMPRESSION_KEY})) { + return CompressionAlgorithm::NONE; + } + + $knownAlgorithms = CompressionAlgorithm::getLabels(); + + Assert::nullOrInArray( + $raw->{self::COMPRESSION_KEY}, + $knownAlgorithms, + sprintf( + 'Unknown compression algorithm %%s. Expected one of "%s".', + implode('", "', $knownAlgorithms), + ), + ); + + return CompressionAlgorithm::fromLabel($raw->{self::COMPRESSION_KEY}); + } + + private static function retrieveFileMode(stdClass $raw, ConfigurationLogger $logger): ?int + { + if (property_exists($raw, self::CHMOD_KEY) && null === $raw->{self::CHMOD_KEY}) { + self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY); + } + + $defaultChmod = intval(0o755, 8); + + if (isset($raw->{self::CHMOD_KEY})) { + $chmod = intval($raw->{self::CHMOD_KEY}, 8); + + if ($defaultChmod === $chmod) { + self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY); + } + + return $chmod; + } + + return $defaultChmod; + } + + private static function retrieveMainScriptPath( + stdClass $raw, + string $basePath, + ?array $decodedJsonContents, + ConfigurationLogger $logger, + ): ?string { + $firstBin = false; + + if (null !== $decodedJsonContents && array_key_exists('bin', $decodedJsonContents)) { + /** @var false|string $firstBin */ + $firstBin = current((array) $decodedJsonContents['bin']); + + if (false !== $firstBin) { + $firstBin = self::normalizePath($firstBin, $basePath); + } + } + + if (isset($raw->{self::MAIN_KEY})) { + $main = $raw->{self::MAIN_KEY}; + + if (is_string($main)) { + $main = self::normalizePath($main, $basePath); + + if ($main === $firstBin) { + $logger->addRecommendation( + sprintf( + 'The "%s" setting can be omitted since is set to its default value', + self::MAIN_KEY, + ), + ); + } + } + } else { + $main = false !== $firstBin ? $firstBin : self::normalizePath(self::DEFAULT_MAIN_SCRIPT, $basePath); + } + + if (is_bool($main)) { + Assert::false( + $main, + 'Cannot "enable" a main script: either disable it with `false` or give the main script file path.', + ); + + return null; + } + + Assert::file($main); + + return $main; + } + + private static function retrieveMainScriptContents(?string $mainScriptPath): ?string + { + if (null === $mainScriptPath) { + return null; + } + + $contents = FS::getFileContents($mainScriptPath); + + // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real + // PHAR entry point file. + // If one needs the shebang, then the main file should act as the stub and be registered as such and in which + // case the main script can be ignored or disabled. + return preg_replace('/^#!.*\s*/', '', $contents); + } + + private static function retrieveComposerFiles(string $basePath): ComposerFiles + { + $retrieveFileAndContents = static function (string $file): ?ComposerFile { + $json = new Json(); + + if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) { + return ComposerFile::createEmpty(); + } + + try { + $contents = (array) $json->decodeFile($file, true); + } catch (ParsingException $exception) { + throw new InvalidArgumentException( + sprintf( + 'Expected the file "%s" to be a valid composer.json file but an error has been found: %s', + $file, + $exception->getMessage(), + ), + 0, + $exception, + ); + } + + return new ComposerFile($file, $contents); + }; + + return new ComposerFiles( + $retrieveFileAndContents(Path::canonicalize($basePath.'/composer.json')), + $retrieveFileAndContents(Path::canonicalize($basePath.'/composer.lock')), + $retrieveFileAndContents(Path::canonicalize($basePath.'/vendor/composer/installed.json')), + ); + } + + /** + * @return string[][] + */ + private static function retrieveMap(stdClass $raw, ConfigurationLogger $logger): array + { + self::checkIfDefaultValue($logger, $raw, self::MAP_KEY, []); + + if (false === isset($raw->{self::MAP_KEY})) { + return []; + } + + $map = []; + + foreach ((array) $raw->{self::MAP_KEY} as $item) { + $processed = []; + + foreach ($item as $match => $replace) { + $processed[Path::canonicalize(trim($match))] = Path::canonicalize(trim($replace)); + } + + if (isset($processed['_empty_'])) { + $processed[''] = $processed['_empty_']; + + unset($processed['_empty_']); + } + + $map[] = $processed; + } + + return $map; + } + + private static function retrieveMetadata(stdClass $raw, ConfigurationLogger $logger) + { + self::checkIfDefaultValue($logger, $raw, self::METADATA_KEY); + + if (false === isset($raw->{self::METADATA_KEY})) { + return null; + } + + $logger->addWarning('Using the "metadata" setting is deprecated and will be removed in 5.0.0.'); + + $metadata = $raw->{self::METADATA_KEY}; + + return is_object($metadata) ? (array) $metadata : $metadata; + } + + /** + * @return string[] The first element is the temporary output path and the second the final one + */ + private static function retrieveOutputPath( + stdClass $raw, + string $basePath, + ?string $mainScriptPath, + ConfigurationLogger $logger, + ): array { + $defaultPath = null; + + if (null !== $mainScriptPath + && 1 === preg_match('/^(?
.*?)(?:\.[\p{L}\d]+)?$/u', $mainScriptPath, $matches) + ) { + $defaultPath = $matches['main'].'.phar'; + } + + if (isset($raw->{self::OUTPUT_KEY})) { + $path = self::normalizePath($raw->{self::OUTPUT_KEY}, $basePath); + + if ($path === $defaultPath) { + self::addRecommendationForDefaultValue($logger, self::OUTPUT_KEY); + } + } elseif (null !== $defaultPath) { + $path = $defaultPath; + } else { + // Last resort, should not happen + $path = self::normalizePath(self::DEFAULT_OUTPUT_FALLBACK, $basePath); + } + + $tmp = $real = $path; + + if (!str_ends_with($real, '.phar')) { + $tmp .= '.phar'; + } + + return [$tmp, $real]; + } + + private static function retrievePrivateKeyPath( + stdClass $raw, + string $basePath, + SigningAlgorithm $signingAlgorithm, + ConfigurationLogger $logger, + ): ?string { + if (property_exists($raw, self::KEY_KEY) && SigningAlgorithm::OPENSSL !== $signingAlgorithm) { + if (null === $raw->{self::KEY_KEY}) { + $logger->addRecommendation( + 'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".', + ); + } else { + $logger->addWarning( + 'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".', + ); + } + + return null; + } + + if (!isset($raw->{self::KEY_KEY})) { + Assert::true( + SigningAlgorithm::OPENSSL !== $signingAlgorithm, + 'Expected to have a private key for OpenSSL signing but none have been provided.', + ); + + return null; + } + + $path = self::normalizePath($raw->{self::KEY_KEY}, $basePath); + + Assert::file($path); + + return $path; + } + + private static function retrievePrivateKeyPassphrase( + stdClass $raw, + SigningAlgorithm $algorithm, + ConfigurationLogger $logger, + ): ?string { + self::checkIfDefaultValue($logger, $raw, self::KEY_PASS_KEY); + + if (false === property_exists($raw, self::KEY_PASS_KEY)) { + return null; + } + + /** @var null|false|string $keyPass */ + $keyPass = $raw->{self::KEY_PASS_KEY}; + + if (SigningAlgorithm::OPENSSL !== $algorithm) { + if (false === $keyPass || null === $keyPass) { + $logger->addRecommendation( + sprintf( + 'The setting "%s" has been set but is unnecessary since the signing algorithm is ' + .'not "OPENSSL".', + self::KEY_PASS_KEY, + ), + ); + } else { + $logger->addWarning( + sprintf( + 'The setting "%s" has been set but ignored the signing algorithm is not "OPENSSL".', + self::KEY_PASS_KEY, + ), + ); + } + + return null; + } + + return is_string($keyPass) ? $keyPass : null; + } + + /** + * @return scalar[] + */ + private static function retrieveReplacements( + stdClass $raw, + ?string $file, + string $path, + ConfigurationLogger $logger, + ): array { + self::checkIfDefaultValue($logger, $raw, self::REPLACEMENTS_KEY, new stdClass()); + + if (null === $file) { + return []; + } + + $replacements = isset($raw->{self::REPLACEMENTS_KEY}) ? (array) $raw->{self::REPLACEMENTS_KEY} : []; + + if (null !== ($git = self::retrievePrettyGitPlaceholder($raw, $logger))) { + $replacements[$git] = self::retrievePrettyGitTag($path); + } + + if (null !== ($git = self::retrieveGitHashPlaceholder($raw, $logger))) { + $replacements[$git] = self::retrieveGitHash($path); + } + + if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw, $logger))) { + $replacements[$git] = self::retrieveGitHash($path, true); + } + + if (null !== ($git = self::retrieveGitTagPlaceholder($raw, $logger))) { + $replacements[$git] = self::retrieveGitTag($path); + } + + if (null !== ($git = self::retrieveGitVersionPlaceholder($raw, $logger))) { + $replacements[$git] = self::retrieveGitVersion($path); + } + + /** + * @var string $datetimeFormat + * @var bool $valueSetByUser + */ + [$datetimeFormat, $valueSetByUser] = self::retrieveDatetimeFormat($raw, $logger); + + if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw, $logger))) { + $replacements[$date] = self::retrieveDatetimeNow($datetimeFormat); + } elseif ($valueSetByUser) { + $logger->addRecommendation( + sprintf( + 'The setting "%s" has been set but is unnecessary because the setting "%s" is not set.', + self::DATETIME_FORMAT_KEY, + self::DATETIME_KEY, + ), + ); + } + + $sigil = self::retrieveReplacementSigil($raw, $logger); + + foreach ($replacements as $key => $value) { + unset($replacements[$key]); + $replacements[$sigil.$key.$sigil] = $value; + } + + return $replacements; + } + + private static function retrieveTimestamp( + stdClass $raw, + SigningAlgorithm $signingAlgorithm, + ConfigurationLogger $logger, + ): ?DateTimeImmutable { + self::checkIfDefaultValue($logger, $raw, self::TIMESTAMP); + + $timestamp = $raw->{self::TIMESTAMP} ?? null; + + if (null === $timestamp) { + return null; + } + + if (SigningAlgorithm::OPENSSL === $signingAlgorithm) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but has been ignored since an OpenSSL signature has been configured (setting "%s").', + self::TIMESTAMP, + self::ALGORITHM_KEY, + ), + ); + + return null; + } + + return new DateTimeImmutable( + $timestamp, + new DateTimeZone('UTC'), + ); + } + + private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string + { + return self::retrievePlaceholder($raw, $logger, self::GIT_KEY); + } + + private static function retrieveGitHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string + { + return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_KEY); + } + + /** + * @param bool $short Use the short version + * + * @return string the commit hash + */ + private static function retrieveGitHash(string $path, bool $short = false): string + { + return self::runGitCommand( + sprintf( + 'git log --pretty="%s" -n1 HEAD', + $short ? '%h' : '%H', + ), + $path, + ); + } + + private static function retrieveGitShortHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string + { + return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_SHORT_KEY); + } + + private static function retrieveGitTagPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string + { + return self::retrievePlaceholder($raw, $logger, self::GIT_TAG_KEY); + } + + private static function retrievePlaceholder(stdClass $raw, ConfigurationLogger $logger, string $key): ?string + { + self::checkIfDefaultValue($logger, $raw, $key); + + return $raw->{$key} ?? null; + } + + private static function retrieveGitTag(string $path): string + { + return self::runGitCommand('git describe --tags HEAD', $path); + } + + private static function retrievePrettyGitTag(string $path): string + { + $version = self::retrieveGitTag($path); + + if (preg_match('/^(?.+)-\d+-g(?[a-f0-9]{7})$/', $version, $matches)) { + return sprintf('%s@%s', $matches['tag'], $matches['hash']); + } + + return $version; + } + + private static function retrieveGitVersionPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string + { + return self::retrievePlaceholder($raw, $logger, self::GIT_VERSION_KEY); + } + + private static function retrieveGitVersion(string $path): ?string + { + try { + return self::retrieveGitTag($path); + } catch (RuntimeException $exception) { + try { + return self::retrieveGitHash($path, true); + } catch (RuntimeException $exception) { + throw new RuntimeException( + sprintf( + 'The tag or commit hash could not be retrieved from "%s": %s', + $path, + $exception->getMessage(), + ), + 0, + $exception, + ); + } + } + } + + private static function retrieveDatetimeNowPlaceHolder(stdClass $raw, ConfigurationLogger $logger): ?string + { + return self::retrievePlaceholder($raw, $logger, self::DATETIME_KEY); + } + + private static function retrieveDatetimeNow(string $format): string + { + return (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format($format); + } + + private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): array + { + self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DEFAULT_DATETIME_FORMAT); + self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DATETIME_FORMAT_DEPRECATED_KEY); + + if (isset($raw->{self::DATETIME_FORMAT_KEY})) { + $format = $raw->{self::DATETIME_FORMAT_KEY}; + } elseif (isset($raw->{self::DATETIME_FORMAT_DEPRECATED_KEY})) { + @trigger_error( + sprintf( + 'The "%s" is deprecated, use "%s" setting instead.', + self::DATETIME_FORMAT_DEPRECATED_KEY, + self::DATETIME_FORMAT_KEY, + ), + E_USER_DEPRECATED, + ); + $logger->addWarning( + sprintf( + 'The "%s" is deprecated, use "%s" setting instead.', + self::DATETIME_FORMAT_DEPRECATED_KEY, + self::DATETIME_FORMAT_KEY, + ), + ); + + $format = $raw->{self::DATETIME_FORMAT_DEPRECATED_KEY}; + } else { + $format = null; + } + + if (null !== $format) { + $formattedDate = (new DateTimeImmutable())->format($format); + + Assert::false( + false === $formattedDate || $formattedDate === $format, + sprintf( + 'Expected the datetime format to be a valid format: "%s" is not', + $format, + ), + ); + + return [$format, true]; + } + + return [self::DEFAULT_DATETIME_FORMAT, false]; + } + + private static function retrieveReplacementSigil(stdClass $raw, ConfigurationLogger $logger): string + { + return self::retrievePlaceholder($raw, $logger, self::REPLACEMENT_SIGIL_KEY) ?? self::DEFAULT_REPLACEMENT_SIGIL; + } + + /** + * @return null|non-empty-string + */ + private static function retrieveShebang(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string + { + self::checkIfDefaultValue($logger, $raw, self::SHEBANG_KEY, self::DEFAULT_SHEBANG); + + if (false === isset($raw->{self::SHEBANG_KEY})) { + return self::DEFAULT_SHEBANG; + } + + $shebang = $raw->{self::SHEBANG_KEY}; + + if (false === $shebang) { + if (false === $stubIsGenerated) { + $logger->addRecommendation( + sprintf( + 'The "%s" has been set to `false` but is unnecessary since the Box built-in stub is not' + .' being used', + self::SHEBANG_KEY, + ), + ); + } + + return null; + } + + Assert::string($shebang, 'Expected shebang to be either a string, false or null, found true'); + + $shebang = trim($shebang); + + Assert::notEmpty($shebang, 'The shebang should not be empty.'); + Assert::true( + str_starts_with($shebang, '#!'), + sprintf( + 'The shebang line must start with "#!". Got "%s" instead', + $shebang, + ), + ); + + if (false === $stubIsGenerated) { + $logger->addWarning( + sprintf( + 'The "%s" has been set but ignored since it is used only with the Box built-in stub which is not' + .' used', + self::SHEBANG_KEY, + ), + ); + } + + return $shebang; + } + + private static function retrieveSigningAlgorithm(stdClass $raw, ConfigurationLogger $logger): SigningAlgorithm + { + if (property_exists($raw, self::ALGORITHM_KEY) && null === $raw->{self::ALGORITHM_KEY}) { + self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY); + } + + if (false === isset($raw->{self::ALGORITHM_KEY})) { + return self::DEFAULT_SIGNING_ALGORITHM; + } + + $algorithmLabel = mb_strtoupper($raw->{self::ALGORITHM_KEY}); + $algorithm = SigningAlgorithm::fromLabel($algorithmLabel); + + if (self::DEFAULT_SIGNING_ALGORITHM === $algorithm) { + self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY); + } + + if (SigningAlgorithm::OPENSSL === $algorithm) { + $logger->addWarning( + 'Using an OpenSSL signature is deprecated and will be removed in 5.0.0. Please check ' + .'https://github.com/box-project/box/blob/main/doc/phar-signing.md for alternatives.', + ); + } + + return $algorithm; + } + + private static function retrieveStubBannerContents(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string + { + self::checkIfDefaultValue($logger, $raw, self::BANNER_KEY, self::getDefaultBanner()); + + if (false === isset($raw->{self::BANNER_KEY})) { + return self::getDefaultBanner(); + } + + $banner = $raw->{self::BANNER_KEY}; + + if (false === $banner) { + if (false === $stubIsGenerated) { + $logger->addRecommendation( + sprintf( + 'The "%s" setting has been set but is unnecessary since the Box built-in stub is not ' + .'being used', + self::BANNER_KEY, + ), + ); + } + + return null; + } + + Assert::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value'); + + if (is_array($banner)) { + $banner = implode("\n", $banner); + } + + if (false === $stubIsGenerated) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but is ignored since the Box built-in stub is not being used', + self::BANNER_KEY, + ), + ); + } + + return $banner; + } + + private static function getDefaultBanner(): string + { + return sprintf(self::DEFAULT_BANNER, get_box_version()); + } + + private static function retrieveStubBannerPath( + stdClass $raw, + string $basePath, + bool $stubIsGenerated, + ConfigurationLogger $logger, + ): ?string { + self::checkIfDefaultValue($logger, $raw, self::BANNER_FILE_KEY); + + if (false === isset($raw->{self::BANNER_FILE_KEY})) { + return null; + } + + $bannerFile = Path::makeAbsolute($raw->{self::BANNER_FILE_KEY}, $basePath); + + Assert::file($bannerFile); + + if (false === $stubIsGenerated) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but is ignored since the Box built-in stub is not being used', + self::BANNER_FILE_KEY, + ), + ); + } + + return $bannerFile; + } + + private static function normalizeStubBannerContents(?string $contents): ?string + { + if (null === $contents) { + return null; + } + + $banner = explode("\n", $contents); + $banner = array_map('trim', $banner); + + return implode("\n", $banner); + } + + private static function retrieveStubPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string + { + self::checkIfDefaultValue($logger, $raw, self::STUB_KEY); + + if (isset($raw->{self::STUB_KEY}) && is_string($raw->{self::STUB_KEY})) { + $stubPath = Path::makeAbsolute($raw->{self::STUB_KEY}, $basePath); + + Assert::file($stubPath); + + return $stubPath; + } + + return null; + } + + private static function retrieveInterceptsFileFunctions( + stdClass $raw, + bool $stubIsGenerated, + ConfigurationLogger $logger, + ): bool { + self::checkIfDefaultValue($logger, $raw, self::INTERCEPT_KEY, false); + + if (false === isset($raw->{self::INTERCEPT_KEY})) { + return false; + } + + $intercept = $raw->{self::INTERCEPT_KEY}; + + if ($intercept && false === $stubIsGenerated) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but is ignored since the Box built-in stub is not being used', + self::INTERCEPT_KEY, + ), + ); + } + + return $intercept; + } + + private static function retrievePromptForPrivateKey( + stdClass $raw, + SigningAlgorithm $signingAlgorithm, + ConfigurationLogger $logger, + ): bool { + if (isset($raw->{self::KEY_PASS_KEY}) && true === $raw->{self::KEY_PASS_KEY}) { + if (SigningAlgorithm::OPENSSL !== $signingAlgorithm) { + $logger->addWarning( + 'A prompt for password for the private key has been requested but ignored since the signing ' + .'algorithm used is not "OPENSSL.', + ); + + return false; + } + + return true; + } + + return false; + } + + private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath, ConfigurationLogger $logger): bool + { + self::checkIfDefaultValue($logger, $raw, self::STUB_KEY, true); + + return null === $stubPath && (false === isset($raw->{self::STUB_KEY}) || false !== $raw->{self::STUB_KEY}); + } + + private static function retrieveCheckRequirements( + stdClass $raw, + bool $hasComposerJson, + bool $hasComposerLock, + bool $pharStubUsed, + ConfigurationLogger $logger, + ): bool { + self::checkIfDefaultValue($logger, $raw, self::CHECK_REQUIREMENTS_KEY, true); + + if (false === property_exists($raw, self::CHECK_REQUIREMENTS_KEY)) { + return $hasComposerJson || $hasComposerLock; + } + + /** @var bool $checkRequirements */ + $checkRequirements = $raw->{self::CHECK_REQUIREMENTS_KEY} ?? true; + + // TODO: in 5.0 we no longer care about the composer.json + if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) { + $logger->addWarning( + 'The requirement checker could not be used because the composer.json and composer.lock file could not ' + .'be found.', + ); + + return false; + } + + if ($checkRequirements && false === $hasComposerLock) { + // TODO: in 5.0: + // - adjust the warning + // - return false here to skip the requirement checker + $logger->addWarning( + 'Enabling the requirement checker when there is no composer.lock is deprecated. In the future the ' + .'requirement checker will be forcefully skipped in this scenario.', + ); + } + + if ($checkRequirements && $pharStubUsed) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but has been ignored since the PHAR built-in stub is being ' + .'used.', + self::CHECK_REQUIREMENTS_KEY, + ), + ); + } + + return $checkRequirements; + } + + private static function retrievePhpScoperConfig(stdClass $raw, string $basePath, ConfigurationLogger $logger): PhpScoperConfiguration + { + self::checkIfDefaultValue($logger, $raw, self::PHP_SCOPER_KEY, self::PHP_SCOPER_CONFIG); + + if (!isset($raw->{self::PHP_SCOPER_KEY})) { + $configFilePath = Path::makeAbsolute(self::PHP_SCOPER_CONFIG, $basePath); + $configFilePath = file_exists($configFilePath) ? $configFilePath : null; + + return PhpScoperConfigurationFactory::create($configFilePath); + } + + $configFile = $raw->{self::PHP_SCOPER_KEY}; + + Assert::string($configFile); + + $configFilePath = Path::makeAbsolute($configFile, $basePath); + + Assert::file($configFilePath); + Assert::readable($configFilePath); + + return PhpScoperConfigurationFactory::create($configFilePath); + } + + /** + * Runs a Git command on the repository. + * + * @return string The trimmed output from the command + */ + private static function runGitCommand(string $command, string $path): string + { + $process = Process::fromShellCommandline($command, $path); + $process->run(); + + if ($process->isSuccessful()) { + return trim($process->getOutput()); + } + + throw new RuntimeException( + sprintf( + 'The tag or commit hash could not be retrieved from "%s": %s', + $path, + $process->getErrorOutput(), + ), + 0, + new ProcessFailedException($process), + ); + } + + /** + * @param string[] $compactorClasses + * + * @return string[]|null + */ + private static function retrievePhpCompactorIgnoredAnnotations( + stdClass $raw, + array $compactorClasses, + ConfigurationLogger $logger, + ): ?array { + $hasPhpCompactor = in_array(PhpCompactor::class, $compactorClasses, true); + + self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, true); + self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, null); + + if (false === property_exists($raw, self::ANNOTATIONS_KEY)) { + return self::DEFAULT_IGNORED_ANNOTATIONS; + } + + if (false === $hasPhpCompactor) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but is ignored since no PHP compactor has been configured', + self::ANNOTATIONS_KEY, + ), + ); + } + + /** @var null|bool|stdClass $annotations */ + $annotations = $raw->{self::ANNOTATIONS_KEY}; + + if (true === $annotations || null === $annotations) { + return self::DEFAULT_IGNORED_ANNOTATIONS; + } + + if (false === $annotations) { + return null; + } + + if (false === property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but no "%s" setting has been found, hence "%s" is treated as' + .' if it is set to `false`', + self::ANNOTATIONS_KEY, + self::IGNORED_ANNOTATIONS_KEY, + self::ANNOTATIONS_KEY, + ), + ); + + return null; + } + + $ignored = []; + + if (property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY) + && in_array($ignored = $annotations->{self::IGNORED_ANNOTATIONS_KEY}, [null, []], true) + ) { + self::addRecommendationForDefaultValue($logger, self::ANNOTATIONS_KEY.'#'.self::IGNORED_ANNOTATIONS_KEY); + + return (array) $ignored; + } + + return $ignored; + } + + private static function createPhpCompactor(?array $ignoredAnnotations): Compactor + { + if (null === $ignoredAnnotations) { + return new PhpCompactor(null); + } + + $ignoredAnnotations = array_values( + array_filter( + array_map( + static fn (string $annotation): ?string => mb_strtolower(trim($annotation)), + $ignoredAnnotations, + ), + ), + ); + + return PhpCompactor::create($ignoredAnnotations); + } + + private static function createPhpScoperCompactor( + stdClass $raw, + string $basePath, + ConfigurationLogger $logger, + ): Compactor { + $phpScoperConfig = self::configurePhpScoperPrefix( + self::retrievePhpScoperConfig($raw, $basePath, $logger), + ); + + $excludedFilePaths = array_values( + array_unique( + array_map( + static fn (string $path): string => Path::makeRelative($path, $basePath), + array_keys( + $phpScoperConfig->getExcludedFilesWithContents(), + ), + ), + ), + ); + + return new PhpScoperCompactor( + new SerializableScoper($phpScoperConfig, ...$excludedFilePaths), + ); + } + + private static function configurePhpScoperPrefix(PhpScoperConfiguration $phpScoperConfig): PhpScoperConfiguration + { + $prefix = $phpScoperConfig->getPrefix(); + if (!str_starts_with($prefix, '_PhpScoper')) { + return $phpScoperConfig; + } + + return $phpScoperConfig->withPrefix(unique_id('_HumbugBox')); + } + + private static function checkIfDefaultValue( + ConfigurationLogger $logger, + stdClass $raw, + string $key, + $defaultValue = null, + ): void { + if (false === property_exists($raw, $key)) { + return; + } + + $value = $raw->{$key}; + + if (null === $value + || (false === is_object($defaultValue) && $defaultValue === $value) + || (is_object($defaultValue) && $defaultValue == $value) + ) { + $logger->addRecommendation( + sprintf( + 'The "%s" setting can be omitted since is set to its default value', + $key, + ), + ); + } + } + + private static function addRecommendationForDefaultValue(ConfigurationLogger $logger, string $key): void + { + $logger->addRecommendation( + sprintf( + 'The "%s" setting can be omitted since is set to its default value', + $key, + ), + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Configuration/ConfigurationLoader.php b/fixtures/bench/with-compactors/src/Configuration/ConfigurationLoader.php new file mode 100644 index 000000000..81405c65d --- /dev/null +++ b/fixtures/bench/with-compactors/src/Configuration/ConfigurationLoader.php @@ -0,0 +1,50 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Configuration; + +use KevinGH\Box\Json\Json; +use stdClass; + +/** + * @private + */ +final class ConfigurationLoader +{ + private const SCHEMA_FILE = __DIR__.'/../../res/schema.json'; + + public function __construct(private readonly Json $json = new Json()) + { + } + + /** + * @param null|non-empty-string $file + */ + public function loadFile(?string $file): Configuration + { + if (null === $file) { + return Configuration::create(null, new stdClass()); + } + + $json = $this->json->decodeFile($file); + + $this->json->validate( + $file, + $json, + $this->json->decodeFile(self::SCHEMA_FILE), + ); + + return Configuration::create($file, $json); + } +} diff --git a/fixtures/bench/with-compactors/src/Configuration/ConfigurationLogger.php b/fixtures/bench/with-compactors/src/Configuration/ConfigurationLogger.php new file mode 100644 index 000000000..d416faf32 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Configuration/ConfigurationLogger.php @@ -0,0 +1,69 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Configuration; + +use Webmozart\Assert\Assert; +use function array_keys; +use function trim; + +/** + * @private + */ +final class ConfigurationLogger +{ + /** + * @var array + */ + private array $recommendations = []; + + /** + * @var array + */ + private array $warnings = []; + + public function addRecommendation(string $message): void + { + $message = trim($message); + + Assert::false('' === $message, 'Expected to have a message but a blank string was given instead.'); + + $this->recommendations[$message] = true; + } + + /** + * @return list + */ + public function getRecommendations(): array + { + return array_keys($this->recommendations); + } + + public function addWarning(string $message): void + { + $message = trim($message); + + Assert::false('' === $message, 'Expected to have a message but a blank string was given instead.'); + + $this->warnings[$message] = true; + } + + /** + * @return list + */ + public function getWarnings(): array + { + return array_keys($this->warnings); + } +} diff --git a/fixtures/bench/with-compactors/src/Configuration/ExportableConfiguration.php b/fixtures/bench/with-compactors/src/Configuration/ExportableConfiguration.php new file mode 100644 index 000000000..2bc2ff595 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Configuration/ExportableConfiguration.php @@ -0,0 +1,173 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Configuration; + +use Closure; +use KevinGH\Box\Compactor\Compactors; +use KevinGH\Box\Composer\ComposerFile; +use KevinGH\Box\MapFile; +use SplFileInfo; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\Finder\SplFileInfo as SymfonySplFileInfo; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use function array_map; +use function iter\values; +use function sort; +use const SORT_STRING; + +/** + * A class similar to {@see Configuration} but for which the property types and values might change in order to improve + * its readability when dumping it into a file. + * + * @internal + */ +final class ExportableConfiguration +{ + public static function create(Configuration $configuration): self + { + $normalizePath = self::createPathNormalizer($configuration->getBasePath()); + $normalizePaths = static function (array $files) use ($normalizePath): array { + $files = array_map($normalizePath, $files); + sort($files, SORT_STRING); + + return $files; + }; + + $composerJson = $configuration->getComposerJson(); + $composerLock = $configuration->getComposerLock(); + + return new self( + $normalizePath($configuration->getConfigurationFile()), + $configuration->getAlias(), + $configuration->getBasePath(), + new ComposerFile( + $normalizePath($composerJson), + $configuration->getDecodedComposerJsonContents() ?? [], + ), + new ComposerFile( + $normalizePath($composerLock), + $configuration->getDecodedComposerLockContents() ?? [], + ), + $normalizePaths($configuration->getFiles()), + $normalizePaths($configuration->getBinaryFiles()), + $configuration->hasAutodiscoveredFiles(), + $configuration->dumpAutoload(), + $configuration->excludeComposerFiles(), + $configuration->excludeDevFiles(), + array_map('get_class', $configuration->getCompactors()->toArray()), + $configuration->getCompressionAlgorithm()->name, + '0'.decoct($configuration->getFileMode()), + $normalizePath($configuration->getMainScriptPath()), + $configuration->getMainScriptContents(), + $configuration->getFileMapper(), + $configuration->getMetadata(), + $normalizePath($configuration->getTmpOutputPath()), + $normalizePath($configuration->getOutputPath()), + // TODO: remove this from the dump & add the SensitiveParam annotation + $configuration->getPrivateKeyPassphrase(), + $normalizePath($configuration->getPrivateKeyPath()), + $configuration->promptForPrivateKey(), + $configuration->getReplacements(), + $configuration->getShebang(), + $configuration->getSigningAlgorithm()->name, + $configuration->getStubBannerContents(), + $normalizePath($configuration->getStubBannerPath()), + $normalizePath($configuration->getStubPath()), + $configuration->isInterceptFileFuncs(), + $configuration->isStubGenerated(), + $configuration->checkRequirements(), + $configuration->getWarnings(), + $configuration->getRecommendations(), + ); + } + + /** + * @return Closure(null|SplFileInfo|string): string|null + */ + private static function createPathNormalizer(string $basePath): Closure + { + return static function (null|SplFileInfo|string $path) use ($basePath): ?string { + if (null === $path) { + return null; + } + + if ($path instanceof SplFileInfo) { + $path = $path->getPathname(); + } + + return Path::makeRelative($path, $basePath); + }; + } + + /** @noinspection PhpPropertyOnlyWrittenInspection */ + private function __construct( + private readonly ?string $file, + private readonly string $alias, + private readonly string $basePath, + private readonly ComposerFile $composerJson, + private readonly ComposerFile $composerLock, + private readonly array $files, + private readonly array $binaryFiles, + private readonly bool $autodiscoveredFiles, + private readonly bool $dumpAutoload, + private readonly bool $excludeComposerFiles, + private readonly bool $excludeDevFiles, + private readonly Compactors|array $compactors, + private readonly string $compressionAlgorithm, + private readonly int|string|null $fileMode, + private readonly ?string $mainScriptPath, + private readonly ?string $mainScriptContents, + private readonly MapFile $fileMapper, + private readonly mixed $metadata, + private readonly string $tmpOutputPath, + private readonly string $outputPath, + private readonly ?string $privateKeyPassphrase, + private readonly ?string $privateKeyPath, + private readonly bool $promptForPrivateKey, + private readonly array $processedReplacements, + private readonly ?string $shebang, + private readonly string $signingAlgorithm, + private readonly ?string $stubBannerContents, + private readonly ?string $stubBannerPath, + private readonly ?string $stubPath, + private readonly bool $isInterceptFileFuncs, + private readonly bool $isStubGenerated, + private readonly bool $checkRequirements, + private readonly array $warnings, + private readonly array $recommendations, + ) { + } + + public function export(): string + { + $cloner = new VarCloner(); + $cloner->setMaxItems(-1); + $cloner->setMaxString(-1); + + $normalizePath = self::createPathNormalizer($this->basePath); + $splInfoCaster = static fn (SplFileInfo $fileInfo): array => [$normalizePath($fileInfo)]; + + $cloner->addCasters([ + SplFileInfo::class => $splInfoCaster, + SymfonySplFileInfo::class => $splInfoCaster, + ]); + + return (new CliDumper())->dump( + $cloner->cloneVar($this), + true, + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Configuration/NoConfigurationFound.php b/fixtures/bench/with-compactors/src/Configuration/NoConfigurationFound.php new file mode 100644 index 000000000..b4a43771a --- /dev/null +++ b/fixtures/bench/with-compactors/src/Configuration/NoConfigurationFound.php @@ -0,0 +1,29 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Configuration; + +use RuntimeException; +use Throwable; + +/** + * @private + */ +final class NoConfigurationFound extends RuntimeException +{ + public function __construct(string $message = 'The configuration file could not be found.', int $code = 0, ?Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Application.php b/fixtures/bench/with-compactors/src/Console/Application.php new file mode 100644 index 000000000..47d6e209b --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Application.php @@ -0,0 +1,109 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Fidry\Console\Application\Application as FidryApplication; +use function KevinGH\Box\get_box_version; +use function sprintf; +use function trim; + +/** + * @private + */ +final class Application implements FidryApplication +{ + private string $version; + private string $releaseDate; + private string $header; + + public function __construct( + private string $name = 'Box', + ?string $version = null, + string $releaseDate = '@release-date@', + private bool $autoExit = true, + private bool $catchExceptions = true, + ) { + $this->version = $version ?? get_box_version(); + $this->releaseDate = !str_contains($releaseDate, '@') ? $releaseDate : ''; + } + + public function getName(): string + { + return $this->name; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getLongVersion(): string + { + return trim( + sprintf( + '%s version %s %s', + $this->getName(), + $this->getVersion(), + $this->releaseDate, + ), + ); + } + + public function getHelp(): string + { + return $this->getHeader(); + } + + public function getHeader(): string + { + if (!isset($this->header)) { + $this->header = Logo::LOGO_ASCII.$this->getLongVersion(); + } + + return $this->header; + } + + public function getCommands(): array + { + return [ + new Command\Composer\ComposerCheckVersion(), + new Command\Composer\ComposerVendorDir(), + new Command\Compile($this->getHeader()), + new Command\Diff(), + new Command\Info(), + new Command\Process(), + new Command\Extract(), + new Command\Validate(), + new Command\Verify(), + new Command\GenerateDockerFile(), + new Command\Namespace_(), + ]; + } + + public function getDefaultCommand(): string + { + return 'list'; + } + + public function isAutoExitEnabled(): bool + { + return $this->autoExit; + } + + public function areExceptionsCaught(): bool + { + return $this->catchExceptions; + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/ChangeWorkingDirOption.php b/fixtures/bench/with-compactors/src/Console/Command/ChangeWorkingDirOption.php new file mode 100644 index 000000000..ac676582a --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/ChangeWorkingDirOption.php @@ -0,0 +1,67 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\IO; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputOption; +use Webmozart\Assert\Assert; +use function chdir; +use function getcwd; +use function sprintf; + +/** + * @private + */ +final class ChangeWorkingDirOption +{ + /** @internal using a static property as traits cannot have constants */ + private const WORKING_DIR_OPT = 'working-dir'; + + public static function getOptionInput(): InputOption + { + return new InputOption( + self::WORKING_DIR_OPT, + 'd', + InputOption::VALUE_REQUIRED, + 'If specified, use the given directory as working directory.', + null, + ); + } + + public static function changeWorkingDirectory(IO $io): void + { + $workingDir = $io->getTypedOption(self::WORKING_DIR_OPT)->asNullableNonEmptyString(); + + if (null === $workingDir) { + return; + } + + Assert::directory( + $workingDir, + 'Could not change the working directory to "%s": directory does not exists or file is not a directory.', + ); + + if (false === chdir($workingDir)) { + throw new RuntimeException( + sprintf( + 'Failed to change the working directory to "%s" from "%s".', + $workingDir, + getcwd(), + ), + ); + } + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/Compile.php b/fixtures/bench/with-compactors/src/Console/Command/Compile.php new file mode 100644 index 000000000..11c817eb9 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/Compile.php @@ -0,0 +1,993 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Amp\MultiReasonException; +use DateTimeImmutable; +use DateTimeInterface; +use Fidry\Console\Command\Command; +use Fidry\Console\Command\CommandAware; +use Fidry\Console\Command\CommandAwareness; +use Fidry\Console\Command\Configuration as CommandConfiguration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use Fidry\FileSystem\FileSystem; +use Fidry\FileSystem\FS; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\Amp\FailureCollector; +use KevinGH\Box\Box; +use KevinGH\Box\Compactor\Compactor; +use KevinGH\Box\Composer\CompilerPsrLogger; +use KevinGH\Box\Composer\ComposerConfiguration; +use KevinGH\Box\Composer\ComposerOrchestrator; +use KevinGH\Box\Composer\ComposerProcessFactory; +use KevinGH\Box\Composer\IncompatibleComposerVersion; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\Console\Logger\CompilerLogger; +use KevinGH\Box\Console\MessageRenderer; +use KevinGH\Box\Console\OpenFileDescriptorLimiter; +use KevinGH\Box\Console\PhpSettingsChecker; +use KevinGH\Box\Constants; +use KevinGH\Box\MapFile; +use KevinGH\Box\Phar\CompressionAlgorithm; +use KevinGH\Box\Phar\SigningAlgorithm; +use KevinGH\Box\RequirementChecker\DecodedComposerJson; +use KevinGH\Box\RequirementChecker\DecodedComposerLock; +use KevinGH\Box\RequirementChecker\RequirementsDumper; +use KevinGH\Box\StubGenerator; +use RuntimeException; +use stdClass; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Filesystem\Path; +use function array_map; +use function array_shift; +use function count; +use function decoct; +use function explode; +use function file_exists; +use function filesize; +use function implode; +use function is_callable; +use function is_string; +use function KevinGH\Box\format_size; +use function KevinGH\Box\format_time; +use function memory_get_peak_usage; +use function memory_get_usage; +use function microtime; +use function putenv; +use function Safe\getcwd; +use function sprintf; +use function var_export; +use const PHP_EOL; + +/** + * @private + */ +final class Compile implements CommandAware +{ + use CommandAwareness; + + public const NAME = 'compile'; + + private const HELP = <<<'HELP' + The %command.name% command will compile code in a new PHAR based on a variety of settings. + + This command relies on a configuration file for loading + PHAR packaging settings. If a configuration file is not + specified through the --config|-c option, one of + the following files will be used (in order): box.json, + box.json.dist + + The configuration file is actually a JSON object saved to a file. For more + information check the documentation online: + + https://github.com/humbug/box + + HELP; + + private const DEBUG_OPTION = 'debug'; + private const NO_PARALLEL_PROCESSING_OPTION = 'no-parallel'; + private const NO_RESTART_OPTION = 'no-restart'; + private const DEV_OPTION = 'dev'; + private const NO_CONFIG_OPTION = 'no-config'; + private const WITH_DOCKER_OPTION = 'with-docker'; + private const COMPOSER_BIN_OPTION = 'composer-bin'; + private const ALLOW_COMPOSER_COMPOSER_CHECK_FAILURE_OPTION = 'allow-composer-check-failure'; + + private const DEBUG_DIR = '.box_dump'; + + public function __construct(private string $header) + { + } + + public function getConfiguration(): CommandConfiguration + { + return new CommandConfiguration( + self::NAME, + '🔨 Compiles an application into a PHAR', + self::HELP, + [], + [ + new InputOption( + self::DEBUG_OPTION, + null, + InputOption::VALUE_NONE, + 'Dump the files added to the PHAR in a `'.self::DEBUG_DIR.'` directory', + ), + new InputOption( + self::NO_PARALLEL_PROCESSING_OPTION, + null, + InputOption::VALUE_NONE, + 'Disable the parallel processing', + ), + new InputOption( + self::NO_RESTART_OPTION, + null, + InputOption::VALUE_NONE, + 'Do not restart the PHP process. Box restarts the process by default to disable xdebug and set `phar.readonly=0`', + ), + new InputOption( + self::DEV_OPTION, + null, + InputOption::VALUE_NONE, + 'Skips the compression step', + ), + new InputOption( + self::NO_CONFIG_OPTION, + null, + InputOption::VALUE_NONE, + 'Ignore the config file even when one is specified with the --config option', + ), + new InputOption( + self::WITH_DOCKER_OPTION, + null, + InputOption::VALUE_NONE, + 'Generates a Dockerfile', + ), + new InputOption( + self::COMPOSER_BIN_OPTION, + null, + InputOption::VALUE_REQUIRED, + 'Composer binary to use', + ), + new InputOption( + self::ALLOW_COMPOSER_COMPOSER_CHECK_FAILURE_OPTION, + null, + InputOption::VALUE_NONE, + 'To continue even if an unsupported Composer version is detected', + ), + ConfigOption::getOptionInput(), + ChangeWorkingDirOption::getOptionInput(), + ], + ); + } + + public function execute(IO $io): int + { + if ($io->getTypedOption(self::NO_RESTART_OPTION)->asBoolean()) { + putenv(Constants::ALLOW_XDEBUG.'=1'); + } + + $debug = $io->getTypedOption(self::DEBUG_OPTION)->asBoolean(); + + if ($debug) { + $io->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + } + + PhpSettingsChecker::check($io); + + $enableParallelization = $io->getTypedOption(self::NO_PARALLEL_PROCESSING_OPTION)->asBoolean(); + + if ($enableParallelization) { + $io->writeln( + '[debug] Disabled parallel processing', + OutputInterface::VERBOSITY_DEBUG, + ); + } + + ChangeWorkingDirOption::changeWorkingDirectory($io); + + $io->writeln($this->header); + + $config = $io->getTypedOption(self::NO_CONFIG_OPTION)->asBoolean() + ? Configuration::create(null, new stdClass()) + : ConfigOption::getConfig($io, true); + $config->setComposerBin(self::getComposerBin($io)); + $path = $config->getOutputPath(); + + $logger = new CompilerLogger($io); + + $startTime = microtime(true); + + $logger->logStartBuilding($path); + + $this->removeExistingArtifacts($config, $logger, $debug); + + // Adding files might result in opening a lot of files. Either because not parallelized or when creating the + // workers for parallelization. + // As a result, we bump the file descriptor to an arbitrary number to ensure this process can run correctly + $restoreLimit = OpenFileDescriptorLimiter::bumpLimit(2048, $io); + + try { + $box = $this->createPhar($config, $logger, $io, $debug, $enableParallelization); + } finally { + $restoreLimit(); + } + + self::correctPermissions($path, $config, $logger); + + self::logEndBuilding($config, $logger, $io, $box, $path, $startTime); + + if ($io->getTypedOption(self::WITH_DOCKER_OPTION)->asBoolean()) { + return $this->generateDockerFile($io); + } + + return ExitCode::SUCCESS; + } + + private function createPhar( + Configuration $config, + CompilerLogger $logger, + IO $io, + bool $enableParallelization, + bool $debug, + ): Box { + $tmpOutputPath = $config->getTmpOutputPath(); + $box = Box::create($tmpOutputPath, enableParallelization: $enableParallelization); + $composerOrchestrator = new ComposerOrchestrator( + ComposerProcessFactory::create( + $config->getComposerBin(), + $io, + ), + new CompilerPsrLogger($logger), + new FileSystem(), + ); + + self::checkComposerVersion($composerOrchestrator, $config, $logger, $io); + + $box->startBuffering(); + + self::registerReplacementValues($config, $box, $logger); + self::registerCompactors($config, $box, $logger); + self::registerFileMapping($config, $box, $logger); + + // Registering the main script _before_ adding the rest if of the files is _very_ important. The temporary + // file used for debugging purposes and the Composer dump autoloading will not work correctly otherwise. + $main = self::registerMainScript($config, $box, $logger); + + $check = self::registerRequirementsChecker($config, $box, $logger); + + self::addFiles($config, $box, $logger, $io); + + self::registerStub($config, $box, $main, $check, $logger); + self::configureMetadata($config, $box, $logger); + + self::commit($box, $composerOrchestrator, $config, $logger); + + self::checkComposerFiles($box, $config, $logger); + + if ($debug) { + $box->extractTo(self::DEBUG_DIR, true); + } + + self::configureCompressionAlgorithm( + $config, + $box, + $io->getTypedOption(self::DEV_OPTION)->asBoolean(), + $io, + $logger, + ); + + self::signPhar($config, $box, $tmpOutputPath, $io, $logger); + + if ($tmpOutputPath !== $config->getOutputPath()) { + FS::rename($tmpOutputPath, $config->getOutputPath()); + } + + return $box; + } + + private static function getComposerBin(IO $io): ?string + { + $composerBin = $io->getTypedOption(self::COMPOSER_BIN_OPTION)->asNullableNonEmptyString(); + + return null === $composerBin ? null : Path::makeAbsolute($composerBin, getcwd()); + } + + private function removeExistingArtifacts(Configuration $config, CompilerLogger $logger, bool $debug): void + { + $path = $config->getOutputPath(); + + if ($debug) { + FS::remove(self::DEBUG_DIR); + + FS::dumpFile( + self::DEBUG_DIR.'/.box_configuration', + ConfigurationExporter::export($config), + ); + } + + if (false === file_exists($path)) { + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Removing the existing PHAR "%s"', + $path, + ), + ); + + FS::remove($path); + } + + private static function checkComposerVersion( + ComposerOrchestrator $composerOrchestrator, + Configuration $config, + CompilerLogger $logger, + IO $io, + ): void { + if (!$config->dumpAutoload()) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Skipping the Composer compatibility check: the autoloader is not dumped', + ); + + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Checking Composer compatibility', + ); + + try { + $composerOrchestrator->checkVersion(); + + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + 'Supported version detected', + ); + } catch (IncompatibleComposerVersion $incompatibleComposerVersion) { + if ($io->getTypedOption(self::ALLOW_COMPOSER_COMPOSER_CHECK_FAILURE_OPTION)->asBoolean()) { + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + 'Warning! Incompatible composer version detected: '.$incompatibleComposerVersion->getMessage(), + ); + + return; // Swallow the exception + } + + throw $incompatibleComposerVersion; + } + } + + private static function registerReplacementValues(Configuration $config, Box $box, CompilerLogger $logger): void + { + $values = $config->getReplacements(); + + if (0 === count($values)) { + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Setting replacement values', + ); + + foreach ($values as $key => $value) { + $logger->log( + CompilerLogger::PLUS_PREFIX, + sprintf( + '%s: %s', + $key, + $value, + ), + ); + } + + $box->registerPlaceholders($values); + } + + private static function registerCompactors(Configuration $config, Box $box, CompilerLogger $logger): void + { + $compactors = $config->getCompactors(); + + if (0 === count($compactors)) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'No compactor to register', + ); + + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Registering compactors', + ); + + $logCompactors = static function (Compactor $compactor) use ($logger): void { + $compactorClassParts = explode('\\', $compactor::class); + + if (str_starts_with($compactorClassParts[0], '_HumbugBox')) { + // Keep the non prefixed class name for the user + array_shift($compactorClassParts); + } + + $logger->log( + CompilerLogger::PLUS_PREFIX, + implode('\\', $compactorClassParts), + ); + }; + + array_map($logCompactors, $compactors->toArray()); + + $box->registerCompactors($compactors); + } + + private static function registerFileMapping(Configuration $config, Box $box, CompilerLogger $logger): void + { + $fileMapper = $config->getFileMapper(); + + self::logMap($fileMapper, $logger); + + $box->registerFileMapping($fileMapper); + } + + private static function addFiles(Configuration $config, Box $box, CompilerLogger $logger, IO $io): void + { + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, 'Adding binary files'); + + $count = count($config->getBinaryFiles()); + + $box->addFiles($config->getBinaryFiles(), true); + + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + 0 === $count + ? 'No file found' + : sprintf('%d file(s)', $count), + ); + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Auto-discover files? %s', + $config->hasAutodiscoveredFiles() ? 'Yes' : 'No', + ), + ); + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Exclude dev files? %s', + $config->excludeDevFiles() ? 'Yes' : 'No', + ), + ); + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, 'Adding files'); + + $count = count($config->getFiles()); + + self::addFilesWithErrorHandling($config, $box, $io); + + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + 0 === $count + ? 'No file found' + : sprintf('%d file(s)', $count), + ); + } + + private static function addFilesWithErrorHandling(Configuration $config, Box $box, IO $io): void + { + try { + $box->addFiles($config->getFiles(), false); + + return; + } catch (MultiReasonException $ampFailure) { + // Continue + } + + // This exception is handled a different way to give me meaningful feedback to the user + $io->error([ + 'An Amp\Parallel error occurred. To diagnostic if it is an Amp error related, you may try again with "--no-parallel".', + 'Reason(s) of the failure:', + ...FailureCollector::collectReasons($ampFailure), + ]); + + throw $ampFailure; + } + + private static function registerMainScript(Configuration $config, Box $box, CompilerLogger $logger): ?string + { + if (false === $config->hasMainScript()) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'No main script path configured', + ); + + return null; + } + + $main = $config->getMainScriptPath(); + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Adding main file: %s', + $main, + ), + ); + + $localMain = $box->addFile( + $main, + $config->getMainScriptContents(), + ); + + $relativeMain = Path::makeRelative($main, $config->getBasePath()); + + if ($localMain !== $relativeMain) { + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + $localMain, + ); + } + + return $localMain; + } + + private static function registerRequirementsChecker(Configuration $config, Box $box, CompilerLogger $logger): bool + { + if (false === $config->checkRequirements()) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Skip requirements checker', + ); + + return false; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Adding requirements checker', + ); + + $checkFiles = RequirementsDumper::dump( + new DecodedComposerJson($config->getDecodedComposerJsonContents() ?? []), + new DecodedComposerLock($config->getDecodedComposerLockContents() ?? []), + $config->getCompressionAlgorithm(), + ); + + foreach ($checkFiles as $fileWithContents) { + [$file, $contents] = $fileWithContents; + + $box->addFile('.box/'.$file, $contents, true); + } + + return true; + } + + private static function registerStub( + Configuration $config, + Box $box, + ?string $main, + bool $checkRequirements, + CompilerLogger $logger, + ): void { + if ($config->isStubGenerated()) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Generating new stub', + ); + + $stub = self::createStub($config, $main, $checkRequirements, $logger); + + $box->setStub($stub); + + return; + } + + if (null !== ($stub = $config->getStubPath())) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Using stub file: %s', + $stub, + ), + ); + + $box->registerStub($stub); + + return; + } + + $box->setAlias($config->getAlias()); + $box->setDefaultStub($main); + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Using default stub', + ); + } + + private static function configureMetadata(Configuration $config, Box $box, CompilerLogger $logger): void + { + if (null !== ($metadata = $config->getMetadata())) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Setting metadata', + ); + + if (is_callable($metadata)) { + $metadata = $metadata(); + } + + $logger->log( + CompilerLogger::MINUS_PREFIX, + is_string($metadata) ? $metadata : var_export($metadata, true), + ); + + $box->setMetadata($metadata); + } + } + + private static function commit( + Box $box, + ComposerOrchestrator $composerOrchestrator, + Configuration $config, + CompilerLogger $logger, + ): void { + $message = $config->dumpAutoload() + ? 'Dumping the Composer autoloader' + : 'Skipping dumping the Composer autoloader'; + + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, $message); + + $excludeDevFiles = $config->excludeDevFiles(); + + $box->endBuffering( + $config->dumpAutoload() + ? static fn (SymbolsRegistry $symbolsRegistry, string $prefix, array $excludeScoperFiles) => $composerOrchestrator->dumpAutoload( + $symbolsRegistry, + $prefix, + $excludeDevFiles, + $excludeScoperFiles, + ) + : null, + ); + } + + private static function checkComposerFiles(Box $box, Configuration $config, CompilerLogger $logger): void + { + $message = $config->excludeComposerFiles() + ? 'Removing the Composer dump artefacts' + : 'Keep the Composer dump artefacts'; + + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, $message); + + if ($config->excludeComposerFiles()) { + $box->removeComposerArtefacts( + ComposerConfiguration::retrieveVendorDir( + $config->getDecodedComposerJsonContents() ?? [], + ), + ); + } + } + + private static function configureCompressionAlgorithm( + Configuration $config, + Box $box, + bool $dev, + IO $io, + CompilerLogger $logger, + ): void { + $algorithm = $config->getCompressionAlgorithm(); + + if (CompressionAlgorithm::NONE === $algorithm) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'No compression', + ); + + return; + } + + if ($dev) { + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, 'Dev mode detected: skipping the compression'); + + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Compressing with the algorithm "%s"', + $algorithm->name, + ), + ); + + $restoreLimit = OpenFileDescriptorLimiter::bumpLimit(count($box), $io); + + try { + $extension = $box->compress($algorithm); + + if (null !== $extension) { + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + sprintf( + 'Warning: the extension "%s" will now be required to execute the PHAR', + $extension, + ), + ); + } + } catch (RuntimeException $exception) { + $io->error($exception->getMessage()); + + // Continue: the compression failure should not result in completely bailing out the compilation process + } finally { + $restoreLimit(); + } + } + + private static function signPhar( + Configuration $config, + Box $box, + string $path, + IO $io, + CompilerLogger $logger, + ): void { + // Sign using private key when applicable + FS::remove($path.'.pubkey'); + + $key = $config->getPrivateKeyPath(); + + if (null === $key) { + self::signPharWithoutPrivateKey( + $box, + $config->getSigningAlgorithm(), + $config->getTimestamp(), + $logger, + ); + } else { + self::signPharWithPrivateKey( + $box, + $key, + $config->getPrivateKeyPassphrase(), + $config->promptForPrivateKey(), + $io, + $logger, + ); + } + } + + private static function signPharWithoutPrivateKey( + Box $box, + SigningAlgorithm $signingAlgorithm, + ?DateTimeImmutable $timestamp, + CompilerLogger $logger, + ): void { + if (null !== $timestamp) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Correcting the timestamp to "%s".', + $timestamp->format(DateTimeInterface::ATOM), + ), + ); + } + + $box->sign($signingAlgorithm, $timestamp); + } + + private static function signPharWithPrivateKey( + Box $box, + string $key, + ?string $passphrase, + bool $prompt, + IO $io, + CompilerLogger $logger, + ): void { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Signing using a private key', + ); + $io->newLine(); + + if ($prompt) { + if (false === $io->isInteractive()) { + throw new RuntimeException( + sprintf( + 'Accessing to the private key "%s" requires a passphrase but none provided. Either ' + .'provide one or run this command in interactive mode.', + $key, + ), + ); + } + + $question = new Question('Private key passphrase'); + $question->setHidden(false); + $question->setHiddenFallback(false); + + $passphrase = $io->askQuestion($question); + + $io->writeln(''); + } + + $box->signUsingFile($key, $passphrase); + } + + private static function correctPermissions(string $path, Configuration $config, CompilerLogger $logger): void + { + if (null !== ($chmod = $config->getFileMode())) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Setting file permissions to %s', + '0'.decoct($chmod), + ), + ); + + FS::chmod($path, $chmod); + } + } + + private static function createStub( + Configuration $config, + ?string $main, + bool $checkRequirements, + CompilerLogger $logger, + ): string { + $shebang = $config->getShebang(); + $bannerPath = $config->getStubBannerPath(); + $bannerContents = $config->getStubBannerContents(); + + if (null !== $shebang) { + $logger->log( + CompilerLogger::MINUS_PREFIX, + sprintf( + 'Using shebang line: %s', + $shebang, + ), + ); + } else { + $logger->log( + CompilerLogger::MINUS_PREFIX, + 'No shebang line', + ); + } + + if (null !== $bannerPath) { + $logger->log( + CompilerLogger::MINUS_PREFIX, + sprintf( + 'Using custom banner from file: %s', + $bannerPath, + ), + ); + } elseif (null !== $bannerContents) { + $logger->log( + CompilerLogger::MINUS_PREFIX, + 'Using banner:', + ); + + $bannerLines = explode("\n", $bannerContents); + + foreach ($bannerLines as $bannerLine) { + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + $bannerLine, + ); + } + } + + return StubGenerator::generateStub( + $config->getAlias(), + $bannerContents, + $main, + $config->isInterceptFileFuncs(), + $shebang, + $checkRequirements, + ); + } + + private static function logMap(MapFile $fileMapper, CompilerLogger $logger): void + { + $map = $fileMapper->getMap(); + + if (0 === count($map)) { + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Mapping paths', + ); + + foreach ($map as $item) { + foreach ($item as $match => $replace) { + if ('' === $match) { + $match = '(all)'; + $replace .= '/'; + } + + $logger->log( + CompilerLogger::MINUS_PREFIX, + sprintf( + '%s > %s', + $match, + $replace, + ), + ); + } + } + } + + private static function logEndBuilding( + Configuration $config, + CompilerLogger $logger, + IO $io, + Box $box, + string $path, + float $startTime, + ): void { + $logger->log( + CompilerLogger::STAR_PREFIX, + 'Done.', + ); + $io->newLine(); + + MessageRenderer::render($io, $config->getRecommendations(), $config->getWarnings()); + + $io->comment( + sprintf( + 'PHAR: %s (%s)', + $box->count() > 1 ? $box->count().' files' : $box->count().' file', + format_size( + filesize($path), + ), + ) + .PHP_EOL + .'You can inspect the generated PHAR with the "info" command.', + ); + + $io->comment( + sprintf( + 'Memory usage: %s (peak: %s), time: %s', + format_size(memory_get_usage()), + format_size(memory_get_peak_usage()), + format_time(microtime(true) - $startTime), + ), + ); + } + + private function generateDockerFile(IO $io): int + { + $input = new StringInput(''); + $input->setInteractive(false); + + return $this->getDockerCommand()->execute( + new IO($input, $io->getOutput()), + ); + } + + private function getDockerCommand(): Command + { + return $this->getCommandRegistry()->findCommand(GenerateDockerFile::NAME); + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/Composer/ComposerCheckVersion.php b/fixtures/bench/with-compactors/src/Console/Command/Composer/ComposerCheckVersion.php new file mode 100644 index 000000000..30fe5e1e0 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/Composer/ComposerCheckVersion.php @@ -0,0 +1,49 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command\Composer; + +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use KevinGH\Box\Composer\ComposerOrchestrator; + +/** + * @private + */ +final class ComposerCheckVersion extends ComposerCommand +{ + public function getConfiguration(): Configuration + { + $parentConfig = parent::getConfiguration(); + + return new Configuration( + 'composer:check-version', + 'Checks if the Composer executable used is compatible with Box', + <<<'HELP' + The %command.name% command will look for the Composer binary (in the system if not configured + in the configuration file) and check if its version is compatible with Box. + HELP, + $parentConfig->getArguments(), + $parentConfig->getOptions(), + ); + } + + protected function orchestrate(ComposerOrchestrator $composerOrchestrator, IO $io): int + { + $composerOrchestrator->checkVersion(); + + return ExitCode::SUCCESS; + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/Composer/ComposerCommand.php b/fixtures/bench/with-compactors/src/Console/Command/Composer/ComposerCommand.php new file mode 100644 index 000000000..083f846b8 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/Composer/ComposerCommand.php @@ -0,0 +1,83 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command\Composer; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration; +use Fidry\Console\IO; +use Fidry\FileSystem\FileSystem; +use KevinGH\Box\Composer\ComposerOrchestrator; +use KevinGH\Box\Composer\ComposerProcessFactory; +use Psr\Log\LogLevel; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Logger\ConsoleLogger; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Path; +use function Safe\getcwd; + +/** + * @private + */ +abstract class ComposerCommand implements Command +{ + private const COMPOSER_BIN_OPTION = 'composer-bin'; + + private const VERBOSITY_LEVEL_MAP = [ + LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL, + LogLevel::INFO => OutputInterface::VERBOSITY_NORMAL, + LogLevel::DEBUG => OutputInterface::VERBOSITY_VERBOSE, + ]; + + public function getConfiguration(): Configuration + { + return new Configuration( + 'To configure.', + 'To configure.', + 'To configure.', + [], + [ + new InputOption( + self::COMPOSER_BIN_OPTION, + null, + InputOption::VALUE_REQUIRED, + 'Composer executable to use.', + ), + ], + ); + } + + final public function execute(IO $io): int + { + $composerOrchestrator = new ComposerOrchestrator( + ComposerProcessFactory::create( + self::getComposerExecutable($io), + $io, + ), + new ConsoleLogger($io->getOutput(), self::VERBOSITY_LEVEL_MAP), + new FileSystem(), + ); + + return $this->orchestrate($composerOrchestrator, $io); + } + + abstract protected function orchestrate(ComposerOrchestrator $composerOrchestrator, IO $io): int; + + private static function getComposerExecutable(IO $io): ?string + { + $composerBin = $io->getTypedOption(self::COMPOSER_BIN_OPTION)->asNullableNonEmptyString(); + + return null === $composerBin ? null : Path::makeAbsolute($composerBin, getcwd()); + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/Composer/ComposerVendorDir.php b/fixtures/bench/with-compactors/src/Console/Command/Composer/ComposerVendorDir.php new file mode 100644 index 000000000..d070cbb02 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/Composer/ComposerVendorDir.php @@ -0,0 +1,49 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command\Composer; + +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use KevinGH\Box\Composer\ComposerOrchestrator; + +/** + * @private + */ +final class ComposerVendorDir extends ComposerCommand +{ + public function getConfiguration(): Configuration + { + $parentConfig = parent::getConfiguration(); + + return new Configuration( + 'composer:vendor-dir', + 'Shows the Composer vendor-dir configured', + <<<'HELP' + The %command.name% command will look for the Composer binary (in the system if not configured + in the configuration file) and print the vendor-dir found. + HELP, + $parentConfig->getArguments(), + $parentConfig->getOptions(), + ); + } + + protected function orchestrate(ComposerOrchestrator $composerOrchestrator, IO $io): int + { + $io->writeln($composerOrchestrator->getVendorDir()); + + return ExitCode::SUCCESS; + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/ConfigOption.php b/fixtures/bench/with-compactors/src/Console/Command/ConfigOption.php new file mode 100644 index 000000000..88a2a20e1 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/ConfigOption.php @@ -0,0 +1,60 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\IO; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\Console\ConfigurationLoader; +use KevinGH\Box\Json\JsonValidationException; +use KevinGH\Box\NotInstantiable; +use Symfony\Component\Console\Input\InputOption; + +/** + * Allows a configuration file path to be specified for a command. + * + * @private + */ +final class ConfigOption +{ + use NotInstantiable; + + private const CONFIG_PARAM = 'config'; + + public static function getOptionInput(): InputOption + { + return new InputOption( + self::CONFIG_PARAM, + 'c', + InputOption::VALUE_REQUIRED, + 'The alternative configuration file path.', + ); + } + + /** + * Returns the configuration settings. + * + * @param bool $allowNoFile Load the config nonetheless if not file is found when true + * + * @throws JsonValidationException + */ + public static function getConfig(IO $io, bool $allowNoFile = false): Configuration + { + return ConfigurationLoader::getConfig( + $io->getInput()->getOption(self::CONFIG_PARAM), + $io, + $allowNoFile, + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/ConfigurationExporter.php b/fixtures/bench/with-compactors/src/Console/Command/ConfigurationExporter.php new file mode 100644 index 000000000..cdfcc35cd --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/ConfigurationExporter.php @@ -0,0 +1,62 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use DateTimeImmutable; +use DateTimeZone; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\NotInstantiable; +use function function_exists; +use function get_loaded_extensions; +use function implode; +use function KevinGH\Box\get_box_version; +use function php_uname; +use const DATE_ATOM; +use const PHP_OS; +use const PHP_VERSION; + +final class ConfigurationExporter +{ + use NotInstantiable; + + public static function export(Configuration $config): string + { + $date = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_ATOM); + $file = $config->getConfigurationFile() ?? 'No config file'; + + $phpVersion = PHP_VERSION; + $phpExtensions = implode(',', get_loaded_extensions()); + $os = function_exists('php_uname') ? PHP_OS.' / '.php_uname('r') : 'Unknown OS'; + $command = implode(' ', $GLOBALS['argv']); + $boxVersion = get_box_version(); + + $header = <<export(); + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/Diff.php b/fixtures/bench/with-compactors/src/Console/Command/Diff.php new file mode 100644 index 000000000..fc8c2596e --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/Diff.php @@ -0,0 +1,350 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use KevinGH\Box\Console\PharInfoRenderer; +use KevinGH\Box\Phar\DiffMode; +use KevinGH\Box\Phar\PharDiff; +use KevinGH\Box\Phar\PharInfo; +use SebastianBergmann\Diff\Differ; +use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\Filesystem\Path; +use ValueError; +use Webmozart\Assert\Assert; +use function array_map; +use function explode; +use function implode; +use function sprintf; +use function str_starts_with; + +/** + * @private + */ +final class Diff implements Command +{ + private const FIRST_PHAR_ARG = 'pharA'; + private const SECOND_PHAR_ARG = 'pharB'; + + private const LIST_FILES_DIFF_OPTION = 'list-diff'; + private const GIT_DIFF_OPTION = 'git-diff'; + private const GNU_DIFF_OPTION = 'gnu-diff'; + private const DIFF_OPTION = 'diff'; + private const CHECK_OPTION = 'check'; + private const CHECKSUM_ALGORITHM_OPTION = 'checksum-algorithm'; + + private const DEFAULT_CHECKSUM_ALGO = 'sha384'; + + public function getConfiguration(): Configuration + { + return new Configuration( + 'diff', + '🕵 Displays the differences between all of the files in two PHARs', + '', + [ + new InputArgument( + self::FIRST_PHAR_ARG, + InputArgument::REQUIRED, + 'The first PHAR', + ), + new InputArgument( + self::SECOND_PHAR_ARG, + InputArgument::REQUIRED, + 'The second PHAR', + ), + ], + [ + new InputOption( + self::GNU_DIFF_OPTION, + null, + InputOption::VALUE_NONE, + '(deprecated) Displays a GNU diff', + ), + new InputOption( + self::GIT_DIFF_OPTION, + null, + InputOption::VALUE_NONE, + '(deprecated) Displays a Git diff', + ), + new InputOption( + self::LIST_FILES_DIFF_OPTION, + null, + InputOption::VALUE_NONE, + '(deprecated) Displays a list of file names diff (default)', + ), + new InputOption( + self::DIFF_OPTION, + null, + InputOption::VALUE_REQUIRED, + sprintf( + 'Displays a diff of the files. Available options are: "%s"', + implode( + '", "', + DiffMode::values(), + ), + ), + DiffMode::CHECKSUM->value, + ), + new InputOption( + self::CHECK_OPTION, + 'c', + InputOption::VALUE_OPTIONAL, + '(deprecated) Verify the authenticity of the contents between the two PHARs with the given hash function', + ), + new InputOption( + self::CHECKSUM_ALGORITHM_OPTION, + null, + InputOption::VALUE_REQUIRED, + sprintf( + 'The hash function used to compare files with the diff mode used is "%s".', + DiffMode::CHECKSUM->value, + ), + self::DEFAULT_CHECKSUM_ALGO, + ), + ], + ); + } + + public function execute(IO $io): int + { + $diff = new PharDiff(...self::getPaths($io)); + $diffMode = self::getDiffMode($io); + $checksumAlgorithm = self::getChecksumAlgorithm($io); + + $io->comment('Comparing the two archives...'); + + if ($diff->equals()) { + $io->success('The two archives are identical.'); + + return ExitCode::SUCCESS; + } + + self::renderSummary($diff->getPharInfoA(), $io); + $io->newLine(); + self::renderSummary($diff->getPharInfoB(), $io); + + $this->renderArchivesDiff($diff, $io); + $this->renderContentsDiff($diff, $diffMode, $checksumAlgorithm, $io); + + return ExitCode::FAILURE; + } + + /** + * @return array{non-empty-string, non-empty-string} + */ + private static function getPaths(IO $io): array + { + $paths = [ + $io->getTypedArgument(self::FIRST_PHAR_ARG)->asNonEmptyString(), + $io->getTypedArgument(self::SECOND_PHAR_ARG)->asNonEmptyString(), + ]; + + Assert::allFile($paths); + + return array_map( + static fn (string $path) => Path::canonicalize($path), + $paths, + ); + } + + private function renderArchivesDiff(PharDiff $diff, IO $io): void + { + $pharASummary = self::getShortSummary($diff->getPharInfoA(), $io); + $pharBSummary = self::getShortSummary($diff->getPharInfoB(), $io); + + if ($pharASummary === $pharBSummary) { + return; + } + + $io->writeln( + self::createColorizedDiff( + $pharASummary, + $pharBSummary, + ), + ); + } + + private static function createColorizedDiff(string $pharASummary, string $pharBSummary): string + { + $differ = new Differ( + new UnifiedDiffOutputBuilder( + "\n--- PHAR A\n+++ PHAR B\n", + ), + ); + + $result = $differ->diff( + $pharASummary, + $pharBSummary, + ); + + $lines = explode("\n", $result); + + $colorizedLines = array_map( + static fn (string $line) => match (true) { + str_starts_with($line, '+') => sprintf( + '%s', + $line, + ), + str_starts_with($line, '-') => sprintf( + '%s', + $line, + ), + default => $line, + }, + $lines, + ); + + return implode("\n", $colorizedLines); + } + + private static function getDiffMode(IO $io): DiffMode + { + if ($io->getTypedOption(self::GNU_DIFF_OPTION)->asBoolean()) { + $io->writeln( + sprintf( + '⚠️ Using the option "%s" is deprecated. Use "--%s=%s" instead.', + self::GNU_DIFF_OPTION, + self::DIFF_OPTION, + DiffMode::GNU->value, + ), + ); + + return DiffMode::GNU; + } + + if ($io->getTypedOption(self::GIT_DIFF_OPTION)->asBoolean()) { + $io->writeln( + sprintf( + '⚠️ Using the option "%s" is deprecated. Use "--%s=%s" instead.', + self::GIT_DIFF_OPTION, + self::DIFF_OPTION, + DiffMode::GIT->value, + ), + ); + + return DiffMode::GIT; + } + + if ($io->getTypedOption(self::LIST_FILES_DIFF_OPTION)->asBoolean()) { + $io->writeln( + sprintf( + '⚠️ Using the option "%s" is deprecated. Use "--%s=%s" instead.', + self::LIST_FILES_DIFF_OPTION, + self::DIFF_OPTION, + DiffMode::FILE_NAME->value, + ), + ); + + return DiffMode::FILE_NAME; + } + + if ($io->hasOption('-c') || $io->hasOption('--check')) { + $io->writeln( + sprintf( + '⚠️ Using the option "%s" is deprecated. Use "--%s=%s" instead.', + self::CHECK_OPTION, + self::DIFF_OPTION, + DiffMode::CHECKSUM->value, + ), + ); + + return DiffMode::FILE_NAME; + } + + $rawDiffOption = $io->getTypedOption(self::DIFF_OPTION)->asNonEmptyString(); + + try { + return DiffMode::from($rawDiffOption); + } catch (ValueError) { + // Rethrow a more user-friendly error message + throw new RuntimeException( + sprintf( + 'Invalid diff mode "%s". Expected one of: "%s".', + $rawDiffOption, + implode( + '", "', + DiffMode::values(), + ), + ), + ); + } + } + + private static function getChecksumAlgorithm(IO $io): string + { + $checksumAlgorithm = $io->getTypedOption(self::CHECK_OPTION)->asNullableNonEmptyString(); + + if (null !== $checksumAlgorithm) { + $io->writeln( + sprintf( + '⚠️ Using the option "%s" is deprecated. Use "--%s=\" instead.', + self::CHECK_OPTION, + self::CHECKSUM_ALGORITHM_OPTION, + ), + ); + + return $checksumAlgorithm; + } + + return $io->getTypedOption(self::CHECKSUM_ALGORITHM_OPTION)->asNullableNonEmptyString() ?? self::DEFAULT_CHECKSUM_ALGO; + } + + private function renderContentsDiff(PharDiff $diff, DiffMode $diffMode, string $checksumAlgorithm, IO $io): void + { + $io->comment( + sprintf( + 'Comparing the two archives contents (%s diff)...', + $diffMode->value, + ), + ); + + $diff->diff($diffMode, $checksumAlgorithm, $io); + } + + private static function renderSummary(PharInfo $pharInfo, IO $io): void + { + $io->writeln( + sprintf( + 'Archive: %s', + $pharInfo->getFileName(), + ), + ); + + PharInfoRenderer::renderShortSummary($pharInfo, $io); + } + + private static function getShortSummary(PharInfo $pharInfo, IO $io): string + { + $output = new BufferedOutput( + $io->getVerbosity(), + false, + clone $io->getOutput()->getFormatter(), + ); + + PharInfoRenderer::renderShortSummary( + $pharInfo, + $io->withOutput($output), + ); + + return $output->fetch(); + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/Extract.php b/fixtures/bench/with-compactors/src/Console/Command/Extract.php new file mode 100644 index 000000000..0d0d999c9 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/Extract.php @@ -0,0 +1,207 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use Fidry\FileSystem\FS; +use KevinGH\Box\Console\PhpSettingsChecker; +use KevinGH\Box\Phar\InvalidPhar; +use KevinGH\Box\Phar\PharFactory; +use KevinGH\Box\Phar\PharMeta; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Throwable; +use function bin2hex; +use function file_exists; +use function realpath; +use function sprintf; +use const DIRECTORY_SEPARATOR; + +/** + * @private + */ +final class Extract implements Command +{ + public const STUB_PATH = '.phar/stub.php'; + public const PHAR_META_PATH = '.phar/meta.json'; + + private const PHAR_ARG = 'phar'; + private const OUTPUT_ARG = 'output'; + private const INTERNAL_OPT = 'internal'; + + public function getConfiguration(): Configuration + { + return new Configuration( + 'extract', + '🚚 Extracts a given PHAR into a directory', + '', + [ + new InputArgument( + self::PHAR_ARG, + InputArgument::REQUIRED, + 'The path to the PHAR file', + ), + new InputArgument( + self::OUTPUT_ARG, + InputArgument::REQUIRED, + 'The output directory', + ), + ], + [ + new InputOption( + self::INTERNAL_OPT, + null, + InputOption::VALUE_NONE, + 'Internal option; Should not be used.', + ), + ], + ); + } + + public function execute(IO $io): int + { + PhpSettingsChecker::check($io); + + $pharPath = self::getPharFilePath($io); + $outputDir = $io->getTypedArgument(self::OUTPUT_ARG)->asNonEmptyString(); + $internal = $io->getTypedOption(self::INTERNAL_OPT)->asBoolean(); + + if (null === $pharPath) { + return ExitCode::FAILURE; + } + + if (file_exists($outputDir)) { + $canDelete = $io->askQuestion( + new ConfirmationQuestion( + 'The output directory already exists. Do you want to delete its current content?', + // If is interactive, we want the prompt to default to false since it can be an error made by the user. + // Otherwise, this is likely launched by a script or Pharaoh in which case we do not care. + $internal, + ), + ); + + if ($canDelete) { + FS::remove($outputDir); + // Continue + } else { + // Do nothing + return ExitCode::FAILURE; + } + } + + FS::mkdir($outputDir); + + try { + self::dumpPhar($pharPath, $outputDir); + } catch (InvalidPhar $invalidPhar) { + if (!$internal) { + throw $invalidPhar; + } + + $io->getErrorIO()->write($invalidPhar->getMessage()); + + return ExitCode::FAILURE; + } + + return ExitCode::SUCCESS; + } + + private static function getPharFilePath(IO $io): ?string + { + $filePath = realpath($io->getTypedArgument(self::PHAR_ARG)->asString()); + + if (false !== $filePath) { + return $filePath; + } + + $io->error( + sprintf( + 'The file "%s" could not be found.', + $io->getTypedArgument(self::PHAR_ARG)->asRaw(), + ), + ); + + return null; + } + + private static function dumpPhar(string $file, string $tmpDir): string + { + // We have to give every one a different alias, or it pukes. + $alias = self::generateAlias($file); + + // Create a temporary PHAR: this is because the extension might be + // missing in which case we would not be able to create a Phar instance + // as it requires the .phar extension. + $tmpFile = $tmpDir.DIRECTORY_SEPARATOR.$alias; + $pubKey = $file.'.pubkey'; + $pubKeyContent = null; + $tmpPubKey = $tmpFile.'.pubkey'; + $stub = $tmpDir.DIRECTORY_SEPARATOR.self::STUB_PATH; + + try { + FS::copy($file, $tmpFile, true); + + if (file_exists($pubKey)) { + FS::copy($pubKey, $tmpPubKey, true); + $pubKeyContent = FS::getFileContents($pubKey); + } + + $phar = PharFactory::create($tmpFile, $file); + $pharMeta = PharMeta::fromPhar($phar, $pubKeyContent); + + $phar->extractTo($tmpDir); + FS::dumpFile($stub, $phar->getStub()); + } catch (Throwable $throwable) { + FS::remove([$tmpFile, $tmpPubKey]); + + throw $throwable; + } + + FS::dumpFile( + $tmpDir.DIRECTORY_SEPARATOR.self::PHAR_META_PATH, + $pharMeta->toJson(), + ); + + // Cleanup the temporary PHAR. + FS::remove([$tmpFile, $tmpPubKey]); + + return $tmpDir; + } + + private static function generateAlias(string $file): string + { + $extension = self::getExtension($file); + + return bin2hex(random_bytes(16)).$extension; + } + + private static function getExtension(string $file): string + { + $lastExtension = pathinfo($file, PATHINFO_EXTENSION); + $extension = ''; + + while ('' !== $lastExtension) { + $extension = '.'.$lastExtension.$extension; + $file = mb_substr($file, 0, -(mb_strlen($lastExtension) + 1)); + $lastExtension = pathinfo($file, PATHINFO_EXTENSION); + } + + return '' === $extension ? '.phar' : $extension; + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/GenerateDockerFile.php b/fixtures/bench/with-compactors/src/Console/Command/GenerateDockerFile.php new file mode 100644 index 000000000..9ba23ce41 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/GenerateDockerFile.php @@ -0,0 +1,217 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\CommandAware; +use Fidry\Console\Command\CommandAwareness; +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use Fidry\FileSystem\FS; +use KevinGH\Box\DockerFileGenerator; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Filesystem\Path; +use Webmozart\Assert\Assert; +use function file_exists; +use function getcwd; +use function realpath; +use function sprintf; + +/** + * @private + */ +final class GenerateDockerFile implements CommandAware +{ + use CommandAwareness; + + public const NAME = 'docker'; + + private const PHAR_ARG = 'phar'; + private const DOCKER_FILE_NAME = 'Dockerfile'; + + public function getConfiguration(): Configuration + { + return new Configuration( + 'docker', + '🐳 Generates a Dockerfile for the given PHAR', + '', + [ + new InputArgument( + self::PHAR_ARG, + InputArgument::OPTIONAL, + 'The PHAR file', + ), + ], + [ConfigOption::getOptionInput()], + ); + } + + public function execute(IO $io): int + { + $pharFilePath = $this->getPharFilePath($io); + + if (null === $pharFilePath) { + return ExitCode::FAILURE; + } + + $io->newLine(); + $io->writeln( + sprintf( + '🐳 Generating a Dockerfile for the PHAR "%s"', + $pharFilePath, + ), + ); + $io->newLine(); + + $requirementsFilePhar = 'phar://'.$pharFilePath.'/.box/.requirements.php'; + + return $this->generateFile( + $pharFilePath, + $requirementsFilePhar, + $io, + ); + } + + /** + * @return null|non-empty-string + */ + private function getPharFilePath(IO $io): ?string + { + $pharFilePath = $io->getTypedArgument(self::PHAR_ARG)->asNullableNonEmptyString(); + + if (null === $pharFilePath) { + $pharFilePath = $this->guessPharPath($io); + } + + if (null === $pharFilePath) { + return null; + } + + $pharFilePath = Path::canonicalize($pharFilePath); + Assert::file($pharFilePath); + + return false !== realpath($pharFilePath) ? realpath($pharFilePath) : $pharFilePath; + } + + private function guessPharPath(IO $io): ?string + { + $config = ConfigOption::getConfig($io, true); + + if (file_exists($config->getOutputPath())) { + return $config->getOutputPath(); + } + + $compile = $io->askQuestion( + new ConfirmationQuestion( + 'The output PHAR could not be found, do you wish to generate it by running "box compile"?', + true, + ), + ); + + if (false === $compile) { + $io->error('Could not find the PHAR to generate the docker file for'); + + return null; + } + + $this->getCompileCommand()->execute( + new IO( + self::createCompileInput($io), + clone $io->getOutput(), + ), + ); + + return $config->getOutputPath(); + } + + private function getCompileCommand(): Compile + { + /* @noinspection PhpIncompatibleReturnTypeInspection */ + return $this->getCommandRegistry()->findCommand(Compile::NAME); + } + + private static function createCompileInput(IO $io): InputInterface + { + if ($io->isQuiet()) { + $compileInput = '--quiet'; + } elseif ($io->isVerbose()) { + $compileInput = '--verbose 1'; + } elseif ($io->isVeryVerbose()) { + $compileInput = '--verbose 2'; + } elseif ($io->isDebug()) { + $compileInput = '--verbose 3'; + } else { + $compileInput = ''; + } + + $compileInput = new StringInput($compileInput); + $compileInput->setInteractive(false); + + return $compileInput; + } + + private function generateFile(string $pharPath, string $requirementsPhar, IO $io): int + { + if (false === file_exists($requirementsPhar)) { + $io->error( + 'Cannot retrieve the requirements for the PHAR. Make sure the PHAR has been built with Box and the requirement checker enabled.', + ); + + return ExitCode::FAILURE; + } + + $requirements = include $requirementsPhar; + + $dockerFileContents = DockerFileGenerator::createForRequirements( + $requirements, + Path::makeRelative($pharPath, getcwd()), + ) + ->generateStub(); + + if (file_exists(self::DOCKER_FILE_NAME)) { + $remove = $io->askQuestion( + new ConfirmationQuestion( + 'A Docker file has already been found, are you sure you want to override it?', + true, + ), + ); + + if (false === $remove) { + $io->writeln('Skipped the docker file generation.'); + + return ExitCode::SUCCESS; + } + } + + FS::dumpFile(self::DOCKER_FILE_NAME, $dockerFileContents); + + $io->success('Done'); + + $io->writeln( + [ + sprintf( + 'You can now inspect your %s file or build your container with:', + self::DOCKER_FILE_NAME, + ), + '$ docker build .', + ], + ); + + return ExitCode::SUCCESS; + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/Info.php b/fixtures/bench/with-compactors/src/Console/Command/Info.php new file mode 100644 index 000000000..3239eee6a --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/Info.php @@ -0,0 +1,245 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use KevinGH\Box\Console\PharInfoRenderer; +use KevinGH\Box\Phar\PharInfo; +use Phar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Filesystem\Path; +use function implode; +use function is_array; +use function realpath; +use function sprintf; + +/** + * @private + */ +final class Info implements Command +{ + private const PHAR_ARG = 'phar'; + private const LIST_OPT = 'list'; + private const MODE_OPT = 'mode'; + private const DEPTH_OPT = 'depth'; + + private const MODES = [ + 'indent', + 'flat', + ]; + + public function getConfiguration(): Configuration + { + return new Configuration( + 'info', + '🔍 Displays information about the PHAR extension or file', + <<<'HELP' + The %command.name% command will display information about the Phar extension, + or the Phar file if specified. + + If the phar argument (the PHAR file path) is provided, information + about the PHAR file itself will be displayed. + + If the --list|-l option is used, the contents of the PHAR file will + be listed. By default, the list is shown as an indented tree. You may + instead choose to view a flat listing, by setting the --mode|-m option + to flat. + HELP, + [ + new InputArgument( + self::PHAR_ARG, + InputArgument::OPTIONAL, + 'The Phar file.', + ), + ], + [ + new InputOption( + self::LIST_OPT, + 'l', + InputOption::VALUE_NONE, + 'List the contents of the Phar?', + ), + new InputOption( + self::MODE_OPT, + 'm', + InputOption::VALUE_REQUIRED, + sprintf( + 'The listing mode. Modes available: "%s"', + implode('", "', self::MODES), + ), + 'indent', + ), + new InputOption( + self::DEPTH_OPT, + 'd', + InputOption::VALUE_REQUIRED, + 'The depth of the tree displayed', + '-1', + ), + ], + ); + } + + public function execute(IO $io): int + { + $io->newLine(); + + $file = $io->getTypedArgument(self::PHAR_ARG)->asNullableNonEmptyString(); + + if (null === $file) { + return self::showGlobalInfo($io); + } + + $file = Path::canonicalize($file); + $fileRealPath = realpath($file); + + if (false === $fileRealPath) { + $io->error( + sprintf( + 'The file "%s" could not be found.', + $file, + ), + ); + + return ExitCode::FAILURE; + } + + return self::showInfo($fileRealPath, $io); + } + + public static function showInfo(string $file, IO $io): int + { + $maxDepth = self::getMaxDepth($io); + $mode = $io->getTypedOption(self::MODE_OPT)->asStringChoice(self::MODES); + + $pharInfo = new PharInfo($file); + + return self::showPharInfo( + $pharInfo, + $io->getTypedOption(self::LIST_OPT)->asBoolean(), + -1 === $maxDepth ? false : $maxDepth, + 'indent' === $mode, + $io, + ); + } + + /** + * @return -1|natural + */ + private static function getMaxDepth(IO $io): int + { + $option = $io->getTypedOption(self::DEPTH_OPT); + + return '-1' === $option->asRaw() + ? -1 + : $option->asNatural(sprintf( + 'Expected the depth to be a positive integer or -1: "%s".', + $option->asRaw(), + )); + } + + private static function showGlobalInfo(IO $io): int + { + self::render( + $io, + [ + 'API Version' => Phar::apiVersion(), + 'Supported Compression' => Phar::getSupportedCompression(), + 'Supported Signatures' => Phar::getSupportedSignatures(), + ], + ); + + $io->newLine(); + $io->comment('Get a PHAR details by giving its path as an argument.'); + + return ExitCode::SUCCESS; + } + + private static function showPharInfo( + PharInfo $pharInfo, + bool $content, + int|false $maxDepth, + bool $indent, + IO $io, + ): int { + PharInfoRenderer::renderVersion($pharInfo, $io); + + $io->newLine(); + + PharInfoRenderer::renderBoxVersion($pharInfo, $io); + + PharInfoRenderer::renderShortSummary( + $pharInfo, + $io, + static fn () => $io->newLine(), + ); + + if ($content) { + PharInfoRenderer::renderContent( + $io, + $pharInfo, + $maxDepth, + $indent, + ); + } else { + $io->newLine(); + $io->comment('Use the --list|-l option to list the content of the PHAR.'); + } + + return ExitCode::SUCCESS; + } + + private static function showPharMeta(PharInfo $pharInfo, IO $io): void + { + PharInfoRenderer::renderVersion($pharInfo, $io); + + $io->newLine(); + + PharInfoRenderer::renderShortSummary( + $pharInfo, + $io, + static fn () => $io->newLine(), + ); + } + + private static function render(IO $io, array $attributes): void + { + $out = false; + + foreach ($attributes as $name => $value) { + if ($out) { + $io->writeln(''); + } + + $io->write("{$name}:"); + + if (is_array($value)) { + $io->writeln(''); + + foreach ($value as $v) { + $io->writeln(" - {$v}"); + } + } else { + $io->writeln(" {$value}"); + } + + $out = true; + } + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/Namespace_.php b/fixtures/bench/with-compactors/src/Console/Command/Namespace_.php new file mode 100644 index 000000000..e019478b3 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/Namespace_.php @@ -0,0 +1,45 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use function current; +use function explode; + +final class Namespace_ implements Command +{ + public function getConfiguration(): Configuration + { + return new Configuration( + 'namespace', + 'Prints the first part of the command namespace', + <<<'HELP' + This command is purely for debugging purposes to ensure it is scoped correctly. + HELP, + ); + } + + public function execute(IO $io): int + { + $namespace = current(explode('\\', self::class)); + + $io->writeln($namespace); + + return ExitCode::SUCCESS; + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/Process.php b/fixtures/bench/with-compactors/src/Console/Command/Process.php new file mode 100644 index 000000000..473fbf50d --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/Process.php @@ -0,0 +1,263 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration as ConsoleConfiguration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use Fidry\FileSystem\FS; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\Compactor\Compactor; +use KevinGH\Box\Compactor\Compactors; +use KevinGH\Box\Compactor\PhpScoper; +use KevinGH\Box\Compactor\Placeholder; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\Console\PhpSettingsChecker; +use stdClass; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use function array_map; +use function array_shift; +use function array_unshift; +use function explode; +use function getcwd; +use function implode; +use function putenv; +use function sprintf; +use const KevinGH\Box\BOX_ALLOW_XDEBUG; + +// TODO: replace the PHP-Scoper compactor in order to warn the user about scoping errors +final class Process implements Command +{ + private const FILE_ARGUMENT = 'file'; + + private const NO_RESTART_OPTION = 'no-restart'; + private const NO_CONFIG_OPTION = 'no-config'; + + public function getConfiguration(): ConsoleConfiguration + { + return new ConsoleConfiguration( + 'process', + '⚡ Applies the registered compactors and replacement values on a file', + 'The %command.name% command will apply the registered compactors and replacement values on the the given file. This is useful to debug the scoping of a specific file for example.', + [ + new InputArgument( + self::FILE_ARGUMENT, + InputArgument::REQUIRED, + 'Path to the file to process', + ), + ], + [ + new InputOption( + self::NO_RESTART_OPTION, + null, + InputOption::VALUE_NONE, + 'Do not restart the PHP process. Box restarts the process by default to disable xdebug', + ), + new InputOption( + self::NO_CONFIG_OPTION, + null, + InputOption::VALUE_NONE, + 'Ignore the config file even when one is specified with the --config option', + ), + ConfigOption::getOptionInput(), + ChangeWorkingDirOption::getOptionInput(), + ], + ); + } + + public function execute(IO $io): int + { + if ($io->getTypedOption(self::NO_RESTART_OPTION)->asBoolean()) { + putenv(BOX_ALLOW_XDEBUG.'=1'); + } + + PhpSettingsChecker::check($io); + + ChangeWorkingDirOption::changeWorkingDirectory($io); + + $io->newLine(); + + $config = $io->getTypedOption(self::NO_CONFIG_OPTION)->asBoolean() + ? Configuration::create(null, new stdClass()) + : ConfigOption::getConfig($io, true); + + $filePath = $io->getTypedArgument(self::FILE_ARGUMENT)->asNonEmptyString(); + + $path = Path::makeRelative($filePath, $config->getBasePath()); + + $compactors = self::retrieveCompactors($config); + + $fileContents = FS::getFileContents( + $absoluteFilePath = Path::makeAbsolute( + $filePath, + getcwd(), + ), + ); + + $io->writeln([ + sprintf( + '⚡ Processing the contents of the file %s', + $absoluteFilePath, + ), + '', + ]); + + self::logPlaceholders($config, $io); + self::logCompactors($compactors, $io); + + $fileProcessedContents = $compactors->compact($path, $fileContents); + + if ($io->isQuiet()) { + $io->writeln($fileProcessedContents, OutputInterface::VERBOSITY_QUIET); + } else { + $symbolsRegistry = self::retrieveSymbolsRegistry($compactors); + + $io->writeln([ + 'Processed contents:', + '', + '"""', + $fileProcessedContents, + '"""', + ]); + + if (null !== $symbolsRegistry) { + $io->writeln([ + '', + 'Symbols Registry:', + '', + '"""', + self::exportSymbolsRegistry($symbolsRegistry, $io), + '"""', + ]); + } + } + + return ExitCode::SUCCESS; + } + + private static function retrieveCompactors(Configuration $config): Compactors + { + $compactors = $config->getCompactors()->toArray(); + + array_unshift( + $compactors, + new Placeholder($config->getReplacements()), + ); + + return new Compactors(...$compactors); + } + + private static function logPlaceholders(Configuration $config, IO $io): void + { + if (0 === count($config->getReplacements())) { + $io->writeln([ + 'No replacement values registered', + '', + ]); + + return; + } + + $io->writeln('Registered replacement values:'); + + foreach ($config->getReplacements() as $key => $value) { + $io->writeln( + sprintf( + ' + %s: %s', + $key, + $value, + ), + ); + } + + $io->newLine(); + } + + private static function logCompactors(Compactors $compactors, IO $io): void + { + $nestedCompactors = $compactors->toArray(); + + foreach ($nestedCompactors as $index => $compactor) { + if ($compactor instanceof Placeholder) { + unset($nestedCompactors[$index]); + } + } + + if ([] === $nestedCompactors) { + $io->writeln([ + 'No compactor registered', + '', + ]); + + return; + } + + $io->writeln('Registered compactors:'); + + $logCompactors = static function (Compactor $compactor) use ($io): void { + $compactorClassParts = explode('\\', $compactor::class); + + if (str_starts_with($compactorClassParts[0], '_HumbugBox')) { + // Keep the non prefixed class name for the user + array_shift($compactorClassParts); + } + + $io->writeln( + sprintf( + ' + %s', + implode('\\', $compactorClassParts), + ), + ); + }; + + array_map($logCompactors, $nestedCompactors); + + $io->newLine(); + } + + private static function retrieveSymbolsRegistry(Compactors $compactors): ?SymbolsRegistry + { + foreach ($compactors->toArray() as $compactor) { + if ($compactor instanceof PhpScoper) { + return $compactor->getScoper()->getSymbolsRegistry(); + } + } + + return null; + } + + private static function exportSymbolsRegistry(SymbolsRegistry $symbolsRegistry, IO $io): string + { + $cloner = new VarCloner(); + $cloner->setMaxItems(-1); + $cloner->setMaxString(-1); + + $cliDumper = new CliDumper(); + if ($io->isDecorated()) { + $cliDumper->setColors(true); + } + + return (string) $cliDumper->dump( + $cloner->cloneVar($symbolsRegistry), + true, + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/Validate.php b/fixtures/bench/with-compactors/src/Console/Command/Validate.php new file mode 100644 index 000000000..ba6a1654a --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/Validate.php @@ -0,0 +1,175 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration as ConsoleConfiguration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\Console\ConfigurationLoader; +use KevinGH\Box\Console\ConfigurationLocator; +use KevinGH\Box\Console\MessageRenderer; +use KevinGH\Box\Json\JsonValidationException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Throwable; +use Webmozart\Assert\Assert; +use function count; +use function sprintf; + +/** + * @private + */ +final class Validate implements Command +{ + private const FILE_ARGUMENT = 'file'; + private const IGNORE_MESSAGES_OPTION = 'ignore-recommendations-and-warnings'; + + public function getConfiguration(): ConsoleConfiguration + { + return new ConsoleConfiguration( + 'validate', + '⚙ Validates the configuration file', + <<<'HELP' + The %command.name% command will validate the configuration file + and report any errors found, if any. + + This command relies on a configuration file for loading + PHAR packaging settings. If a configuration file is not + specified through the --configuration|-c option, one of + the following files will be used (in order): box.json, + box.json.dist + + HELP, + [ + new InputArgument( + self::FILE_ARGUMENT, + InputArgument::OPTIONAL, + 'The configuration file. (default: box.json, box.json.dist)', + ), + ], + [ + new InputOption( + self::IGNORE_MESSAGES_OPTION, + 'i', + InputOption::VALUE_NONE, + 'Will not return a faulty code when a recommendation or warning is found', + ), + ], + ); + } + + public function execute(IO $io): int + { + try { + $config = ConfigurationLoader::getConfig( + $io->getTypedArgument(self::FILE_ARGUMENT)->asNullableNonEmptyString() ?? ConfigurationLocator::findDefaultPath(), + $io, + false, + ); + } catch (Throwable $throwable) { + // Continue + } + + if (isset($config)) { + return self::checkConfig($config, $io); + } + + Assert::true(isset($throwable)); + + return self::handleFailure($throwable, $io); + } + + private static function checkConfig(Configuration $config, IO $io): int + { + $ignoreRecommendationsAndWarnings = $io->getTypedOption(self::IGNORE_MESSAGES_OPTION)->asBoolean(); + + $recommendations = $config->getRecommendations(); + $warnings = $config->getWarnings(); + + MessageRenderer::render($io, $recommendations, $warnings); + + $hasRecommendationsOrWarnings = 0 === count($recommendations) && 0 === count($warnings); + + if (false === $hasRecommendationsOrWarnings) { + if (0 === count($recommendations)) { + $io->caution('The configuration file passed the validation with warnings.'); + } elseif (0 === count($warnings)) { + $io->caution('The configuration file passed the validation with recommendations.'); + } else { + $io->caution('The configuration file passed the validation with recommendations and warnings.'); + } + } else { + $io->success('The configuration file passed the validation.'); + } + + return $hasRecommendationsOrWarnings || $ignoreRecommendationsAndWarnings + ? ExitCode::SUCCESS + : ExitCode::FAILURE; + } + + private static function handleFailure(Throwable $throwable, IO $io): int + { + if ($io->isVerbose()) { + throw new RuntimeException( + sprintf( + 'The configuration file failed validation: %s', + $throwable->getMessage(), + ), + $throwable->getCode(), + $throwable, + ); + } + + return $throwable instanceof JsonValidationException + ? self::handleJsonValidationFailure($throwable, $io) + : self::handleGenericFailure($throwable, $io); + } + + private static function handleJsonValidationFailure(JsonValidationException $exception, IO $io): int + { + $io->writeln( + sprintf( + 'The configuration file failed validation: "%s" does not match the expected JSON ' + .'schema:', + $exception->getValidatedFile(), + ), + ); + + $io->writeln(''); + + foreach ($exception->getErrors() as $error) { + $io->writeln(" - {$error}"); + } + + return ExitCode::FAILURE; + } + + private static function handleGenericFailure(Throwable $throwable, IO $io): int + { + $errorMessage = sprintf('The configuration file failed validation: %s', $throwable->getMessage()); + + $io->writeln( + sprintf( + '%s', + $errorMessage, + ), + ); + + return ExitCode::FAILURE; + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Command/Verify.php b/fixtures/bench/with-compactors/src/Console/Command/Verify.php new file mode 100644 index 000000000..346872e9c --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Command/Verify.php @@ -0,0 +1,154 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use KevinGH\Box\Phar\PharInfo; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Filesystem\Path; +use Throwable; +use Webmozart\Assert\Assert; +use function realpath; +use function sprintf; + +/** + * @private + */ +final class Verify implements Command +{ + private const PHAR_ARG = 'phar'; + + public function getConfiguration(): Configuration + { + return new Configuration( + 'verify', + '🔐️ Verifies the PHAR signature', + <<<'HELP' + The %command.name% command will verify the signature of the PHAR. + + Why would I require that box handle the verification process? + + If you meet all the following conditions: + - The openssl extension is not installed + - You need to verify a PHAR signed using a private key + + Box supports verifying private key signed PHARs without using + either extensions. Note however, that the entire PHAR will need + to be read into memory before the verification can be performed. + HELP, + [ + new InputArgument( + self::PHAR_ARG, + InputArgument::REQUIRED, + 'The PHAR file', + ), + ], + ); + } + + public function execute(IO $io): int + { + $pharFilePath = self::getPharFilePath($io); + + $io->newLine(); + $io->writeln( + sprintf( + '🔐️ Verifying the PHAR "%s"', + $pharFilePath, + ), + ); + $io->newLine(); + + [$verified, $signature, $throwable] = self::verifyPhar($pharFilePath); + + if (false === $verified || false === $signature) { + return self::failVerification($throwable, $io); + } + + $io->writeln('The PHAR passed verification.'); + + $io->newLine(); + $io->writeln( + sprintf( + '%s signature: %s', + $signature['hash_type'], + $signature['hash'], + ), + ); + + return ExitCode::SUCCESS; + } + + private static function getPharFilePath(IO $io): string + { + $pharPath = Path::canonicalize( + $io->getTypedArgument(self::PHAR_ARG)->asNonEmptyString(), + ); + + Assert::file($pharPath); + + $pharRealPath = realpath($pharPath); + + return false === $pharRealPath ? $pharPath : $pharRealPath; + } + + /** + * @return array{bool, array{hash: string, hash_type:string}|false, Throwable|null} + */ + private static function verifyPhar(string $pharFilePath): array + { + $verified = false; + $signature = false; + $throwable = null; + + try { + $pharInfo = new PharInfo($pharFilePath); + + $verified = true; + $signature = $pharInfo->getSignature(); + } catch (Throwable $throwable) { + // Continue + } + + return [ + $verified, + $signature, + $throwable, + ]; + } + + private static function failVerification(?Throwable $throwable, IO $io): int + { + $message = null !== $throwable && '' !== $throwable->getMessage() + ? $throwable->getMessage() + : 'Unknown reason.'; + + $io->writeln( + sprintf( + 'The PHAR failed the verification: %s', + $message, + ), + ); + + if (null !== $throwable && $io->isDebug()) { + throw $throwable; + } + + return ExitCode::FAILURE; + } +} diff --git a/fixtures/bench/with-compactors/src/Console/ConfigurationLoader.php b/fixtures/bench/with-compactors/src/Console/ConfigurationLoader.php new file mode 100644 index 000000000..e04ddc4c3 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/ConfigurationLoader.php @@ -0,0 +1,85 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Fidry\Console\IO; +use InvalidArgumentException; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\Configuration\ConfigurationLoader as ConfigLoader; +use KevinGH\Box\Configuration\NoConfigurationFound; +use KevinGH\Box\Json\JsonValidationException; +use KevinGH\Box\NotInstantiable; +use function sprintf; + +/** + * Utility to load the configuration. + * + * @private + */ +final class ConfigurationLoader +{ + use NotInstantiable; + + /** + * Returns the configuration settings. + * + * @param bool $allowNoFile Load the config nonetheless if not file is found when true + * + * @throws JsonValidationException|NoConfigurationFound + */ + public static function getConfig( + ?string $configPath, + IO $io, + bool $allowNoFile, + ): Configuration { + $configPath = self::getConfigPath($configPath, $io, $allowNoFile); + $configLoader = new ConfigLoader(); + + try { + return $configLoader->loadFile($configPath); + } catch (InvalidArgumentException $invalidConfig) { + $io->error('The configuration file is invalid.'); + + throw $invalidConfig; + } + } + + private static function getConfigPath( + ?string $configPath, + IO $io, + bool $allowNoFile, + ): ?string { + try { + $configPath ??= ConfigurationLocator::findDefaultPath(); + } catch (NoConfigurationFound $noConfigurationFound) { + if (false === $allowNoFile) { + throw $noConfigurationFound; + } + + $io->comment('Loading without a configuration file.'); + + return null; + } + + $io->comment( + sprintf( + 'Loading the configuration file "%s".', + $configPath, + ), + ); + + return $configPath; + } +} diff --git a/fixtures/bench/with-compactors/src/Console/ConfigurationLocator.php b/fixtures/bench/with-compactors/src/Console/ConfigurationLocator.php new file mode 100644 index 000000000..5bf9da767 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/ConfigurationLocator.php @@ -0,0 +1,53 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use KevinGH\Box\Configuration\NoConfigurationFound; +use KevinGH\Box\NotInstantiable; +use function file_exists; +use function realpath; + +/** + * @private + */ +final class ConfigurationLocator +{ + use NotInstantiable; + + private const FILE_NAME = 'box.json'; + + /** + * @var list + */ + private static array $candidates; + + public static function findDefaultPath(): string + { + if (!isset(self::$candidates)) { + self::$candidates = [ + self::FILE_NAME, + self::FILE_NAME.'.dist', + ]; + } + + foreach (self::$candidates as $candidate) { + if (file_exists($candidate)) { + return realpath($candidate); + } + } + + throw new NoConfigurationFound(); + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Logger/CompilerLogger.php b/fixtures/bench/with-compactors/src/Console/Logger/CompilerLogger.php new file mode 100644 index 000000000..5b19775f5 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Logger/CompilerLogger.php @@ -0,0 +1,69 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Logger; + +use Fidry\Console\IO; +use InvalidArgumentException; +use Symfony\Component\Console\Output\OutputInterface; +use function sprintf; + +/** + * @internal + */ +final class CompilerLogger +{ + public const QUESTION_MARK_PREFIX = '?'; + public const STAR_PREFIX = '*'; + public const PLUS_PREFIX = '+'; + public const MINUS_PREFIX = '-'; + public const CHEVRON_PREFIX = '>'; + + public function __construct(private readonly IO $io) + { + } + + public function getIO(): IO + { + return $this->io; + } + + public function log(string $prefix, string $message, int $verbosity = OutputInterface::OUTPUT_NORMAL): void + { + $prefix = match ($prefix) { + '!' => "{$prefix}", + self::STAR_PREFIX => "{$prefix}", + self::QUESTION_MARK_PREFIX => "{$prefix}", + self::PLUS_PREFIX, self::MINUS_PREFIX => " {$prefix}", + self::CHEVRON_PREFIX => " {$prefix}", + default => throw new InvalidArgumentException('Expected one of the logger constant as a prefix.'), + }; + + $this->io->writeln( + "{$prefix} {$message}", + $verbosity, + ); + } + + public function logStartBuilding(string $path): void + { + $this->io->writeln( + sprintf( + '🔨 Building the PHAR "%s"', + $path, + ), + ); + $this->io->newLine(); + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Logo.php b/fixtures/bench/with-compactors/src/Console/Logo.php new file mode 100644 index 000000000..dffe7a177 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Logo.php @@ -0,0 +1,37 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use KevinGH\Box\NotInstantiable; + +/** + * @private + */ +final class Logo +{ + use NotInstantiable; + + public const LOGO_ASCII = <<<'ASCII' + + ____ + / __ )____ _ __ + / __ / __ \| |/_/ + / /_/ / /_/ /> < + /_____/\____/_/|_| + + + + ASCII; +} diff --git a/fixtures/bench/with-compactors/src/Console/MessageRenderer.php b/fixtures/bench/with-compactors/src/Console/MessageRenderer.php new file mode 100644 index 000000000..b39bbd14c --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/MessageRenderer.php @@ -0,0 +1,78 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Fidry\Console\IO; +use KevinGH\Box\NotInstantiable; +use Webmozart\Assert\Assert; +use function array_map; +use function count; +use function sprintf; + +/** + * Utility to writing on the console output the configuration recommendations and warnings. + * + * @private + */ +final class MessageRenderer +{ + use NotInstantiable; + + /** + * @param string[] $recommendations + * @param string[] $warnings + */ + public static function render(IO $io, array $recommendations, array $warnings): void + { + Assert::allString($recommendations); + Assert::allString($warnings); + + $renderMessage = static fn (string $message): string => " - {$message}"; + + if ([] === $recommendations) { + $io->writeln('No recommendation found.'); + } else { + $io->writeln( + sprintf( + '💡 %d %s found:', + count($recommendations), + count($recommendations) > 1 ? 'recommendations' : 'recommendation', + ), + ); + + $io->writeln( + array_map($renderMessage, $recommendations), + ); + } + + if ([] === $warnings) { + $io->writeln('No warning found.'); + } else { + $io->writeln( + sprintf( + '⚠️ %d %s found:', + count($warnings), + count($warnings) > 1 ? 'warnings' : 'warning', + ), + ); + + $io->writeln( + array_map($renderMessage, $warnings), + ); + } + + $io->newLine(); + } +} diff --git a/fixtures/bench/with-compactors/src/Console/OpenFileDescriptorLimiter.php b/fixtures/bench/with-compactors/src/Console/OpenFileDescriptorLimiter.php new file mode 100644 index 000000000..bded877e8 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/OpenFileDescriptorLimiter.php @@ -0,0 +1,95 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Closure; +use Fidry\Console\IO; +use KevinGH\Box\Noop; +use KevinGH\Box\NotInstantiable; +use Symfony\Component\Console\Output\OutputInterface; +use function function_exists; +use function posix_getrlimit; +use function posix_setrlimit; +use function sprintf; +use const POSIX_RLIMIT_INFINITY; +use const POSIX_RLIMIT_NOFILE; + +/** + * @internal + */ +final class OpenFileDescriptorLimiter +{ + use NotInstantiable; + + private const LIMIT_MARGIN = 128; + + /** + * Bumps the maximum number of open file descriptor if necessary. + * + * @return Closure Callable to call to restore the original maximum number of open files descriptors + */ + public static function bumpLimit(int $count, IO $io): Closure + { + $count += self::LIMIT_MARGIN; // Add a little extra for good measure + + if (false === function_exists('posix_getrlimit') || false === function_exists('posix_setrlimit')) { + $io->writeln( + '[debug] Could not check the maximum number of open file descriptors: the functions "posix_getrlimit()" and ' + .'"posix_setrlimit" could not be found.', + OutputInterface::VERBOSITY_DEBUG, + ); + + return Noop::create(); + } + + $softLimit = posix_getrlimit()['soft openfiles']; + $hardLimit = posix_getrlimit()['hard openfiles']; + + if ($softLimit >= $count) { + return Noop::create(); + } + + $io->writeln( + sprintf( + '[debug] Increased the maximum number of open file descriptors from ("%s", "%s") to ("%s", "%s")' + .'', + $softLimit, + $hardLimit, + $count, + 'unlimited', + ), + OutputInterface::VERBOSITY_DEBUG, + ); + + posix_setrlimit( + POSIX_RLIMIT_NOFILE, + $count, + 'unlimited' === $hardLimit ? POSIX_RLIMIT_INFINITY : $hardLimit, + ); + + return static function () use ($io, $softLimit, $hardLimit): void { + posix_setrlimit( + POSIX_RLIMIT_NOFILE, + $softLimit, + 'unlimited' === $hardLimit ? POSIX_RLIMIT_INFINITY : $hardLimit, + ); + + $io->writeln( + '[debug] Restored the maximum number of open file descriptors', + OutputInterface::VERBOSITY_DEBUG, + ); + }; + } +} diff --git a/fixtures/bench/with-compactors/src/Console/OutputFormatterConfigurator.php b/fixtures/bench/with-compactors/src/Console/OutputFormatterConfigurator.php new file mode 100644 index 000000000..f06656610 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/OutputFormatterConfigurator.php @@ -0,0 +1,57 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Fidry\Console\IO; +use KevinGH\Box\NotInstantiable; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; + +/** + * Utility to configure the output formatter styles. + * + * @private + */ +final class OutputFormatterConfigurator +{ + use NotInstantiable; + + public static function configure(IO $io): void + { + self::configureFormatter( + $io->getOutput()->getFormatter(), + ); + } + + public static function configureFormatter(OutputFormatterInterface $outputFormatter): void + { + $outputFormatter->setStyle( + 'recommendation', + new OutputFormatterStyle('black', 'yellow'), + ); + $outputFormatter->setStyle( + 'warning', + new OutputFormatterStyle('white', 'red'), + ); + $outputFormatter->setStyle( + 'diff-expected', + new OutputFormatterStyle('green'), + ); + $outputFormatter->setStyle( + 'diff-actual', + new OutputFormatterStyle('red'), + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Console/PharInfoRenderer.php b/fixtures/bench/with-compactors/src/Console/PharInfoRenderer.php new file mode 100644 index 000000000..14559216c --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/PharInfoRenderer.php @@ -0,0 +1,480 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Closure; +use DateTimeImmutable; +use Fidry\Console\IO; +use KevinGH\Box\Noop; +use KevinGH\Box\NotInstantiable; +use KevinGH\Box\Phar\CompressionAlgorithm; +use KevinGH\Box\Phar\PharInfo; +use KevinGH\Box\RequirementChecker\Requirement; +use KevinGH\Box\RequirementChecker\RequirementType; +use SplFileInfo; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Path; +use function array_filter; +use function array_key_last; +use function array_map; +use function array_reduce; +use function array_sum; +use function array_values; +use function count; +use function implode; +use function is_array; +use function KevinGH\Box\format_size; +use function KevinGH\Box\format_size as format_size1; +use function key; +use function preg_match; +use function round; +use function Safe\filesize; +use function sprintf; + +/** + * Utility to write to the console output various PHAR related pieces of information. + * + * @private + */ +final class PharInfoRenderer +{ + use NotInstantiable; + + private const BOX_REQUIREMENTS = '.box/.requirements.php'; + private const BOX_VERSION_PATTERN = '/ \* Generated by Humbug Box (?.+)\.\s/'; + private const INDENT_SIZE = 2; + + public static function renderShortSummary( + PharInfo $pharInfo, + IO $io, + ?Closure $separator = null, + ): void { + $separator ??= Noop::create(); + + $methods = [ + self::renderCompression(...), + self::renderSignature(...), + self::renderMetadata(...), + self::renderTimestamp(...), + self::renderRequirementChecker(...), + self::renderContentsSummary(...), + ]; + + $lastIndex = count($methods) - 1; + + foreach ($methods as $index => $method) { + $method($pharInfo, $io); + + if ($index !== $lastIndex) { + $separator(); + } + } + } + + public static function renderVersion(PharInfo $pharInfo, IO $io): void + { + $io->writeln( + sprintf( + 'API Version: %s', + $pharInfo->getVersion(), + ), + ); + } + + public static function renderBoxVersion(PharInfo $pharInfo, IO $io): void + { + $version = self::extractBoxVersion($pharInfo); + + if (null === $version) { + return; + } + + $io->writeln( + sprintf( + 'Built with Box: %s', + $version, + ), + ); + $io->newLine(); + } + + public static function renderCompression(PharInfo $pharInfo, IO $io): void + { + $io->writeln( + sprintf( + 'Archive Compression: %s', + self::translateCompressionAlgorithm($pharInfo->getCompression()), + ), + ); + + $count = $pharInfo->getFilesCompressionCount(); + // Rename "none" to "None" + $count['None'] = $count[CompressionAlgorithm::NONE->name]; + unset($count[CompressionAlgorithm::NONE->name]); + $count = array_filter($count); + + $totalCount = array_sum($count); + + if (1 === count($count)) { + $io->writeln( + sprintf( + 'Files Compression: %s', + key($count), + ), + ); + + return; + } + + $io->writeln('Files Compression:'); + $lastAlgorithmName = array_key_last($count); + + $totalPercentage = 100; + + foreach ($count as $algorithmName => $nbrOfFiles) { + if ($lastAlgorithmName === $algorithmName) { + $percentage = $totalPercentage; + } else { + $percentage = round($nbrOfFiles * 100 / $totalCount, 2); + + $totalPercentage -= $percentage; + } + + $io->writeln( + sprintf( + ' - %s (%0.2f%%)', + $algorithmName, + $percentage, + ), + ); + } + } + + public static function renderSignature(PharInfo $pharInfo, IO $io): void + { + $signature = $pharInfo->getSignature(); + + if (null === $signature) { + $io->writeln('Signature unreadable'); + + return; + } + + $io->writeln( + sprintf( + 'Signature: %s', + $signature['hash_type'], + ), + ); + $io->writeln( + sprintf( + 'Signature Hash: %s', + $signature['hash'], + ), + ); + } + + public static function renderMetadata(PharInfo $pharInfo, IO $io): void + { + $metadata = $pharInfo->getNormalizedMetadata(); + + if (null === $metadata) { + $io->writeln('Metadata: None'); + } else { + $io->writeln('Metadata:'); + $io->writeln($metadata); + } + } + + public static function renderTimestamp(PharInfo $pharInfo, IO $io): void + { + $timestamp = $pharInfo->getTimestamp(); + $dateTime = (new DateTimeImmutable())->setTimestamp($timestamp); + + $io->writeln( + sprintf( + 'Timestamp: %s (%s)', + $timestamp, + $dateTime->format(DateTimeImmutable::ATOM), + ), + ); + } + + public static function renderRequirementChecker( + PharInfo $pharInfo, + IO $io, + ): void { + $requirements = $pharInfo->getFiles()[self::BOX_REQUIREMENTS] ?? null; + + if (null === $requirements) { + $io->writeln('RequirementChecker: Not found.'); + + return; + } + + $evaluatedRequirements = require $requirements->getPathname(); + + if (!is_array($evaluatedRequirements)) { + $io->writeln('RequirementChecker: Could not be checked.'); + + return; + } + + $io->write('RequirementChecker:'); + + if (0 === count($evaluatedRequirements)) { + $io->writeln(' No requirement found.'); + + return; + } + $io->writeln(''); + + [$required, $conflicting] = self::retrieveRequirements($evaluatedRequirements); + + self::renderRequiredSection($required, $io); + self::renderConflictingSection($conflicting, $io); + } + + public static function renderContentsSummary(PharInfo $pharInfo, IO $io): void + { + $count = array_filter($pharInfo->getFilesCompressionCount()); + $totalCount = array_sum($count); + + $io->writeln( + sprintf( + 'Contents:%s (%s)', + 1 === $totalCount ? ' 1 file' : " {$totalCount} files", + format_size( + filesize($pharInfo->getFile()), + ), + ), + ); + } + + /** + * @param false|positive-int|0 $maxDepth + * @param false|int $indent Nbr of indent or `false` + */ + public static function renderContent( + OutputInterface $output, + PharInfo $pharInfo, + int|false $maxDepth, + bool $indent, + ): void { + $depth = 0; + $renderedDirectories = []; + + foreach ($pharInfo->getFiles() as $splFileInfo) { + if (false !== $maxDepth && $depth > $maxDepth) { + continue; + } + + if ($indent) { + self::renderParentDirectoriesIfNecessary( + $splFileInfo, + $output, + $depth, + $renderedDirectories, + ); + } + + [ + 'compression' => $compression, + 'compressedSize' => $compressionSize, + ] = $pharInfo->getFileMeta($splFileInfo->getRelativePathname()); + + $compressionLine = CompressionAlgorithm::NONE === $compression + ? '[NONE]' + : "[{$compression->name}]"; + + self::print( + $output, + sprintf( + '%s %s - %s', + $indent + ? $splFileInfo->getFilename() + : $splFileInfo->getRelativePathname(), + $compressionLine, + format_size1($compressionSize), + ), + $depth, + $indent, + ); + } + } + + private static function extractBoxVersion(PharInfo $pharInfo): ?string + { + $stub = $pharInfo->getStubContent(); + + if (null !== $stub && 1 === preg_match(self::BOX_VERSION_PATTERN, $stub, $matches)) { + return $matches['version']; + } + + return null; + } + + /** + * @return array{Requirement[], Requirement[]} + */ + private static function retrieveRequirements(array $requirements): array + { + $evaluatedRequirements = array_map( + Requirement::fromArray(...), + $requirements, + ); + + [$required, $conflicting] = array_reduce( + $evaluatedRequirements, + static function ($carry, Requirement $requirement): array { + $hash = implode( + ':', + [ + $requirement->type->value, + $requirement->condition, + $requirement->source, + ], + ); + + if (RequirementType::EXTENSION_CONFLICT === $requirement->type) { + $carry[1][$hash] = $requirement; + } else { + $carry[0][$hash] = $requirement; + } + + return $carry; + }, + [[], []], + ); + + return [ + array_values($required), + array_values($conflicting), + ]; + } + + /** + * @param Requirement[] $required + */ + private static function renderRequiredSection( + array $required, + IO $io, + ): void { + if (0 === count($required)) { + return; + } + + $io->writeln(' Required:'); + $io->writeln( + array_map( + static fn (Requirement $requirement) => match ($requirement->type) { + RequirementType::PHP => sprintf( + ' - PHP %s (%s)', + $requirement->condition, + $requirement->source ?? 'root', + ), + RequirementType::EXTENSION => sprintf( + ' - ext-%s (%s)', + $requirement->condition, + $requirement->source ?? 'root', + ), + }, + $required, + ), + ); + } + + /** + * @param Requirement[] $conflicting + */ + private static function renderConflictingSection( + array $conflicting, + IO $io, + ): void { + if (0 === count($conflicting)) { + return; + } + + $io->writeln(' Conflict:'); + $io->writeln( + array_map( + static fn (Requirement $requirement) => sprintf( + ' - ext-%s (%s)', + $requirement->condition, + $requirement->source ?? 'root', + ), + $conflicting, + ), + ); + } + + private static function renderParentDirectoriesIfNecessary( + SplFileInfo $fileInfo, + OutputInterface $output, + int &$depth, + array &$renderedDirectories, + ): void { + $depth = 0; + $relativePath = $fileInfo->getRelativePath(); + + if ('' === $relativePath) { + // No parent directory: there is nothing to do. + return; + } + + $parentDirectories = explode( + '/', + Path::normalize($relativePath), + ); + + foreach ($parentDirectories as $index => $parentDirectory) { + if (array_key_exists($parentDirectory, $renderedDirectories)) { + ++$depth; + + continue; + } + + self::print( + $output, + "{$parentDirectory}/", + $index, + true, + ); + + $renderedDirectories[$parentDirectory] = true; + ++$depth; + } + + $depth = count($parentDirectories); + } + + private static function print( + OutputInterface $output, + string $message, + int $depth, + bool $indent, + ): void { + if ($indent) { + $output->write(str_repeat(' ', $depth * self::INDENT_SIZE)); + } + + $output->writeln($message); + } + + private static function translateCompressionAlgorithm(CompressionAlgorithm $algorithm): string + { + return CompressionAlgorithm::NONE === $algorithm ? 'None' : $algorithm->name; + } +} diff --git a/fixtures/bench/with-compactors/src/Console/Php/PhpSettingsHandler.php b/fixtures/bench/with-compactors/src/Console/Php/PhpSettingsHandler.php new file mode 100644 index 000000000..059716b90 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/Php/PhpSettingsHandler.php @@ -0,0 +1,159 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Php; + +use Composer\XdebugHandler\XdebugHandler; +use Fidry\FileSystem\FS; +use KevinGH\Box\Constants; +use KevinGH\Box\Phar\PharPhpSettings; +use Psr\Log\LoggerInterface; +use Webmozart\Assert\Assert; +use function getenv; +use function ini_get; +use function ini_set; +use function KevinGH\Box\format_size; +use function KevinGH\Box\memory_to_bytes; +use function sprintf; +use function trim; +use const PHP_EOL; + +/** + * @private + */ +final class PhpSettingsHandler extends XdebugHandler +{ + private LoggerInterface $logger; + private bool $pharReadonly; + + public function __construct(LoggerInterface $logger) + { + parent::__construct('box'); + + $this->setPersistent(); + + $this->setLogger($logger); + $this->logger = $logger; + + $this->pharReadonly = PharPhpSettings::isReadonly(); + $this->setPersistent(); + } + + public function check(): void + { + $this->bumpMemoryLimit(); + + parent::check(); + } + + protected function requiresRestart(bool $default): bool + { + if ($this->pharReadonly) { + $this->logger->debug('phar.readonly is enabled'); + + return true; + } + + $this->logger->debug('phar.readonly is disabled'); + + return parent::requiresRestart($default); + } + + protected function restart(array $command): void + { + // Disable phar.readonly if set + $this->disablePharReadonly(); + + parent::restart($command); + } + + private function disablePharReadonly(): void + { + if (PharPhpSettings::isReadonly()) { + Assert::notNull($this->tmpIni); + + FS::appendToFile($this->tmpIni, 'phar.readonly=0'.PHP_EOL); + + $this->logger->debug('Configured `phar.readonly=0`'); + } + } + + /** + * @see https://github.com/composer/composer/blob/34c371f5f23e25eb9aa54ccc65136cf50930612e/bin/composer#L20-L50 + */ + private function bumpMemoryLimit(): void + { + $userDefinedMemoryLimit = self::getUserDefinedMemoryLimit(); + + $memoryLimit = trim(ini_get('memory_limit')); + $memoryLimitInBytes = '-1' === $memoryLimit ? -1 : memory_to_bytes($memoryLimit); + + // Whether the memory limit should be dumped + $bumpMemoryLimit = ( + null === $userDefinedMemoryLimit + && -1 !== $memoryLimitInBytes + && $memoryLimitInBytes < 1024 * 1024 * 512 + ); + // Whether the memory limit should be set to the user defined memory limit + $setUserDefinedMemoryLimit = ( + null !== $userDefinedMemoryLimit + && $memoryLimitInBytes !== $userDefinedMemoryLimit + ); + + if ($bumpMemoryLimit && false === $setUserDefinedMemoryLimit) { + ini_set('memory_limit', '512M'); + + $this->logger->debug( + sprintf( + 'Changed the memory limit from "%s" to "%s"', + format_size($memoryLimitInBytes, 0), + '512M', + ), + ); + } elseif ($setUserDefinedMemoryLimit) { + ini_set('memory_limit', (string) $userDefinedMemoryLimit); + + $this->logger->debug( + sprintf( + 'Changed the memory limit from "%s" to %s="%s"', + format_size($memoryLimitInBytes, 0), + Constants::MEMORY_LIMIT, + format_size($userDefinedMemoryLimit, 0), + ), + ); + } else { + $this->logger->debug( + sprintf( + 'Current memory limit: "%s"', + format_size($memoryLimitInBytes, 0), + ), + ); + } + } + + private static function getUserDefinedMemoryLimit(): ?int + { + $memoryLimit = getenv(Constants::MEMORY_LIMIT); + + if (false === $memoryLimit) { + $memoryLimitInBytes = null; + } elseif ('-1' === $memoryLimit) { + $memoryLimitInBytes = -1; + } else { + $memoryLimitInBytes = memory_to_bytes($memoryLimit); + } + + return $memoryLimitInBytes; + } +} diff --git a/fixtures/bench/with-compactors/src/Console/PhpSettingsChecker.php b/fixtures/bench/with-compactors/src/Console/PhpSettingsChecker.php new file mode 100644 index 000000000..54908a792 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Console/PhpSettingsChecker.php @@ -0,0 +1,37 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Fidry\Console\IO; +use KevinGH\Box\Console\Php\PhpSettingsHandler; +use KevinGH\Box\NotInstantiable; +use Symfony\Component\Console\Logger\ConsoleLogger; + +/** + * @internal + */ +final class PhpSettingsChecker +{ + use NotInstantiable; + + public static function check(IO $io): void + { + (new PhpSettingsHandler( + new ConsoleLogger( + $io->getOutput(), + ), + ))->check(); + } +} diff --git a/fixtures/bench/with-compactors/src/Constants.php b/fixtures/bench/with-compactors/src/Constants.php new file mode 100644 index 000000000..8fa753326 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Constants.php @@ -0,0 +1,24 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +final class Constants +{ + use NotInstantiable; + + public const MEMORY_LIMIT = 'BOX_MEMORY_LIMIT'; + public const ALLOW_XDEBUG = 'BOX_ALLOW_XDEBUG'; + public const BIN = 'BOX_BIN'; +} diff --git a/fixtures/bench/with-compactors/src/DockerFileGenerator.php b/fixtures/bench/with-compactors/src/DockerFileGenerator.php new file mode 100644 index 000000000..f8ae840a9 --- /dev/null +++ b/fixtures/bench/with-compactors/src/DockerFileGenerator.php @@ -0,0 +1,163 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Composer\Semver\Semver; +use UnexpectedValueException; +use Webmozart\Assert\Assert; +use function array_column; +use function array_filter; +use function array_unique; +use function basename; +use function count; +use function implode; +use function sprintf; +use function strtr; +use const PHP_EOL; + +/** + * @private + */ +final class DockerFileGenerator +{ + private const FILE_TEMPLATE = <<<'Dockerfile' + FROM php:__BASE_PHP_IMAGE_TOKEN__ + + COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ + __REQUIRED_EXTENSIONS__ + COPY __PHAR_FILE_PATH_TOKEN__ /__PHAR_FILE_NAME_TOKEN__ + + ENTRYPOINT ["/__PHAR_FILE_NAME_TOKEN__"] + + Dockerfile; + + private const PHP_DOCKER_IMAGES = [ + // TODO: allow future images + '8.2.0' => '8.2-cli-alpine', + '8.1.0' => '8.1-cli-alpine', + '8.0.0' => '8.0-cli-alpine', + '7.4.0' => '7.4-cli-alpine', + '7.3.0' => '7.3-cli-alpine', + '7.2.0' => '7.2-cli-alpine', + '7.1.0' => '7.1-cli-alpine', + '7.0.0' => '7-cli-alpine', + ]; + + private string $image; + + /** + * @var string[] + */ + private array $extensions; + + /** + * Creates a new instance of the generator. + * + * @param array $requirements List of requirements following the format defined by the RequirementChecker component + * @param string $sourcePhar source PHAR location; This PHAR is going to be copied over to the image so the path + * should either be absolute or relative to the location of the Dockerfile + */ + public static function createForRequirements(array $requirements, string $sourcePhar): self + { + return new self( + self::retrievePhpImageName($requirements), + self::retrievePhpExtensions($requirements), + $sourcePhar, + ); + } + + /** + * @param string[] $extensions + * @param string $sourcePhar source PHAR location; This PHAR is going to be copied over to the image so the path + * should either be absolute or relative to the location of the Dockerfile + */ + public function __construct( + string $image, + array $extensions, + private readonly string $sourcePhar, + ) { + Assert::inArray($image, self::PHP_DOCKER_IMAGES); + Assert::allString($extensions); + + $this->image = $image; + $this->extensions = $extensions; + } + + public function generateStub(): string + { + $requiredExtensions = 0 === count($this->extensions) + ? '' + : sprintf( + 'RUN install-php-extensions %s%s', + implode(' ', $this->extensions), + PHP_EOL, + ); + + return strtr( + self::FILE_TEMPLATE, + [ + '__BASE_PHP_IMAGE_TOKEN__' => $this->image, + '__PHAR_FILE_PATH_TOKEN__' => $this->sourcePhar, + '__PHAR_FILE_NAME_TOKEN__' => basename($this->sourcePhar), + '__REQUIRED_EXTENSIONS__' => $requiredExtensions, + ], + ); + } + + private static function retrievePhpImageName(array $requirements): string + { + $conditions = array_column( + array_filter( + $requirements, + static fn (array $requirement): bool => 'php' === $requirement['type'], + ), + 'condition', + ); + + foreach (self::PHP_DOCKER_IMAGES as $php => $image) { + foreach ($conditions as $condition) { + if (false === Semver::satisfies($php, $condition)) { + continue 2; + } + } + + return $image; + } + + throw new UnexpectedValueException( + sprintf( + 'Could not find a suitable Docker base image for the PHP constraint(s) "%s". Images available: "%s".', + implode('", "', $conditions), + implode('", "', self::PHP_DOCKER_IMAGES), + ), + ); + } + + /** + * @return string[] + */ + private static function retrievePhpExtensions(array $requirements): array + { + return array_unique( + array_column( + array_filter( + $requirements, + static fn (array $requirement): bool => 'extension' === $requirement['type'], + ), + 'condition', + ), + ); + } +} diff --git a/fixtures/bench/with-compactors/src/ExecutableFinder.php b/fixtures/bench/with-compactors/src/ExecutableFinder.php new file mode 100644 index 000000000..f692d0cde --- /dev/null +++ b/fixtures/bench/with-compactors/src/ExecutableFinder.php @@ -0,0 +1,52 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use RuntimeException; +use Symfony\Component\Process\PhpExecutableFinder as SymfonyPhpExecutableFinder; + +final class ExecutableFinder +{ + private static string $boxExecutable; + private static string $phpExecutable; + + public static function findBoxExecutable(): string + { + if (isset(self::$boxExecutable)) { + return self::$boxExecutable; + } + + self::$boxExecutable = getenv(Constants::BIN) ?: $_SERVER['SCRIPT_NAME']; + + return self::$boxExecutable; + } + + public static function findPhpExecutable(): string + { + if (isset(self::$phpExecutable)) { + return self::$phpExecutable; + } + + $phpExecutable = (new SymfonyPhpExecutableFinder())->find(); + + if (false === $phpExecutable) { + throw new RuntimeException('Could not find a PHP executable.'); + } + + self::$phpExecutable = $phpExecutable; + + return self::$phpExecutable; + } +} diff --git a/fixtures/bench/with-compactors/src/Json/Json.php b/fixtures/bench/with-compactors/src/Json/Json.php new file mode 100644 index 000000000..a49b999fd --- /dev/null +++ b/fixtures/bench/with-compactors/src/Json/Json.php @@ -0,0 +1,110 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Json; + +use Fidry\FileSystem\FS; +use JsonSchema\Validator; +use Seld\JsonLint\JsonParser; +use Seld\JsonLint\ParsingException; +use stdClass; +use function implode; +use function json_decode; +use function json_last_error; +use const JSON_ERROR_NONE; +use const JSON_ERROR_UTF8; + +/** + * @private + */ +final class Json +{ + private JsonParser $linter; + + public function __construct() + { + $this->linter = new JsonParser(); + } + + /** + * @throws ParsingException + */ + public function lint(string $json): void + { + $result = $this->linter->lint($json); + + if ($result instanceof ParsingException) { + throw $result; + } + } + + /** + * @throws ParsingException + */ + public function decode(string $json, bool $assoc = false): array|stdClass + { + $data = json_decode($json, $assoc); + + if (JSON_ERROR_NONE !== ($error = json_last_error())) { + // Swallow the UTF-8 error and relies on the lint instead otherwise + if (JSON_ERROR_UTF8 === $error) { + throw new ParsingException('JSON decoding failed: Malformed UTF-8 characters, possibly incorrectly encoded'); + } + + $this->lint($json); + } + + return false === $assoc ? (object) $data : $data; // If JSON is an empty JSON json_decode returns an empty + // array instead of an stdClass instance + } + + /** + * @throws ParsingException + */ + public function decodeFile(string $file, bool $assoc = false): array|stdClass + { + $json = FS::getFileContents($file); + + return $this->decode($json, $assoc); + } + + /** + * Validates the decoded JSON data. + * + * @param string $file The JSON file + * @param stdClass $json The decoded JSON data + * @param stdClass $schema The JSON schema + * + * @throws JsonValidationException If the JSON data failed validation + */ + public function validate(string $file, stdClass $json, stdClass $schema): void + { + $validator = new Validator(); + $validator->check($json, $schema); + + if (!$validator->isValid()) { + $errors = []; + + foreach ($validator->getErrors() as $error) { + $errors[] = ($error['property'] ? $error['property'].' : ' : '').$error['message']; + } + + $message = [] !== $errors + ? "\"{$file}\" does not match the expected JSON schema:\n - ".implode("\n - ", $errors) + : "\"{$file}\" does not match the expected JSON schema."; + + throw new JsonValidationException($message, $file, $errors); + } + } +} diff --git a/fixtures/bench/with-compactors/src/Json/JsonValidationException.php b/fixtures/bench/with-compactors/src/Json/JsonValidationException.php new file mode 100644 index 000000000..288837755 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Json/JsonValidationException.php @@ -0,0 +1,62 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Json; + +use Throwable; +use UnexpectedValueException; +use Webmozart\Assert\Assert; + +/** + * @private + */ +final class JsonValidationException extends UnexpectedValueException +{ + private ?string $validatedFile; + private array $errors; + + /** + * @param string[] $errors + */ + public function __construct( + string $message, + ?string $file = null, + array $errors = [], + int $code = 0, + ?Throwable $previous = null, + ) { + if (null !== $file) { + Assert::file($file); + } + Assert::allString($errors); + + $this->validatedFile = $file; + $this->errors = $errors; + + parent::__construct($message, $code, $previous); + } + + public function getValidatedFile(): ?string + { + return $this->validatedFile; + } + + /** + * @return string[] + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/fixtures/bench/with-compactors/src/MapFile.php b/fixtures/bench/with-compactors/src/MapFile.php new file mode 100644 index 000000000..2bedefd79 --- /dev/null +++ b/fixtures/bench/with-compactors/src/MapFile.php @@ -0,0 +1,68 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Symfony\Component\Filesystem\Path; +use function preg_quote; +use function preg_replace; + +/** + * @internal + * + * @private + */ +final class MapFile +{ + /** + * @param string[][] $map + */ + public function __construct( + // Cannot have readonly properties: requires to be serializable + private string $basePath, + private array $map, + ) { + } + + public function __invoke(string $path): ?string + { + $relativePath = Path::makeRelative($path, $this->basePath); + + foreach ($this->map as $item) { + foreach ($item as $match => $replace) { + if ('' === $match) { + return $replace.'/'.$relativePath; + } + + if (str_starts_with($relativePath, $match)) { + return preg_replace( + '/^'.preg_quote($match, '/').'/', + $replace, + $relativePath, + ); + } + } + } + + return $relativePath; + } + + /** + * @return string[][] $map + */ + public function getMap(): array + { + return $this->map; + } +} diff --git a/fixtures/bench/with-compactors/src/Noop.php b/fixtures/bench/with-compactors/src/Noop.php new file mode 100644 index 000000000..3a7a12ebd --- /dev/null +++ b/fixtures/bench/with-compactors/src/Noop.php @@ -0,0 +1,42 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Closure; + +/** + * @private + */ +final class Noop +{ + use NotInstantiable; + + /** + * @var Closure():void + */ + private static Closure $noop; + + /** + * @return Closure():void + */ + public static function create(): Closure + { + if (!isset(self::$noop)) { + self::$noop = static function (): void {}; + } + + return self::$noop; + } +} diff --git a/fixtures/bench/with-compactors/src/NotInstantiable.php b/fixtures/bench/with-compactors/src/NotInstantiable.php new file mode 100644 index 000000000..5bd2a242c --- /dev/null +++ b/fixtures/bench/with-compactors/src/NotInstantiable.php @@ -0,0 +1,25 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +/** + * @private + */ +trait NotInstantiable +{ + private function __construct() + { + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/CompressionAlgorithm.php b/fixtures/bench/with-compactors/src/Phar/CompressionAlgorithm.php new file mode 100644 index 000000000..e8860b749 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/CompressionAlgorithm.php @@ -0,0 +1,71 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use Phar; +use function array_keys; +use function array_search; + +/** + * The required extension to execute the PHAR now that it is compressed. + * + * This is a tiny wrapper around the PHAR compression algorithm + * to make it a bit more type-safe and convenient to work with. + * + * @private + */ +enum CompressionAlgorithm: int +{ + case GZ = Phar::GZ; + case BZ2 = Phar::BZ2; + case NONE = Phar::NONE; + + private const LABELS = [ + 'GZ' => self::GZ, + 'BZ2' => self::BZ2, + 'NONE' => self::NONE, + ]; + + /** + * @return list + */ + public static function getLabels(): array + { + return array_keys(self::LABELS); + } + + public static function fromLabel(?string $label): self + { + return match ($label) { + 'BZ2' => self::BZ2, + 'GZ' => self::GZ, + 'NONE', null => self::NONE, + }; + } + + public function getLabel(): string + { + return array_search($this, self::LABELS, true); + } + + public function getRequiredExtension(): ?string + { + return match ($this) { + self::BZ2 => 'bz2', + self::GZ => 'zlib', + self::NONE => null, + }; + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/DiffMode.php b/fixtures/bench/with-compactors/src/Phar/DiffMode.php new file mode 100644 index 000000000..f2e651717 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/DiffMode.php @@ -0,0 +1,36 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use function array_map; + +enum DiffMode: string +{ + case FILE_NAME = 'file-name'; + case GIT = 'git'; + case GNU = 'gnu'; + case CHECKSUM = 'checksum'; + + /** + * @return list + */ + public static function values(): array + { + return array_map( + static fn (self $enum) => $enum->value, + self::cases(), + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/Differ/ChecksumDiffer.php b/fixtures/bench/with-compactors/src/Phar/Differ/ChecksumDiffer.php new file mode 100644 index 000000000..06121135d --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/Differ/ChecksumDiffer.php @@ -0,0 +1,136 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar\Differ; + +use Fidry\Console\IO; +use KevinGH\Box\Phar\PharInfo; +use UnexpectedValueException; +use ValueError; +use function hash; +use function implode; + +final class ChecksumDiffer implements Differ +{ + public function __construct( + private string $checksumAlgorithm, + ) { + } + + public function diff( + PharInfo $pharInfoA, + PharInfo $pharInfoB, + IO $io, + ): void { + $diff = self::computeDiff( + $pharInfoA, + $pharInfoB, + $this->checksumAlgorithm, + ); + + $io->writeln($diff ?? Differ::NO_DIFF_MESSAGE); + } + + private static function computeDiff( + PharInfo $pharInfoA, + PharInfo $pharInfoB, + string $checksumAlgorithm, + ): ?string { + $pharInfoAFileHashes = self::getFileHashesByRelativePathname( + $pharInfoA, + $checksumAlgorithm, + ); + $pharInfoBFileHashes = self::getFileHashesByRelativePathname( + $pharInfoB, + $checksumAlgorithm, + ); + $output = [ + '--- PHAR A', + '+++ PHAR B', + '@@ @@', + ]; + + foreach ($pharInfoAFileHashes as $filePath => $fileAHash) { + if (!array_key_exists($filePath, $pharInfoBFileHashes)) { + $output[] = $filePath; + $output[] = sprintf( + "\t- %s", + $fileAHash, + ); + + continue; + } + + $fileBHash = $pharInfoBFileHashes[$filePath]; + unset($pharInfoBFileHashes[$filePath]); + + if ($fileAHash === $fileBHash) { + continue; + } + + $output[] = $filePath; + $output[] = sprintf( + "\t- %s", + $fileAHash, + ); + $output[] = sprintf( + "\t+ %s", + $fileBHash, + ); + } + + foreach ($pharInfoBFileHashes as $filePath => $fileBHash) { + $output[] = $filePath; + $output[] = sprintf( + "\t+ %s", + $fileBHash, + ); + } + + return 3 === count($output) ? null : implode("\n", $output); + } + + /** + * @return array + */ + private static function getFileHashesByRelativePathname( + PharInfo $pharInfo, + string $algorithm, + ): array { + $hashFiles = []; + + try { + $hashFiles[$pharInfo->getStubPath()] = hash( + $algorithm, + $pharInfo->getStubContent(), + ); + + foreach ($pharInfo->getFiles() as $file) { + $hashFiles[$file->getRelativePathname()] = hash_file( + $algorithm, + $file->getPathname(), + ); + } + } catch (ValueError) { + throw new UnexpectedValueException( + sprintf( + 'Unexpected algorithm "%s". Please pick a registered hashing algorithm (checksum `hash_algos()`).', + $algorithm, + ), + ); + } + + return $hashFiles; + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/Differ/Differ.php b/fixtures/bench/with-compactors/src/Phar/Differ/Differ.php new file mode 100644 index 000000000..afdf6bd87 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/Differ/Differ.php @@ -0,0 +1,29 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar\Differ; + +use Fidry\Console\IO; +use KevinGH\Box\Phar\PharInfo; + +interface Differ +{ + public const NO_DIFF_MESSAGE = 'No difference could be observed with this mode.'; + + public function diff( + PharInfo $pharInfoA, + PharInfo $pharInfoB, + IO $io, + ): void; +} diff --git a/fixtures/bench/with-compactors/src/Phar/Differ/DifferFactory.php b/fixtures/bench/with-compactors/src/Phar/Differ/DifferFactory.php new file mode 100644 index 000000000..aeeeffe45 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/Differ/DifferFactory.php @@ -0,0 +1,33 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar\Differ; + +use KevinGH\Box\Console\Command\Extract; +use KevinGH\Box\Phar\DiffMode; + +final class DifferFactory +{ + public function create( + DiffMode $mode, + string $checksumAlgorithm, + ): Differ { + return match ($mode) { + DiffMode::FILE_NAME => new FilenameDiffer(), + DiffMode::GIT => new GitDiffer(), + DiffMode::GNU => new ProcessCommandBasedDiffer('diff --exclude='.Extract::PHAR_META_PATH), + DiffMode::CHECKSUM => new ChecksumDiffer($checksumAlgorithm), + }; + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/Differ/FilenameDiffer.php b/fixtures/bench/with-compactors/src/Phar/Differ/FilenameDiffer.php new file mode 100644 index 000000000..706d0af3d --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/Differ/FilenameDiffer.php @@ -0,0 +1,148 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar\Differ; + +use Fidry\Console\IO; +use KevinGH\Box\Console\PharInfoRenderer; +use KevinGH\Box\Phar\PharInfo; +use SplFileInfo; +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\Finder\Finder; +use function array_diff; +use function array_map; +use function array_sum; +use function count; +use function explode; +use function iterator_to_array; +use function sprintf; +use function str_replace; + +final class FilenameDiffer implements Differ +{ + public function diff( + PharInfo $pharInfoA, + PharInfo $pharInfoB, + IO $io, + ): void { + $pharAFiles = self::collectFiles($pharInfoA); + $pharBFiles = self::collectFiles($pharInfoB); + + $diffResult = [ + array_diff($pharAFiles, $pharBFiles), + array_diff($pharBFiles, $pharAFiles), + ]; + $diffCount = array_sum(array_map('count', $diffResult)); + + if (0 === $diffCount) { + $io->writeln(Differ::NO_DIFF_MESSAGE); + + return; + } + + self::printDiff( + $pharInfoA, + $pharInfoB, + $diffResult[0], + $diffResult[1], + $io, + ); + } + + /** + * @param list $filesInANotInB + * @param list $filesInBNotInA + */ + private static function printDiff( + PharInfo $pharInfoA, + PharInfo $pharInfoB, + array $filesInANotInB, + array $filesInBNotInA, + IO $io, + ): void { + $io->writeln(sprintf( + '--- Files present in "%s" but not in "%s"', + $pharInfoA->getFileName(), + $pharInfoB->getFileName(), + )); + $io->writeln(sprintf( + '+++ Files present in "%s" but not in "%s"', + $pharInfoB->getFileName(), + $pharInfoA->getFileName(), + )); + + $io->newLine(); + + self::renderPaths('-', $pharInfoA, $filesInANotInB, $io); + $io->newLine(); + self::renderPaths('+', $pharInfoB, $filesInBNotInA, $io); + + $io->newLine(2); + + $io->error( + sprintf( + '%d file(s) difference', + count($filesInANotInB) + count($filesInBNotInA), + ), + ); + } + + /** + * @param list $paths + */ + private static function renderPaths(string $symbol, PharInfo $pharInfo, array $paths, IO $io): void + { + $bufferedOutput = new BufferedOutput( + $io->getVerbosity(), + $io->isDecorated(), + $io->getOutput()->getFormatter(), + ); + + PharInfoRenderer::renderContent( + $bufferedOutput, + $pharInfo, + false, + false, + ); + + $lines = array_map( + static fn (string $line) => '' === $line ? '' : $symbol.' '.$line, + explode( + PHP_EOL, + $bufferedOutput->fetch(), + ), + ); + + $io->write($lines); + } + + /** + * @return string[] + */ + private static function collectFiles(PharInfo $pharInfo): array + { + $basePath = $pharInfo->getTmp().DIRECTORY_SEPARATOR; + + return array_map( + static fn (SplFileInfo $fileInfo): string => str_replace($basePath, '', $fileInfo->getRealPath()), + iterator_to_array( + Finder::create() + ->files() + ->in($basePath) + ->ignoreDotFiles(false), + false, + ), + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/Differ/GitDiffer.php b/fixtures/bench/with-compactors/src/Phar/Differ/GitDiffer.php new file mode 100644 index 000000000..5e2302fa7 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/Differ/GitDiffer.php @@ -0,0 +1,65 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar\Differ; + +use Fidry\Console\IO; +use KevinGH\Box\Console\Command\Extract; +use KevinGH\Box\Phar\PharInfo; +use function array_filter; +use function explode; +use function implode; +use function sprintf; +use function str_starts_with; + +final class GitDiffer implements Differ +{ + public function diff(PharInfo $pharInfoA, PharInfo $pharInfoB, IO $io): void + { + $gitDiff = ProcessCommandBasedDiffer::getDiff( + $pharInfoA, + $pharInfoB, + 'git diff --no-index', + ); + + if (null === $gitDiff) { + $io->writeln(Differ::NO_DIFF_MESSAGE); + + return; + } + + $separator = 'diff --git '; + + $diffLines = explode( + $separator, + $gitDiff, + ); + + $pharMetaLine = sprintf( + 'a%2$s/%1$s b%3$s/%1$s', + Extract::PHAR_META_PATH, + $pharInfoA->getFileName(), + $pharInfoB->getFileName(), + ); + + $filteredLines = array_filter( + $diffLines, + static fn (string $line) => !str_starts_with($line, $pharMetaLine) + ); + + $filteredDiff = implode($separator, $filteredLines); + + $io->writeln('' === $filteredDiff ? Differ::NO_DIFF_MESSAGE : $filteredDiff); + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/Differ/ProcessCommandBasedDiffer.php b/fixtures/bench/with-compactors/src/Phar/Differ/ProcessCommandBasedDiffer.php new file mode 100644 index 000000000..eab2442a1 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/Differ/ProcessCommandBasedDiffer.php @@ -0,0 +1,79 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar\Differ; + +use Fidry\Console\IO; +use KevinGH\Box\Phar\PharInfo; +use Symfony\Component\Process\Process; + +final class ProcessCommandBasedDiffer implements Differ +{ + public function __construct(private string $command) + { + } + + public function diff(PharInfo $pharInfoA, PharInfo $pharInfoB, IO $io): void + { + $result = self::getDiff( + $pharInfoA, + $pharInfoB, + $this->command, + ); + + $io->writeln($result ?? Differ::NO_DIFF_MESSAGE); + } + + public static function getDiff(PharInfo $pharInfoA, PharInfo $pharInfoB, string $command): ?string + { + $pharInfoATmp = $pharInfoA->getTmp(); + $pharInfoBTmp = $pharInfoB->getTmp(); + + $pharInfoAFileName = $pharInfoA->getFileName(); + $pharInfoBFileName = $pharInfoB->getFileName(); + + $diffCommand = implode( + ' ', + [ + $command, + $pharInfoATmp, + $pharInfoBTmp, + ], + ); + + $diffProcess = Process::fromShellCommandline($diffCommand); + $diffProcess->run(); + + // We do not check if the process is successful as if there + // is a difference between the two files then the process + // _will_ be unsuccessful. + $diff = trim($diffProcess->getOutput()); + + if ('' === $diff) { + return null; + } + + return str_replace( + [ + $pharInfoATmp, + $pharInfoBTmp, + ], + [ + $pharInfoAFileName, + $pharInfoBFileName, + ], + $diff, + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/IncompariblePhars.php b/fixtures/bench/with-compactors/src/Phar/IncompariblePhars.php new file mode 100644 index 000000000..55913d967 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/IncompariblePhars.php @@ -0,0 +1,23 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +final class IncompariblePhars extends PharError +{ + public static function signedPhars(): self + { + return new self('Cannot compare PHARs which have an external public key.'); + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/InvalidPhar.php b/fixtures/bench/with-compactors/src/Phar/InvalidPhar.php new file mode 100644 index 000000000..f2eaf5d67 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/InvalidPhar.php @@ -0,0 +1,178 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use Throwable; +use UnexpectedValueException; +use function Safe\preg_match; +use function sprintf; +use function str_contains; +use function str_ends_with; +use function str_starts_with; +use function ucfirst; + +final class InvalidPhar extends PharError +{ + public static function fileNotLocal( + string $file, + ?string $originalFile = null, + ): self { + // Covers: + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L1328 + return new self( + sprintf( + 'Could not create a Phar or PharData instance for the file path "%s"%s. PHAR objects can only be created from local files.', + $file, + null === $originalFile + ? '' + : sprintf( + ' (of the original file "%s")', + $originalFile, + ), + ), + ); + } + + public static function fileNotFound( + string $file, + ?string $originalFile = null, + ): self { + return new self( + sprintf( + 'Could not find the file "%s"%s.', + $file, + null === $originalFile + ? '' + : sprintf( + ' (of the original file "%s")', + $originalFile, + ), + ), + ); + } + + public static function fileNotReadable(string $file): self + { + return new self( + sprintf( + 'Could not read the file "%s".', + $file, + ), + ); + } + + public static function forPhar( + string $file, + ?string $originalFile, + ?Throwable $previous, + ): self { + return new self( + self::mapThrowableToErrorMessage($file, $originalFile, $previous, false), + previous: $previous, + ); + } + + public static function forPharData( + string $file, + ?string $originalFile, + ?Throwable $previous, + ): self { + return new self( + self::mapThrowableToErrorMessage($file, $originalFile, $previous, true), + previous: $previous, + ); + } + + public static function forPharAndPharData( + string $file, + ?string $originalFile, + ?Throwable $previous, + ): self { + return new self( + self::mapThrowableToErrorMessage($file, $originalFile, $previous, null), + previous: $previous, + ); + } + + private static function mapThrowableToErrorMessage( + string $file, + ?string $originalFile, + ?Throwable $throwable, + ?bool $isPharData, + ): string { + if (null === $isPharData) { + $pharObject = 'Phar or PharData'; + } else { + $pharObject = $isPharData ? 'PharData' : 'Phar'; + } + + $errorMessageStart = sprintf( + 'Could not create a %s instance for the file "%s"%s', + $pharObject, + $file, + null === $originalFile + ? '' + : sprintf( + ' (of the original file "%s")', + $originalFile, + ), + ); + $message = $throwable?->getMessage() ?? ''; + + if ($throwable instanceof UnexpectedValueException) { + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L1330 + if (str_ends_with($message, 'file extension (or combination) not recognised or the directory does not exist')) { + return sprintf( + $errorMessageStart.'. The file must have the extension "%s".', + $isPharData ? '.zip", ".tar", ".tar.bz2" or ".tar.gz' : '.phar', + ); + } + + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L1791 + // and a few other similar errors. + if (str_starts_with($message, 'internal corruption of phar ')) { + preg_match('/^internal corruption of phar \".+\" \((?.+)\)$/', $message, $matches); + + return sprintf( + $errorMessageStart.'. The archive is corrupted: %s.', + ucfirst($matches['reason']), + ); + } + + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L874 + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L892 + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L903 + if (str_contains($message, ' openssl signature ')) { + return $errorMessageStart.'. The OpenSSL signature could not be read or verified.'; + } + + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L1002 + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L1012 + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L1024 + // And analogue ones for the other signatures + if (str_contains($message, ' has a broken signature') + || str_contains($message, ' signature could not be verified') + || str_contains($message, ' has a broken or unsupported signature') + ) { + return $errorMessageStart.'. The archive signature is broken.'; + } + } + + return sprintf( + $errorMessageStart.': %s', + $message, + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/PharDiff.php b/fixtures/bench/with-compactors/src/Phar/PharDiff.php new file mode 100644 index 000000000..9d3f6976d --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/PharDiff.php @@ -0,0 +1,68 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use Fidry\Console\IO; +use KevinGH\Box\Phar\Differ\DifferFactory; +use function array_map; + +/** + * @internal + */ +final class PharDiff +{ + private readonly PharInfo $pharInfoA; + private readonly PharInfo $pharInfoB; + private readonly DifferFactory $differFactory; + + public function __construct(string $pathA, string $pathB) + { + [$pharInfoA, $pharInfoB] = array_map( + static fn (string $path) => new PharInfo($path), + [$pathA, $pathB], + ); + + $this->pharInfoA = $pharInfoA; + $this->pharInfoB = $pharInfoB; + + $this->differFactory = new DifferFactory(); + } + + public function getPharInfoA(): PharInfo + { + return $this->pharInfoA; + } + + public function getPharInfoB(): PharInfo + { + return $this->pharInfoB; + } + + public function equals(): bool + { + return $this->pharInfoA->equals($this->pharInfoB); + } + + public function diff(DiffMode $mode, string $checksumAlgorithm, IO $io): void + { + $this->differFactory + ->create($mode, $checksumAlgorithm) + ->diff( + $this->pharInfoA, + $this->pharInfoB, + $io, + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/PharError.php b/fixtures/bench/with-compactors/src/Phar/PharError.php new file mode 100644 index 000000000..d3c8e06be --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/PharError.php @@ -0,0 +1,50 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/* + * This file originates from https://github.com/paragonie/pharaoh. + * + * For maintenance reasons it had to be in-lined within Box. To simplify the + * configuration for PHP-CS-Fixer, the original license is in-lined as follows: + * + * The MIT License (MIT) + * + * Copyright (c) 2015 - 2018 Paragon Initiative Enterprises + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace KevinGH\Box\Phar; + +use Exception; + +class PharError extends Exception +{ +} diff --git a/fixtures/bench/with-compactors/src/Phar/PharFactory.php b/fixtures/bench/with-compactors/src/Phar/PharFactory.php new file mode 100644 index 000000000..6cdc7df81 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/PharFactory.php @@ -0,0 +1,119 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use Phar; +use PharData; +use Symfony\Component\Filesystem\Path; +use Throwable; +use function file_exists; + +/** + * Factory class to instantiate an _existing_ file (i.e. not to create a brand-new PHAR object). + * It is a thin wrapper around the native PHP constructor but with more friendly errors upon failure. + */ +final class PharFactory +{ + private function __construct() + { + } + + /** + * @throws InvalidPhar + */ + public static function create( + string $file, + ?string $originalFile = null, + ): Phar|PharData { + if (!Path::isLocal($file)) { + // This is needed as otherwise Phar::__construct() does correctly bail out on a URL + // path, but not on other non-local variants, e.g. FTPS, which case it may fail still + // but after a timeout, which is too slow. + throw InvalidPhar::fileNotLocal($file, $originalFile); + } + + if (!file_exists($file)) { + // We need to check this case since the goal of this factory is to instantiate an existing + // PHAR, not create a new one. + throw InvalidPhar::fileNotFound($file, $originalFile); + } + + try { + return new Phar($file); + } catch (Throwable $cannotCreatePhar) { + // Continue + } + + try { + return new PharData($file); + } catch (Throwable) { + throw InvalidPhar::forPharAndPharData($file, $originalFile, $cannotCreatePhar); + } + } + + /** + * @throws InvalidPhar + */ + public static function createPhar( + string $file, + ?string $originalFile = null, + ): Phar { + if (!Path::isLocal($file)) { + // This is needed as otherwise Phar::__construct() does correctly bail out on a URL + // path, but not on other non-local variants, e.g. FTPS, which case it may fail still + // but after a timeout, which is too slow. + throw InvalidPhar::fileNotLocal($file, $originalFile); + } + + if (!file_exists($file)) { + // We need to check this case since the goal of this factory is to instantiate an existing + // PHAR, not create a new one. + throw InvalidPhar::fileNotFound($file, $originalFile); + } + + try { + return new Phar($file); + } catch (Throwable $throwable) { + throw InvalidPhar::forPhar($file, $originalFile, $throwable); + } + } + + /** + * @throws InvalidPhar + */ + public static function createPharData( + string $file, + ?string $originalFile = null, + ): PharData { + if (!Path::isLocal($file)) { + // This is needed as otherwise Phar::__construct() does correctly bail out on a URL + // path, but not on other non-local variants, e.g. FTPS, which case it may fail still + // but after a timeout, which is too slow. + throw InvalidPhar::fileNotLocal($file, $originalFile); + } + + if (!file_exists($file)) { + // We need to check this case since the goal of this factory is to instantiate an existing + // PHAR, not create a new one. + throw InvalidPhar::fileNotFound($file); + } + + try { + return new PharData($file); + } catch (Throwable $throwable) { + throw InvalidPhar::forPharData($file, $originalFile, $throwable); + } + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/PharInfo.php b/fixtures/bench/with-compactors/src/Phar/PharInfo.php new file mode 100644 index 000000000..a71723cb1 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/PharInfo.php @@ -0,0 +1,344 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/* + * This file originates from https://github.com/paragonie/pharaoh. + * + * For maintenance reasons it had to be in-lined within Box. To simplify the + * configuration for PHP-CS-Fixer, the original license is in-lined as follows: + * + * The MIT License (MIT) + * + * Copyright (c) 2015 - 2018 Paragon Initiative Enterprises + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace KevinGH\Box\Phar; + +use Fidry\FileSystem\FS; +use KevinGH\Box\Console\Command\Extract; +use KevinGH\Box\ExecutableFinder; +use OutOfBoundsException; +use Phar; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; +use function bin2hex; +use function file_exists; +use function is_readable; +use function iter\mapKeys; +use function iter\toArrayWithKeys; +use function random_bytes; +use function sprintf; +use const DIRECTORY_SEPARATOR; + +/** + * @private + * + * PharInfo is a wrapper around the native Phar class. Its goal is to provide an equivalent API whilst being in-memory + * safe. + * + * Indeed, the native Phar API is extremely limited due to the fact that it loads the code in-memory. This pollutes the + * current process and will result in a crash if another PHAR with the same alias is loaded. This PharInfo class + * circumvents those issues by extracting all the desired information in a separate process. + */ +final class PharInfo +{ + private static array $ALGORITHMS; + private static string $stubfile; + + private PharMeta $meta; + private string $tmp; + private string $file; + private string $fileName; + private array $compressionCount; + + /** + * @var array + */ + private array $files; + + public function __construct(string $file) + { + $file = Path::canonicalize($file); + + if (!file_exists($file)) { + throw InvalidPhar::fileNotFound($file); + } + + if (!is_readable($file)) { + throw InvalidPhar::fileNotReadable($file); + } + + self::initAlgorithms(); + self::initStubFileName(); + + $this->file = $file; + $this->fileName = basename($file); + + $this->tmp = FS::makeTmpDir('HumbugBox', 'Pharaoh'); + + self::dumpPhar($file, $this->tmp); + [ + $this->meta, + $this->files, + ] = self::loadDumpedPharFiles($this->tmp); + } + + public function __destruct() + { + unset($this->pharInfo); + + if (isset($this->phar)) { + $path = $this->phar->getPath(); + unset($this->phar); + + Phar::unlinkArchive($path); + } + + if (isset($this->tmp)) { + FS::remove($this->tmp); + } + } + + public function getTmp(): string + { + return $this->tmp; + } + + public function getFile(): string + { + return $this->file; + } + + public function getPubKeyContent(): ?string + { + return $this->meta->pubKeyContent; + } + + public function hasPubKey(): bool + { + return null !== $this->getPubKeyContent(); + } + + public function getFileName(): string + { + return $this->fileName; + } + + public function equals(self $pharInfo): bool + { + return + $this->contentEquals($pharInfo) + && $this->getCompression() === $pharInfo->getCompression() + && $this->getNormalizedMetadata() === $pharInfo->getNormalizedMetadata(); + } + + /** + * Checks if the content of the given PHAR equals the current one. Note that by content is meant + * the list of files and their content. The files compression or the PHAR metadata are not considered. + */ + private function contentEquals(self $pharInfo): bool + { + // The signature only checks if the contents are equal (same files, each files same content), but do + // not check the compression of the files. + // As a result, we also need to check the compression of each file. + if ($this->getSignature() != $pharInfo->getSignature()) { + return false; + } + + foreach ($this->meta->filesMeta as $file => ['compression' => $compressionAlgorithm]) { + ['compression' => $otherCompressionAlgorithm] = $this->getFileMeta($file); + + if ($otherCompressionAlgorithm !== $compressionAlgorithm) { + return false; + } + } + + return true; + } + + public function getCompression(): CompressionAlgorithm + { + return $this->meta->compression; + } + + /** + * @return array The number of files per compression algorithm label. + */ + public function getFilesCompressionCount(): array + { + if (!isset($this->compressionCount)) { + $this->compressionCount = self::calculateCompressionCount($this->meta->filesMeta); + } + + return $this->compressionCount; + } + + /** + * @return array{'compression': CompressionAlgorithm, compressedSize: int} + */ + public function getFileMeta(string $path): array + { + $meta = $this->meta->filesMeta[$path] ?? null; + + if (null === $meta) { + throw new OutOfBoundsException( + sprintf( + 'No metadata found for the file "%s".', + $path, + ), + ); + } + + return $meta; + } + + public function getVersion(): ?string + { + // TODO: review this fallback value + return $this->meta->version ?? 'No information found'; + } + + public function getNormalizedMetadata(): ?string + { + return $this->meta->normalizedMetadata; + } + + public function getTimestamp(): int + { + return $this->meta->timestamp; + } + + public function getSignature(): ?array + { + return $this->meta->signature; + } + + public function getStubPath(): string + { + return Extract::STUB_PATH; + } + + public function getStubContent(): ?string + { + return $this->meta->stub; + } + + /** + * @return array + */ + public function getFiles(): array + { + return $this->files; + } + + private static function initAlgorithms(): void + { + if (!isset(self::$ALGORITHMS)) { + self::$ALGORITHMS = []; + + foreach (CompressionAlgorithm::cases() as $compressionAlgorithm) { + self::$ALGORITHMS[$compressionAlgorithm->value] = $compressionAlgorithm->name; + } + } + } + + private static function initStubFileName(): void + { + if (!isset(self::$stubfile)) { + self::$stubfile = bin2hex(random_bytes(12)).'.pharstub'; + } + } + + private static function dumpPhar(string $file, string $tmp): void + { + $extractPharProcess = new Process([ + ExecutableFinder::findPhpExecutable(), + ExecutableFinder::findBoxExecutable(), + 'extract', + $file, + $tmp, + '--no-interaction', + '--internal', + ]); + $extractPharProcess->run(); + + if (false === $extractPharProcess->isSuccessful()) { + throw new InvalidPhar( + $extractPharProcess->getErrorOutput(), + $extractPharProcess->getExitCode(), + new ProcessFailedException($extractPharProcess), + ); + } + } + + /** + * @return array{PharMeta, array} + */ + private static function loadDumpedPharFiles(string $tmp): array + { + $dumpedFiles = toArrayWithKeys( + mapKeys( + static fn (string $filePath) => Path::makeRelative($filePath, $tmp), + Finder::create() + ->files() + ->ignoreDotFiles(false) + ->exclude('.phar') + ->in($tmp), + ), + ); + + $meta = PharMeta::fromJson(FS::getFileContents($tmp.DIRECTORY_SEPARATOR.Extract::PHAR_META_PATH)); + unset($dumpedFiles[Extract::PHAR_META_PATH]); + + return [$meta, $dumpedFiles]; + } + + /** + * @param array $filesMeta + */ + private static function calculateCompressionCount(array $filesMeta): array + { + $count = array_fill_keys( + self::$ALGORITHMS, + 0, + ); + + foreach ($filesMeta as ['compression' => $compression]) { + ++$count[$compression->name]; + } + + return $count; + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/PharMeta.php b/fixtures/bench/with-compactors/src/Phar/PharMeta.php new file mode 100644 index 000000000..af51a3a4b --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/PharMeta.php @@ -0,0 +1,199 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use JetBrains\PhpStorm\ArrayShape; +use Phar; +use PharData; +use PharFileInfo; +use RecursiveDirectoryIterator; +use SplFileInfo; +use Symfony\Component\Filesystem\Path; +use UnexpectedValueException; +use function ksort; +use function Safe\json_decode; +use function Safe\json_encode; +use function Safe\realpath; +use function sprintf; +use function var_export; +use const SORT_LOCALE_STRING; + +/** + * Represents the PHAR metadata (partially). The goal is to capture enough information to interpret a PHAR + * without instantiating a Phar or PharData instance. + * + * @private + */ +final class PharMeta +{ + /** + * @param non-empty-string|null $stub + * @param non-empty-string|null $version + * @param non-empty-string|null $normalizedMetadata + * @param non-empty-string|null $pubKeyContent + * @param array $filesMeta + */ + public function __construct( + public readonly CompressionAlgorithm $compression, + #[ArrayShape(['hash' => 'string', 'hash_type' => 'string'])] + public readonly ?array $signature, + public readonly ?string $stub, + public readonly ?string $version, + public readonly ?string $normalizedMetadata, + public readonly int $timestamp, + public readonly ?string $pubKeyContent, + public readonly array $filesMeta, + ) { + } + + public static function fromPhar(Phar|PharData $phar, ?string $pubKeyContent): self + { + $compression = $phar->isCompressed(); + $signature = $phar->getSignature(); + $stub = $phar->getStub(); + $version = $phar->getVersion(); + $metadata = $phar->getMetadata(); + $timestamp = $phar->getMTime(); + + return new self( + false === $compression ? CompressionAlgorithm::NONE : CompressionAlgorithm::from($compression), + false === $signature ? null : $signature, + '' === $stub ? null : $stub, + '' === $version ? null : $version, + // TODO: check $unserializeOptions here + null === $metadata ? null : var_export($metadata, true), + $timestamp, + $pubKeyContent, + self::collectFilesMeta($phar), + ); + } + + public static function fromJson(string $json): self + { + $decodedJson = json_decode($json, true); + + $filesMeta = $decodedJson['filesMeta']; + + foreach ($filesMeta as &$fileMeta) { + $fileMeta['compression'] = CompressionAlgorithm::from($fileMeta['compression']); + } + + return new self( + CompressionAlgorithm::from($decodedJson['compression']), + $decodedJson['signature'], + $decodedJson['stub'], + $decodedJson['version'], + $decodedJson['normalizedMetadata'], + $decodedJson['timestamp'], + $decodedJson['pubKeyContent'], + $filesMeta, + ); + } + + public function toJson(): string + { + return json_encode([ + 'compression' => $this->compression, + 'signature' => $this->signature, + 'stub' => $this->stub, + 'version' => $this->version, + 'normalizedMetadata' => $this->normalizedMetadata, + 'timestamp' => $this->timestamp, + 'pubKeyContent' => $this->pubKeyContent, + 'filesMeta' => $this->filesMeta, + ]); + } + + /** + * @return array + */ + private static function collectFilesMeta(Phar|PharData $phar): array + { + $filesMeta = []; + + $root = self::getPharRoot($phar); + + self::traverseSource( + $root, + $phar, + $filesMeta, + ); + + ksort($filesMeta, SORT_LOCALE_STRING); + + return $filesMeta; + } + + /** + * @param iterable $source + * + * @return array + */ + private static function traverseSource( + string $root, + iterable $source, + array &$filesMeta, + ): void { + foreach ($source as $path => $pharFileInfo) { + if (!($pharFileInfo instanceof PharFileInfo)) { + $pharFileInfo = new PharFileInfo($path); + } + + if ($pharFileInfo->isDir()) { + self::traverseSource( + $root, + new RecursiveDirectoryIterator($pharFileInfo->getPathname()), + $filesMeta, + ); + + continue; + } + + $relativePath = Path::makeRelative($path, $root); + + $filesMeta[$relativePath] = [ + 'compression' => self::getCompressionAlgorithm($pharFileInfo), + 'compressedSize' => $pharFileInfo->getCompressedSize(), + ]; + } + } + + private static function getPharRoot(Phar|PharData $phar): string + { + return 'phar://'.Path::normalize(realpath($phar->getPath())); + } + + private static function getCompressionAlgorithm(PharFileInfo $pharFileInfo): CompressionAlgorithm + { + if (false === $pharFileInfo->isCompressed()) { + return CompressionAlgorithm::NONE; + } + + foreach (CompressionAlgorithm::cases() as $compressionAlgorithm) { + if (CompressionAlgorithm::NONE !== $compressionAlgorithm + && $pharFileInfo->isCompressed($compressionAlgorithm->value) + ) { + return $compressionAlgorithm; + } + } + + throw new UnexpectedValueException( + sprintf( + 'Unknown compression algorithm for the file "%s', + $pharFileInfo->getPath(), + ), + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/PharPhpSettings.php b/fixtures/bench/with-compactors/src/Phar/PharPhpSettings.php new file mode 100644 index 000000000..7a581259c --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/PharPhpSettings.php @@ -0,0 +1,29 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use function ini_get; + +final class PharPhpSettings +{ + public static function isReadonly(): bool + { + return '1' === ini_get('phar.readonly'); + } + + private function __construct() + { + } +} diff --git a/fixtures/bench/with-compactors/src/Phar/SigningAlgorithm.php b/fixtures/bench/with-compactors/src/Phar/SigningAlgorithm.php new file mode 100644 index 000000000..a8bd4a610 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Phar/SigningAlgorithm.php @@ -0,0 +1,75 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use Phar; +use UnexpectedValueException; +use function array_keys; +use function array_search; + +/** + * The required extension to execute the PHAR now that it is compressed. + * + * This is a tiny wrapper around the PHAR compression algorithm + * to make it a bit more type-safe and convenient to work with. + * + * @private + */ +enum SigningAlgorithm: int +{ + case MD5 = Phar::MD5; + case SHA1 = Phar::SHA1; + case SHA256 = Phar::SHA256; + case SHA512 = Phar::SHA512; + case OPENSSL = Phar::OPENSSL; + + private const LABELS = [ + 'MD5' => Phar::MD5, + 'SHA1' => Phar::SHA1, + 'SHA256' => Phar::SHA256, + 'SHA512' => Phar::SHA512, + 'OPENSSL' => Phar::OPENSSL, + ]; + + /** + * @return list + */ + public static function getLabels(): array + { + return array_keys(self::LABELS); + } + + public static function fromLabel(string $label): self + { + return match ($label) { + 'MD5' => self::MD5, + 'SHA1' => self::SHA1, + 'SHA256' => self::SHA256, + 'SHA512' => self::SHA512, + 'OPENSSL' => self::OPENSSL, + default => throw new UnexpectedValueException( + sprintf( + 'The signing algorithm "%s" is not supported by your current PHAR version.', + $label, + ), + ), + }; + } + + public function getLabel(): string + { + return array_search($this, self::LABELS, true); + } +} diff --git a/fixtures/bench/with-compactors/src/PharInfo/PharInfo.php b/fixtures/bench/with-compactors/src/PharInfo/PharInfo.php new file mode 100644 index 000000000..27199a5f6 --- /dev/null +++ b/fixtures/bench/with-compactors/src/PharInfo/PharInfo.php @@ -0,0 +1,149 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PharInfo; + +use KevinGH\Box\Phar\CompressionAlgorithm; +use Phar; +use PharData; +use PharFileInfo; +use RecursiveIteratorIterator; +use UnexpectedValueException; +use function KevinGH\Box\unique_id; +use function realpath; +use function str_replace; + +/** + * @deprecated Deprecated since 4.4.1 in favour of \KevinGH\Box\Phar\PharInfo. + */ +final class PharInfo +{ + private static array $ALGORITHMS; + + private PharData|Phar $phar; + + private ?array $compressionCount = null; + private ?string $hash = null; + + public function __construct(string $pharFile) + { + self::initAlgorithms(); + + try { + $this->phar = new Phar($pharFile); + } catch (UnexpectedValueException) { + $this->phar = new PharData($pharFile); + } + } + + private static function initAlgorithms(): void + { + if (!isset(self::$ALGORITHMS)) { + self::$ALGORITHMS = []; + + foreach (CompressionAlgorithm::cases() as $compressionAlgorithm) { + self::$ALGORITHMS[$compressionAlgorithm->value] = $compressionAlgorithm->name; + } + } + } + + public function equals(self $pharInfo): bool + { + return + $pharInfo->getCompressionCount() === $this->getCompressionCount() + && $pharInfo->getNormalizedMetadata() === $this->getNormalizedMetadata(); + } + + public function getCompressionCount(): array + { + if (null === $this->compressionCount || $this->hash !== $this->getPharHash()) { + $this->compressionCount = $this->calculateCompressionCount(); + $this->compressionCount['None'] = $this->compressionCount[CompressionAlgorithm::NONE->name]; + unset($this->compressionCount[CompressionAlgorithm::NONE->name]); + $this->hash = $this->getPharHash(); + } + + return $this->compressionCount; + } + + public function getPhar(): Phar|PharData + { + return $this->phar; + } + + public function getRoot(): string + { + // Do not cache the result + return 'phar://'.str_replace('\\', '/', realpath($this->phar->getPath())).'/'; + } + + public function getVersion(): string + { + // Do not cache the result + return '' !== $this->phar->getVersion() ? $this->phar->getVersion() : 'No information found'; + } + + public function getNormalizedMetadata(): ?string + { + // Do not cache the result + $metadata = var_export($this->phar->getMetadata(), true); + + return 'NULL' === $metadata ? null : $metadata; + } + + private function getPharHash(): string + { + // If no signature is available (e.g. a tar.gz file), we generate a random hash to ensure + // it will always be invalidated + return $this->phar->getSignature()['hash'] ?? unique_id(''); + } + + private function calculateCompressionCount(): array + { + $count = array_fill_keys( + self::$ALGORITHMS, + 0, + ); + + if ($this->phar instanceof PharData) { + $count[self::$ALGORITHMS[$this->phar->isCompressed()]] = 1; + + return $count; + } + + $countFile = static function (array $count, PharFileInfo $file): array { + if (false === $file->isCompressed()) { + ++$count[CompressionAlgorithm::NONE->name]; + + return $count; + } + + foreach (self::$ALGORITHMS as $compressionAlgorithmCode => $compressionAlgorithmName) { + if ($file->isCompressed($compressionAlgorithmCode)) { + ++$count[$compressionAlgorithmName]; + + return $count; + } + } + + return $count; + }; + + return array_reduce( + iterator_to_array(new RecursiveIteratorIterator($this->phar), true), + $countFile, + $count, + ); + } +} diff --git a/fixtures/bench/with-compactors/src/PhpScoper/ConfigurationFactory.php b/fixtures/bench/with-compactors/src/PhpScoper/ConfigurationFactory.php new file mode 100644 index 000000000..35428a04f --- /dev/null +++ b/fixtures/bench/with-compactors/src/PhpScoper/ConfigurationFactory.php @@ -0,0 +1,46 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Configuration\Configuration as PhpScoperConfiguration; +use Humbug\PhpScoper\Container; +use InvalidArgumentException; +use KevinGH\Box\NotInstantiable; +use Throwable; +use function sprintf; + +final class ConfigurationFactory +{ + use NotInstantiable; + + public static function create(?string $filePath): PhpScoperConfiguration + { + $configFactory = (new Container())->getConfigurationFactory(); + + try { + return $configFactory->create($filePath); + } catch (Throwable $throwable) { + throw new InvalidArgumentException( + sprintf( + 'Could not create a PHP-Scoper config from the file "%s": %s', + $filePath, + $throwable->getMessage(), + ), + $throwable->getCode(), + $throwable, + ); + } + } +} diff --git a/fixtures/bench/with-compactors/src/PhpScoper/ExcludedFilesScoper.php b/fixtures/bench/with-compactors/src/PhpScoper/ExcludedFilesScoper.php new file mode 100644 index 000000000..fd3c8f90c --- /dev/null +++ b/fixtures/bench/with-compactors/src/PhpScoper/ExcludedFilesScoper.php @@ -0,0 +1,41 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Scoper\Scoper as PhpScoperScoper; +use function array_flip; +use function array_key_exists; +use function func_get_args; + +final class ExcludedFilesScoper implements PhpScoperScoper +{ + private array $excludedFilePathsAsKeys; + + public function __construct( + private PhpScoperScoper $decoratedScoper, + string ...$excludedFilePaths, + ) { + $this->excludedFilePathsAsKeys = array_flip($excludedFilePaths); + } + + public function scope(string $filePath, string $contents): string + { + if (array_key_exists($filePath, $this->excludedFilePathsAsKeys)) { + return $contents; + } + + return $this->decoratedScoper->scope(...func_get_args()); + } +} diff --git a/fixtures/bench/with-compactors/src/PhpScoper/NullScoper.php b/fixtures/bench/with-compactors/src/PhpScoper/NullScoper.php new file mode 100644 index 000000000..affe05f6f --- /dev/null +++ b/fixtures/bench/with-compactors/src/PhpScoper/NullScoper.php @@ -0,0 +1,53 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Symbol\SymbolsRegistry; + +/** + * @private + */ +final class NullScoper implements Scoper +{ + public function __construct( + private SymbolsRegistry $symbolsRegistry = new SymbolsRegistry(), + ) { + } + + public function scope(string $filePath, string $contents): string + { + return $contents; + } + + public function changeSymbolsRegistry(SymbolsRegistry $symbolsRegistry): void + { + $this->symbolsRegistry = $symbolsRegistry; + } + + public function getSymbolsRegistry(): SymbolsRegistry + { + return $this->symbolsRegistry; + } + + public function getPrefix(): string + { + return ''; + } + + public function getExcludedFilePaths(): array + { + return []; + } +} diff --git a/fixtures/bench/with-compactors/src/PhpScoper/PatcherFactory.php b/fixtures/bench/with-compactors/src/PhpScoper/PatcherFactory.php new file mode 100644 index 000000000..9afa8be0c --- /dev/null +++ b/fixtures/bench/with-compactors/src/PhpScoper/PatcherFactory.php @@ -0,0 +1,44 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Patcher\Patcher; +use Humbug\PhpScoper\Patcher\PatcherChain; +use KevinGH\Box\NotInstantiable; +use Laravel\SerializableClosure\SerializableClosure; + +final class PatcherFactory +{ + use NotInstantiable; + + /** + * @param callable[] $patcher + * + * @return SerializableClosure[] + */ + public static function createSerializablePatchers(Patcher $patcher): Patcher + { + if (!($patcher instanceof PatcherChain)) { + return $patcher; + } + + $serializablePatchers = array_map( + static fn (callable $patcher) => SerializablePatcher::create($patcher), + $patcher->getPatchers(), + ); + + return new PatcherChain($serializablePatchers); + } +} diff --git a/fixtures/bench/with-compactors/src/PhpScoper/Scoper.php b/fixtures/bench/with-compactors/src/PhpScoper/Scoper.php new file mode 100644 index 000000000..07ec340fd --- /dev/null +++ b/fixtures/bench/with-compactors/src/PhpScoper/Scoper.php @@ -0,0 +1,36 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Symbol\SymbolsRegistry; + +interface Scoper +{ + /** + * Scope AKA. apply the given prefix to the file in the appropriate way. + * + * @param string $filePath File to scope + * @param string $contents Contents of the file to scope + * + * @return string Contents of the file with the prefix applied + */ + public function scope(string $filePath, string $contents): string; + + public function changeSymbolsRegistry(SymbolsRegistry $symbolsRegistry): void; + + public function getSymbolsRegistry(): SymbolsRegistry; + + public function getPrefix(): string; +} diff --git a/fixtures/bench/with-compactors/src/PhpScoper/SerializablePatcher.php b/fixtures/bench/with-compactors/src/PhpScoper/SerializablePatcher.php new file mode 100644 index 000000000..14f51d7c0 --- /dev/null +++ b/fixtures/bench/with-compactors/src/PhpScoper/SerializablePatcher.php @@ -0,0 +1,47 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Closure; +use Humbug\PhpScoper\Patcher\Patcher; +use Laravel\SerializableClosure\SerializableClosure; +use function func_get_args; + +/** + * @var PatcherCallable = (string $filePath, string $prefix, string $contents): string + */ +final class SerializablePatcher implements Patcher +{ + public static function create(callable $patcher): self + { + if ($patcher instanceof Patcher) { + $patcher = static fn (mixed ...$args) => $patcher(...$args); + } + + return new self(new SerializableClosure($patcher)); + } + + /** + * @param PatcherCallable $patch + */ + private function __construct(private Closure|SerializableClosure $patch) + { + } + + public function __invoke(string $filePath, string $prefix, string $contents): string + { + return ($this->patch)(...func_get_args()); + } +} diff --git a/fixtures/bench/with-compactors/src/PhpScoper/SerializableScoper.php b/fixtures/bench/with-compactors/src/PhpScoper/SerializableScoper.php new file mode 100644 index 000000000..152c00e87 --- /dev/null +++ b/fixtures/bench/with-compactors/src/PhpScoper/SerializableScoper.php @@ -0,0 +1,122 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Configuration\Configuration as PhpScoperConfiguration; +use Humbug\PhpScoper\Container as PhpScoperContainer; +use Humbug\PhpScoper\Scoper\Scoper as PhpScoperScoper; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use function count; + +/** + * @private + */ +final class SerializableScoper implements Scoper +{ + private PhpScoperConfiguration $scoperConfig; + private PhpScoperContainer $scoperContainer; + private PhpScoperScoper $scoper; + private SymbolsRegistry $symbolsRegistry; + + /** + * @var list + */ + public array $excludedFilePaths; + + public function __construct( + PhpScoperConfiguration $scoperConfig, + string ...$excludedFilePaths, + ) { + $this->scoperConfig = $scoperConfig->withPatcher( + PatcherFactory::createSerializablePatchers($scoperConfig->getPatcher()) + ); + $this->excludedFilePaths = $excludedFilePaths; + $this->symbolsRegistry = new SymbolsRegistry(); + } + + public function scope(string $filePath, string $contents): string + { + return $this->getScoper()->scope( + $filePath, + $contents, + ); + } + + public function changeSymbolsRegistry(SymbolsRegistry $symbolsRegistry): void + { + $this->symbolsRegistry = $symbolsRegistry; + + unset($this->scoper); + } + + public function getSymbolsRegistry(): SymbolsRegistry + { + return $this->symbolsRegistry; + } + + public function getPrefix(): string + { + return $this->scoperConfig->getPrefix(); + } + + private function getScoper(): PhpScoperScoper + { + if (isset($this->scoper)) { + return $this->scoper; + } + + if (!isset($this->scoperContainer)) { + $this->scoperContainer = new PhpScoperContainer(); + } + + $this->scoper = $this->createScoper(); + + return $this->scoper; + } + + public function __wakeup(): void + { + // We need to make sure that a fresh Scoper & PHP-Parser Parser/Lexer + // is used within a sub-process. + // Otherwise, there is a risk of data corruption or that a compatibility + // layer of some sorts (such as the tokens for PHP-Paser) is not + // triggered in the sub-process resulting in obscure errors + unset($this->scoper, $this->scoperContainer); + } + + private function createScoper(): PhpScoperScoper + { + $scoper = $this->scoperContainer + ->getScoperFactory() + ->createScoper( + $this->scoperConfig, + $this->symbolsRegistry, + ); + + if (0 === count($this->excludedFilePaths)) { + return $scoper; + } + + return new ExcludedFilesScoper( + $scoper, + ...$this->excludedFilePaths, + ); + } + + public function getExcludedFilePaths(): array + { + return $this->excludedFilePaths; + } +} diff --git a/fixtures/bench/with-compactors/src/RequirementChecker/AppRequirementsFactory.php b/fixtures/bench/with-compactors/src/RequirementChecker/AppRequirementsFactory.php new file mode 100644 index 000000000..283672ad0 --- /dev/null +++ b/fixtures/bench/with-compactors/src/RequirementChecker/AppRequirementsFactory.php @@ -0,0 +1,231 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +use KevinGH\Box\Phar\CompressionAlgorithm; +use function array_diff_key; +use function array_filter; +use function array_map; +use function array_merge_recursive; +use function array_values; + +/** + * Collect the list of requirements for running the application. + * + * @private + */ +final class AppRequirementsFactory +{ + private const SELF_PACKAGE = '__APPLICATION__'; + + /** + * @return list Configured requirements + */ + public static function create( + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): array { + return self::configureExtensionRequirements( + self::retrievePhpVersionRequirements($composerJson, $composerLock), + $composerJson, + $composerLock, + $compressionAlgorithm, + ); + } + + /** + * @return list + */ + private static function retrievePhpVersionRequirements( + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + ): array { + // If the application has a constraint on the PHP version, it is the authority. + return $composerLock->hasRequiredPhpVersion() || $composerJson->hasRequiredPhpVersion() + ? self::retrievePHPRequirementFromPlatform($composerJson, $composerLock) + : self::retrievePHPRequirementFromPackages($composerLock); + } + + /** + * @return list + */ + private static function retrievePHPRequirementFromPlatform( + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + ): array { + $requiredPhpVersion = $composerLock->getRequiredPhpVersion() ?? $composerJson->getRequiredPhpVersion(); + + return null === $requiredPhpVersion ? [] : [Requirement::forPHP($requiredPhpVersion, null)]; + } + + /** + * @return list + */ + private static function retrievePHPRequirementFromPackages(DecodedComposerLock $composerLock): array + { + return array_values( + array_map( + static fn (PackageInfo $packageInfo) => Requirement::forPHP( + $packageInfo->getRequiredPhpVersion(), + $packageInfo->getName(), + ), + array_filter( + $composerLock->getPackages(), + static fn (PackageInfo $packageInfo) => $packageInfo->hasRequiredPhpVersion(), + ), + ), + ); + } + + /** + * @param list $requirements + */ + private static function configureExtensionRequirements( + array $requirements, + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): array { + [$extensionRequirements, $extensionConflicts] = self::collectExtensionRequirements( + $composerJson, + $composerLock, + $compressionAlgorithm, + ); + + foreach ($extensionRequirements as $extension => $packages) { + foreach ($packages as $package) { + $requirements[] = Requirement::forRequiredExtension( + $extension, + self::SELF_PACKAGE === $package ? null : $package, + ); + } + } + + foreach ($extensionConflicts as $extension => $packages) { + foreach ($packages as $package) { + $requirements[] = Requirement::forConflictingExtension( + $extension, + self::SELF_PACKAGE === $package ? null : $package, + ); + } + } + + return $requirements; + } + + /** + * Collects the extension required. It also accounts for the polyfills, i.e. if the polyfill + * `symfony/polyfill-mbstring` is provided then the extension `ext-mbstring` will not be required. + * + * @return array{array>, array>} + */ + private static function collectExtensionRequirements( + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): array { + $requirements = []; + + $compressionAlgorithmRequiredExtension = $compressionAlgorithm->getRequiredExtension(); + + if (null !== $compressionAlgorithmRequiredExtension) { + $requirements[$compressionAlgorithmRequiredExtension] = [self::SELF_PACKAGE]; + } + + foreach ($composerLock->getPlatformExtensions() as $extension) { + $requirements[$extension] = [self::SELF_PACKAGE]; + } + + // If the lock is present it is the authority. If not fallback on the .json. It is pointless to check both + // since they will contain redundant information. + [$polyfills, $requirements, $conflicts] = $composerLock->isEmpty() + ? self::collectComposerJsonExtensionRequirements($composerJson, $requirements) + : self::collectComposerLockExtensionRequirements($composerLock, $requirements); + + $jsonConflicts = self::collectComposerJsonExtensionRequirements($composerJson, $requirements)[2]; + + return [ + array_diff_key($requirements, $polyfills), + array_merge_recursive($conflicts, $jsonConflicts), + ]; + } + + /** + * @param array> $requirements The key is the extension name and the value the list of sources (app literal string or the package name). + * + * @return array{array, array>, array>} + */ + private static function collectComposerJsonExtensionRequirements( + DecodedComposerJson $composerJson, + array $requirements, + ): array { + $polyfills = []; + $conflicts = []; + + foreach ($composerJson->getRequiredItems() as $packageInfo) { + $polyfilledExtension = $packageInfo->getPolyfilledExtension(); + + if (null !== $polyfilledExtension) { + $polyfills[$polyfilledExtension] = true; + + continue; + } + + foreach ($packageInfo->getRequiredExtensions() as $extension) { + $requirements[$extension] = [self::SELF_PACKAGE]; + } + } + + foreach ($composerJson->getConflictingExtensions() as $extension) { + $conflicts[$extension] = [self::SELF_PACKAGE]; + } + + return [ + $polyfills, + $requirements, + $conflicts, + ]; + } + + /** + * @param array> $requirements The key is the extension name and the value the list of sources (app literal string or the package name). + * + * @return array{array, array>, array>} + */ + private static function collectComposerLockExtensionRequirements( + DecodedComposerLock $composerLock, + array $requirements, + ): array { + $polyfills = []; + $conflicts = []; + + foreach ($composerLock->getPackages() as $packageInfo) { + foreach ($packageInfo->getPolyfilledExtensions() as $polyfilledExtension) { + $polyfills[$polyfilledExtension] = true; + } + + foreach ($packageInfo->getRequiredExtensions() as $extension) { + $requirements[$extension][] = $packageInfo->getName(); + } + + foreach ($packageInfo->getConflictingExtensions() as $extension) { + $conflicts[$extension][] = $packageInfo->getName(); + } + } + + return [$polyfills, $requirements, $conflicts]; + } +} diff --git a/fixtures/bench/with-compactors/src/RequirementChecker/DecodedComposerJson.php b/fixtures/bench/with-compactors/src/RequirementChecker/DecodedComposerJson.php new file mode 100644 index 000000000..01e0fb953 --- /dev/null +++ b/fixtures/bench/with-compactors/src/RequirementChecker/DecodedComposerJson.php @@ -0,0 +1,63 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +use function array_keys; + +/** + * @private + */ +final class DecodedComposerJson +{ + /** + * @param array $composerJsonDecodedContents Decoded JSON contents of the `composer.json` file + */ + public function __construct(private readonly array $composerJsonDecodedContents) + { + } + + public function getRequiredPhpVersion(): ?string + { + return $this->composerJsonDecodedContents['require']['php'] ?? null; + } + + public function hasRequiredPhpVersion(): bool + { + return null !== $this->getRequiredPhpVersion(); + } + + /** + * @return list + */ + public function getRequiredItems(): array + { + $require = $this->composerJsonDecodedContents['require'] ?? []; + + return array_map( + static fn (string $packageName) => new RequiredItem([$packageName => $require[$packageName]]), + array_keys($require), + ); + } + + /** + * @return list + */ + public function getConflictingExtensions(): array + { + return PackageInfo::parseExtensions( + $this->composerJsonDecodedContents['conflict'] ?? [], + ); + } +} diff --git a/fixtures/bench/with-compactors/src/RequirementChecker/DecodedComposerLock.php b/fixtures/bench/with-compactors/src/RequirementChecker/DecodedComposerLock.php new file mode 100644 index 000000000..ba5d7f473 --- /dev/null +++ b/fixtures/bench/with-compactors/src/RequirementChecker/DecodedComposerLock.php @@ -0,0 +1,64 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +use function array_map; + +/** + * @private + */ +final class DecodedComposerLock +{ + /** + * @param array $composerLockDecodedContents Decoded JSON contents of the `composer.lock` file + */ + public function __construct(private readonly array $composerLockDecodedContents) + { + } + + public function isEmpty(): bool + { + return [] === $this->composerLockDecodedContents; + } + + public function getRequiredPhpVersion(): ?string + { + return $this->composerLockDecodedContents['platform']['php'] ?? null; + } + + public function hasRequiredPhpVersion(): bool + { + return null !== $this->getRequiredPhpVersion(); + } + + /** + * @return list + */ + public function getPlatformExtensions(): array + { + return PackageInfo::parseExtensions($this->composerLockDecodedContents['platform'] ?? []); + } + + /** + * @return list + */ + public function getPackages(): array + { + return array_map( + static fn (array $package) => new PackageInfo($package), + $this->composerLockDecodedContents['packages'] ?? [], + ); + } +} diff --git a/fixtures/bench/with-compactors/src/RequirementChecker/PackageInfo.php b/fixtures/bench/with-compactors/src/RequirementChecker/PackageInfo.php new file mode 100644 index 000000000..3ff05aa25 --- /dev/null +++ b/fixtures/bench/with-compactors/src/RequirementChecker/PackageInfo.php @@ -0,0 +1,121 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +use function array_key_exists; + +/** + * @private + */ +final class PackageInfo +{ + private const EXTENSION_REGEX = '/^ext-(?.+)$/'; + + // Some extensions name differs in how they are registered in composer.json + // and the name used when doing a `extension_loaded()` check. + // See https://github.com/box-project/box/issues/653. + private const EXTENSION_NAME_MAP = [ + 'zend-opcache' => 'zend opcache', + ]; + + private const POLYFILL_MAP = [ + 'paragonie/sodium_compat' => 'libsodium', + 'phpseclib/mcrypt_compat' => 'mcrypt', + ]; + + private const SYMFONY_POLYFILL_REGEX = '/symfony\/polyfill-(?.+)/'; + + public function __construct(private readonly array $packageInfo) + { + } + + public function getName(): string + { + return $this->packageInfo['name']; + } + + public function getRequiredPhpVersion(): ?string + { + return $this->packageInfo['require']['php'] ?? null; + } + + public function hasRequiredPhpVersion(): bool + { + return null !== $this->getRequiredPhpVersion(); + } + + /** + * @return list + */ + public function getRequiredExtensions(): array + { + return self::parseExtensions($this->packageInfo['require'] ?? []); + } + + /** + * @return list + */ + public function getPolyfilledExtensions(): array + { + if (array_key_exists('provide', $this->packageInfo)) { + return self::parseExtensions($this->packageInfo['provide']); + } + + // TODO: remove the following code in 5.0. + $name = $this->packageInfo['name']; + + if (array_key_exists($name, self::POLYFILL_MAP)) { + return [self::POLYFILL_MAP[$name]]; + } + + if (1 !== preg_match(self::SYMFONY_POLYFILL_REGEX, $name, $matches)) { + return []; + } + + $extension = $matches['extension']; + + return str_starts_with($extension, 'php') ? [] : [$extension]; + } + + /** + * @return list + */ + public function getConflictingExtensions(): array + { + return array_key_exists('conflict', $this->packageInfo) + ? self::parseExtensions($this->packageInfo['conflict']) + : []; + } + + /** + * @param array $constraints + * + * @return list + */ + public static function parseExtensions(array $constraints): array + { + $extensions = []; + + foreach ($constraints as $package => $constraint) { + if (preg_match(self::EXTENSION_REGEX, $package, $matches)) { + $extension = $matches['extension']; + + $extensions[] = self::EXTENSION_NAME_MAP[$extension] ?? $extension; + } + } + + return $extensions; + } +} diff --git a/fixtures/bench/with-compactors/src/RequirementChecker/RequiredItem.php b/fixtures/bench/with-compactors/src/RequirementChecker/RequiredItem.php new file mode 100644 index 000000000..5bd38e19e --- /dev/null +++ b/fixtures/bench/with-compactors/src/RequirementChecker/RequiredItem.php @@ -0,0 +1,68 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +use function array_key_exists; +use function key; + +/** + * @private + */ +final class RequiredItem +{ + private const POLYFILL_MAP = [ + 'paragonie/sodium_compat' => 'libsodium', + 'phpseclib/mcrypt_compat' => 'mcrypt', + ]; + + private const SYMFONY_POLYFILL_REGEX = '/symfony\/polyfill-(?.+)/'; + + /** + * @param array $packageInfo + */ + public function __construct(private readonly array $packageInfo) + { + } + + public function getName(): string + { + return key($this->packageInfo); + } + + /** + * @return list + */ + public function getRequiredExtensions(): array + { + return PackageInfo::parseExtensions($this->packageInfo); + } + + public function getPolyfilledExtension(): ?string + { + $name = $this->getName(); + + if (array_key_exists($name, self::POLYFILL_MAP)) { + return self::POLYFILL_MAP[$name]; + } + + if (1 !== preg_match(self::SYMFONY_POLYFILL_REGEX, $name, $matches)) { + return null; + } + + $extension = $matches['extension']; + + return str_starts_with($extension, 'php') ? null : $extension; + } +} diff --git a/fixtures/bench/with-compactors/src/RequirementChecker/Requirement.php b/fixtures/bench/with-compactors/src/RequirementChecker/Requirement.php new file mode 100644 index 000000000..73feb9a7d --- /dev/null +++ b/fixtures/bench/with-compactors/src/RequirementChecker/Requirement.php @@ -0,0 +1,139 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +/** + * @private + */ +final class Requirement +{ + public function __construct( + public readonly RequirementType $type, + public readonly string $condition, + public readonly ?string $source, + public readonly string $message, + public readonly string $helpMessage, + ) { + } + + public static function forPHP(string $requiredPhpVersion, ?string $packageName): self + { + return new self( + RequirementType::PHP, + $requiredPhpVersion, + $packageName, + null === $packageName + ? sprintf( + 'This application requires a PHP version matching "%s".', + $requiredPhpVersion, + ) + : sprintf( + 'The package "%s" requires a PHP version matching "%s".', + $packageName, + $requiredPhpVersion, + ), + null === $packageName + ? sprintf( + 'This application requires a PHP version matching "%s".', + $requiredPhpVersion, + ) + : sprintf( + 'The package "%s" requires a PHP version matching "%s".', + $packageName, + $requiredPhpVersion, + ), + ); + } + + public static function forRequiredExtension(string $extension, ?string $packageName): self + { + return new self( + RequirementType::EXTENSION, + $extension, + $packageName, + null === $packageName + ? sprintf( + 'This application requires the extension "%s".', + $extension, + ) + : sprintf( + 'The package "%s" requires the extension "%s".', + $packageName, + $extension, + ), + null === $packageName + ? sprintf( + 'This application requires the extension "%s". You either need to enable it or request the application to be shipped with a polyfill for this extension.', + $extension, + ) + : sprintf( + 'The package "%s" requires the extension "%s". You either need to enable it or request the application to be shipped with a polyfill for this extension.', + $packageName, + $extension, + ), + ); + } + + public static function forConflictingExtension(string $extension, ?string $packageName): self + { + return new self( + RequirementType::EXTENSION_CONFLICT, + $extension, + $packageName, + null === $packageName + ? sprintf( + 'This application conflicts with the extension "%s".', + $extension, + ) + : sprintf( + 'The package "%s" conflicts with the extension "%s".', + $packageName, + $extension, + ), + null === $packageName + ? sprintf( + 'This application conflicts with the extension "%s". You need to disable it in order to run this application.', + $extension, + ) + : sprintf( + 'The package "%s" conflicts with the extension "%s". You need to disable it in order to run this application.', + $packageName, + $extension, + ), + ); + } + + public static function fromArray(array $value): self + { + return new self( + RequirementType::from($value['type']), + $value['condition'], + $value['source'] ?? null, + $value['message'], + $value['helpMessage'], + ); + } + + public function toArray(): array + { + return [ + 'type' => $this->type->value, + 'condition' => $this->condition, + 'source' => $this->source, + 'message' => $this->message, + 'helpMessage' => $this->helpMessage, + ]; + } +} diff --git a/fixtures/bench/with-compactors/src/RequirementChecker/RequirementType.php b/fixtures/bench/with-compactors/src/RequirementChecker/RequirementType.php new file mode 100644 index 000000000..e5edb1882 --- /dev/null +++ b/fixtures/bench/with-compactors/src/RequirementChecker/RequirementType.php @@ -0,0 +1,22 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +enum RequirementType: string +{ + case PHP = 'php'; + case EXTENSION = 'extension'; + case EXTENSION_CONFLICT = 'extension-conflict'; +} diff --git a/fixtures/bench/with-compactors/src/RequirementChecker/RequirementsDumper.php b/fixtures/bench/with-compactors/src/RequirementChecker/RequirementsDumper.php new file mode 100644 index 000000000..8399d8070 --- /dev/null +++ b/fixtures/bench/with-compactors/src/RequirementChecker/RequirementsDumper.php @@ -0,0 +1,95 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +use Fidry\FileSystem\FS; +use KevinGH\Box\Phar\CompressionAlgorithm; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; +use Webmozart\Assert\Assert; +use function array_map; +use function str_replace; +use function var_export; + +/** + * @private + */ +final class RequirementsDumper +{ + private const REQUIREMENTS_CONFIG_TEMPLATE = <<<'PHP' + + */ + public static function dump( + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): array { + Assert::directory(self::REQUIREMENT_CHECKER_PATH, 'Expected the requirement checker to have been dumped'); + + $filesWithContents = [ + self::dumpRequirementsConfig( + $composerJson, + $composerLock, + $compressionAlgorithm, + ), + ]; + + /** @var SplFileInfo[] $requirementCheckerFiles */ + $requirementCheckerFiles = Finder::create() + ->files() + ->in(self::REQUIREMENT_CHECKER_PATH); + + foreach ($requirementCheckerFiles as $file) { + $filesWithContents[] = [ + $file->getRelativePathname(), + FS::getFileContents($file->getPathname()), + ]; + } + + return $filesWithContents; + } + + private static function dumpRequirementsConfig( + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): array { + $requirements = array_map( + static fn (Requirement $requirement) => $requirement->toArray(), + AppRequirementsFactory::create( + $composerJson, + $composerLock, + $compressionAlgorithm, + ), + ); + + return [ + '.requirements.php', + str_replace( + '\'__CONFIG__\'', + var_export($requirements, true), + self::REQUIREMENTS_CONFIG_TEMPLATE, + ), + ]; + } +} diff --git a/fixtures/bench/with-compactors/src/StubGenerator.php b/fixtures/bench/with-compactors/src/StubGenerator.php new file mode 100644 index 000000000..b5f704ab7 --- /dev/null +++ b/fixtures/bench/with-compactors/src/StubGenerator.php @@ -0,0 +1,170 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use function addcslashes; +use function implode; +use function str_replace; + +/** + * Generates a new PHP bootstrap loader stub for a PHAR. + * + * @private + */ +final class StubGenerator +{ + use NotInstantiable; + + private const CHECK_FILE_NAME = 'bin/check-requirements.php'; + + private const STUB_TEMPLATE = <<<'STUB' + __BOX_SHEBANG__ + + + STUB; + + /** + * @param null|string $alias The alias to be used in "phar://" URLs + * @param null|string $banner The top header comment banner text + * @param null|string $index The location within the PHAR of index script + * @param bool $intercept Use the Phar::interceptFileFuncs() method? + * @param null|non-empty-string $shebang The shebang line + */ + public static function generateStub( + ?string $alias = null, + ?string $banner = null, + ?string $index = null, + bool $intercept = false, + ?string $shebang = null, + bool $checkRequirements = true, + ): string { + $stub = self::STUB_TEMPLATE; + + $stub = str_replace( + "__BOX_SHEBANG__\n", + null === $shebang ? '' : $shebang."\n", + $stub, + ); + + $stub = str_replace( + "__BOX_BANNER__\n", + self::generateBannerStmt($banner), + $stub, + ); + + return str_replace( + "__BOX_PHAR_CONFIG__\n", + self::generatePharConfigStmt( + $alias, + $index, + $intercept, + $checkRequirements, + ), + $stub, + ); + } + + private static function generateBannerStmt(?string $banner): string + { + if (null === $banner) { + return ''; + } + + $generatedBanner = "/*\n * "; + + $generatedBanner .= str_replace( + " \n", + "\n", + str_replace("\n", "\n * ", $banner), + ); + + $generatedBanner .= "\n */"; + + return "\n".$generatedBanner."\n"; + } + + private static function getAliasStmt(?string $alias): ?string + { + return null !== $alias ? 'Phar::mapPhar('.self::arg($alias).');' : null; + } + + /** + * Escapes an argument so it can be written as a string in a call. + * + * @return string The escaped argument + */ + private static function arg(string $arg, string $quote = "'"): string + { + return $quote.addcslashes($arg, $quote).$quote; + } + + private static function generatePharConfigStmt( + ?string $alias = null, + ?string $index = null, + bool $intercept = false, + bool $checkRequirements = true, + ): string { + $previous = false; + $stub = []; + $aliasStmt = self::getAliasStmt($alias); + + if (null !== $aliasStmt) { + $stub[] = $aliasStmt; + + $previous = true; + } + + if ($intercept) { + $stub[] = 'Phar::interceptFileFuncs();'; + + $previous = true; + } + + if (false !== $checkRequirements) { + if ($previous) { + $stub[] = ''; + } + + $checkRequirementsFile = self::CHECK_FILE_NAME; + + $stub[] = null === $alias + ? "require 'phar://' . __FILE__ . '/.box/{$checkRequirementsFile}';" + : "require 'phar://{$alias}/.box/{$checkRequirementsFile}';"; + + $previous = true; + } + + if (null !== $index) { + if ($previous) { + $stub[] = ''; + } + + $stub[] = null === $alias + ? "require 'phar://' . __FILE__ . '/{$index}';" + : "require 'phar://{$alias}/{$index}';"; + } + + if ([] === $stub) { + return "// No PHAR config\n"; + } + + return implode("\n", $stub)."\n"; + } +} diff --git a/fixtures/bench/with-compactors/src/Test/CommandTestCase.php b/fixtures/bench/with-compactors/src/Test/CommandTestCase.php new file mode 100644 index 000000000..33483611e --- /dev/null +++ b/fixtures/bench/with-compactors/src/Test/CommandTestCase.php @@ -0,0 +1,78 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Test; + +use Fidry\Console\Bridge\Command\SymfonyCommand; +use Fidry\Console\Command\Command; +use Fidry\Console\Test\CommandTester; +use Fidry\Console\Test\OutputAssertions; +use Symfony\Component\Console\Application; + +/** + * @private + */ +abstract class CommandTestCase extends FileSystemTestCase +{ + protected CommandTester $commandTester; + protected Command $command; + + protected function setUp(): void + { + parent::setUp(); + + $this->command = $this->getCommand(); + + $command = new SymfonyCommand($this->command); + + $application = new Application(); + $application->add($command); + + $this->commandTester = new CommandTester( + $application->get( + $command->getName(), + ), + ); + } + + protected function tearDown(): void + { + unset($this->command, $this->commandTester); + + parent::tearDown(); + } + + /** + * Returns the command to be tested. + * + * @return Command the command + */ + abstract protected function getCommand(): Command; + + /** + * @param callable(string):string $extraNormalizers + */ + public function assertSameOutput( + string $expectedOutput, + int $expectedStatusCode, + callable ...$extraNormalizers, + ): void { + OutputAssertions::assertSameOutput( + $expectedOutput, + $expectedStatusCode, + $this->commandTester, + ...$extraNormalizers, + ); + } +} diff --git a/fixtures/bench/with-compactors/src/Test/FileSystemTestCase.php b/fixtures/bench/with-compactors/src/Test/FileSystemTestCase.php new file mode 100644 index 000000000..84ea49117 --- /dev/null +++ b/fixtures/bench/with-compactors/src/Test/FileSystemTestCase.php @@ -0,0 +1,26 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Test; + +/** + * @private + */ +abstract class FileSystemTestCase extends \Fidry\FileSystem\Test\FileSystemTestCase +{ + public static function getTmpDirNamespace(): string + { + return 'BoxTest'; + } +} diff --git a/fixtures/bench/with-compactors/src/Test/RequiresPharReadonlyOff.php b/fixtures/bench/with-compactors/src/Test/RequiresPharReadonlyOff.php new file mode 100644 index 000000000..7e78de14c --- /dev/null +++ b/fixtures/bench/with-compactors/src/Test/RequiresPharReadonlyOff.php @@ -0,0 +1,33 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Test; + +use KevinGH\Box\Phar\PharPhpSettings; + +/** + * @private + */ +trait RequiresPharReadonlyOff +{ + private function markAsSkippedIfPharReadonlyIsOn(): void + { + if (PharPhpSettings::isReadonly()) { + $this->markTestSkipped( + 'Requires phar.readonly to be set to 0. Either update your php.ini file or run this test with ' + .'php -d phar.readonly=0.', + ); + } + } +} diff --git a/fixtures/bench/with-compactors/src/functions.php b/fixtures/bench/with-compactors/src/functions.php new file mode 100644 index 000000000..1ff33245f --- /dev/null +++ b/fixtures/bench/with-compactors/src/functions.php @@ -0,0 +1,192 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Composer\InstalledVersions; +use ErrorException; +use Phar; +use Symfony\Component\Console\Helper\Helper; +use Webmozart\Assert\Assert; +use function bin2hex; +use function class_alias; +use function class_exists; +use function floor; +use function is_float; +use function is_int; +use function log; +use function number_format; +use function random_bytes; +use function sprintf; +use function str_replace; + +/** + * @private + */ +function get_box_version(): string +{ + // Load manually the InstalledVersions class. + // Indeed, this class is registered to the autoloader by Composer itself which + // results an incorrect classmap entry in the scoped code. + // This strategy avoids having to exclude completely the file from the scoping. + foreach ([__DIR__.'/../vendor/composer/InstalledVersions.php', __DIR__.'/../../../composer/InstalledVersions.php'] as $file) { + if (file_exists($file)) { + require_once $file; + break; + } + } + + $prettyVersion = InstalledVersions::getPrettyVersion('humbug/box'); + $commitHash = InstalledVersions::getReference('humbug/box'); + + if (null === $commitHash) { + return $prettyVersion; + } + + return $prettyVersion.'@'.mb_substr($commitHash, 0, 7); +} + +/** + * @deprecated since 4.3.0. Use \KevinGH\Box\Phar\CompressionAlgorithm instead. + * @private + * + * @return array + */ +function get_phar_compression_algorithms(): array +{ + static $algorithms = [ + 'GZ' => Phar::GZ, + 'BZ2' => Phar::BZ2, + 'NONE' => Phar::NONE, + ]; + + return $algorithms; +} + +/** + * @deprecated Since 4.5.0. Use \KevinGH\Box\Phar\SigningAlgorithm instead. + * + * @private + * + * @return array + */ +function get_phar_signing_algorithms(): array +{ + static $algorithms = [ + 'MD5' => Phar::MD5, + 'SHA1' => Phar::SHA1, + 'SHA256' => Phar::SHA256, + 'SHA512' => Phar::SHA512, + 'OPENSSL' => Phar::OPENSSL, + ]; + + return $algorithms; +} + +/** + * @private + */ +function format_size(float|int $size, int $decimals = 2): string +{ + Assert::true(is_int($size) || is_float($size)); + + if (-1 === $size) { + return '-1'; + } + + $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + $power = $size > 0 ? (int) floor(log($size, 1024)) : 0; + + return sprintf( + '%s%s', + number_format( + $size / (1024 ** $power), + $decimals, + ), + $units[$power], + ); +} + +/** + * @private + */ +function memory_to_bytes(string $value): float|int +{ + $unit = mb_strtolower($value[mb_strlen($value) - 1]); + + $bytes = (int) $value; + + switch ($unit) { + case 'g': + $bytes *= 1024; + // no break (cumulative multiplier) + case 'm': + $bytes *= 1024; + // no break (cumulative multiplier) + case 'k': + $bytes *= 1024; + } + + return $bytes; +} + +/** + * @private + */ +function format_time(float $secs): string +{ + return str_replace( + ' ', + '', + Helper::formatTime($secs), + ); +} + +/** + * @private + */ +function register_aliases(): void +{ + // Exposes the finder used by PHP-Scoper PHAR to allow its usage in the configuration file. + if (false === class_exists(\Isolated\Symfony\Component\Finder\Finder::class)) { + class_alias(\Symfony\Component\Finder\Finder::class, \Isolated\Symfony\Component\Finder\Finder::class); + } +} + +/** + * @private + * + * @return string Random 12 characters long (plus the prefix) string composed of a-z characters and digits + */ +function unique_id(string $prefix): string +{ + return $prefix.bin2hex(random_bytes(6)); +} + +/** + * Converts errors to exceptions. + * + * @private + */ +function register_error_handler(): void +{ + set_error_handler( + static function (int $code, string $message, string $file = '', int $line = -1): void { + if (error_reporting() & $code) { + throw new ErrorException($message, 0, $code, $file, $line); + } + }, + ); +} diff --git a/fixtures/bench/without-compactors/.gitignore b/fixtures/bench/without-compactors/.gitignore new file mode 100644 index 000000000..2a37274e3 --- /dev/null +++ b/fixtures/bench/without-compactors/.gitignore @@ -0,0 +1,2 @@ +/.box_dump/ +/vendor/ diff --git a/fixtures/bench/without-compactors/box.json.dist b/fixtures/bench/without-compactors/box.json.dist new file mode 100644 index 000000000..3661decb2 --- /dev/null +++ b/fixtures/bench/without-compactors/box.json.dist @@ -0,0 +1,9 @@ +{ + "$schema": "../../../res/schema.json", + + "main": "box.php", + "output": "../../../dist/bench/box.phar", + "directories-bin": ["../../../res/requirement-checker"], + "datetime": "release-date", + "dump-autoload": false +} diff --git a/fixtures/bench/without-compactors/box.php b/fixtures/bench/without-compactors/box.php new file mode 100755 index 000000000..98d6c6c43 --- /dev/null +++ b/fixtures/bench/without-compactors/box.php @@ -0,0 +1,60 @@ +#!/usr/bin/env php + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Fidry\Console\Application\ApplicationRunner; +use Fidry\Console\IO; +use KevinGH\Box\Console\Application; +use KevinGH\Box\Console\OutputFormatterConfigurator; +use RuntimeException; +use function file_exists; +use function in_array; +use const PHP_EOL; +use const PHP_SAPI; + +// See https://github.com/easysoft/phpmicro for the micro SAPI. +if (false === in_array(PHP_SAPI, ['cli', 'phpdbg', 'embed', 'micro'], true)) { + echo PHP_EOL.'Box may only be invoked from a command line, got "'.PHP_SAPI.'"'.PHP_EOL; + + exit(1); +} + +(static function (): void { + if (file_exists($autoload = __DIR__.'/../../../autoload.php')) { + // Is installed via Composer + include_once $autoload; + + return; + } + + if (file_exists($autoload = __DIR__.'/../vendor/autoload.php')) { + // Is installed locally + include_once $autoload; + + return; + } + + throw new RuntimeException('Unable to find the Composer autoloader.'); +})(); + +register_aliases(); +register_error_handler(); + +$io = IO::createDefault(); +OutputFormatterConfigurator::configure($io); + +$runner = new ApplicationRunner(new Application()); +$runner->run($io); diff --git a/fixtures/bench/without-compactors/composer.json b/fixtures/bench/without-compactors/composer.json new file mode 100644 index 000000000..d4b8722c6 --- /dev/null +++ b/fixtures/bench/without-compactors/composer.json @@ -0,0 +1,114 @@ +{ + "name": "humbug/box", + "description": "Fast, zero config application bundler with PHARs.", + "license": "MIT", + "keywords": [ + "phar" + ], + "authors": [ + { + "name": "Kevin Herrera", + "email": "kevin@herrera.io", + "homepage": "http://kevin.herrera.io" + }, + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + } + ], + "require": { + "php": "^8.2", + "ext-iconv": "*", + "ext-mbstring": "*", + "ext-phar": "*", + "composer-plugin-api": "^2.2", + "amphp/parallel-functions": "^1.1", + "composer/semver": "^3.3.2", + "composer/xdebug-handler": "^3.0.3", + "fidry/console": "^0.6.0", + "fidry/filesystem": "^1.1", + "humbug/php-scoper": "^0.18.6", + "justinrainbow/json-schema": "^5.2.12", + "laravel/serializable-closure": "^1.2.2", + "nikic/iter": "^2.2", + "nikic/php-parser": "^4.15.2", + "phpdocumentor/reflection-docblock": "^5.3", + "phpdocumentor/type-resolver": "^1.7", + "psr/log": "^3.0", + "sebastian/diff": "^4.0", + "seld/jsonlint": "^1.9", + "seld/phar-utils": "^1.2", + "symfony/filesystem": "^6.1.5", + "symfony/finder": "^6.1.3", + "symfony/polyfill-iconv": "^1.28", + "symfony/polyfill-mbstring": "^1.28", + "symfony/process": "^6.1.3", + "symfony/var-dumper": "^6.1.6", + "webmozart/assert": "^1.11" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ergebnis/composer-normalize": "^2.29", + "fidry/makefile": "^1.0.1", + "mikey179/vfsstream": "^1.6.11", + "phpspec/prophecy": "^1.17", + "phpspec/prophecy-phpunit": "^2.0.2", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^6.1.6", + "symfony/yaml": "^6.2", + "webmozarts/strict-phpunit": "^7.6" + }, + "replace": { + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*" + }, + "suggest": { + "ext-openssl": "To accelerate private key generation." + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "KevinGH\\Box\\": "src" + }, + "files": [ + "src/functions.php" + ], + "exclude-from-classmap": [ + "/Test/", + "vendor/humbug/php-scoper/vendor-hotfix" + ] + }, + "autoload-dev": { + "psr-4": { + "KevinGH\\Box\\": [ + "fixtures", + "tests" + ] + } + }, + "bin": [ + "bin/box" + ], + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true, + "composer/package-versions-deprecated": false, + "ergebnis/composer-normalize": true + }, + "platform": { + "php": "8.2" + }, + "platform-check": false, + "sort-packages": true + }, + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "4.x-dev" + } + } +} diff --git a/fixtures/bench/without-compactors/composer.lock b/fixtures/bench/without-compactors/composer.lock new file mode 100644 index 000000000..7dc128640 --- /dev/null +++ b/fixtures/bench/without-compactors/composer.lock @@ -0,0 +1,5770 @@ +{ + "_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": "3cba203db0b518d2e4a789aca01b9887", + "packages": [ + { + "name": "amphp/amp", + "version": "v2.6.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", + "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^7 | ^8 | ^9", + "psalm/phar": "^3.11@dev", + "react/promise": "^2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "files": [ + "lib/functions.php", + "lib/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.6.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2022-02-20T17:52:18+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v1.8.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.4", + "friendsofphp/php-cs-fixer": "^2.3", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^6 || ^7 || ^8", + "psalm/phar": "^3.11.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "http://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2021-03-30T17:13:30+00:00" + }, + { + "name": "amphp/parallel", + "version": "v1.4.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/parallel.git", + "reference": "3aac213ba7858566fd83d38ccb85b91b2d652cb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parallel/zipball/3aac213ba7858566fd83d38ccb85b91b2d652cb0", + "reference": "3aac213ba7858566fd83d38ccb85b91b2d652cb0", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "amphp/byte-stream": "^1.6.1", + "amphp/parser": "^1", + "amphp/process": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^1.0.1", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.1", + "phpunit/phpunit": "^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Context/functions.php", + "lib/Sync/functions.php", + "lib/Worker/functions.php" + ], + "psr-4": { + "Amp\\Parallel\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", + "keywords": [ + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" + ], + "support": { + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v1.4.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2023-03-23T08:04:23+00:00" + }, + { + "name": "amphp/parallel-functions", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/parallel-functions.git", + "reference": "04e92fcacfc921a56dfe12c23b3265e62593a7cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parallel-functions/zipball/04e92fcacfc921a56dfe12c23b3265e62593a7cb", + "reference": "04e92fcacfc921a56dfe12c23b3265e62593a7cb", + "shasum": "" + }, + "require": { + "amphp/amp": "^2.0.3", + "amphp/parallel": "^1.4", + "amphp/serialization": "^1.0", + "laravel/serializable-closure": "^1.0", + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "v2.x-dev", + "amphp/phpunit-util": "^2.0", + "phpunit/phpunit": "^9.5.11" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\ParallelFunctions\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Parallel processing made simple.", + "support": { + "issues": "https://github.com/amphp/parallel-functions/issues", + "source": "https://github.com/amphp/parallel-functions/tree/v1.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2022-02-03T19:32:41+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "ff1de4144726c5dad5fab97f66692ebe8de3e151" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/ff1de4144726c5dad5fab97f66692ebe8de3e151", + "reference": "ff1de4144726c5dad5fab97f66692ebe8de3e151", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2022-12-30T18:08:47+00:00" + }, + { + "name": "amphp/process", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "76e9495fd6818b43a20167cb11d8a67f7744ee0f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/76e9495fd6818b43a20167cb11d8a67f7744ee0f", + "reference": "76e9495fd6818b43a20167cb11d8a67f7744ee0f", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "amphp/byte-stream": "^1.4", + "php": ">=7" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "phpunit/phpunit": "^6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous process manager.", + "homepage": "https://github.com/amphp/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2022-07-06T23:50:12+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/sync", + "version": "v1.4.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "85ab06764f4f36d63b1356b466df6111cf4b89cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/85ab06764f4f36d63b1356b466df6111cf4b89cf", + "reference": "85ab06764f4f36d63b1356b466df6111cf4b89cf", + "shasum": "" + }, + "require": { + "amphp/amp": "^2.2", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.1", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/ConcurrentIterator/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Mutex, Semaphore, and other synchronization tools for Amp.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v1.4.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2021-10-25T18:29:10+00:00" + }, + { + "name": "composer/pcre", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-10-11T07:11:09+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-08-31T09:50:34+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T21:32:43+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "reference": "4f2d4f2836e7ec4e7a8625e75c6aa916004db931", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.2" + }, + "time": "2023-09-27T20:04:15+00:00" + }, + { + "name": "fidry/console", + "version": "0.6.8", + "source": { + "type": "git", + "url": "https://github.com/theofidry/console.git", + "reference": "f77b0abe54fd6fea6ce8733c98817d82c07b6b8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/console/zipball/f77b0abe54fd6fea6ce8733c98817d82c07b6b8e", + "reference": "f77b0abe54fd6fea6ce8733c98817d82c07b6b8e", + "shasum": "" + }, + "require": { + "php": "^8.2", + "psr/log": "^3.0", + "symfony/console": "^6.3", + "symfony/event-dispatcher-contracts": "^2.5 || ^3.0", + "symfony/service-contracts": "^2.5 || ^3.0", + "thecodingmachine/safe": "^2.0", + "webmozart/assert": "^1.11" + }, + "conflict": { + "symfony/dependency-injection": "<6.3.0", + "symfony/framework-bundle": "<6.3.0", + "symfony/http-kernel": "<6.3.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.27", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^10.2", + "symfony/dependency-injection": "^6.3", + "symfony/flex": "^2.4.0", + "symfony/framework-bundle": "^6.3", + "symfony/http-kernel": "^6.3", + "symfony/yaml": "^6.3" + }, + "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.8" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2023-11-18T22:57:23+00:00" + }, + { + "name": "fidry/filesystem", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/filesystem.git", + "reference": "1dd372ab3eb8b84ffe9578bff576b00c9a44ee46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/filesystem/zipball/1dd372ab3eb8b84ffe9578bff576b00c9a44ee46", + "reference": "1dd372ab3eb8b84ffe9578bff576b00c9a44ee46", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/filesystem": "^6.3", + "thecodingmachine/safe": "^2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "ergebnis/composer-normalize": "^2.28", + "infection/infection": "^0.26", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^10.3", + "symfony/finder": "^6.3", + "symfony/phpunit-bridge": "^6.2" + }, + "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.1.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2023-10-07T07:32:54+00:00" + }, + { + "name": "humbug/php-scoper", + "version": "0.18.9", + "source": { + "type": "git", + "url": "https://github.com/humbug/php-scoper.git", + "reference": "dd0a6721c38a26e18c1e36c0a718b6262350dd96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/humbug/php-scoper/zipball/dd0a6721c38a26e18c1e36c0a718b6262350dd96", + "reference": "dd0a6721c38a26e18c1e36c0a718b6262350dd96", + "shasum": "" + }, + "require": { + "fidry/console": "^0.6.6", + "fidry/filesystem": "^1.1", + "jetbrains/phpstorm-stubs": "^v2022.2", + "nikic/php-parser": "^4.12", + "php": "^8.2", + "symfony/console": "^5.2 || ^6.0", + "symfony/filesystem": "^5.2 || ^6.0", + "symfony/finder": "^5.2 || ^6.0", + "thecodingmachine/safe": "^2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.1", + "ergebnis/composer-normalize": "^2.28", + "fidry/makefile": "^1.0", + "humbug/box": "^4.5.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.0", + "symfony/yaml": "^6.1" + }, + "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.9" + }, + "time": "2023-11-21T21:00:48+00:00" + }, + { + "name": "jetbrains/phpstorm-stubs", + "version": "v2022.3", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-stubs.git", + "reference": "6b568c153cea002dc6fad96285c3063d07cab18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/6b568c153cea002dc6fad96285c3063d07cab18d", + "reference": "6b568c153cea002dc6fad96285c3063d07cab18d", + "shasum": "" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "@stable", + "nikic/php-parser": "@stable", + "php": "^8.0", + "phpdocumentor/reflection-docblock": "@stable", + "phpunit/phpunit": "@stable" + }, + "type": "library", + "autoload": { + "files": [ + "PhpStormStubsMap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "PHP runtime & extensions header files for PhpStorm", + "homepage": "https://www.jetbrains.com/phpstorm", + "keywords": [ + "autocomplete", + "code", + "inference", + "inspection", + "jetbrains", + "phpstorm", + "stubs", + "type" + ], + "support": { + "source": "https://github.com/JetBrains/phpstorm-stubs/tree/v2022.3" + }, + "time": "2022-10-17T09:21:37+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "v5.2.13", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13" + }, + "time": "2023-09-26T02:20:38+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v1.3.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "3dbf8a8e914634c48d389c1234552666b3d43754" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3dbf8a8e914634c48d389c1234552666b3d43754", + "reference": "3dbf8a8e914634c48d389c1234552666b3d43754", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "nesbot/carbon": "^2.61", + "pestphp/pest": "^1.21.3", + "phpstan/phpstan": "^1.8.2", + "symfony/var-dumper": "^5.4.11" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2023-11-08T14:08:06+00:00" + }, + { + "name": "nikic/iter", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/iter.git", + "reference": "d9f88bc04b5b453914373e70c041353d8e67c3f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/iter/zipball/d9f88bc04b5b453914373e70c041353d8e67c3f5", + "reference": "d9f88bc04b5b453914373e70c041353d8e67c3f5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "vimeo/psalm": "^4.18 || ^5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/iter.func.php", + "src/iter.php", + "src/iter.rewindable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Iteration primitives using generators", + "keywords": [ + "functional", + "generator", + "iterator" + ], + "support": { + "issues": "https://github.com/nikic/iter/issues", + "source": "https://github.com/nikic/iter/tree/v2.3.0" + }, + "time": "2023-07-25T19:55:40+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.17.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-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/v4.17.1" + }, + "time": "2023-08-13T19:53:39+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + }, + "time": "2021-10-19T17:43:47+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", + "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.3" + }, + "time": "2023-08-12T11:01:26+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.24.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "12f01d214f1c73b9c91fdb3b1c415e4c70652083" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/12f01d214f1c73b9c91fdb3b1c415e4c70652083", + "reference": "12f01d214f1c73b9c91fdb3b1c415e4c70652083", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.3" + }, + "time": "2023-11-18T20:15:32+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.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "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.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-05-07T05:35:17+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "594fd6462aad8ecee0b45ca5045acea4776667f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/594fd6462aad8ecee0b45ca5045acea4776667f1", + "reference": "594fd6462aad8ecee0b45ca5045acea4776667f1", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2023-05-11T13:16:46+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phar" + ], + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1" + }, + "time": "2022-08-31T10:31:18+00:00" + }, + { + "name": "symfony/console", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0d14a9f6d04d4ac38a8cea1171f4554e325dae92", + "reference": "0d14a9f6d04d4ac38a8cea1171f4554e325dae92", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.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/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-10-31T08:09:35+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "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.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "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.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "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/v6.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-06-01T08:30:39+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/a1b31d88c0e998168ca7792f222cbecee47428c4", + "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.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/v6.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-09-26T12:56:25+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-iconv", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "6de50471469b8c9afc38164452ab2b6170ee71c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/6de50471469b8c9afc38164452ab2b6170ee71c1", + "reference": "6de50471469b8c9afc38164452ab2b6170ee71c1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-iconv": "*" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "875e90aeea2777b6f135677f618529449334a612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-28T09:04:16+00:00" + }, + { + "name": "symfony/process", + "version": "v6.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/0b5c29118f2e980d455d2e34a5659f4579847c54", + "reference": "0b5c29118f2e980d455d2e34a5659f4579847c54", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "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": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-07T10:39:22+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/b3313c2dbffaf71c8de2934e2ea56ed2291a3838", + "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "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.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-30T20:28:31+00:00" + }, + { + "name": "symfony/string", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "13880a87790c76ef994c91e87efb96134522577a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/13880a87790c76ef994c91e87efb96134522577a", + "reference": "13880a87790c76ef994c91e87efb96134522577a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/intl": "^6.2", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.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/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-09T08:28:21+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "81acabba9046550e89634876ca64bfcd3c06aa0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/81acabba9046550e89634876ca64bfcd3c06aa0a", + "reference": "81acabba9046550e89634876ca64bfcd3c06aa0a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "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/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-08T10:42:36+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.2", + "thecodingmachine/phpstan-strict-rules": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "files": [ + "deprecated/apc.php", + "deprecated/array.php", + "deprecated/datetime.php", + "deprecated/libevent.php", + "deprecated/misc.php", + "deprecated/password.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "deprecated/strings.php", + "lib/special_cases.php", + "deprecated/mysqli.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/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/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/", + "deprecated/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/v2.5.0" + }, + "time": "2023-04-05T11:54:14+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "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.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "packages-dev": [ + { + "name": "bamarni/composer-bin-plugin", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/bamarni/composer-bin-plugin.git", + "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880", + "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "ext-json": "*", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.5", + "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0", + "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Bamarni\\Composer\\Bin\\BamarniBinPlugin" + }, + "autoload": { + "psr-4": { + "Bamarni\\Composer\\Bin\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "No conflicts for your bin dependencies", + "keywords": [ + "composer", + "conflict", + "dependency", + "executable", + "isolation", + "tool" + ], + "support": { + "issues": "https://github.com/bamarni/composer-bin-plugin/issues", + "source": "https://github.com/bamarni/composer-bin-plugin/tree/1.8.2" + }, + "time": "2022-10-31T08:38:03+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "ergebnis/composer-normalize", + "version": "2.39.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/composer-normalize.git", + "reference": "a878360bc8cb5cb440b9381f72b0aaa125f937c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/a878360bc8cb5cb440b9381f72b0aaa125f937c7", + "reference": "a878360bc8cb5cb440b9381f72b0aaa125f937c7", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "ergebnis/json": "^1.1.0", + "ergebnis/json-normalizer": "^4.3.0", + "ergebnis/json-printer": "^3.4.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12", + "localheinz/diff": "^1.1.1", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "composer/composer": "^2.6.5", + "ergebnis/license": "^2.2.0", + "ergebnis/php-cs-fixer-config": "~6.7.0", + "ergebnis/phpunit-slow-test-detector": "^2.3.0", + "fakerphp/faker": "^1.23.0", + "infection/infection": "~0.27.4", + "phpunit/phpunit": "^10.4.1", + "psalm/plugin-phpunit": "~0.18.4", + "rector/rector": "~0.18.5", + "symfony/filesystem": "^6.0.13", + "vimeo/psalm": "^5.15.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Ergebnis\\Composer\\Normalize\\NormalizePlugin", + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + }, + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Ergebnis\\Composer\\Normalize\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a composer plugin for normalizing composer.json.", + "homepage": "https://github.com/ergebnis/composer-normalize", + "keywords": [ + "composer", + "normalize", + "normalizer", + "plugin" + ], + "support": { + "issues": "https://github.com/ergebnis/composer-normalize/issues", + "security": "https://github.com/ergebnis/composer-normalize/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/composer-normalize" + }, + "time": "2023-10-10T15:43:27+00:00" + }, + { + "name": "ergebnis/json", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json.git", + "reference": "9f2b9086c43b189d7044a5b6215a931fb6e9125d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json/zipball/9f2b9086c43b189d7044a5b6215a931fb6e9125d", + "reference": "9f2b9086c43b189d7044a5b6215a931fb6e9125d", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.29.0", + "ergebnis/data-provider": "^3.0.0", + "ergebnis/license": "^2.2.0", + "ergebnis/php-cs-fixer-config": "^6.6.0", + "ergebnis/phpunit-slow-test-detector": "^2.3.0", + "fakerphp/faker": "^1.23.0", + "infection/infection": "~0.27.4", + "phpunit/phpunit": "^10.4.1", + "psalm/plugin-phpunit": "~0.18.4", + "rector/rector": "~0.18.5", + "vimeo/psalm": "^5.15.0" + }, + "type": "library", + "extra": { + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a Json value object for representing a valid JSON string.", + "homepage": "https://github.com/ergebnis/json", + "keywords": [ + "json" + ], + "support": { + "issues": "https://github.com/ergebnis/json/issues", + "security": "https://github.com/ergebnis/json/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json" + }, + "time": "2023-10-10T07:57:48+00:00" + }, + { + "name": "ergebnis/json-normalizer", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-normalizer.git", + "reference": "716fa0a5dcc75fbcb2c1c2e0542b2f56732460bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-normalizer/zipball/716fa0a5dcc75fbcb2c1c2e0542b2f56732460bd", + "reference": "716fa0a5dcc75fbcb2c1c2e0542b2f56732460bd", + "shasum": "" + }, + "require": { + "ergebnis/json": "^1.1.0", + "ergebnis/json-pointer": "^3.2.0", + "ergebnis/json-printer": "^3.4.0", + "ergebnis/json-schema-validator": "^4.1.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "composer/semver": "^3.4.0", + "ergebnis/data-provider": "^3.0.0", + "ergebnis/license": "^2.2.0", + "ergebnis/php-cs-fixer-config": "~6.7.0", + "ergebnis/phpunit-slow-test-detector": "^2.3.0", + "fakerphp/faker": "^1.23.0", + "infection/infection": "~0.27.4", + "phpunit/phpunit": "^10.4.1", + "psalm/plugin-phpunit": "~0.18.4", + "rector/rector": "~0.18.5", + "symfony/filesystem": "^6.3.1", + "symfony/finder": "^6.3.5", + "vimeo/psalm": "^5.15.0" + }, + "suggest": { + "composer/semver": "If you want to use ComposerJsonNormalizer or VersionConstraintNormalizer" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Normalizer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides generic and vendor-specific normalizers for normalizing JSON documents.", + "homepage": "https://github.com/ergebnis/json-normalizer", + "keywords": [ + "json", + "normalizer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-normalizer/issues", + "security": "https://github.com/ergebnis/json-normalizer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-normalizer" + }, + "time": "2023-10-10T15:15:03+00:00" + }, + { + "name": "ergebnis/json-pointer", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-pointer.git", + "reference": "8e517faefc06b7c761eaa041febef51a9375819a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-pointer/zipball/8e517faefc06b7c761eaa041febef51a9375819a", + "reference": "8e517faefc06b7c761eaa041febef51a9375819a", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.29.0", + "ergebnis/data-provider": "^3.0.0", + "ergebnis/license": "^2.2.0", + "ergebnis/php-cs-fixer-config": "~6.7.0", + "ergebnis/phpunit-slow-test-detector": "^2.3.0", + "fakerphp/faker": "^1.23.0", + "infection/infection": "~0.27.4", + "phpunit/phpunit": "^10.4.1", + "psalm/plugin-phpunit": "~0.18.4", + "rector/rector": "~0.18.5", + "vimeo/psalm": "^5.15.0" + }, + "type": "library", + "extra": { + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Pointer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides JSON pointer as a value object.", + "homepage": "https://github.com/ergebnis/json-pointer", + "keywords": [ + "RFC6901", + "json", + "pointer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-pointer/issues", + "security": "https://github.com/ergebnis/json-pointer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-pointer" + }, + "time": "2023-10-10T14:41:06+00:00" + }, + { + "name": "ergebnis/json-printer", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-printer.git", + "reference": "05841593d72499de4f7ce4034a237c77e470558f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-printer/zipball/05841593d72499de4f7ce4034a237c77e470558f", + "reference": "05841593d72499de4f7ce4034a237c77e470558f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "ergebnis/license": "^2.2.0", + "ergebnis/php-cs-fixer-config": "^6.6.0", + "ergebnis/phpunit-slow-test-detector": "^2.3.0", + "fakerphp/faker": "^1.23.0", + "infection/infection": "~0.27.3", + "phpunit/phpunit": "^10.4.1", + "psalm/plugin-phpunit": "~0.18.4", + "rector/rector": "~0.18.5", + "vimeo/psalm": "^5.15.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Printer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a JSON printer, allowing for flexible indentation.", + "homepage": "https://github.com/ergebnis/json-printer", + "keywords": [ + "formatter", + "json", + "printer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-printer/issues", + "security": "https://github.com/ergebnis/json-printer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-printer" + }, + "time": "2023-10-10T07:42:48+00:00" + }, + { + "name": "ergebnis/json-schema-validator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-schema-validator.git", + "reference": "d568ed85d1cdc2e49d650c2fc234dc2516f3f25b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-schema-validator/zipball/d568ed85d1cdc2e49d650c2fc234dc2516f3f25b", + "reference": "d568ed85d1cdc2e49d650c2fc234dc2516f3f25b", + "shasum": "" + }, + "require": { + "ergebnis/json": "^1.0.1", + "ergebnis/json-pointer": "^3.2.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.21.0", + "ergebnis/data-provider": "^3.0.0", + "ergebnis/license": "^2.2.0", + "ergebnis/php-cs-fixer-config": "~6.6.0", + "ergebnis/phpunit-slow-test-detector": "^2.3.0", + "fakerphp/faker": "^1.23.0", + "infection/infection": "~0.27.4", + "phpunit/phpunit": "^10.4.1", + "psalm/plugin-phpunit": "~0.18.4", + "rector/rector": "~0.18.5", + "vimeo/psalm": "^5.15.0" + }, + "type": "library", + "extra": { + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\SchemaValidator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a JSON schema validator, building on top of justinrainbow/json-schema.", + "homepage": "https://github.com/ergebnis/json-schema-validator", + "keywords": [ + "json", + "schema", + "validator" + ], + "support": { + "issues": "https://github.com/ergebnis/json-schema-validator/issues", + "security": "https://github.com/ergebnis/json-schema-validator/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-schema-validator" + }, + "time": "2023-10-10T14:16:57+00:00" + }, + { + "name": "fidry/makefile", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/theofidry/makefile.git", + "reference": "be5a048dcc5648d04e924facce0f85b406d731e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/makefile/zipball/be5a048dcc5648d04e924facce0f85b406d731e4", + "reference": "be5a048dcc5648d04e924facce0f85b406d731e4", + "shasum": "" + }, + "require": { + "php": "^8.1", + "thecodingmachine/safe": "^2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "ergebnis/composer-normalize": "^2.28", + "infection/infection": "^0.26", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fidry\\Makefile\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Utility to parse a Makefile and implement some convention rules.", + "keywords": [ + "Makefile" + ], + "support": { + "issues": "https://github.com/theofidry/makefile/issues", + "source": "https://github.com/theofidry/makefile/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2023-10-13T23:49:14+00:00" + }, + { + "name": "localheinz/diff", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/localheinz/diff.git", + "reference": "851bb20ea8358c86f677f5f111c4ab031b1c764c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/localheinz/diff/zipball/851bb20ea8358c86f677f5f111c4ab031b1c764c", + "reference": "851bb20ea8358c86f677f5f111c4ab031b1c764c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Fork of sebastian/diff for use with ergebnis/composer-normalize", + "homepage": "https://github.com/localheinz/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "source": "https://github.com/localheinz/diff/tree/main" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-06T04:49:32+00:00" + }, + { + "name": "mikey179/vfsstream", + "version": "v1.6.11", + "source": { + "type": "git", + "url": "https://github.com/bovigo/vfsStream.git", + "reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f", + "reference": "17d16a85e6c26ce1f3e2fa9ceeacdc2855db1e9f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.5|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "support": { + "issues": "https://github.com/bovigo/vfsStream/issues", + "source": "https://github.com/bovigo/vfsStream/tree/master", + "wiki": "https://github.com/bovigo/vfsStream/wiki" + }, + "time": "2022-02-23T02:02:42+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "15873c65b207b07765dbc3c95d20fdf4a320cbe2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/15873c65b207b07765dbc3c95d20fdf4a320cbe2", + "reference": "15873c65b207b07765dbc3c95d20fdf4a320cbe2", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2 || ^2.0", + "php": "^7.2 || 8.0.* || 8.1.* || 8.2.*", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0 || ^7.0", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.17.0" + }, + "time": "2023-02-02T15:41:36+00:00" + }, + { + "name": "phpspec/prophecy-phpunit", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy-phpunit.git", + "reference": "9f26c224a2fa335f33e6666cc078fbf388255e87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy-phpunit/zipball/9f26c224a2fa335f33e6666cc078fbf388255e87", + "reference": "9f26c224a2fa335f33e6666cc078fbf388255e87", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8", + "phpspec/prophecy": "^1.3", + "phpunit/phpunit": "^9.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\PhpUnit\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + } + ], + "description": "Integrating the Prophecy mocking library in PHPUnit test cases", + "homepage": "http://phpspec.net", + "keywords": [ + "phpunit", + "prophecy" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy-phpunit/issues", + "source": "https://github.com/phpspec/prophecy-phpunit/tree/v2.0.2" + }, + "time": "2023-04-18T11:58:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.29", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.15", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-09-19T04:57:46+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.13", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", + "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.28", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2023-09-19T05:39:22+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:03:37+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bde739e7565280bda77be70044ac1047bc007e34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-02T09:26:13+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "symfony/phpunit-bridge", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/phpunit-bridge.git", + "reference": "45610900872a35b77db7698651f36129906041ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/45610900872a35b77db7698651f36129906041ea", + "reference": "45610900872a35b77db7698651f36129906041ea", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "conflict": { + "phpunit/phpunit": "<7.5|9.1.2" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/error-handler": "^5.4|^6.0", + "symfony/polyfill-php81": "^1.27" + }, + "bin": [ + "bin/simple-phpunit" + ], + "type": "symfony-bridge", + "extra": { + "thanks": { + "name": "phpunit/phpunit", + "url": "https://github.com/sebastianbergmann/phpunit" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Bridge\\PhpUnit\\": "" + }, + "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 utilities for PHPUnit, especially user deprecation notices management", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/phpunit-bridge/tree/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-10-31T08:07:48+00:00" + }, + { + "name": "symfony/yaml", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "3493af8a8dad7fa91c77fa473ba23ecd95334a92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/3493af8a8dad7fa91c77fa473ba23ecd95334a92", + "reference": "3493af8a8dad7fa91c77fa473ba23ecd95334a92", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "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": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-06T10:58:05+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + }, + { + "name": "webmozarts/strict-phpunit", + "version": "7.7.7", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/strict-phpunit.git", + "reference": "bec31848d596536af8119a37de61ba23e0599377" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/strict-phpunit/zipball/bec31848d596536af8119a37de61ba23e0599377", + "reference": "bec31848d596536af8119a37de61ba23e0599377", + "shasum": "" + }, + "require": { + "php": ">=7.4.0", + "phpunit/phpunit": "^9.4.3" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.28", + "infection/infection": "^0.26.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozarts\\StrictPHPUnit\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bernhard.schussek@webmozarts.com" + } + ], + "description": "Enables type-safe comparisons of objects in PHPUnit", + "support": { + "issues": "https://github.com/webmozarts/strict-phpunit/issues", + "source": "https://github.com/webmozarts/strict-phpunit/tree/7.7.7" + }, + "time": "2023-03-29T20:04:11+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2", + "ext-iconv": "*", + "ext-mbstring": "*", + "ext-phar": "*", + "composer-plugin-api": "^2.2" + }, + "platform-dev": [], + "platform-overrides": { + "php": "8.2" + }, + "plugin-api-version": "2.6.0" +} diff --git a/fixtures/bench/without-compactors/src/Amp/FailureCollector.php b/fixtures/bench/without-compactors/src/Amp/FailureCollector.php new file mode 100644 index 000000000..ba8f3efd1 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Amp/FailureCollector.php @@ -0,0 +1,39 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Amp; + +use Amp\MultiReasonException; +use KevinGH\Box\NotInstantiable; +use Throwable; +use function array_map; +use function array_unique; + +final class FailureCollector +{ + use NotInstantiable; + + /** + * @return list + */ + public static function collectReasons(MultiReasonException $exception): array + { + return array_unique( + array_map( + static fn (Throwable $throwable) => $throwable->getMessage(), + $exception->getReasons(), + ), + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Annotation/CompactedFormatter.php b/fixtures/bench/without-compactors/src/Annotation/CompactedFormatter.php new file mode 100644 index 000000000..3aeef0cb4 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Annotation/CompactedFormatter.php @@ -0,0 +1,42 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Annotation; + +use phpDocumentor\Reflection\DocBlock\Tag; +use phpDocumentor\Reflection\DocBlock\Tags\Formatter; +use phpDocumentor\Reflection\DocBlock\Tags\Generic; +use function array_map; +use function explode; +use function implode; + +final class CompactedFormatter implements Formatter +{ + public function format(Tag $tag): string + { + if (!$tag instanceof Generic) { + return trim('@'.$tag->getName()); + } + + $description = (string) $tag; + + if (!str_starts_with($description, '(')) { + return trim('@'.$tag->getName()); + } + + $description = implode('', array_map('trim', explode("\n", (string) $tag))); + + return trim('@'.$tag->getName().$description); + } +} diff --git a/fixtures/bench/without-compactors/src/Annotation/DocblockAnnotationParser.php b/fixtures/bench/without-compactors/src/Annotation/DocblockAnnotationParser.php new file mode 100644 index 000000000..b53fb1cce --- /dev/null +++ b/fixtures/bench/without-compactors/src/Annotation/DocblockAnnotationParser.php @@ -0,0 +1,94 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Annotation; + +use InvalidArgumentException; +use phpDocumentor\Reflection\DocBlock; +use phpDocumentor\Reflection\DocBlock\Tag; +use phpDocumentor\Reflection\DocBlock\Tags\Formatter; +use phpDocumentor\Reflection\DocBlockFactoryInterface; +use function array_filter; +use function array_flip; +use function array_key_exists; +use function array_map; +use function array_values; + +/** + * @private + */ +final class DocblockAnnotationParser +{ + /** + * @var array + */ + private array $ignoredAnnotationsAsKeys; + + /** + * @param string[] $ignoredAnnotations + */ + public function __construct( + private DocBlockFactoryInterface $factory, + private Formatter $tagsFormatter, + array $ignoredAnnotations, + ) { + $this->ignoredAnnotationsAsKeys = array_flip($ignoredAnnotations); + } + + /** + * @return string[] Parsed compacted annotations parsed from the docblock + */ + public function parse(string $docblock): array + { + $doc = $this->createDocBlock($docblock); + + $tags = self::extractTags($doc, $this->ignoredAnnotationsAsKeys); + + return array_map( + fn (Tag $tag) => $tag->render($this->tagsFormatter), + $tags, + ); + } + + private function createDocBlock(string $docblock): DocBlock + { + try { + return $this->factory->create($docblock); + } catch (InvalidArgumentException $invalidDocBlock) { + throw new MalformedTagException( + 'The annotations could not be parsed.', + 0, + $invalidDocBlock, + ); + } + } + + /** + * @param array $ignoredAnnotations + * + * @return list + */ + private static function extractTags(DocBlock $docBlock, array $ignoredAnnotations): array + { + return array_values( + array_filter( + $docBlock->getTags(), + static fn (Tag $tag) => !array_key_exists( + mb_strtolower($tag->getName()), + $ignoredAnnotations, + ), + ), + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Annotation/MalformedTagException.php b/fixtures/bench/without-compactors/src/Annotation/MalformedTagException.php new file mode 100644 index 000000000..29f0898c7 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Annotation/MalformedTagException.php @@ -0,0 +1,24 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Annotation; + +use UnexpectedValueException; + +/** + * @private + */ +final class MalformedTagException extends UnexpectedValueException +{ +} diff --git a/fixtures/bench/without-compactors/src/Box.php b/fixtures/bench/without-compactors/src/Box.php new file mode 100644 index 000000000..39d3fb747 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Box.php @@ -0,0 +1,524 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Amp\MultiReasonException; +use BadMethodCallException; +use Countable; +use DateTimeImmutable; +use Fidry\FileSystem\FS; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\Compactor\Compactors; +use KevinGH\Box\Compactor\PhpScoper; +use KevinGH\Box\Compactor\Placeholder; +use KevinGH\Box\Phar\CompressionAlgorithm; +use KevinGH\Box\Phar\SigningAlgorithm; +use KevinGH\Box\PhpScoper\NullScoper; +use KevinGH\Box\PhpScoper\Scoper; +use Phar; +use RecursiveDirectoryIterator; +use RuntimeException; +use Seld\PharUtils\Timestamps; +use SplFileInfo; +use Webmozart\Assert\Assert; +use function Amp\ParallelFunctions\parallelMap; +use function Amp\Promise\wait; +use function array_filter; +use function array_map; +use function array_unshift; +use function chdir; +use function dirname; +use function extension_loaded; +use function file_exists; +use function getcwd; +use function is_object; +use function openssl_pkey_export; +use function openssl_pkey_get_details; +use function openssl_pkey_get_private; +use function sprintf; + +/** + * Box is a utility class to generate a PHAR. + * + * @private + */ +final class Box implements Countable +{ + private Compactors $compactors; + private Placeholder $placeholderCompactor; + private MapFile $mapFile; + private Scoper $scoper; + private bool $buffering = false; + + /** + * @var array Relative file path as key and file contents as value + */ + private array $bufferedFiles = []; + + private function __construct( + private Phar $phar, + private readonly string $pharFilePath, + private readonly bool $enableParallelization, + ) { + $this->compactors = new Compactors(); + $this->placeholderCompactor = new Placeholder([]); + $this->mapFile = new MapFile(getcwd(), []); + $this->scoper = new NullScoper(); + } + + /** + * Creates a new PHAR and Box instance. + * + * @param string $pharFilePath The PHAR file name + * @param int $pharFlags Flags to pass to the Phar parent class RecursiveDirectoryIterator + * @param string $pharAlias Alias with which the Phar archive should be referred to in calls to stream functionality + * + * @see RecursiveDirectoryIterator + */ + public static function create( + string $pharFilePath, + int $pharFlags = 0, + ?string $pharAlias = null, + bool $enableParallelization = false, + ): self { + // Ensure the parent directory of the PHAR file exists as `new \Phar()` does not create it and would fail + // otherwise. + FS::mkdir(dirname($pharFilePath)); + + return new self( + new Phar($pharFilePath, $pharFlags, $pharAlias), + $pharFilePath, + $enableParallelization, + ); + } + + public function startBuffering(): void + { + Assert::false($this->buffering, 'The buffering must be ended before starting it again'); + + $this->buffering = true; + + $this->phar->startBuffering(); + } + + /** + * @param callable(SymbolsRegistry, string): void $dumpAutoload + */ + public function endBuffering(?callable $dumpAutoload): void + { + Assert::true($this->buffering, 'The buffering must be started before ending it'); + + $dumpAutoload ??= static fn () => null; + $cwd = getcwd(); + + $tmp = FS::makeTmpDir('box', self::class); + chdir($tmp); + + if ([] === $this->bufferedFiles) { + $this->bufferedFiles = [ + '.box_empty' => 'A PHAR cannot be empty so Box adds this file to ensure the PHAR is created still.', + ]; + } + + try { + foreach ($this->bufferedFiles as $file => $contents) { + FS::dumpFile($file, $contents); + } + + if (null !== $dumpAutoload) { + $dumpAutoload( + $this->scoper->getSymbolsRegistry(), + $this->scoper->getPrefix(), + $this->scoper->getExcludedFilePaths(), + ); + } + + chdir($cwd); + + $this->phar->buildFromDirectory($tmp); + } finally { + FS::remove($tmp); + } + + $this->buffering = false; + + $this->phar->stopBuffering(); + } + + /** + * @param non-empty-string $normalizedVendorDir Normalized path ("/" path separator and no trailing "/") to the Composer vendor directory + */ + public function removeComposerArtefacts(string $normalizedVendorDir): void + { + Assert::false($this->buffering, 'The buffering must have ended before removing the Composer artefacts'); + + $composerFiles = [ + 'composer.json', + 'composer.lock', + $normalizedVendorDir.'/composer/installed.json', + ]; + + $this->phar->startBuffering(); + + foreach ($composerFiles as $composerFile) { + $localComposerFile = ($this->mapFile)($composerFile); + + $pharFilePath = sprintf( + 'phar://%s/%s', + $this->phar->getPath(), + $localComposerFile, + ); + + if (file_exists($pharFilePath)) { + $this->phar->delete($localComposerFile); + } + } + + $this->phar->stopBuffering(); + } + + public function compress(CompressionAlgorithm $compressionAlgorithm): ?string + { + Assert::false($this->buffering, 'Cannot compress files while buffering.'); + + $extensionRequired = $compressionAlgorithm->getRequiredExtension(); + + if (null !== $extensionRequired && false === extension_loaded($extensionRequired)) { + throw new RuntimeException( + sprintf( + 'Cannot compress the PHAR with the compression algorithm "%s": the extension "%s" is required but appear to not be loaded', + $compressionAlgorithm->name, + $extensionRequired, + ), + ); + } + + try { + if (CompressionAlgorithm::NONE === $compressionAlgorithm) { + $this->phar->decompressFiles(); + } else { + $this->phar->compressFiles($compressionAlgorithm->value); + } + } catch (BadMethodCallException $exception) { + $exceptionMessage = 'unable to create temporary file' !== $exception->getMessage() + ? 'Could not compress the PHAR: '.$exception->getMessage() + : sprintf( + 'Could not compress the PHAR: the compression requires too many file descriptors to be opened (%s). Check your system limits or install the posix extension to allow Box to automatically configure it during the compression', + $this->phar->count(), + ); + + throw new RuntimeException($exceptionMessage, $exception->getCode(), $exception); + } + + return $extensionRequired; + } + + public function registerCompactors(Compactors $compactors): void + { + $compactorsArray = $compactors->toArray(); + + foreach ($compactorsArray as $index => $compactor) { + if ($compactor instanceof PhpScoper) { + $this->scoper = $compactor->getScoper(); + + continue; + } + + if ($compactor instanceof Placeholder) { + // Removes the known Placeholder compactors in favour of the Box one + unset($compactorsArray[$index]); + } + } + + array_unshift($compactorsArray, $this->placeholderCompactor); + + $this->compactors = new Compactors(...$compactorsArray); + } + + /** + * @param scalar[] $placeholders + */ + public function registerPlaceholders(array $placeholders): void + { + $message = 'Expected value "%s" to be a scalar or stringable object.'; + + foreach ($placeholders as $index => $placeholder) { + if (is_object($placeholder)) { + Assert::methodExists($placeholder, '__toString', $message); + + $placeholders[$index] = (string) $placeholder; + + break; + } + + Assert::scalar($placeholder, $message); + } + + $this->placeholderCompactor = new Placeholder($placeholders); + + $this->registerCompactors($this->compactors); + } + + public function registerFileMapping(MapFile $fileMapper): void + { + $this->mapFile = $fileMapper; + } + + public function registerStub(string $file): void + { + $contents = $this->placeholderCompactor->compact( + $file, + FS::getFileContents($file), + ); + + $this->phar->setStub($contents); + } + + /** + * @param array $files + * + * @throws MultiReasonException + */ + public function addFiles(array $files, bool $binary): void + { + Assert::true($this->buffering, 'Cannot add files if the buffering has not started.'); + + $files = array_map('strval', $files); + + if ($binary) { + foreach ($files as $file) { + $this->addFile($file, null, true); + } + + return; + } + + foreach ($this->processContents($files) as [$file, $contents]) { + $this->bufferedFiles[$file] = $contents; + } + } + + /** + * Adds the a file to the PHAR. The contents will first be compacted and have its placeholders + * replaced. + * + * @param null|string $contents If null the content of the file will be used + * @param bool $binary When true means the file content shouldn't be processed + * + * @return string File local path + */ + public function addFile(string $file, ?string $contents = null, bool $binary = false): string + { + Assert::true($this->buffering, 'Cannot add files if the buffering has not started.'); + + if (null === $contents) { + $contents = FS::getFileContents($file); + } + + $local = ($this->mapFile)($file); + + $this->bufferedFiles[$local] = $binary ? $contents : $this->compactors->compact($local, $contents); + + return $local; + } + + /** + * @internal + */ + public function getPhar(): Phar + { + return $this->phar; + } + + public function setAlias(string $alias): void + { + $aliasWasAdded = $this->phar->setAlias($alias); + + Assert::true( + $aliasWasAdded, + sprintf( + 'The alias "%s" is invalid. See Phar::setAlias() documentation for more information.', + $alias, + ), + ); + } + + public function setStub(string $stub): void + { + $this->phar->setStub($stub); + } + + public function setDefaultStub(string $main): void + { + $this->phar->setDefaultStub($main); + } + + public function setMetadata(mixed $metadata): void + { + $this->phar->setMetadata($metadata); + } + + public function extractTo(string $directory, bool $overwrite = false): void + { + $this->phar->extractTo($directory, overwrite: $overwrite); + } + + public function sign( + SigningAlgorithm $signingAlgorithm, + ?DateTimeImmutable $timestamp = null, + ): void { + if (null === $timestamp) { + $this->phar->setSignatureAlgorithm($signingAlgorithm->value); + + return; + } + + $phar = $this->phar; + $phar->__destruct(); + unset($this->phar); + + $util = new Timestamps($this->pharFilePath); + $util->updateTimestamps($timestamp); + $util->save( + $this->pharFilePath, + $signingAlgorithm->value, + ); + + $this->phar = new Phar($this->pharFilePath); + } + + /** + * Signs the PHAR using a private key file. + * + * @param string $file the private key file name + * @param null|string $password the private key password + */ + public function signUsingFile(string $file, ?string $password = null): void + { + $this->signUsingKey(FS::getFileContents($file), $password); + } + + /** + * Signs the PHAR using a private key. + * + * @param string $key The private key + * @param null|string $password The private key password + */ + public function signUsingKey(string $key, ?string $password): void + { + $pubKey = $this->pharFilePath.'.pubkey'; + + Assert::writable(dirname($pubKey)); + Assert::true(extension_loaded('openssl')); + + if (file_exists($pubKey)) { + Assert::file( + $pubKey, + 'Cannot create public key: %s already exists and is not a file.', + ); + } + + $resource = openssl_pkey_get_private($key, (string) $password); + + Assert::notSame(false, $resource, 'Could not retrieve the private key, check that the password is correct.'); + + openssl_pkey_export($resource, $private); + + $details = openssl_pkey_get_details($resource); + + $this->phar->setSignatureAlgorithm(Phar::OPENSSL, $private); + + FS::dumpFile($pubKey, $details['key']); + } + + /** + * @param string[] $files + * + * @throws MultiReasonException + * + * @return array array of tuples where the first element is the local file path (path inside the PHAR) and the + * second element is the processed contents + */ + private function processContents(array $files): array + { + $mapFile = $this->mapFile; + $compactors = $this->compactors; + $cwd = getcwd(); + $enableParallelization = $this->enableParallelization; + + $processFile = static function (string $file) use ($cwd, $mapFile, $compactors, $enableParallelization): array { + chdir($cwd); + + // Keep the fully qualified call here since this function may be executed without the right autoloading + // mechanism + \KevinGH\Box\register_aliases(); + if ($enableParallelization) { + \KevinGH\Box\register_error_handler(); + } + + $contents = \Fidry\FileSystem\FS::getFileContents($file); + + $local = $mapFile($file); + + $processedContents = $compactors->compact($local, $contents); + + return [$local, $processedContents, $compactors->getScoperSymbolsRegistry()]; + }; + + if ($this->scoper instanceof NullScoper || !$enableParallelization) { + return array_map($processFile, $files); + } + + // In the case of parallel processing, an issue is caused due to the statefulness nature of the PhpScoper + // symbols registry. + // + // Indeed, the PhpScoper symbols registry stores the records of exposed/excluded classes and functions. If nothing is done, + // then the symbols registry retrieved in the end will here will be "blank" since the updated symbols registries are the ones + // from the workers used for the parallel processing. + // + // In order to avoid that, the symbols registries will be returned as a result as well in order to be able to merge + // all the symbols registries into one. + // + // This process is allowed thanks to the nature of the state of the symbols registries: having redundant classes or + // functions registered can easily be deal with so merging all those different states is actually + // straightforward. + $tuples = wait(parallelMap($files, $processFile)); + + if ([] === $tuples) { + return []; + } + + $filesWithContents = []; + $symbolRegistries = []; + + foreach ($tuples as [$local, $processedContents, $symbolRegistry]) { + $filesWithContents[] = [$local, $processedContents]; + $symbolRegistries[] = $symbolRegistry; + } + + $this->compactors->registerSymbolsRegistry( + SymbolsRegistry::createFromRegistries(array_filter($symbolRegistries)), + ); + + return $filesWithContents; + } + + public function count(): int + { + Assert::false($this->buffering, 'Cannot count the number of files in the PHAR when buffering'); + + return $this->phar->count(); + } +} diff --git a/fixtures/bench/without-compactors/src/Compactor/BaseCompactor.php b/fixtures/bench/without-compactors/src/Compactor/BaseCompactor.php new file mode 100644 index 000000000..74ab1e476 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Compactor/BaseCompactor.php @@ -0,0 +1,36 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +/** + * Base compactor class providing a slightly simpler API to compact the content only if the file is supported. + * + * @private + */ +abstract class BaseCompactor implements Compactor +{ + public function compact(string $file, string $contents): string + { + if ($this->supports($file)) { + return $this->compactContent($contents); + } + + return $contents; + } + + abstract protected function compactContent(string $contents): string; + + abstract protected function supports(string $file): bool; +} diff --git a/fixtures/bench/without-compactors/src/Compactor/Compactor.php b/fixtures/bench/without-compactors/src/Compactor/Compactor.php new file mode 100644 index 000000000..48f475aa5 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Compactor/Compactor.php @@ -0,0 +1,34 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +/** + * A compactor is a class called to process a file contents before adding it to the PHAR. This make it possible to for + * example strip down the file from useless phpdoc. + * + * @private + */ +interface Compactor +{ + /** + * Compacts the file contents. + * + * @param string $file The file name + * @param string $contents The file contents + * + * @return string The compacted contents + */ + public function compact(string $file, string $contents): string; +} diff --git a/fixtures/bench/without-compactors/src/Compactor/Compactors.php b/fixtures/bench/without-compactors/src/Compactor/Compactors.php new file mode 100644 index 000000000..f96bc2e60 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Compactor/Compactors.php @@ -0,0 +1,86 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +use Countable; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\PhpScoper\Scoper; +use function array_reduce; +use function count; + +/** + * @private + */ +final class Compactors implements Compactor, Countable +{ + /** + * @var Compactor[] + */ + private array $compactors; + + private ?PhpScoper $scoperCompactor = null; + + public function __construct(Compactor ...$compactors) + { + $this->compactors = $compactors; + + foreach ($compactors as $compactor) { + if ($compactor instanceof PhpScoper) { + $this->scoperCompactor = $compactor; + + // We do not expect more than one Scoper Compactor. If there is more than + // one then the latter is ignored. + break; + } + } + } + + public function compact(string $file, string $contents): string + { + return array_reduce( + $this->compactors, + static fn (string $contents, Compactor $compactor): string => $compactor->compact($file, $contents), + $contents, + ); + } + + public function getScoper(): ?Scoper + { + return $this->scoperCompactor?->getScoper(); + } + + public function getScoperSymbolsRegistry(): ?SymbolsRegistry + { + return $this->scoperCompactor?->getScoper()->getSymbolsRegistry(); + } + + public function registerSymbolsRegistry(SymbolsRegistry $symbolsRegistry): void + { + $this->scoperCompactor?->getScoper()->changeSymbolsRegistry($symbolsRegistry); + } + + /** + * @return Compactor[] + */ + public function toArray(): array + { + return $this->compactors; + } + + public function count(): int + { + return count($this->compactors); + } +} diff --git a/fixtures/bench/without-compactors/src/Compactor/FileExtensionCompactor.php b/fixtures/bench/without-compactors/src/Compactor/FileExtensionCompactor.php new file mode 100644 index 000000000..4c62dbb8d --- /dev/null +++ b/fixtures/bench/without-compactors/src/Compactor/FileExtensionCompactor.php @@ -0,0 +1,55 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +use Webmozart\Assert\Assert; +use function in_array; +use function pathinfo; +use const PATHINFO_EXTENSION; + +/** + * An abstract compactor class that handles matching supported file by their types. + * + * @private + */ +abstract class FileExtensionCompactor extends BaseCompactor +{ + /** + * @var string[] + */ + private array $extensions; + + /** + * @param string[] $extensions the list of supported file extensions + */ + public function __construct(array $extensions) + { + Assert::allString($extensions); + + $this->extensions = $extensions; + } + + protected function supports(string $file): bool + { + return in_array( + pathinfo( + $file, + PATHINFO_EXTENSION, + ), + $this->extensions, + true, + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Compactor/Json.php b/fixtures/bench/without-compactors/src/Compactor/Json.php new file mode 100644 index 000000000..600594c69 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Compactor/Json.php @@ -0,0 +1,45 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +use function json_decode; +use function json_encode; +use function json_last_error; +use const JSON_ERROR_NONE; +use const JSON_THROW_ON_ERROR; + +/** + * Compacts JSON files by re-encoding without pretty print. + * + * @private + */ +final class Json extends FileExtensionCompactor +{ + public function __construct(array $extensions = ['json', 'lock']) + { + parent::__construct($extensions); + } + + protected function compactContent(string $contents): string + { + $decodedContents = json_decode($contents, false); + + if (JSON_ERROR_NONE !== json_last_error()) { + return $contents; + } + + return json_encode($decodedContents, JSON_THROW_ON_ERROR); + } +} diff --git a/fixtures/bench/without-compactors/src/Compactor/NullCompactor.php b/fixtures/bench/without-compactors/src/Compactor/NullCompactor.php new file mode 100644 index 000000000..a02a7c334 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Compactor/NullCompactor.php @@ -0,0 +1,26 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +/** + * @private + */ +final class NullCompactor implements Compactor +{ + public function compact(string $file, string $contents): string + { + return $contents; + } +} diff --git a/fixtures/bench/without-compactors/src/Compactor/Php.php b/fixtures/bench/without-compactors/src/Compactor/Php.php new file mode 100644 index 000000000..6f0d1e077 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Compactor/Php.php @@ -0,0 +1,264 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +use KevinGH\Box\Annotation\CompactedFormatter; +use KevinGH\Box\Annotation\DocblockAnnotationParser; +use phpDocumentor\Reflection\DocBlockFactory; +use PhpToken; +use RuntimeException; +use Webmozart\Assert\Assert; +use function array_pop; +use function array_slice; +use function array_splice; +use function count; +use function is_int; +use function ltrim; +use function preg_replace; +use function str_repeat; +use const T_COMMENT; +use const T_DOC_COMMENT; +use const T_WHITESPACE; + +/** + * A PHP source code compactor copied from Composer. + * + * @see https://github.com/composer/composer/blob/a8df30c09be550bffc37ba540fb7c7f0383c3944/src/Composer/Compiler.php#L214 + * + * @author Kevin Herrera + * @author Fabien Potencier + * @author Jordi Boggiano + * @author Théo Fidry + * @author Juliette Reinders Folmer + * @author Alessandro Chitolina + * + * @private + */ +final class Php extends FileExtensionCompactor +{ + /** + * @param list $ignoredAnnotations + */ + public static function create(array $ignoredAnnotations): self + { + return new self( + new DocblockAnnotationParser( + DocBlockFactory::createInstance(), + new CompactedFormatter(), + $ignoredAnnotations, + ), + ); + } + + public function __construct( + private ?DocblockAnnotationParser $annotationParser, + array $extensions = ['php'], + ) { + parent::__construct($extensions); + } + + protected function compactContent(string $contents): string + { + $output = ''; + $tokens = PhpToken::tokenize($contents); + $tokenCount = count($tokens); + + for ($index = 0; $index < $tokenCount; ++$index) { + $token = $tokens[$index]; + $tokenText = $token->text; + + if ($token->is([T_COMMENT, T_DOC_COMMENT])) { + if (str_starts_with($tokenText, '#[')) { + // This is, in all likelihood, the start of a PHP >= 8.0 attribute. + // Note: $tokens may be updated by reference as well! + $retokenized = $this->retokenizeAttribute($tokens, $index); + + if (null !== $retokenized) { + array_splice($tokens, $index, 1, $retokenized); + $tokenCount = count($tokens); + } + + $attributeCloser = self::findAttributeCloser($tokens, $index); + + if (is_int($attributeCloser)) { + $output .= '#['; + } else { + // Turns out this was not an attribute. Treat it as a plain comment. + $output .= str_repeat("\n", mb_substr_count($tokenText, "\n")); + } + } elseif (str_contains($tokenText, '@')) { + try { + $output .= $this->compactAnnotations($tokenText); + } catch (RuntimeException) { + $output .= $tokenText; + } + } else { + $output .= str_repeat("\n", mb_substr_count($tokenText, "\n")); + } + } elseif ($token->is(T_WHITESPACE)) { + $whitespace = $tokenText; + $previousIndex = ($index - 1); + + // Handle whitespace potentially being split into two tokens after attribute retokenization. + $nextToken = $tokens[$index + 1] ?? null; + + if (null !== $nextToken + && $nextToken->is(T_WHITESPACE) + ) { + $whitespace .= $nextToken->text; + ++$index; + } + + // reduce wide spaces + $whitespace = preg_replace('{[ \t]+}', ' ', $whitespace); + + // normalize newlines to \n + $whitespace = preg_replace('{(?:\r\n|\r|\n)}', "\n", $whitespace); + + // If the new line was split off from the whitespace token due to it being included in + // the previous (comment) token (PHP < 8), remove leading spaces. + + $previousToken = $tokens[$previousIndex]; + + if ($previousToken->is(T_COMMENT) + && str_contains($previousToken->text, "\n") + ) { + $whitespace = ltrim($whitespace, ' '); + } + + // trim leading spaces + $whitespace = preg_replace('{\n +}', "\n", $whitespace); + + $output .= $whitespace; + } else { + $output .= $tokenText; + } + } + + return $output; + } + + private function compactAnnotations(string $docblock): string + { + if (null === $this->annotationParser) { + return $docblock; + } + + $breaksNbr = mb_substr_count($docblock, "\n"); + + $annotations = $this->annotationParser->parse($docblock); + + if ([] === $annotations) { + return str_repeat("\n", $breaksNbr); + } + + $compactedDocblock = '/**'; + + foreach ($annotations as $annotation) { + $compactedDocblock .= "\n".$annotation; + } + + $breaksNbr -= count($annotations); + + if ($breaksNbr > 0) { + $compactedDocblock .= str_repeat("\n", $breaksNbr - 1); + $compactedDocblock .= "\n*/"; + } else { + // A space is required here to avoid having /***/ + $compactedDocblock .= ' */'; + } + + return $compactedDocblock; + } + + /** + * @param list $tokens + */ + private static function findAttributeCloser(array $tokens, int $opener): ?int + { + $tokenCount = count($tokens); + $brackets = [$opener]; + $closer = null; + + for ($i = ($opener + 1); $i < $tokenCount; ++$i) { + $tokenText = $tokens[$i]->text; + + // Allow for short arrays within attributes. + if ('[' === $tokenText) { + $brackets[] = $i; + + continue; + } + + if (']' === $tokenText) { + array_pop($brackets); + + if (0 === count($brackets)) { + $closer = $i; + break; + } + } + } + + return $closer; + } + + /** + * @param non-empty-list $tokens + */ + private function retokenizeAttribute(array &$tokens, int $opener): ?array + { + Assert::keyExists($tokens, $opener); + + $token = $tokens[$opener]; + $attributeBody = mb_substr($token->text, 2); + $subTokens = PhpToken::tokenize('text; + } + + $subTokens = PhpToken::tokenize(' + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +use KevinGH\Box\PhpScoper\Scoper; +use Throwable; + +/** + * @private + */ +final class PhpScoper implements Compactor +{ + public function __construct(private Scoper $scoper) + { + } + + public function compact(string $file, string $contents): string + { + try { + return $this->scoper->scope($file, $contents); + } catch (Throwable) { + return $contents; + } + } + + public function getScoper(): Scoper + { + return $this->scoper; + } +} diff --git a/fixtures/bench/without-compactors/src/Compactor/Placeholder.php b/fixtures/bench/without-compactors/src/Compactor/Placeholder.php new file mode 100644 index 000000000..66f66835e --- /dev/null +++ b/fixtures/bench/without-compactors/src/Compactor/Placeholder.php @@ -0,0 +1,46 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Compactor; + +use Webmozart\Assert\Assert; +use function array_keys; +use function str_replace; + +final class Placeholder implements Compactor +{ + /** + * @var scalar[] + */ + private array $placeholders; + + /** + * @param scalar[] $placeholders + */ + public function __construct(array $placeholders) + { + Assert::allScalar($placeholders); + + $this->placeholders = $placeholders; + } + + public function compact(string $file, string $contents): string + { + return str_replace( + array_keys($this->placeholders), + $this->placeholders, + $contents, + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Composer/AutoloadDumper.php b/fixtures/bench/without-compactors/src/Composer/AutoloadDumper.php new file mode 100644 index 000000000..6a4133c88 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Composer/AutoloadDumper.php @@ -0,0 +1,116 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use Humbug\PhpScoper\Autoload\ScoperAutoloadGenerator; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\NotInstantiable; +use UnexpectedValueException; +use function array_map; +use function explode; +use function implode; +use function preg_match; +use function preg_replace; +use function str_replace; +use const PHP_EOL; + +final class AutoloadDumper +{ + use NotInstantiable; + + public static function generateAutoloadStatements( + SymbolsRegistry $symbolsRegistry, + array $excludedComposerAutoloadFileHashes, + string $autoloadContents, + ): string { + if (0 === $symbolsRegistry->count()) { + return $autoloadContents; + } + + $autoloadContents = self::extractInlinedAutoloadContents($autoloadContents); + $scoperStatements = self::getOriginalScoperAutoloaderContents( + $symbolsRegistry, + $excludedComposerAutoloadFileHashes, + ); + + $indentedAutoloadContents = self::fixInlinedAutoloadIndent( + $autoloadContents, + self::getLoaderStatementIndent($scoperStatements), + ); + + $mergedAutoloadContents = preg_replace( + '/(\s*\\$loader \= .*autoload\.php.*)/', + $indentedAutoloadContents, + $scoperStatements, + ); + + return self::cleanupDuplicateLineReturns($mergedAutoloadContents); + } + + private static function extractInlinedAutoloadContents(string $autoloadContents): string + { + $autoloadContents = str_replace('dump(); + + return preg_replace( + '/scoper\-autoload\.php \@generated by PhpScoper/', + '@generated by Humbug Box', + $scoperStatements, + ); + } + + private static function getLoaderStatementIndent(string $scoperStatements): string + { + if (1 !== preg_match('/(? *)\\$loader \= .*autoload\.php.*/', $scoperStatements, $matches)) { + throw new UnexpectedValueException('Could not process the scoper autoloader statements'); + } + + return $matches['indent']; + } + + private static function fixInlinedAutoloadIndent(string $autoloadContents, string $indent): string + { + $lines = explode(PHP_EOL, $autoloadContents); + + $indentedLines = array_map( + static fn (string $line) => '' === $line ? $line : $indent.$line, + $lines, + ); + + return implode(PHP_EOL, $indentedLines); + } + + private static function cleanupDuplicateLineReturns(string $value): string + { + return preg_replace( + '/\n{2,}/m', + PHP_EOL.PHP_EOL, + $value, + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Composer/CompilerPsrLogger.php b/fixtures/bench/without-compactors/src/Composer/CompilerPsrLogger.php new file mode 100644 index 000000000..fdd52fdca --- /dev/null +++ b/fixtures/bench/without-compactors/src/Composer/CompilerPsrLogger.php @@ -0,0 +1,71 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use KevinGH\Box\Console\Logger\CompilerLogger; +use Psr\Log\AbstractLogger; +use Psr\Log\LogLevel; +use Stringable; +use Symfony\Component\Console\Output\OutputInterface; +use function array_key_exists; + +final class CompilerPsrLogger extends AbstractLogger +{ + public function __construct( + private CompilerLogger $decoratedLogger, + ) { + } + + public function log($level, Stringable|string $message, array $context = []): void + { + $verbosity = self::getVerbosity($level); + $output = self::getOutput($context); + + if (null === $output) { + $this->decoratedLogger->log( + CompilerLogger::CHEVRON_PREFIX, + $message, + $verbosity, + ); + } else { + $this->decoratedLogger->getIO()->writeln( + $output, + $verbosity, + ); + } + } + + private static function getVerbosity(string $level): int + { + return match ($level) { + LogLevel::INFO => OutputInterface::VERBOSITY_VERBOSE, + LogLevel::DEBUG => OutputInterface::VERBOSITY_VERY_VERBOSE, + default => OutputInterface::OUTPUT_NORMAL, + }; + } + + private static function getOutput(array $context): ?string + { + $knownKeys = ['stdout', 'stderr']; + + foreach ($knownKeys as $knownKey) { + if (array_key_exists($knownKey, $context)) { + return $context[$knownKey]; + } + } + + return null; + } +} diff --git a/fixtures/bench/without-compactors/src/Composer/ComposerConfiguration.php b/fixtures/bench/without-compactors/src/Composer/ComposerConfiguration.php new file mode 100644 index 000000000..0529f6ee9 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Composer/ComposerConfiguration.php @@ -0,0 +1,107 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use Symfony\Component\Filesystem\Path; +use function array_column; +use function array_filter; +use function array_key_exists; +use function array_map; +use function realpath; +use const DIRECTORY_SEPARATOR; + +/** + * @private + */ +final class ComposerConfiguration +{ + /** + * Attempts to locate the `composer.json` and `composer.lock` files in the provided base-path in order to collect + * all the dev packages. + * + * @return string[] Dev package paths + */ + public static function retrieveDevPackages( + string $basePath, + ?array $composerJsonDecodedContents, + ?array $composerLockDecodedContents, + bool $excludeDevPackages, + ): array { + if (null === $composerJsonDecodedContents + || null === $composerLockDecodedContents + || false === $excludeDevPackages + ) { + return []; + } + + return self::getDevPackagePaths( + $basePath, + $composerJsonDecodedContents, + $composerLockDecodedContents, + ); + } + + /** + * @return string[] Dev packages paths + */ + private static function getDevPackagePaths( + string $basePath, + array $composerJsonDecodedContents, + array $composerLockDecodedContents, + ): array { + $vendorDir = Path::makeAbsolute( + self::retrieveVendorDir($composerJsonDecodedContents), + $basePath, + ); + + $packageNames = self::retrieveDevPackageNames($composerLockDecodedContents); + + return array_filter( + array_map( + static function (string $packageName) use ($vendorDir): ?string { + $realPath = realpath($vendorDir.DIRECTORY_SEPARATOR.$packageName); + + return false !== $realPath ? $realPath : null; + }, + $packageNames, + ), + ); + } + + public static function retrieveVendorDir(array $composerJsonDecodedContents): string + { + if (false === array_key_exists('config', $composerJsonDecodedContents)) { + return 'vendor'; + } + + if (false === array_key_exists('vendor-dir', $composerJsonDecodedContents['config'])) { + return 'vendor'; + } + + return $composerJsonDecodedContents['config']['vendor-dir']; + } + + /** + * @return string[] Names of the dev packages + */ + private static function retrieveDevPackageNames(array $composerLockDecodedContents): array + { + if (false === array_key_exists('packages-dev', $composerLockDecodedContents)) { + return []; + } + + return array_column($composerLockDecodedContents['packages-dev'], 'name'); + } +} diff --git a/fixtures/bench/without-compactors/src/Composer/ComposerFile.php b/fixtures/bench/without-compactors/src/Composer/ComposerFile.php new file mode 100644 index 000000000..1d362361b --- /dev/null +++ b/fixtures/bench/without-compactors/src/Composer/ComposerFile.php @@ -0,0 +1,50 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use Webmozart\Assert\Assert; + +final class ComposerFile +{ + private ?string $path; + private array $contents; + + public static function createEmpty(): self + { + return new self(null, []); + } + + public function __construct(?string $path, array $contents) + { + Assert::nullOrNotEmpty($path); + + if (null === $path) { + Assert::same([], $contents); + } + + $this->path = $path; + $this->contents = $contents; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function getDecodedContents(): array + { + return $this->contents; + } +} diff --git a/fixtures/bench/without-compactors/src/Composer/ComposerFiles.php b/fixtures/bench/without-compactors/src/Composer/ComposerFiles.php new file mode 100644 index 000000000..79a36ab14 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Composer/ComposerFiles.php @@ -0,0 +1,68 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use function array_filter; +use function array_map; +use function array_values; + +final class ComposerFiles +{ + public static function createEmpty(): self + { + return new self( + ComposerFile::createEmpty(), + ComposerFile::createEmpty(), + ComposerFile::createEmpty(), + ); + } + + public function __construct( + private readonly ComposerFile $composerJson, + private readonly ComposerFile $composerLock, + private readonly ComposerFile $installedJson, + ) { + } + + public function getComposerJson(): ComposerFile + { + return $this->composerJson; + } + + public function getComposerLock(): ComposerFile + { + return $this->composerLock; + } + + public function getInstalledJson(): ComposerFile + { + return $this->installedJson; + } + + /** + * @return list + */ + public function getPaths(): array + { + return array_values( + array_filter( + array_map( + static fn (ComposerFile $file): ?string => $file->getPath(), + [$this->composerJson, $this->composerLock, $this->installedJson], + ), + ), + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Composer/ComposerOrchestrator.php b/fixtures/bench/without-compactors/src/Composer/ComposerOrchestrator.php new file mode 100644 index 000000000..02c7f74ed --- /dev/null +++ b/fixtures/bench/without-compactors/src/Composer/ComposerOrchestrator.php @@ -0,0 +1,186 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use Composer\Semver\Semver; +use Fidry\Console\IO; +use Fidry\FileSystem\FileSystem; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\NotInstantiable; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use RuntimeException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use function sprintf; +use function trim; +use const PHP_EOL; + +/** + * @private + */ +final class ComposerOrchestrator +{ + use NotInstantiable; + + public const SUPPORTED_VERSION_CONSTRAINTS = '^2.2.0'; + + private string $detectedVersion; + + public static function create(): self + { + return new self( + ComposerProcessFactory::create(io: IO::createNull()), + new NullLogger(), + new FileSystem(), + ); + } + + public function __construct( + private ComposerProcessFactory $processFactory, + private LoggerInterface $logger, + private FileSystem $fileSystem, + ) { + } + + /** + * @throws UndetectableComposerVersion + */ + public function getVersion(): string + { + if (isset($this->detectedVersion)) { + return $this->detectedVersion; + } + + $getVersionProcess = $this->processFactory->getVersionProcess(); + + $this->logger->info($getVersionProcess->getCommandLine()); + + $getVersionProcess->run(); + + if (false === $getVersionProcess->isSuccessful()) { + throw UndetectableComposerVersion::forFailedProcess($getVersionProcess); + } + + $output = $getVersionProcess->getOutput(); + + if (1 !== preg_match('/Composer version (\S+?) /', $output, $match)) { + throw UndetectableComposerVersion::forOutput( + $getVersionProcess, + $output, + ); + } + + $this->detectedVersion = $match[1]; + + return $this->detectedVersion; + } + + /** + * @throws UndetectableComposerVersion + * @throws IncompatibleComposerVersion + */ + public function checkVersion(): void + { + $version = $this->getVersion(); + + $this->logger->info( + sprintf( + 'Version detected: %s (Box requires %s)', + $version, + self::SUPPORTED_VERSION_CONSTRAINTS, + ), + ); + + if (!Semver::satisfies($version, self::SUPPORTED_VERSION_CONSTRAINTS)) { + throw IncompatibleComposerVersion::create($version, self::SUPPORTED_VERSION_CONSTRAINTS); + } + } + + public function dumpAutoload( + SymbolsRegistry $symbolsRegistry, + string $prefix, + bool $excludeDevFiles, + array $excludedComposerAutoloadFileHashes, + ): void { + $this->dumpAutoloader(true === $excludeDevFiles); + + if ('' === $prefix) { + return; + } + + $autoloadFile = $this->getVendorDir().'/autoload.php'; + + $autoloadContents = AutoloadDumper::generateAutoloadStatements( + $symbolsRegistry, + $excludedComposerAutoloadFileHashes, + $this->fileSystem->getFileContents($autoloadFile), + ); + + $this->fileSystem->dumpFile($autoloadFile, $autoloadContents); + } + + public function getVendorDir(): string + { + $vendorDirProcess = $this->processFactory->getVendorDirProcess(); + + $this->logger->info($vendorDirProcess->getCommandLine()); + + $vendorDirProcess->run(); + + if (false === $vendorDirProcess->isSuccessful()) { + throw new RuntimeException( + 'Could not retrieve the vendor dir.', + 0, + new ProcessFailedException($vendorDirProcess), + ); + } + + return trim($vendorDirProcess->getOutput()); + } + + private function dumpAutoloader(bool $noDev): void + { + $dumpAutoloadProcess = $this->processFactory->getDumpAutoloaderProcess($noDev); + + $this->logger->info($dumpAutoloadProcess->getCommandLine()); + + $dumpAutoloadProcess->run(); + + if (false === $dumpAutoloadProcess->isSuccessful()) { + throw new RuntimeException( + 'Could not dump the autoloader.', + 0, + new ProcessFailedException($dumpAutoloadProcess), + ); + } + + $output = $dumpAutoloadProcess->getOutput(); + $errorOutput = $dumpAutoloadProcess->getErrorOutput(); + + if ('' !== $output) { + $this->logger->info( + 'STDOUT output:'.PHP_EOL.$output, + ['stdout' => $output], + ); + } + + if ('' !== $errorOutput) { + $this->logger->info( + 'STDERR output:'.PHP_EOL.$errorOutput, + ['stderr' => $errorOutput], + ); + } + } +} diff --git a/fixtures/bench/without-compactors/src/Composer/ComposerProcessFactory.php b/fixtures/bench/without-compactors/src/Composer/ComposerProcessFactory.php new file mode 100644 index 000000000..6584ce950 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Composer/ComposerProcessFactory.php @@ -0,0 +1,164 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use Closure; +use Fidry\Console\IO; +use KevinGH\Box\Constants; +use RuntimeException; +use Symfony\Component\Process\ExecutableFinder; +use Symfony\Component\Process\Process; + +/** + * @final + * @private + */ +class ComposerProcessFactory +{ + private string $composerExecutable; + + public static function create( + ?string $composerExecutable = null, + ?IO $io = null, + ): self { + $io ??= IO::createNull(); + + return new self( + null === $composerExecutable + ? self::retrieveComposerExecutable(...) + : static fn () => $composerExecutable, + self::retrieveSubProcessVerbosity($io), + $io->isDecorated(), + self::getDefaultEnvVars(), + ); + } + + /** + * @param Closure():string $composerExecutableFactory + */ + public function __construct( + private Closure $composerExecutableFactory, + private ?string $verbosity, + private bool $ansi, + private array $defaultEnvironmentVariables, + ) { + } + + public function getVersionProcess(): Process + { + return $this->createProcess( + [ + $this->getComposerExecutable(), + '--version', + // Never use ANSI support here as we want to parse the raw output. + '--no-ansi', + ], + // Ensure that even if this command gets executed within the app with --quiet it still + // works. + ['SHELL_VERBOSITY' => 0], + ); + } + + public function getDumpAutoloaderProcess(bool $noDev): Process + { + $composerCommand = [$this->getComposerExecutable(), 'dump-autoload', '--classmap-authoritative']; + + if (true === $noDev) { + $composerCommand[] = '--no-dev'; + } + + if (null !== $this->verbosity) { + $composerCommand[] = $this->verbosity; + } + + if ($this->ansi) { + $composerCommand[] = '--ansi'; + } + + return $this->createProcess($composerCommand); + } + + public function getVendorDirProcess(): Process + { + return $this->createProcess( + [ + $this->getComposerExecutable(), + 'config', + 'vendor-dir', + // Never use ANSI support here as we want to parse the raw output. + '--no-ansi', + ], + // Ensure that even if this command gets executed within the app with --quiet it still + // works. + ['SHELL_VERBOSITY' => 0], + ); + } + + private function createProcess(array $command, array $environmentVariables = []): Process + { + return new Process( + $command, + env: [ + ...$this->defaultEnvironmentVariables, + ...$environmentVariables, + ], + ); + } + + private function getComposerExecutable(): string + { + if (!isset($this->composerExecutable)) { + $this->composerExecutable = ($this->composerExecutableFactory)(); + } + + return $this->composerExecutable; + } + + private static function retrieveSubProcessVerbosity(IO $io): ?string + { + if ($io->isDebug()) { + return '-vvv'; + } + + if ($io->isVeryVerbose()) { + return '-v'; + } + + return null; + } + + private static function getDefaultEnvVars(): array + { + $vars = ['COMPOSER_ORIGINAL_INIS' => '']; + + if ('1' === (string) getenv(Constants::ALLOW_XDEBUG)) { + $vars['COMPOSER_ALLOW_XDEBUG'] = '1'; + } + + return $vars; + } + + private static function retrieveComposerExecutable(): string + { + $executableFinder = new ExecutableFinder(); + $executableFinder->addSuffix('.phar'); + + if (null === $composer = $executableFinder->find('composer')) { + throw new RuntimeException('Could not find a Composer executable.'); + } + + return $composer; + } +} diff --git a/fixtures/bench/without-compactors/src/Composer/IncompatibleComposerVersion.php b/fixtures/bench/without-compactors/src/Composer/IncompatibleComposerVersion.php new file mode 100644 index 000000000..b893111eb --- /dev/null +++ b/fixtures/bench/without-compactors/src/Composer/IncompatibleComposerVersion.php @@ -0,0 +1,32 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use RuntimeException; +use function sprintf; + +final class IncompatibleComposerVersion extends RuntimeException +{ + public static function create(string $version, string $constraints): self + { + return new self( + sprintf( + 'The Composer version "%s" does not satisfy the constraint "%s".', + $version, + $constraints, + ) + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Composer/UndetectableComposerVersion.php b/fixtures/bench/without-compactors/src/Composer/UndetectableComposerVersion.php new file mode 100644 index 000000000..f2c6115b7 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Composer/UndetectableComposerVersion.php @@ -0,0 +1,65 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Composer; + +use JetBrains\PhpStorm\Pure; +use RuntimeException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; +use Throwable; +use function implode; +use function sprintf; +use const PHP_EOL; + +final class UndetectableComposerVersion extends RuntimeException +{ + #[Pure] + public function __construct( + string $message, + public readonly ?Process $process = null, + int $code = 0, + ?Throwable $previous = null, + ) { + parent::__construct($message, $code, $previous); + } + + public static function forFailedProcess(Process $process): self + { + $previous = new ProcessFailedException($process); + + return new self( + sprintf( + 'Could not detect the Composer version: %s', + $previous->getMessage(), + ), + $process, + previous: $previous, + ); + } + + public static function forOutput(Process $process, string $normalizedOutput): self + { + return new self( + implode( + PHP_EOL, + [ + 'Could not determine the Composer version from the following output:', + $normalizedOutput, + ], + ), + $process, + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Configuration/Configuration.php b/fixtures/bench/without-compactors/src/Configuration/Configuration.php new file mode 100644 index 000000000..ddc5b051c --- /dev/null +++ b/fixtures/bench/without-compactors/src/Configuration/Configuration.php @@ -0,0 +1,2738 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Configuration; + +use Closure; +use DateTimeImmutable; +use DateTimeZone; +use Fidry\FileSystem\FS; +use Humbug\PhpScoper\Configuration\Configuration as PhpScoperConfiguration; +use InvalidArgumentException; +use KevinGH\Box\Compactor\Compactor; +use KevinGH\Box\Compactor\Compactors; +use KevinGH\Box\Compactor\Php as PhpCompactor; +use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor; +use KevinGH\Box\Composer\ComposerConfiguration; +use KevinGH\Box\Composer\ComposerFile; +use KevinGH\Box\Composer\ComposerFiles; +use KevinGH\Box\Json\Json; +use KevinGH\Box\MapFile; +use KevinGH\Box\Phar\CompressionAlgorithm; +use KevinGH\Box\Phar\SigningAlgorithm; +use KevinGH\Box\PhpScoper\ConfigurationFactory as PhpScoperConfigurationFactory; +use KevinGH\Box\PhpScoper\SerializableScoper; +use Phar; +use RuntimeException; +use Seld\JsonLint\ParsingException; +use SplFileInfo; +use stdClass; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; +use Webmozart\Assert\Assert; +use function array_diff; +use function array_filter; +use function array_flip; +use function array_key_exists; +use function array_keys; +use function array_map; +use function array_merge; +use function array_unique; +use function array_values; +use function array_walk; +use function constant; +use function current; +use function dirname; +use function explode; +use function file_exists; +use function getcwd; +use function implode; +use function in_array; +use function intval; +use function is_array; +use function is_bool; +use function is_file; +use function is_link; +use function is_object; +use function is_readable; +use function is_string; +use function iter\map; +use function iter\toArray; +use function iter\values; +use function KevinGH\Box\get_box_version; +use function KevinGH\Box\unique_id; +use function krsort; +use function preg_match; +use function preg_replace; +use function property_exists; +use function realpath; +use function sprintf; +use function str_starts_with; +use function trigger_error; +use function trim; +use const E_USER_DEPRECATED; + +/** + * @private + */ +final class Configuration +{ + private const DEFAULT_OUTPUT_FALLBACK = 'test.phar'; + private const DEFAULT_MAIN_SCRIPT = 'index.php'; + private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s T'; + private const DEFAULT_REPLACEMENT_SIGIL = '@'; + private const DEFAULT_SHEBANG = '#!/usr/bin/env php'; + private const DEFAULT_BANNER = <<<'BANNER' + Generated by Humbug Box %s. + + @link https://github.com/humbug/box + BANNER; + private const FILES_SETTINGS = [ + 'directories', + 'finder', + ]; + private const PHP_SCOPER_CONFIG = 'scoper.inc.php'; + private const DEFAULT_SIGNING_ALGORITHM = SigningAlgorithm::SHA512; + private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-'; + + private const DEFAULT_IGNORED_ANNOTATIONS = [ + 'abstract', + 'access', + 'annotation', + 'api', + 'attribute', + 'attributes', + 'author', + 'category', + 'code', + 'codecoverageignore', + 'codecoverageignoreend', + 'codecoverageignorestart', + 'copyright', + 'deprec', + 'deprecated', + 'endcode', + 'example', + 'exception', + 'filesource', + 'final', + 'fixme', + 'global', + 'ignore', + 'ingroup', + 'inheritdoc', + 'internal', + 'license', + 'link', + 'magic', + 'method', + 'name', + 'override', + 'package', + 'package_version', + 'param', + 'private', + 'property', + 'required', + 'return', + 'see', + 'since', + 'static', + 'staticvar', + 'subpackage', + 'suppresswarnings', + 'target', + 'throw', + 'throws', + 'todo', + 'tutorial', + 'usedby', + 'uses', + 'var', + 'version', + ]; + + private const ALGORITHM_KEY = 'algorithm'; + private const ALIAS_KEY = 'alias'; + private const ANNOTATIONS_KEY = 'annotations'; + private const IGNORED_ANNOTATIONS_KEY = 'ignore'; + private const AUTO_DISCOVERY_KEY = 'force-autodiscovery'; + private const BANNER_KEY = 'banner'; + private const BANNER_FILE_KEY = 'banner-file'; + private const BASE_PATH_KEY = 'base-path'; + private const BLACKLIST_KEY = 'blacklist'; + private const CHECK_REQUIREMENTS_KEY = 'check-requirements'; + private const CHMOD_KEY = 'chmod'; + private const COMPACTORS_KEY = 'compactors'; + private const COMPRESSION_KEY = 'compression'; + private const DATETIME_KEY = 'datetime'; + private const DATETIME_FORMAT_KEY = 'datetime-format'; + private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format'; + private const DIRECTORIES_KEY = 'directories'; + private const DIRECTORIES_BIN_KEY = 'directories-bin'; + private const DUMP_AUTOLOAD_KEY = 'dump-autoload'; + private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files'; + private const EXCLUDE_DEV_FILES_KEY = 'exclude-dev-files'; + private const FILES_KEY = 'files'; + private const FILES_BIN_KEY = 'files-bin'; + private const FINDER_KEY = 'finder'; + private const FINDER_BIN_KEY = 'finder-bin'; + private const GIT_KEY = 'git'; + private const GIT_COMMIT_KEY = 'git-commit'; + private const GIT_COMMIT_SHORT_KEY = 'git-commit-short'; + private const GIT_TAG_KEY = 'git-tag'; + private const GIT_VERSION_KEY = 'git-version'; + private const INTERCEPT_KEY = 'intercept'; + private const KEY_KEY = 'key'; + private const KEY_PASS_KEY = 'key-pass'; + private const MAIN_KEY = 'main'; + private const MAP_KEY = 'map'; + private const METADATA_KEY = 'metadata'; + private const OUTPUT_KEY = 'output'; + private const PHP_SCOPER_KEY = 'php-scoper'; + private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil'; + private const REPLACEMENTS_KEY = 'replacements'; + private const SHEBANG_KEY = 'shebang'; + private const STUB_KEY = 'stub'; + private const TIMESTAMP = 'timestamp'; + + private ?string $mainScriptPath; + private ?string $mainScriptContents; + private ?string $composerBin = null; + + public static function create(?string $file, stdClass $raw): self + { + $logger = new ConfigurationLogger(); + + $basePath = self::retrieveBasePath($file, $raw, $logger); + + $composerFiles = self::retrieveComposerFiles($basePath); + + $dumpAutoload = self::retrieveDumpAutoload($raw, $composerFiles, $logger); + + $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw, $logger); + + $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles->getComposerJson()->getDecodedContents(), $logger); + $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath); + + [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger); + + $stubPath = self::retrieveStubPath($raw, $basePath, $logger); + $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger); + + $alias = self::retrieveAlias($raw, null !== $stubPath, $logger); + + $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger); + + $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger); + $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger); + + if (null !== $stubBannerPath) { + $stubBannerContents = FS::getFileContents($stubBannerPath); + } + + $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents); + + if (null !== $stubBannerPath && self::getDefaultBanner() === $stubBannerContents) { + self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY); + } + + $isInterceptsFileFunctions = self::retrieveInterceptsFileFunctions($raw, $isStubGenerated, $logger); + + $checkRequirements = self::retrieveCheckRequirements( + $raw, + null !== $composerFiles->getComposerJson()->getPath(), + null !== $composerFiles->getComposerLock()->getPath(), + false === $isStubGenerated && null === $stubPath, + $logger, + ); + + $excludeDevPackages = self::retrieveExcludeDevFiles($raw, $dumpAutoload, $logger); + + $devPackages = ComposerConfiguration::retrieveDevPackages( + $basePath, + $composerFiles->getComposerJson()->getDecodedContents(), + $composerFiles->getComposerLock()->getDecodedContents(), + $excludeDevPackages, + ); + + /** + * @var string[] $excludedPaths + * @var Closure $blacklistFilter + */ + [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter( + $raw, + $basePath, + $logger, + $tmpOutputPath, + $outputPath, + $mainScriptPath, + ); + // Excluded paths above is a bit misleading since including a file directly has precedence over the blacklist. + // If you consider the following: + // + // { + // "files": ["file1"], + // "blacklist": ["file1"], + // } + // + // In the end the file "file1" _will_ be included: blacklist are here to help out to exclude files for finders + // and directories but the user should always have the possibility to force his way to include a file. + // + // The exception however, is for the following which is essential for the good functioning of Box + $alwaysExcludedPaths = array_map( + static fn (string $excludedPath): string => self::normalizePath($excludedPath, $basePath), + array_filter([$tmpOutputPath, $outputPath, $mainScriptPath]), + ); + + $autodiscoverFiles = self::autodiscoverFiles($file, $raw); + $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw, $logger); + + $filesAggregate = self::collectFiles( + $raw, + $basePath, + $mainScriptPath, + $blacklistFilter, + $excludedPaths, + $alwaysExcludedPaths, + $devPackages, + $composerFiles, + $autodiscoverFiles, + $forceFilesAutodiscovery, + $logger, + ); + $binaryFilesAggregate = self::collectBinaryFiles( + $raw, + $basePath, + $blacklistFilter, + $excludedPaths, + $alwaysExcludedPaths, + $devPackages, + $logger, + ); + + $compactors = self::retrieveCompactors($raw, $basePath, $logger); + $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw, $logger); + + $fileMode = self::retrieveFileMode($raw, $logger); + + $map = self::retrieveMap($raw, $logger); + $fileMapper = new MapFile($basePath, $map); + + $metadata = self::retrieveMetadata($raw, $logger); + + $signingAlgorithm = self::retrieveSigningAlgorithm($raw, $logger); + $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger); + $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger); + $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger); + + $replacements = self::retrieveReplacements($raw, $file, $basePath, $logger); + + $timestamp = self::retrieveTimestamp($raw, $signingAlgorithm, $logger); + + return new self( + $file, + $alias, + $basePath, + $composerFiles->getComposerJson(), + $composerFiles->getComposerLock(), + $filesAggregate, + $binaryFilesAggregate, + $autodiscoverFiles || $forceFilesAutodiscovery, + $dumpAutoload, + $excludeComposerFiles, + $excludeDevPackages, + $compactors, + $compressionAlgorithm, + $fileMode, + $mainScriptPath, + $mainScriptContents, + $fileMapper, + $metadata, + $tmpOutputPath, + $outputPath, + $privateKeyPassphrase, + $privateKeyPath, + $promptForPrivateKey, + $replacements, + $shebang, + $signingAlgorithm, + $stubBannerContents, + $stubBannerPath, + $stubPath, + $isInterceptsFileFunctions, + $isStubGenerated, + $timestamp, + $checkRequirements, + $logger->getWarnings(), + $logger->getRecommendations(), + ); + } + + /** + * @param string $basePath Utility to private the base path used and be able to retrieve a + * path relative to it (the base path) + * @param array $composerJson The first element is the path to the `composer.json` file as a + * string and the second element its decoded contents as an + * associative array. + * @param array $composerLock The first element is the path to the `composer.lock` file as a + * string and the second element its decoded contents as an + * associative array. + * @param SplFileInfo[] $files List of files + * @param SplFileInfo[] $binaryFiles List of binary files + * @param bool $dumpAutoload Whether the Composer autoloader should be dumped + * @param bool $excludeComposerFiles Whether the Composer files composer.json, composer.lock and + * installed.json should be removed from the PHAR + * @param CompressionAlgorithm $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants + * @param null|int $fileMode File mode in octal form + * @param string $mainScriptPath The main script file path + * @param string $mainScriptContents The processed content of the main script file + * @param MapFile $fileMapper Utility to map the files from outside and inside the PHAR + * @param mixed $metadata The PHAR Metadata + * @param bool $promptForPrivateKey If the user should be prompted for the private key passphrase + * @param array $processedReplacements The processed list of replacement placeholders and their values + * @param null|non-empty-string $shebang The shebang line + * @param SigningAlgorithm $signingAlgorithm The PHAR siging algorithm. See \Phar constants + * @param null|string $stubBannerContents The stub banner comment + * @param null|string $stubBannerPath The path to the stub banner comment file + * @param null|string $stubPath The PHAR stub file path + * @param bool $isInterceptFileFuncs Whether Phar::interceptFileFuncs() should be used + * @param bool $isStubGenerated Whether if the PHAR stub should be generated + * @param null|DateTimeImmutable $timestamp Timestamp at which the PHAR will be set to. + * @param bool $checkRequirements Whether the PHAR will check the application requirements before + * running + * @param string[] $warnings + * @param string[] $recommendations + */ + private function __construct( + private ?string $file, + private string $alias, + private string $basePath, + private ComposerFile $composerJson, + private ComposerFile $composerLock, + private array $files, + private array $binaryFiles, + private bool $autodiscoveredFiles, + private bool $dumpAutoload, + private bool $excludeComposerFiles, + private bool $excludeDevFiles, + private Compactors|array $compactors, + private CompressionAlgorithm $compressionAlgorithm, + private int|string|null $fileMode, + ?string $mainScriptPath, + ?string $mainScriptContents, + private MapFile $fileMapper, + private mixed $metadata, + private string $tmpOutputPath, + private string $outputPath, + private ?string $privateKeyPassphrase, + private ?string $privateKeyPath, + private bool $promptForPrivateKey, + private array $processedReplacements, + private ?string $shebang, + private SigningAlgorithm $signingAlgorithm, + private ?string $stubBannerContents, + private ?string $stubBannerPath, + private ?string $stubPath, + private bool $isInterceptFileFuncs, + private bool $isStubGenerated, + private ?DateTimeImmutable $timestamp, + private bool $checkRequirements, + private array $warnings, + private array $recommendations, + ) { + if (null === $mainScriptPath) { + Assert::null($mainScriptContents); + } else { + Assert::notNull($mainScriptContents); + } + + $this->mainScriptPath = $mainScriptPath; + $this->mainScriptContents = $mainScriptContents; + } + + public function setComposerBin(?string $composerBin): void + { + $this->composerBin = $composerBin; + } + + public function getComposerBin(): ?string + { + return $this->composerBin; + } + + public function export(): string + { + return ExportableConfiguration::create($this)->export(); + } + + public function getConfigurationFile(): ?string + { + return $this->file; + } + + public function getAlias(): string + { + return $this->alias; + } + + public function getBasePath(): string + { + return $this->basePath; + } + + public function getComposerJson(): ?string + { + return $this->composerJson->getPath(); + } + + public function getDecodedComposerJsonContents(): ?array + { + return null === $this->composerJson->getPath() ? null : $this->composerJson->getDecodedContents(); + } + + public function getComposerLock(): ?string + { + return $this->composerLock->getPath(); + } + + public function getDecodedComposerLockContents(): ?array + { + return null === $this->composerLock->getPath() ? null : $this->composerLock->getDecodedContents(); + } + + /** + * @return SplFileInfo[] + */ + public function getFiles(): array + { + return $this->files; + } + + /** + * @return SplFileInfo[] + */ + public function getBinaryFiles(): array + { + return $this->binaryFiles; + } + + public function hasAutodiscoveredFiles(): bool + { + return $this->autodiscoveredFiles; + } + + public function dumpAutoload(): bool + { + return $this->dumpAutoload; + } + + public function excludeComposerFiles(): bool + { + return $this->excludeComposerFiles; + } + + public function excludeDevFiles(): bool + { + return $this->excludeDevFiles; + } + + public function getCompactors(): Compactors + { + return $this->compactors; + } + + public function getCompressionAlgorithm(): CompressionAlgorithm + { + return $this->compressionAlgorithm; + } + + public function getFileMode(): ?int + { + return $this->fileMode; + } + + public function hasMainScript(): bool + { + return null !== $this->mainScriptPath; + } + + public function getMainScriptPath(): string + { + Assert::notNull( + $this->mainScriptPath, + 'Cannot retrieve the main script path: no main script configured.', + ); + + return $this->mainScriptPath; + } + + public function getMainScriptContents(): string + { + Assert::notNull( + $this->mainScriptPath, + 'Cannot retrieve the main script contents: no main script configured.', + ); + + return $this->mainScriptContents; + } + + public function checkRequirements(): bool + { + return $this->checkRequirements; + } + + public function getTmpOutputPath(): string + { + return $this->tmpOutputPath; + } + + public function getOutputPath(): string + { + return $this->outputPath; + } + + public function getFileMapper(): MapFile + { + return $this->fileMapper; + } + + public function getMetadata(): mixed + { + return $this->metadata; + } + + public function getPrivateKeyPassphrase(): ?string + { + return $this->privateKeyPassphrase; + } + + public function getPrivateKeyPath(): ?string + { + return $this->privateKeyPath; + } + + /** + * @deprecated Use promptForPrivateKey() instead + */ + public function isPrivateKeyPrompt(): bool + { + return $this->promptForPrivateKey; + } + + public function promptForPrivateKey(): bool + { + return $this->promptForPrivateKey; + } + + /** + * @return scalar[] + */ + public function getReplacements(): array + { + return $this->processedReplacements; + } + + public function getShebang(): ?string + { + return $this->shebang; + } + + public function getSigningAlgorithm(): SigningAlgorithm + { + return $this->signingAlgorithm; + } + + public function getStubBannerContents(): ?string + { + return $this->stubBannerContents; + } + + public function getStubBannerPath(): ?string + { + return $this->stubBannerPath; + } + + public function getStubPath(): ?string + { + return $this->stubPath; + } + + public function isInterceptFileFuncs(): bool + { + return $this->isInterceptFileFuncs; + } + + public function isStubGenerated(): bool + { + return $this->isStubGenerated; + } + + public function getTimestamp(): ?DateTimeImmutable + { + return $this->timestamp; + } + + /** + * @return string[] + */ + public function getWarnings(): array + { + return $this->warnings; + } + + /** + * @return string[] + */ + public function getRecommendations(): array + { + return $this->recommendations; + } + + private static function retrieveAlias(stdClass $raw, bool $userStubUsed, ConfigurationLogger $logger): string + { + self::checkIfDefaultValue($logger, $raw, self::ALIAS_KEY); + + if (false === isset($raw->{self::ALIAS_KEY})) { + return unique_id(self::DEFAULT_ALIAS_PREFIX).'.phar'; + } + + $alias = trim($raw->{self::ALIAS_KEY}); + + Assert::notEmpty($alias, 'A PHAR alias cannot be empty when provided.'); + + if ($userStubUsed) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but is ignored since a custom stub path is used', + self::ALIAS_KEY, + ), + ); + } + + return $alias; + } + + private static function retrieveBasePath(?string $file, stdClass $raw, ConfigurationLogger $logger): string + { + if (null === $file) { + return getcwd(); + } + + if (false === isset($raw->{self::BASE_PATH_KEY})) { + return realpath(dirname($file)); + } + + $basePath = trim($raw->{self::BASE_PATH_KEY}); + + Assert::directory( + $basePath, + 'The base path %s is not a directory or does not exist.', + ); + + $basePath = realpath($basePath); + $defaultPath = realpath(dirname($file)); + + if ($basePath === $defaultPath) { + self::addRecommendationForDefaultValue($logger, self::BASE_PATH_KEY); + } + + return $basePath; + } + + /** + * Checks if files should be auto-discovered. It does NOT account for the force-autodiscovery setting. + */ + private static function autodiscoverFiles(?string $file, stdClass $raw): bool + { + if (null === $file) { + return true; + } + + $associativeRaw = (array) $raw; + + return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($associativeRaw)); + } + + private static function retrieveForceFilesAutodiscovery(stdClass $raw, ConfigurationLogger $logger): bool + { + self::checkIfDefaultValue($logger, $raw, self::AUTO_DISCOVERY_KEY, false); + + return $raw->{self::AUTO_DISCOVERY_KEY} ?? false; + } + + private static function retrieveBlacklistFilter( + stdClass $raw, + string $basePath, + ConfigurationLogger $logger, + ?string ...$excludedPaths, + ): array { + $blacklist = array_flip( + self::retrieveBlacklist($raw, $basePath, $logger, ...$excludedPaths), + ); + + $blacklistFilter = static function (SplFileInfo $file) use ($blacklist): ?bool { + if ($file->isLink()) { + return false; + } + + if (false === $file->getRealPath()) { + return false; + } + + if (array_key_exists($file->getRealPath(), $blacklist)) { + return false; + } + + return null; + }; + + return [array_keys($blacklist), $blacklistFilter]; + } + + /** + * @param null[]|string[] $excludedPaths + * + * @return string[] + */ + private static function retrieveBlacklist( + stdClass $raw, + string $basePath, + ConfigurationLogger $logger, + ?string ...$excludedPaths, + ): array { + self::checkIfDefaultValue($logger, $raw, self::BLACKLIST_KEY, []); + + $normalizedBlacklist = array_map( + static fn (string $excludedPath): string => self::normalizePath($excludedPath, $basePath), + array_filter($excludedPaths), + ); + + /** @var string[] $blacklist */ + $blacklist = $raw->{self::BLACKLIST_KEY} ?? []; + + foreach ($blacklist as $file) { + $normalizedBlacklist[] = self::normalizePath($file, $basePath); + $normalizedBlacklist[] = Path::canonicalize(Path::makeRelative(trim($file), $basePath)); + } + + return array_unique($normalizedBlacklist); + } + + /** + * @param string[] $excludedPaths + * @param string[] $alwaysExcludedPaths + * @param string[] $devPackages + * + * @return SplFileInfo[] + */ + private static function collectFiles( + stdClass $raw, + string $basePath, + ?string $mainScriptPath, + Closure $blacklistFilter, + array $excludedPaths, + array $alwaysExcludedPaths, + array $devPackages, + ComposerFiles $composerFiles, + bool $autodiscoverFiles, + bool $forceFilesAutodiscovery, + ConfigurationLogger $logger, + ): array { + $files = [self::retrieveFiles($raw, self::FILES_KEY, $basePath, $composerFiles, $alwaysExcludedPaths, $logger)]; + + if ($autodiscoverFiles || $forceFilesAutodiscovery) { + [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude( + $basePath, + $composerFiles->getComposerJson()->getDecodedContents(), + $devPackages, + $composerFiles->getPaths(), + $excludedPaths, + ); + + $files[] = self::wrapInSplFileInfo($filesToAppend); + + $files[] = self::retrieveAllFiles( + $basePath, + $directories, + $mainScriptPath, + $blacklistFilter, + $excludedPaths, + $devPackages, + ); + } + + if (false === $autodiscoverFiles) { + $files[] = self::retrieveDirectories( + $raw, + self::DIRECTORIES_KEY, + $basePath, + $blacklistFilter, + $excludedPaths, + $logger, + ); + + $filesFromFinders = self::retrieveFilesFromFinders( + $raw, + self::FINDER_KEY, + $basePath, + $blacklistFilter, + $devPackages, + $logger, + ); + + foreach ($filesFromFinders as $filesFromFinder) { + // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files + $files[] = $filesFromFinder; + } + + $files[] = self::wrapInSplFileInfo($composerFiles->getPaths()); + } + + return self::retrieveFilesAggregate(...$files); + } + + /** + * @param string[] $excludedPaths + * @param string[] $alwaysExcludedPaths + * @param string[] $devPackages + * + * @return SplFileInfo[] + */ + private static function collectBinaryFiles( + stdClass $raw, + string $basePath, + Closure $blacklistFilter, + array $excludedPaths, + array $alwaysExcludedPaths, + array $devPackages, + ConfigurationLogger $logger, + ): array { + $binaryFiles = self::retrieveFiles($raw, self::FILES_BIN_KEY, $basePath, ComposerFiles::createEmpty(), $alwaysExcludedPaths, $logger); + + $binaryDirectories = self::retrieveDirectories( + $raw, + self::DIRECTORIES_BIN_KEY, + $basePath, + $blacklistFilter, + $excludedPaths, + $logger, + ); + + $binaryFilesFromFinders = self::retrieveFilesFromFinders( + $raw, + self::FINDER_BIN_KEY, + $basePath, + $blacklistFilter, + $devPackages, + $logger, + ); + + return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders); + } + + /** + * @param string[] $excludedFiles + * + * @return SplFileInfo[] + */ + private static function retrieveFiles( + stdClass $raw, + string $key, + string $basePath, + ComposerFiles $composerFiles, + array $excludedFiles, + ConfigurationLogger $logger, + ): array { + self::checkIfDefaultValue($logger, $raw, $key, []); + + $excludedFiles = array_flip($excludedFiles); + $files = array_filter([ + $composerFiles->getComposerJson()->getPath(), + $composerFiles->getComposerLock()->getPath(), + ]); + + if (false === isset($raw->{$key})) { + return self::wrapInSplFileInfo($files); + } + + if ([] === (array) $raw->{$key}) { + return self::wrapInSplFileInfo($files); + } + + $files = array_merge((array) $raw->{$key}, $files); + + Assert::allString($files); + + $normalizePath = static function (string $file) use ($basePath, $key, $excludedFiles): ?SplFileInfo { + $file = self::normalizePath($file, $basePath); + + Assert::false( + is_link($file), + sprintf( + 'Cannot add the link "%s": links are not supported.', + $file, + ), + ); + + Assert::file( + $file, + sprintf( + '"%s" must contain a list of existing files. Could not find %%s.', + $key, + ), + ); + + return array_key_exists($file, $excludedFiles) ? null : new SplFileInfo($file); + }; + + return array_filter(array_map($normalizePath, $files)); + } + + /** + * @param string $key Config property name + * @param string[] $excludedPaths + * + * @return iterable&(SplFileInfo[]&Finder) + */ + private static function retrieveDirectories( + stdClass $raw, + string $key, + string $basePath, + Closure $blacklistFilter, + array $excludedPaths, + ConfigurationLogger $logger, + ): iterable { + $directories = self::retrieveDirectoryPaths($raw, $key, $basePath, $logger); + + if ([] !== $directories) { + $finder = Finder::create() + ->files() + ->filter($blacklistFilter) + ->ignoreVCS(true) + ->in($directories); + + foreach ($excludedPaths as $excludedPath) { + $finder->notPath($excludedPath); + } + + return $finder; + } + + return []; + } + + /** + * @param string[] $devPackages + * + * @return iterable[]|SplFileInfo[][] + */ + private static function retrieveFilesFromFinders( + stdClass $raw, + string $key, + string $basePath, + Closure $blacklistFilter, + array $devPackages, + ConfigurationLogger $logger, + ): array { + self::checkIfDefaultValue($logger, $raw, $key, []); + + if (false === isset($raw->{$key})) { + return []; + } + + $finder = $raw->{$key}; + + return self::processFinders($finder, $basePath, $blacklistFilter, $devPackages); + } + + /** + * @param iterable[]|SplFileInfo[][] $fileIterators + * + * @return SplFileInfo[] + */ + private static function retrieveFilesAggregate(iterable ...$fileIterators): array + { + $files = []; + + foreach ($fileIterators as $fileIterator) { + foreach ($fileIterator as $file) { + $files[(string) $file] = $file; + } + } + + return array_values($files); + } + + /** + * @param string[] $devPackages + * + * @return Finder[]|SplFileInfo[][] + */ + private static function processFinders( + array $findersConfig, + string $basePath, + Closure $blacklistFilter, + array $devPackages, + ): array { + $processFinderConfig = static fn (stdClass $config) => self::processFinder($config, $basePath, $blacklistFilter, $devPackages); + + return array_map($processFinderConfig, $findersConfig); + } + + /** + * @param string[] $devPackages + * + * @return Finder|SplFileInfo[] + */ + private static function processFinder( + stdClass $config, + string $basePath, + Closure $blacklistFilter, + array $devPackages, + ): Finder { + $finder = Finder::create() + ->files() + ->filter($blacklistFilter) + ->filter( + static function (SplFileInfo $fileInfo) use ($devPackages): bool { + foreach ($devPackages as $devPackage) { + if ($devPackage === Path::getLongestCommonBasePath($devPackage, $fileInfo->getRealPath())) { + // File belongs to the dev package + return false; + } + } + + return true; + }, + ) + ->ignoreVCS(true); + + $normalizedConfig = (static function (array $config, Finder $finder): array { + $normalizedConfig = []; + + foreach ($config as $method => $arguments) { + $method = trim($method); + $arguments = (array) $arguments; + + Assert::methodExists($finder, $method); + + $normalizedConfig[$method] = $arguments; + } + + krsort($normalizedConfig); + + return $normalizedConfig; + })((array) $config, $finder); + + $createNormalizedDirectories = static function (string $directory) use ($basePath): ?string { + $directory = self::normalizePath($directory, $basePath); + + Assert::false( + is_link($directory), + sprintf( + 'Cannot append the link "%s" to the Finder: links are not supported.', + $directory, + ), + ); + + Assert::directory($directory); + + return $directory; + }; + + $normalizeFileOrDirectory = static function (?string &$fileOrDirectory) use ($basePath, $blacklistFilter): void { + if (null === $fileOrDirectory) { + return; + } + + $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath); + + Assert::false( + is_link($fileOrDirectory), + sprintf( + 'Cannot append the link "%s" to the Finder: links are not supported.', + $fileOrDirectory, + ), + ); + + Assert::true( + file_exists($fileOrDirectory), + sprintf( + 'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).', + $fileOrDirectory, + ), + ); + + if (false === is_file($fileOrDirectory)) { + Assert::directory($fileOrDirectory); + } else { + Assert::file($fileOrDirectory); + } + + if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) { + $fileOrDirectory = null; + } + }; + + foreach ($normalizedConfig as $method => $arguments) { + if ('in' === $method) { + $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments); + } + + if ('exclude' === $method) { + $arguments = array_unique(array_map('trim', $arguments)); + } + + if ('append' === $method) { + array_walk($arguments, $normalizeFileOrDirectory); + + $arguments = [array_filter($arguments)]; + } + + foreach ($arguments as $argument) { + $finder->{$method}($argument); + } + } + + return $finder; + } + + /** + * @param string[] $devPackages + * @param string[] $filesToAppend + * + * @return string[][] + */ + private static function retrieveAllDirectoriesToInclude( + string $basePath, + ?array $decodedJsonContents, + array $devPackages, + array $filesToAppend, + array $excludedPaths, + ): array { + $toString = static fn (string|SplFileInfo $file): string => (string) $file; + + if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) { + $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath); + } else { + $vendorDir = self::normalizePath('vendor', $basePath); + } + + if (file_exists($vendorDir)) { + // Note that some files may not exist. For example installed.json does not exist at all if no dependencies + // are included in composer.json. + $requiredComposerFiles = [ + 'installed.json', + 'installed.php', + 'InstalledVersions.php', + ]; + + foreach ($requiredComposerFiles as $requiredComposerFile) { + $normalizePath = self::normalizePath($vendorDir.'/composer/'.$requiredComposerFile, $basePath); + + if (file_exists($normalizePath)) { + $filesToAppend[] = $normalizePath; + } + } + + $vendorPackages = toArray(values(map( + $toString, + Finder::create() + ->in($vendorDir) + ->directories() + ->depth(1) + ->ignoreUnreadableDirs() + ->filter( + static function (SplFileInfo $fileInfo): ?bool { + if ($fileInfo->isLink()) { + return false; + } + + return null; + }, + ), + ))); + + $vendorPackages = array_diff($vendorPackages, $devPackages); + + if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) { + $files = toArray(values(map( + $toString, + Finder::create() + ->in($basePath) + ->files() + ->depth(0), + ))); + + $directories = toArray(values(map( + $toString, + Finder::create() + ->in($basePath) + ->notPath('vendor') + ->directories() + ->depth(0), + ))); + + return [ + array_merge( + array_diff($files, $excludedPaths), + $filesToAppend, + ), + array_merge( + array_diff($directories, $excludedPaths), + $vendorPackages, + ), + ]; + } + + $paths = $vendorPackages; + } else { + $paths = []; + } + + $autoload = $decodedJsonContents['autoload'] ?? []; + + if (array_key_exists('psr-4', $autoload)) { + foreach ($autoload['psr-4'] as $path) { + /** @var string|string[] $path */ + $composerPaths = (array) $path; + + foreach ($composerPaths as $composerPath) { + $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath; + } + } + } + + if (array_key_exists('psr-0', $autoload)) { + foreach ($autoload['psr-0'] as $path) { + /** @var string|string[] $path */ + $composerPaths = (array) $path; + + foreach ($composerPaths as $composerPath) { + $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath; + } + } + } + + if (array_key_exists('classmap', $autoload)) { + foreach ($autoload['classmap'] as $path) { + // @var string $path + $paths[] = $path; + } + } + + $normalizePath = static fn (string $path): string => Path::isAbsolute($path) + ? Path::canonicalize($path) + : self::normalizePath(trim($path, '/ '), $basePath); + + if (array_key_exists('files', $autoload)) { + foreach ($autoload['files'] as $path) { + /** @var string $path */ + $path = $normalizePath($path); + + Assert::file($path); + Assert::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.'); + + $filesToAppend[] = $path; + } + } + + $files = $filesToAppend; + $directories = []; + + foreach ($paths as $path) { + $path = $normalizePath($path); + + Assert::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.'); + Assert::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.'); + + if (is_file($path)) { + $files[] = $path; + } else { + $directories[] = $path; + } + } + + [$files, $directories] = [ + array_unique($files), + array_unique($directories), + ]; + + return [ + array_diff($files, $excludedPaths), + array_diff($directories, $excludedPaths), + ]; + } + + /** + * @param string[] $directories + * @param string[] $excludedPaths + * @param string[] $devPackages + * + * @return Finder|SplFileInfo[] + */ + private static function retrieveAllFiles( + string $basePath, + array $directories, + ?string $mainScriptPath, + Closure $blacklistFilter, + array $excludedPaths, + array $devPackages, + ): iterable { + if ([] === $directories) { + return []; + } + + $relativeDevPackages = array_map( + static fn (string $packagePath): string => Path::makeRelative($packagePath, $basePath), + $devPackages, + ); + + $finder = Finder::create() + ->files() + ->filter($blacklistFilter) + ->exclude($relativeDevPackages) + ->ignoreVCS(true) + ->ignoreDotFiles(true) + // Remove build files + ->notName('composer.json') + ->notName('composer.lock') + ->notName('Makefile') + ->notName('Vagrantfile') + ->notName('phpstan*.neon*') + ->notName('infection*.json*') + ->notName('humbug*.json*') + ->notName('easy-coding-standard.neon*') + ->notName('phpbench.json*') + ->notName('phpcs.xml*') + ->notName('psalm.xml*') + ->notName('scoper.inc*') + ->notName('box*.json*') + ->notName('phpdoc*.xml*') + ->notName('codecov.yml*') + ->notName('Dockerfile') + ->exclude('build') + ->exclude('dist') + ->exclude('example') + ->exclude('examples') + // Remove documentation + ->notName('*.md') + ->notName('*.rst') + ->notName('/^readme((?!\.php)(\..*+))?$/i') + ->notName('/^upgrade((?!\.php)(\..*+))?$/i') + ->notName('/^contributing((?!\.php)(\..*+))?$/i') + ->notName('/^changelog((?!\.php)(\..*+))?$/i') + ->notName('/^authors?((?!\.php)(\..*+))?$/i') + ->notName('/^conduct((?!\.php)(\..*+))?$/i') + ->notName('/^todo((?!\.php)(\..*+))?$/i') + ->exclude('doc') + ->exclude('docs') + ->exclude('documentation') + // Remove backup files + ->notName('*~') + ->notName('*.back') + ->notName('*.swp') + // Remove tests + ->exclude('tests') + ->exclude('Tests') + ->notName('/phpunit.*\.xml(.dist)?/') + ->notName('/behat.*\.yml(.dist)?/') + ->exclude('spec') + ->exclude('specs') + ->exclude('features') + // Remove CI config + ->exclude('travis') + ->notName('travis.yml') + ->notName('appveyor.yml') + ->notName('build.xml*'); + + if (null !== $mainScriptPath) { + $finder->notPath(Path::makeRelative($mainScriptPath, $basePath)); + } + + $finder->in($directories); + + $excludedPaths = array_unique( + array_filter( + array_map( + static fn (string $path): string => Path::makeRelative($path, $basePath), + $excludedPaths, + ), + static fn (string $path): bool => !str_starts_with($path, '..'), + ), + ); + + foreach ($excludedPaths as $excludedPath) { + $finder->notPath($excludedPath); + } + + return $finder; + } + + /** + * @param string $key Config property name + * + * @return string[] + */ + private static function retrieveDirectoryPaths( + stdClass $raw, + string $key, + string $basePath, + ConfigurationLogger $logger, + ): array { + self::checkIfDefaultValue($logger, $raw, $key, []); + + if (false === isset($raw->{$key})) { + return []; + } + + $directories = $raw->{$key}; + + $normalizeDirectory = static function (string $directory) use ($basePath, $key): string { + $directory = self::normalizePath($directory, $basePath); + + Assert::false( + is_link($directory), + sprintf( + 'Cannot add the link "%s": links are not supported.', + $directory, + ), + ); + + Assert::directory( + $directory, + sprintf( + '"%s" must contain a list of existing directories. Could not find %%s.', + $key, + ), + ); + + return $directory; + }; + + return array_map($normalizeDirectory, $directories); + } + + private static function normalizePath(string $file, string $basePath): string + { + return Path::makeAbsolute(trim($file), $basePath); + } + + /** + * @param string[] $files + * + * @return SplFileInfo[] + */ + private static function wrapInSplFileInfo(array $files): array + { + return array_map( + static fn (string $file): SplFileInfo => new SplFileInfo($file), + $files, + ); + } + + private static function retrieveDumpAutoload(stdClass $raw, ComposerFiles $composerFiles, ConfigurationLogger $logger): bool + { + self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, null); + + $canDumpAutoload = ( + null !== $composerFiles->getComposerJson()->getPath() + && ( + // The composer.lock and installed.json are optional (e.g. if there is no dependencies installed) + // but when one is present, the other must be as well otherwise the dumped autoloader will be broken + ( + null === $composerFiles->getComposerLock()->getPath() + && null === $composerFiles->getInstalledJson()->getPath() + ) + || ( + null !== $composerFiles->getComposerLock()->getPath() + && null !== $composerFiles->getInstalledJson()->getPath() + ) + || ( + null === $composerFiles->getComposerLock()->getPath() + && null !== $composerFiles->getInstalledJson()->getPath() + && [] === $composerFiles->getInstalledJson()->getDecodedContents() + ) + ) + ); + + if ($canDumpAutoload) { + self::checkIfDefaultValue($logger, $raw, self::DUMP_AUTOLOAD_KEY, true); + } + + if (false === property_exists($raw, self::DUMP_AUTOLOAD_KEY)) { + return $canDumpAutoload; + } + + $dumpAutoload = $raw->{self::DUMP_AUTOLOAD_KEY} ?? true; + + if (false === $canDumpAutoload && $dumpAutoload) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but has been ignored because the composer.json, composer.lock' + .' and vendor/composer/installed.json files are necessary but could not be found.', + self::DUMP_AUTOLOAD_KEY, + ), + ); + + return false; + } + + return $canDumpAutoload && false !== $dumpAutoload; + } + + private static function retrieveExcludeDevFiles(stdClass $raw, bool $dumpAutoload, ConfigurationLogger $logger): bool + { + self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_DEV_FILES_KEY, $dumpAutoload); + + if (false === property_exists($raw, self::EXCLUDE_DEV_FILES_KEY)) { + return $dumpAutoload; + } + + $excludeDevFiles = $raw->{self::EXCLUDE_DEV_FILES_KEY} ?? $dumpAutoload; + + if (true === $excludeDevFiles && false === $dumpAutoload) { + $logger->addWarning(sprintf( + 'The "%s" setting has been set but has been ignored because the Composer autoloader is not dumped', + self::EXCLUDE_DEV_FILES_KEY, + )); + + return false; + } + + return $excludeDevFiles; + } + + private static function retrieveExcludeComposerFiles(stdClass $raw, ConfigurationLogger $logger): bool + { + self::checkIfDefaultValue($logger, $raw, self::EXCLUDE_COMPOSER_FILES_KEY, true); + + return $raw->{self::EXCLUDE_COMPOSER_FILES_KEY} ?? true; + } + + private static function retrieveCompactors(stdClass $raw, string $basePath, ConfigurationLogger $logger): Compactors + { + self::checkIfDefaultValue($logger, $raw, self::COMPACTORS_KEY, []); + + $compactorClasses = array_unique((array) ($raw->{self::COMPACTORS_KEY} ?? [])); + + // Needs to do this check before returning the compactors in order to properly inform the users about + // possible misconfiguration + $ignoredAnnotations = self::retrievePhpCompactorIgnoredAnnotations($raw, $compactorClasses, $logger); + + if (false === isset($raw->{self::COMPACTORS_KEY})) { + return new Compactors(); + } + + $compactors = new Compactors( + ...self::createCompactors( + $raw, + $basePath, + $compactorClasses, + $ignoredAnnotations, + $logger, + ), + ); + + self::checkCompactorsOrder($logger, $compactors); + + return $compactors; + } + + /** + * @param string[] $compactorClasses + * @param string[]|null $ignoredAnnotations + * + * @return Compactor[] + */ + private static function createCompactors( + stdClass $raw, + string $basePath, + array $compactorClasses, + ?array $ignoredAnnotations, + ConfigurationLogger $logger, + ): array { + return array_map( + static function (string $class) use ($raw, $basePath, $logger, $ignoredAnnotations): Compactor { + Assert::classExists($class, 'The compactor class %s does not exist.'); + Assert::isAOf($class, Compactor::class, sprintf('The class "%s" is not a compactor class.', $class)); + + if (in_array($class, [PhpCompactor::class, 'KevinGH\Box\Compactor\Php'], true)) { + return self::createPhpCompactor($ignoredAnnotations); + } + + if (in_array($class, [PhpScoperCompactor::class, 'KevinGH\Box\Compactor\PhpScoper'], true)) { + return self::createPhpScoperCompactor($raw, $basePath, $logger); + } + + return new $class(); + }, + $compactorClasses, + ); + } + + private static function checkCompactorsOrder(ConfigurationLogger $logger, Compactors $compactors): void + { + $scoperCompactor = false; + + foreach ($compactors->toArray() as $compactor) { + if ($compactor instanceof PhpScoperCompactor) { + $scoperCompactor = true; + } + + if ($compactor instanceof PhpCompactor) { + if (true === $scoperCompactor) { + $logger->addRecommendation( + 'The PHP compactor has been registered after the PhpScoper compactor. It is ' + .'recommended to register the PHP compactor before for a clearer code and faster processing.', + ); + } + + break; + } + } + } + + private static function retrieveCompressionAlgorithm(stdClass $raw, ConfigurationLogger $logger): CompressionAlgorithm + { + self::checkIfDefaultValue($logger, $raw, self::COMPRESSION_KEY, 'NONE'); + + if (false === isset($raw->{self::COMPRESSION_KEY})) { + return CompressionAlgorithm::NONE; + } + + $knownAlgorithms = CompressionAlgorithm::getLabels(); + + Assert::nullOrInArray( + $raw->{self::COMPRESSION_KEY}, + $knownAlgorithms, + sprintf( + 'Unknown compression algorithm %%s. Expected one of "%s".', + implode('", "', $knownAlgorithms), + ), + ); + + return CompressionAlgorithm::fromLabel($raw->{self::COMPRESSION_KEY}); + } + + private static function retrieveFileMode(stdClass $raw, ConfigurationLogger $logger): ?int + { + if (property_exists($raw, self::CHMOD_KEY) && null === $raw->{self::CHMOD_KEY}) { + self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY); + } + + $defaultChmod = intval(0o755, 8); + + if (isset($raw->{self::CHMOD_KEY})) { + $chmod = intval($raw->{self::CHMOD_KEY}, 8); + + if ($defaultChmod === $chmod) { + self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY); + } + + return $chmod; + } + + return $defaultChmod; + } + + private static function retrieveMainScriptPath( + stdClass $raw, + string $basePath, + ?array $decodedJsonContents, + ConfigurationLogger $logger, + ): ?string { + $firstBin = false; + + if (null !== $decodedJsonContents && array_key_exists('bin', $decodedJsonContents)) { + /** @var false|string $firstBin */ + $firstBin = current((array) $decodedJsonContents['bin']); + + if (false !== $firstBin) { + $firstBin = self::normalizePath($firstBin, $basePath); + } + } + + if (isset($raw->{self::MAIN_KEY})) { + $main = $raw->{self::MAIN_KEY}; + + if (is_string($main)) { + $main = self::normalizePath($main, $basePath); + + if ($main === $firstBin) { + $logger->addRecommendation( + sprintf( + 'The "%s" setting can be omitted since is set to its default value', + self::MAIN_KEY, + ), + ); + } + } + } else { + $main = false !== $firstBin ? $firstBin : self::normalizePath(self::DEFAULT_MAIN_SCRIPT, $basePath); + } + + if (is_bool($main)) { + Assert::false( + $main, + 'Cannot "enable" a main script: either disable it with `false` or give the main script file path.', + ); + + return null; + } + + Assert::file($main); + + return $main; + } + + private static function retrieveMainScriptContents(?string $mainScriptPath): ?string + { + if (null === $mainScriptPath) { + return null; + } + + $contents = FS::getFileContents($mainScriptPath); + + // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real + // PHAR entry point file. + // If one needs the shebang, then the main file should act as the stub and be registered as such and in which + // case the main script can be ignored or disabled. + return preg_replace('/^#!.*\s*/', '', $contents); + } + + private static function retrieveComposerFiles(string $basePath): ComposerFiles + { + $retrieveFileAndContents = static function (string $file): ?ComposerFile { + $json = new Json(); + + if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) { + return ComposerFile::createEmpty(); + } + + try { + $contents = (array) $json->decodeFile($file, true); + } catch (ParsingException $exception) { + throw new InvalidArgumentException( + sprintf( + 'Expected the file "%s" to be a valid composer.json file but an error has been found: %s', + $file, + $exception->getMessage(), + ), + 0, + $exception, + ); + } + + return new ComposerFile($file, $contents); + }; + + return new ComposerFiles( + $retrieveFileAndContents(Path::canonicalize($basePath.'/composer.json')), + $retrieveFileAndContents(Path::canonicalize($basePath.'/composer.lock')), + $retrieveFileAndContents(Path::canonicalize($basePath.'/vendor/composer/installed.json')), + ); + } + + /** + * @return string[][] + */ + private static function retrieveMap(stdClass $raw, ConfigurationLogger $logger): array + { + self::checkIfDefaultValue($logger, $raw, self::MAP_KEY, []); + + if (false === isset($raw->{self::MAP_KEY})) { + return []; + } + + $map = []; + + foreach ((array) $raw->{self::MAP_KEY} as $item) { + $processed = []; + + foreach ($item as $match => $replace) { + $processed[Path::canonicalize(trim($match))] = Path::canonicalize(trim($replace)); + } + + if (isset($processed['_empty_'])) { + $processed[''] = $processed['_empty_']; + + unset($processed['_empty_']); + } + + $map[] = $processed; + } + + return $map; + } + + private static function retrieveMetadata(stdClass $raw, ConfigurationLogger $logger) + { + self::checkIfDefaultValue($logger, $raw, self::METADATA_KEY); + + if (false === isset($raw->{self::METADATA_KEY})) { + return null; + } + + $logger->addWarning('Using the "metadata" setting is deprecated and will be removed in 5.0.0.'); + + $metadata = $raw->{self::METADATA_KEY}; + + return is_object($metadata) ? (array) $metadata : $metadata; + } + + /** + * @return string[] The first element is the temporary output path and the second the final one + */ + private static function retrieveOutputPath( + stdClass $raw, + string $basePath, + ?string $mainScriptPath, + ConfigurationLogger $logger, + ): array { + $defaultPath = null; + + if (null !== $mainScriptPath + && 1 === preg_match('/^(?
.*?)(?:\.[\p{L}\d]+)?$/u', $mainScriptPath, $matches) + ) { + $defaultPath = $matches['main'].'.phar'; + } + + if (isset($raw->{self::OUTPUT_KEY})) { + $path = self::normalizePath($raw->{self::OUTPUT_KEY}, $basePath); + + if ($path === $defaultPath) { + self::addRecommendationForDefaultValue($logger, self::OUTPUT_KEY); + } + } elseif (null !== $defaultPath) { + $path = $defaultPath; + } else { + // Last resort, should not happen + $path = self::normalizePath(self::DEFAULT_OUTPUT_FALLBACK, $basePath); + } + + $tmp = $real = $path; + + if (!str_ends_with($real, '.phar')) { + $tmp .= '.phar'; + } + + return [$tmp, $real]; + } + + private static function retrievePrivateKeyPath( + stdClass $raw, + string $basePath, + SigningAlgorithm $signingAlgorithm, + ConfigurationLogger $logger, + ): ?string { + if (property_exists($raw, self::KEY_KEY) && SigningAlgorithm::OPENSSL !== $signingAlgorithm) { + if (null === $raw->{self::KEY_KEY}) { + $logger->addRecommendation( + 'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".', + ); + } else { + $logger->addWarning( + 'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".', + ); + } + + return null; + } + + if (!isset($raw->{self::KEY_KEY})) { + Assert::true( + SigningAlgorithm::OPENSSL !== $signingAlgorithm, + 'Expected to have a private key for OpenSSL signing but none have been provided.', + ); + + return null; + } + + $path = self::normalizePath($raw->{self::KEY_KEY}, $basePath); + + Assert::file($path); + + return $path; + } + + private static function retrievePrivateKeyPassphrase( + stdClass $raw, + SigningAlgorithm $algorithm, + ConfigurationLogger $logger, + ): ?string { + self::checkIfDefaultValue($logger, $raw, self::KEY_PASS_KEY); + + if (false === property_exists($raw, self::KEY_PASS_KEY)) { + return null; + } + + /** @var null|false|string $keyPass */ + $keyPass = $raw->{self::KEY_PASS_KEY}; + + if (SigningAlgorithm::OPENSSL !== $algorithm) { + if (false === $keyPass || null === $keyPass) { + $logger->addRecommendation( + sprintf( + 'The setting "%s" has been set but is unnecessary since the signing algorithm is ' + .'not "OPENSSL".', + self::KEY_PASS_KEY, + ), + ); + } else { + $logger->addWarning( + sprintf( + 'The setting "%s" has been set but ignored the signing algorithm is not "OPENSSL".', + self::KEY_PASS_KEY, + ), + ); + } + + return null; + } + + return is_string($keyPass) ? $keyPass : null; + } + + /** + * @return scalar[] + */ + private static function retrieveReplacements( + stdClass $raw, + ?string $file, + string $path, + ConfigurationLogger $logger, + ): array { + self::checkIfDefaultValue($logger, $raw, self::REPLACEMENTS_KEY, new stdClass()); + + if (null === $file) { + return []; + } + + $replacements = isset($raw->{self::REPLACEMENTS_KEY}) ? (array) $raw->{self::REPLACEMENTS_KEY} : []; + + if (null !== ($git = self::retrievePrettyGitPlaceholder($raw, $logger))) { + $replacements[$git] = self::retrievePrettyGitTag($path); + } + + if (null !== ($git = self::retrieveGitHashPlaceholder($raw, $logger))) { + $replacements[$git] = self::retrieveGitHash($path); + } + + if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw, $logger))) { + $replacements[$git] = self::retrieveGitHash($path, true); + } + + if (null !== ($git = self::retrieveGitTagPlaceholder($raw, $logger))) { + $replacements[$git] = self::retrieveGitTag($path); + } + + if (null !== ($git = self::retrieveGitVersionPlaceholder($raw, $logger))) { + $replacements[$git] = self::retrieveGitVersion($path); + } + + /** + * @var string $datetimeFormat + * @var bool $valueSetByUser + */ + [$datetimeFormat, $valueSetByUser] = self::retrieveDatetimeFormat($raw, $logger); + + if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw, $logger))) { + $replacements[$date] = self::retrieveDatetimeNow($datetimeFormat); + } elseif ($valueSetByUser) { + $logger->addRecommendation( + sprintf( + 'The setting "%s" has been set but is unnecessary because the setting "%s" is not set.', + self::DATETIME_FORMAT_KEY, + self::DATETIME_KEY, + ), + ); + } + + $sigil = self::retrieveReplacementSigil($raw, $logger); + + foreach ($replacements as $key => $value) { + unset($replacements[$key]); + $replacements[$sigil.$key.$sigil] = $value; + } + + return $replacements; + } + + private static function retrieveTimestamp( + stdClass $raw, + SigningAlgorithm $signingAlgorithm, + ConfigurationLogger $logger, + ): ?DateTimeImmutable { + self::checkIfDefaultValue($logger, $raw, self::TIMESTAMP); + + $timestamp = $raw->{self::TIMESTAMP} ?? null; + + if (null === $timestamp) { + return null; + } + + if (SigningAlgorithm::OPENSSL === $signingAlgorithm) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but has been ignored since an OpenSSL signature has been configured (setting "%s").', + self::TIMESTAMP, + self::ALGORITHM_KEY, + ), + ); + + return null; + } + + return new DateTimeImmutable( + $timestamp, + new DateTimeZone('UTC'), + ); + } + + private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string + { + return self::retrievePlaceholder($raw, $logger, self::GIT_KEY); + } + + private static function retrieveGitHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string + { + return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_KEY); + } + + /** + * @param bool $short Use the short version + * + * @return string the commit hash + */ + private static function retrieveGitHash(string $path, bool $short = false): string + { + return self::runGitCommand( + sprintf( + 'git log --pretty="%s" -n1 HEAD', + $short ? '%h' : '%H', + ), + $path, + ); + } + + private static function retrieveGitShortHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string + { + return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_SHORT_KEY); + } + + private static function retrieveGitTagPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string + { + return self::retrievePlaceholder($raw, $logger, self::GIT_TAG_KEY); + } + + private static function retrievePlaceholder(stdClass $raw, ConfigurationLogger $logger, string $key): ?string + { + self::checkIfDefaultValue($logger, $raw, $key); + + return $raw->{$key} ?? null; + } + + private static function retrieveGitTag(string $path): string + { + return self::runGitCommand('git describe --tags HEAD', $path); + } + + private static function retrievePrettyGitTag(string $path): string + { + $version = self::retrieveGitTag($path); + + if (preg_match('/^(?.+)-\d+-g(?[a-f0-9]{7})$/', $version, $matches)) { + return sprintf('%s@%s', $matches['tag'], $matches['hash']); + } + + return $version; + } + + private static function retrieveGitVersionPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string + { + return self::retrievePlaceholder($raw, $logger, self::GIT_VERSION_KEY); + } + + private static function retrieveGitVersion(string $path): ?string + { + try { + return self::retrieveGitTag($path); + } catch (RuntimeException $exception) { + try { + return self::retrieveGitHash($path, true); + } catch (RuntimeException $exception) { + throw new RuntimeException( + sprintf( + 'The tag or commit hash could not be retrieved from "%s": %s', + $path, + $exception->getMessage(), + ), + 0, + $exception, + ); + } + } + } + + private static function retrieveDatetimeNowPlaceHolder(stdClass $raw, ConfigurationLogger $logger): ?string + { + return self::retrievePlaceholder($raw, $logger, self::DATETIME_KEY); + } + + private static function retrieveDatetimeNow(string $format): string + { + return (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format($format); + } + + private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): array + { + self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DEFAULT_DATETIME_FORMAT); + self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DATETIME_FORMAT_DEPRECATED_KEY); + + if (isset($raw->{self::DATETIME_FORMAT_KEY})) { + $format = $raw->{self::DATETIME_FORMAT_KEY}; + } elseif (isset($raw->{self::DATETIME_FORMAT_DEPRECATED_KEY})) { + @trigger_error( + sprintf( + 'The "%s" is deprecated, use "%s" setting instead.', + self::DATETIME_FORMAT_DEPRECATED_KEY, + self::DATETIME_FORMAT_KEY, + ), + E_USER_DEPRECATED, + ); + $logger->addWarning( + sprintf( + 'The "%s" is deprecated, use "%s" setting instead.', + self::DATETIME_FORMAT_DEPRECATED_KEY, + self::DATETIME_FORMAT_KEY, + ), + ); + + $format = $raw->{self::DATETIME_FORMAT_DEPRECATED_KEY}; + } else { + $format = null; + } + + if (null !== $format) { + $formattedDate = (new DateTimeImmutable())->format($format); + + Assert::false( + false === $formattedDate || $formattedDate === $format, + sprintf( + 'Expected the datetime format to be a valid format: "%s" is not', + $format, + ), + ); + + return [$format, true]; + } + + return [self::DEFAULT_DATETIME_FORMAT, false]; + } + + private static function retrieveReplacementSigil(stdClass $raw, ConfigurationLogger $logger): string + { + return self::retrievePlaceholder($raw, $logger, self::REPLACEMENT_SIGIL_KEY) ?? self::DEFAULT_REPLACEMENT_SIGIL; + } + + /** + * @return null|non-empty-string + */ + private static function retrieveShebang(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string + { + self::checkIfDefaultValue($logger, $raw, self::SHEBANG_KEY, self::DEFAULT_SHEBANG); + + if (false === isset($raw->{self::SHEBANG_KEY})) { + return self::DEFAULT_SHEBANG; + } + + $shebang = $raw->{self::SHEBANG_KEY}; + + if (false === $shebang) { + if (false === $stubIsGenerated) { + $logger->addRecommendation( + sprintf( + 'The "%s" has been set to `false` but is unnecessary since the Box built-in stub is not' + .' being used', + self::SHEBANG_KEY, + ), + ); + } + + return null; + } + + Assert::string($shebang, 'Expected shebang to be either a string, false or null, found true'); + + $shebang = trim($shebang); + + Assert::notEmpty($shebang, 'The shebang should not be empty.'); + Assert::true( + str_starts_with($shebang, '#!'), + sprintf( + 'The shebang line must start with "#!". Got "%s" instead', + $shebang, + ), + ); + + if (false === $stubIsGenerated) { + $logger->addWarning( + sprintf( + 'The "%s" has been set but ignored since it is used only with the Box built-in stub which is not' + .' used', + self::SHEBANG_KEY, + ), + ); + } + + return $shebang; + } + + private static function retrieveSigningAlgorithm(stdClass $raw, ConfigurationLogger $logger): SigningAlgorithm + { + if (property_exists($raw, self::ALGORITHM_KEY) && null === $raw->{self::ALGORITHM_KEY}) { + self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY); + } + + if (false === isset($raw->{self::ALGORITHM_KEY})) { + return self::DEFAULT_SIGNING_ALGORITHM; + } + + $algorithmLabel = mb_strtoupper($raw->{self::ALGORITHM_KEY}); + $algorithm = SigningAlgorithm::fromLabel($algorithmLabel); + + if (self::DEFAULT_SIGNING_ALGORITHM === $algorithm) { + self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY); + } + + if (SigningAlgorithm::OPENSSL === $algorithm) { + $logger->addWarning( + 'Using an OpenSSL signature is deprecated and will be removed in 5.0.0. Please check ' + .'https://github.com/box-project/box/blob/main/doc/phar-signing.md for alternatives.', + ); + } + + return $algorithm; + } + + private static function retrieveStubBannerContents(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string + { + self::checkIfDefaultValue($logger, $raw, self::BANNER_KEY, self::getDefaultBanner()); + + if (false === isset($raw->{self::BANNER_KEY})) { + return self::getDefaultBanner(); + } + + $banner = $raw->{self::BANNER_KEY}; + + if (false === $banner) { + if (false === $stubIsGenerated) { + $logger->addRecommendation( + sprintf( + 'The "%s" setting has been set but is unnecessary since the Box built-in stub is not ' + .'being used', + self::BANNER_KEY, + ), + ); + } + + return null; + } + + Assert::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value'); + + if (is_array($banner)) { + $banner = implode("\n", $banner); + } + + if (false === $stubIsGenerated) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but is ignored since the Box built-in stub is not being used', + self::BANNER_KEY, + ), + ); + } + + return $banner; + } + + private static function getDefaultBanner(): string + { + return sprintf(self::DEFAULT_BANNER, get_box_version()); + } + + private static function retrieveStubBannerPath( + stdClass $raw, + string $basePath, + bool $stubIsGenerated, + ConfigurationLogger $logger, + ): ?string { + self::checkIfDefaultValue($logger, $raw, self::BANNER_FILE_KEY); + + if (false === isset($raw->{self::BANNER_FILE_KEY})) { + return null; + } + + $bannerFile = Path::makeAbsolute($raw->{self::BANNER_FILE_KEY}, $basePath); + + Assert::file($bannerFile); + + if (false === $stubIsGenerated) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but is ignored since the Box built-in stub is not being used', + self::BANNER_FILE_KEY, + ), + ); + } + + return $bannerFile; + } + + private static function normalizeStubBannerContents(?string $contents): ?string + { + if (null === $contents) { + return null; + } + + $banner = explode("\n", $contents); + $banner = array_map('trim', $banner); + + return implode("\n", $banner); + } + + private static function retrieveStubPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string + { + self::checkIfDefaultValue($logger, $raw, self::STUB_KEY); + + if (isset($raw->{self::STUB_KEY}) && is_string($raw->{self::STUB_KEY})) { + $stubPath = Path::makeAbsolute($raw->{self::STUB_KEY}, $basePath); + + Assert::file($stubPath); + + return $stubPath; + } + + return null; + } + + private static function retrieveInterceptsFileFunctions( + stdClass $raw, + bool $stubIsGenerated, + ConfigurationLogger $logger, + ): bool { + self::checkIfDefaultValue($logger, $raw, self::INTERCEPT_KEY, false); + + if (false === isset($raw->{self::INTERCEPT_KEY})) { + return false; + } + + $intercept = $raw->{self::INTERCEPT_KEY}; + + if ($intercept && false === $stubIsGenerated) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but is ignored since the Box built-in stub is not being used', + self::INTERCEPT_KEY, + ), + ); + } + + return $intercept; + } + + private static function retrievePromptForPrivateKey( + stdClass $raw, + SigningAlgorithm $signingAlgorithm, + ConfigurationLogger $logger, + ): bool { + if (isset($raw->{self::KEY_PASS_KEY}) && true === $raw->{self::KEY_PASS_KEY}) { + if (SigningAlgorithm::OPENSSL !== $signingAlgorithm) { + $logger->addWarning( + 'A prompt for password for the private key has been requested but ignored since the signing ' + .'algorithm used is not "OPENSSL.', + ); + + return false; + } + + return true; + } + + return false; + } + + private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath, ConfigurationLogger $logger): bool + { + self::checkIfDefaultValue($logger, $raw, self::STUB_KEY, true); + + return null === $stubPath && (false === isset($raw->{self::STUB_KEY}) || false !== $raw->{self::STUB_KEY}); + } + + private static function retrieveCheckRequirements( + stdClass $raw, + bool $hasComposerJson, + bool $hasComposerLock, + bool $pharStubUsed, + ConfigurationLogger $logger, + ): bool { + self::checkIfDefaultValue($logger, $raw, self::CHECK_REQUIREMENTS_KEY, true); + + if (false === property_exists($raw, self::CHECK_REQUIREMENTS_KEY)) { + return $hasComposerJson || $hasComposerLock; + } + + /** @var bool $checkRequirements */ + $checkRequirements = $raw->{self::CHECK_REQUIREMENTS_KEY} ?? true; + + // TODO: in 5.0 we no longer care about the composer.json + if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) { + $logger->addWarning( + 'The requirement checker could not be used because the composer.json and composer.lock file could not ' + .'be found.', + ); + + return false; + } + + if ($checkRequirements && false === $hasComposerLock) { + // TODO: in 5.0: + // - adjust the warning + // - return false here to skip the requirement checker + $logger->addWarning( + 'Enabling the requirement checker when there is no composer.lock is deprecated. In the future the ' + .'requirement checker will be forcefully skipped in this scenario.', + ); + } + + if ($checkRequirements && $pharStubUsed) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but has been ignored since the PHAR built-in stub is being ' + .'used.', + self::CHECK_REQUIREMENTS_KEY, + ), + ); + } + + return $checkRequirements; + } + + private static function retrievePhpScoperConfig(stdClass $raw, string $basePath, ConfigurationLogger $logger): PhpScoperConfiguration + { + self::checkIfDefaultValue($logger, $raw, self::PHP_SCOPER_KEY, self::PHP_SCOPER_CONFIG); + + if (!isset($raw->{self::PHP_SCOPER_KEY})) { + $configFilePath = Path::makeAbsolute(self::PHP_SCOPER_CONFIG, $basePath); + $configFilePath = file_exists($configFilePath) ? $configFilePath : null; + + return PhpScoperConfigurationFactory::create($configFilePath); + } + + $configFile = $raw->{self::PHP_SCOPER_KEY}; + + Assert::string($configFile); + + $configFilePath = Path::makeAbsolute($configFile, $basePath); + + Assert::file($configFilePath); + Assert::readable($configFilePath); + + return PhpScoperConfigurationFactory::create($configFilePath); + } + + /** + * Runs a Git command on the repository. + * + * @return string The trimmed output from the command + */ + private static function runGitCommand(string $command, string $path): string + { + $process = Process::fromShellCommandline($command, $path); + $process->run(); + + if ($process->isSuccessful()) { + return trim($process->getOutput()); + } + + throw new RuntimeException( + sprintf( + 'The tag or commit hash could not be retrieved from "%s": %s', + $path, + $process->getErrorOutput(), + ), + 0, + new ProcessFailedException($process), + ); + } + + /** + * @param string[] $compactorClasses + * + * @return string[]|null + */ + private static function retrievePhpCompactorIgnoredAnnotations( + stdClass $raw, + array $compactorClasses, + ConfigurationLogger $logger, + ): ?array { + $hasPhpCompactor = in_array(PhpCompactor::class, $compactorClasses, true); + + self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, true); + self::checkIfDefaultValue($logger, $raw, self::ANNOTATIONS_KEY, null); + + if (false === property_exists($raw, self::ANNOTATIONS_KEY)) { + return self::DEFAULT_IGNORED_ANNOTATIONS; + } + + if (false === $hasPhpCompactor) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but is ignored since no PHP compactor has been configured', + self::ANNOTATIONS_KEY, + ), + ); + } + + /** @var null|bool|stdClass $annotations */ + $annotations = $raw->{self::ANNOTATIONS_KEY}; + + if (true === $annotations || null === $annotations) { + return self::DEFAULT_IGNORED_ANNOTATIONS; + } + + if (false === $annotations) { + return null; + } + + if (false === property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY)) { + $logger->addWarning( + sprintf( + 'The "%s" setting has been set but no "%s" setting has been found, hence "%s" is treated as' + .' if it is set to `false`', + self::ANNOTATIONS_KEY, + self::IGNORED_ANNOTATIONS_KEY, + self::ANNOTATIONS_KEY, + ), + ); + + return null; + } + + $ignored = []; + + if (property_exists($annotations, self::IGNORED_ANNOTATIONS_KEY) + && in_array($ignored = $annotations->{self::IGNORED_ANNOTATIONS_KEY}, [null, []], true) + ) { + self::addRecommendationForDefaultValue($logger, self::ANNOTATIONS_KEY.'#'.self::IGNORED_ANNOTATIONS_KEY); + + return (array) $ignored; + } + + return $ignored; + } + + private static function createPhpCompactor(?array $ignoredAnnotations): Compactor + { + if (null === $ignoredAnnotations) { + return new PhpCompactor(null); + } + + $ignoredAnnotations = array_values( + array_filter( + array_map( + static fn (string $annotation): ?string => mb_strtolower(trim($annotation)), + $ignoredAnnotations, + ), + ), + ); + + return PhpCompactor::create($ignoredAnnotations); + } + + private static function createPhpScoperCompactor( + stdClass $raw, + string $basePath, + ConfigurationLogger $logger, + ): Compactor { + $phpScoperConfig = self::configurePhpScoperPrefix( + self::retrievePhpScoperConfig($raw, $basePath, $logger), + ); + + $excludedFilePaths = array_values( + array_unique( + array_map( + static fn (string $path): string => Path::makeRelative($path, $basePath), + array_keys( + $phpScoperConfig->getExcludedFilesWithContents(), + ), + ), + ), + ); + + return new PhpScoperCompactor( + new SerializableScoper($phpScoperConfig, ...$excludedFilePaths), + ); + } + + private static function configurePhpScoperPrefix(PhpScoperConfiguration $phpScoperConfig): PhpScoperConfiguration + { + $prefix = $phpScoperConfig->getPrefix(); + if (!str_starts_with($prefix, '_PhpScoper')) { + return $phpScoperConfig; + } + + return $phpScoperConfig->withPrefix(unique_id('_HumbugBox')); + } + + private static function checkIfDefaultValue( + ConfigurationLogger $logger, + stdClass $raw, + string $key, + $defaultValue = null, + ): void { + if (false === property_exists($raw, $key)) { + return; + } + + $value = $raw->{$key}; + + if (null === $value + || (false === is_object($defaultValue) && $defaultValue === $value) + || (is_object($defaultValue) && $defaultValue == $value) + ) { + $logger->addRecommendation( + sprintf( + 'The "%s" setting can be omitted since is set to its default value', + $key, + ), + ); + } + } + + private static function addRecommendationForDefaultValue(ConfigurationLogger $logger, string $key): void + { + $logger->addRecommendation( + sprintf( + 'The "%s" setting can be omitted since is set to its default value', + $key, + ), + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Configuration/ConfigurationLoader.php b/fixtures/bench/without-compactors/src/Configuration/ConfigurationLoader.php new file mode 100644 index 000000000..81405c65d --- /dev/null +++ b/fixtures/bench/without-compactors/src/Configuration/ConfigurationLoader.php @@ -0,0 +1,50 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Configuration; + +use KevinGH\Box\Json\Json; +use stdClass; + +/** + * @private + */ +final class ConfigurationLoader +{ + private const SCHEMA_FILE = __DIR__.'/../../res/schema.json'; + + public function __construct(private readonly Json $json = new Json()) + { + } + + /** + * @param null|non-empty-string $file + */ + public function loadFile(?string $file): Configuration + { + if (null === $file) { + return Configuration::create(null, new stdClass()); + } + + $json = $this->json->decodeFile($file); + + $this->json->validate( + $file, + $json, + $this->json->decodeFile(self::SCHEMA_FILE), + ); + + return Configuration::create($file, $json); + } +} diff --git a/fixtures/bench/without-compactors/src/Configuration/ConfigurationLogger.php b/fixtures/bench/without-compactors/src/Configuration/ConfigurationLogger.php new file mode 100644 index 000000000..d416faf32 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Configuration/ConfigurationLogger.php @@ -0,0 +1,69 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Configuration; + +use Webmozart\Assert\Assert; +use function array_keys; +use function trim; + +/** + * @private + */ +final class ConfigurationLogger +{ + /** + * @var array + */ + private array $recommendations = []; + + /** + * @var array + */ + private array $warnings = []; + + public function addRecommendation(string $message): void + { + $message = trim($message); + + Assert::false('' === $message, 'Expected to have a message but a blank string was given instead.'); + + $this->recommendations[$message] = true; + } + + /** + * @return list + */ + public function getRecommendations(): array + { + return array_keys($this->recommendations); + } + + public function addWarning(string $message): void + { + $message = trim($message); + + Assert::false('' === $message, 'Expected to have a message but a blank string was given instead.'); + + $this->warnings[$message] = true; + } + + /** + * @return list + */ + public function getWarnings(): array + { + return array_keys($this->warnings); + } +} diff --git a/fixtures/bench/without-compactors/src/Configuration/ExportableConfiguration.php b/fixtures/bench/without-compactors/src/Configuration/ExportableConfiguration.php new file mode 100644 index 000000000..2bc2ff595 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Configuration/ExportableConfiguration.php @@ -0,0 +1,173 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Configuration; + +use Closure; +use KevinGH\Box\Compactor\Compactors; +use KevinGH\Box\Composer\ComposerFile; +use KevinGH\Box\MapFile; +use SplFileInfo; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\Finder\SplFileInfo as SymfonySplFileInfo; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use function array_map; +use function iter\values; +use function sort; +use const SORT_STRING; + +/** + * A class similar to {@see Configuration} but for which the property types and values might change in order to improve + * its readability when dumping it into a file. + * + * @internal + */ +final class ExportableConfiguration +{ + public static function create(Configuration $configuration): self + { + $normalizePath = self::createPathNormalizer($configuration->getBasePath()); + $normalizePaths = static function (array $files) use ($normalizePath): array { + $files = array_map($normalizePath, $files); + sort($files, SORT_STRING); + + return $files; + }; + + $composerJson = $configuration->getComposerJson(); + $composerLock = $configuration->getComposerLock(); + + return new self( + $normalizePath($configuration->getConfigurationFile()), + $configuration->getAlias(), + $configuration->getBasePath(), + new ComposerFile( + $normalizePath($composerJson), + $configuration->getDecodedComposerJsonContents() ?? [], + ), + new ComposerFile( + $normalizePath($composerLock), + $configuration->getDecodedComposerLockContents() ?? [], + ), + $normalizePaths($configuration->getFiles()), + $normalizePaths($configuration->getBinaryFiles()), + $configuration->hasAutodiscoveredFiles(), + $configuration->dumpAutoload(), + $configuration->excludeComposerFiles(), + $configuration->excludeDevFiles(), + array_map('get_class', $configuration->getCompactors()->toArray()), + $configuration->getCompressionAlgorithm()->name, + '0'.decoct($configuration->getFileMode()), + $normalizePath($configuration->getMainScriptPath()), + $configuration->getMainScriptContents(), + $configuration->getFileMapper(), + $configuration->getMetadata(), + $normalizePath($configuration->getTmpOutputPath()), + $normalizePath($configuration->getOutputPath()), + // TODO: remove this from the dump & add the SensitiveParam annotation + $configuration->getPrivateKeyPassphrase(), + $normalizePath($configuration->getPrivateKeyPath()), + $configuration->promptForPrivateKey(), + $configuration->getReplacements(), + $configuration->getShebang(), + $configuration->getSigningAlgorithm()->name, + $configuration->getStubBannerContents(), + $normalizePath($configuration->getStubBannerPath()), + $normalizePath($configuration->getStubPath()), + $configuration->isInterceptFileFuncs(), + $configuration->isStubGenerated(), + $configuration->checkRequirements(), + $configuration->getWarnings(), + $configuration->getRecommendations(), + ); + } + + /** + * @return Closure(null|SplFileInfo|string): string|null + */ + private static function createPathNormalizer(string $basePath): Closure + { + return static function (null|SplFileInfo|string $path) use ($basePath): ?string { + if (null === $path) { + return null; + } + + if ($path instanceof SplFileInfo) { + $path = $path->getPathname(); + } + + return Path::makeRelative($path, $basePath); + }; + } + + /** @noinspection PhpPropertyOnlyWrittenInspection */ + private function __construct( + private readonly ?string $file, + private readonly string $alias, + private readonly string $basePath, + private readonly ComposerFile $composerJson, + private readonly ComposerFile $composerLock, + private readonly array $files, + private readonly array $binaryFiles, + private readonly bool $autodiscoveredFiles, + private readonly bool $dumpAutoload, + private readonly bool $excludeComposerFiles, + private readonly bool $excludeDevFiles, + private readonly Compactors|array $compactors, + private readonly string $compressionAlgorithm, + private readonly int|string|null $fileMode, + private readonly ?string $mainScriptPath, + private readonly ?string $mainScriptContents, + private readonly MapFile $fileMapper, + private readonly mixed $metadata, + private readonly string $tmpOutputPath, + private readonly string $outputPath, + private readonly ?string $privateKeyPassphrase, + private readonly ?string $privateKeyPath, + private readonly bool $promptForPrivateKey, + private readonly array $processedReplacements, + private readonly ?string $shebang, + private readonly string $signingAlgorithm, + private readonly ?string $stubBannerContents, + private readonly ?string $stubBannerPath, + private readonly ?string $stubPath, + private readonly bool $isInterceptFileFuncs, + private readonly bool $isStubGenerated, + private readonly bool $checkRequirements, + private readonly array $warnings, + private readonly array $recommendations, + ) { + } + + public function export(): string + { + $cloner = new VarCloner(); + $cloner->setMaxItems(-1); + $cloner->setMaxString(-1); + + $normalizePath = self::createPathNormalizer($this->basePath); + $splInfoCaster = static fn (SplFileInfo $fileInfo): array => [$normalizePath($fileInfo)]; + + $cloner->addCasters([ + SplFileInfo::class => $splInfoCaster, + SymfonySplFileInfo::class => $splInfoCaster, + ]); + + return (new CliDumper())->dump( + $cloner->cloneVar($this), + true, + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Configuration/NoConfigurationFound.php b/fixtures/bench/without-compactors/src/Configuration/NoConfigurationFound.php new file mode 100644 index 000000000..b4a43771a --- /dev/null +++ b/fixtures/bench/without-compactors/src/Configuration/NoConfigurationFound.php @@ -0,0 +1,29 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Configuration; + +use RuntimeException; +use Throwable; + +/** + * @private + */ +final class NoConfigurationFound extends RuntimeException +{ + public function __construct(string $message = 'The configuration file could not be found.', int $code = 0, ?Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Application.php b/fixtures/bench/without-compactors/src/Console/Application.php new file mode 100644 index 000000000..47d6e209b --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Application.php @@ -0,0 +1,109 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Fidry\Console\Application\Application as FidryApplication; +use function KevinGH\Box\get_box_version; +use function sprintf; +use function trim; + +/** + * @private + */ +final class Application implements FidryApplication +{ + private string $version; + private string $releaseDate; + private string $header; + + public function __construct( + private string $name = 'Box', + ?string $version = null, + string $releaseDate = '@release-date@', + private bool $autoExit = true, + private bool $catchExceptions = true, + ) { + $this->version = $version ?? get_box_version(); + $this->releaseDate = !str_contains($releaseDate, '@') ? $releaseDate : ''; + } + + public function getName(): string + { + return $this->name; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getLongVersion(): string + { + return trim( + sprintf( + '%s version %s %s', + $this->getName(), + $this->getVersion(), + $this->releaseDate, + ), + ); + } + + public function getHelp(): string + { + return $this->getHeader(); + } + + public function getHeader(): string + { + if (!isset($this->header)) { + $this->header = Logo::LOGO_ASCII.$this->getLongVersion(); + } + + return $this->header; + } + + public function getCommands(): array + { + return [ + new Command\Composer\ComposerCheckVersion(), + new Command\Composer\ComposerVendorDir(), + new Command\Compile($this->getHeader()), + new Command\Diff(), + new Command\Info(), + new Command\Process(), + new Command\Extract(), + new Command\Validate(), + new Command\Verify(), + new Command\GenerateDockerFile(), + new Command\Namespace_(), + ]; + } + + public function getDefaultCommand(): string + { + return 'list'; + } + + public function isAutoExitEnabled(): bool + { + return $this->autoExit; + } + + public function areExceptionsCaught(): bool + { + return $this->catchExceptions; + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/ChangeWorkingDirOption.php b/fixtures/bench/without-compactors/src/Console/Command/ChangeWorkingDirOption.php new file mode 100644 index 000000000..ac676582a --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/ChangeWorkingDirOption.php @@ -0,0 +1,67 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\IO; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputOption; +use Webmozart\Assert\Assert; +use function chdir; +use function getcwd; +use function sprintf; + +/** + * @private + */ +final class ChangeWorkingDirOption +{ + /** @internal using a static property as traits cannot have constants */ + private const WORKING_DIR_OPT = 'working-dir'; + + public static function getOptionInput(): InputOption + { + return new InputOption( + self::WORKING_DIR_OPT, + 'd', + InputOption::VALUE_REQUIRED, + 'If specified, use the given directory as working directory.', + null, + ); + } + + public static function changeWorkingDirectory(IO $io): void + { + $workingDir = $io->getTypedOption(self::WORKING_DIR_OPT)->asNullableNonEmptyString(); + + if (null === $workingDir) { + return; + } + + Assert::directory( + $workingDir, + 'Could not change the working directory to "%s": directory does not exists or file is not a directory.', + ); + + if (false === chdir($workingDir)) { + throw new RuntimeException( + sprintf( + 'Failed to change the working directory to "%s" from "%s".', + $workingDir, + getcwd(), + ), + ); + } + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/Compile.php b/fixtures/bench/without-compactors/src/Console/Command/Compile.php new file mode 100644 index 000000000..11c817eb9 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/Compile.php @@ -0,0 +1,993 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Amp\MultiReasonException; +use DateTimeImmutable; +use DateTimeInterface; +use Fidry\Console\Command\Command; +use Fidry\Console\Command\CommandAware; +use Fidry\Console\Command\CommandAwareness; +use Fidry\Console\Command\Configuration as CommandConfiguration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use Fidry\FileSystem\FileSystem; +use Fidry\FileSystem\FS; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\Amp\FailureCollector; +use KevinGH\Box\Box; +use KevinGH\Box\Compactor\Compactor; +use KevinGH\Box\Composer\CompilerPsrLogger; +use KevinGH\Box\Composer\ComposerConfiguration; +use KevinGH\Box\Composer\ComposerOrchestrator; +use KevinGH\Box\Composer\ComposerProcessFactory; +use KevinGH\Box\Composer\IncompatibleComposerVersion; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\Console\Logger\CompilerLogger; +use KevinGH\Box\Console\MessageRenderer; +use KevinGH\Box\Console\OpenFileDescriptorLimiter; +use KevinGH\Box\Console\PhpSettingsChecker; +use KevinGH\Box\Constants; +use KevinGH\Box\MapFile; +use KevinGH\Box\Phar\CompressionAlgorithm; +use KevinGH\Box\Phar\SigningAlgorithm; +use KevinGH\Box\RequirementChecker\DecodedComposerJson; +use KevinGH\Box\RequirementChecker\DecodedComposerLock; +use KevinGH\Box\RequirementChecker\RequirementsDumper; +use KevinGH\Box\StubGenerator; +use RuntimeException; +use stdClass; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Filesystem\Path; +use function array_map; +use function array_shift; +use function count; +use function decoct; +use function explode; +use function file_exists; +use function filesize; +use function implode; +use function is_callable; +use function is_string; +use function KevinGH\Box\format_size; +use function KevinGH\Box\format_time; +use function memory_get_peak_usage; +use function memory_get_usage; +use function microtime; +use function putenv; +use function Safe\getcwd; +use function sprintf; +use function var_export; +use const PHP_EOL; + +/** + * @private + */ +final class Compile implements CommandAware +{ + use CommandAwareness; + + public const NAME = 'compile'; + + private const HELP = <<<'HELP' + The %command.name% command will compile code in a new PHAR based on a variety of settings. + + This command relies on a configuration file for loading + PHAR packaging settings. If a configuration file is not + specified through the --config|-c option, one of + the following files will be used (in order): box.json, + box.json.dist + + The configuration file is actually a JSON object saved to a file. For more + information check the documentation online: + + https://github.com/humbug/box + + HELP; + + private const DEBUG_OPTION = 'debug'; + private const NO_PARALLEL_PROCESSING_OPTION = 'no-parallel'; + private const NO_RESTART_OPTION = 'no-restart'; + private const DEV_OPTION = 'dev'; + private const NO_CONFIG_OPTION = 'no-config'; + private const WITH_DOCKER_OPTION = 'with-docker'; + private const COMPOSER_BIN_OPTION = 'composer-bin'; + private const ALLOW_COMPOSER_COMPOSER_CHECK_FAILURE_OPTION = 'allow-composer-check-failure'; + + private const DEBUG_DIR = '.box_dump'; + + public function __construct(private string $header) + { + } + + public function getConfiguration(): CommandConfiguration + { + return new CommandConfiguration( + self::NAME, + '🔨 Compiles an application into a PHAR', + self::HELP, + [], + [ + new InputOption( + self::DEBUG_OPTION, + null, + InputOption::VALUE_NONE, + 'Dump the files added to the PHAR in a `'.self::DEBUG_DIR.'` directory', + ), + new InputOption( + self::NO_PARALLEL_PROCESSING_OPTION, + null, + InputOption::VALUE_NONE, + 'Disable the parallel processing', + ), + new InputOption( + self::NO_RESTART_OPTION, + null, + InputOption::VALUE_NONE, + 'Do not restart the PHP process. Box restarts the process by default to disable xdebug and set `phar.readonly=0`', + ), + new InputOption( + self::DEV_OPTION, + null, + InputOption::VALUE_NONE, + 'Skips the compression step', + ), + new InputOption( + self::NO_CONFIG_OPTION, + null, + InputOption::VALUE_NONE, + 'Ignore the config file even when one is specified with the --config option', + ), + new InputOption( + self::WITH_DOCKER_OPTION, + null, + InputOption::VALUE_NONE, + 'Generates a Dockerfile', + ), + new InputOption( + self::COMPOSER_BIN_OPTION, + null, + InputOption::VALUE_REQUIRED, + 'Composer binary to use', + ), + new InputOption( + self::ALLOW_COMPOSER_COMPOSER_CHECK_FAILURE_OPTION, + null, + InputOption::VALUE_NONE, + 'To continue even if an unsupported Composer version is detected', + ), + ConfigOption::getOptionInput(), + ChangeWorkingDirOption::getOptionInput(), + ], + ); + } + + public function execute(IO $io): int + { + if ($io->getTypedOption(self::NO_RESTART_OPTION)->asBoolean()) { + putenv(Constants::ALLOW_XDEBUG.'=1'); + } + + $debug = $io->getTypedOption(self::DEBUG_OPTION)->asBoolean(); + + if ($debug) { + $io->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + } + + PhpSettingsChecker::check($io); + + $enableParallelization = $io->getTypedOption(self::NO_PARALLEL_PROCESSING_OPTION)->asBoolean(); + + if ($enableParallelization) { + $io->writeln( + '[debug] Disabled parallel processing', + OutputInterface::VERBOSITY_DEBUG, + ); + } + + ChangeWorkingDirOption::changeWorkingDirectory($io); + + $io->writeln($this->header); + + $config = $io->getTypedOption(self::NO_CONFIG_OPTION)->asBoolean() + ? Configuration::create(null, new stdClass()) + : ConfigOption::getConfig($io, true); + $config->setComposerBin(self::getComposerBin($io)); + $path = $config->getOutputPath(); + + $logger = new CompilerLogger($io); + + $startTime = microtime(true); + + $logger->logStartBuilding($path); + + $this->removeExistingArtifacts($config, $logger, $debug); + + // Adding files might result in opening a lot of files. Either because not parallelized or when creating the + // workers for parallelization. + // As a result, we bump the file descriptor to an arbitrary number to ensure this process can run correctly + $restoreLimit = OpenFileDescriptorLimiter::bumpLimit(2048, $io); + + try { + $box = $this->createPhar($config, $logger, $io, $debug, $enableParallelization); + } finally { + $restoreLimit(); + } + + self::correctPermissions($path, $config, $logger); + + self::logEndBuilding($config, $logger, $io, $box, $path, $startTime); + + if ($io->getTypedOption(self::WITH_DOCKER_OPTION)->asBoolean()) { + return $this->generateDockerFile($io); + } + + return ExitCode::SUCCESS; + } + + private function createPhar( + Configuration $config, + CompilerLogger $logger, + IO $io, + bool $enableParallelization, + bool $debug, + ): Box { + $tmpOutputPath = $config->getTmpOutputPath(); + $box = Box::create($tmpOutputPath, enableParallelization: $enableParallelization); + $composerOrchestrator = new ComposerOrchestrator( + ComposerProcessFactory::create( + $config->getComposerBin(), + $io, + ), + new CompilerPsrLogger($logger), + new FileSystem(), + ); + + self::checkComposerVersion($composerOrchestrator, $config, $logger, $io); + + $box->startBuffering(); + + self::registerReplacementValues($config, $box, $logger); + self::registerCompactors($config, $box, $logger); + self::registerFileMapping($config, $box, $logger); + + // Registering the main script _before_ adding the rest if of the files is _very_ important. The temporary + // file used for debugging purposes and the Composer dump autoloading will not work correctly otherwise. + $main = self::registerMainScript($config, $box, $logger); + + $check = self::registerRequirementsChecker($config, $box, $logger); + + self::addFiles($config, $box, $logger, $io); + + self::registerStub($config, $box, $main, $check, $logger); + self::configureMetadata($config, $box, $logger); + + self::commit($box, $composerOrchestrator, $config, $logger); + + self::checkComposerFiles($box, $config, $logger); + + if ($debug) { + $box->extractTo(self::DEBUG_DIR, true); + } + + self::configureCompressionAlgorithm( + $config, + $box, + $io->getTypedOption(self::DEV_OPTION)->asBoolean(), + $io, + $logger, + ); + + self::signPhar($config, $box, $tmpOutputPath, $io, $logger); + + if ($tmpOutputPath !== $config->getOutputPath()) { + FS::rename($tmpOutputPath, $config->getOutputPath()); + } + + return $box; + } + + private static function getComposerBin(IO $io): ?string + { + $composerBin = $io->getTypedOption(self::COMPOSER_BIN_OPTION)->asNullableNonEmptyString(); + + return null === $composerBin ? null : Path::makeAbsolute($composerBin, getcwd()); + } + + private function removeExistingArtifacts(Configuration $config, CompilerLogger $logger, bool $debug): void + { + $path = $config->getOutputPath(); + + if ($debug) { + FS::remove(self::DEBUG_DIR); + + FS::dumpFile( + self::DEBUG_DIR.'/.box_configuration', + ConfigurationExporter::export($config), + ); + } + + if (false === file_exists($path)) { + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Removing the existing PHAR "%s"', + $path, + ), + ); + + FS::remove($path); + } + + private static function checkComposerVersion( + ComposerOrchestrator $composerOrchestrator, + Configuration $config, + CompilerLogger $logger, + IO $io, + ): void { + if (!$config->dumpAutoload()) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Skipping the Composer compatibility check: the autoloader is not dumped', + ); + + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Checking Composer compatibility', + ); + + try { + $composerOrchestrator->checkVersion(); + + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + 'Supported version detected', + ); + } catch (IncompatibleComposerVersion $incompatibleComposerVersion) { + if ($io->getTypedOption(self::ALLOW_COMPOSER_COMPOSER_CHECK_FAILURE_OPTION)->asBoolean()) { + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + 'Warning! Incompatible composer version detected: '.$incompatibleComposerVersion->getMessage(), + ); + + return; // Swallow the exception + } + + throw $incompatibleComposerVersion; + } + } + + private static function registerReplacementValues(Configuration $config, Box $box, CompilerLogger $logger): void + { + $values = $config->getReplacements(); + + if (0 === count($values)) { + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Setting replacement values', + ); + + foreach ($values as $key => $value) { + $logger->log( + CompilerLogger::PLUS_PREFIX, + sprintf( + '%s: %s', + $key, + $value, + ), + ); + } + + $box->registerPlaceholders($values); + } + + private static function registerCompactors(Configuration $config, Box $box, CompilerLogger $logger): void + { + $compactors = $config->getCompactors(); + + if (0 === count($compactors)) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'No compactor to register', + ); + + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Registering compactors', + ); + + $logCompactors = static function (Compactor $compactor) use ($logger): void { + $compactorClassParts = explode('\\', $compactor::class); + + if (str_starts_with($compactorClassParts[0], '_HumbugBox')) { + // Keep the non prefixed class name for the user + array_shift($compactorClassParts); + } + + $logger->log( + CompilerLogger::PLUS_PREFIX, + implode('\\', $compactorClassParts), + ); + }; + + array_map($logCompactors, $compactors->toArray()); + + $box->registerCompactors($compactors); + } + + private static function registerFileMapping(Configuration $config, Box $box, CompilerLogger $logger): void + { + $fileMapper = $config->getFileMapper(); + + self::logMap($fileMapper, $logger); + + $box->registerFileMapping($fileMapper); + } + + private static function addFiles(Configuration $config, Box $box, CompilerLogger $logger, IO $io): void + { + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, 'Adding binary files'); + + $count = count($config->getBinaryFiles()); + + $box->addFiles($config->getBinaryFiles(), true); + + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + 0 === $count + ? 'No file found' + : sprintf('%d file(s)', $count), + ); + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Auto-discover files? %s', + $config->hasAutodiscoveredFiles() ? 'Yes' : 'No', + ), + ); + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Exclude dev files? %s', + $config->excludeDevFiles() ? 'Yes' : 'No', + ), + ); + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, 'Adding files'); + + $count = count($config->getFiles()); + + self::addFilesWithErrorHandling($config, $box, $io); + + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + 0 === $count + ? 'No file found' + : sprintf('%d file(s)', $count), + ); + } + + private static function addFilesWithErrorHandling(Configuration $config, Box $box, IO $io): void + { + try { + $box->addFiles($config->getFiles(), false); + + return; + } catch (MultiReasonException $ampFailure) { + // Continue + } + + // This exception is handled a different way to give me meaningful feedback to the user + $io->error([ + 'An Amp\Parallel error occurred. To diagnostic if it is an Amp error related, you may try again with "--no-parallel".', + 'Reason(s) of the failure:', + ...FailureCollector::collectReasons($ampFailure), + ]); + + throw $ampFailure; + } + + private static function registerMainScript(Configuration $config, Box $box, CompilerLogger $logger): ?string + { + if (false === $config->hasMainScript()) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'No main script path configured', + ); + + return null; + } + + $main = $config->getMainScriptPath(); + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Adding main file: %s', + $main, + ), + ); + + $localMain = $box->addFile( + $main, + $config->getMainScriptContents(), + ); + + $relativeMain = Path::makeRelative($main, $config->getBasePath()); + + if ($localMain !== $relativeMain) { + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + $localMain, + ); + } + + return $localMain; + } + + private static function registerRequirementsChecker(Configuration $config, Box $box, CompilerLogger $logger): bool + { + if (false === $config->checkRequirements()) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Skip requirements checker', + ); + + return false; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Adding requirements checker', + ); + + $checkFiles = RequirementsDumper::dump( + new DecodedComposerJson($config->getDecodedComposerJsonContents() ?? []), + new DecodedComposerLock($config->getDecodedComposerLockContents() ?? []), + $config->getCompressionAlgorithm(), + ); + + foreach ($checkFiles as $fileWithContents) { + [$file, $contents] = $fileWithContents; + + $box->addFile('.box/'.$file, $contents, true); + } + + return true; + } + + private static function registerStub( + Configuration $config, + Box $box, + ?string $main, + bool $checkRequirements, + CompilerLogger $logger, + ): void { + if ($config->isStubGenerated()) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Generating new stub', + ); + + $stub = self::createStub($config, $main, $checkRequirements, $logger); + + $box->setStub($stub); + + return; + } + + if (null !== ($stub = $config->getStubPath())) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Using stub file: %s', + $stub, + ), + ); + + $box->registerStub($stub); + + return; + } + + $box->setAlias($config->getAlias()); + $box->setDefaultStub($main); + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Using default stub', + ); + } + + private static function configureMetadata(Configuration $config, Box $box, CompilerLogger $logger): void + { + if (null !== ($metadata = $config->getMetadata())) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Setting metadata', + ); + + if (is_callable($metadata)) { + $metadata = $metadata(); + } + + $logger->log( + CompilerLogger::MINUS_PREFIX, + is_string($metadata) ? $metadata : var_export($metadata, true), + ); + + $box->setMetadata($metadata); + } + } + + private static function commit( + Box $box, + ComposerOrchestrator $composerOrchestrator, + Configuration $config, + CompilerLogger $logger, + ): void { + $message = $config->dumpAutoload() + ? 'Dumping the Composer autoloader' + : 'Skipping dumping the Composer autoloader'; + + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, $message); + + $excludeDevFiles = $config->excludeDevFiles(); + + $box->endBuffering( + $config->dumpAutoload() + ? static fn (SymbolsRegistry $symbolsRegistry, string $prefix, array $excludeScoperFiles) => $composerOrchestrator->dumpAutoload( + $symbolsRegistry, + $prefix, + $excludeDevFiles, + $excludeScoperFiles, + ) + : null, + ); + } + + private static function checkComposerFiles(Box $box, Configuration $config, CompilerLogger $logger): void + { + $message = $config->excludeComposerFiles() + ? 'Removing the Composer dump artefacts' + : 'Keep the Composer dump artefacts'; + + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, $message); + + if ($config->excludeComposerFiles()) { + $box->removeComposerArtefacts( + ComposerConfiguration::retrieveVendorDir( + $config->getDecodedComposerJsonContents() ?? [], + ), + ); + } + } + + private static function configureCompressionAlgorithm( + Configuration $config, + Box $box, + bool $dev, + IO $io, + CompilerLogger $logger, + ): void { + $algorithm = $config->getCompressionAlgorithm(); + + if (CompressionAlgorithm::NONE === $algorithm) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'No compression', + ); + + return; + } + + if ($dev) { + $logger->log(CompilerLogger::QUESTION_MARK_PREFIX, 'Dev mode detected: skipping the compression'); + + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Compressing with the algorithm "%s"', + $algorithm->name, + ), + ); + + $restoreLimit = OpenFileDescriptorLimiter::bumpLimit(count($box), $io); + + try { + $extension = $box->compress($algorithm); + + if (null !== $extension) { + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + sprintf( + 'Warning: the extension "%s" will now be required to execute the PHAR', + $extension, + ), + ); + } + } catch (RuntimeException $exception) { + $io->error($exception->getMessage()); + + // Continue: the compression failure should not result in completely bailing out the compilation process + } finally { + $restoreLimit(); + } + } + + private static function signPhar( + Configuration $config, + Box $box, + string $path, + IO $io, + CompilerLogger $logger, + ): void { + // Sign using private key when applicable + FS::remove($path.'.pubkey'); + + $key = $config->getPrivateKeyPath(); + + if (null === $key) { + self::signPharWithoutPrivateKey( + $box, + $config->getSigningAlgorithm(), + $config->getTimestamp(), + $logger, + ); + } else { + self::signPharWithPrivateKey( + $box, + $key, + $config->getPrivateKeyPassphrase(), + $config->promptForPrivateKey(), + $io, + $logger, + ); + } + } + + private static function signPharWithoutPrivateKey( + Box $box, + SigningAlgorithm $signingAlgorithm, + ?DateTimeImmutable $timestamp, + CompilerLogger $logger, + ): void { + if (null !== $timestamp) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Correcting the timestamp to "%s".', + $timestamp->format(DateTimeInterface::ATOM), + ), + ); + } + + $box->sign($signingAlgorithm, $timestamp); + } + + private static function signPharWithPrivateKey( + Box $box, + string $key, + ?string $passphrase, + bool $prompt, + IO $io, + CompilerLogger $logger, + ): void { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Signing using a private key', + ); + $io->newLine(); + + if ($prompt) { + if (false === $io->isInteractive()) { + throw new RuntimeException( + sprintf( + 'Accessing to the private key "%s" requires a passphrase but none provided. Either ' + .'provide one or run this command in interactive mode.', + $key, + ), + ); + } + + $question = new Question('Private key passphrase'); + $question->setHidden(false); + $question->setHiddenFallback(false); + + $passphrase = $io->askQuestion($question); + + $io->writeln(''); + } + + $box->signUsingFile($key, $passphrase); + } + + private static function correctPermissions(string $path, Configuration $config, CompilerLogger $logger): void + { + if (null !== ($chmod = $config->getFileMode())) { + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + sprintf( + 'Setting file permissions to %s', + '0'.decoct($chmod), + ), + ); + + FS::chmod($path, $chmod); + } + } + + private static function createStub( + Configuration $config, + ?string $main, + bool $checkRequirements, + CompilerLogger $logger, + ): string { + $shebang = $config->getShebang(); + $bannerPath = $config->getStubBannerPath(); + $bannerContents = $config->getStubBannerContents(); + + if (null !== $shebang) { + $logger->log( + CompilerLogger::MINUS_PREFIX, + sprintf( + 'Using shebang line: %s', + $shebang, + ), + ); + } else { + $logger->log( + CompilerLogger::MINUS_PREFIX, + 'No shebang line', + ); + } + + if (null !== $bannerPath) { + $logger->log( + CompilerLogger::MINUS_PREFIX, + sprintf( + 'Using custom banner from file: %s', + $bannerPath, + ), + ); + } elseif (null !== $bannerContents) { + $logger->log( + CompilerLogger::MINUS_PREFIX, + 'Using banner:', + ); + + $bannerLines = explode("\n", $bannerContents); + + foreach ($bannerLines as $bannerLine) { + $logger->log( + CompilerLogger::CHEVRON_PREFIX, + $bannerLine, + ); + } + } + + return StubGenerator::generateStub( + $config->getAlias(), + $bannerContents, + $main, + $config->isInterceptFileFuncs(), + $shebang, + $checkRequirements, + ); + } + + private static function logMap(MapFile $fileMapper, CompilerLogger $logger): void + { + $map = $fileMapper->getMap(); + + if (0 === count($map)) { + return; + } + + $logger->log( + CompilerLogger::QUESTION_MARK_PREFIX, + 'Mapping paths', + ); + + foreach ($map as $item) { + foreach ($item as $match => $replace) { + if ('' === $match) { + $match = '(all)'; + $replace .= '/'; + } + + $logger->log( + CompilerLogger::MINUS_PREFIX, + sprintf( + '%s > %s', + $match, + $replace, + ), + ); + } + } + } + + private static function logEndBuilding( + Configuration $config, + CompilerLogger $logger, + IO $io, + Box $box, + string $path, + float $startTime, + ): void { + $logger->log( + CompilerLogger::STAR_PREFIX, + 'Done.', + ); + $io->newLine(); + + MessageRenderer::render($io, $config->getRecommendations(), $config->getWarnings()); + + $io->comment( + sprintf( + 'PHAR: %s (%s)', + $box->count() > 1 ? $box->count().' files' : $box->count().' file', + format_size( + filesize($path), + ), + ) + .PHP_EOL + .'You can inspect the generated PHAR with the "info" command.', + ); + + $io->comment( + sprintf( + 'Memory usage: %s (peak: %s), time: %s', + format_size(memory_get_usage()), + format_size(memory_get_peak_usage()), + format_time(microtime(true) - $startTime), + ), + ); + } + + private function generateDockerFile(IO $io): int + { + $input = new StringInput(''); + $input->setInteractive(false); + + return $this->getDockerCommand()->execute( + new IO($input, $io->getOutput()), + ); + } + + private function getDockerCommand(): Command + { + return $this->getCommandRegistry()->findCommand(GenerateDockerFile::NAME); + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/Composer/ComposerCheckVersion.php b/fixtures/bench/without-compactors/src/Console/Command/Composer/ComposerCheckVersion.php new file mode 100644 index 000000000..30fe5e1e0 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/Composer/ComposerCheckVersion.php @@ -0,0 +1,49 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command\Composer; + +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use KevinGH\Box\Composer\ComposerOrchestrator; + +/** + * @private + */ +final class ComposerCheckVersion extends ComposerCommand +{ + public function getConfiguration(): Configuration + { + $parentConfig = parent::getConfiguration(); + + return new Configuration( + 'composer:check-version', + 'Checks if the Composer executable used is compatible with Box', + <<<'HELP' + The %command.name% command will look for the Composer binary (in the system if not configured + in the configuration file) and check if its version is compatible with Box. + HELP, + $parentConfig->getArguments(), + $parentConfig->getOptions(), + ); + } + + protected function orchestrate(ComposerOrchestrator $composerOrchestrator, IO $io): int + { + $composerOrchestrator->checkVersion(); + + return ExitCode::SUCCESS; + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/Composer/ComposerCommand.php b/fixtures/bench/without-compactors/src/Console/Command/Composer/ComposerCommand.php new file mode 100644 index 000000000..083f846b8 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/Composer/ComposerCommand.php @@ -0,0 +1,83 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command\Composer; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration; +use Fidry\Console\IO; +use Fidry\FileSystem\FileSystem; +use KevinGH\Box\Composer\ComposerOrchestrator; +use KevinGH\Box\Composer\ComposerProcessFactory; +use Psr\Log\LogLevel; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Logger\ConsoleLogger; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Path; +use function Safe\getcwd; + +/** + * @private + */ +abstract class ComposerCommand implements Command +{ + private const COMPOSER_BIN_OPTION = 'composer-bin'; + + private const VERBOSITY_LEVEL_MAP = [ + LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL, + LogLevel::INFO => OutputInterface::VERBOSITY_NORMAL, + LogLevel::DEBUG => OutputInterface::VERBOSITY_VERBOSE, + ]; + + public function getConfiguration(): Configuration + { + return new Configuration( + 'To configure.', + 'To configure.', + 'To configure.', + [], + [ + new InputOption( + self::COMPOSER_BIN_OPTION, + null, + InputOption::VALUE_REQUIRED, + 'Composer executable to use.', + ), + ], + ); + } + + final public function execute(IO $io): int + { + $composerOrchestrator = new ComposerOrchestrator( + ComposerProcessFactory::create( + self::getComposerExecutable($io), + $io, + ), + new ConsoleLogger($io->getOutput(), self::VERBOSITY_LEVEL_MAP), + new FileSystem(), + ); + + return $this->orchestrate($composerOrchestrator, $io); + } + + abstract protected function orchestrate(ComposerOrchestrator $composerOrchestrator, IO $io): int; + + private static function getComposerExecutable(IO $io): ?string + { + $composerBin = $io->getTypedOption(self::COMPOSER_BIN_OPTION)->asNullableNonEmptyString(); + + return null === $composerBin ? null : Path::makeAbsolute($composerBin, getcwd()); + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/Composer/ComposerVendorDir.php b/fixtures/bench/without-compactors/src/Console/Command/Composer/ComposerVendorDir.php new file mode 100644 index 000000000..d070cbb02 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/Composer/ComposerVendorDir.php @@ -0,0 +1,49 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command\Composer; + +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use KevinGH\Box\Composer\ComposerOrchestrator; + +/** + * @private + */ +final class ComposerVendorDir extends ComposerCommand +{ + public function getConfiguration(): Configuration + { + $parentConfig = parent::getConfiguration(); + + return new Configuration( + 'composer:vendor-dir', + 'Shows the Composer vendor-dir configured', + <<<'HELP' + The %command.name% command will look for the Composer binary (in the system if not configured + in the configuration file) and print the vendor-dir found. + HELP, + $parentConfig->getArguments(), + $parentConfig->getOptions(), + ); + } + + protected function orchestrate(ComposerOrchestrator $composerOrchestrator, IO $io): int + { + $io->writeln($composerOrchestrator->getVendorDir()); + + return ExitCode::SUCCESS; + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/ConfigOption.php b/fixtures/bench/without-compactors/src/Console/Command/ConfigOption.php new file mode 100644 index 000000000..88a2a20e1 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/ConfigOption.php @@ -0,0 +1,60 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\IO; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\Console\ConfigurationLoader; +use KevinGH\Box\Json\JsonValidationException; +use KevinGH\Box\NotInstantiable; +use Symfony\Component\Console\Input\InputOption; + +/** + * Allows a configuration file path to be specified for a command. + * + * @private + */ +final class ConfigOption +{ + use NotInstantiable; + + private const CONFIG_PARAM = 'config'; + + public static function getOptionInput(): InputOption + { + return new InputOption( + self::CONFIG_PARAM, + 'c', + InputOption::VALUE_REQUIRED, + 'The alternative configuration file path.', + ); + } + + /** + * Returns the configuration settings. + * + * @param bool $allowNoFile Load the config nonetheless if not file is found when true + * + * @throws JsonValidationException + */ + public static function getConfig(IO $io, bool $allowNoFile = false): Configuration + { + return ConfigurationLoader::getConfig( + $io->getInput()->getOption(self::CONFIG_PARAM), + $io, + $allowNoFile, + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/ConfigurationExporter.php b/fixtures/bench/without-compactors/src/Console/Command/ConfigurationExporter.php new file mode 100644 index 000000000..cdfcc35cd --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/ConfigurationExporter.php @@ -0,0 +1,62 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use DateTimeImmutable; +use DateTimeZone; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\NotInstantiable; +use function function_exists; +use function get_loaded_extensions; +use function implode; +use function KevinGH\Box\get_box_version; +use function php_uname; +use const DATE_ATOM; +use const PHP_OS; +use const PHP_VERSION; + +final class ConfigurationExporter +{ + use NotInstantiable; + + public static function export(Configuration $config): string + { + $date = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_ATOM); + $file = $config->getConfigurationFile() ?? 'No config file'; + + $phpVersion = PHP_VERSION; + $phpExtensions = implode(',', get_loaded_extensions()); + $os = function_exists('php_uname') ? PHP_OS.' / '.php_uname('r') : 'Unknown OS'; + $command = implode(' ', $GLOBALS['argv']); + $boxVersion = get_box_version(); + + $header = <<export(); + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/Diff.php b/fixtures/bench/without-compactors/src/Console/Command/Diff.php new file mode 100644 index 000000000..fc8c2596e --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/Diff.php @@ -0,0 +1,350 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use KevinGH\Box\Console\PharInfoRenderer; +use KevinGH\Box\Phar\DiffMode; +use KevinGH\Box\Phar\PharDiff; +use KevinGH\Box\Phar\PharInfo; +use SebastianBergmann\Diff\Differ; +use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\Filesystem\Path; +use ValueError; +use Webmozart\Assert\Assert; +use function array_map; +use function explode; +use function implode; +use function sprintf; +use function str_starts_with; + +/** + * @private + */ +final class Diff implements Command +{ + private const FIRST_PHAR_ARG = 'pharA'; + private const SECOND_PHAR_ARG = 'pharB'; + + private const LIST_FILES_DIFF_OPTION = 'list-diff'; + private const GIT_DIFF_OPTION = 'git-diff'; + private const GNU_DIFF_OPTION = 'gnu-diff'; + private const DIFF_OPTION = 'diff'; + private const CHECK_OPTION = 'check'; + private const CHECKSUM_ALGORITHM_OPTION = 'checksum-algorithm'; + + private const DEFAULT_CHECKSUM_ALGO = 'sha384'; + + public function getConfiguration(): Configuration + { + return new Configuration( + 'diff', + '🕵 Displays the differences between all of the files in two PHARs', + '', + [ + new InputArgument( + self::FIRST_PHAR_ARG, + InputArgument::REQUIRED, + 'The first PHAR', + ), + new InputArgument( + self::SECOND_PHAR_ARG, + InputArgument::REQUIRED, + 'The second PHAR', + ), + ], + [ + new InputOption( + self::GNU_DIFF_OPTION, + null, + InputOption::VALUE_NONE, + '(deprecated) Displays a GNU diff', + ), + new InputOption( + self::GIT_DIFF_OPTION, + null, + InputOption::VALUE_NONE, + '(deprecated) Displays a Git diff', + ), + new InputOption( + self::LIST_FILES_DIFF_OPTION, + null, + InputOption::VALUE_NONE, + '(deprecated) Displays a list of file names diff (default)', + ), + new InputOption( + self::DIFF_OPTION, + null, + InputOption::VALUE_REQUIRED, + sprintf( + 'Displays a diff of the files. Available options are: "%s"', + implode( + '", "', + DiffMode::values(), + ), + ), + DiffMode::CHECKSUM->value, + ), + new InputOption( + self::CHECK_OPTION, + 'c', + InputOption::VALUE_OPTIONAL, + '(deprecated) Verify the authenticity of the contents between the two PHARs with the given hash function', + ), + new InputOption( + self::CHECKSUM_ALGORITHM_OPTION, + null, + InputOption::VALUE_REQUIRED, + sprintf( + 'The hash function used to compare files with the diff mode used is "%s".', + DiffMode::CHECKSUM->value, + ), + self::DEFAULT_CHECKSUM_ALGO, + ), + ], + ); + } + + public function execute(IO $io): int + { + $diff = new PharDiff(...self::getPaths($io)); + $diffMode = self::getDiffMode($io); + $checksumAlgorithm = self::getChecksumAlgorithm($io); + + $io->comment('Comparing the two archives...'); + + if ($diff->equals()) { + $io->success('The two archives are identical.'); + + return ExitCode::SUCCESS; + } + + self::renderSummary($diff->getPharInfoA(), $io); + $io->newLine(); + self::renderSummary($diff->getPharInfoB(), $io); + + $this->renderArchivesDiff($diff, $io); + $this->renderContentsDiff($diff, $diffMode, $checksumAlgorithm, $io); + + return ExitCode::FAILURE; + } + + /** + * @return array{non-empty-string, non-empty-string} + */ + private static function getPaths(IO $io): array + { + $paths = [ + $io->getTypedArgument(self::FIRST_PHAR_ARG)->asNonEmptyString(), + $io->getTypedArgument(self::SECOND_PHAR_ARG)->asNonEmptyString(), + ]; + + Assert::allFile($paths); + + return array_map( + static fn (string $path) => Path::canonicalize($path), + $paths, + ); + } + + private function renderArchivesDiff(PharDiff $diff, IO $io): void + { + $pharASummary = self::getShortSummary($diff->getPharInfoA(), $io); + $pharBSummary = self::getShortSummary($diff->getPharInfoB(), $io); + + if ($pharASummary === $pharBSummary) { + return; + } + + $io->writeln( + self::createColorizedDiff( + $pharASummary, + $pharBSummary, + ), + ); + } + + private static function createColorizedDiff(string $pharASummary, string $pharBSummary): string + { + $differ = new Differ( + new UnifiedDiffOutputBuilder( + "\n--- PHAR A\n+++ PHAR B\n", + ), + ); + + $result = $differ->diff( + $pharASummary, + $pharBSummary, + ); + + $lines = explode("\n", $result); + + $colorizedLines = array_map( + static fn (string $line) => match (true) { + str_starts_with($line, '+') => sprintf( + '%s', + $line, + ), + str_starts_with($line, '-') => sprintf( + '%s', + $line, + ), + default => $line, + }, + $lines, + ); + + return implode("\n", $colorizedLines); + } + + private static function getDiffMode(IO $io): DiffMode + { + if ($io->getTypedOption(self::GNU_DIFF_OPTION)->asBoolean()) { + $io->writeln( + sprintf( + '⚠️ Using the option "%s" is deprecated. Use "--%s=%s" instead.', + self::GNU_DIFF_OPTION, + self::DIFF_OPTION, + DiffMode::GNU->value, + ), + ); + + return DiffMode::GNU; + } + + if ($io->getTypedOption(self::GIT_DIFF_OPTION)->asBoolean()) { + $io->writeln( + sprintf( + '⚠️ Using the option "%s" is deprecated. Use "--%s=%s" instead.', + self::GIT_DIFF_OPTION, + self::DIFF_OPTION, + DiffMode::GIT->value, + ), + ); + + return DiffMode::GIT; + } + + if ($io->getTypedOption(self::LIST_FILES_DIFF_OPTION)->asBoolean()) { + $io->writeln( + sprintf( + '⚠️ Using the option "%s" is deprecated. Use "--%s=%s" instead.', + self::LIST_FILES_DIFF_OPTION, + self::DIFF_OPTION, + DiffMode::FILE_NAME->value, + ), + ); + + return DiffMode::FILE_NAME; + } + + if ($io->hasOption('-c') || $io->hasOption('--check')) { + $io->writeln( + sprintf( + '⚠️ Using the option "%s" is deprecated. Use "--%s=%s" instead.', + self::CHECK_OPTION, + self::DIFF_OPTION, + DiffMode::CHECKSUM->value, + ), + ); + + return DiffMode::FILE_NAME; + } + + $rawDiffOption = $io->getTypedOption(self::DIFF_OPTION)->asNonEmptyString(); + + try { + return DiffMode::from($rawDiffOption); + } catch (ValueError) { + // Rethrow a more user-friendly error message + throw new RuntimeException( + sprintf( + 'Invalid diff mode "%s". Expected one of: "%s".', + $rawDiffOption, + implode( + '", "', + DiffMode::values(), + ), + ), + ); + } + } + + private static function getChecksumAlgorithm(IO $io): string + { + $checksumAlgorithm = $io->getTypedOption(self::CHECK_OPTION)->asNullableNonEmptyString(); + + if (null !== $checksumAlgorithm) { + $io->writeln( + sprintf( + '⚠️ Using the option "%s" is deprecated. Use "--%s=\" instead.', + self::CHECK_OPTION, + self::CHECKSUM_ALGORITHM_OPTION, + ), + ); + + return $checksumAlgorithm; + } + + return $io->getTypedOption(self::CHECKSUM_ALGORITHM_OPTION)->asNullableNonEmptyString() ?? self::DEFAULT_CHECKSUM_ALGO; + } + + private function renderContentsDiff(PharDiff $diff, DiffMode $diffMode, string $checksumAlgorithm, IO $io): void + { + $io->comment( + sprintf( + 'Comparing the two archives contents (%s diff)...', + $diffMode->value, + ), + ); + + $diff->diff($diffMode, $checksumAlgorithm, $io); + } + + private static function renderSummary(PharInfo $pharInfo, IO $io): void + { + $io->writeln( + sprintf( + 'Archive: %s', + $pharInfo->getFileName(), + ), + ); + + PharInfoRenderer::renderShortSummary($pharInfo, $io); + } + + private static function getShortSummary(PharInfo $pharInfo, IO $io): string + { + $output = new BufferedOutput( + $io->getVerbosity(), + false, + clone $io->getOutput()->getFormatter(), + ); + + PharInfoRenderer::renderShortSummary( + $pharInfo, + $io->withOutput($output), + ); + + return $output->fetch(); + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/Extract.php b/fixtures/bench/without-compactors/src/Console/Command/Extract.php new file mode 100644 index 000000000..0d0d999c9 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/Extract.php @@ -0,0 +1,207 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use Fidry\FileSystem\FS; +use KevinGH\Box\Console\PhpSettingsChecker; +use KevinGH\Box\Phar\InvalidPhar; +use KevinGH\Box\Phar\PharFactory; +use KevinGH\Box\Phar\PharMeta; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Throwable; +use function bin2hex; +use function file_exists; +use function realpath; +use function sprintf; +use const DIRECTORY_SEPARATOR; + +/** + * @private + */ +final class Extract implements Command +{ + public const STUB_PATH = '.phar/stub.php'; + public const PHAR_META_PATH = '.phar/meta.json'; + + private const PHAR_ARG = 'phar'; + private const OUTPUT_ARG = 'output'; + private const INTERNAL_OPT = 'internal'; + + public function getConfiguration(): Configuration + { + return new Configuration( + 'extract', + '🚚 Extracts a given PHAR into a directory', + '', + [ + new InputArgument( + self::PHAR_ARG, + InputArgument::REQUIRED, + 'The path to the PHAR file', + ), + new InputArgument( + self::OUTPUT_ARG, + InputArgument::REQUIRED, + 'The output directory', + ), + ], + [ + new InputOption( + self::INTERNAL_OPT, + null, + InputOption::VALUE_NONE, + 'Internal option; Should not be used.', + ), + ], + ); + } + + public function execute(IO $io): int + { + PhpSettingsChecker::check($io); + + $pharPath = self::getPharFilePath($io); + $outputDir = $io->getTypedArgument(self::OUTPUT_ARG)->asNonEmptyString(); + $internal = $io->getTypedOption(self::INTERNAL_OPT)->asBoolean(); + + if (null === $pharPath) { + return ExitCode::FAILURE; + } + + if (file_exists($outputDir)) { + $canDelete = $io->askQuestion( + new ConfirmationQuestion( + 'The output directory already exists. Do you want to delete its current content?', + // If is interactive, we want the prompt to default to false since it can be an error made by the user. + // Otherwise, this is likely launched by a script or Pharaoh in which case we do not care. + $internal, + ), + ); + + if ($canDelete) { + FS::remove($outputDir); + // Continue + } else { + // Do nothing + return ExitCode::FAILURE; + } + } + + FS::mkdir($outputDir); + + try { + self::dumpPhar($pharPath, $outputDir); + } catch (InvalidPhar $invalidPhar) { + if (!$internal) { + throw $invalidPhar; + } + + $io->getErrorIO()->write($invalidPhar->getMessage()); + + return ExitCode::FAILURE; + } + + return ExitCode::SUCCESS; + } + + private static function getPharFilePath(IO $io): ?string + { + $filePath = realpath($io->getTypedArgument(self::PHAR_ARG)->asString()); + + if (false !== $filePath) { + return $filePath; + } + + $io->error( + sprintf( + 'The file "%s" could not be found.', + $io->getTypedArgument(self::PHAR_ARG)->asRaw(), + ), + ); + + return null; + } + + private static function dumpPhar(string $file, string $tmpDir): string + { + // We have to give every one a different alias, or it pukes. + $alias = self::generateAlias($file); + + // Create a temporary PHAR: this is because the extension might be + // missing in which case we would not be able to create a Phar instance + // as it requires the .phar extension. + $tmpFile = $tmpDir.DIRECTORY_SEPARATOR.$alias; + $pubKey = $file.'.pubkey'; + $pubKeyContent = null; + $tmpPubKey = $tmpFile.'.pubkey'; + $stub = $tmpDir.DIRECTORY_SEPARATOR.self::STUB_PATH; + + try { + FS::copy($file, $tmpFile, true); + + if (file_exists($pubKey)) { + FS::copy($pubKey, $tmpPubKey, true); + $pubKeyContent = FS::getFileContents($pubKey); + } + + $phar = PharFactory::create($tmpFile, $file); + $pharMeta = PharMeta::fromPhar($phar, $pubKeyContent); + + $phar->extractTo($tmpDir); + FS::dumpFile($stub, $phar->getStub()); + } catch (Throwable $throwable) { + FS::remove([$tmpFile, $tmpPubKey]); + + throw $throwable; + } + + FS::dumpFile( + $tmpDir.DIRECTORY_SEPARATOR.self::PHAR_META_PATH, + $pharMeta->toJson(), + ); + + // Cleanup the temporary PHAR. + FS::remove([$tmpFile, $tmpPubKey]); + + return $tmpDir; + } + + private static function generateAlias(string $file): string + { + $extension = self::getExtension($file); + + return bin2hex(random_bytes(16)).$extension; + } + + private static function getExtension(string $file): string + { + $lastExtension = pathinfo($file, PATHINFO_EXTENSION); + $extension = ''; + + while ('' !== $lastExtension) { + $extension = '.'.$lastExtension.$extension; + $file = mb_substr($file, 0, -(mb_strlen($lastExtension) + 1)); + $lastExtension = pathinfo($file, PATHINFO_EXTENSION); + } + + return '' === $extension ? '.phar' : $extension; + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/GenerateDockerFile.php b/fixtures/bench/without-compactors/src/Console/Command/GenerateDockerFile.php new file mode 100644 index 000000000..9ba23ce41 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/GenerateDockerFile.php @@ -0,0 +1,217 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\CommandAware; +use Fidry\Console\Command\CommandAwareness; +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use Fidry\FileSystem\FS; +use KevinGH\Box\DockerFileGenerator; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Filesystem\Path; +use Webmozart\Assert\Assert; +use function file_exists; +use function getcwd; +use function realpath; +use function sprintf; + +/** + * @private + */ +final class GenerateDockerFile implements CommandAware +{ + use CommandAwareness; + + public const NAME = 'docker'; + + private const PHAR_ARG = 'phar'; + private const DOCKER_FILE_NAME = 'Dockerfile'; + + public function getConfiguration(): Configuration + { + return new Configuration( + 'docker', + '🐳 Generates a Dockerfile for the given PHAR', + '', + [ + new InputArgument( + self::PHAR_ARG, + InputArgument::OPTIONAL, + 'The PHAR file', + ), + ], + [ConfigOption::getOptionInput()], + ); + } + + public function execute(IO $io): int + { + $pharFilePath = $this->getPharFilePath($io); + + if (null === $pharFilePath) { + return ExitCode::FAILURE; + } + + $io->newLine(); + $io->writeln( + sprintf( + '🐳 Generating a Dockerfile for the PHAR "%s"', + $pharFilePath, + ), + ); + $io->newLine(); + + $requirementsFilePhar = 'phar://'.$pharFilePath.'/.box/.requirements.php'; + + return $this->generateFile( + $pharFilePath, + $requirementsFilePhar, + $io, + ); + } + + /** + * @return null|non-empty-string + */ + private function getPharFilePath(IO $io): ?string + { + $pharFilePath = $io->getTypedArgument(self::PHAR_ARG)->asNullableNonEmptyString(); + + if (null === $pharFilePath) { + $pharFilePath = $this->guessPharPath($io); + } + + if (null === $pharFilePath) { + return null; + } + + $pharFilePath = Path::canonicalize($pharFilePath); + Assert::file($pharFilePath); + + return false !== realpath($pharFilePath) ? realpath($pharFilePath) : $pharFilePath; + } + + private function guessPharPath(IO $io): ?string + { + $config = ConfigOption::getConfig($io, true); + + if (file_exists($config->getOutputPath())) { + return $config->getOutputPath(); + } + + $compile = $io->askQuestion( + new ConfirmationQuestion( + 'The output PHAR could not be found, do you wish to generate it by running "box compile"?', + true, + ), + ); + + if (false === $compile) { + $io->error('Could not find the PHAR to generate the docker file for'); + + return null; + } + + $this->getCompileCommand()->execute( + new IO( + self::createCompileInput($io), + clone $io->getOutput(), + ), + ); + + return $config->getOutputPath(); + } + + private function getCompileCommand(): Compile + { + /* @noinspection PhpIncompatibleReturnTypeInspection */ + return $this->getCommandRegistry()->findCommand(Compile::NAME); + } + + private static function createCompileInput(IO $io): InputInterface + { + if ($io->isQuiet()) { + $compileInput = '--quiet'; + } elseif ($io->isVerbose()) { + $compileInput = '--verbose 1'; + } elseif ($io->isVeryVerbose()) { + $compileInput = '--verbose 2'; + } elseif ($io->isDebug()) { + $compileInput = '--verbose 3'; + } else { + $compileInput = ''; + } + + $compileInput = new StringInput($compileInput); + $compileInput->setInteractive(false); + + return $compileInput; + } + + private function generateFile(string $pharPath, string $requirementsPhar, IO $io): int + { + if (false === file_exists($requirementsPhar)) { + $io->error( + 'Cannot retrieve the requirements for the PHAR. Make sure the PHAR has been built with Box and the requirement checker enabled.', + ); + + return ExitCode::FAILURE; + } + + $requirements = include $requirementsPhar; + + $dockerFileContents = DockerFileGenerator::createForRequirements( + $requirements, + Path::makeRelative($pharPath, getcwd()), + ) + ->generateStub(); + + if (file_exists(self::DOCKER_FILE_NAME)) { + $remove = $io->askQuestion( + new ConfirmationQuestion( + 'A Docker file has already been found, are you sure you want to override it?', + true, + ), + ); + + if (false === $remove) { + $io->writeln('Skipped the docker file generation.'); + + return ExitCode::SUCCESS; + } + } + + FS::dumpFile(self::DOCKER_FILE_NAME, $dockerFileContents); + + $io->success('Done'); + + $io->writeln( + [ + sprintf( + 'You can now inspect your %s file or build your container with:', + self::DOCKER_FILE_NAME, + ), + '$ docker build .', + ], + ); + + return ExitCode::SUCCESS; + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/Info.php b/fixtures/bench/without-compactors/src/Console/Command/Info.php new file mode 100644 index 000000000..3239eee6a --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/Info.php @@ -0,0 +1,245 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use KevinGH\Box\Console\PharInfoRenderer; +use KevinGH\Box\Phar\PharInfo; +use Phar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Filesystem\Path; +use function implode; +use function is_array; +use function realpath; +use function sprintf; + +/** + * @private + */ +final class Info implements Command +{ + private const PHAR_ARG = 'phar'; + private const LIST_OPT = 'list'; + private const MODE_OPT = 'mode'; + private const DEPTH_OPT = 'depth'; + + private const MODES = [ + 'indent', + 'flat', + ]; + + public function getConfiguration(): Configuration + { + return new Configuration( + 'info', + '🔍 Displays information about the PHAR extension or file', + <<<'HELP' + The %command.name% command will display information about the Phar extension, + or the Phar file if specified. + + If the phar argument (the PHAR file path) is provided, information + about the PHAR file itself will be displayed. + + If the --list|-l option is used, the contents of the PHAR file will + be listed. By default, the list is shown as an indented tree. You may + instead choose to view a flat listing, by setting the --mode|-m option + to flat. + HELP, + [ + new InputArgument( + self::PHAR_ARG, + InputArgument::OPTIONAL, + 'The Phar file.', + ), + ], + [ + new InputOption( + self::LIST_OPT, + 'l', + InputOption::VALUE_NONE, + 'List the contents of the Phar?', + ), + new InputOption( + self::MODE_OPT, + 'm', + InputOption::VALUE_REQUIRED, + sprintf( + 'The listing mode. Modes available: "%s"', + implode('", "', self::MODES), + ), + 'indent', + ), + new InputOption( + self::DEPTH_OPT, + 'd', + InputOption::VALUE_REQUIRED, + 'The depth of the tree displayed', + '-1', + ), + ], + ); + } + + public function execute(IO $io): int + { + $io->newLine(); + + $file = $io->getTypedArgument(self::PHAR_ARG)->asNullableNonEmptyString(); + + if (null === $file) { + return self::showGlobalInfo($io); + } + + $file = Path::canonicalize($file); + $fileRealPath = realpath($file); + + if (false === $fileRealPath) { + $io->error( + sprintf( + 'The file "%s" could not be found.', + $file, + ), + ); + + return ExitCode::FAILURE; + } + + return self::showInfo($fileRealPath, $io); + } + + public static function showInfo(string $file, IO $io): int + { + $maxDepth = self::getMaxDepth($io); + $mode = $io->getTypedOption(self::MODE_OPT)->asStringChoice(self::MODES); + + $pharInfo = new PharInfo($file); + + return self::showPharInfo( + $pharInfo, + $io->getTypedOption(self::LIST_OPT)->asBoolean(), + -1 === $maxDepth ? false : $maxDepth, + 'indent' === $mode, + $io, + ); + } + + /** + * @return -1|natural + */ + private static function getMaxDepth(IO $io): int + { + $option = $io->getTypedOption(self::DEPTH_OPT); + + return '-1' === $option->asRaw() + ? -1 + : $option->asNatural(sprintf( + 'Expected the depth to be a positive integer or -1: "%s".', + $option->asRaw(), + )); + } + + private static function showGlobalInfo(IO $io): int + { + self::render( + $io, + [ + 'API Version' => Phar::apiVersion(), + 'Supported Compression' => Phar::getSupportedCompression(), + 'Supported Signatures' => Phar::getSupportedSignatures(), + ], + ); + + $io->newLine(); + $io->comment('Get a PHAR details by giving its path as an argument.'); + + return ExitCode::SUCCESS; + } + + private static function showPharInfo( + PharInfo $pharInfo, + bool $content, + int|false $maxDepth, + bool $indent, + IO $io, + ): int { + PharInfoRenderer::renderVersion($pharInfo, $io); + + $io->newLine(); + + PharInfoRenderer::renderBoxVersion($pharInfo, $io); + + PharInfoRenderer::renderShortSummary( + $pharInfo, + $io, + static fn () => $io->newLine(), + ); + + if ($content) { + PharInfoRenderer::renderContent( + $io, + $pharInfo, + $maxDepth, + $indent, + ); + } else { + $io->newLine(); + $io->comment('Use the --list|-l option to list the content of the PHAR.'); + } + + return ExitCode::SUCCESS; + } + + private static function showPharMeta(PharInfo $pharInfo, IO $io): void + { + PharInfoRenderer::renderVersion($pharInfo, $io); + + $io->newLine(); + + PharInfoRenderer::renderShortSummary( + $pharInfo, + $io, + static fn () => $io->newLine(), + ); + } + + private static function render(IO $io, array $attributes): void + { + $out = false; + + foreach ($attributes as $name => $value) { + if ($out) { + $io->writeln(''); + } + + $io->write("{$name}:"); + + if (is_array($value)) { + $io->writeln(''); + + foreach ($value as $v) { + $io->writeln(" - {$v}"); + } + } else { + $io->writeln(" {$value}"); + } + + $out = true; + } + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/Namespace_.php b/fixtures/bench/without-compactors/src/Console/Command/Namespace_.php new file mode 100644 index 000000000..e019478b3 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/Namespace_.php @@ -0,0 +1,45 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use function current; +use function explode; + +final class Namespace_ implements Command +{ + public function getConfiguration(): Configuration + { + return new Configuration( + 'namespace', + 'Prints the first part of the command namespace', + <<<'HELP' + This command is purely for debugging purposes to ensure it is scoped correctly. + HELP, + ); + } + + public function execute(IO $io): int + { + $namespace = current(explode('\\', self::class)); + + $io->writeln($namespace); + + return ExitCode::SUCCESS; + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/Process.php b/fixtures/bench/without-compactors/src/Console/Command/Process.php new file mode 100644 index 000000000..473fbf50d --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/Process.php @@ -0,0 +1,263 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration as ConsoleConfiguration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use Fidry\FileSystem\FS; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use KevinGH\Box\Compactor\Compactor; +use KevinGH\Box\Compactor\Compactors; +use KevinGH\Box\Compactor\PhpScoper; +use KevinGH\Box\Compactor\Placeholder; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\Console\PhpSettingsChecker; +use stdClass; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use function array_map; +use function array_shift; +use function array_unshift; +use function explode; +use function getcwd; +use function implode; +use function putenv; +use function sprintf; +use const KevinGH\Box\BOX_ALLOW_XDEBUG; + +// TODO: replace the PHP-Scoper compactor in order to warn the user about scoping errors +final class Process implements Command +{ + private const FILE_ARGUMENT = 'file'; + + private const NO_RESTART_OPTION = 'no-restart'; + private const NO_CONFIG_OPTION = 'no-config'; + + public function getConfiguration(): ConsoleConfiguration + { + return new ConsoleConfiguration( + 'process', + '⚡ Applies the registered compactors and replacement values on a file', + 'The %command.name% command will apply the registered compactors and replacement values on the the given file. This is useful to debug the scoping of a specific file for example.', + [ + new InputArgument( + self::FILE_ARGUMENT, + InputArgument::REQUIRED, + 'Path to the file to process', + ), + ], + [ + new InputOption( + self::NO_RESTART_OPTION, + null, + InputOption::VALUE_NONE, + 'Do not restart the PHP process. Box restarts the process by default to disable xdebug', + ), + new InputOption( + self::NO_CONFIG_OPTION, + null, + InputOption::VALUE_NONE, + 'Ignore the config file even when one is specified with the --config option', + ), + ConfigOption::getOptionInput(), + ChangeWorkingDirOption::getOptionInput(), + ], + ); + } + + public function execute(IO $io): int + { + if ($io->getTypedOption(self::NO_RESTART_OPTION)->asBoolean()) { + putenv(BOX_ALLOW_XDEBUG.'=1'); + } + + PhpSettingsChecker::check($io); + + ChangeWorkingDirOption::changeWorkingDirectory($io); + + $io->newLine(); + + $config = $io->getTypedOption(self::NO_CONFIG_OPTION)->asBoolean() + ? Configuration::create(null, new stdClass()) + : ConfigOption::getConfig($io, true); + + $filePath = $io->getTypedArgument(self::FILE_ARGUMENT)->asNonEmptyString(); + + $path = Path::makeRelative($filePath, $config->getBasePath()); + + $compactors = self::retrieveCompactors($config); + + $fileContents = FS::getFileContents( + $absoluteFilePath = Path::makeAbsolute( + $filePath, + getcwd(), + ), + ); + + $io->writeln([ + sprintf( + '⚡ Processing the contents of the file %s', + $absoluteFilePath, + ), + '', + ]); + + self::logPlaceholders($config, $io); + self::logCompactors($compactors, $io); + + $fileProcessedContents = $compactors->compact($path, $fileContents); + + if ($io->isQuiet()) { + $io->writeln($fileProcessedContents, OutputInterface::VERBOSITY_QUIET); + } else { + $symbolsRegistry = self::retrieveSymbolsRegistry($compactors); + + $io->writeln([ + 'Processed contents:', + '', + '"""', + $fileProcessedContents, + '"""', + ]); + + if (null !== $symbolsRegistry) { + $io->writeln([ + '', + 'Symbols Registry:', + '', + '"""', + self::exportSymbolsRegistry($symbolsRegistry, $io), + '"""', + ]); + } + } + + return ExitCode::SUCCESS; + } + + private static function retrieveCompactors(Configuration $config): Compactors + { + $compactors = $config->getCompactors()->toArray(); + + array_unshift( + $compactors, + new Placeholder($config->getReplacements()), + ); + + return new Compactors(...$compactors); + } + + private static function logPlaceholders(Configuration $config, IO $io): void + { + if (0 === count($config->getReplacements())) { + $io->writeln([ + 'No replacement values registered', + '', + ]); + + return; + } + + $io->writeln('Registered replacement values:'); + + foreach ($config->getReplacements() as $key => $value) { + $io->writeln( + sprintf( + ' + %s: %s', + $key, + $value, + ), + ); + } + + $io->newLine(); + } + + private static function logCompactors(Compactors $compactors, IO $io): void + { + $nestedCompactors = $compactors->toArray(); + + foreach ($nestedCompactors as $index => $compactor) { + if ($compactor instanceof Placeholder) { + unset($nestedCompactors[$index]); + } + } + + if ([] === $nestedCompactors) { + $io->writeln([ + 'No compactor registered', + '', + ]); + + return; + } + + $io->writeln('Registered compactors:'); + + $logCompactors = static function (Compactor $compactor) use ($io): void { + $compactorClassParts = explode('\\', $compactor::class); + + if (str_starts_with($compactorClassParts[0], '_HumbugBox')) { + // Keep the non prefixed class name for the user + array_shift($compactorClassParts); + } + + $io->writeln( + sprintf( + ' + %s', + implode('\\', $compactorClassParts), + ), + ); + }; + + array_map($logCompactors, $nestedCompactors); + + $io->newLine(); + } + + private static function retrieveSymbolsRegistry(Compactors $compactors): ?SymbolsRegistry + { + foreach ($compactors->toArray() as $compactor) { + if ($compactor instanceof PhpScoper) { + return $compactor->getScoper()->getSymbolsRegistry(); + } + } + + return null; + } + + private static function exportSymbolsRegistry(SymbolsRegistry $symbolsRegistry, IO $io): string + { + $cloner = new VarCloner(); + $cloner->setMaxItems(-1); + $cloner->setMaxString(-1); + + $cliDumper = new CliDumper(); + if ($io->isDecorated()) { + $cliDumper->setColors(true); + } + + return (string) $cliDumper->dump( + $cloner->cloneVar($symbolsRegistry), + true, + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/Validate.php b/fixtures/bench/without-compactors/src/Console/Command/Validate.php new file mode 100644 index 000000000..ba6a1654a --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/Validate.php @@ -0,0 +1,175 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration as ConsoleConfiguration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\Console\ConfigurationLoader; +use KevinGH\Box\Console\ConfigurationLocator; +use KevinGH\Box\Console\MessageRenderer; +use KevinGH\Box\Json\JsonValidationException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Throwable; +use Webmozart\Assert\Assert; +use function count; +use function sprintf; + +/** + * @private + */ +final class Validate implements Command +{ + private const FILE_ARGUMENT = 'file'; + private const IGNORE_MESSAGES_OPTION = 'ignore-recommendations-and-warnings'; + + public function getConfiguration(): ConsoleConfiguration + { + return new ConsoleConfiguration( + 'validate', + '⚙ Validates the configuration file', + <<<'HELP' + The %command.name% command will validate the configuration file + and report any errors found, if any. + + This command relies on a configuration file for loading + PHAR packaging settings. If a configuration file is not + specified through the --configuration|-c option, one of + the following files will be used (in order): box.json, + box.json.dist + + HELP, + [ + new InputArgument( + self::FILE_ARGUMENT, + InputArgument::OPTIONAL, + 'The configuration file. (default: box.json, box.json.dist)', + ), + ], + [ + new InputOption( + self::IGNORE_MESSAGES_OPTION, + 'i', + InputOption::VALUE_NONE, + 'Will not return a faulty code when a recommendation or warning is found', + ), + ], + ); + } + + public function execute(IO $io): int + { + try { + $config = ConfigurationLoader::getConfig( + $io->getTypedArgument(self::FILE_ARGUMENT)->asNullableNonEmptyString() ?? ConfigurationLocator::findDefaultPath(), + $io, + false, + ); + } catch (Throwable $throwable) { + // Continue + } + + if (isset($config)) { + return self::checkConfig($config, $io); + } + + Assert::true(isset($throwable)); + + return self::handleFailure($throwable, $io); + } + + private static function checkConfig(Configuration $config, IO $io): int + { + $ignoreRecommendationsAndWarnings = $io->getTypedOption(self::IGNORE_MESSAGES_OPTION)->asBoolean(); + + $recommendations = $config->getRecommendations(); + $warnings = $config->getWarnings(); + + MessageRenderer::render($io, $recommendations, $warnings); + + $hasRecommendationsOrWarnings = 0 === count($recommendations) && 0 === count($warnings); + + if (false === $hasRecommendationsOrWarnings) { + if (0 === count($recommendations)) { + $io->caution('The configuration file passed the validation with warnings.'); + } elseif (0 === count($warnings)) { + $io->caution('The configuration file passed the validation with recommendations.'); + } else { + $io->caution('The configuration file passed the validation with recommendations and warnings.'); + } + } else { + $io->success('The configuration file passed the validation.'); + } + + return $hasRecommendationsOrWarnings || $ignoreRecommendationsAndWarnings + ? ExitCode::SUCCESS + : ExitCode::FAILURE; + } + + private static function handleFailure(Throwable $throwable, IO $io): int + { + if ($io->isVerbose()) { + throw new RuntimeException( + sprintf( + 'The configuration file failed validation: %s', + $throwable->getMessage(), + ), + $throwable->getCode(), + $throwable, + ); + } + + return $throwable instanceof JsonValidationException + ? self::handleJsonValidationFailure($throwable, $io) + : self::handleGenericFailure($throwable, $io); + } + + private static function handleJsonValidationFailure(JsonValidationException $exception, IO $io): int + { + $io->writeln( + sprintf( + 'The configuration file failed validation: "%s" does not match the expected JSON ' + .'schema:', + $exception->getValidatedFile(), + ), + ); + + $io->writeln(''); + + foreach ($exception->getErrors() as $error) { + $io->writeln(" - {$error}"); + } + + return ExitCode::FAILURE; + } + + private static function handleGenericFailure(Throwable $throwable, IO $io): int + { + $errorMessage = sprintf('The configuration file failed validation: %s', $throwable->getMessage()); + + $io->writeln( + sprintf( + '%s', + $errorMessage, + ), + ); + + return ExitCode::FAILURE; + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Command/Verify.php b/fixtures/bench/without-compactors/src/Console/Command/Verify.php new file mode 100644 index 000000000..346872e9c --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Command/Verify.php @@ -0,0 +1,154 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\Configuration; +use Fidry\Console\ExitCode; +use Fidry\Console\IO; +use KevinGH\Box\Phar\PharInfo; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Filesystem\Path; +use Throwable; +use Webmozart\Assert\Assert; +use function realpath; +use function sprintf; + +/** + * @private + */ +final class Verify implements Command +{ + private const PHAR_ARG = 'phar'; + + public function getConfiguration(): Configuration + { + return new Configuration( + 'verify', + '🔐️ Verifies the PHAR signature', + <<<'HELP' + The %command.name% command will verify the signature of the PHAR. + + Why would I require that box handle the verification process? + + If you meet all the following conditions: + - The openssl extension is not installed + - You need to verify a PHAR signed using a private key + + Box supports verifying private key signed PHARs without using + either extensions. Note however, that the entire PHAR will need + to be read into memory before the verification can be performed. + HELP, + [ + new InputArgument( + self::PHAR_ARG, + InputArgument::REQUIRED, + 'The PHAR file', + ), + ], + ); + } + + public function execute(IO $io): int + { + $pharFilePath = self::getPharFilePath($io); + + $io->newLine(); + $io->writeln( + sprintf( + '🔐️ Verifying the PHAR "%s"', + $pharFilePath, + ), + ); + $io->newLine(); + + [$verified, $signature, $throwable] = self::verifyPhar($pharFilePath); + + if (false === $verified || false === $signature) { + return self::failVerification($throwable, $io); + } + + $io->writeln('The PHAR passed verification.'); + + $io->newLine(); + $io->writeln( + sprintf( + '%s signature: %s', + $signature['hash_type'], + $signature['hash'], + ), + ); + + return ExitCode::SUCCESS; + } + + private static function getPharFilePath(IO $io): string + { + $pharPath = Path::canonicalize( + $io->getTypedArgument(self::PHAR_ARG)->asNonEmptyString(), + ); + + Assert::file($pharPath); + + $pharRealPath = realpath($pharPath); + + return false === $pharRealPath ? $pharPath : $pharRealPath; + } + + /** + * @return array{bool, array{hash: string, hash_type:string}|false, Throwable|null} + */ + private static function verifyPhar(string $pharFilePath): array + { + $verified = false; + $signature = false; + $throwable = null; + + try { + $pharInfo = new PharInfo($pharFilePath); + + $verified = true; + $signature = $pharInfo->getSignature(); + } catch (Throwable $throwable) { + // Continue + } + + return [ + $verified, + $signature, + $throwable, + ]; + } + + private static function failVerification(?Throwable $throwable, IO $io): int + { + $message = null !== $throwable && '' !== $throwable->getMessage() + ? $throwable->getMessage() + : 'Unknown reason.'; + + $io->writeln( + sprintf( + 'The PHAR failed the verification: %s', + $message, + ), + ); + + if (null !== $throwable && $io->isDebug()) { + throw $throwable; + } + + return ExitCode::FAILURE; + } +} diff --git a/fixtures/bench/without-compactors/src/Console/ConfigurationLoader.php b/fixtures/bench/without-compactors/src/Console/ConfigurationLoader.php new file mode 100644 index 000000000..e04ddc4c3 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/ConfigurationLoader.php @@ -0,0 +1,85 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Fidry\Console\IO; +use InvalidArgumentException; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\Configuration\ConfigurationLoader as ConfigLoader; +use KevinGH\Box\Configuration\NoConfigurationFound; +use KevinGH\Box\Json\JsonValidationException; +use KevinGH\Box\NotInstantiable; +use function sprintf; + +/** + * Utility to load the configuration. + * + * @private + */ +final class ConfigurationLoader +{ + use NotInstantiable; + + /** + * Returns the configuration settings. + * + * @param bool $allowNoFile Load the config nonetheless if not file is found when true + * + * @throws JsonValidationException|NoConfigurationFound + */ + public static function getConfig( + ?string $configPath, + IO $io, + bool $allowNoFile, + ): Configuration { + $configPath = self::getConfigPath($configPath, $io, $allowNoFile); + $configLoader = new ConfigLoader(); + + try { + return $configLoader->loadFile($configPath); + } catch (InvalidArgumentException $invalidConfig) { + $io->error('The configuration file is invalid.'); + + throw $invalidConfig; + } + } + + private static function getConfigPath( + ?string $configPath, + IO $io, + bool $allowNoFile, + ): ?string { + try { + $configPath ??= ConfigurationLocator::findDefaultPath(); + } catch (NoConfigurationFound $noConfigurationFound) { + if (false === $allowNoFile) { + throw $noConfigurationFound; + } + + $io->comment('Loading without a configuration file.'); + + return null; + } + + $io->comment( + sprintf( + 'Loading the configuration file "%s".', + $configPath, + ), + ); + + return $configPath; + } +} diff --git a/fixtures/bench/without-compactors/src/Console/ConfigurationLocator.php b/fixtures/bench/without-compactors/src/Console/ConfigurationLocator.php new file mode 100644 index 000000000..5bf9da767 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/ConfigurationLocator.php @@ -0,0 +1,53 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use KevinGH\Box\Configuration\NoConfigurationFound; +use KevinGH\Box\NotInstantiable; +use function file_exists; +use function realpath; + +/** + * @private + */ +final class ConfigurationLocator +{ + use NotInstantiable; + + private const FILE_NAME = 'box.json'; + + /** + * @var list + */ + private static array $candidates; + + public static function findDefaultPath(): string + { + if (!isset(self::$candidates)) { + self::$candidates = [ + self::FILE_NAME, + self::FILE_NAME.'.dist', + ]; + } + + foreach (self::$candidates as $candidate) { + if (file_exists($candidate)) { + return realpath($candidate); + } + } + + throw new NoConfigurationFound(); + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Logger/CompilerLogger.php b/fixtures/bench/without-compactors/src/Console/Logger/CompilerLogger.php new file mode 100644 index 000000000..5b19775f5 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Logger/CompilerLogger.php @@ -0,0 +1,69 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Logger; + +use Fidry\Console\IO; +use InvalidArgumentException; +use Symfony\Component\Console\Output\OutputInterface; +use function sprintf; + +/** + * @internal + */ +final class CompilerLogger +{ + public const QUESTION_MARK_PREFIX = '?'; + public const STAR_PREFIX = '*'; + public const PLUS_PREFIX = '+'; + public const MINUS_PREFIX = '-'; + public const CHEVRON_PREFIX = '>'; + + public function __construct(private readonly IO $io) + { + } + + public function getIO(): IO + { + return $this->io; + } + + public function log(string $prefix, string $message, int $verbosity = OutputInterface::OUTPUT_NORMAL): void + { + $prefix = match ($prefix) { + '!' => "{$prefix}", + self::STAR_PREFIX => "{$prefix}", + self::QUESTION_MARK_PREFIX => "{$prefix}", + self::PLUS_PREFIX, self::MINUS_PREFIX => " {$prefix}", + self::CHEVRON_PREFIX => " {$prefix}", + default => throw new InvalidArgumentException('Expected one of the logger constant as a prefix.'), + }; + + $this->io->writeln( + "{$prefix} {$message}", + $verbosity, + ); + } + + public function logStartBuilding(string $path): void + { + $this->io->writeln( + sprintf( + '🔨 Building the PHAR "%s"', + $path, + ), + ); + $this->io->newLine(); + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Logo.php b/fixtures/bench/without-compactors/src/Console/Logo.php new file mode 100644 index 000000000..dffe7a177 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Logo.php @@ -0,0 +1,37 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use KevinGH\Box\NotInstantiable; + +/** + * @private + */ +final class Logo +{ + use NotInstantiable; + + public const LOGO_ASCII = <<<'ASCII' + + ____ + / __ )____ _ __ + / __ / __ \| |/_/ + / /_/ / /_/ /> < + /_____/\____/_/|_| + + + + ASCII; +} diff --git a/fixtures/bench/without-compactors/src/Console/MessageRenderer.php b/fixtures/bench/without-compactors/src/Console/MessageRenderer.php new file mode 100644 index 000000000..b39bbd14c --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/MessageRenderer.php @@ -0,0 +1,78 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Fidry\Console\IO; +use KevinGH\Box\NotInstantiable; +use Webmozart\Assert\Assert; +use function array_map; +use function count; +use function sprintf; + +/** + * Utility to writing on the console output the configuration recommendations and warnings. + * + * @private + */ +final class MessageRenderer +{ + use NotInstantiable; + + /** + * @param string[] $recommendations + * @param string[] $warnings + */ + public static function render(IO $io, array $recommendations, array $warnings): void + { + Assert::allString($recommendations); + Assert::allString($warnings); + + $renderMessage = static fn (string $message): string => " - {$message}"; + + if ([] === $recommendations) { + $io->writeln('No recommendation found.'); + } else { + $io->writeln( + sprintf( + '💡 %d %s found:', + count($recommendations), + count($recommendations) > 1 ? 'recommendations' : 'recommendation', + ), + ); + + $io->writeln( + array_map($renderMessage, $recommendations), + ); + } + + if ([] === $warnings) { + $io->writeln('No warning found.'); + } else { + $io->writeln( + sprintf( + '⚠️ %d %s found:', + count($warnings), + count($warnings) > 1 ? 'warnings' : 'warning', + ), + ); + + $io->writeln( + array_map($renderMessage, $warnings), + ); + } + + $io->newLine(); + } +} diff --git a/fixtures/bench/without-compactors/src/Console/OpenFileDescriptorLimiter.php b/fixtures/bench/without-compactors/src/Console/OpenFileDescriptorLimiter.php new file mode 100644 index 000000000..bded877e8 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/OpenFileDescriptorLimiter.php @@ -0,0 +1,95 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Closure; +use Fidry\Console\IO; +use KevinGH\Box\Noop; +use KevinGH\Box\NotInstantiable; +use Symfony\Component\Console\Output\OutputInterface; +use function function_exists; +use function posix_getrlimit; +use function posix_setrlimit; +use function sprintf; +use const POSIX_RLIMIT_INFINITY; +use const POSIX_RLIMIT_NOFILE; + +/** + * @internal + */ +final class OpenFileDescriptorLimiter +{ + use NotInstantiable; + + private const LIMIT_MARGIN = 128; + + /** + * Bumps the maximum number of open file descriptor if necessary. + * + * @return Closure Callable to call to restore the original maximum number of open files descriptors + */ + public static function bumpLimit(int $count, IO $io): Closure + { + $count += self::LIMIT_MARGIN; // Add a little extra for good measure + + if (false === function_exists('posix_getrlimit') || false === function_exists('posix_setrlimit')) { + $io->writeln( + '[debug] Could not check the maximum number of open file descriptors: the functions "posix_getrlimit()" and ' + .'"posix_setrlimit" could not be found.', + OutputInterface::VERBOSITY_DEBUG, + ); + + return Noop::create(); + } + + $softLimit = posix_getrlimit()['soft openfiles']; + $hardLimit = posix_getrlimit()['hard openfiles']; + + if ($softLimit >= $count) { + return Noop::create(); + } + + $io->writeln( + sprintf( + '[debug] Increased the maximum number of open file descriptors from ("%s", "%s") to ("%s", "%s")' + .'', + $softLimit, + $hardLimit, + $count, + 'unlimited', + ), + OutputInterface::VERBOSITY_DEBUG, + ); + + posix_setrlimit( + POSIX_RLIMIT_NOFILE, + $count, + 'unlimited' === $hardLimit ? POSIX_RLIMIT_INFINITY : $hardLimit, + ); + + return static function () use ($io, $softLimit, $hardLimit): void { + posix_setrlimit( + POSIX_RLIMIT_NOFILE, + $softLimit, + 'unlimited' === $hardLimit ? POSIX_RLIMIT_INFINITY : $hardLimit, + ); + + $io->writeln( + '[debug] Restored the maximum number of open file descriptors', + OutputInterface::VERBOSITY_DEBUG, + ); + }; + } +} diff --git a/fixtures/bench/without-compactors/src/Console/OutputFormatterConfigurator.php b/fixtures/bench/without-compactors/src/Console/OutputFormatterConfigurator.php new file mode 100644 index 000000000..f06656610 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/OutputFormatterConfigurator.php @@ -0,0 +1,57 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Fidry\Console\IO; +use KevinGH\Box\NotInstantiable; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; + +/** + * Utility to configure the output formatter styles. + * + * @private + */ +final class OutputFormatterConfigurator +{ + use NotInstantiable; + + public static function configure(IO $io): void + { + self::configureFormatter( + $io->getOutput()->getFormatter(), + ); + } + + public static function configureFormatter(OutputFormatterInterface $outputFormatter): void + { + $outputFormatter->setStyle( + 'recommendation', + new OutputFormatterStyle('black', 'yellow'), + ); + $outputFormatter->setStyle( + 'warning', + new OutputFormatterStyle('white', 'red'), + ); + $outputFormatter->setStyle( + 'diff-expected', + new OutputFormatterStyle('green'), + ); + $outputFormatter->setStyle( + 'diff-actual', + new OutputFormatterStyle('red'), + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Console/PharInfoRenderer.php b/fixtures/bench/without-compactors/src/Console/PharInfoRenderer.php new file mode 100644 index 000000000..14559216c --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/PharInfoRenderer.php @@ -0,0 +1,480 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Closure; +use DateTimeImmutable; +use Fidry\Console\IO; +use KevinGH\Box\Noop; +use KevinGH\Box\NotInstantiable; +use KevinGH\Box\Phar\CompressionAlgorithm; +use KevinGH\Box\Phar\PharInfo; +use KevinGH\Box\RequirementChecker\Requirement; +use KevinGH\Box\RequirementChecker\RequirementType; +use SplFileInfo; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Path; +use function array_filter; +use function array_key_last; +use function array_map; +use function array_reduce; +use function array_sum; +use function array_values; +use function count; +use function implode; +use function is_array; +use function KevinGH\Box\format_size; +use function KevinGH\Box\format_size as format_size1; +use function key; +use function preg_match; +use function round; +use function Safe\filesize; +use function sprintf; + +/** + * Utility to write to the console output various PHAR related pieces of information. + * + * @private + */ +final class PharInfoRenderer +{ + use NotInstantiable; + + private const BOX_REQUIREMENTS = '.box/.requirements.php'; + private const BOX_VERSION_PATTERN = '/ \* Generated by Humbug Box (?.+)\.\s/'; + private const INDENT_SIZE = 2; + + public static function renderShortSummary( + PharInfo $pharInfo, + IO $io, + ?Closure $separator = null, + ): void { + $separator ??= Noop::create(); + + $methods = [ + self::renderCompression(...), + self::renderSignature(...), + self::renderMetadata(...), + self::renderTimestamp(...), + self::renderRequirementChecker(...), + self::renderContentsSummary(...), + ]; + + $lastIndex = count($methods) - 1; + + foreach ($methods as $index => $method) { + $method($pharInfo, $io); + + if ($index !== $lastIndex) { + $separator(); + } + } + } + + public static function renderVersion(PharInfo $pharInfo, IO $io): void + { + $io->writeln( + sprintf( + 'API Version: %s', + $pharInfo->getVersion(), + ), + ); + } + + public static function renderBoxVersion(PharInfo $pharInfo, IO $io): void + { + $version = self::extractBoxVersion($pharInfo); + + if (null === $version) { + return; + } + + $io->writeln( + sprintf( + 'Built with Box: %s', + $version, + ), + ); + $io->newLine(); + } + + public static function renderCompression(PharInfo $pharInfo, IO $io): void + { + $io->writeln( + sprintf( + 'Archive Compression: %s', + self::translateCompressionAlgorithm($pharInfo->getCompression()), + ), + ); + + $count = $pharInfo->getFilesCompressionCount(); + // Rename "none" to "None" + $count['None'] = $count[CompressionAlgorithm::NONE->name]; + unset($count[CompressionAlgorithm::NONE->name]); + $count = array_filter($count); + + $totalCount = array_sum($count); + + if (1 === count($count)) { + $io->writeln( + sprintf( + 'Files Compression: %s', + key($count), + ), + ); + + return; + } + + $io->writeln('Files Compression:'); + $lastAlgorithmName = array_key_last($count); + + $totalPercentage = 100; + + foreach ($count as $algorithmName => $nbrOfFiles) { + if ($lastAlgorithmName === $algorithmName) { + $percentage = $totalPercentage; + } else { + $percentage = round($nbrOfFiles * 100 / $totalCount, 2); + + $totalPercentage -= $percentage; + } + + $io->writeln( + sprintf( + ' - %s (%0.2f%%)', + $algorithmName, + $percentage, + ), + ); + } + } + + public static function renderSignature(PharInfo $pharInfo, IO $io): void + { + $signature = $pharInfo->getSignature(); + + if (null === $signature) { + $io->writeln('Signature unreadable'); + + return; + } + + $io->writeln( + sprintf( + 'Signature: %s', + $signature['hash_type'], + ), + ); + $io->writeln( + sprintf( + 'Signature Hash: %s', + $signature['hash'], + ), + ); + } + + public static function renderMetadata(PharInfo $pharInfo, IO $io): void + { + $metadata = $pharInfo->getNormalizedMetadata(); + + if (null === $metadata) { + $io->writeln('Metadata: None'); + } else { + $io->writeln('Metadata:'); + $io->writeln($metadata); + } + } + + public static function renderTimestamp(PharInfo $pharInfo, IO $io): void + { + $timestamp = $pharInfo->getTimestamp(); + $dateTime = (new DateTimeImmutable())->setTimestamp($timestamp); + + $io->writeln( + sprintf( + 'Timestamp: %s (%s)', + $timestamp, + $dateTime->format(DateTimeImmutable::ATOM), + ), + ); + } + + public static function renderRequirementChecker( + PharInfo $pharInfo, + IO $io, + ): void { + $requirements = $pharInfo->getFiles()[self::BOX_REQUIREMENTS] ?? null; + + if (null === $requirements) { + $io->writeln('RequirementChecker: Not found.'); + + return; + } + + $evaluatedRequirements = require $requirements->getPathname(); + + if (!is_array($evaluatedRequirements)) { + $io->writeln('RequirementChecker: Could not be checked.'); + + return; + } + + $io->write('RequirementChecker:'); + + if (0 === count($evaluatedRequirements)) { + $io->writeln(' No requirement found.'); + + return; + } + $io->writeln(''); + + [$required, $conflicting] = self::retrieveRequirements($evaluatedRequirements); + + self::renderRequiredSection($required, $io); + self::renderConflictingSection($conflicting, $io); + } + + public static function renderContentsSummary(PharInfo $pharInfo, IO $io): void + { + $count = array_filter($pharInfo->getFilesCompressionCount()); + $totalCount = array_sum($count); + + $io->writeln( + sprintf( + 'Contents:%s (%s)', + 1 === $totalCount ? ' 1 file' : " {$totalCount} files", + format_size( + filesize($pharInfo->getFile()), + ), + ), + ); + } + + /** + * @param false|positive-int|0 $maxDepth + * @param false|int $indent Nbr of indent or `false` + */ + public static function renderContent( + OutputInterface $output, + PharInfo $pharInfo, + int|false $maxDepth, + bool $indent, + ): void { + $depth = 0; + $renderedDirectories = []; + + foreach ($pharInfo->getFiles() as $splFileInfo) { + if (false !== $maxDepth && $depth > $maxDepth) { + continue; + } + + if ($indent) { + self::renderParentDirectoriesIfNecessary( + $splFileInfo, + $output, + $depth, + $renderedDirectories, + ); + } + + [ + 'compression' => $compression, + 'compressedSize' => $compressionSize, + ] = $pharInfo->getFileMeta($splFileInfo->getRelativePathname()); + + $compressionLine = CompressionAlgorithm::NONE === $compression + ? '[NONE]' + : "[{$compression->name}]"; + + self::print( + $output, + sprintf( + '%s %s - %s', + $indent + ? $splFileInfo->getFilename() + : $splFileInfo->getRelativePathname(), + $compressionLine, + format_size1($compressionSize), + ), + $depth, + $indent, + ); + } + } + + private static function extractBoxVersion(PharInfo $pharInfo): ?string + { + $stub = $pharInfo->getStubContent(); + + if (null !== $stub && 1 === preg_match(self::BOX_VERSION_PATTERN, $stub, $matches)) { + return $matches['version']; + } + + return null; + } + + /** + * @return array{Requirement[], Requirement[]} + */ + private static function retrieveRequirements(array $requirements): array + { + $evaluatedRequirements = array_map( + Requirement::fromArray(...), + $requirements, + ); + + [$required, $conflicting] = array_reduce( + $evaluatedRequirements, + static function ($carry, Requirement $requirement): array { + $hash = implode( + ':', + [ + $requirement->type->value, + $requirement->condition, + $requirement->source, + ], + ); + + if (RequirementType::EXTENSION_CONFLICT === $requirement->type) { + $carry[1][$hash] = $requirement; + } else { + $carry[0][$hash] = $requirement; + } + + return $carry; + }, + [[], []], + ); + + return [ + array_values($required), + array_values($conflicting), + ]; + } + + /** + * @param Requirement[] $required + */ + private static function renderRequiredSection( + array $required, + IO $io, + ): void { + if (0 === count($required)) { + return; + } + + $io->writeln(' Required:'); + $io->writeln( + array_map( + static fn (Requirement $requirement) => match ($requirement->type) { + RequirementType::PHP => sprintf( + ' - PHP %s (%s)', + $requirement->condition, + $requirement->source ?? 'root', + ), + RequirementType::EXTENSION => sprintf( + ' - ext-%s (%s)', + $requirement->condition, + $requirement->source ?? 'root', + ), + }, + $required, + ), + ); + } + + /** + * @param Requirement[] $conflicting + */ + private static function renderConflictingSection( + array $conflicting, + IO $io, + ): void { + if (0 === count($conflicting)) { + return; + } + + $io->writeln(' Conflict:'); + $io->writeln( + array_map( + static fn (Requirement $requirement) => sprintf( + ' - ext-%s (%s)', + $requirement->condition, + $requirement->source ?? 'root', + ), + $conflicting, + ), + ); + } + + private static function renderParentDirectoriesIfNecessary( + SplFileInfo $fileInfo, + OutputInterface $output, + int &$depth, + array &$renderedDirectories, + ): void { + $depth = 0; + $relativePath = $fileInfo->getRelativePath(); + + if ('' === $relativePath) { + // No parent directory: there is nothing to do. + return; + } + + $parentDirectories = explode( + '/', + Path::normalize($relativePath), + ); + + foreach ($parentDirectories as $index => $parentDirectory) { + if (array_key_exists($parentDirectory, $renderedDirectories)) { + ++$depth; + + continue; + } + + self::print( + $output, + "{$parentDirectory}/", + $index, + true, + ); + + $renderedDirectories[$parentDirectory] = true; + ++$depth; + } + + $depth = count($parentDirectories); + } + + private static function print( + OutputInterface $output, + string $message, + int $depth, + bool $indent, + ): void { + if ($indent) { + $output->write(str_repeat(' ', $depth * self::INDENT_SIZE)); + } + + $output->writeln($message); + } + + private static function translateCompressionAlgorithm(CompressionAlgorithm $algorithm): string + { + return CompressionAlgorithm::NONE === $algorithm ? 'None' : $algorithm->name; + } +} diff --git a/fixtures/bench/without-compactors/src/Console/Php/PhpSettingsHandler.php b/fixtures/bench/without-compactors/src/Console/Php/PhpSettingsHandler.php new file mode 100644 index 000000000..059716b90 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/Php/PhpSettingsHandler.php @@ -0,0 +1,159 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console\Php; + +use Composer\XdebugHandler\XdebugHandler; +use Fidry\FileSystem\FS; +use KevinGH\Box\Constants; +use KevinGH\Box\Phar\PharPhpSettings; +use Psr\Log\LoggerInterface; +use Webmozart\Assert\Assert; +use function getenv; +use function ini_get; +use function ini_set; +use function KevinGH\Box\format_size; +use function KevinGH\Box\memory_to_bytes; +use function sprintf; +use function trim; +use const PHP_EOL; + +/** + * @private + */ +final class PhpSettingsHandler extends XdebugHandler +{ + private LoggerInterface $logger; + private bool $pharReadonly; + + public function __construct(LoggerInterface $logger) + { + parent::__construct('box'); + + $this->setPersistent(); + + $this->setLogger($logger); + $this->logger = $logger; + + $this->pharReadonly = PharPhpSettings::isReadonly(); + $this->setPersistent(); + } + + public function check(): void + { + $this->bumpMemoryLimit(); + + parent::check(); + } + + protected function requiresRestart(bool $default): bool + { + if ($this->pharReadonly) { + $this->logger->debug('phar.readonly is enabled'); + + return true; + } + + $this->logger->debug('phar.readonly is disabled'); + + return parent::requiresRestart($default); + } + + protected function restart(array $command): void + { + // Disable phar.readonly if set + $this->disablePharReadonly(); + + parent::restart($command); + } + + private function disablePharReadonly(): void + { + if (PharPhpSettings::isReadonly()) { + Assert::notNull($this->tmpIni); + + FS::appendToFile($this->tmpIni, 'phar.readonly=0'.PHP_EOL); + + $this->logger->debug('Configured `phar.readonly=0`'); + } + } + + /** + * @see https://github.com/composer/composer/blob/34c371f5f23e25eb9aa54ccc65136cf50930612e/bin/composer#L20-L50 + */ + private function bumpMemoryLimit(): void + { + $userDefinedMemoryLimit = self::getUserDefinedMemoryLimit(); + + $memoryLimit = trim(ini_get('memory_limit')); + $memoryLimitInBytes = '-1' === $memoryLimit ? -1 : memory_to_bytes($memoryLimit); + + // Whether the memory limit should be dumped + $bumpMemoryLimit = ( + null === $userDefinedMemoryLimit + && -1 !== $memoryLimitInBytes + && $memoryLimitInBytes < 1024 * 1024 * 512 + ); + // Whether the memory limit should be set to the user defined memory limit + $setUserDefinedMemoryLimit = ( + null !== $userDefinedMemoryLimit + && $memoryLimitInBytes !== $userDefinedMemoryLimit + ); + + if ($bumpMemoryLimit && false === $setUserDefinedMemoryLimit) { + ini_set('memory_limit', '512M'); + + $this->logger->debug( + sprintf( + 'Changed the memory limit from "%s" to "%s"', + format_size($memoryLimitInBytes, 0), + '512M', + ), + ); + } elseif ($setUserDefinedMemoryLimit) { + ini_set('memory_limit', (string) $userDefinedMemoryLimit); + + $this->logger->debug( + sprintf( + 'Changed the memory limit from "%s" to %s="%s"', + format_size($memoryLimitInBytes, 0), + Constants::MEMORY_LIMIT, + format_size($userDefinedMemoryLimit, 0), + ), + ); + } else { + $this->logger->debug( + sprintf( + 'Current memory limit: "%s"', + format_size($memoryLimitInBytes, 0), + ), + ); + } + } + + private static function getUserDefinedMemoryLimit(): ?int + { + $memoryLimit = getenv(Constants::MEMORY_LIMIT); + + if (false === $memoryLimit) { + $memoryLimitInBytes = null; + } elseif ('-1' === $memoryLimit) { + $memoryLimitInBytes = -1; + } else { + $memoryLimitInBytes = memory_to_bytes($memoryLimit); + } + + return $memoryLimitInBytes; + } +} diff --git a/fixtures/bench/without-compactors/src/Console/PhpSettingsChecker.php b/fixtures/bench/without-compactors/src/Console/PhpSettingsChecker.php new file mode 100644 index 000000000..54908a792 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Console/PhpSettingsChecker.php @@ -0,0 +1,37 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Console; + +use Fidry\Console\IO; +use KevinGH\Box\Console\Php\PhpSettingsHandler; +use KevinGH\Box\NotInstantiable; +use Symfony\Component\Console\Logger\ConsoleLogger; + +/** + * @internal + */ +final class PhpSettingsChecker +{ + use NotInstantiable; + + public static function check(IO $io): void + { + (new PhpSettingsHandler( + new ConsoleLogger( + $io->getOutput(), + ), + ))->check(); + } +} diff --git a/fixtures/bench/without-compactors/src/Constants.php b/fixtures/bench/without-compactors/src/Constants.php new file mode 100644 index 000000000..8fa753326 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Constants.php @@ -0,0 +1,24 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +final class Constants +{ + use NotInstantiable; + + public const MEMORY_LIMIT = 'BOX_MEMORY_LIMIT'; + public const ALLOW_XDEBUG = 'BOX_ALLOW_XDEBUG'; + public const BIN = 'BOX_BIN'; +} diff --git a/fixtures/bench/without-compactors/src/DockerFileGenerator.php b/fixtures/bench/without-compactors/src/DockerFileGenerator.php new file mode 100644 index 000000000..f8ae840a9 --- /dev/null +++ b/fixtures/bench/without-compactors/src/DockerFileGenerator.php @@ -0,0 +1,163 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Composer\Semver\Semver; +use UnexpectedValueException; +use Webmozart\Assert\Assert; +use function array_column; +use function array_filter; +use function array_unique; +use function basename; +use function count; +use function implode; +use function sprintf; +use function strtr; +use const PHP_EOL; + +/** + * @private + */ +final class DockerFileGenerator +{ + private const FILE_TEMPLATE = <<<'Dockerfile' + FROM php:__BASE_PHP_IMAGE_TOKEN__ + + COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ + __REQUIRED_EXTENSIONS__ + COPY __PHAR_FILE_PATH_TOKEN__ /__PHAR_FILE_NAME_TOKEN__ + + ENTRYPOINT ["/__PHAR_FILE_NAME_TOKEN__"] + + Dockerfile; + + private const PHP_DOCKER_IMAGES = [ + // TODO: allow future images + '8.2.0' => '8.2-cli-alpine', + '8.1.0' => '8.1-cli-alpine', + '8.0.0' => '8.0-cli-alpine', + '7.4.0' => '7.4-cli-alpine', + '7.3.0' => '7.3-cli-alpine', + '7.2.0' => '7.2-cli-alpine', + '7.1.0' => '7.1-cli-alpine', + '7.0.0' => '7-cli-alpine', + ]; + + private string $image; + + /** + * @var string[] + */ + private array $extensions; + + /** + * Creates a new instance of the generator. + * + * @param array $requirements List of requirements following the format defined by the RequirementChecker component + * @param string $sourcePhar source PHAR location; This PHAR is going to be copied over to the image so the path + * should either be absolute or relative to the location of the Dockerfile + */ + public static function createForRequirements(array $requirements, string $sourcePhar): self + { + return new self( + self::retrievePhpImageName($requirements), + self::retrievePhpExtensions($requirements), + $sourcePhar, + ); + } + + /** + * @param string[] $extensions + * @param string $sourcePhar source PHAR location; This PHAR is going to be copied over to the image so the path + * should either be absolute or relative to the location of the Dockerfile + */ + public function __construct( + string $image, + array $extensions, + private readonly string $sourcePhar, + ) { + Assert::inArray($image, self::PHP_DOCKER_IMAGES); + Assert::allString($extensions); + + $this->image = $image; + $this->extensions = $extensions; + } + + public function generateStub(): string + { + $requiredExtensions = 0 === count($this->extensions) + ? '' + : sprintf( + 'RUN install-php-extensions %s%s', + implode(' ', $this->extensions), + PHP_EOL, + ); + + return strtr( + self::FILE_TEMPLATE, + [ + '__BASE_PHP_IMAGE_TOKEN__' => $this->image, + '__PHAR_FILE_PATH_TOKEN__' => $this->sourcePhar, + '__PHAR_FILE_NAME_TOKEN__' => basename($this->sourcePhar), + '__REQUIRED_EXTENSIONS__' => $requiredExtensions, + ], + ); + } + + private static function retrievePhpImageName(array $requirements): string + { + $conditions = array_column( + array_filter( + $requirements, + static fn (array $requirement): bool => 'php' === $requirement['type'], + ), + 'condition', + ); + + foreach (self::PHP_DOCKER_IMAGES as $php => $image) { + foreach ($conditions as $condition) { + if (false === Semver::satisfies($php, $condition)) { + continue 2; + } + } + + return $image; + } + + throw new UnexpectedValueException( + sprintf( + 'Could not find a suitable Docker base image for the PHP constraint(s) "%s". Images available: "%s".', + implode('", "', $conditions), + implode('", "', self::PHP_DOCKER_IMAGES), + ), + ); + } + + /** + * @return string[] + */ + private static function retrievePhpExtensions(array $requirements): array + { + return array_unique( + array_column( + array_filter( + $requirements, + static fn (array $requirement): bool => 'extension' === $requirement['type'], + ), + 'condition', + ), + ); + } +} diff --git a/fixtures/bench/without-compactors/src/ExecutableFinder.php b/fixtures/bench/without-compactors/src/ExecutableFinder.php new file mode 100644 index 000000000..f692d0cde --- /dev/null +++ b/fixtures/bench/without-compactors/src/ExecutableFinder.php @@ -0,0 +1,52 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use RuntimeException; +use Symfony\Component\Process\PhpExecutableFinder as SymfonyPhpExecutableFinder; + +final class ExecutableFinder +{ + private static string $boxExecutable; + private static string $phpExecutable; + + public static function findBoxExecutable(): string + { + if (isset(self::$boxExecutable)) { + return self::$boxExecutable; + } + + self::$boxExecutable = getenv(Constants::BIN) ?: $_SERVER['SCRIPT_NAME']; + + return self::$boxExecutable; + } + + public static function findPhpExecutable(): string + { + if (isset(self::$phpExecutable)) { + return self::$phpExecutable; + } + + $phpExecutable = (new SymfonyPhpExecutableFinder())->find(); + + if (false === $phpExecutable) { + throw new RuntimeException('Could not find a PHP executable.'); + } + + self::$phpExecutable = $phpExecutable; + + return self::$phpExecutable; + } +} diff --git a/fixtures/bench/without-compactors/src/Json/Json.php b/fixtures/bench/without-compactors/src/Json/Json.php new file mode 100644 index 000000000..a49b999fd --- /dev/null +++ b/fixtures/bench/without-compactors/src/Json/Json.php @@ -0,0 +1,110 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Json; + +use Fidry\FileSystem\FS; +use JsonSchema\Validator; +use Seld\JsonLint\JsonParser; +use Seld\JsonLint\ParsingException; +use stdClass; +use function implode; +use function json_decode; +use function json_last_error; +use const JSON_ERROR_NONE; +use const JSON_ERROR_UTF8; + +/** + * @private + */ +final class Json +{ + private JsonParser $linter; + + public function __construct() + { + $this->linter = new JsonParser(); + } + + /** + * @throws ParsingException + */ + public function lint(string $json): void + { + $result = $this->linter->lint($json); + + if ($result instanceof ParsingException) { + throw $result; + } + } + + /** + * @throws ParsingException + */ + public function decode(string $json, bool $assoc = false): array|stdClass + { + $data = json_decode($json, $assoc); + + if (JSON_ERROR_NONE !== ($error = json_last_error())) { + // Swallow the UTF-8 error and relies on the lint instead otherwise + if (JSON_ERROR_UTF8 === $error) { + throw new ParsingException('JSON decoding failed: Malformed UTF-8 characters, possibly incorrectly encoded'); + } + + $this->lint($json); + } + + return false === $assoc ? (object) $data : $data; // If JSON is an empty JSON json_decode returns an empty + // array instead of an stdClass instance + } + + /** + * @throws ParsingException + */ + public function decodeFile(string $file, bool $assoc = false): array|stdClass + { + $json = FS::getFileContents($file); + + return $this->decode($json, $assoc); + } + + /** + * Validates the decoded JSON data. + * + * @param string $file The JSON file + * @param stdClass $json The decoded JSON data + * @param stdClass $schema The JSON schema + * + * @throws JsonValidationException If the JSON data failed validation + */ + public function validate(string $file, stdClass $json, stdClass $schema): void + { + $validator = new Validator(); + $validator->check($json, $schema); + + if (!$validator->isValid()) { + $errors = []; + + foreach ($validator->getErrors() as $error) { + $errors[] = ($error['property'] ? $error['property'].' : ' : '').$error['message']; + } + + $message = [] !== $errors + ? "\"{$file}\" does not match the expected JSON schema:\n - ".implode("\n - ", $errors) + : "\"{$file}\" does not match the expected JSON schema."; + + throw new JsonValidationException($message, $file, $errors); + } + } +} diff --git a/fixtures/bench/without-compactors/src/Json/JsonValidationException.php b/fixtures/bench/without-compactors/src/Json/JsonValidationException.php new file mode 100644 index 000000000..288837755 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Json/JsonValidationException.php @@ -0,0 +1,62 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Json; + +use Throwable; +use UnexpectedValueException; +use Webmozart\Assert\Assert; + +/** + * @private + */ +final class JsonValidationException extends UnexpectedValueException +{ + private ?string $validatedFile; + private array $errors; + + /** + * @param string[] $errors + */ + public function __construct( + string $message, + ?string $file = null, + array $errors = [], + int $code = 0, + ?Throwable $previous = null, + ) { + if (null !== $file) { + Assert::file($file); + } + Assert::allString($errors); + + $this->validatedFile = $file; + $this->errors = $errors; + + parent::__construct($message, $code, $previous); + } + + public function getValidatedFile(): ?string + { + return $this->validatedFile; + } + + /** + * @return string[] + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/fixtures/bench/without-compactors/src/MapFile.php b/fixtures/bench/without-compactors/src/MapFile.php new file mode 100644 index 000000000..2bedefd79 --- /dev/null +++ b/fixtures/bench/without-compactors/src/MapFile.php @@ -0,0 +1,68 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Symfony\Component\Filesystem\Path; +use function preg_quote; +use function preg_replace; + +/** + * @internal + * + * @private + */ +final class MapFile +{ + /** + * @param string[][] $map + */ + public function __construct( + // Cannot have readonly properties: requires to be serializable + private string $basePath, + private array $map, + ) { + } + + public function __invoke(string $path): ?string + { + $relativePath = Path::makeRelative($path, $this->basePath); + + foreach ($this->map as $item) { + foreach ($item as $match => $replace) { + if ('' === $match) { + return $replace.'/'.$relativePath; + } + + if (str_starts_with($relativePath, $match)) { + return preg_replace( + '/^'.preg_quote($match, '/').'/', + $replace, + $relativePath, + ); + } + } + } + + return $relativePath; + } + + /** + * @return string[][] $map + */ + public function getMap(): array + { + return $this->map; + } +} diff --git a/fixtures/bench/without-compactors/src/Noop.php b/fixtures/bench/without-compactors/src/Noop.php new file mode 100644 index 000000000..3a7a12ebd --- /dev/null +++ b/fixtures/bench/without-compactors/src/Noop.php @@ -0,0 +1,42 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Closure; + +/** + * @private + */ +final class Noop +{ + use NotInstantiable; + + /** + * @var Closure():void + */ + private static Closure $noop; + + /** + * @return Closure():void + */ + public static function create(): Closure + { + if (!isset(self::$noop)) { + self::$noop = static function (): void {}; + } + + return self::$noop; + } +} diff --git a/fixtures/bench/without-compactors/src/NotInstantiable.php b/fixtures/bench/without-compactors/src/NotInstantiable.php new file mode 100644 index 000000000..5bd2a242c --- /dev/null +++ b/fixtures/bench/without-compactors/src/NotInstantiable.php @@ -0,0 +1,25 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +/** + * @private + */ +trait NotInstantiable +{ + private function __construct() + { + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/CompressionAlgorithm.php b/fixtures/bench/without-compactors/src/Phar/CompressionAlgorithm.php new file mode 100644 index 000000000..e8860b749 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/CompressionAlgorithm.php @@ -0,0 +1,71 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use Phar; +use function array_keys; +use function array_search; + +/** + * The required extension to execute the PHAR now that it is compressed. + * + * This is a tiny wrapper around the PHAR compression algorithm + * to make it a bit more type-safe and convenient to work with. + * + * @private + */ +enum CompressionAlgorithm: int +{ + case GZ = Phar::GZ; + case BZ2 = Phar::BZ2; + case NONE = Phar::NONE; + + private const LABELS = [ + 'GZ' => self::GZ, + 'BZ2' => self::BZ2, + 'NONE' => self::NONE, + ]; + + /** + * @return list + */ + public static function getLabels(): array + { + return array_keys(self::LABELS); + } + + public static function fromLabel(?string $label): self + { + return match ($label) { + 'BZ2' => self::BZ2, + 'GZ' => self::GZ, + 'NONE', null => self::NONE, + }; + } + + public function getLabel(): string + { + return array_search($this, self::LABELS, true); + } + + public function getRequiredExtension(): ?string + { + return match ($this) { + self::BZ2 => 'bz2', + self::GZ => 'zlib', + self::NONE => null, + }; + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/DiffMode.php b/fixtures/bench/without-compactors/src/Phar/DiffMode.php new file mode 100644 index 000000000..f2e651717 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/DiffMode.php @@ -0,0 +1,36 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use function array_map; + +enum DiffMode: string +{ + case FILE_NAME = 'file-name'; + case GIT = 'git'; + case GNU = 'gnu'; + case CHECKSUM = 'checksum'; + + /** + * @return list + */ + public static function values(): array + { + return array_map( + static fn (self $enum) => $enum->value, + self::cases(), + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/Differ/ChecksumDiffer.php b/fixtures/bench/without-compactors/src/Phar/Differ/ChecksumDiffer.php new file mode 100644 index 000000000..06121135d --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/Differ/ChecksumDiffer.php @@ -0,0 +1,136 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar\Differ; + +use Fidry\Console\IO; +use KevinGH\Box\Phar\PharInfo; +use UnexpectedValueException; +use ValueError; +use function hash; +use function implode; + +final class ChecksumDiffer implements Differ +{ + public function __construct( + private string $checksumAlgorithm, + ) { + } + + public function diff( + PharInfo $pharInfoA, + PharInfo $pharInfoB, + IO $io, + ): void { + $diff = self::computeDiff( + $pharInfoA, + $pharInfoB, + $this->checksumAlgorithm, + ); + + $io->writeln($diff ?? Differ::NO_DIFF_MESSAGE); + } + + private static function computeDiff( + PharInfo $pharInfoA, + PharInfo $pharInfoB, + string $checksumAlgorithm, + ): ?string { + $pharInfoAFileHashes = self::getFileHashesByRelativePathname( + $pharInfoA, + $checksumAlgorithm, + ); + $pharInfoBFileHashes = self::getFileHashesByRelativePathname( + $pharInfoB, + $checksumAlgorithm, + ); + $output = [ + '--- PHAR A', + '+++ PHAR B', + '@@ @@', + ]; + + foreach ($pharInfoAFileHashes as $filePath => $fileAHash) { + if (!array_key_exists($filePath, $pharInfoBFileHashes)) { + $output[] = $filePath; + $output[] = sprintf( + "\t- %s", + $fileAHash, + ); + + continue; + } + + $fileBHash = $pharInfoBFileHashes[$filePath]; + unset($pharInfoBFileHashes[$filePath]); + + if ($fileAHash === $fileBHash) { + continue; + } + + $output[] = $filePath; + $output[] = sprintf( + "\t- %s", + $fileAHash, + ); + $output[] = sprintf( + "\t+ %s", + $fileBHash, + ); + } + + foreach ($pharInfoBFileHashes as $filePath => $fileBHash) { + $output[] = $filePath; + $output[] = sprintf( + "\t+ %s", + $fileBHash, + ); + } + + return 3 === count($output) ? null : implode("\n", $output); + } + + /** + * @return array + */ + private static function getFileHashesByRelativePathname( + PharInfo $pharInfo, + string $algorithm, + ): array { + $hashFiles = []; + + try { + $hashFiles[$pharInfo->getStubPath()] = hash( + $algorithm, + $pharInfo->getStubContent(), + ); + + foreach ($pharInfo->getFiles() as $file) { + $hashFiles[$file->getRelativePathname()] = hash_file( + $algorithm, + $file->getPathname(), + ); + } + } catch (ValueError) { + throw new UnexpectedValueException( + sprintf( + 'Unexpected algorithm "%s". Please pick a registered hashing algorithm (checksum `hash_algos()`).', + $algorithm, + ), + ); + } + + return $hashFiles; + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/Differ/Differ.php b/fixtures/bench/without-compactors/src/Phar/Differ/Differ.php new file mode 100644 index 000000000..afdf6bd87 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/Differ/Differ.php @@ -0,0 +1,29 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar\Differ; + +use Fidry\Console\IO; +use KevinGH\Box\Phar\PharInfo; + +interface Differ +{ + public const NO_DIFF_MESSAGE = 'No difference could be observed with this mode.'; + + public function diff( + PharInfo $pharInfoA, + PharInfo $pharInfoB, + IO $io, + ): void; +} diff --git a/fixtures/bench/without-compactors/src/Phar/Differ/DifferFactory.php b/fixtures/bench/without-compactors/src/Phar/Differ/DifferFactory.php new file mode 100644 index 000000000..aeeeffe45 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/Differ/DifferFactory.php @@ -0,0 +1,33 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar\Differ; + +use KevinGH\Box\Console\Command\Extract; +use KevinGH\Box\Phar\DiffMode; + +final class DifferFactory +{ + public function create( + DiffMode $mode, + string $checksumAlgorithm, + ): Differ { + return match ($mode) { + DiffMode::FILE_NAME => new FilenameDiffer(), + DiffMode::GIT => new GitDiffer(), + DiffMode::GNU => new ProcessCommandBasedDiffer('diff --exclude='.Extract::PHAR_META_PATH), + DiffMode::CHECKSUM => new ChecksumDiffer($checksumAlgorithm), + }; + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/Differ/FilenameDiffer.php b/fixtures/bench/without-compactors/src/Phar/Differ/FilenameDiffer.php new file mode 100644 index 000000000..706d0af3d --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/Differ/FilenameDiffer.php @@ -0,0 +1,148 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar\Differ; + +use Fidry\Console\IO; +use KevinGH\Box\Console\PharInfoRenderer; +use KevinGH\Box\Phar\PharInfo; +use SplFileInfo; +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\Finder\Finder; +use function array_diff; +use function array_map; +use function array_sum; +use function count; +use function explode; +use function iterator_to_array; +use function sprintf; +use function str_replace; + +final class FilenameDiffer implements Differ +{ + public function diff( + PharInfo $pharInfoA, + PharInfo $pharInfoB, + IO $io, + ): void { + $pharAFiles = self::collectFiles($pharInfoA); + $pharBFiles = self::collectFiles($pharInfoB); + + $diffResult = [ + array_diff($pharAFiles, $pharBFiles), + array_diff($pharBFiles, $pharAFiles), + ]; + $diffCount = array_sum(array_map('count', $diffResult)); + + if (0 === $diffCount) { + $io->writeln(Differ::NO_DIFF_MESSAGE); + + return; + } + + self::printDiff( + $pharInfoA, + $pharInfoB, + $diffResult[0], + $diffResult[1], + $io, + ); + } + + /** + * @param list $filesInANotInB + * @param list $filesInBNotInA + */ + private static function printDiff( + PharInfo $pharInfoA, + PharInfo $pharInfoB, + array $filesInANotInB, + array $filesInBNotInA, + IO $io, + ): void { + $io->writeln(sprintf( + '--- Files present in "%s" but not in "%s"', + $pharInfoA->getFileName(), + $pharInfoB->getFileName(), + )); + $io->writeln(sprintf( + '+++ Files present in "%s" but not in "%s"', + $pharInfoB->getFileName(), + $pharInfoA->getFileName(), + )); + + $io->newLine(); + + self::renderPaths('-', $pharInfoA, $filesInANotInB, $io); + $io->newLine(); + self::renderPaths('+', $pharInfoB, $filesInBNotInA, $io); + + $io->newLine(2); + + $io->error( + sprintf( + '%d file(s) difference', + count($filesInANotInB) + count($filesInBNotInA), + ), + ); + } + + /** + * @param list $paths + */ + private static function renderPaths(string $symbol, PharInfo $pharInfo, array $paths, IO $io): void + { + $bufferedOutput = new BufferedOutput( + $io->getVerbosity(), + $io->isDecorated(), + $io->getOutput()->getFormatter(), + ); + + PharInfoRenderer::renderContent( + $bufferedOutput, + $pharInfo, + false, + false, + ); + + $lines = array_map( + static fn (string $line) => '' === $line ? '' : $symbol.' '.$line, + explode( + PHP_EOL, + $bufferedOutput->fetch(), + ), + ); + + $io->write($lines); + } + + /** + * @return string[] + */ + private static function collectFiles(PharInfo $pharInfo): array + { + $basePath = $pharInfo->getTmp().DIRECTORY_SEPARATOR; + + return array_map( + static fn (SplFileInfo $fileInfo): string => str_replace($basePath, '', $fileInfo->getRealPath()), + iterator_to_array( + Finder::create() + ->files() + ->in($basePath) + ->ignoreDotFiles(false), + false, + ), + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/Differ/GitDiffer.php b/fixtures/bench/without-compactors/src/Phar/Differ/GitDiffer.php new file mode 100644 index 000000000..5e2302fa7 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/Differ/GitDiffer.php @@ -0,0 +1,65 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar\Differ; + +use Fidry\Console\IO; +use KevinGH\Box\Console\Command\Extract; +use KevinGH\Box\Phar\PharInfo; +use function array_filter; +use function explode; +use function implode; +use function sprintf; +use function str_starts_with; + +final class GitDiffer implements Differ +{ + public function diff(PharInfo $pharInfoA, PharInfo $pharInfoB, IO $io): void + { + $gitDiff = ProcessCommandBasedDiffer::getDiff( + $pharInfoA, + $pharInfoB, + 'git diff --no-index', + ); + + if (null === $gitDiff) { + $io->writeln(Differ::NO_DIFF_MESSAGE); + + return; + } + + $separator = 'diff --git '; + + $diffLines = explode( + $separator, + $gitDiff, + ); + + $pharMetaLine = sprintf( + 'a%2$s/%1$s b%3$s/%1$s', + Extract::PHAR_META_PATH, + $pharInfoA->getFileName(), + $pharInfoB->getFileName(), + ); + + $filteredLines = array_filter( + $diffLines, + static fn (string $line) => !str_starts_with($line, $pharMetaLine) + ); + + $filteredDiff = implode($separator, $filteredLines); + + $io->writeln('' === $filteredDiff ? Differ::NO_DIFF_MESSAGE : $filteredDiff); + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/Differ/ProcessCommandBasedDiffer.php b/fixtures/bench/without-compactors/src/Phar/Differ/ProcessCommandBasedDiffer.php new file mode 100644 index 000000000..eab2442a1 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/Differ/ProcessCommandBasedDiffer.php @@ -0,0 +1,79 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar\Differ; + +use Fidry\Console\IO; +use KevinGH\Box\Phar\PharInfo; +use Symfony\Component\Process\Process; + +final class ProcessCommandBasedDiffer implements Differ +{ + public function __construct(private string $command) + { + } + + public function diff(PharInfo $pharInfoA, PharInfo $pharInfoB, IO $io): void + { + $result = self::getDiff( + $pharInfoA, + $pharInfoB, + $this->command, + ); + + $io->writeln($result ?? Differ::NO_DIFF_MESSAGE); + } + + public static function getDiff(PharInfo $pharInfoA, PharInfo $pharInfoB, string $command): ?string + { + $pharInfoATmp = $pharInfoA->getTmp(); + $pharInfoBTmp = $pharInfoB->getTmp(); + + $pharInfoAFileName = $pharInfoA->getFileName(); + $pharInfoBFileName = $pharInfoB->getFileName(); + + $diffCommand = implode( + ' ', + [ + $command, + $pharInfoATmp, + $pharInfoBTmp, + ], + ); + + $diffProcess = Process::fromShellCommandline($diffCommand); + $diffProcess->run(); + + // We do not check if the process is successful as if there + // is a difference between the two files then the process + // _will_ be unsuccessful. + $diff = trim($diffProcess->getOutput()); + + if ('' === $diff) { + return null; + } + + return str_replace( + [ + $pharInfoATmp, + $pharInfoBTmp, + ], + [ + $pharInfoAFileName, + $pharInfoBFileName, + ], + $diff, + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/IncompariblePhars.php b/fixtures/bench/without-compactors/src/Phar/IncompariblePhars.php new file mode 100644 index 000000000..55913d967 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/IncompariblePhars.php @@ -0,0 +1,23 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +final class IncompariblePhars extends PharError +{ + public static function signedPhars(): self + { + return new self('Cannot compare PHARs which have an external public key.'); + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/InvalidPhar.php b/fixtures/bench/without-compactors/src/Phar/InvalidPhar.php new file mode 100644 index 000000000..f2eaf5d67 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/InvalidPhar.php @@ -0,0 +1,178 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use Throwable; +use UnexpectedValueException; +use function Safe\preg_match; +use function sprintf; +use function str_contains; +use function str_ends_with; +use function str_starts_with; +use function ucfirst; + +final class InvalidPhar extends PharError +{ + public static function fileNotLocal( + string $file, + ?string $originalFile = null, + ): self { + // Covers: + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L1328 + return new self( + sprintf( + 'Could not create a Phar or PharData instance for the file path "%s"%s. PHAR objects can only be created from local files.', + $file, + null === $originalFile + ? '' + : sprintf( + ' (of the original file "%s")', + $originalFile, + ), + ), + ); + } + + public static function fileNotFound( + string $file, + ?string $originalFile = null, + ): self { + return new self( + sprintf( + 'Could not find the file "%s"%s.', + $file, + null === $originalFile + ? '' + : sprintf( + ' (of the original file "%s")', + $originalFile, + ), + ), + ); + } + + public static function fileNotReadable(string $file): self + { + return new self( + sprintf( + 'Could not read the file "%s".', + $file, + ), + ); + } + + public static function forPhar( + string $file, + ?string $originalFile, + ?Throwable $previous, + ): self { + return new self( + self::mapThrowableToErrorMessage($file, $originalFile, $previous, false), + previous: $previous, + ); + } + + public static function forPharData( + string $file, + ?string $originalFile, + ?Throwable $previous, + ): self { + return new self( + self::mapThrowableToErrorMessage($file, $originalFile, $previous, true), + previous: $previous, + ); + } + + public static function forPharAndPharData( + string $file, + ?string $originalFile, + ?Throwable $previous, + ): self { + return new self( + self::mapThrowableToErrorMessage($file, $originalFile, $previous, null), + previous: $previous, + ); + } + + private static function mapThrowableToErrorMessage( + string $file, + ?string $originalFile, + ?Throwable $throwable, + ?bool $isPharData, + ): string { + if (null === $isPharData) { + $pharObject = 'Phar or PharData'; + } else { + $pharObject = $isPharData ? 'PharData' : 'Phar'; + } + + $errorMessageStart = sprintf( + 'Could not create a %s instance for the file "%s"%s', + $pharObject, + $file, + null === $originalFile + ? '' + : sprintf( + ' (of the original file "%s")', + $originalFile, + ), + ); + $message = $throwable?->getMessage() ?? ''; + + if ($throwable instanceof UnexpectedValueException) { + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L1330 + if (str_ends_with($message, 'file extension (or combination) not recognised or the directory does not exist')) { + return sprintf( + $errorMessageStart.'. The file must have the extension "%s".', + $isPharData ? '.zip", ".tar", ".tar.bz2" or ".tar.gz' : '.phar', + ); + } + + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L1791 + // and a few other similar errors. + if (str_starts_with($message, 'internal corruption of phar ')) { + preg_match('/^internal corruption of phar \".+\" \((?.+)\)$/', $message, $matches); + + return sprintf( + $errorMessageStart.'. The archive is corrupted: %s.', + ucfirst($matches['reason']), + ); + } + + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L874 + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L892 + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L903 + if (str_contains($message, ' openssl signature ')) { + return $errorMessageStart.'. The OpenSSL signature could not be read or verified.'; + } + + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L1002 + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L1012 + // https://github.com/php/php-src/blob/930db2b2d315b2acc917706cf76bed8b09f94b79/ext/phar/phar.c#L1024 + // And analogue ones for the other signatures + if (str_contains($message, ' has a broken signature') + || str_contains($message, ' signature could not be verified') + || str_contains($message, ' has a broken or unsupported signature') + ) { + return $errorMessageStart.'. The archive signature is broken.'; + } + } + + return sprintf( + $errorMessageStart.': %s', + $message, + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/PharDiff.php b/fixtures/bench/without-compactors/src/Phar/PharDiff.php new file mode 100644 index 000000000..9d3f6976d --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/PharDiff.php @@ -0,0 +1,68 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use Fidry\Console\IO; +use KevinGH\Box\Phar\Differ\DifferFactory; +use function array_map; + +/** + * @internal + */ +final class PharDiff +{ + private readonly PharInfo $pharInfoA; + private readonly PharInfo $pharInfoB; + private readonly DifferFactory $differFactory; + + public function __construct(string $pathA, string $pathB) + { + [$pharInfoA, $pharInfoB] = array_map( + static fn (string $path) => new PharInfo($path), + [$pathA, $pathB], + ); + + $this->pharInfoA = $pharInfoA; + $this->pharInfoB = $pharInfoB; + + $this->differFactory = new DifferFactory(); + } + + public function getPharInfoA(): PharInfo + { + return $this->pharInfoA; + } + + public function getPharInfoB(): PharInfo + { + return $this->pharInfoB; + } + + public function equals(): bool + { + return $this->pharInfoA->equals($this->pharInfoB); + } + + public function diff(DiffMode $mode, string $checksumAlgorithm, IO $io): void + { + $this->differFactory + ->create($mode, $checksumAlgorithm) + ->diff( + $this->pharInfoA, + $this->pharInfoB, + $io, + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/PharError.php b/fixtures/bench/without-compactors/src/Phar/PharError.php new file mode 100644 index 000000000..d3c8e06be --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/PharError.php @@ -0,0 +1,50 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/* + * This file originates from https://github.com/paragonie/pharaoh. + * + * For maintenance reasons it had to be in-lined within Box. To simplify the + * configuration for PHP-CS-Fixer, the original license is in-lined as follows: + * + * The MIT License (MIT) + * + * Copyright (c) 2015 - 2018 Paragon Initiative Enterprises + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace KevinGH\Box\Phar; + +use Exception; + +class PharError extends Exception +{ +} diff --git a/fixtures/bench/without-compactors/src/Phar/PharFactory.php b/fixtures/bench/without-compactors/src/Phar/PharFactory.php new file mode 100644 index 000000000..6cdc7df81 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/PharFactory.php @@ -0,0 +1,119 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use Phar; +use PharData; +use Symfony\Component\Filesystem\Path; +use Throwable; +use function file_exists; + +/** + * Factory class to instantiate an _existing_ file (i.e. not to create a brand-new PHAR object). + * It is a thin wrapper around the native PHP constructor but with more friendly errors upon failure. + */ +final class PharFactory +{ + private function __construct() + { + } + + /** + * @throws InvalidPhar + */ + public static function create( + string $file, + ?string $originalFile = null, + ): Phar|PharData { + if (!Path::isLocal($file)) { + // This is needed as otherwise Phar::__construct() does correctly bail out on a URL + // path, but not on other non-local variants, e.g. FTPS, which case it may fail still + // but after a timeout, which is too slow. + throw InvalidPhar::fileNotLocal($file, $originalFile); + } + + if (!file_exists($file)) { + // We need to check this case since the goal of this factory is to instantiate an existing + // PHAR, not create a new one. + throw InvalidPhar::fileNotFound($file, $originalFile); + } + + try { + return new Phar($file); + } catch (Throwable $cannotCreatePhar) { + // Continue + } + + try { + return new PharData($file); + } catch (Throwable) { + throw InvalidPhar::forPharAndPharData($file, $originalFile, $cannotCreatePhar); + } + } + + /** + * @throws InvalidPhar + */ + public static function createPhar( + string $file, + ?string $originalFile = null, + ): Phar { + if (!Path::isLocal($file)) { + // This is needed as otherwise Phar::__construct() does correctly bail out on a URL + // path, but not on other non-local variants, e.g. FTPS, which case it may fail still + // but after a timeout, which is too slow. + throw InvalidPhar::fileNotLocal($file, $originalFile); + } + + if (!file_exists($file)) { + // We need to check this case since the goal of this factory is to instantiate an existing + // PHAR, not create a new one. + throw InvalidPhar::fileNotFound($file, $originalFile); + } + + try { + return new Phar($file); + } catch (Throwable $throwable) { + throw InvalidPhar::forPhar($file, $originalFile, $throwable); + } + } + + /** + * @throws InvalidPhar + */ + public static function createPharData( + string $file, + ?string $originalFile = null, + ): PharData { + if (!Path::isLocal($file)) { + // This is needed as otherwise Phar::__construct() does correctly bail out on a URL + // path, but not on other non-local variants, e.g. FTPS, which case it may fail still + // but after a timeout, which is too slow. + throw InvalidPhar::fileNotLocal($file, $originalFile); + } + + if (!file_exists($file)) { + // We need to check this case since the goal of this factory is to instantiate an existing + // PHAR, not create a new one. + throw InvalidPhar::fileNotFound($file); + } + + try { + return new PharData($file); + } catch (Throwable $throwable) { + throw InvalidPhar::forPharData($file, $originalFile, $throwable); + } + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/PharInfo.php b/fixtures/bench/without-compactors/src/Phar/PharInfo.php new file mode 100644 index 000000000..a71723cb1 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/PharInfo.php @@ -0,0 +1,344 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/* + * This file originates from https://github.com/paragonie/pharaoh. + * + * For maintenance reasons it had to be in-lined within Box. To simplify the + * configuration for PHP-CS-Fixer, the original license is in-lined as follows: + * + * The MIT License (MIT) + * + * Copyright (c) 2015 - 2018 Paragon Initiative Enterprises + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace KevinGH\Box\Phar; + +use Fidry\FileSystem\FS; +use KevinGH\Box\Console\Command\Extract; +use KevinGH\Box\ExecutableFinder; +use OutOfBoundsException; +use Phar; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; +use function bin2hex; +use function file_exists; +use function is_readable; +use function iter\mapKeys; +use function iter\toArrayWithKeys; +use function random_bytes; +use function sprintf; +use const DIRECTORY_SEPARATOR; + +/** + * @private + * + * PharInfo is a wrapper around the native Phar class. Its goal is to provide an equivalent API whilst being in-memory + * safe. + * + * Indeed, the native Phar API is extremely limited due to the fact that it loads the code in-memory. This pollutes the + * current process and will result in a crash if another PHAR with the same alias is loaded. This PharInfo class + * circumvents those issues by extracting all the desired information in a separate process. + */ +final class PharInfo +{ + private static array $ALGORITHMS; + private static string $stubfile; + + private PharMeta $meta; + private string $tmp; + private string $file; + private string $fileName; + private array $compressionCount; + + /** + * @var array + */ + private array $files; + + public function __construct(string $file) + { + $file = Path::canonicalize($file); + + if (!file_exists($file)) { + throw InvalidPhar::fileNotFound($file); + } + + if (!is_readable($file)) { + throw InvalidPhar::fileNotReadable($file); + } + + self::initAlgorithms(); + self::initStubFileName(); + + $this->file = $file; + $this->fileName = basename($file); + + $this->tmp = FS::makeTmpDir('HumbugBox', 'Pharaoh'); + + self::dumpPhar($file, $this->tmp); + [ + $this->meta, + $this->files, + ] = self::loadDumpedPharFiles($this->tmp); + } + + public function __destruct() + { + unset($this->pharInfo); + + if (isset($this->phar)) { + $path = $this->phar->getPath(); + unset($this->phar); + + Phar::unlinkArchive($path); + } + + if (isset($this->tmp)) { + FS::remove($this->tmp); + } + } + + public function getTmp(): string + { + return $this->tmp; + } + + public function getFile(): string + { + return $this->file; + } + + public function getPubKeyContent(): ?string + { + return $this->meta->pubKeyContent; + } + + public function hasPubKey(): bool + { + return null !== $this->getPubKeyContent(); + } + + public function getFileName(): string + { + return $this->fileName; + } + + public function equals(self $pharInfo): bool + { + return + $this->contentEquals($pharInfo) + && $this->getCompression() === $pharInfo->getCompression() + && $this->getNormalizedMetadata() === $pharInfo->getNormalizedMetadata(); + } + + /** + * Checks if the content of the given PHAR equals the current one. Note that by content is meant + * the list of files and their content. The files compression or the PHAR metadata are not considered. + */ + private function contentEquals(self $pharInfo): bool + { + // The signature only checks if the contents are equal (same files, each files same content), but do + // not check the compression of the files. + // As a result, we also need to check the compression of each file. + if ($this->getSignature() != $pharInfo->getSignature()) { + return false; + } + + foreach ($this->meta->filesMeta as $file => ['compression' => $compressionAlgorithm]) { + ['compression' => $otherCompressionAlgorithm] = $this->getFileMeta($file); + + if ($otherCompressionAlgorithm !== $compressionAlgorithm) { + return false; + } + } + + return true; + } + + public function getCompression(): CompressionAlgorithm + { + return $this->meta->compression; + } + + /** + * @return array The number of files per compression algorithm label. + */ + public function getFilesCompressionCount(): array + { + if (!isset($this->compressionCount)) { + $this->compressionCount = self::calculateCompressionCount($this->meta->filesMeta); + } + + return $this->compressionCount; + } + + /** + * @return array{'compression': CompressionAlgorithm, compressedSize: int} + */ + public function getFileMeta(string $path): array + { + $meta = $this->meta->filesMeta[$path] ?? null; + + if (null === $meta) { + throw new OutOfBoundsException( + sprintf( + 'No metadata found for the file "%s".', + $path, + ), + ); + } + + return $meta; + } + + public function getVersion(): ?string + { + // TODO: review this fallback value + return $this->meta->version ?? 'No information found'; + } + + public function getNormalizedMetadata(): ?string + { + return $this->meta->normalizedMetadata; + } + + public function getTimestamp(): int + { + return $this->meta->timestamp; + } + + public function getSignature(): ?array + { + return $this->meta->signature; + } + + public function getStubPath(): string + { + return Extract::STUB_PATH; + } + + public function getStubContent(): ?string + { + return $this->meta->stub; + } + + /** + * @return array + */ + public function getFiles(): array + { + return $this->files; + } + + private static function initAlgorithms(): void + { + if (!isset(self::$ALGORITHMS)) { + self::$ALGORITHMS = []; + + foreach (CompressionAlgorithm::cases() as $compressionAlgorithm) { + self::$ALGORITHMS[$compressionAlgorithm->value] = $compressionAlgorithm->name; + } + } + } + + private static function initStubFileName(): void + { + if (!isset(self::$stubfile)) { + self::$stubfile = bin2hex(random_bytes(12)).'.pharstub'; + } + } + + private static function dumpPhar(string $file, string $tmp): void + { + $extractPharProcess = new Process([ + ExecutableFinder::findPhpExecutable(), + ExecutableFinder::findBoxExecutable(), + 'extract', + $file, + $tmp, + '--no-interaction', + '--internal', + ]); + $extractPharProcess->run(); + + if (false === $extractPharProcess->isSuccessful()) { + throw new InvalidPhar( + $extractPharProcess->getErrorOutput(), + $extractPharProcess->getExitCode(), + new ProcessFailedException($extractPharProcess), + ); + } + } + + /** + * @return array{PharMeta, array} + */ + private static function loadDumpedPharFiles(string $tmp): array + { + $dumpedFiles = toArrayWithKeys( + mapKeys( + static fn (string $filePath) => Path::makeRelative($filePath, $tmp), + Finder::create() + ->files() + ->ignoreDotFiles(false) + ->exclude('.phar') + ->in($tmp), + ), + ); + + $meta = PharMeta::fromJson(FS::getFileContents($tmp.DIRECTORY_SEPARATOR.Extract::PHAR_META_PATH)); + unset($dumpedFiles[Extract::PHAR_META_PATH]); + + return [$meta, $dumpedFiles]; + } + + /** + * @param array $filesMeta + */ + private static function calculateCompressionCount(array $filesMeta): array + { + $count = array_fill_keys( + self::$ALGORITHMS, + 0, + ); + + foreach ($filesMeta as ['compression' => $compression]) { + ++$count[$compression->name]; + } + + return $count; + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/PharMeta.php b/fixtures/bench/without-compactors/src/Phar/PharMeta.php new file mode 100644 index 000000000..af51a3a4b --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/PharMeta.php @@ -0,0 +1,199 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use JetBrains\PhpStorm\ArrayShape; +use Phar; +use PharData; +use PharFileInfo; +use RecursiveDirectoryIterator; +use SplFileInfo; +use Symfony\Component\Filesystem\Path; +use UnexpectedValueException; +use function ksort; +use function Safe\json_decode; +use function Safe\json_encode; +use function Safe\realpath; +use function sprintf; +use function var_export; +use const SORT_LOCALE_STRING; + +/** + * Represents the PHAR metadata (partially). The goal is to capture enough information to interpret a PHAR + * without instantiating a Phar or PharData instance. + * + * @private + */ +final class PharMeta +{ + /** + * @param non-empty-string|null $stub + * @param non-empty-string|null $version + * @param non-empty-string|null $normalizedMetadata + * @param non-empty-string|null $pubKeyContent + * @param array $filesMeta + */ + public function __construct( + public readonly CompressionAlgorithm $compression, + #[ArrayShape(['hash' => 'string', 'hash_type' => 'string'])] + public readonly ?array $signature, + public readonly ?string $stub, + public readonly ?string $version, + public readonly ?string $normalizedMetadata, + public readonly int $timestamp, + public readonly ?string $pubKeyContent, + public readonly array $filesMeta, + ) { + } + + public static function fromPhar(Phar|PharData $phar, ?string $pubKeyContent): self + { + $compression = $phar->isCompressed(); + $signature = $phar->getSignature(); + $stub = $phar->getStub(); + $version = $phar->getVersion(); + $metadata = $phar->getMetadata(); + $timestamp = $phar->getMTime(); + + return new self( + false === $compression ? CompressionAlgorithm::NONE : CompressionAlgorithm::from($compression), + false === $signature ? null : $signature, + '' === $stub ? null : $stub, + '' === $version ? null : $version, + // TODO: check $unserializeOptions here + null === $metadata ? null : var_export($metadata, true), + $timestamp, + $pubKeyContent, + self::collectFilesMeta($phar), + ); + } + + public static function fromJson(string $json): self + { + $decodedJson = json_decode($json, true); + + $filesMeta = $decodedJson['filesMeta']; + + foreach ($filesMeta as &$fileMeta) { + $fileMeta['compression'] = CompressionAlgorithm::from($fileMeta['compression']); + } + + return new self( + CompressionAlgorithm::from($decodedJson['compression']), + $decodedJson['signature'], + $decodedJson['stub'], + $decodedJson['version'], + $decodedJson['normalizedMetadata'], + $decodedJson['timestamp'], + $decodedJson['pubKeyContent'], + $filesMeta, + ); + } + + public function toJson(): string + { + return json_encode([ + 'compression' => $this->compression, + 'signature' => $this->signature, + 'stub' => $this->stub, + 'version' => $this->version, + 'normalizedMetadata' => $this->normalizedMetadata, + 'timestamp' => $this->timestamp, + 'pubKeyContent' => $this->pubKeyContent, + 'filesMeta' => $this->filesMeta, + ]); + } + + /** + * @return array + */ + private static function collectFilesMeta(Phar|PharData $phar): array + { + $filesMeta = []; + + $root = self::getPharRoot($phar); + + self::traverseSource( + $root, + $phar, + $filesMeta, + ); + + ksort($filesMeta, SORT_LOCALE_STRING); + + return $filesMeta; + } + + /** + * @param iterable $source + * + * @return array + */ + private static function traverseSource( + string $root, + iterable $source, + array &$filesMeta, + ): void { + foreach ($source as $path => $pharFileInfo) { + if (!($pharFileInfo instanceof PharFileInfo)) { + $pharFileInfo = new PharFileInfo($path); + } + + if ($pharFileInfo->isDir()) { + self::traverseSource( + $root, + new RecursiveDirectoryIterator($pharFileInfo->getPathname()), + $filesMeta, + ); + + continue; + } + + $relativePath = Path::makeRelative($path, $root); + + $filesMeta[$relativePath] = [ + 'compression' => self::getCompressionAlgorithm($pharFileInfo), + 'compressedSize' => $pharFileInfo->getCompressedSize(), + ]; + } + } + + private static function getPharRoot(Phar|PharData $phar): string + { + return 'phar://'.Path::normalize(realpath($phar->getPath())); + } + + private static function getCompressionAlgorithm(PharFileInfo $pharFileInfo): CompressionAlgorithm + { + if (false === $pharFileInfo->isCompressed()) { + return CompressionAlgorithm::NONE; + } + + foreach (CompressionAlgorithm::cases() as $compressionAlgorithm) { + if (CompressionAlgorithm::NONE !== $compressionAlgorithm + && $pharFileInfo->isCompressed($compressionAlgorithm->value) + ) { + return $compressionAlgorithm; + } + } + + throw new UnexpectedValueException( + sprintf( + 'Unknown compression algorithm for the file "%s', + $pharFileInfo->getPath(), + ), + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/PharPhpSettings.php b/fixtures/bench/without-compactors/src/Phar/PharPhpSettings.php new file mode 100644 index 000000000..7a581259c --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/PharPhpSettings.php @@ -0,0 +1,29 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use function ini_get; + +final class PharPhpSettings +{ + public static function isReadonly(): bool + { + return '1' === ini_get('phar.readonly'); + } + + private function __construct() + { + } +} diff --git a/fixtures/bench/without-compactors/src/Phar/SigningAlgorithm.php b/fixtures/bench/without-compactors/src/Phar/SigningAlgorithm.php new file mode 100644 index 000000000..a8bd4a610 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Phar/SigningAlgorithm.php @@ -0,0 +1,75 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Phar; + +use Phar; +use UnexpectedValueException; +use function array_keys; +use function array_search; + +/** + * The required extension to execute the PHAR now that it is compressed. + * + * This is a tiny wrapper around the PHAR compression algorithm + * to make it a bit more type-safe and convenient to work with. + * + * @private + */ +enum SigningAlgorithm: int +{ + case MD5 = Phar::MD5; + case SHA1 = Phar::SHA1; + case SHA256 = Phar::SHA256; + case SHA512 = Phar::SHA512; + case OPENSSL = Phar::OPENSSL; + + private const LABELS = [ + 'MD5' => Phar::MD5, + 'SHA1' => Phar::SHA1, + 'SHA256' => Phar::SHA256, + 'SHA512' => Phar::SHA512, + 'OPENSSL' => Phar::OPENSSL, + ]; + + /** + * @return list + */ + public static function getLabels(): array + { + return array_keys(self::LABELS); + } + + public static function fromLabel(string $label): self + { + return match ($label) { + 'MD5' => self::MD5, + 'SHA1' => self::SHA1, + 'SHA256' => self::SHA256, + 'SHA512' => self::SHA512, + 'OPENSSL' => self::OPENSSL, + default => throw new UnexpectedValueException( + sprintf( + 'The signing algorithm "%s" is not supported by your current PHAR version.', + $label, + ), + ), + }; + } + + public function getLabel(): string + { + return array_search($this, self::LABELS, true); + } +} diff --git a/fixtures/bench/without-compactors/src/PharInfo/PharInfo.php b/fixtures/bench/without-compactors/src/PharInfo/PharInfo.php new file mode 100644 index 000000000..27199a5f6 --- /dev/null +++ b/fixtures/bench/without-compactors/src/PharInfo/PharInfo.php @@ -0,0 +1,149 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PharInfo; + +use KevinGH\Box\Phar\CompressionAlgorithm; +use Phar; +use PharData; +use PharFileInfo; +use RecursiveIteratorIterator; +use UnexpectedValueException; +use function KevinGH\Box\unique_id; +use function realpath; +use function str_replace; + +/** + * @deprecated Deprecated since 4.4.1 in favour of \KevinGH\Box\Phar\PharInfo. + */ +final class PharInfo +{ + private static array $ALGORITHMS; + + private PharData|Phar $phar; + + private ?array $compressionCount = null; + private ?string $hash = null; + + public function __construct(string $pharFile) + { + self::initAlgorithms(); + + try { + $this->phar = new Phar($pharFile); + } catch (UnexpectedValueException) { + $this->phar = new PharData($pharFile); + } + } + + private static function initAlgorithms(): void + { + if (!isset(self::$ALGORITHMS)) { + self::$ALGORITHMS = []; + + foreach (CompressionAlgorithm::cases() as $compressionAlgorithm) { + self::$ALGORITHMS[$compressionAlgorithm->value] = $compressionAlgorithm->name; + } + } + } + + public function equals(self $pharInfo): bool + { + return + $pharInfo->getCompressionCount() === $this->getCompressionCount() + && $pharInfo->getNormalizedMetadata() === $this->getNormalizedMetadata(); + } + + public function getCompressionCount(): array + { + if (null === $this->compressionCount || $this->hash !== $this->getPharHash()) { + $this->compressionCount = $this->calculateCompressionCount(); + $this->compressionCount['None'] = $this->compressionCount[CompressionAlgorithm::NONE->name]; + unset($this->compressionCount[CompressionAlgorithm::NONE->name]); + $this->hash = $this->getPharHash(); + } + + return $this->compressionCount; + } + + public function getPhar(): Phar|PharData + { + return $this->phar; + } + + public function getRoot(): string + { + // Do not cache the result + return 'phar://'.str_replace('\\', '/', realpath($this->phar->getPath())).'/'; + } + + public function getVersion(): string + { + // Do not cache the result + return '' !== $this->phar->getVersion() ? $this->phar->getVersion() : 'No information found'; + } + + public function getNormalizedMetadata(): ?string + { + // Do not cache the result + $metadata = var_export($this->phar->getMetadata(), true); + + return 'NULL' === $metadata ? null : $metadata; + } + + private function getPharHash(): string + { + // If no signature is available (e.g. a tar.gz file), we generate a random hash to ensure + // it will always be invalidated + return $this->phar->getSignature()['hash'] ?? unique_id(''); + } + + private function calculateCompressionCount(): array + { + $count = array_fill_keys( + self::$ALGORITHMS, + 0, + ); + + if ($this->phar instanceof PharData) { + $count[self::$ALGORITHMS[$this->phar->isCompressed()]] = 1; + + return $count; + } + + $countFile = static function (array $count, PharFileInfo $file): array { + if (false === $file->isCompressed()) { + ++$count[CompressionAlgorithm::NONE->name]; + + return $count; + } + + foreach (self::$ALGORITHMS as $compressionAlgorithmCode => $compressionAlgorithmName) { + if ($file->isCompressed($compressionAlgorithmCode)) { + ++$count[$compressionAlgorithmName]; + + return $count; + } + } + + return $count; + }; + + return array_reduce( + iterator_to_array(new RecursiveIteratorIterator($this->phar), true), + $countFile, + $count, + ); + } +} diff --git a/fixtures/bench/without-compactors/src/PhpScoper/ConfigurationFactory.php b/fixtures/bench/without-compactors/src/PhpScoper/ConfigurationFactory.php new file mode 100644 index 000000000..35428a04f --- /dev/null +++ b/fixtures/bench/without-compactors/src/PhpScoper/ConfigurationFactory.php @@ -0,0 +1,46 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Configuration\Configuration as PhpScoperConfiguration; +use Humbug\PhpScoper\Container; +use InvalidArgumentException; +use KevinGH\Box\NotInstantiable; +use Throwable; +use function sprintf; + +final class ConfigurationFactory +{ + use NotInstantiable; + + public static function create(?string $filePath): PhpScoperConfiguration + { + $configFactory = (new Container())->getConfigurationFactory(); + + try { + return $configFactory->create($filePath); + } catch (Throwable $throwable) { + throw new InvalidArgumentException( + sprintf( + 'Could not create a PHP-Scoper config from the file "%s": %s', + $filePath, + $throwable->getMessage(), + ), + $throwable->getCode(), + $throwable, + ); + } + } +} diff --git a/fixtures/bench/without-compactors/src/PhpScoper/ExcludedFilesScoper.php b/fixtures/bench/without-compactors/src/PhpScoper/ExcludedFilesScoper.php new file mode 100644 index 000000000..fd3c8f90c --- /dev/null +++ b/fixtures/bench/without-compactors/src/PhpScoper/ExcludedFilesScoper.php @@ -0,0 +1,41 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Scoper\Scoper as PhpScoperScoper; +use function array_flip; +use function array_key_exists; +use function func_get_args; + +final class ExcludedFilesScoper implements PhpScoperScoper +{ + private array $excludedFilePathsAsKeys; + + public function __construct( + private PhpScoperScoper $decoratedScoper, + string ...$excludedFilePaths, + ) { + $this->excludedFilePathsAsKeys = array_flip($excludedFilePaths); + } + + public function scope(string $filePath, string $contents): string + { + if (array_key_exists($filePath, $this->excludedFilePathsAsKeys)) { + return $contents; + } + + return $this->decoratedScoper->scope(...func_get_args()); + } +} diff --git a/fixtures/bench/without-compactors/src/PhpScoper/NullScoper.php b/fixtures/bench/without-compactors/src/PhpScoper/NullScoper.php new file mode 100644 index 000000000..affe05f6f --- /dev/null +++ b/fixtures/bench/without-compactors/src/PhpScoper/NullScoper.php @@ -0,0 +1,53 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Symbol\SymbolsRegistry; + +/** + * @private + */ +final class NullScoper implements Scoper +{ + public function __construct( + private SymbolsRegistry $symbolsRegistry = new SymbolsRegistry(), + ) { + } + + public function scope(string $filePath, string $contents): string + { + return $contents; + } + + public function changeSymbolsRegistry(SymbolsRegistry $symbolsRegistry): void + { + $this->symbolsRegistry = $symbolsRegistry; + } + + public function getSymbolsRegistry(): SymbolsRegistry + { + return $this->symbolsRegistry; + } + + public function getPrefix(): string + { + return ''; + } + + public function getExcludedFilePaths(): array + { + return []; + } +} diff --git a/fixtures/bench/without-compactors/src/PhpScoper/PatcherFactory.php b/fixtures/bench/without-compactors/src/PhpScoper/PatcherFactory.php new file mode 100644 index 000000000..9afa8be0c --- /dev/null +++ b/fixtures/bench/without-compactors/src/PhpScoper/PatcherFactory.php @@ -0,0 +1,44 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Patcher\Patcher; +use Humbug\PhpScoper\Patcher\PatcherChain; +use KevinGH\Box\NotInstantiable; +use Laravel\SerializableClosure\SerializableClosure; + +final class PatcherFactory +{ + use NotInstantiable; + + /** + * @param callable[] $patcher + * + * @return SerializableClosure[] + */ + public static function createSerializablePatchers(Patcher $patcher): Patcher + { + if (!($patcher instanceof PatcherChain)) { + return $patcher; + } + + $serializablePatchers = array_map( + static fn (callable $patcher) => SerializablePatcher::create($patcher), + $patcher->getPatchers(), + ); + + return new PatcherChain($serializablePatchers); + } +} diff --git a/fixtures/bench/without-compactors/src/PhpScoper/Scoper.php b/fixtures/bench/without-compactors/src/PhpScoper/Scoper.php new file mode 100644 index 000000000..07ec340fd --- /dev/null +++ b/fixtures/bench/without-compactors/src/PhpScoper/Scoper.php @@ -0,0 +1,36 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Symbol\SymbolsRegistry; + +interface Scoper +{ + /** + * Scope AKA. apply the given prefix to the file in the appropriate way. + * + * @param string $filePath File to scope + * @param string $contents Contents of the file to scope + * + * @return string Contents of the file with the prefix applied + */ + public function scope(string $filePath, string $contents): string; + + public function changeSymbolsRegistry(SymbolsRegistry $symbolsRegistry): void; + + public function getSymbolsRegistry(): SymbolsRegistry; + + public function getPrefix(): string; +} diff --git a/fixtures/bench/without-compactors/src/PhpScoper/SerializablePatcher.php b/fixtures/bench/without-compactors/src/PhpScoper/SerializablePatcher.php new file mode 100644 index 000000000..14f51d7c0 --- /dev/null +++ b/fixtures/bench/without-compactors/src/PhpScoper/SerializablePatcher.php @@ -0,0 +1,47 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Closure; +use Humbug\PhpScoper\Patcher\Patcher; +use Laravel\SerializableClosure\SerializableClosure; +use function func_get_args; + +/** + * @var PatcherCallable = (string $filePath, string $prefix, string $contents): string + */ +final class SerializablePatcher implements Patcher +{ + public static function create(callable $patcher): self + { + if ($patcher instanceof Patcher) { + $patcher = static fn (mixed ...$args) => $patcher(...$args); + } + + return new self(new SerializableClosure($patcher)); + } + + /** + * @param PatcherCallable $patch + */ + private function __construct(private Closure|SerializableClosure $patch) + { + } + + public function __invoke(string $filePath, string $prefix, string $contents): string + { + return ($this->patch)(...func_get_args()); + } +} diff --git a/fixtures/bench/without-compactors/src/PhpScoper/SerializableScoper.php b/fixtures/bench/without-compactors/src/PhpScoper/SerializableScoper.php new file mode 100644 index 000000000..152c00e87 --- /dev/null +++ b/fixtures/bench/without-compactors/src/PhpScoper/SerializableScoper.php @@ -0,0 +1,122 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\PhpScoper; + +use Humbug\PhpScoper\Configuration\Configuration as PhpScoperConfiguration; +use Humbug\PhpScoper\Container as PhpScoperContainer; +use Humbug\PhpScoper\Scoper\Scoper as PhpScoperScoper; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use function count; + +/** + * @private + */ +final class SerializableScoper implements Scoper +{ + private PhpScoperConfiguration $scoperConfig; + private PhpScoperContainer $scoperContainer; + private PhpScoperScoper $scoper; + private SymbolsRegistry $symbolsRegistry; + + /** + * @var list + */ + public array $excludedFilePaths; + + public function __construct( + PhpScoperConfiguration $scoperConfig, + string ...$excludedFilePaths, + ) { + $this->scoperConfig = $scoperConfig->withPatcher( + PatcherFactory::createSerializablePatchers($scoperConfig->getPatcher()) + ); + $this->excludedFilePaths = $excludedFilePaths; + $this->symbolsRegistry = new SymbolsRegistry(); + } + + public function scope(string $filePath, string $contents): string + { + return $this->getScoper()->scope( + $filePath, + $contents, + ); + } + + public function changeSymbolsRegistry(SymbolsRegistry $symbolsRegistry): void + { + $this->symbolsRegistry = $symbolsRegistry; + + unset($this->scoper); + } + + public function getSymbolsRegistry(): SymbolsRegistry + { + return $this->symbolsRegistry; + } + + public function getPrefix(): string + { + return $this->scoperConfig->getPrefix(); + } + + private function getScoper(): PhpScoperScoper + { + if (isset($this->scoper)) { + return $this->scoper; + } + + if (!isset($this->scoperContainer)) { + $this->scoperContainer = new PhpScoperContainer(); + } + + $this->scoper = $this->createScoper(); + + return $this->scoper; + } + + public function __wakeup(): void + { + // We need to make sure that a fresh Scoper & PHP-Parser Parser/Lexer + // is used within a sub-process. + // Otherwise, there is a risk of data corruption or that a compatibility + // layer of some sorts (such as the tokens for PHP-Paser) is not + // triggered in the sub-process resulting in obscure errors + unset($this->scoper, $this->scoperContainer); + } + + private function createScoper(): PhpScoperScoper + { + $scoper = $this->scoperContainer + ->getScoperFactory() + ->createScoper( + $this->scoperConfig, + $this->symbolsRegistry, + ); + + if (0 === count($this->excludedFilePaths)) { + return $scoper; + } + + return new ExcludedFilesScoper( + $scoper, + ...$this->excludedFilePaths, + ); + } + + public function getExcludedFilePaths(): array + { + return $this->excludedFilePaths; + } +} diff --git a/fixtures/bench/without-compactors/src/RequirementChecker/AppRequirementsFactory.php b/fixtures/bench/without-compactors/src/RequirementChecker/AppRequirementsFactory.php new file mode 100644 index 000000000..283672ad0 --- /dev/null +++ b/fixtures/bench/without-compactors/src/RequirementChecker/AppRequirementsFactory.php @@ -0,0 +1,231 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +use KevinGH\Box\Phar\CompressionAlgorithm; +use function array_diff_key; +use function array_filter; +use function array_map; +use function array_merge_recursive; +use function array_values; + +/** + * Collect the list of requirements for running the application. + * + * @private + */ +final class AppRequirementsFactory +{ + private const SELF_PACKAGE = '__APPLICATION__'; + + /** + * @return list Configured requirements + */ + public static function create( + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): array { + return self::configureExtensionRequirements( + self::retrievePhpVersionRequirements($composerJson, $composerLock), + $composerJson, + $composerLock, + $compressionAlgorithm, + ); + } + + /** + * @return list + */ + private static function retrievePhpVersionRequirements( + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + ): array { + // If the application has a constraint on the PHP version, it is the authority. + return $composerLock->hasRequiredPhpVersion() || $composerJson->hasRequiredPhpVersion() + ? self::retrievePHPRequirementFromPlatform($composerJson, $composerLock) + : self::retrievePHPRequirementFromPackages($composerLock); + } + + /** + * @return list + */ + private static function retrievePHPRequirementFromPlatform( + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + ): array { + $requiredPhpVersion = $composerLock->getRequiredPhpVersion() ?? $composerJson->getRequiredPhpVersion(); + + return null === $requiredPhpVersion ? [] : [Requirement::forPHP($requiredPhpVersion, null)]; + } + + /** + * @return list + */ + private static function retrievePHPRequirementFromPackages(DecodedComposerLock $composerLock): array + { + return array_values( + array_map( + static fn (PackageInfo $packageInfo) => Requirement::forPHP( + $packageInfo->getRequiredPhpVersion(), + $packageInfo->getName(), + ), + array_filter( + $composerLock->getPackages(), + static fn (PackageInfo $packageInfo) => $packageInfo->hasRequiredPhpVersion(), + ), + ), + ); + } + + /** + * @param list $requirements + */ + private static function configureExtensionRequirements( + array $requirements, + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): array { + [$extensionRequirements, $extensionConflicts] = self::collectExtensionRequirements( + $composerJson, + $composerLock, + $compressionAlgorithm, + ); + + foreach ($extensionRequirements as $extension => $packages) { + foreach ($packages as $package) { + $requirements[] = Requirement::forRequiredExtension( + $extension, + self::SELF_PACKAGE === $package ? null : $package, + ); + } + } + + foreach ($extensionConflicts as $extension => $packages) { + foreach ($packages as $package) { + $requirements[] = Requirement::forConflictingExtension( + $extension, + self::SELF_PACKAGE === $package ? null : $package, + ); + } + } + + return $requirements; + } + + /** + * Collects the extension required. It also accounts for the polyfills, i.e. if the polyfill + * `symfony/polyfill-mbstring` is provided then the extension `ext-mbstring` will not be required. + * + * @return array{array>, array>} + */ + private static function collectExtensionRequirements( + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): array { + $requirements = []; + + $compressionAlgorithmRequiredExtension = $compressionAlgorithm->getRequiredExtension(); + + if (null !== $compressionAlgorithmRequiredExtension) { + $requirements[$compressionAlgorithmRequiredExtension] = [self::SELF_PACKAGE]; + } + + foreach ($composerLock->getPlatformExtensions() as $extension) { + $requirements[$extension] = [self::SELF_PACKAGE]; + } + + // If the lock is present it is the authority. If not fallback on the .json. It is pointless to check both + // since they will contain redundant information. + [$polyfills, $requirements, $conflicts] = $composerLock->isEmpty() + ? self::collectComposerJsonExtensionRequirements($composerJson, $requirements) + : self::collectComposerLockExtensionRequirements($composerLock, $requirements); + + $jsonConflicts = self::collectComposerJsonExtensionRequirements($composerJson, $requirements)[2]; + + return [ + array_diff_key($requirements, $polyfills), + array_merge_recursive($conflicts, $jsonConflicts), + ]; + } + + /** + * @param array> $requirements The key is the extension name and the value the list of sources (app literal string or the package name). + * + * @return array{array, array>, array>} + */ + private static function collectComposerJsonExtensionRequirements( + DecodedComposerJson $composerJson, + array $requirements, + ): array { + $polyfills = []; + $conflicts = []; + + foreach ($composerJson->getRequiredItems() as $packageInfo) { + $polyfilledExtension = $packageInfo->getPolyfilledExtension(); + + if (null !== $polyfilledExtension) { + $polyfills[$polyfilledExtension] = true; + + continue; + } + + foreach ($packageInfo->getRequiredExtensions() as $extension) { + $requirements[$extension] = [self::SELF_PACKAGE]; + } + } + + foreach ($composerJson->getConflictingExtensions() as $extension) { + $conflicts[$extension] = [self::SELF_PACKAGE]; + } + + return [ + $polyfills, + $requirements, + $conflicts, + ]; + } + + /** + * @param array> $requirements The key is the extension name and the value the list of sources (app literal string or the package name). + * + * @return array{array, array>, array>} + */ + private static function collectComposerLockExtensionRequirements( + DecodedComposerLock $composerLock, + array $requirements, + ): array { + $polyfills = []; + $conflicts = []; + + foreach ($composerLock->getPackages() as $packageInfo) { + foreach ($packageInfo->getPolyfilledExtensions() as $polyfilledExtension) { + $polyfills[$polyfilledExtension] = true; + } + + foreach ($packageInfo->getRequiredExtensions() as $extension) { + $requirements[$extension][] = $packageInfo->getName(); + } + + foreach ($packageInfo->getConflictingExtensions() as $extension) { + $conflicts[$extension][] = $packageInfo->getName(); + } + } + + return [$polyfills, $requirements, $conflicts]; + } +} diff --git a/fixtures/bench/without-compactors/src/RequirementChecker/DecodedComposerJson.php b/fixtures/bench/without-compactors/src/RequirementChecker/DecodedComposerJson.php new file mode 100644 index 000000000..01e0fb953 --- /dev/null +++ b/fixtures/bench/without-compactors/src/RequirementChecker/DecodedComposerJson.php @@ -0,0 +1,63 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +use function array_keys; + +/** + * @private + */ +final class DecodedComposerJson +{ + /** + * @param array $composerJsonDecodedContents Decoded JSON contents of the `composer.json` file + */ + public function __construct(private readonly array $composerJsonDecodedContents) + { + } + + public function getRequiredPhpVersion(): ?string + { + return $this->composerJsonDecodedContents['require']['php'] ?? null; + } + + public function hasRequiredPhpVersion(): bool + { + return null !== $this->getRequiredPhpVersion(); + } + + /** + * @return list + */ + public function getRequiredItems(): array + { + $require = $this->composerJsonDecodedContents['require'] ?? []; + + return array_map( + static fn (string $packageName) => new RequiredItem([$packageName => $require[$packageName]]), + array_keys($require), + ); + } + + /** + * @return list + */ + public function getConflictingExtensions(): array + { + return PackageInfo::parseExtensions( + $this->composerJsonDecodedContents['conflict'] ?? [], + ); + } +} diff --git a/fixtures/bench/without-compactors/src/RequirementChecker/DecodedComposerLock.php b/fixtures/bench/without-compactors/src/RequirementChecker/DecodedComposerLock.php new file mode 100644 index 000000000..ba5d7f473 --- /dev/null +++ b/fixtures/bench/without-compactors/src/RequirementChecker/DecodedComposerLock.php @@ -0,0 +1,64 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +use function array_map; + +/** + * @private + */ +final class DecodedComposerLock +{ + /** + * @param array $composerLockDecodedContents Decoded JSON contents of the `composer.lock` file + */ + public function __construct(private readonly array $composerLockDecodedContents) + { + } + + public function isEmpty(): bool + { + return [] === $this->composerLockDecodedContents; + } + + public function getRequiredPhpVersion(): ?string + { + return $this->composerLockDecodedContents['platform']['php'] ?? null; + } + + public function hasRequiredPhpVersion(): bool + { + return null !== $this->getRequiredPhpVersion(); + } + + /** + * @return list + */ + public function getPlatformExtensions(): array + { + return PackageInfo::parseExtensions($this->composerLockDecodedContents['platform'] ?? []); + } + + /** + * @return list + */ + public function getPackages(): array + { + return array_map( + static fn (array $package) => new PackageInfo($package), + $this->composerLockDecodedContents['packages'] ?? [], + ); + } +} diff --git a/fixtures/bench/without-compactors/src/RequirementChecker/PackageInfo.php b/fixtures/bench/without-compactors/src/RequirementChecker/PackageInfo.php new file mode 100644 index 000000000..3ff05aa25 --- /dev/null +++ b/fixtures/bench/without-compactors/src/RequirementChecker/PackageInfo.php @@ -0,0 +1,121 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +use function array_key_exists; + +/** + * @private + */ +final class PackageInfo +{ + private const EXTENSION_REGEX = '/^ext-(?.+)$/'; + + // Some extensions name differs in how they are registered in composer.json + // and the name used when doing a `extension_loaded()` check. + // See https://github.com/box-project/box/issues/653. + private const EXTENSION_NAME_MAP = [ + 'zend-opcache' => 'zend opcache', + ]; + + private const POLYFILL_MAP = [ + 'paragonie/sodium_compat' => 'libsodium', + 'phpseclib/mcrypt_compat' => 'mcrypt', + ]; + + private const SYMFONY_POLYFILL_REGEX = '/symfony\/polyfill-(?.+)/'; + + public function __construct(private readonly array $packageInfo) + { + } + + public function getName(): string + { + return $this->packageInfo['name']; + } + + public function getRequiredPhpVersion(): ?string + { + return $this->packageInfo['require']['php'] ?? null; + } + + public function hasRequiredPhpVersion(): bool + { + return null !== $this->getRequiredPhpVersion(); + } + + /** + * @return list + */ + public function getRequiredExtensions(): array + { + return self::parseExtensions($this->packageInfo['require'] ?? []); + } + + /** + * @return list + */ + public function getPolyfilledExtensions(): array + { + if (array_key_exists('provide', $this->packageInfo)) { + return self::parseExtensions($this->packageInfo['provide']); + } + + // TODO: remove the following code in 5.0. + $name = $this->packageInfo['name']; + + if (array_key_exists($name, self::POLYFILL_MAP)) { + return [self::POLYFILL_MAP[$name]]; + } + + if (1 !== preg_match(self::SYMFONY_POLYFILL_REGEX, $name, $matches)) { + return []; + } + + $extension = $matches['extension']; + + return str_starts_with($extension, 'php') ? [] : [$extension]; + } + + /** + * @return list + */ + public function getConflictingExtensions(): array + { + return array_key_exists('conflict', $this->packageInfo) + ? self::parseExtensions($this->packageInfo['conflict']) + : []; + } + + /** + * @param array $constraints + * + * @return list + */ + public static function parseExtensions(array $constraints): array + { + $extensions = []; + + foreach ($constraints as $package => $constraint) { + if (preg_match(self::EXTENSION_REGEX, $package, $matches)) { + $extension = $matches['extension']; + + $extensions[] = self::EXTENSION_NAME_MAP[$extension] ?? $extension; + } + } + + return $extensions; + } +} diff --git a/fixtures/bench/without-compactors/src/RequirementChecker/RequiredItem.php b/fixtures/bench/without-compactors/src/RequirementChecker/RequiredItem.php new file mode 100644 index 000000000..5bd38e19e --- /dev/null +++ b/fixtures/bench/without-compactors/src/RequirementChecker/RequiredItem.php @@ -0,0 +1,68 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +use function array_key_exists; +use function key; + +/** + * @private + */ +final class RequiredItem +{ + private const POLYFILL_MAP = [ + 'paragonie/sodium_compat' => 'libsodium', + 'phpseclib/mcrypt_compat' => 'mcrypt', + ]; + + private const SYMFONY_POLYFILL_REGEX = '/symfony\/polyfill-(?.+)/'; + + /** + * @param array $packageInfo + */ + public function __construct(private readonly array $packageInfo) + { + } + + public function getName(): string + { + return key($this->packageInfo); + } + + /** + * @return list + */ + public function getRequiredExtensions(): array + { + return PackageInfo::parseExtensions($this->packageInfo); + } + + public function getPolyfilledExtension(): ?string + { + $name = $this->getName(); + + if (array_key_exists($name, self::POLYFILL_MAP)) { + return self::POLYFILL_MAP[$name]; + } + + if (1 !== preg_match(self::SYMFONY_POLYFILL_REGEX, $name, $matches)) { + return null; + } + + $extension = $matches['extension']; + + return str_starts_with($extension, 'php') ? null : $extension; + } +} diff --git a/fixtures/bench/without-compactors/src/RequirementChecker/Requirement.php b/fixtures/bench/without-compactors/src/RequirementChecker/Requirement.php new file mode 100644 index 000000000..73feb9a7d --- /dev/null +++ b/fixtures/bench/without-compactors/src/RequirementChecker/Requirement.php @@ -0,0 +1,139 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +/** + * @private + */ +final class Requirement +{ + public function __construct( + public readonly RequirementType $type, + public readonly string $condition, + public readonly ?string $source, + public readonly string $message, + public readonly string $helpMessage, + ) { + } + + public static function forPHP(string $requiredPhpVersion, ?string $packageName): self + { + return new self( + RequirementType::PHP, + $requiredPhpVersion, + $packageName, + null === $packageName + ? sprintf( + 'This application requires a PHP version matching "%s".', + $requiredPhpVersion, + ) + : sprintf( + 'The package "%s" requires a PHP version matching "%s".', + $packageName, + $requiredPhpVersion, + ), + null === $packageName + ? sprintf( + 'This application requires a PHP version matching "%s".', + $requiredPhpVersion, + ) + : sprintf( + 'The package "%s" requires a PHP version matching "%s".', + $packageName, + $requiredPhpVersion, + ), + ); + } + + public static function forRequiredExtension(string $extension, ?string $packageName): self + { + return new self( + RequirementType::EXTENSION, + $extension, + $packageName, + null === $packageName + ? sprintf( + 'This application requires the extension "%s".', + $extension, + ) + : sprintf( + 'The package "%s" requires the extension "%s".', + $packageName, + $extension, + ), + null === $packageName + ? sprintf( + 'This application requires the extension "%s". You either need to enable it or request the application to be shipped with a polyfill for this extension.', + $extension, + ) + : sprintf( + 'The package "%s" requires the extension "%s". You either need to enable it or request the application to be shipped with a polyfill for this extension.', + $packageName, + $extension, + ), + ); + } + + public static function forConflictingExtension(string $extension, ?string $packageName): self + { + return new self( + RequirementType::EXTENSION_CONFLICT, + $extension, + $packageName, + null === $packageName + ? sprintf( + 'This application conflicts with the extension "%s".', + $extension, + ) + : sprintf( + 'The package "%s" conflicts with the extension "%s".', + $packageName, + $extension, + ), + null === $packageName + ? sprintf( + 'This application conflicts with the extension "%s". You need to disable it in order to run this application.', + $extension, + ) + : sprintf( + 'The package "%s" conflicts with the extension "%s". You need to disable it in order to run this application.', + $packageName, + $extension, + ), + ); + } + + public static function fromArray(array $value): self + { + return new self( + RequirementType::from($value['type']), + $value['condition'], + $value['source'] ?? null, + $value['message'], + $value['helpMessage'], + ); + } + + public function toArray(): array + { + return [ + 'type' => $this->type->value, + 'condition' => $this->condition, + 'source' => $this->source, + 'message' => $this->message, + 'helpMessage' => $this->helpMessage, + ]; + } +} diff --git a/fixtures/bench/without-compactors/src/RequirementChecker/RequirementType.php b/fixtures/bench/without-compactors/src/RequirementChecker/RequirementType.php new file mode 100644 index 000000000..e5edb1882 --- /dev/null +++ b/fixtures/bench/without-compactors/src/RequirementChecker/RequirementType.php @@ -0,0 +1,22 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +enum RequirementType: string +{ + case PHP = 'php'; + case EXTENSION = 'extension'; + case EXTENSION_CONFLICT = 'extension-conflict'; +} diff --git a/fixtures/bench/without-compactors/src/RequirementChecker/RequirementsDumper.php b/fixtures/bench/without-compactors/src/RequirementChecker/RequirementsDumper.php new file mode 100644 index 000000000..8399d8070 --- /dev/null +++ b/fixtures/bench/without-compactors/src/RequirementChecker/RequirementsDumper.php @@ -0,0 +1,95 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\RequirementChecker; + +use Fidry\FileSystem\FS; +use KevinGH\Box\Phar\CompressionAlgorithm; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; +use Webmozart\Assert\Assert; +use function array_map; +use function str_replace; +use function var_export; + +/** + * @private + */ +final class RequirementsDumper +{ + private const REQUIREMENTS_CONFIG_TEMPLATE = <<<'PHP' + + */ + public static function dump( + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): array { + Assert::directory(self::REQUIREMENT_CHECKER_PATH, 'Expected the requirement checker to have been dumped'); + + $filesWithContents = [ + self::dumpRequirementsConfig( + $composerJson, + $composerLock, + $compressionAlgorithm, + ), + ]; + + /** @var SplFileInfo[] $requirementCheckerFiles */ + $requirementCheckerFiles = Finder::create() + ->files() + ->in(self::REQUIREMENT_CHECKER_PATH); + + foreach ($requirementCheckerFiles as $file) { + $filesWithContents[] = [ + $file->getRelativePathname(), + FS::getFileContents($file->getPathname()), + ]; + } + + return $filesWithContents; + } + + private static function dumpRequirementsConfig( + DecodedComposerJson $composerJson, + DecodedComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): array { + $requirements = array_map( + static fn (Requirement $requirement) => $requirement->toArray(), + AppRequirementsFactory::create( + $composerJson, + $composerLock, + $compressionAlgorithm, + ), + ); + + return [ + '.requirements.php', + str_replace( + '\'__CONFIG__\'', + var_export($requirements, true), + self::REQUIREMENTS_CONFIG_TEMPLATE, + ), + ]; + } +} diff --git a/fixtures/bench/without-compactors/src/StubGenerator.php b/fixtures/bench/without-compactors/src/StubGenerator.php new file mode 100644 index 000000000..b5f704ab7 --- /dev/null +++ b/fixtures/bench/without-compactors/src/StubGenerator.php @@ -0,0 +1,170 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use function addcslashes; +use function implode; +use function str_replace; + +/** + * Generates a new PHP bootstrap loader stub for a PHAR. + * + * @private + */ +final class StubGenerator +{ + use NotInstantiable; + + private const CHECK_FILE_NAME = 'bin/check-requirements.php'; + + private const STUB_TEMPLATE = <<<'STUB' + __BOX_SHEBANG__ + + + STUB; + + /** + * @param null|string $alias The alias to be used in "phar://" URLs + * @param null|string $banner The top header comment banner text + * @param null|string $index The location within the PHAR of index script + * @param bool $intercept Use the Phar::interceptFileFuncs() method? + * @param null|non-empty-string $shebang The shebang line + */ + public static function generateStub( + ?string $alias = null, + ?string $banner = null, + ?string $index = null, + bool $intercept = false, + ?string $shebang = null, + bool $checkRequirements = true, + ): string { + $stub = self::STUB_TEMPLATE; + + $stub = str_replace( + "__BOX_SHEBANG__\n", + null === $shebang ? '' : $shebang."\n", + $stub, + ); + + $stub = str_replace( + "__BOX_BANNER__\n", + self::generateBannerStmt($banner), + $stub, + ); + + return str_replace( + "__BOX_PHAR_CONFIG__\n", + self::generatePharConfigStmt( + $alias, + $index, + $intercept, + $checkRequirements, + ), + $stub, + ); + } + + private static function generateBannerStmt(?string $banner): string + { + if (null === $banner) { + return ''; + } + + $generatedBanner = "/*\n * "; + + $generatedBanner .= str_replace( + " \n", + "\n", + str_replace("\n", "\n * ", $banner), + ); + + $generatedBanner .= "\n */"; + + return "\n".$generatedBanner."\n"; + } + + private static function getAliasStmt(?string $alias): ?string + { + return null !== $alias ? 'Phar::mapPhar('.self::arg($alias).');' : null; + } + + /** + * Escapes an argument so it can be written as a string in a call. + * + * @return string The escaped argument + */ + private static function arg(string $arg, string $quote = "'"): string + { + return $quote.addcslashes($arg, $quote).$quote; + } + + private static function generatePharConfigStmt( + ?string $alias = null, + ?string $index = null, + bool $intercept = false, + bool $checkRequirements = true, + ): string { + $previous = false; + $stub = []; + $aliasStmt = self::getAliasStmt($alias); + + if (null !== $aliasStmt) { + $stub[] = $aliasStmt; + + $previous = true; + } + + if ($intercept) { + $stub[] = 'Phar::interceptFileFuncs();'; + + $previous = true; + } + + if (false !== $checkRequirements) { + if ($previous) { + $stub[] = ''; + } + + $checkRequirementsFile = self::CHECK_FILE_NAME; + + $stub[] = null === $alias + ? "require 'phar://' . __FILE__ . '/.box/{$checkRequirementsFile}';" + : "require 'phar://{$alias}/.box/{$checkRequirementsFile}';"; + + $previous = true; + } + + if (null !== $index) { + if ($previous) { + $stub[] = ''; + } + + $stub[] = null === $alias + ? "require 'phar://' . __FILE__ . '/{$index}';" + : "require 'phar://{$alias}/{$index}';"; + } + + if ([] === $stub) { + return "// No PHAR config\n"; + } + + return implode("\n", $stub)."\n"; + } +} diff --git a/fixtures/bench/without-compactors/src/Test/CommandTestCase.php b/fixtures/bench/without-compactors/src/Test/CommandTestCase.php new file mode 100644 index 000000000..33483611e --- /dev/null +++ b/fixtures/bench/without-compactors/src/Test/CommandTestCase.php @@ -0,0 +1,78 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Test; + +use Fidry\Console\Bridge\Command\SymfonyCommand; +use Fidry\Console\Command\Command; +use Fidry\Console\Test\CommandTester; +use Fidry\Console\Test\OutputAssertions; +use Symfony\Component\Console\Application; + +/** + * @private + */ +abstract class CommandTestCase extends FileSystemTestCase +{ + protected CommandTester $commandTester; + protected Command $command; + + protected function setUp(): void + { + parent::setUp(); + + $this->command = $this->getCommand(); + + $command = new SymfonyCommand($this->command); + + $application = new Application(); + $application->add($command); + + $this->commandTester = new CommandTester( + $application->get( + $command->getName(), + ), + ); + } + + protected function tearDown(): void + { + unset($this->command, $this->commandTester); + + parent::tearDown(); + } + + /** + * Returns the command to be tested. + * + * @return Command the command + */ + abstract protected function getCommand(): Command; + + /** + * @param callable(string):string $extraNormalizers + */ + public function assertSameOutput( + string $expectedOutput, + int $expectedStatusCode, + callable ...$extraNormalizers, + ): void { + OutputAssertions::assertSameOutput( + $expectedOutput, + $expectedStatusCode, + $this->commandTester, + ...$extraNormalizers, + ); + } +} diff --git a/fixtures/bench/without-compactors/src/Test/FileSystemTestCase.php b/fixtures/bench/without-compactors/src/Test/FileSystemTestCase.php new file mode 100644 index 000000000..84ea49117 --- /dev/null +++ b/fixtures/bench/without-compactors/src/Test/FileSystemTestCase.php @@ -0,0 +1,26 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Test; + +/** + * @private + */ +abstract class FileSystemTestCase extends \Fidry\FileSystem\Test\FileSystemTestCase +{ + public static function getTmpDirNamespace(): string + { + return 'BoxTest'; + } +} diff --git a/fixtures/bench/without-compactors/src/Test/RequiresPharReadonlyOff.php b/fixtures/bench/without-compactors/src/Test/RequiresPharReadonlyOff.php new file mode 100644 index 000000000..7e78de14c --- /dev/null +++ b/fixtures/bench/without-compactors/src/Test/RequiresPharReadonlyOff.php @@ -0,0 +1,33 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box\Test; + +use KevinGH\Box\Phar\PharPhpSettings; + +/** + * @private + */ +trait RequiresPharReadonlyOff +{ + private function markAsSkippedIfPharReadonlyIsOn(): void + { + if (PharPhpSettings::isReadonly()) { + $this->markTestSkipped( + 'Requires phar.readonly to be set to 0. Either update your php.ini file or run this test with ' + .'php -d phar.readonly=0.', + ); + } + } +} diff --git a/fixtures/bench/without-compactors/src/functions.php b/fixtures/bench/without-compactors/src/functions.php new file mode 100644 index 000000000..1ff33245f --- /dev/null +++ b/fixtures/bench/without-compactors/src/functions.php @@ -0,0 +1,192 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace KevinGH\Box; + +use Composer\InstalledVersions; +use ErrorException; +use Phar; +use Symfony\Component\Console\Helper\Helper; +use Webmozart\Assert\Assert; +use function bin2hex; +use function class_alias; +use function class_exists; +use function floor; +use function is_float; +use function is_int; +use function log; +use function number_format; +use function random_bytes; +use function sprintf; +use function str_replace; + +/** + * @private + */ +function get_box_version(): string +{ + // Load manually the InstalledVersions class. + // Indeed, this class is registered to the autoloader by Composer itself which + // results an incorrect classmap entry in the scoped code. + // This strategy avoids having to exclude completely the file from the scoping. + foreach ([__DIR__.'/../vendor/composer/InstalledVersions.php', __DIR__.'/../../../composer/InstalledVersions.php'] as $file) { + if (file_exists($file)) { + require_once $file; + break; + } + } + + $prettyVersion = InstalledVersions::getPrettyVersion('humbug/box'); + $commitHash = InstalledVersions::getReference('humbug/box'); + + if (null === $commitHash) { + return $prettyVersion; + } + + return $prettyVersion.'@'.mb_substr($commitHash, 0, 7); +} + +/** + * @deprecated since 4.3.0. Use \KevinGH\Box\Phar\CompressionAlgorithm instead. + * @private + * + * @return array + */ +function get_phar_compression_algorithms(): array +{ + static $algorithms = [ + 'GZ' => Phar::GZ, + 'BZ2' => Phar::BZ2, + 'NONE' => Phar::NONE, + ]; + + return $algorithms; +} + +/** + * @deprecated Since 4.5.0. Use \KevinGH\Box\Phar\SigningAlgorithm instead. + * + * @private + * + * @return array + */ +function get_phar_signing_algorithms(): array +{ + static $algorithms = [ + 'MD5' => Phar::MD5, + 'SHA1' => Phar::SHA1, + 'SHA256' => Phar::SHA256, + 'SHA512' => Phar::SHA512, + 'OPENSSL' => Phar::OPENSSL, + ]; + + return $algorithms; +} + +/** + * @private + */ +function format_size(float|int $size, int $decimals = 2): string +{ + Assert::true(is_int($size) || is_float($size)); + + if (-1 === $size) { + return '-1'; + } + + $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + $power = $size > 0 ? (int) floor(log($size, 1024)) : 0; + + return sprintf( + '%s%s', + number_format( + $size / (1024 ** $power), + $decimals, + ), + $units[$power], + ); +} + +/** + * @private + */ +function memory_to_bytes(string $value): float|int +{ + $unit = mb_strtolower($value[mb_strlen($value) - 1]); + + $bytes = (int) $value; + + switch ($unit) { + case 'g': + $bytes *= 1024; + // no break (cumulative multiplier) + case 'm': + $bytes *= 1024; + // no break (cumulative multiplier) + case 'k': + $bytes *= 1024; + } + + return $bytes; +} + +/** + * @private + */ +function format_time(float $secs): string +{ + return str_replace( + ' ', + '', + Helper::formatTime($secs), + ); +} + +/** + * @private + */ +function register_aliases(): void +{ + // Exposes the finder used by PHP-Scoper PHAR to allow its usage in the configuration file. + if (false === class_exists(\Isolated\Symfony\Component\Finder\Finder::class)) { + class_alias(\Symfony\Component\Finder\Finder::class, \Isolated\Symfony\Component\Finder\Finder::class); + } +} + +/** + * @private + * + * @return string Random 12 characters long (plus the prefix) string composed of a-z characters and digits + */ +function unique_id(string $prefix): string +{ + return $prefix.bin2hex(random_bytes(6)); +} + +/** + * Converts errors to exceptions. + * + * @private + */ +function register_error_handler(): void +{ + set_error_handler( + static function (int $code, string $message, string $file = '', int $line = -1): void { + if (error_reporting() & $code) { + throw new ErrorException($message, 0, $code, $file, $line); + } + }, + ); +} diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 000000000..b296c3629 --- /dev/null +++ b/phpbench.json @@ -0,0 +1,4 @@ +{ + "$schema":"vendor-bin/phpbench/vendor/phpbench/phpbench/phpbench.schema.json", + "runner.bootstrap": "vendor/autoload.php" +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index cd0535530..e460404e5 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -21,11 +21,13 @@ tests/AutoReview + tests/Benchmark tests/ tests/AutoReview + tests/Benchmark diff --git a/src/Console/Command/Compile.php b/src/Console/Command/Compile.php index 11c817eb9..4d3fb718d 100644 --- a/src/Console/Command/Compile.php +++ b/src/Console/Command/Compile.php @@ -191,9 +191,9 @@ public function execute(IO $io): int PhpSettingsChecker::check($io); - $enableParallelization = $io->getTypedOption(self::NO_PARALLEL_PROCESSING_OPTION)->asBoolean(); + $enableParallelization = !$io->getTypedOption(self::NO_PARALLEL_PROCESSING_OPTION)->asBoolean(); - if ($enableParallelization) { + if (!$enableParallelization) { $io->writeln( '[debug] Disabled parallel processing', OutputInterface::VERBOSITY_DEBUG, diff --git a/tests/Benchmark/CompileBench.php b/tests/Benchmark/CompileBench.php new file mode 100644 index 000000000..4dceedd34 --- /dev/null +++ b/tests/Benchmark/CompileBench.php @@ -0,0 +1,116 @@ +runner = new ApplicationRunner( + new Application( + autoExit: false, + catchExceptions: false, + ), + ); + } + + #[Warmup(1)] + public function warmUp(): void + { + FS::remove( + __DIR__.'/../../dist/bench/box.phar', + ); + + self::removeOutputArtifact(); + self::assertVendorsAreInstalled(); + } + + private static function removeOutputArtifact(): void + { + FS::remove(__DIR__.'/../../dist/bench/box.phar'); + } + + private static function assertVendorsAreInstalled(): void + { + $vendorDirs = [ + self::WITH_COMPACTORS_DIR.'/vendor', + self::WITHOUT_COMPACTORS_DIR.'/vendor', + ]; + + foreach ($vendorDirs as $vendorDir) { + Assert::assertDirectoryExists($vendorDir); + } + } + + #[ParamProviders('parameterProvider')] + #[Iterations(10)] + public function bench(array $params): void + { + [$workingDirectory, $enableParallelization] = $params; + + chdir($workingDirectory); + + $exitCode = $this->runner->run( + self::createIO($enableParallelization), + ); + + Assert::assertSame(ExitCode::SUCCESS, $exitCode); + } + + public static function parameterProvider(): iterable + { + yield 'no compactors' => [ + self::WITHOUT_COMPACTORS_DIR, + false, + ]; + + yield 'with compactors; no parallel processing' => [ + self::WITH_COMPACTORS_DIR, + false, + ]; + + yield 'with compactors; parallel processing' => [ + self::WITH_COMPACTORS_DIR, + true, + ]; + } + + private static function createIO(bool $enableParallelization): IO + { + $input = [ + 'compile', + '--no-restart' => null, + ]; + + if (!$enableParallelization) { + $input['--no-parallel'] = null; + } + + return new IO( + new ArrayInput($input), + new NullOutput(), + ); + } +} \ No newline at end of file diff --git a/vendor-bin/phpbench/composer.json b/vendor-bin/phpbench/composer.json new file mode 100644 index 000000000..2da4cc9c5 --- /dev/null +++ b/vendor-bin/phpbench/composer.json @@ -0,0 +1,5 @@ +{ + "require-dev": { + "phpbench/phpbench": "^1.2" + } +}