├── .gitignore ├── .editorconfig ├── phpstan.neon ├── bin └── check-packages ├── composer.json ├── src ├── Objects │ └── Package.php └── Console │ └── CheckCommand.php ├── .php-cs-fixer.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | composer.lock 4 | .php-cs-fixer.cache 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [*.php] 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | # Project configuration for phpstan static analysis. 2 | # 3 | # See https://phpstan.org/config-reference for all options 4 | 5 | parameters: 6 | level: 8 7 | errorFormat: table 8 | paths: 9 | - src 10 | editorUrl: '%%file%%:%%line%%' 11 | editorUrlTitle: '%%file%%:%%line%%' 12 | -------------------------------------------------------------------------------- /bin/check-packages: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add($command); 20 | 21 | $app->setDefaultCommand($command->getName(), true); 22 | $app->run(); 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cviebrock/package-checker", 3 | "description": "Tool to check all the composer packages in a project for PHP compatibility.", 4 | "homepage": "https://github.com/cviebrock/package-checker", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Colin Viebrock", 9 | "email": "colin@viebrock.ca" 10 | } 11 | ], 12 | "bin": [ 13 | "bin/check-packages" 14 | ], 15 | "require": { 16 | "php": "^8.0", 17 | "composer/composer": "^2.7", 18 | "composer/semver": "^3.4", 19 | "symfony/console": "^6.0 || ^7.0" 20 | }, 21 | "require-dev": { 22 | "friendsofphp/php-cs-fixer": "^3.64", 23 | "phpstan/phpstan": "^1.12" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Silverorange\\PackageChecker\\": "src/" 28 | } 29 | }, 30 | "scripts": { 31 | "phpcs": "./vendor/bin/php-cs-fixer check -v", 32 | "phpcs:fix": "./vendor/bin/php-cs-fixer fix -v", 33 | "phpstan": "./vendor/bin/phpstan analyze" 34 | }, 35 | "config": { 36 | "preferred-install": "dist", 37 | "sort-packages": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Objects/Package.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public array $require = []; 21 | 22 | /** 23 | * @var array 24 | */ 25 | public array $requireDev = []; 26 | 27 | public ?string $phpRequirement = null; 28 | 29 | public ?string $homepage = null; 30 | 31 | public ?string $source = null; 32 | 33 | public function getPackagistLink(): string 34 | { 35 | $version = str_starts_with($this->version, 'v') 36 | ? $this->version 37 | : 'v' . $this->version; 38 | 39 | return "https://packagist.org/packages/{$this->name}#{$version}"; 40 | } 41 | 42 | /** 43 | * @return self::VALIDITY_* 44 | */ 45 | public function isValidForTarget(string $phpVersion): string 46 | { 47 | if ($this->phpRequirement === null) { 48 | return self::VALIDITY_UNKNOWN; 49 | } 50 | 51 | return Semver::satisfies($phpVersion, $this->phpRequirement) 52 | ? self::VALIDITY_OK 53 | : self::VALIDITY_FAIL; 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | public function getLinks(): array 60 | { 61 | return [ 62 | 'Homepage' => $this->homepage ?? 'none', 63 | 'Source' => $this->source ?? 'none', 64 | 'Packagist' => $this->getPackagistLink(), 65 | ]; 66 | } 67 | 68 | public static function fromRawData(object $package): self 69 | { 70 | $new = new self(); 71 | $new->name = $package->name ?? ''; 72 | $new->version = $package->version ?? ''; 73 | $new->require = (array) ($package->require ?? []); 74 | $new->requireDev = (array) ($package->{'require-dev'} ?? []); 75 | $new->phpRequirement = $new->require['php'] ?? null; 76 | $new->homepage = $package->homepage ?? null; 77 | $new->source = $package->support->source ?? null; 78 | 79 | return $new; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__); 9 | 10 | return (new Config()) 11 | ->setRules([ 12 | '@PhpCsFixer' => true, 13 | '@PHP82Migration' => true, 14 | 'indentation_type' => true, 15 | 16 | // Overrides for (opinionated) @PhpCsFixer and @Symfony rules: 17 | 18 | // Align "=>" in multi-line array definitions, unless a blank line exists between elements 19 | 'binary_operator_spaces' => [ 20 | 'operators' => [ 21 | '=>' => 'align_single_space_minimal', 22 | ], 23 | ], 24 | 25 | // Subset of statements that should be proceeded with blank line 26 | 'blank_line_before_statement' => [ 27 | 'statements' => [ 28 | 'case', 29 | 'continue', 30 | 'declare', 31 | 'default', 32 | 'return', 33 | 'throw', 34 | 'try', 35 | 'yield', 36 | 'yield_from', 37 | ], 38 | ], 39 | 40 | // Enforce space around concatenation operator 41 | 'concat_space' => [ 42 | 'spacing' => 'one', 43 | ], 44 | 45 | // Use {} for empty loop bodies 46 | 'empty_loop_body' => [ 47 | 'style' => 'braces', 48 | ], 49 | 50 | // Don't change any increment/decrement styles 51 | 'increment_style' => false, 52 | 53 | // Forbid multi-line whitespace before the closing semicolon 54 | 'multiline_whitespace_before_semicolons' => [ 55 | 'strategy' => 'no_multi_line', 56 | ], 57 | 58 | // Clean up PHPDocs, but leave @inheritDoc entries alone 59 | 'no_superfluous_phpdoc_tags' => [ 60 | 'allow_mixed' => true, 61 | 'remove_inheritdoc' => false, 62 | ], 63 | 64 | // Ensure that traits, constants, properties, and the constructor 65 | // are listed first in classes, and magic properties are at the end 66 | 'ordered_class_elements' => [ 67 | 'order' => [ 68 | 'use_trait', 69 | 'constant_public', 70 | 'constant_protected', 71 | 'constant_private', 72 | 'property_public', 73 | 'property_protected', 74 | 'property_private', 75 | 'construct', 76 | ], 77 | ], 78 | 79 | // Ensure that param and return types are sorted consistently, with null at end 80 | 'phpdoc_types_order' => [ 81 | 'sort_algorithm' => 'alpha', 82 | 'null_adjustment' => 'always_last', 83 | ], 84 | 85 | // Yoda style is too weird 86 | 'yoda_style' => false, 87 | ]) 88 | ->setParallelConfig(ParallelConfigFactory::detect()) 89 | ->setIndent(' ') 90 | ->setLineEnding("\n") 91 | ->setFinder($finder); 92 | -------------------------------------------------------------------------------- /src/Console/CheckCommand.php: -------------------------------------------------------------------------------- 1 | setHelp('Checks all the composer packages in a project for PHP compatibility.'); 24 | 25 | $this->addOption( 26 | 'targetVersion', 27 | 't', 28 | InputOption::VALUE_REQUIRED, 29 | 'Target version of PHP against which to compare packages', 30 | phpversion() 31 | ); 32 | 33 | $this->addOption( 34 | 'direct', 35 | 'D', 36 | InputOption::VALUE_NONE, 37 | 'Shows only packages that are directly required by the root package' 38 | ); 39 | } 40 | 41 | protected function execute(InputInterface $input, OutputInterface $output): int 42 | { 43 | $io = new SymfonyStyle($input, $output); 44 | 45 | $direct = (bool) $input->getOption('direct'); 46 | 47 | try { 48 | $packages = $this->getLoadedPackages($direct); 49 | } catch (\Exception $e) { 50 | $io->error($e->getMessage()); 51 | 52 | return Command::FAILURE; 53 | } 54 | 55 | $targetPHPVersion = $input->getOption('targetVersion'); 56 | 57 | $results = [ 58 | Package::VALIDITY_OK => [], 59 | Package::VALIDITY_FAIL => [], 60 | Package::VALIDITY_UNKNOWN => [], 61 | ]; 62 | 63 | $table = $io->createTable(); 64 | $table->setHeaders(['Package', 'Version', 'PHP']); 65 | 66 | foreach ($packages as $package) { 67 | $isValid = $package->isValidForTarget($targetPHPVersion); 68 | $results[$isValid][] = $package; 69 | 70 | $table->addRow([ 71 | $this->getStatusIndicator($isValid) . ' ' . $package->name, 72 | $package->version, 73 | $package->phpRequirement, 74 | ]); 75 | } 76 | 77 | if ($io->isVerbose()) { 78 | $table->render(); 79 | } 80 | 81 | if (!$io->isQuiet()) { 82 | $io->section('SUMMARY'); 83 | $summary = array_map( 84 | fn ($v) => count($v), 85 | $results, 86 | ); 87 | 88 | $io->createTable() 89 | ->setVertical() 90 | ->setHeaders(array_keys($summary)) 91 | ->addRow($summary) 92 | ->render(); 93 | $io->newLine(); 94 | } 95 | 96 | if ($io->isVeryVerbose()) { 97 | if (count($results[Package::VALIDITY_FAIL]) > 0) { 98 | $io->section('FAILURES'); 99 | $io->writeln('This packages do not meet the target PHP requirement:'); 100 | $this->showDetailsTableArray($io, $results[Package::VALIDITY_FAIL]); 101 | } 102 | 103 | if (count($results[Package::VALIDITY_UNKNOWN]) > 0) { 104 | $io->section('UNKNOWN'); 105 | $io->writeln('These packages do not have a PHP requirement, so may or may not be valid:'); 106 | $this->showDetailsTableArray($io, $results[Package::VALIDITY_UNKNOWN]); 107 | } 108 | } 109 | 110 | return count($results[Package::VALIDITY_OK]) === count($packages) 111 | ? Command::SUCCESS 112 | : Command::FAILURE; 113 | } 114 | 115 | /** 116 | * @return array 117 | * 118 | * @throws \Exception 119 | */ 120 | private function getLoadedPackages(bool $directOnly): array 121 | { 122 | $data = file_get_contents( 123 | $this->getProjectRoot() . '/composer.lock' 124 | ); 125 | 126 | if ($data === false) { 127 | throw new \Exception('Could not read composer.lock.'); 128 | } 129 | 130 | $json = json_decode($data); 131 | 132 | $packages = array_merge( 133 | $json->packages, 134 | $json->{'packages-dev'} 135 | ); 136 | 137 | if ($directOnly) { 138 | $data = file_get_contents( 139 | $this->getProjectRoot() . '/composer.json' 140 | ); 141 | if ($data === false) { 142 | throw new \Exception('Could not read composer.json to find direct packages.'); 143 | } 144 | 145 | $json = json_decode($data); 146 | $directPackages = array_keys(array_merge( 147 | (array) $json->require, 148 | (array) $json->{'require-dev'} 149 | )); 150 | 151 | $packages = array_filter( 152 | $packages, 153 | fn ($p) => in_array($p->name, $directPackages) 154 | ); 155 | } 156 | 157 | $result = array_map( 158 | fn ($p) => Package::fromRawData($p), 159 | $packages 160 | ); 161 | 162 | usort($result, fn ($a, $b) => strcmp($a->name, $b->name)); 163 | 164 | return $result; 165 | } 166 | 167 | private function getProjectRoot(): string 168 | { 169 | return realpath( 170 | dirname(ComposerFactory::getComposerFile()) 171 | ) ?: ''; 172 | } 173 | 174 | /** 175 | * @phpstan-param Package::VALIDITY_* $isValid 176 | */ 177 | private function getStatusIndicator(string $isValid): string 178 | { 179 | return match ($isValid) { 180 | Package::VALIDITY_OK => '✔', 181 | Package::VALIDITY_FAIL => '✘', 182 | Package::VALIDITY_UNKNOWN => '?', 183 | }; 184 | } 185 | 186 | /** 187 | * @param array $packages 188 | */ 189 | private function showDetailsTableArray(SymfonyStyle $io, array $packages): void 190 | { 191 | $io->newLine(); 192 | 193 | $table = $io->createTable() 194 | ->setVertical() 195 | ->setHeaders(['Package', 'Version', 'PHP', 'Links']); 196 | 197 | foreach ($packages as $package) { 198 | $packageLinks = $package->getLinks(); 199 | $links = array_map( 200 | fn ($k, $v) => "{$k}: {$v}", 201 | array_keys($packageLinks), 202 | array_values($packageLinks), 203 | ); 204 | $table->addRow([ 205 | $package->name, 206 | $package->version, 207 | $package->phpRequirement ?? 'none', 208 | join("\n", $links), 209 | ]); 210 | } 211 | 212 | $table->render(); 213 | 214 | $io->newLine(); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # package-checker 2 | 3 | Tool to check all the composer packages in a project for PHP compatibility. 4 | 5 | The tool works by reading the `composer.lock` file in the root of your project, iterating over each of the installed packages, and checking to see if any of them have a `"require": { "php": "xxx" }` version that isn't satisfied by the target PHP version. 6 | 7 | ## Installation 8 | 9 | ```shell 10 | composer require --dev cviebrock/package-checker 11 | ``` 12 | 13 | ## Run 14 | 15 | ```shell 16 | ./vendor/bin/check-packages 17 | ``` 18 | 19 | The tool will check all installed packages against the version of PHP currently running. To test against a specific version of PHP, use the `--targetVersion` or `-t` option: 20 | 21 | ```shell 22 | ./vendor/bin/check-packages --targetVersion=8.2 23 | ./vendor/bin/check-packages -t 8.2 24 | ``` 25 | 26 | To check only those packages required by the root project, use the `-D` or `--direct` flag. 27 | 28 | You can also change the amount of output with the `-v` and `-q` flags: 29 | 30 | | Flag | Output | 31 | |:--------:|:--------------------------------------------------------------------| 32 | | `-q` | No output; only return exit code. | 33 | | no flags | Outputs a summary of the ok, failed, and unknown packages. | 34 | | `-v` | Also includes a detailed listing of each package checked. | 35 | | `-vv` | Also includes a detailed report on the failed and unknown packages. | 36 | 37 | The command will return an exit code of `0` if all packages meet the target requirement. Otherwise, it will exit with `1` if there are any packages that don't meet the target, or whose compatibility is unknown. 38 | 39 | ### Sample Output 40 | 41 | ``` 42 | > ./vendor/bin/check-packages -t 8.0 -vv 43 | 44 | ❯ ./vendor/bin/check-packages -t 8.2 -vv 45 | ---------------------------------------------- ---------- ------------------------ 46 | Package Version PHP 47 | ---------------------------------------------- ---------- ------------------------ 48 | ✔ aws/aws-crt-php v1.2.6 >=5.5 49 | ✔ aws/aws-sdk-php 3.321.8 >=7.2.5 50 | ✔ clue/ndjson-react v1.3.0 >=5.3 51 | ✔ clue/stream-filter v1.7.0 >=5.3 52 | ✔ codescale/ffmpeg-php 3.2.2 >=7 53 | ✔ composer/ca-bundle 1.5.1 ^7.2 || ^8.0 54 | ✔ composer/class-map-generator 1.3.4 ^7.2 || ^8.0 55 | ✔ composer/composer 2.7.9 ^7.2.5 || ^8.0 56 | ✔ composer/metadata-minifier 1.0.0 ^5.3.2 || ^7.0 || ^8.0 57 | ✔ composer/pcre 3.3.1 ^7.4 || ^8.0 58 | ✔ composer/semver 3.4.2 ^5.3.2 || ^7.0 || ^8.0 59 | ✔ composer/spdx-licenses 1.5.8 ^5.3.2 || ^7.0 || ^8.0 60 | ✔ composer/xdebug-handler 3.0.5 ^7.2.5 || ^8.0 61 | ✔ doctrine/lexer 3.0.1 ^8.1 62 | ✔ dompdf/dompdf v2.0.8 ^7.1 || ^8.0 63 | ✔ egulias/email-validator 4.0.2 >=8.1 64 | ✔ evenement/evenement v3.0.2 >=7.0 65 | ✔ fidry/cpu-core-counter 1.2.0 ^7.2 || ^8.0 66 | ✔ friendsofphp/php-cs-fixer v3.64.0 ^7.4 || ^8.0 67 | ✔ guzzlehttp/guzzle 7.9.2 ^7.2.5 || ^8.0 68 | ✔ guzzlehttp/promises 2.0.3 ^7.2.5 || ^8.0 69 | ✔ guzzlehttp/psr7 2.7.0 ^7.2.5 || ^8.0 70 | ✔ http-interop/http-factory-guzzle 1.2.0 >=7.3 71 | ✔ jean85/pretty-package-versions 2.0.6 ^7.1|^8.0 72 | ✔ justinrainbow/json-schema 5.3.0 >=7.1 73 | ✔ league/climate 3.8.2 ^7.3 || ^8.0 74 | ✔ masterminds/html5 2.9.0 >=5.3.0 75 | ✔ mtdowling/jmespath.php 2.8.0 ^7.2.5 || ^8.0 76 | ✔ pear/console_commandline v1.2.6 >=5.3.0 77 | ? pear/console_getopt v1.4.3 78 | ✔ pear/pear-core-minimal v1.10.15 >=5.4 79 | ✔ pear/pear_exception v1.0.2 >=5.2.0 80 | ✔ pear/text_password 1.2.2 >=5.2.1 81 | ? phenx/php-font-lib 0.5.6 82 | ✔ phenx/php-svg-lib 0.5.4 ^7.1 || ^8.0 83 | ✔ php-http/client-common 2.7.1 ^7.1 || ^8.0 84 | ✔ php-http/discovery 1.19.4 ^7.1 || ^8.0 85 | ✔ php-http/httplug 2.4.0 ^7.1 || ^8.0 86 | ✔ php-http/message 1.16.1 ^7.2 || ^8.0 87 | ✔ php-http/message-factory 1.1.0 >=5.4 88 | ✔ php-http/promise 1.3.1 ^7.1 || ^8.0 89 | ✔ phpstan/phpstan 1.12.3 ^7.2|^8.0 90 | ✔ psr/container 2.0.2 >=7.4.0 91 | ✔ psr/event-dispatcher 1.0.0 >=7.2.0 92 | ✔ psr/http-client 1.0.3 ^7.0 || ^8.0 93 | ✔ psr/http-factory 1.1.0 >=7.1 94 | ✔ psr/http-message 2.0 ^7.2 || ^8.0 95 | ✔ psr/log 3.0.1 >=8.0.0 96 | ✔ ralouphie/getallheaders 3.0.3 >=5.6 97 | ✔ react/cache v1.2.0 >=5.3.0 98 | ✔ react/child-process v0.6.5 >=5.3.0 99 | ✔ react/dns v1.13.0 >=5.3.0 100 | ✔ react/event-loop v1.5.0 >=5.3.0 101 | ✔ react/promise v3.2.0 >=7.1.0 102 | ✔ react/socket v1.16.0 >=5.3.0 103 | ✔ react/stream v1.4.0 >=5.3.8 104 | ✔ sabberworm/php-css-parser v8.6.0 >=5.6.20 105 | ✔ sebastian/diff 5.1.1 >=8.1 106 | ✔ seld/cli-prompt 1.0.4 >=5.3 107 | ✔ seld/jsonlint 1.11.0 ^5.3 || ^7.0 || ^8.0 108 | ✔ seld/phar-utils 1.2.1 >=5.3 109 | ✔ seld/signal-handler 2.0.2 >=7.2.0 110 | ? sentry/sdk 3.6.0 111 | ✔ sentry/sentry 3.22.1 ^7.2|^8.0 112 | ✔ silverorange/admin 6.1.5 >=5.3.0 113 | ✔ silverorange/ambiguous-class-name-detector 1.0.1 >=7.1.0 114 | ✔ silverorange/concentrate 2.0.2 >=7.1.0 115 | ✔ silverorange/mdb2 3.1.1 >=5.3.0 116 | ✔ silverorange/mdb2_driver_pgsql 2.2.0 >=5.3.0 117 | ✔ silverorange/package-checker dev-main ^8.0 118 | ✔ silverorange/site 14.4.0 >=5.5.0 119 | ✔ silverorange/swat 6.1.5 >=5.6.0 120 | ✔ silverorange/xml_rpc_ajax 3.1.1 >=5.2.1 121 | ✔ silverorange/yui 1.0.12 >=5.2.1 122 | ✔ squizlabs/php_codesniffer 3.10.2 >=5.4.0 123 | ✔ symfony/console v6.4.11 >=8.1 124 | ✔ symfony/deprecation-contracts v3.5.0 >=8.1 125 | ✔ symfony/event-dispatcher v6.4.8 >=8.1 126 | ✔ symfony/event-dispatcher-contracts v3.5.0 >=8.1 127 | ✔ symfony/filesystem v6.4.9 >=8.1 128 | ✔ symfony/finder v6.4.11 >=8.1 129 | ✔ symfony/http-client v6.4.11 >=8.1 130 | ✔ symfony/http-client-contracts v3.5.0 >=8.1 131 | ✔ symfony/mailer v5.4.41 >=7.2.5 132 | ✔ symfony/mime v6.4.11 >=8.1 133 | ✔ symfony/options-resolver v6.4.8 >=8.1 134 | ✔ symfony/polyfill-ctype v1.31.0 >=7.2 135 | ✔ symfony/polyfill-intl-grapheme v1.31.0 >=7.2 136 | ✔ symfony/polyfill-intl-idn v1.31.0 >=7.2 137 | ✔ symfony/polyfill-intl-normalizer v1.31.0 >=7.2 138 | ✔ symfony/polyfill-mbstring v1.31.0 >=7.2 139 | ✔ symfony/polyfill-php73 v1.31.0 >=7.2 140 | ✔ symfony/polyfill-php80 v1.31.0 >=7.2 141 | ✔ symfony/polyfill-php81 v1.31.0 >=7.2 142 | ✔ symfony/process v6.4.8 >=8.1 143 | ✔ symfony/service-contracts v3.5.0 >=8.1 144 | ✔ symfony/stopwatch v6.4.8 >=8.1 145 | ✔ symfony/string v6.4.11 >=8.1 146 | ✔ symfony/yaml v5.4.43 >=7.2.5 147 | ---------------------------------------------- ---------- ------------------------ 148 | 149 | SUMMARY 150 | ------- 151 | 152 | ------------- 153 | OK: 96 154 | FAIL: 0 155 | UNKNOWN: 3 156 | ------------- 157 | 158 | UNKNOWN 159 | ------- 160 | 161 | These packages do not have a PHP requirement, so may or may not be valid: 162 | 163 | ------------------------------------------------------------------------------- 164 | Package: pear/console_getopt 165 | Version: v1.4.3 166 | PHP: none 167 | Links: Homepage: none 168 | Source: https://github.com/pear/Console_Getopt 169 | Packagist: https://packagist.org/packages/pear/console_getopt#v1.4.3 170 | ------------------------------------------------------------------------------- 171 | Package: phenx/php-font-lib 172 | Version: 0.5.6 173 | PHP: none 174 | Links: Homepage: https://github.com/PhenX/php-font-lib 175 | Source: https://github.com/dompdf/php-font-lib/tree/0.5.6 176 | Packagist: https://packagist.org/packages/phenx/php-font-lib#v0.5.6 177 | ------------------------------------------------------------------------------- 178 | Package: sentry/sdk 179 | Version: 3.6.0 180 | PHP: none 181 | Links: Homepage: http://sentry.io 182 | Source: https://github.com/getsentry/sentry-php-sdk/tree/3.6.0 183 | Packagist: https://packagist.org/packages/sentry/sdk#v3.6.0 184 | ------------------------------------------------------------------------------- 185 | ``` 186 | --------------------------------------------------------------------------------