├── LICENSE ├── README.md ├── composer.json └── src ├── GeneratedConfig.php └── Plugin.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ondřej Mirtes 4 | Copyright (c) 2025 PHPStan s.r.o. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPStan Extension Installer 2 | 3 | [![Build](https://github.com/phpstan/extension-installer/workflows/Build/badge.svg)](https://github.com/phpstan/extension-installer/actions) 4 | [![Latest Stable Version](https://poser.pugx.org/phpstan/extension-installer/v/stable)](https://packagist.org/packages/phpstan/extension-installer) 5 | [![License](https://poser.pugx.org/phpstan/extension-installer/license)](https://packagist.org/packages/phpstan/extension-installer) 6 | 7 | Composer plugin for automatic installation of [PHPStan](https://phpstan.org/) extensions. 8 | 9 | # Motivation 10 | 11 | ```diff 12 | diff --git a/phpstan.neon b/phpstan.neon 13 | index db4e3df32e..2ca30fa20a 100644 14 | --- a/phpstan.neon 15 | +++ b/phpstan.neon 16 | @@ -1,12 +1,3 @@ 17 | -includes: 18 | - - vendor/phpstan/phpstan-doctrine/extension.neon 19 | - - vendor/phpstan/phpstan-doctrine/rules.neon 20 | - - vendor/phpstan/phpstan-nette/extension.neon 21 | - - vendor/phpstan/phpstan-nette/rules.neon 22 | - - vendor/phpstan/phpstan-phpunit/extension.neon 23 | - - vendor/phpstan/phpstan-phpunit/rules.neon 24 | - - vendor/phpstan/phpstan-strict-rules/rules.neon 25 | - 26 | parameters: 27 | autoload_directories: 28 | - %rootDir%/../../../build/SlevomatSniffs 29 | diff --git a/composer.json b/composer.json 30 | index 1b578dd624..f6ebf6e477 100644 31 | --- a/composer.json 32 | +++ b/composer.json 33 | @@ -142,6 +142,7 @@ 34 | "jakub-onderka/php-parallel-lint": "1.0.0", 35 | "justinrainbow/json-schema": "5.2.8", 36 | "ondrejmirtes/mocktainer": "0.8", 37 | + "phpstan/extension-installer": "^1.0", 38 | "phpstan/phpstan": "^0.11.7", 39 | "phpstan/phpstan-doctrine": "^0.11.3", 40 | "phpstan/phpstan-nette": "^0.11.1", 41 | ``` 42 | 43 | ## Usage 44 | 45 | ```bash 46 | composer require --dev phpstan/extension-installer 47 | ``` 48 | 49 | Starting from Composer 2.2.0 you'll get the following question: 50 | ``` 51 | phpstan/extension-installer contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins 52 | Do you trust "phpstan/extension-installer" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?] 53 | ``` 54 | 55 | Answer with `y` to allow the plugin. 56 | 57 | ## Instructions for extension developers 58 | 59 | It's best (but optional) to set the extension's composer package [type](https://getcomposer.org/doc/04-schema.md#type) to `phpstan-extension` for this plugin to be able to recognize it and to be [discoverable on Packagist](https://packagist.org/explore/?type=phpstan-extension). 60 | 61 | Add `phpstan` key in the extension `composer.json`'s `extra` section: 62 | 63 | ```json 64 | { 65 | "extra": { 66 | "phpstan": { 67 | "includes": [ 68 | "extension.neon" 69 | ] 70 | } 71 | } 72 | } 73 | ``` 74 | 75 | ## Ignoring a particular extension 76 | 77 | You may want to disable auto-installation of a particular extension to handle installation manually. Ignore an extension by adding an `extra.phpstan/extension-installer.ignore` array in `composer.json` that specifies a list of packages to ignore: 78 | 79 | ```json 80 | { 81 | "extra": { 82 | "phpstan/extension-installer": { 83 | "ignore": [ 84 | "phpstan/phpstan-phpunit" 85 | ] 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | ## Limitations 92 | 93 | The extension installer depends on Composer script events, therefore you cannot use `--no-scripts` flag. 94 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpstan/extension-installer", 3 | "type": "composer-plugin", 4 | "description": "Composer plugin for automatic installation of PHPStan extensions", 5 | "license": [ 6 | "MIT" 7 | ], 8 | "keywords": ["dev", "static analysis"], 9 | "require": { 10 | "php": "^7.2 || ^8.0", 11 | "composer-plugin-api": "^2.0", 12 | "phpstan/phpstan": "^1.12.0 || ^2.0" 13 | }, 14 | "require-dev": { 15 | "composer/composer": "^2.0", 16 | "php-parallel-lint/php-parallel-lint": "^1.2.0", 17 | "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" 18 | }, 19 | "config": { 20 | "sort-packages": true, 21 | "allow-plugins": { 22 | "ocramius/package-versions": true 23 | } 24 | }, 25 | "extra": { 26 | "class": "PHPStan\\ExtensionInstaller\\Plugin" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "PHPStan\\ExtensionInstaller\\": "src/" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/GeneratedConfig.php: -------------------------------------------------------------------------------- 1 | 82 | */ 83 | public static function getSubscribedEvents(): array 84 | { 85 | return [ 86 | ScriptEvents::POST_INSTALL_CMD => 'process', 87 | ScriptEvents::POST_UPDATE_CMD => 'process', 88 | ]; 89 | } 90 | 91 | public function process(Event $event): void 92 | { 93 | $io = $event->getIO(); 94 | 95 | if (!file_exists(__DIR__)) { 96 | $io->write('phpstan/extension-installer: Package not found (probably scheduled for removal); extensions installation skipped.'); 97 | return; 98 | } 99 | 100 | $composer = $event->getComposer(); 101 | $installationManager = $composer->getInstallationManager(); 102 | 103 | $generatedConfigFilePath = __DIR__ . '/GeneratedConfig.php'; 104 | $oldGeneratedConfigFileHash = null; 105 | if (is_file($generatedConfigFilePath)) { 106 | $oldGeneratedConfigFileHash = md5_file($generatedConfigFilePath); 107 | } 108 | $notInstalledPackages = []; 109 | $installedPackages = []; 110 | $ignoredPackages = []; 111 | 112 | $data = []; 113 | $fs = new Filesystem(); 114 | $ignore = []; 115 | 116 | $packageExtra = $composer->getPackage()->getExtra(); 117 | 118 | if (isset($packageExtra['phpstan/extension-installer']['ignore'])) { 119 | $ignore = $packageExtra['phpstan/extension-installer']['ignore']; 120 | } 121 | 122 | $phpstanVersionConstraints = []; 123 | 124 | foreach ($composer->getRepositoryManager()->getLocalRepository()->getPackages() as $package) { 125 | if ( 126 | $package->getType() !== 'phpstan-extension' 127 | && !isset($package->getExtra()['phpstan']) 128 | ) { 129 | if ( 130 | strpos($package->getName(), 'phpstan') !== false 131 | && !in_array($package->getName(), [ 132 | 'phpstan/phpstan', 133 | 'phpstan/phpstan-shim', 134 | 'phpstan/phpdoc-parser', 135 | 'phpstan/extension-installer', 136 | ], true) 137 | ) { 138 | $notInstalledPackages[$package->getName()] = $package->getFullPrettyVersion(); 139 | } 140 | continue; 141 | } 142 | 143 | if (in_array($package->getName(), $ignore, true)) { 144 | $ignoredPackages[] = $package->getName(); 145 | continue; 146 | } 147 | 148 | $installPath = $installationManager->getInstallPath($package); 149 | if ($installPath === null) { 150 | continue; 151 | } 152 | 153 | $absoluteInstallPath = $fs->isAbsolutePath($installPath) 154 | ? $installPath 155 | : getcwd() . DIRECTORY_SEPARATOR . $installPath; 156 | 157 | $packageRequires = $package->getRequires(); 158 | $phpstanConstraint = null; 159 | if (array_key_exists('phpstan/phpstan', $packageRequires)) { 160 | $phpstanConstraint = $packageRequires['phpstan/phpstan']->getConstraint(); 161 | if ($phpstanConstraint->getLowerBound()->isZero()) { 162 | continue; 163 | } 164 | if ($phpstanConstraint->getUpperBound()->isPositiveInfinity()) { 165 | continue; 166 | } 167 | $phpstanVersionConstraints[] = $phpstanConstraint; 168 | } 169 | 170 | $data[$package->getName()] = [ 171 | 'install_path' => $absoluteInstallPath, 172 | 'relative_install_path' => $fs->findShortestPath(dirname($generatedConfigFilePath), $absoluteInstallPath, true), 173 | 'extra' => $package->getExtra()['phpstan'] ?? null, 174 | 'version' => $package->getFullPrettyVersion(), 175 | 'phpstanVersionConstraint' => $phpstanConstraint !== null ? $this->constraintIntoString($phpstanConstraint) : null, 176 | ]; 177 | 178 | $installedPackages[$package->getName()] = true; 179 | } 180 | 181 | $phpstanVersionConstraint = null; 182 | if (count($phpstanVersionConstraints) > 0 && class_exists(Intervals::class)) { 183 | if (count($phpstanVersionConstraints) === 1) { 184 | $multiConstraint = $phpstanVersionConstraints[0]; 185 | } else { 186 | $multiConstraint = new MultiConstraint($phpstanVersionConstraints); 187 | } 188 | $phpstanVersionConstraint = $this->constraintIntoString(Intervals::compactConstraint($multiConstraint)); 189 | } 190 | 191 | ksort($data); 192 | ksort($installedPackages); 193 | ksort($notInstalledPackages); 194 | sort($ignoredPackages); 195 | 196 | $generatedConfigFileContents = sprintf(self::$generatedFileTemplate, var_export($data, true), var_export($notInstalledPackages, true), var_export($phpstanVersionConstraint, true)); 197 | file_put_contents($generatedConfigFilePath, $generatedConfigFileContents); 198 | $io->write('phpstan/extension-installer: Extensions installed'); 199 | 200 | if ($oldGeneratedConfigFileHash === md5($generatedConfigFileContents)) { 201 | return; 202 | } 203 | 204 | foreach (array_keys($installedPackages) as $name) { 205 | $io->write(sprintf('> %s: installed', $name)); 206 | } 207 | 208 | foreach (array_keys($notInstalledPackages) as $name) { 209 | $io->write(sprintf('> %s: not supported', $name)); 210 | } 211 | 212 | foreach ($ignoredPackages as $name) { 213 | $io->write(sprintf('> %s: ignored', $name)); 214 | } 215 | } 216 | 217 | private function constraintIntoString(ConstraintInterface $constraint): string 218 | { 219 | return sprintf( 220 | '%s%s, %s%s', 221 | $constraint->getLowerBound()->isInclusive() ? '>=' : '>', 222 | $constraint->getLowerBound()->getVersion(), 223 | $constraint->getUpperBound()->isInclusive() ? '<=' : '<', 224 | $constraint->getUpperBound()->getVersion() 225 | ); 226 | } 227 | 228 | } 229 | --------------------------------------------------------------------------------