Skip to content

Commit 831bdc3

Browse files
committed
bug symfony#25282 [DI] Register singly-implemented interfaces when doing PSR-4 discovery (nicolas-grekas)
This PR was merged into the 3.4 branch. Discussion ---------- [DI] Register singly-implemented interfaces when doing PSR-4 discovery | Q | A | ------------- | --- | Branch? | 3.4 | Bug fix? | yes | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - I'm feeling bad for not having this idea before 3.4.0 went out, therefore submitting on 3.4, despite this being a new feature, technically. On a DX pov still, this is a bugfix :) I'll let you accept the argument or not... So, when doing PSR-4-based service registration, we keep only classes as services. This systematically leads to the question: "But what about interfaces, shouldn't we type-hint against abstractions and not classes?!" And the answer has invariably been: "Well, just create an alias!" Which means doing configuration manually. I fear that if we leave things as is, we're going to grow a "generation" of devs that will hijack autowiring and abuse hinting for classes instead of interfaces. BUT, here is the idea implemented by this PR: let's create an alias for every singly-implemented interface we discover while looking for classes! Plain local, simple, and obvious, isn't it? Votes pending :) Commits ------- fcd4aa7 [DI] Register singly-implemented interfaces when doing PSR-4 discovery
2 parents 24be059 + fcd4aa7 commit 831bdc3

File tree

6 files changed

+85
-6
lines changed

6 files changed

+85
-6
lines changed

src/Symfony/Component/DependencyInjection/Loader/FileLoader.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,25 @@ public function registerClasses(Definition $prototype, $namespace, $resource, $e
5656

5757
$classes = $this->findClasses($namespace, $resource, $exclude);
5858
// prepare for deep cloning
59-
$prototype = serialize($prototype);
59+
$serializedPrototype = serialize($prototype);
60+
$interfaces = array();
61+
$singlyImplemented = array();
6062

6163
foreach ($classes as $class) {
62-
$this->setDefinition($class, unserialize($prototype));
64+
if (interface_exists($class, false)) {
65+
$interfaces[] = $class;
66+
} else {
67+
$this->setDefinition($class, unserialize($serializedPrototype));
68+
foreach (class_implements($class, false) as $interface) {
69+
$singlyImplemented[$interface] = isset($singlyImplemented[$interface]) ? false : $class;
70+
}
71+
}
72+
}
73+
foreach ($interfaces as $interface) {
74+
if (!empty($singlyImplemented[$interface])) {
75+
$this->container->setAlias($interface, $singlyImplemented[$interface])
76+
->setPublic(false);
77+
}
6378
}
6479
}
6580

@@ -129,7 +144,7 @@ private function findClasses($namespace, $pattern, $excludePattern)
129144
throw new InvalidArgumentException(sprintf('Expected to find class "%s" in file "%s" while importing services from resource "%s", but it was not found! Check the namespace prefix used with the resource.', $class, $path, $pattern));
130145
}
131146

132-
if ($r->isInstantiable()) {
147+
if ($r->isInstantiable() || $r->isInterface()) {
133148
$classes[] = $class;
134149
}
135150
}

src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Foo.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
44

5-
class Foo
5+
class Foo implements FooInterface, Sub\BarInterface
66
{
77
public function __construct($bar = null)
88
{
99
}
1010

11-
function setFoo(self $foo)
11+
public function setFoo(self $foo)
1212
{
1313
}
1414
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
4+
5+
interface FooInterface
6+
{
7+
}

src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/Sub/Bar.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub;
44

5-
class Bar
5+
class Bar implements BarInterface
66
{
77
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub;
4+
5+
interface BarInterface
6+
{
7+
}

src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Tests\Loader;
1313

14+
use Psr\Container\ContainerInterface as PsrContainerInterface;
1415
use PHPUnit\Framework\TestCase;
1516
use Symfony\Component\Config\FileLocator;
1617
use Symfony\Component\Config\Loader\LoaderResolver;
1718
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\DependencyInjection\ContainerInterface;
1820
use Symfony\Component\DependencyInjection\Definition;
1921
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
2022
use Symfony\Component\DependencyInjection\Loader\FileLoader;
@@ -25,7 +27,10 @@
2527
use Symfony\Component\DependencyInjection\Reference;
2628
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\AnotherSub\DeeperBaz;
2729
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\Baz;
30+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Foo;
31+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\FooInterface;
2832
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\Bar;
33+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\BarInterface;
2934

3035
class FileLoaderTest extends TestCase
3136
{
@@ -91,6 +96,14 @@ public function testRegisterClasses()
9196
array('service_container', Bar::class),
9297
array_keys($container->getDefinitions())
9398
);
99+
$this->assertEquals(
100+
array(
101+
PsrContainerInterface::class,
102+
ContainerInterface::class,
103+
BarInterface::class,
104+
),
105+
array_keys($container->getAliases())
106+
);
94107
}
95108

96109
public function testRegisterClassesWithExclude()
@@ -111,6 +124,43 @@ public function testRegisterClassesWithExclude()
111124
$this->assertTrue($container->has(Baz::class));
112125
$this->assertFalse($container->has(Foo::class));
113126
$this->assertFalse($container->has(DeeperBaz::class));
127+
128+
$this->assertEquals(
129+
array(
130+
PsrContainerInterface::class,
131+
ContainerInterface::class,
132+
BarInterface::class,
133+
),
134+
array_keys($container->getAliases())
135+
);
136+
}
137+
138+
public function testNestedRegisterClasses()
139+
{
140+
$container = new ContainerBuilder();
141+
$loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures'));
142+
143+
$prototype = new Definition();
144+
$prototype->setPublic(true)->setPrivate(true);
145+
$loader->registerClasses($prototype, 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', 'Prototype/*');
146+
147+
$this->assertTrue($container->has(Bar::class));
148+
$this->assertTrue($container->has(Baz::class));
149+
$this->assertTrue($container->has(Foo::class));
150+
151+
$this->assertEquals(
152+
array(
153+
PsrContainerInterface::class,
154+
ContainerInterface::class,
155+
FooInterface::class,
156+
),
157+
array_keys($container->getAliases())
158+
);
159+
160+
$alias = $container->getAlias(FooInterface::class);
161+
$this->assertSame(Foo::class, (string) $alias);
162+
$this->assertFalse($alias->isPublic());
163+
$this->assertFalse($alias->isPrivate());
114164
}
115165

116166
/**

0 commit comments

Comments
 (0)