Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion build/composer-dependency-analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
'symfony/process',
'symfony/service-contracts',
'symfony/string',
'nette/php-generator',
];

$unknownClasses = [
Expand Down
12 changes: 0 additions & 12 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 0 additions & 6 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
108 changes: 100 additions & 8 deletions src/DependencyInjection/AutowiredAttributeServicesExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{

Expand Down Expand Up @@ -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<string> $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];
}
}

}
1 change: 1 addition & 0 deletions src/DependencyInjection/AutowiredExtensionsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
*
* Container::getExtensionsCollection() looks up the same services by name at runtime.
*/
#[ContainerExtension(name: 'autowiredExtensions')]
final class AutowiredExtensionsExtension extends CompilerExtension
{

Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/ConditionalTagsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use function is_array;
use function sprintf;

#[ContainerExtension(name: 'conditionalTags')]
final class ConditionalTagsExtension extends CompilerExtension
{

Expand Down
23 changes: 23 additions & 0 deletions src/DependencyInjection/ContainerExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection;

use Attribute;

/**
* Registers a Nette\DI\CompilerExtension under the given name, the same way an entry in the
* `extensions:` section of a configuration file would. The name is also the key of the section
* holding the extension's own configuration.
*
* Works thanks to https://github.com/ondrejmirtes/composer-attribute-collector
* and ContainerExtensionsExtension.
*/
#[Attribute(flags: Attribute::TARGET_CLASS)]
final class ContainerExtension
{

public function __construct(public string $name)
{
}

}
63 changes: 63 additions & 0 deletions src/DependencyInjection/ContainerExtensionsExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection;

use Nette\DI\CompilerExtension;
use Nette\DI\Extensions\ExtensionsExtension;
use olvlvl\ComposerAttributeCollector\Attributes;
use Override;
use PHPStan\ShouldNotHappenException;
use function is_a;
use function sprintf;
use function strcmp;
use function usort;

/**
* Registers every compiler extension marked with #[ContainerExtension], so that PHPStan's own
* extensions do not have to be listed in the `extensions:` section of conf/config.neon.
*
* Extending ExtensionsExtension is required, not cosmetic. Compiler::processExtensions() starts with
*
* $first = $this->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();
}

}
4 changes: 2 additions & 2 deletions src/DependencyInjection/ContainerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/ExpandRelativePathExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Nette\Schema\Schema;
use Override;

#[ContainerExtension(name: 'expandRelativePaths')]
final class ExpandRelativePathExtension extends CompilerExtension
{

Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/FnsrExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use function getenv;
use const PHP_VERSION_ID;

#[ContainerExtension(name: 'fnsr')]
final class FnsrExtension extends CompilerExtension
{

Expand Down
2 changes: 1 addition & 1 deletion src/DependencyInjection/NeonAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '!';

Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/ParametersSchemaExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Nette\Schema\Schema;
use Override;

#[ContainerExtension(name: 'parametersSchema')]
final class ParametersSchemaExtension extends CompilerExtension
{

Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/RulesExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Override;
use PHPStan\Rules\LazyRegistry;

#[ContainerExtension(name: 'rules')]
final class RulesExtension extends CompilerExtension
{

Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/ValidateExcludePathsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use function is_file;
use function sprintf;

#[ContainerExtension(name: 'validateExcludePaths')]
final class ValidateExcludePathsExtension extends CompilerExtension
{

Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/ValidateIgnoredErrorsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
use function sprintf;
use const PHP_VERSION_ID;

#[ContainerExtension(name: 'validateIgnoredErrors')]
final class ValidateIgnoredErrorsExtension extends CompilerExtension
{

Expand Down
Loading
Loading