Skip to content

Commit a14d653

Browse files
n-valverdefabpot
authored andcommitted
[DependencyInjection] Add CheckAliasValidityPass to check interface compatibility
1 parent cb8e3f3 commit a14d653

File tree

6 files changed

+156
-0
lines changed

6 files changed

+156
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
7.1
55
---
66

7+
* Add `CheckAliasValidityPass` to check service compatibility with aliased interface
78
* Add argument `$prepend` to `ContainerConfigurator::extension()` to prepend the configuration instead of appending it
89
* Have `ServiceLocator` implement `ServiceCollectionInterface`
910
* Add `#[Lazy]` attribute as shortcut for `#[Autowire(lazy: [bool|string])]` and `#[Autoconfigure(lazy: [bool|string])]`

Compiler/CheckAliasValidityPass.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
16+
17+
/**
18+
* This pass validates aliases, it provides the following checks:
19+
*
20+
* - 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.
21+
*/
22+
class CheckAliasValidityPass implements CompilerPassInterface
23+
{
24+
public function process(ContainerBuilder $container): void
25+
{
26+
foreach ($container->getAliases() as $id => $alias) {
27+
try {
28+
if (!$container->hasDefinition((string) $alias)) {
29+
continue;
30+
}
31+
32+
$target = $container->getDefinition((string) $alias);
33+
if (null === $target->getClass() || null !== $target->getFactory()) {
34+
continue;
35+
}
36+
37+
$reflection = $container->getReflectionClass($id);
38+
if (null === $reflection || !$reflection->isInterface()) {
39+
continue;
40+
}
41+
42+
$targetReflection = $container->getReflectionClass($target->getClass());
43+
if (null !== $targetReflection && !$targetReflection->implementsInterface($id)) {
44+
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));
45+
}
46+
} catch (\ReflectionException) {
47+
continue;
48+
}
49+
}
50+
}
51+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\DependencyInjection\Compiler\CheckAliasValidityPass;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
18+
use Symfony\Component\DependencyInjection\Reference;
19+
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckAliasValidityPass\FooImplementing;
20+
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckAliasValidityPass\FooInterface;
21+
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckAliasValidityPass\FooNotImplementing;
22+
23+
class CheckAliasValidityPassTest extends TestCase
24+
{
25+
public function testProcessDetectsClassNotImplementingAliasedInterface()
26+
{
27+
$this->expectException(RuntimeException::class);
28+
$container = new ContainerBuilder();
29+
$container->register('a')->setClass(FooNotImplementing::class);
30+
$container->setAlias(FooInterface::class, 'a');
31+
32+
$this->process($container);
33+
}
34+
35+
public function testProcessAcceptsClassImplementingAliasedInterface()
36+
{
37+
$container = new ContainerBuilder();
38+
$container->register('a')->setClass(FooImplementing::class);
39+
$container->setAlias(FooInterface::class, 'a');
40+
41+
$this->process($container);
42+
$this->addToAssertionCount(1);
43+
}
44+
45+
public function testProcessIgnoresArbitraryAlias()
46+
{
47+
$container = new ContainerBuilder();
48+
$container->register('a')->setClass(FooImplementing::class);
49+
$container->setAlias('not_an_interface', 'a');
50+
51+
$this->process($container);
52+
$this->addToAssertionCount(1);
53+
}
54+
55+
public function testProcessIgnoresTargetWithFactory()
56+
{
57+
$container = new ContainerBuilder();
58+
$container->register('a')->setFactory(new Reference('foo'));
59+
$container->setAlias(FooInterface::class, 'a');
60+
61+
$this->process($container);
62+
$this->addToAssertionCount(1);
63+
}
64+
65+
public function testProcessIgnoresTargetWithoutClass()
66+
{
67+
$container = new ContainerBuilder();
68+
$container->register('a');
69+
$container->setAlias(FooInterface::class, 'a');
70+
71+
$this->process($container);
72+
$this->addToAssertionCount(1);
73+
}
74+
75+
protected function process(ContainerBuilder $container): void
76+
{
77+
$pass = new CheckAliasValidityPass();
78+
$pass->process($container);
79+
}
80+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\CheckAliasValidityPass;
4+
5+
class FooImplementing implements FooInterface
6+
{
7+
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\CheckAliasValidityPass;
4+
5+
interface FooInterface
6+
{
7+
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\CheckAliasValidityPass;
4+
5+
class FooNotImplementing
6+
{
7+
8+
}

0 commit comments

Comments
 (0)