10000 [DependencyInjection] Add `CheckAliasValidityPass` to check interface… · symfony/symfony@41b5f63 · GitHub
[go: up one dir, main page]

Skip to content

Commit 41b5f63

Browse files
committed
[DependencyInjection] Add CheckAliasValidityPass to check interface compatibility
Adding a pass to check that an aliased interface resolves to a service which implements the interface
1 parent d9eccd1 commit 41b5f63

File tree

6 files changed

+158
-0
lines changed

6 files changed

+158
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
17+
18+
/**
19+
* This pass validates aliases, it provides the following checks:
20+
*
21+
* - 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.
22+
*/
23+
class CheckAliasValidityPass implements CompilerPassInterface
24+
{
25+
public function process(ContainerBuilder $container): void
26+
{
27+
foreach ($container->getAliases() as $id => $alias) {
28+
try {
29+
if ($alias->isPrivate() || !$container->hasDefinition((string) $alias)) {
30+
continue;
31+
}
32+
33+
$reflection = $container->getReflectionClass($id);
34+
if ($reflection === null || !$reflection->isInterface()) {
35+
continue;
36+
}
37+
38+
$target = $container->getDefinition((string) $alias);
39+
if ($target->getClass() === null) {
40+
continue;
41+
}
42+
43+
$targetReflection = $container->getReflectionClass($target->getClass());
44+
if ($targetReflection !== null && !$targetReflection->implementsInterface($id)) {
45+
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));
46+
}
47+
} catch (\ReflectionException) {
48+
continue;
49+
}
50+
}
51+
}
52+
}

src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public function __construct()
7171
new ResolveTaggedIteratorArgumentPass(),
7272
new ResolveServiceSubscribersPass(),
7373
new ResolveReferencesToAliasesPass(),
74+
new CheckAliasValidityPass(),
7475
new ResolveInvalidReferencesPass(),
7576
new AnalyzeServiceReferencesPass(true),
7677
new CheckCircularReferencesPass(),
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Compiler\CheckDefinitionValidityPass;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\DependencyInjection\Exception\EnvParameterException;
19+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
20+
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckAliasValidityPass\FooImplementing;
21+
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckAliasValidityPass\FooInterface;
22+
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckAliasValidityPass\FooNotImplementing;
23+
24+
class CheckAliasValidityPassTest extends TestCase
25+
{
26+
public function testProcessDetectsClassNotImplementingAliasedInterface(): void
27+
{
28+
$this->expectException(RuntimeException::class);
29+
$container = new ContainerBuilder();
30+
$container->register('a')->setClass(FooNotImplementing::class);
31+
$container->setAlias(FooInterface::class, 'a')->setPublic(true);
32+
33+
$this->process($container);
34+
}
35+
36+
public function testProcessAcceptsClassImplementingAliasedInterface(): void
37+
{
38+
$container = new ContainerBuilder();
39+
$container->register('a')->setClass(FooImplementing::class);
40+
$container->setAlias(FooInterface::class, 'a')->setPublic(true);
41+
42+
$this->process($container);
43+
$this->addToAssertionCount(1);
44+
}
45+
46+
public function testProcessIgnoresArbitraryAlias(): void
47+
{
48+
$container = new ContainerBuilder();
49+
$container->register('a')->setClass(FooImplementing::class);
50+
$container->setAlias('not_an_interface', 'a')->setPublic(true);
51+
52+
$this->process($container);
53+
$this->addToAssertionCount(1);
54+
}
55+
56+
public function testProcessIgnoresPrivateAlias(): void
57+
{
58+
$container = new ContainerBuilder();
59+
$container->register('a')->setClass(FooImplementing::class);
60+
$container->setAlias(FooInterface::class, 'a')->setPublic(false);
61+
62+
$this->process($container);
63+
$this->addToAssertionCount(1);
64+
}
65+
66+
public function testProcessIgnoresTargetWithoutClass(): void
67+
{
68+
$container = new ContainerBuilder();
69+
$container->register('a');
70+
$container->setAlias(FooInterface::class, 'a')->setPublic(true);
71+
72+
$this->process($container);
73+
$this->addToAssertionCount(1);
74+
}
75+
76+
protected function process(ContainerBuilder $container): void
77+
{
78+
$pass = new CheckAliasValidityPass();
79+
$pass->process($container);
80+
}
81+
}
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+
}
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)
0