From b7e3fb971c910dc8e05475f641a8df4b66751c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Fri, 15 Dec 2023 09:35:55 +0100 Subject: [PATCH 01/14] feat: Add a info:requirements command Related to #1237. --- .../complete/composer.json | 25 + .../complete/composer.lock | 711 ++++++++++++++++++ src/Console/Application.php | 5 + src/Console/Command/Info/Requirements.php | 192 +++++ .../AppRequirementsFactory.php | 5 +- .../Console/Command/Info/RequirementsTest.php | 125 +++ 6 files changed, 1061 insertions(+), 2 deletions(-) create mode 100644 fixtures/requirement-checker/complete/composer.json create mode 100644 fixtures/requirement-checker/complete/composer.lock create mode 100644 src/Console/Command/Info/Requirements.php create mode 100644 tests/Console/Command/Info/RequirementsTest.php diff --git a/fixtures/requirement-checker/complete/composer.json b/fixtures/requirement-checker/complete/composer.json new file mode 100644 index 000000000..bb6e4677b --- /dev/null +++ b/fixtures/requirement-checker/complete/composer.json @@ -0,0 +1,25 @@ +{ + "require": { + "php": "*", + "ext-aerospike": "*", + "ext-http": "*", + "ext-zend-opcache": "*", + "paragonie/sodium_compat": "^1.20", + "phpseclib/mcrypt_compat": "^2.0", + "laminas/laminas-code": "^4.13", + "symfony/polyfill-mbstring": "^1.28", + "symfony/polyfill-php72": "^1.28" + }, + "conflict": { + "ext-apache": "*", + "ext-bcmath": "*", + "symfony/polyfill-php83": "*" + }, + "provide": { + "ext-crypto": "*", + "ext-calendar": "*" + }, + "require-dev": { + "symfony/polyfill-iconv": "^1.28" + } +} diff --git a/fixtures/requirement-checker/complete/composer.lock b/fixtures/requirement-checker/complete/composer.lock new file mode 100644 index 000000000..98052fe7c --- /dev/null +++ b/fixtures/requirement-checker/complete/composer.lock @@ -0,0 +1,711 @@ +{ + "_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": "84607e1c7ffedc4f1c8d3d1b6d91b7f9", + "packages": [ + { + "name": "laminas/laminas-code", + "version": "4.13.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-code.git", + "reference": "7353d4099ad5388e84737dd16994316a04f48dbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/7353d4099ad5388e84737dd16994316a04f48dbf", + "reference": "7353d4099ad5388e84737dd16994316a04f48dbf", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0.1", + "ext-phar": "*", + "laminas/laminas-coding-standard": "^2.5.0", + "laminas/laminas-stdlib": "^3.17.0", + "phpunit/phpunit": "^10.3.3", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.15.0" + }, + "suggest": { + "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", + "laminas/laminas-stdlib": "Laminas\\Stdlib component" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Code\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", + "homepage": "https://laminas.dev", + "keywords": [ + "code", + "laminas", + "laminasframework" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-code/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-code/issues", + "rss": "https://github.com/laminas/laminas-code/releases.atom", + "source": "https://github.com/laminas/laminas-code" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2023-10-18T10:00:55+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.6.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "58c3f47f650c94ec05a151692652a868995d2938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", + "reference": "58c3f47f650c94ec05a151692652a868995d2938", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2022-06-14T06:56:20+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "paragonie/sodium_compat", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "e592a3e06d1fa0d43988c7c7d9948ca836f644b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/e592a3e06d1fa0d43988c7c7d9948ca836f644b6", + "reference": "e592a3e06d1fa0d43988c7c7d9948ca836f644b6", + "shasum": "" + }, + "require": { + "paragonie/random_compat": ">=1", + "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9" + }, + "suggest": { + "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", + "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "autoload": { + "files": [ + "autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "support": { + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v1.20.0" + }, + "time": "2023-04-30T00:54:53+00:00" + }, + { + "name": "phpseclib/mcrypt_compat", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/mcrypt_compat.git", + "reference": "6505669343743daf290b7d7b6b7105f85fd9988f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/mcrypt_compat/zipball/6505669343743daf290b7d7b6b7105f85fd9988f", + "reference": "6505669343743daf290b7d7b6b7105f85fd9988f", + "shasum": "" + }, + "require": { + "php": ">=5.6.1", + "phpseclib/phpseclib": ">=3.0.13 <4.0.0" + }, + "provide": { + "ext-mcrypt": "5.6.40" + }, + "require-dev": { + "phpunit/phpunit": "^5.7|^6.0|^9.4" + }, + "suggest": { + "ext-openssl": "Will enable faster cryptographic operations" + }, + "type": "library", + "autoload": { + "files": [ + "lib/mcrypt.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "homepage": "http://phpseclib.sourceforge.net" + } + ], + "description": "PHP 5.x-8.x polyfill for mcrypt extension", + "keywords": [ + "cryptograpy", + "encryption", + "mcrypt", + "polyfill" + ], + "support": { + "email": "terrafrost@php.net", + "issues": "https://github.com/phpseclib/mcrypt_compat/issues", + "source": "https://github.com/phpseclib/mcrypt_compat" + }, + "funding": [ + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/mcrypt_compat", + "type": "tidelift" + } + ], + "time": "2022-12-19T00:32:45+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.34", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "56c79f16a6ae17e42089c06a2144467acc35348a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56c79f16a6ae17e42089c06a2144467acc35348a", + "reference": "56c79f16a6ae17e42089c06a2144467acc35348a", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.34" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2023-11-27T11:13:31+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/polyfill-php72", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179", + "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "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\\Php72\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/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" + } + ], + "packages-dev": [ + { + "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" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "*", + "ext-aerospike": "*", + "ext-http": "*", + "ext-zend-opcache": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/src/Console/Application.php b/src/Console/Application.php index 92043fe81..d09d6aabe 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -23,11 +23,13 @@ use KevinGH\Box\Console\Command\Extract; use KevinGH\Box\Console\Command\GenerateDockerFile; use KevinGH\Box\Console\Command\Info; +use KevinGH\Box\Console\Command\Info\Requirements as InfoRequirements; use KevinGH\Box\Console\Command\Info\Signature as InfoSignature; use KevinGH\Box\Console\Command\Namespace_; use KevinGH\Box\Console\Command\Process; use KevinGH\Box\Console\Command\Validate; use KevinGH\Box\Console\Command\Verify; +use KevinGH\Box\RequirementChecker\AppRequirementsFactory; use function KevinGH\Box\get_box_version; use function sprintf; use function trim; @@ -98,6 +100,9 @@ public function getCommands(): array new Info(), new Info('info:general'), new InfoSignature(), + new InfoRequirements( + new AppRequirementsFactory(), + ), new CheckSignature(), new Process(), new Extract(), diff --git a/src/Console/Command/Info/Requirements.php b/src/Console/Command/Info/Requirements.php new file mode 100644 index 000000000..9dcca2679 --- /dev/null +++ b/src/Console/Command/Info/Requirements.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\Console\Command\Info; + +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\Composer\Artifact\DecodedComposerJson; +use KevinGH\Box\Composer\Artifact\DecodedComposerLock; +use KevinGH\Box\Configuration\Configuration; +use KevinGH\Box\Console\Command\ChangeWorkingDirOption; +use KevinGH\Box\Console\Command\ConfigOption; +use KevinGH\Box\Console\Php\PhpSettingsChecker; +use KevinGH\Box\Constants; +use KevinGH\Box\RequirementChecker\AppRequirementsFactory; +use KevinGH\Box\RequirementChecker\Requirement; +use KevinGH\Box\RequirementChecker\RequirementType; +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 iter\toArray; +use function putenv; +use function sprintf; + +final class Requirements implements Command +{ + private const NO_CONFIG_OPTION = 'no-config'; + + public function __construct( + private AppRequirementsFactory $factory, + ) { + } + + public function getConfiguration(): ConsoleConfiguration + { + return new ConsoleConfiguration( + 'info:requirements', + 'Lists the application requirements found;', + 'The %command.name% command will list the requirements required to run the built PHAR.', + options: [ + 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 + { + ChangeWorkingDirOption::changeWorkingDirectory($io); + + $config = $io->getTypedOption(self::NO_CONFIG_OPTION)->asBoolean() + ? Configuration::create(null, new stdClass()) + : ConfigOption::getConfig($io, true); + + $requirements = $this->factory->create( + new DecodedComposerJson($config->getDecodedComposerJsonContents() ?? []), + new DecodedComposerLock($config->getDecodedComposerLockContents() ?? []), + $config->getCompressionAlgorithm(), + ); + + [$required, $conflicting] = self::retrieveRequirements($requirements); + + self::renderRequiredSection($required, $io); + self::renderConflictingSection($conflicting, $io); + + return ExitCode::SUCCESS; + } + + /** + * @return array{Requirement[], Requirement[]} + */ + private static function retrieveRequirements(\KevinGH\Box\RequirementChecker\Requirements $requirements): array + { + [$required, $conflicting] = array_reduce( + toArray($requirements), + 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, + ), + ); + } +} diff --git a/src/RequirementChecker/AppRequirementsFactory.php b/src/RequirementChecker/AppRequirementsFactory.php index 386746e26..13c6692da 100644 --- a/src/RequirementChecker/AppRequirementsFactory.php +++ b/src/RequirementChecker/AppRequirementsFactory.php @@ -23,12 +23,13 @@ * Collect the list of requirements for running the application. * * @private + * @final */ -final class AppRequirementsFactory +class AppRequirementsFactory { private const SELF_PACKAGE = null; - public static function create( + public function create( DecodedComposerJson $composerJson, DecodedComposerLock $composerLock, CompressionAlgorithm $compressionAlgorithm, diff --git a/tests/Console/Command/Info/RequirementsTest.php b/tests/Console/Command/Info/RequirementsTest.php new file mode 100644 index 000000000..77e9a33f2 --- /dev/null +++ b/tests/Console/Command/Info/RequirementsTest.php @@ -0,0 +1,125 @@ + + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Console\Command\Info; + +use Fidry\Console\Command\Command; +use Fidry\Console\ExitCode; +use Fidry\Console\Test\CommandTester; +use InvalidArgumentException; +use KevinGH\Box\Console\Command\Info; +use KevinGH\Box\Console\Command\Info\Requirements as RequirementsCommand; +use KevinGH\Box\Console\PharInfoRenderer; +use KevinGH\Box\Phar\Throwable\InvalidPhar; +use KevinGH\Box\Platform; +use KevinGH\Box\RequirementChecker\AppRequirementsFactory; +use KevinGH\Box\RequirementChecker\Requirements; +use KevinGH\Box\Test\CommandTestCase; +use Phar; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; +use Symfony\Component\Console\Output\OutputInterface; +use function extension_loaded; +use function implode; + +/** + * @internal + */ +#[CoversClass(RequirementsCommand::class)] +class RequirementsTest extends TestCase +{ + use ProphecyTrait; + + private const FIXTURES = __DIR__.'/../../../../fixtures/requirement-checker'; + + private ObjectProphecy|AppRequirementsFactory $factoryProphecy; + private CommandTester $commandTester; + + protected function setUp(): void + { + $this->factoryProphecy = $this->prophesize(AppRequirementsFactory::class); + + $this->commandTester = CommandTester::fromConsoleCommand( + new RequirementsCommand($this->factoryProphecy->reveal()), + ); + } + + #[DataProvider('requirementsProvider')] + public function test_it_provides_info_about_the_app_requirements( + Requirements $requirements, + string $expected, + ): void { + $this->factoryProphecy + ->create(Argument::cetera()) + ->willReturn($requirements); + + $this->commandTester->execute(['--no-config' => null]); + + $this->commandTester->assertCommandIsSuccessful(); + $display = $this->commandTester->getNormalizedDisplay(); + + self::assertSame($expected, $display); + } + + public static function requirementsProvider(): iterable + { + yield 'empty' => [ + new Requirements([]), + '', + ]; + + yield 'a real case' => [ + , + '', + ]; + + return; + yield 'PHAR with requirement checker; one PHP and extension and conflict requirement' => [ + ['phar' => self::FIXTURES.'/req-checker-ext-and-php-and-conflict.phar'], + <<<'OUTPUT' + + API Version: 1.1.0 + + Built with Box: dev-main@b2c33cd + + Archive Compression: None + Files Compression: None + + Signature: SHA-1 + Signature Hash: 2882E27FCEE2268DB6E18A7BBB8B92906F286458 + + Metadata: None + + Timestamp: 1697989559 (2023-10-22T15:45:59+00:00) + + RequirementChecker: + Required: + - PHP ^7.2 (root) + - ext-json (root) + Conflict: + - ext-aerospike (root) + + Contents: 45 files (148.23KB) + + // Use the --list|-l option to list the content of the PHAR. + + + OUTPUT, + ]; + } +} From df88dba2df0ca59164833c42ad1d3ff977ae0e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sun, 10 Mar 2024 13:55:47 +0100 Subject: [PATCH 02/14] fix cs --- src/Console/Command/Info/Requirements.php | 20 +------------- src/RequirementChecker/RequirementsDumper.php | 6 +---- .../Benchmark/AppRequirementFactoryBench.php | 26 ++++++++----------- .../Console/Command/Info/RequirementsTest.php | 19 +++----------- .../AppRequirementsFactoryTest.php | 6 +---- 5 files changed, 17 insertions(+), 60 deletions(-) diff --git a/src/Console/Command/Info/Requirements.php b/src/Console/Command/Info/Requirements.php index 9dcca2679..2728b9f76 100644 --- a/src/Console/Command/Info/Requirements.php +++ b/src/Console/Command/Info/Requirements.php @@ -18,40 +18,22 @@ 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\Composer\Artifact\DecodedComposerJson; use KevinGH\Box\Composer\Artifact\DecodedComposerLock; use KevinGH\Box\Configuration\Configuration; use KevinGH\Box\Console\Command\ChangeWorkingDirOption; use KevinGH\Box\Console\Command\ConfigOption; -use KevinGH\Box\Console\Php\PhpSettingsChecker; -use KevinGH\Box\Constants; use KevinGH\Box\RequirementChecker\AppRequirementsFactory; use KevinGH\Box\RequirementChecker\Requirement; use KevinGH\Box\RequirementChecker\RequirementType; 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 iter\toArray; -use function putenv; use function sprintf; -final class Requirements implements Command +final readonly class Requirements implements Command { private const NO_CONFIG_OPTION = 'no-config'; diff --git a/src/RequirementChecker/RequirementsDumper.php b/src/RequirementChecker/RequirementsDumper.php index 5adf23ee1..763eaec78 100644 --- a/src/RequirementChecker/RequirementsDumper.php +++ b/src/RequirementChecker/RequirementsDumper.php @@ -75,11 +75,7 @@ private static function dumpRequirementsConfig( ComposerLock $composerLock, CompressionAlgorithm $compressionAlgorithm, ): array { - $requirements = AppRequirementsFactory::create( - $composerJson, - $composerLock, - $compressionAlgorithm, - ); + $requirements = (new AppRequirementsFactory())->create($composerJson, $composerLock, $compressionAlgorithm); return [ '.requirements.php', diff --git a/tests/Benchmark/AppRequirementFactoryBench.php b/tests/Benchmark/AppRequirementFactoryBench.php index 1d070f2b8..ecdc3b454 100644 --- a/tests/Benchmark/AppRequirementFactoryBench.php +++ b/tests/Benchmark/AppRequirementFactoryBench.php @@ -37,23 +37,19 @@ public function setUp(): void #[BeforeMethods('setUp')] public function bench(): void { - AppRequirementsFactory::create( - new ComposerJson( - '', - json_decode( - file_get_contents(self::FIXTURES.'/composer.json'), - true, - ), + (new AppRequirementsFactory())->create(new ComposerJson( + '', + json_decode( + file_get_contents(self::FIXTURES.'/composer.json'), + true, ), - new ComposerLock( - '', - json_decode( - file_get_contents(self::FIXTURES.'/composer.lock'), - true, - ), + ), new ComposerLock( + '', + json_decode( + file_get_contents(self::FIXTURES.'/composer.lock'), + true, ), - CompressionAlgorithm::BZ2, - ); + ), CompressionAlgorithm::BZ2); } private static function assertVendorsAreInstalled(): void diff --git a/tests/Console/Command/Info/RequirementsTest.php b/tests/Console/Command/Info/RequirementsTest.php index 77e9a33f2..da3bb603d 100644 --- a/tests/Console/Command/Info/RequirementsTest.php +++ b/tests/Console/Command/Info/RequirementsTest.php @@ -14,28 +14,16 @@ namespace Console\Command\Info; -use Fidry\Console\Command\Command; -use Fidry\Console\ExitCode; use Fidry\Console\Test\CommandTester; -use InvalidArgumentException; -use KevinGH\Box\Console\Command\Info; use KevinGH\Box\Console\Command\Info\Requirements as RequirementsCommand; -use KevinGH\Box\Console\PharInfoRenderer; -use KevinGH\Box\Phar\Throwable\InvalidPhar; -use KevinGH\Box\Platform; use KevinGH\Box\RequirementChecker\AppRequirementsFactory; use KevinGH\Box\RequirementChecker\Requirements; -use KevinGH\Box\Test\CommandTestCase; -use Phar; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\Console\Output\OutputInterface; -use function extension_loaded; -use function implode; /** * @internal @@ -47,7 +35,7 @@ class RequirementsTest extends TestCase private const FIXTURES = __DIR__.'/../../../../fixtures/requirement-checker'; - private ObjectProphecy|AppRequirementsFactory $factoryProphecy; + private AppRequirementsFactory|ObjectProphecy $factoryProphecy; private CommandTester $commandTester; protected function setUp(): void @@ -62,7 +50,7 @@ protected function setUp(): void #[DataProvider('requirementsProvider')] public function test_it_provides_info_about_the_app_requirements( Requirements $requirements, - string $expected, + string $expected, ): void { $this->factoryProphecy ->create(Argument::cetera()) @@ -83,8 +71,7 @@ public static function requirementsProvider(): iterable '', ]; - yield 'a real case' => [ - , + yield 'a real case' => [, '', ]; diff --git a/tests/RequirementChecker/AppRequirementsFactoryTest.php b/tests/RequirementChecker/AppRequirementsFactoryTest.php index a50b0c865..5dcdbebac 100644 --- a/tests/RequirementChecker/AppRequirementsFactoryTest.php +++ b/tests/RequirementChecker/AppRequirementsFactoryTest.php @@ -49,11 +49,7 @@ public function test_it_can_generate_and_serialized_requirements_from_a_composer : json_decode($composerLockContents, true, flags: JSON_THROW_ON_ERROR), ); - $actual = AppRequirementsFactory::create( - $composerJson, - $composerLock, - $compressionAlgorithm, - ); + $actual = (new AppRequirementsFactory())->create($composerJson, $composerLock, $compressionAlgorithm); self::assertEquals( (new Requirements($expected))->toArray(), From 7658d01d8255013c090dc79e32dd3449c580e2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sun, 10 Mar 2024 14:10:03 +0100 Subject: [PATCH 03/14] add missing file --- .../requirement-checker/complete/index.php | 1 + src/Console/Command/Info/Requirements.php | 25 +++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 fixtures/requirement-checker/complete/index.php diff --git a/fixtures/requirement-checker/complete/index.php b/fixtures/requirement-checker/complete/index.php new file mode 100644 index 000000000..b3d9bbc7f --- /dev/null +++ b/fixtures/requirement-checker/complete/index.php @@ -0,0 +1 @@ +%command.name% command will list the requirements required to run the built PHAR.', + 'Lists the application requirements found.', + 'The %command.name% command will list the PHP versions and extensions required to run the built PHAR.', options: [ new InputOption( self::NO_CONFIG_OPTION, null, InputOption::VALUE_NONE, - 'Ignore the config file even when one is specified with the --config option', + 'Ignore the config file even when one is specified with the `--config` option.', ), ConfigOption::getOptionInput(), ChangeWorkingDirOption::getOptionInput(), @@ -69,9 +69,24 @@ public function execute(IO $io): int ? Configuration::create(null, new stdClass()) : ConfigOption::getConfig($io, true); + $composerJson = $config->getComposerJson(); + $composerLock = $config->getComposerLock(); + + if (null === $composerJson) { + $io->error('Could not find a composer.json file.'); + + return ExitCode::FAILURE; + } + + if (null === $composerLock) { + $io->error('Could not find a composer.lock file.'); + + return ExitCode::FAILURE; + } + $requirements = $this->factory->create( - new DecodedComposerJson($config->getDecodedComposerJsonContents() ?? []), - new DecodedComposerLock($config->getDecodedComposerLockContents() ?? []), + $composerJson, + $composerLock, $config->getCompressionAlgorithm(), ); From 0de4334fb841798b3d106c0351ece035f3c600c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Sun, 10 Mar 2024 18:49:30 +0100 Subject: [PATCH 04/14] WIp --- src/Console/Command/Info/Requirements.php | 198 +++++++++++++----- .../AppRequirementsFactory.php | 30 ++- src/RequirementChecker/Requirement.php | 20 ++ src/RequirementChecker/RequirementType.php | 1 + .../RequirementsBuilder.php | 97 +++++++-- 5 files changed, 281 insertions(+), 65 deletions(-) diff --git a/src/Console/Command/Info/Requirements.php b/src/Console/Command/Info/Requirements.php index e74215ee0..6a4621a97 100644 --- a/src/Console/Command/Info/Requirements.php +++ b/src/Console/Command/Info/Requirements.php @@ -18,18 +18,19 @@ use Fidry\Console\Command\Configuration as ConsoleConfiguration; use Fidry\Console\ExitCode; use Fidry\Console\IO; -use KevinGH\Box\Composer\Artifact\DecodedComposerJson; -use KevinGH\Box\Composer\Artifact\DecodedComposerLock; use KevinGH\Box\Configuration\Configuration; use KevinGH\Box\Console\Command\ChangeWorkingDirOption; use KevinGH\Box\Console\Command\ConfigOption; use KevinGH\Box\RequirementChecker\AppRequirementsFactory; use KevinGH\Box\RequirementChecker\Requirement; +use KevinGH\Box\RequirementChecker\Requirements as RequirementsCollection; use KevinGH\Box\RequirementChecker\RequirementType; use stdClass; use Symfony\Component\Console\Input\InputOption; +use function array_filter; use function array_map; use function implode; +use function iter\filter; use function iter\toArray; use function sprintf; @@ -84,80 +85,175 @@ public function execute(IO $io): int return ExitCode::FAILURE; } - $requirements = $this->factory->create( + $requirements = $this->factory->createUnfiltered( $composerJson, $composerLock, $config->getCompressionAlgorithm(), ); - [$required, $conflicting] = self::retrieveRequirements($requirements); + [ + $phpRequirements, + $requiredExtensions, + $providedExtensions, + $conflictingExtensions, + ] = self::filterRequirements($requirements); - self::renderRequiredSection($required, $io); - self::renderConflictingSection($conflicting, $io); + $optimizedRequiredRequirements = toArray( + filter( + static fn (Requirement $requirement) => $requirement->type === RequirementType::EXTENSION, + $this->factory + ->create( + $composerJson, + $composerLock, + $config->getCompressionAlgorithm(), + ), + ), + ); + + self::renderRequiredPHPVersionsSection($phpRequirements, $io); + self::renderRequiredExtensionsSection($requiredExtensions, $io); + self::renderProvidedExtensionsSection($providedExtensions, $io); + self::renderOptimizedRequiredExtensionsSection($optimizedRequiredRequirements, $io); + self::renderConflictingExtensionsSection($conflictingExtensions, $io); return ExitCode::SUCCESS; } - /** - * @return array{Requirement[], Requirement[]} - */ - private static function retrieveRequirements(\KevinGH\Box\RequirementChecker\Requirements $requirements): array + private static function filterRequirements(RequirementsCollection $requirements): array { - [$required, $conflicting] = array_reduce( - toArray($requirements), - 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; - }, - [[], []], - ); + $phpRequirements = []; + $requiredExtensions = []; + $providedExtensions = []; + $conflictingExtensions = []; + + foreach ($requirements as $requirement) { + /** @var Requirement $requirement */ + switch ($requirement->type) { + case RequirementType::PHP: + $phpRequirements[] = $requirement; + break; + + case RequirementType::EXTENSION: + $requiredExtensions[] = $requirement; + break; + + case RequirementType::PROVIDED_EXTENSION: + $providedExtensions[] = $requirement; + break; + + case RequirementType::EXTENSION_CONFLICT: + $conflictingExtensions[] = $requirement; + break; + } + } return [ - array_values($required), - array_values($conflicting), + $phpRequirements, + $requiredExtensions, + $providedExtensions, + $conflictingExtensions, ]; } /** * @param Requirement[] $required */ - private static function renderRequiredSection( + private static function renderRequiredPHPVersionsSection( array $required, IO $io, ): void { if (0 === count($required)) { + $io->writeln('No PHP requirement found.'); + return; } - $io->writeln(' Required:'); + $io->writeln('Required PHP versions:'); $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', - ), - }, + static fn (Requirement $requirement) => sprintf( + ' - %s (%s)', + $requirement->condition, + $requirement->source ?? 'root', + ), + $required, + ), + ); + } + + /** + * @param Requirement[] $required + */ + private static function renderRequiredExtensionsSection( + array $required, + IO $io, + ): void { + if (0 === count($required)) { + $io->writeln('No required extension found.'); + + return; + } + + $io->writeln('Required extensions:'); + $io->writeln( + array_map( + static fn (Requirement $requirement) => sprintf( + ' - ext-%s (%s)', + $requirement->condition, + $requirement->source ?? 'root', + ), + $required, + ), + ); + } + + /** + * @param Requirement[] $provided + */ + private static function renderProvidedExtensionsSection( + array $provided, + IO $io, + ): void { + if (0 === count($provided)) { + $io->writeln('No provided extension found.'); + + return; + } + + $io->writeln('Provided extensions:'); + $io->writeln( + array_map( + static fn (Requirement $requirement) => sprintf( + ' - ext-%s (%s)', + $requirement->condition, + $requirement->source ?? 'root', + ), + $provided, + ), + ); + } + + /** + * @param Requirement[] $required + */ + private static function renderOptimizedRequiredExtensionsSection( + array $required, + IO $io, + ): void { + if (0 === count($required)) { + $io->writeln('No required extension found.'); + + return; + } + + $io->writeln('Final required extensions: (accounts for the provided extensions)'); + $io->writeln( + array_map( + static fn (Requirement $requirement) => sprintf( + ' - ext-%s (%s)', + $requirement->condition, + $requirement->source ?? 'root', + ), $required, ), ); @@ -166,15 +262,17 @@ private static function renderRequiredSection( /** * @param Requirement[] $conflicting */ - private static function renderConflictingSection( + private static function renderConflictingExtensionsSection( array $conflicting, IO $io, ): void { if (0 === count($conflicting)) { + $io->writeln('No conflicting package found.'); + return; } - $io->writeln(' Conflict:'); + $io->writeln('Conflicting extensions:'); $io->writeln( array_map( static fn (Requirement $requirement) => sprintf( diff --git a/src/RequirementChecker/AppRequirementsFactory.php b/src/RequirementChecker/AppRequirementsFactory.php index 01849f645..5e503a656 100644 --- a/src/RequirementChecker/AppRequirementsFactory.php +++ b/src/RequirementChecker/AppRequirementsFactory.php @@ -29,11 +29,39 @@ class AppRequirementsFactory { private const SELF_PACKAGE = null; + public function createUnfiltered( + ComposerJson $composerJson, + ComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): Requirements { + return $this + ->createBuilder( + $composerJson, + $composerLock, + $compressionAlgorithm, + ) + ->getAll(); + } + public function create( ComposerJson $composerJson, ComposerLock $composerLock, CompressionAlgorithm $compressionAlgorithm, ): Requirements { + return $this + ->createBuilder( + $composerJson, + $composerLock, + $compressionAlgorithm, + ) + ->build(); + } + + private function createBuilder( + ComposerJson $composerJson, + ComposerLock $composerLock, + CompressionAlgorithm $compressionAlgorithm, + ): RequirementsBuilder { $requirementsBuilder = new RequirementsBuilder(); self::retrievePhpVersionRequirements($requirementsBuilder, $composerJson, $composerLock); @@ -41,7 +69,7 @@ public function create( self::collectComposerLockExtensionRequirements($composerLock, $requirementsBuilder); self::collectComposerJsonExtensionRequirements($composerJson, $requirementsBuilder); - return $requirementsBuilder->build(); + return $requirementsBuilder; } private static function retrievePhpVersionRequirements( diff --git a/src/RequirementChecker/Requirement.php b/src/RequirementChecker/Requirement.php index fe1d8e3ef..6c198c4bb 100644 --- a/src/RequirementChecker/Requirement.php +++ b/src/RequirementChecker/Requirement.php @@ -86,6 +86,26 @@ public static function forRequiredExtension(string $extension, ?string $packageN ); } + public static function forProvidedExtension(string $extension, ?string $packageName): self + { + return new self( + RequirementType::PROVIDED_EXTENSION, + $extension, + $packageName, + null === $packageName + ? sprintf( + 'This application provides the extension "%s".', + $extension, + ) + : sprintf( + 'The package "%s" provides the extension "%s".', + $packageName, + $extension, + ), + '', + ); + } + public static function forConflictingExtension(string $extension, ?string $packageName): self { return new self( diff --git a/src/RequirementChecker/RequirementType.php b/src/RequirementChecker/RequirementType.php index e5edb1882..89d5e642a 100644 --- a/src/RequirementChecker/RequirementType.php +++ b/src/RequirementChecker/RequirementType.php @@ -18,5 +18,6 @@ enum RequirementType: string { case PHP = 'php'; case EXTENSION = 'extension'; + case PROVIDED_EXTENSION = 'provided-extension'; case EXTENSION_CONFLICT = 'extension-conflict'; } diff --git a/src/RequirementChecker/RequirementsBuilder.php b/src/RequirementChecker/RequirementsBuilder.php index 6c517b2e0..dab74fcf9 100644 --- a/src/RequirementChecker/RequirementsBuilder.php +++ b/src/RequirementChecker/RequirementsBuilder.php @@ -16,6 +16,7 @@ use KevinGH\Box\Composer\Package\Extension; use function array_diff_key; +use function array_map; use function array_unique; use function natsort; use function strnatcmp; @@ -48,14 +49,46 @@ public function addConflictingExtension(Extension $extension, ?string $source): $this->conflictingExtensions[$extension->name][] = $source; } + public function getAll(): Requirements + { + $requirements = $this->predefinedRequirements; + + foreach ($this->getUnfilteredSortedRequiredExtensions() as $extensionName => $sources) { + foreach ($sources as $source) { + $requirements[] = Requirement::forRequiredExtension( + $extensionName, + $source, + ); + } + } + + foreach ($this->getSortedProvidedExtensions() as $extensionName => $sources) { + foreach ($sources as $source) { + $requirements[] = Requirement::forProvidedExtension( + $extensionName, + $source, + ); + } + } + + foreach ($this->getSortedConflictedExtensions() as $extensionName => $sources) { + foreach ($sources as $source) { + $requirements[] = Requirement::forConflictingExtension( + $extensionName, + $source, + ); + } + } + + return new Requirements($requirements); + } + public function build(): Requirements { $requirements = $this->predefinedRequirements; foreach ($this->getSortedRequiredExtensions() as $extensionName => $sources) { - $sortedDistinctSources = self::createSortedDistinctList($sources); - - foreach ($sortedDistinctSources as $source) { + foreach ($sources as $source) { $requirements[] = Requirement::forRequiredExtension( $extensionName, $source, @@ -64,9 +97,7 @@ public function build(): Requirements } foreach ($this->getSortedConflictedExtensions() as $extensionName => $sources) { - $sortedDistinctSources = self::createSortedDistinctList($sources); - - foreach ($sortedDistinctSources as $source) { + foreach ($sources as $source) { $requirements[] = Requirement::forConflictingExtension( $extensionName, $source, @@ -80,16 +111,42 @@ public function build(): Requirements /** * @return array> */ - private function getSortedRequiredExtensions(): array + private function getUnfilteredSortedRequiredExtensions(): array { - $extensions = array_diff_key( - $this->requiredExtensions, - $this->providedExtensions, + return array_map( + self::createSortedDistinctList(...), + self::sortByExtensionName( + $this->requiredExtensions, + ), ); + } + /** + * @return array> + */ + private function getSortedProvidedExtensions(): array + { + return array_map( + self::createSortedDistinctList(...), + self::sortByExtensionName( + $this->providedExtensions, + ), + ); + } - uksort($extensions, strnatcmp(...)); - - return $extensions; + /** + * @return array> + */ + private function getSortedRequiredExtensions(): array + { + return array_map( + self::createSortedDistinctList(...), + self::sortByExtensionName( + array_diff_key( + $this->requiredExtensions, + $this->providedExtensions, + ), + ), + ); } /** @@ -97,8 +154,20 @@ private function getSortedRequiredExtensions(): array */ private function getSortedConflictedExtensions(): array { - $extensions = $this->conflictingExtensions; + return array_map( + self::createSortedDistinctList(...), + self::sortByExtensionName($this->conflictingExtensions), + ); + } + /** + * @template T + * + * @param array $extensions + * @return array + */ + private static function sortByExtensionName(array $extensions): array + { uksort($extensions, strnatcmp(...)); return $extensions; From 06fdc8c50669a2c0661a6e5e1060e01d26ca9557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Mon, 11 Mar 2024 22:38:59 +0100 Subject: [PATCH 05/14] fix cs --- fixtures/requirement-checker/complete/index.php | 12 +++++++++++- src/Console/Command/Info/Requirements.php | 6 ++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/fixtures/requirement-checker/complete/index.php b/fixtures/requirement-checker/complete/index.php index b3d9bbc7f..80c8b2af6 100644 --- a/fixtures/requirement-checker/complete/index.php +++ b/fixtures/requirement-checker/complete/index.php @@ -1 +1,11 @@ - + * Théo Fidry + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ diff --git a/src/Console/Command/Info/Requirements.php b/src/Console/Command/Info/Requirements.php index 6a4621a97..cd2acd7d0 100644 --- a/src/Console/Command/Info/Requirements.php +++ b/src/Console/Command/Info/Requirements.php @@ -27,9 +27,7 @@ use KevinGH\Box\RequirementChecker\RequirementType; use stdClass; use Symfony\Component\Console\Input\InputOption; -use function array_filter; use function array_map; -use function implode; use function iter\filter; use function iter\toArray; use function sprintf; @@ -100,7 +98,7 @@ public function execute(IO $io): int $optimizedRequiredRequirements = toArray( filter( - static fn (Requirement $requirement) => $requirement->type === RequirementType::EXTENSION, + static fn (Requirement $requirement) => RequirementType::EXTENSION === $requirement->type, $this->factory ->create( $composerJson, @@ -212,7 +210,7 @@ private static function renderRequiredExtensionsSection( */ private static function renderProvidedExtensionsSection( array $provided, - IO $io, + IO $io, ): void { if (0 === count($provided)) { $io->writeln('No provided extension found.'); From ccd6dc2ca6acfd2a435b2d10f1054fcbb03ee614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Mon, 11 Mar 2024 23:34:56 +0100 Subject: [PATCH 06/14] WIp --- .../complete/composer.json | 1 + .../complete/composer.lock | 72 +++--- src/Console/Application.php | 2 +- ...quirements.php => RequirementsCommand.php} | 223 +++++++++++------- .../RequirementsBuilder.php | 85 ++++--- .../Console/Command/Info/RequirementsTest.php | 2 +- 6 files changed, 224 insertions(+), 161 deletions(-) rename src/Console/Command/Info/{Requirements.php => RequirementsCommand.php} (58%) diff --git a/fixtures/requirement-checker/complete/composer.json b/fixtures/requirement-checker/complete/composer.json index bb6e4677b..173e35d0e 100644 --- a/fixtures/requirement-checker/complete/composer.json +++ b/fixtures/requirement-checker/complete/composer.json @@ -3,6 +3,7 @@ "php": "*", "ext-aerospike": "*", "ext-http": "*", + "ext-mcrypt": "*", "ext-zend-opcache": "*", "paragonie/sodium_compat": "^1.20", "phpseclib/mcrypt_compat": "^2.0", diff --git a/fixtures/requirement-checker/complete/composer.lock b/fixtures/requirement-checker/complete/composer.lock index 98052fe7c..c51d4c7b7 100644 --- a/fixtures/requirement-checker/complete/composer.lock +++ b/fixtures/requirement-checker/complete/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "84607e1c7ffedc4f1c8d3d1b6d91b7f9", + "content-hash": "dee2b55f0996c51560d3af90cf0e7e1f", "packages": [ { "name": "laminas/laminas-code", @@ -274,21 +274,21 @@ }, { "name": "phpseclib/mcrypt_compat", - "version": "2.0.4", + "version": "2.0.6", "source": { "type": "git", "url": "https://github.com/phpseclib/mcrypt_compat.git", - "reference": "6505669343743daf290b7d7b6b7105f85fd9988f" + "reference": "e5924504997b4f90772034cefd89dc2f4ec189dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/mcrypt_compat/zipball/6505669343743daf290b7d7b6b7105f85fd9988f", - "reference": "6505669343743daf290b7d7b6b7105f85fd9988f", + "url": "https://api.github.com/repos/phpseclib/mcrypt_compat/zipball/e5924504997b4f90772034cefd89dc2f4ec189dc", + "reference": "e5924504997b4f90772034cefd89dc2f4ec189dc", "shasum": "" }, "require": { "php": ">=5.6.1", - "phpseclib/phpseclib": ">=3.0.13 <4.0.0" + "phpseclib/phpseclib": ">=3.0.36 <4.0.0" }, "provide": { "ext-mcrypt": "5.6.40" @@ -338,20 +338,20 @@ "type": "tidelift" } ], - "time": "2022-12-19T00:32:45+00:00" + "time": "2024-02-26T14:52:18+00:00" }, { "name": "phpseclib/phpseclib", - "version": "3.0.34", + "version": "3.0.37", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "56c79f16a6ae17e42089c06a2144467acc35348a" + "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56c79f16a6ae17e42089c06a2144467acc35348a", - "reference": "56c79f16a6ae17e42089c06a2144467acc35348a", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8", + "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8", "shasum": "" }, "require": { @@ -432,7 +432,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.34" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.37" }, "funding": [ { @@ -448,20 +448,20 @@ "type": "tidelift" } ], - "time": "2023-11-27T11:13:31+00:00" + "time": "2024-03-03T02:14:58+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", "shasum": "" }, "require": { @@ -475,9 +475,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -515,7 +512,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" }, "funding": [ { @@ -531,20 +528,20 @@ "type": "tidelift" } ], - "time": "2023-07-28T09:04:16+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179" + "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25", + "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25", "shasum": "" }, "require": { @@ -552,9 +549,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -591,7 +585,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0" }, "funding": [ { @@ -607,22 +601,22 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" } ], "packages-dev": [ { "name": "symfony/polyfill-iconv", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "6de50471469b8c9afc38164452ab2b6170ee71c1" + "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/6de50471469b8c9afc38164452ab2b6170ee71c1", - "reference": "6de50471469b8c9afc38164452ab2b6170ee71c1", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f", + "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f", "shasum": "" }, "require": { @@ -636,9 +630,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -676,7 +667,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.29.0" }, "funding": [ { @@ -692,7 +683,7 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" } ], "aliases": [], @@ -704,6 +695,7 @@ "php": "*", "ext-aerospike": "*", "ext-http": "*", + "ext-mcrypt": "*", "ext-zend-opcache": "*" }, "platform-dev": [], diff --git a/src/Console/Application.php b/src/Console/Application.php index 52361416b..15d95120a 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -23,7 +23,7 @@ use KevinGH\Box\Console\Command\ExtractCommand; use KevinGH\Box\Console\Command\GenerateDockerFileCommand; use KevinGH\Box\Console\Command\Info\InfoSignatureCommand as InfoSignature; -use KevinGH\Box\Console\Command\Info\Requirements as InfoRequirements; +use KevinGH\Box\Console\Command\Info\RequirementsCommand as InfoRequirements; use KevinGH\Box\Console\Command\InfoCommand; use KevinGH\Box\Console\Command\NamespaceCommand; use KevinGH\Box\Console\Command\ProcessCommand; diff --git a/src/Console/Command/Info/Requirements.php b/src/Console/Command/Info/RequirementsCommand.php similarity index 58% rename from src/Console/Command/Info/Requirements.php rename to src/Console/Command/Info/RequirementsCommand.php index cd2acd7d0..a14f15e0d 100644 --- a/src/Console/Command/Info/Requirements.php +++ b/src/Console/Command/Info/RequirementsCommand.php @@ -18,6 +18,8 @@ use Fidry\Console\Command\Configuration as ConsoleConfiguration; use Fidry\Console\ExitCode; use Fidry\Console\IO; +use KevinGH\Box\Composer\Artifact\ComposerJson; +use KevinGH\Box\Composer\Artifact\ComposerLock; use KevinGH\Box\Configuration\Configuration; use KevinGH\Box\Console\Command\ChangeWorkingDirOption; use KevinGH\Box\Console\Command\ConfigOption; @@ -26,13 +28,17 @@ use KevinGH\Box\RequirementChecker\Requirements as RequirementsCollection; use KevinGH\Box\RequirementChecker\RequirementType; use stdClass; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputOption; use function array_map; +use function count; +use function is_array; use function iter\filter; use function iter\toArray; use function sprintf; -final readonly class Requirements implements Command +final readonly class RequirementsCommand implements Command { private const NO_CONFIG_OPTION = 'no-config'; @@ -83,45 +89,87 @@ public function execute(IO $io): int return ExitCode::FAILURE; } - $requirements = $this->factory->createUnfiltered( - $composerJson, - $composerLock, - $config->getCompressionAlgorithm(), - ); - [ $phpRequirements, $requiredExtensions, - $providedExtensions, $conflictingExtensions, - ] = self::filterRequirements($requirements); - - $optimizedRequiredRequirements = toArray( - filter( - static fn (Requirement $requirement) => RequirementType::EXTENSION === $requirement->type, - $this->factory - ->create( - $composerJson, - $composerLock, - $config->getCompressionAlgorithm(), - ), - ), + ] = $this->getAllRequirements( + $composerJson, + $composerLock, + $config, + ); + + $optimizedExtensionRequirements = $this->getOptimizedExtensionRequirements( + $composerJson, + $composerLock, + $config, ); self::renderRequiredPHPVersionsSection($phpRequirements, $io); - self::renderRequiredExtensionsSection($requiredExtensions, $io); - self::renderProvidedExtensionsSection($providedExtensions, $io); - self::renderOptimizedRequiredExtensionsSection($optimizedRequiredRequirements, $io); + $io->newLine(); + + self::renderExtensionsSection( + $requiredExtensions, + $io, + ); + $io->newLine(); + + self::renderOptimizedRequiredExtensionsSection($optimizedExtensionRequirements, $io); + $io->newLine(); + self::renderConflictingExtensionsSection($conflictingExtensions, $io); + $io->newLine(); return ExitCode::SUCCESS; } + /** + * @return array{Requirement[], Requirement[], Requirement[]} + */ + private function getAllRequirements( + ComposerJson $composerJson, + ComposerLock $composerLock, + Configuration $config, + ): array + { + $requirements = $this->factory->createUnfiltered( + $composerJson, + $composerLock, + $config->getCompressionAlgorithm(), + ); + + return self::filterRequirements($requirements); + } + + /** + * @return Requirement[] + */ + private function getOptimizedExtensionRequirements( + ComposerJson $composerJson, + ComposerLock $composerLock, + Configuration $config, + ): array + { + $optimizedRequirements = $this->factory->create( + $composerJson, + $composerLock, + $config->getCompressionAlgorithm(), + ); + + $isExtension = static fn (Requirement $requirement) => RequirementType::EXTENSION === $requirement->type; + + return toArray( + filter($isExtension, $optimizedRequirements), + ); + } + + /** + * @return array{Requirement[], Requirement[], Requirement[], Requirement[]} + */ private static function filterRequirements(RequirementsCollection $requirements): array { $phpRequirements = []; $requiredExtensions = []; - $providedExtensions = []; $conflictingExtensions = []; foreach ($requirements as $requirement) { @@ -132,11 +180,8 @@ private static function filterRequirements(RequirementsCollection $requirements) break; case RequirementType::EXTENSION: - $requiredExtensions[] = $requirement; - break; - case RequirementType::PROVIDED_EXTENSION: - $providedExtensions[] = $requirement; + $requiredExtensions[] = $requirement; break; case RequirementType::EXTENSION_CONFLICT: @@ -148,33 +193,34 @@ private static function filterRequirements(RequirementsCollection $requirements) return [ $phpRequirements, $requiredExtensions, - $providedExtensions, $conflictingExtensions, ]; } /** - * @param Requirement[] $required + * @param Requirement[] $requirements */ private static function renderRequiredPHPVersionsSection( - array $required, - IO $io, + array $requirements, + IO $io, ): void { - if (0 === count($required)) { - $io->writeln('No PHP requirement found.'); + if (0 === count($requirements)) { + $io->writeln('No PHP constraint found.'); return; } - $io->writeln('Required PHP versions:'); - $io->writeln( + $io->writeln('The following PHP constraints were found:'); + + self::renderTable( + $io, + ['Constraints', 'Source'], array_map( - static fn (Requirement $requirement) => sprintf( - ' - %s (%s)', + static fn (Requirement $requirement) => [ $requirement->condition, $requirement->source ?? 'root', - ), - $required, + ], + $requirements, ), ); } @@ -182,51 +228,31 @@ private static function renderRequiredPHPVersionsSection( /** * @param Requirement[] $required */ - private static function renderRequiredExtensionsSection( + private static function renderExtensionsSection( array $required, IO $io, ): void { if (0 === count($required)) { - $io->writeln('No required extension found.'); + $io->writeln('No extension constraint found.'); return; } - $io->writeln('Required extensions:'); - $io->writeln( - array_map( - static fn (Requirement $requirement) => sprintf( - ' - ext-%s (%s)', - $requirement->condition, - $requirement->source ?? 'root', - ), - $required, - ), - ); - } + $io->writeln('The following extensions constraints were found:'); - /** - * @param Requirement[] $provided - */ - private static function renderProvidedExtensionsSection( - array $provided, - IO $io, - ): void { - if (0 === count($provided)) { - $io->writeln('No provided extension found.'); - - return; - } - - $io->writeln('Provided extensions:'); - $io->writeln( + self::renderTable( + $io, + ['Type', 'Extension', 'Source'], array_map( - static fn (Requirement $requirement) => sprintf( - ' - ext-%s (%s)', + static fn (Requirement $requirement) => [ + match ($requirement->type) { + RequirementType::EXTENSION => 'required', + RequirementType::PROVIDED_EXTENSION => 'provided', + }, $requirement->condition, $requirement->source ?? 'root', - ), - $provided, + ], + $required, ), ); } @@ -238,20 +264,24 @@ private static function renderOptimizedRequiredExtensionsSection( array $required, IO $io, ): void { + $io->writeln('The required and provided extensions constraints (see above) are resolved to compute the final required extensions.'); + if (0 === count($required)) { - $io->writeln('No required extension found.'); + $io->writeln('The application does not have any extension constraint.'); return; } - $io->writeln('Final required extensions: (accounts for the provided extensions)'); - $io->writeln( + $io->writeln('The application requires the following extension constraints:'); + + self::renderTable( + $io, + ['Extension', 'Source'], array_map( - static fn (Requirement $requirement) => sprintf( - ' - ext-%s (%s)', + static fn (Requirement $requirement) => [ $requirement->condition, $requirement->source ?? 'root', - ), + ], $required, ), ); @@ -265,21 +295,46 @@ private static function renderConflictingExtensionsSection( IO $io, ): void { if (0 === count($conflicting)) { - $io->writeln('No conflicting package found.'); + $io->writeln('No conflicting extension found.'); return; } $io->writeln('Conflicting extensions:'); - $io->writeln( + + self::renderTable( + $io, + ['Extension', 'Source'], array_map( - static fn (Requirement $requirement) => sprintf( - ' - ext-%s (%s)', + static fn (Requirement $requirement) => [ $requirement->condition, $requirement->source ?? 'root', - ), + ], $conflicting, ), ); } + + private static function renderTable( + IO $io, + array $headers, + array|TableSeparator ...$rowsList, + ): void + { + /** @var Table $table */ + $table = $io->createTable(); + $table->setStyle('box'); + + $table->setHeaders($headers); + + foreach ($rowsList as $rowsOrTableSeparator) { + if (is_array($rowsOrTableSeparator)) { + $table->addRows($rowsOrTableSeparator); + } else { + $table->addRow($rowsOrTableSeparator); + } + } + + $table->render(); + } } diff --git a/src/RequirementChecker/RequirementsBuilder.php b/src/RequirementChecker/RequirementsBuilder.php index d4518edc9..a0306930e 100644 --- a/src/RequirementChecker/RequirementsBuilder.php +++ b/src/RequirementChecker/RequirementsBuilder.php @@ -19,14 +19,36 @@ use function array_map; use function array_unique; use function natsort; +use function strcmp; use function strnatcmp; use function uksort; +use function usort; final class RequirementsBuilder { + /** + * @var list + */ private array $predefinedRequirements = []; + + /** + * @var array> + */ private array $requiredExtensions = []; + + /** + * @var array> + */ private array $providedExtensions = []; + + /** + * @var array + */ + private array $allExtensions = []; + + /** + * @var array> + */ private array $conflictingExtensions = []; public function addRequirement(Requirement $requirement): void @@ -37,11 +59,13 @@ public function addRequirement(Requirement $requirement): void public function addRequiredExtension(Extension $extension, ?string $source): void { $this->requiredExtensions[$extension->name][] = $source; + $this->allExtensions[$extension->name][] = [$source, RequirementType::EXTENSION]; } public function addProvidedExtension(Extension $extension, ?string $source): void { $this->providedExtensions[$extension->name][] = $source; + $this->allExtensions[$extension->name][] = [$source, RequirementType::PROVIDED_EXTENSION]; } public function addConflictingExtension(Extension $extension, ?string $source): void @@ -53,21 +77,17 @@ public function all(): Requirements { $requirements = $this->predefinedRequirements; - foreach ($this->getUnfilteredSortedRequiredExtensions() as $extensionName => $sources) { - foreach ($sources as $source) { - $requirements[] = Requirement::forRequiredExtension( - $extensionName, - $source, - ); - } - } - - foreach ($this->getSortedProvidedExtensions() as $extensionName => $sources) { - foreach ($sources as $source) { - $requirements[] = Requirement::forProvidedExtension( - $extensionName, - $source, - ); + foreach ($this->getSortedRequiredAndProvidedExtensions() as $extensionName => $sources) { + foreach ($sources as [$source, $type]) { + $requirements[] = RequirementType::EXTENSION === $type + ? Requirement::forRequiredExtension( + $extensionName, + $source, + ) + : Requirement::forProvidedExtension( + $extensionName, + $source, + ); } } @@ -109,33 +129,28 @@ public function build(): Requirements } /** - * @return array> + * @return array> */ - private function getUnfilteredSortedRequiredExtensions(): array + private function getSortedRequiredAndProvidedExtensions(): array { return array_map( - self::createSortedDistinctList(...), - self::sortByExtensionName( - $this->requiredExtensions, - ), - ); - } + static function (array $sources): array { + usort( + $sources, + static fn (array $sourceTypePairA, array $sourceTypePairB) => strcmp( + (string) $sourceTypePairA[0], + (string) $sourceTypePairB[0], + ), + ); - /** - * @return array> - */ - private function getSortedProvidedExtensions(): array - { - return array_map( - self::createSortedDistinctList(...), - self::sortByExtensionName( - $this->providedExtensions, - ), + return $sources; + }, + self::sortByExtensionName($this->allExtensions), ); } /** - * @return array> + * @return array> */ private function getSortedRequiredExtensions(): array { @@ -151,7 +166,7 @@ private function getSortedRequiredExtensions(): array } /** - * @return array> + * @return array> */ private function getSortedConflictedExtensions(): array { diff --git a/tests/Console/Command/Info/RequirementsTest.php b/tests/Console/Command/Info/RequirementsTest.php index da3bb603d..6905b9879 100644 --- a/tests/Console/Command/Info/RequirementsTest.php +++ b/tests/Console/Command/Info/RequirementsTest.php @@ -15,7 +15,7 @@ namespace Console\Command\Info; use Fidry\Console\Test\CommandTester; -use KevinGH\Box\Console\Command\Info\Requirements as RequirementsCommand; +use KevinGH\Box\Console\Command\Info\RequirementsCommand as RequirementsCommand; use KevinGH\Box\RequirementChecker\AppRequirementsFactory; use KevinGH\Box\RequirementChecker\Requirements; use PHPUnit\Framework\Attributes\CoversClass; From b87dfd3f71b38512dd52b41a660cbaf706cdd0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Tue, 12 Mar 2024 21:12:42 +0100 Subject: [PATCH 07/14] rename test --- .../Info/{RequirementsTest.php => RequirementsCommandTest.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/Console/Command/Info/{RequirementsTest.php => RequirementsCommandTest.php} (98%) diff --git a/tests/Console/Command/Info/RequirementsTest.php b/tests/Console/Command/Info/RequirementsCommandTest.php similarity index 98% rename from tests/Console/Command/Info/RequirementsTest.php rename to tests/Console/Command/Info/RequirementsCommandTest.php index 6905b9879..fa8e8db33 100644 --- a/tests/Console/Command/Info/RequirementsTest.php +++ b/tests/Console/Command/Info/RequirementsCommandTest.php @@ -29,7 +29,7 @@ * @internal */ #[CoversClass(RequirementsCommand::class)] -class RequirementsTest extends TestCase +class RequirementsCommandTest extends TestCase { use ProphecyTrait; From 6690364df346b4ca0c354f0168f432235889ef4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Tue, 12 Mar 2024 21:19:34 +0100 Subject: [PATCH 08/14] add tet --- .../complete/composer.json | 26 ----- .../Command/Info/RequirementsCommand.php | 1 - .../AppRequirementsFactory.php | 3 +- .../Command/Info/RequirementsCommandTest.php | 105 ++++++++++++------ 4 files changed, 70 insertions(+), 65 deletions(-) delete mode 100644 fixtures/requirement-checker/complete/composer.json diff --git a/fixtures/requirement-checker/complete/composer.json b/fixtures/requirement-checker/complete/composer.json deleted file mode 100644 index 173e35d0e..000000000 --- a/fixtures/requirement-checker/complete/composer.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "require": { - "php": "*", - "ext-aerospike": "*", - "ext-http": "*", - "ext-mcrypt": "*", - "ext-zend-opcache": "*", - "paragonie/sodium_compat": "^1.20", - "phpseclib/mcrypt_compat": "^2.0", - "laminas/laminas-code": "^4.13", - "symfony/polyfill-mbstring": "^1.28", - "symfony/polyfill-php72": "^1.28" - }, - "conflict": { - "ext-apache": "*", - "ext-bcmath": "*", - "symfony/polyfill-php83": "*" - }, - "provide": { - "ext-crypto": "*", - "ext-calendar": "*" - }, - "require-dev": { - "symfony/polyfill-iconv": "^1.28" - } -} diff --git a/src/Console/Command/Info/RequirementsCommand.php b/src/Console/Command/Info/RequirementsCommand.php index a14f15e0d..a575fb2b6 100644 --- a/src/Console/Command/Info/RequirementsCommand.php +++ b/src/Console/Command/Info/RequirementsCommand.php @@ -118,7 +118,6 @@ public function execute(IO $io): int $io->newLine(); self::renderConflictingExtensionsSection($conflictingExtensions, $io); - $io->newLine(); return ExitCode::SUCCESS; } diff --git a/src/RequirementChecker/AppRequirementsFactory.php b/src/RequirementChecker/AppRequirementsFactory.php index 5aede6d6e..f1f123a07 100644 --- a/src/RequirementChecker/AppRequirementsFactory.php +++ b/src/RequirementChecker/AppRequirementsFactory.php @@ -22,9 +22,10 @@ /** * Collect the list of requirements for running the application. * + * @final * @private */ -final class AppRequirementsFactory +class AppRequirementsFactory { private const SELF_PACKAGE = null; diff --git a/tests/Console/Command/Info/RequirementsCommandTest.php b/tests/Console/Command/Info/RequirementsCommandTest.php index fa8e8db33..dbdf3b686 100644 --- a/tests/Console/Command/Info/RequirementsCommandTest.php +++ b/tests/Console/Command/Info/RequirementsCommandTest.php @@ -17,6 +17,7 @@ use Fidry\Console\Test\CommandTester; use KevinGH\Box\Console\Command\Info\RequirementsCommand as RequirementsCommand; use KevinGH\Box\RequirementChecker\AppRequirementsFactory; +use KevinGH\Box\RequirementChecker\Requirement; use KevinGH\Box\RequirementChecker\Requirements; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; @@ -33,8 +34,6 @@ class RequirementsCommandTest extends TestCase { use ProphecyTrait; - private const FIXTURES = __DIR__.'/../../../../fixtures/requirement-checker'; - private AppRequirementsFactory|ObjectProphecy $factoryProphecy; private CommandTester $commandTester; @@ -49,12 +48,17 @@ protected function setUp(): void #[DataProvider('requirementsProvider')] public function test_it_provides_info_about_the_app_requirements( - Requirements $requirements, + Requirements $allRequirements, + Requirements $optimizedRequirements, string $expected, ): void { + $this->factoryProphecy + ->createUnfiltered(Argument::cetera()) + ->willReturn($allRequirements); + $this->factoryProphecy ->create(Argument::cetera()) - ->willReturn($requirements); + ->willReturn($optimizedRequirements); $this->commandTester->execute(['--no-config' => null]); @@ -68,45 +72,72 @@ public static function requirementsProvider(): iterable { yield 'empty' => [ new Requirements([]), - '', - ]; - - yield 'a real case' => [, - '', - ]; - - return; - yield 'PHAR with requirement checker; one PHP and extension and conflict requirement' => [ - ['phar' => self::FIXTURES.'/req-checker-ext-and-php-and-conflict.phar'], + new Requirements([]), <<<'OUTPUT' + No PHP constraint found. - API Version: 1.1.0 + No extension constraint found. - Built with Box: dev-main@b2c33cd + The required and provided extensions constraints (see above) are resolved to compute the final required extensions. + The application does not have any extension constraint. - Archive Compression: None - Files Compression: None - - Signature: SHA-1 - Signature Hash: 2882E27FCEE2268DB6E18A7BBB8B92906F286458 - - Metadata: None - - Timestamp: 1697989559 (2023-10-22T15:45:59+00:00) - - RequirementChecker: - Required: - - PHP ^7.2 (root) - - ext-json (root) - Conflict: - - ext-aerospike (root) - - Contents: 45 files (148.23KB) - - // Use the --list|-l option to list the content of the PHAR. + No conflicting extension found. + OUTPUT, + ]; - OUTPUT, + yield 'a real case' => [ + new Requirements([ + Requirement::forPHP('>=7.2', null), + Requirement::forRequiredExtension('http', 'package1'), + Requirement::forRequiredExtension('http', 'package2'), + Requirement::forProvidedExtension('http', null), + Requirement::forRequiredExtension('openssl', 'package1'), + Requirement::forProvidedExtension('zip', null), + Requirement::forConflictingExtension('openssl', 'package3'), + Requirement::forConflictingExtension('phar', 'package1'), + ]), + new Requirements([ + Requirement::forRequiredExtension('openssl', 'package1'), + Requirement::forConflictingExtension('openssl', 'package3'), + Requirement::forConflictingExtension('phar', 'package1'), + ]), + <<<'OUTPUT' + The following PHP constraints were found: + ┌─────────────┬────────┐ + │ Constraints │ Source │ + ├─────────────┼────────┤ + │ >=7.2 │ root │ + └─────────────┴────────┘ + + The following extensions constraints were found: + ┌──────────┬───────────┬──────────┐ + │ Type │ Extension │ Source │ + ├──────────┼───────────┼──────────┤ + │ required │ http │ package1 │ + │ required │ http │ package2 │ + │ provided │ http │ root │ + │ required │ openssl │ package1 │ + │ provided │ zip │ root │ + └──────────┴───────────┴──────────┘ + + The required and provided extensions constraints (see above) are resolved to compute the final required extensions. + The application requires the following extension constraints: + ┌───────────┬──────────┐ + │ Extension │ Source │ + ├───────────┼──────────┤ + │ openssl │ package1 │ + └───────────┴──────────┘ + + Conflicting extensions: + ┌───────────┬──────────┐ + │ Extension │ Source │ + ├───────────┼──────────┤ + │ openssl │ package3 │ + │ phar │ package1 │ + └───────────┴──────────┘ + + OUTPUT, ]; } } From baa821c6b70fc7497116c895aff8a85c48b8d47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Tue, 12 Mar 2024 21:19:46 +0100 Subject: [PATCH 09/14] fix cs --- .../Command/Info/RequirementsCommand.php | 12 +-- .../Command/Info/RequirementsCommandTest.php | 84 +++++++++---------- 2 files changed, 46 insertions(+), 50 deletions(-) diff --git a/src/Console/Command/Info/RequirementsCommand.php b/src/Console/Command/Info/RequirementsCommand.php index a575fb2b6..5270915c9 100644 --- a/src/Console/Command/Info/RequirementsCommand.php +++ b/src/Console/Command/Info/RequirementsCommand.php @@ -36,7 +36,6 @@ use function is_array; use function iter\filter; use function iter\toArray; -use function sprintf; final readonly class RequirementsCommand implements Command { @@ -129,8 +128,7 @@ private function getAllRequirements( ComposerJson $composerJson, ComposerLock $composerLock, Configuration $config, - ): array - { + ): array { $requirements = $this->factory->createUnfiltered( $composerJson, $composerLock, @@ -147,8 +145,7 @@ private function getOptimizedExtensionRequirements( ComposerJson $composerJson, ComposerLock $composerLock, Configuration $config, - ): array - { + ): array { $optimizedRequirements = $this->factory->create( $composerJson, $composerLock, @@ -201,7 +198,7 @@ private static function filterRequirements(RequirementsCollection $requirements) */ private static function renderRequiredPHPVersionsSection( array $requirements, - IO $io, + IO $io, ): void { if (0 === count($requirements)) { $io->writeln('No PHP constraint found.'); @@ -318,8 +315,7 @@ private static function renderTable( IO $io, array $headers, array|TableSeparator ...$rowsList, - ): void - { + ): void { /** @var Table $table */ $table = $io->createTable(); $table->setStyle('box'); diff --git a/tests/Console/Command/Info/RequirementsCommandTest.php b/tests/Console/Command/Info/RequirementsCommandTest.php index dbdf3b686..62653ef4d 100644 --- a/tests/Console/Command/Info/RequirementsCommandTest.php +++ b/tests/Console/Command/Info/RequirementsCommandTest.php @@ -15,7 +15,7 @@ namespace Console\Command\Info; use Fidry\Console\Test\CommandTester; -use KevinGH\Box\Console\Command\Info\RequirementsCommand as RequirementsCommand; +use KevinGH\Box\Console\Command\Info\RequirementsCommand; use KevinGH\Box\RequirementChecker\AppRequirementsFactory; use KevinGH\Box\RequirementChecker\Requirement; use KevinGH\Box\RequirementChecker\Requirements; @@ -74,16 +74,16 @@ public static function requirementsProvider(): iterable new Requirements([]), new Requirements([]), <<<'OUTPUT' - No PHP constraint found. + No PHP constraint found. - No extension constraint found. + No extension constraint found. - The required and provided extensions constraints (see above) are resolved to compute the final required extensions. - The application does not have any extension constraint. + The required and provided extensions constraints (see above) are resolved to compute the final required extensions. + The application does not have any extension constraint. - No conflicting extension found. + No conflicting extension found. - OUTPUT, + OUTPUT, ]; yield 'a real case' => [ @@ -103,41 +103,41 @@ public static function requirementsProvider(): iterable Requirement::forConflictingExtension('phar', 'package1'), ]), <<<'OUTPUT' - The following PHP constraints were found: - ┌─────────────┬────────┐ - │ Constraints │ Source │ - ├─────────────┼────────┤ - │ >=7.2 │ root │ - └─────────────┴────────┘ - - The following extensions constraints were found: - ┌──────────┬───────────┬──────────┐ - │ Type │ Extension │ Source │ - ├──────────┼───────────┼──────────┤ - │ required │ http │ package1 │ - │ required │ http │ package2 │ - │ provided │ http │ root │ - │ required │ openssl │ package1 │ - │ provided │ zip │ root │ - └──────────┴───────────┴──────────┘ - - The required and provided extensions constraints (see above) are resolved to compute the final required extensions. - The application requires the following extension constraints: - ┌───────────┬──────────┐ - │ Extension │ Source │ - ├───────────┼──────────┤ - │ openssl │ package1 │ - └───────────┴──────────┘ - - Conflicting extensions: - ┌───────────┬──────────┐ - │ Extension │ Source │ - ├───────────┼──────────┤ - │ openssl │ package3 │ - │ phar │ package1 │ - └───────────┴──────────┘ - - OUTPUT, + The following PHP constraints were found: + ┌─────────────┬────────┐ + │ Constraints │ Source │ + ├─────────────┼────────┤ + │ >=7.2 │ root │ + └─────────────┴────────┘ + + The following extensions constraints were found: + ┌──────────┬───────────┬──────────┐ + │ Type │ Extension │ Source │ + ├──────────┼───────────┼──────────┤ + │ required │ http │ package1 │ + │ required │ http │ package2 │ + │ provided │ http │ root │ + │ required │ openssl │ package1 │ + │ provided │ zip │ root │ + └──────────┴───────────┴──────────┘ + + The required and provided extensions constraints (see above) are resolved to compute the final required extensions. + The application requires the following extension constraints: + ┌───────────┬──────────┐ + │ Extension │ Source │ + ├───────────┼──────────┤ + │ openssl │ package1 │ + └───────────┴──────────┘ + + Conflicting extensions: + ┌───────────┬──────────┐ + │ Extension │ Source │ + ├───────────┼──────────┤ + │ openssl │ package3 │ + │ phar │ package1 │ + └───────────┴──────────┘ + + OUTPUT, ]; } } From f6caf075d2d9b83444cecd39319f17dc028bb260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Tue, 12 Mar 2024 21:20:09 +0100 Subject: [PATCH 10/14] cleanup --- .../complete/composer.lock | 703 ------------------ .../requirement-checker/complete/index.php | 11 - 2 files changed, 714 deletions(-) delete mode 100644 fixtures/requirement-checker/complete/composer.lock delete mode 100644 fixtures/requirement-checker/complete/index.php diff --git a/fixtures/requirement-checker/complete/composer.lock b/fixtures/requirement-checker/complete/composer.lock deleted file mode 100644 index c51d4c7b7..000000000 --- a/fixtures/requirement-checker/complete/composer.lock +++ /dev/null @@ -1,703 +0,0 @@ -{ - "_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": "dee2b55f0996c51560d3af90cf0e7e1f", - "packages": [ - { - "name": "laminas/laminas-code", - "version": "4.13.0", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-code.git", - "reference": "7353d4099ad5388e84737dd16994316a04f48dbf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-code/zipball/7353d4099ad5388e84737dd16994316a04f48dbf", - "reference": "7353d4099ad5388e84737dd16994316a04f48dbf", - "shasum": "" - }, - "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0" - }, - "require-dev": { - "doctrine/annotations": "^2.0.1", - "ext-phar": "*", - "laminas/laminas-coding-standard": "^2.5.0", - "laminas/laminas-stdlib": "^3.17.0", - "phpunit/phpunit": "^10.3.3", - "psalm/plugin-phpunit": "^0.18.4", - "vimeo/psalm": "^5.15.0" - }, - "suggest": { - "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", - "laminas/laminas-stdlib": "Laminas\\Stdlib component" - }, - "type": "library", - "autoload": { - "psr-4": { - "Laminas\\Code\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", - "homepage": "https://laminas.dev", - "keywords": [ - "code", - "laminas", - "laminasframework" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-code/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-code/issues", - "rss": "https://github.com/laminas/laminas-code/releases.atom", - "source": "https://github.com/laminas/laminas-code" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2023-10-18T10:00:55+00:00" - }, - { - "name": "paragonie/constant_time_encoding", - "version": "v2.6.3", - "source": { - "type": "git", - "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "58c3f47f650c94ec05a151692652a868995d2938" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", - "reference": "58c3f47f650c94ec05a151692652a868995d2938", - "shasum": "" - }, - "require": { - "php": "^7|^8" - }, - "require-dev": { - "phpunit/phpunit": "^6|^7|^8|^9", - "vimeo/psalm": "^1|^2|^3|^4" - }, - "type": "library", - "autoload": { - "psr-4": { - "ParagonIE\\ConstantTime\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com", - "role": "Maintainer" - }, - { - "name": "Steve 'Sc00bz' Thomas", - "email": "steve@tobtu.com", - "homepage": "https://www.tobtu.com", - "role": "Original Developer" - } - ], - "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", - "keywords": [ - "base16", - "base32", - "base32_decode", - "base32_encode", - "base64", - "base64_decode", - "base64_encode", - "bin2hex", - "encoding", - "hex", - "hex2bin", - "rfc4648" - ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/constant_time_encoding/issues", - "source": "https://github.com/paragonie/constant_time_encoding" - }, - "time": "2022-06-14T06:56:20+00:00" - }, - { - "name": "paragonie/random_compat", - "version": "v9.99.100", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", - "shasum": "" - }, - "require": { - "php": ">= 7" - }, - "require-dev": { - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^1" - }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", - "keywords": [ - "csprng", - "polyfill", - "pseudorandom", - "random" - ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/random_compat/issues", - "source": "https://github.com/paragonie/random_compat" - }, - "time": "2020-10-15T08:29:30+00:00" - }, - { - "name": "paragonie/sodium_compat", - "version": "v1.20.0", - "source": { - "type": "git", - "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "e592a3e06d1fa0d43988c7c7d9948ca836f644b6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/e592a3e06d1fa0d43988c7c7d9948ca836f644b6", - "reference": "e592a3e06d1fa0d43988c7c7d9948ca836f644b6", - "shasum": "" - }, - "require": { - "paragonie/random_compat": ">=1", - "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" - }, - "require-dev": { - "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9" - }, - "suggest": { - "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", - "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." - }, - "type": "library", - "autoload": { - "files": [ - "autoload.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com" - }, - { - "name": "Frank Denis", - "email": "jedisct1@pureftpd.org" - } - ], - "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", - "keywords": [ - "Authentication", - "BLAKE2b", - "ChaCha20", - "ChaCha20-Poly1305", - "Chapoly", - "Curve25519", - "Ed25519", - "EdDSA", - "Edwards-curve Digital Signature Algorithm", - "Elliptic Curve Diffie-Hellman", - "Poly1305", - "Pure-PHP cryptography", - "RFC 7748", - "RFC 8032", - "Salpoly", - "Salsa20", - "X25519", - "XChaCha20-Poly1305", - "XSalsa20-Poly1305", - "Xchacha20", - "Xsalsa20", - "aead", - "cryptography", - "ecdh", - "elliptic curve", - "elliptic curve cryptography", - "encryption", - "libsodium", - "php", - "public-key cryptography", - "secret-key cryptography", - "side-channel resistant" - ], - "support": { - "issues": "https://github.com/paragonie/sodium_compat/issues", - "source": "https://github.com/paragonie/sodium_compat/tree/v1.20.0" - }, - "time": "2023-04-30T00:54:53+00:00" - }, - { - "name": "phpseclib/mcrypt_compat", - "version": "2.0.6", - "source": { - "type": "git", - "url": "https://github.com/phpseclib/mcrypt_compat.git", - "reference": "e5924504997b4f90772034cefd89dc2f4ec189dc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpseclib/mcrypt_compat/zipball/e5924504997b4f90772034cefd89dc2f4ec189dc", - "reference": "e5924504997b4f90772034cefd89dc2f4ec189dc", - "shasum": "" - }, - "require": { - "php": ">=5.6.1", - "phpseclib/phpseclib": ">=3.0.36 <4.0.0" - }, - "provide": { - "ext-mcrypt": "5.6.40" - }, - "require-dev": { - "phpunit/phpunit": "^5.7|^6.0|^9.4" - }, - "suggest": { - "ext-openssl": "Will enable faster cryptographic operations" - }, - "type": "library", - "autoload": { - "files": [ - "lib/mcrypt.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jim Wigginton", - "email": "terrafrost@php.net", - "homepage": "http://phpseclib.sourceforge.net" - } - ], - "description": "PHP 5.x-8.x polyfill for mcrypt extension", - "keywords": [ - "cryptograpy", - "encryption", - "mcrypt", - "polyfill" - ], - "support": { - "email": "terrafrost@php.net", - "issues": "https://github.com/phpseclib/mcrypt_compat/issues", - "source": "https://github.com/phpseclib/mcrypt_compat" - }, - "funding": [ - { - "url": "https://www.patreon.com/phpseclib", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpseclib/mcrypt_compat", - "type": "tidelift" - } - ], - "time": "2024-02-26T14:52:18+00:00" - }, - { - "name": "phpseclib/phpseclib", - "version": "3.0.37", - "source": { - "type": "git", - "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8", - "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8", - "shasum": "" - }, - "require": { - "paragonie/constant_time_encoding": "^1|^2", - "paragonie/random_compat": "^1.4|^2.0|^9.99.99", - "php": ">=5.6.1" - }, - "require-dev": { - "phpunit/phpunit": "*" - }, - "suggest": { - "ext-dom": "Install the DOM extension to load XML formatted public keys.", - "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", - "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", - "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", - "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." - }, - "type": "library", - "autoload": { - "files": [ - "phpseclib/bootstrap.php" - ], - "psr-4": { - "phpseclib3\\": "phpseclib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jim Wigginton", - "email": "terrafrost@php.net", - "role": "Lead Developer" - }, - { - "name": "Patrick Monnerat", - "email": "pm@datasphere.ch", - "role": "Developer" - }, - { - "name": "Andreas Fischer", - "email": "bantu@phpbb.com", - "role": "Developer" - }, - { - "name": "Hans-Jürgen Petrich", - "email": "petrich@tronic-media.com", - "role": "Developer" - }, - { - "name": "Graham Campbell", - "email": "graham@alt-three.com", - "role": "Developer" - } - ], - "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", - "homepage": "http://phpseclib.sourceforge.net", - "keywords": [ - "BigInteger", - "aes", - "asn.1", - "asn1", - "blowfish", - "crypto", - "cryptography", - "encryption", - "rsa", - "security", - "sftp", - "signature", - "signing", - "ssh", - "twofish", - "x.509", - "x509" - ], - "support": { - "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.37" - }, - "funding": [ - { - "url": "https://github.com/terrafrost", - "type": "github" - }, - { - "url": "https://www.patreon.com/phpseclib", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", - "type": "tidelift" - } - ], - "time": "2024-03-03T02:14:58+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "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.29.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": "2024-01-29T20:11:03+00:00" - }, - { - "name": "symfony/polyfill-php72", - "version": "v1.29.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25", - "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.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": "2024-01-29T20:11:03+00:00" - } - ], - "packages-dev": [ - { - "name": "symfony/polyfill-iconv", - "version": "v1.29.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f", - "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-iconv": "*" - }, - "suggest": { - "ext-iconv": "For best performance" - }, - "type": "library", - "extra": { - "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.29.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": "2024-01-29T20:11:03+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": "*", - "ext-aerospike": "*", - "ext-http": "*", - "ext-mcrypt": "*", - "ext-zend-opcache": "*" - }, - "platform-dev": [], - "plugin-api-version": "2.6.0" -} diff --git a/fixtures/requirement-checker/complete/index.php b/fixtures/requirement-checker/complete/index.php deleted file mode 100644 index 80c8b2af6..000000000 --- a/fixtures/requirement-checker/complete/index.php +++ /dev/null @@ -1,11 +0,0 @@ - - * Théo Fidry - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ From 6bf9e36eb0dedf48daecdd54cb51065ea48ccabc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Tue, 12 Mar 2024 21:25:00 +0100 Subject: [PATCH 11/14] fix tests --- src/Console/Command/Info/RequirementsCommand.php | 2 +- tests/Console/ApplicationTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Console/Command/Info/RequirementsCommand.php b/src/Console/Command/Info/RequirementsCommand.php index 5270915c9..bfbc6a883 100644 --- a/src/Console/Command/Info/RequirementsCommand.php +++ b/src/Console/Command/Info/RequirementsCommand.php @@ -50,7 +50,7 @@ public function getConfiguration(): ConsoleConfiguration { return new ConsoleConfiguration( 'info:requirements', - 'Lists the application requirements found.', + 'Lists the application requirements found', 'The %command.name% command will list the PHP versions and extensions required to run the built PHAR.', options: [ new InputOption( diff --git a/tests/Console/ApplicationTest.php b/tests/Console/ApplicationTest.php index 0aefedcd6..14146e51e 100644 --- a/tests/Console/ApplicationTest.php +++ b/tests/Console/ApplicationTest.php @@ -123,6 +123,7 @@ public function test_get_helper_menu(): void extract 🚚 Extracts a given PHAR into a directory help Display help for a command info 🔍 Displays information about the PHAR extension or file + info:requirements Lists the application requirements found list List commands namespace Prints the first part of the command namespace process ⚡ Applies the registered compactors and replacement values on a file From 4c9eebd159f440ad1877860378cb6ed78ed4e4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Tue, 12 Mar 2024 21:37:32 +0100 Subject: [PATCH 12/14] fix dege case --- .../RequirementsBuilder.php | 2 +- .../RequirementsBuilderTest.php | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/RequirementChecker/RequirementsBuilder.php b/src/RequirementChecker/RequirementsBuilder.php index d8a5b4a2d..1cf8a9339 100644 --- a/src/RequirementChecker/RequirementsBuilder.php +++ b/src/RequirementChecker/RequirementsBuilder.php @@ -138,7 +138,7 @@ static function (array $sources): array { usort( $sources, static fn (array $sourceTypePairA, array $sourceTypePairB) => strcmp( - $sourceTypePairA[0], + (string) $sourceTypePairA[0], (string) $sourceTypePairB[0], ), ); diff --git a/tests/RequirementChecker/RequirementsBuilderTest.php b/tests/RequirementChecker/RequirementsBuilderTest.php index f98cad507..a99cbef0f 100644 --- a/tests/RequirementChecker/RequirementsBuilderTest.php +++ b/tests/RequirementChecker/RequirementsBuilderTest.php @@ -108,9 +108,40 @@ public function test_it_can_build_requirements_from_provided_extensions(): void new Extension('http'), 'package2', ); + $this->builder->addProvidedExtension( + new Extension('http'), + null, + ); + + $expectedBuiltRequirements = new Requirements([]); + $expectedAllRequirements = new Requirements([ + Requirement::forProvidedExtension('http', null), + Requirement::forProvidedExtension('http', 'package1'), + Requirement::forProvidedExtension('http', 'package2'), + ]); + + $this->assertBuiltRequirementsEquals($expectedBuiltRequirements); + $this->assertAllRequirementsEquals($expectedAllRequirements); + } + + public function test_it_can_build_requirements_from_provided_extensions_sorting_edge_case(): void + { + $this->builder->addProvidedExtension( + new Extension('http'), + null, + ); + $this->builder->addProvidedExtension( + new Extension('http'), + 'package1', + ); + $this->builder->addProvidedExtension( + new Extension('http'), + 'package2', + ); $expectedBuiltRequirements = new Requirements([]); $expectedAllRequirements = new Requirements([ + Requirement::forProvidedExtension('http', null), Requirement::forProvidedExtension('http', 'package1'), Requirement::forProvidedExtension('http', 'package2'), ]); From 4379043d9c53362ed611843c358d480926800dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Wed, 13 Mar 2024 08:22:54 +0100 Subject: [PATCH 13/14] fix menu test --- tests/Console/ApplicationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Console/ApplicationTest.php b/tests/Console/ApplicationTest.php index 14146e51e..2a0a3354f 100644 --- a/tests/Console/ApplicationTest.php +++ b/tests/Console/ApplicationTest.php @@ -123,7 +123,6 @@ public function test_get_helper_menu(): void extract 🚚 Extracts a given PHAR into a directory help Display help for a command info 🔍 Displays information about the PHAR extension or file - info:requirements Lists the application requirements found list List commands namespace Prints the first part of the command namespace process ⚡ Applies the registered compactors and replacement values on a file @@ -136,6 +135,7 @@ public function test_get_helper_menu(): void composer:vendor-dir Shows the Composer vendor-dir configured info info:general 🔍 Displays information about the PHAR extension or file + info:requirements Lists the application requirements found info:signature Displays the hash of the signature EOF; From d4fafcef28b2154a1233319be9710616c1a17ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Wed, 13 Mar 2024 08:38:04 +0100 Subject: [PATCH 14/14] fix infectino --- infection.json.dist | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/infection.json.dist b/infection.json.dist index 13c0f673e..e93213c3b 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -16,7 +16,17 @@ "global-ignoreSourceCodeByRegex": [ "Assert::.*" ], + "Coalesce": { + ignore: [ + "KevinGH\\Box\\Console\\Command\\Info\\RequirementsCommand::execute" + ] + }, "IdenticalEqual": false, + "MethodCallRemoval": { + ignore: [ + "KevinGH\\Box\\Console\\Command\\Info\\RequirementsCommand::execute" + ] + }, "NotIdenticalNotEqual": false, "PublicVisibility": false }