diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 6fe797e3a3152..950c2b8c596a7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.1 --- + * Add `CheckAliasValidityPass` to `lint:container` command * Add `private_ranges` as a shortcut for private IP address ranges to the `trusted_proxies` option * Mark classes `ConfigBuilderCacheWarmer`, `Router`, `SerializerCacheWarmer`, `TranslationsCacheWarmer`, `Translator` and `ValidatorCacheWarmer` as `final` * Move the Router `cache_dir` to `kernel.build_dir` diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php index b63ebe431787e..cd6e0657ccac9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php @@ -19,6 +19,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Compiler\CheckAliasValidityPass; use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Compiler\ResolveFactoryClassPass; @@ -107,6 +108,7 @@ private function getContainerBuilder(): ContainerBuilder $container->setParameter('container.build_hash', 'lint_container'); $container->setParameter('container.build_id', 'lint_container'); + $container->addCompilerPass(new CheckAliasValidityPass(), PassConfig::TYPE_BEFORE_REMOVING, -100); $container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100); return $this->container = $container; diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 96ca110562db9..9e14c79fe0aec 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.1 --- + * Add `CheckAliasValidityPass` to check service compatibility with aliased interface * Add argument `$prepend` to `ContainerConfigurator::extension()` to prepend the configuration instead of appending it * Have `ServiceLocator` implement `ServiceCollectionInterface` * Add `#[Lazy]` attribute as shortcut for `#[Autowire(lazy: [bool|string])]` and `#[Autoconfigure(lazy: [bool|string])]` diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckAliasValidityPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckAliasValidityPass.php new file mode 100644 index 0000000000000..44a1bdc320e97 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckAliasValidityPass.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; + +/** + * This pass validates aliases, it provides the following checks: + * + * - An alias which happens to be an interface must resolve to a service implementing this interface. This ensures injecting the aliased interface won't cause a type error at runtime. + */ +class CheckAliasValidityPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + foreach ($container->getAliases() as $id => $alias) { + try { + if (!$container->hasDefinition((string) $alias)) { + continue; + } + + $target = $container->getDefinition((string) $alias); + if (null === $target->getClass() || null !== $target->getFactory()) { + continue; + } + + $reflection = $container->getReflectionClass($id); + if (null === $reflection || !$reflection->isInterface()) { + continue; + } + + $targetReflection = $container->getReflectionClass($target->getClass()); + if (null !== $targetReflection && !$targetReflection->implementsInterface($id)) { + throw new RuntimeException(sprintf('Invalid alias definition: alias "%s" is referencing class "%s" but this class does not implement "%s". Because this alias is an interface, "%s" must implement "%s".', $id, $target->getClass(), $id, $target->getClass(), $id)); + } + } catch (\ReflectionException) { + continue; + } + } + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckAliasValidityPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckAliasValidityPassTest.php new file mode 100644 index 0000000000000..3c3b126c91bf1 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckAliasValidityPassTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Compiler\CheckAliasValidityPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckAliasValidityPass\FooImplementing; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckAliasValidityPass\FooInterface; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckAliasValidityPass\FooNotImplementing; + +class CheckAliasValidityPassTest extends TestCase +{ + public function testProcessDetectsClassNotImplementingAliasedInterface() + { + $this->expectException(RuntimeException::class); + $container = new ContainerBuilder(); + $container->register('a')->setClass(FooNotImplementing::class); + $container->setAlias(FooInterface::class, 'a'); + + $this->process($container); + } + + public function testProcessAcceptsClassImplementingAliasedInterface() + { + $container = new ContainerBuilder(); + $container->register('a')->setClass(FooImplementing::class); + $container->setAlias(FooInterface::class, 'a'); + + $this->process($container); + $this->addToAssertionCount(1); + } + + public function testProcessIgnoresArbitraryAlias() + { + $container = new ContainerBuilder(); + $container->register('a')->setClass(FooImplementing::class); + $container->setAlias('not_an_interface', 'a'); + + $this->process($container); + $this->addToAssertionCount(1); + } + + public function testProcessIgnoresTargetWithFactory() + { + $container = new ContainerBuilder(); + $container->register('a')->setFactory(new Reference('foo')); + $container->setAlias(FooInterface::class, 'a'); + + $this->process($container); + $this->addToAssertionCount(1); + } + + public function testProcessIgnoresTargetWithoutClass() + { + $container = new ContainerBuilder(); + $container->register('a'); + $container->setAlias(FooInterface::class, 'a'); + + $this->process($container); + $this->addToAssertionCount(1); + } + + protected function process(ContainerBuilder $container): void + { + $pass = new CheckAliasValidityPass(); + $pass->process($container); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckAliasValidityPass/FooImplementing.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckAliasValidityPass/FooImplementing.php new file mode 100644 index 0000000000000..cb3d67dde9b59 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckAliasValidityPass/FooImplementing.php @@ -0,0 +1,8 @@ +