├── 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 | [](https://github.com/phpstan/extension-installer/actions)
4 | [](https://packagist.org/packages/phpstan/extension-installer)
5 | [](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 |
--------------------------------------------------------------------------------