Skip to content

Commit 88ccab4

Browse files
committed
[DependencyInjection] Automatic registration of class named services
1 parent 003507d commit 88ccab4

15 files changed

+472
-5
lines changed

src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\DependencyInjection\Container;
1717
use Symfony\Component\DependencyInjection\ContainerBuilder;
1818
use Symfony\Component\DependencyInjection\Definition;
19+
use Symfony\Component\DependencyInjection\Exception\AmbiguousServiceException;
1920
use Symfony\Component\DependencyInjection\Reference;
2021
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
2122

@@ -34,6 +35,10 @@ public static function assertSaneContainer(Container $container, $message = '')
3435
try {
3536
$container->get($id);
3637
} catch (\Exception $e) {
38+
if ($e instanceof AmbiguousServiceException) {
39+
continue;
40+
}
41+
3742
$errors[$id] = $e->getMessage();
3843
}
3944
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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;
13+
14+
class AmbiguousDefinition extends Definition
15+
{
16+
private $services;
17+
18+
public function __construct($class, array $services)
19+
{
20+
parent::__construct($class, [$class, $services]);
21+
$this->setFactory([AmbiguousService::class, 'throwException']);
22+
$this->services = $services;
23+
}
24+
25+
public function getServices()
26+
{
27+
return $this->services;
28+
}
29+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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;
13+
14+
use Symfony\Component\DependencyInjection\Exception\AmbiguousServiceException;
15+
16+
class AmbiguousService
17+
{
18+
static public function throwException($class, $services)
19+
{
20+
throw new AmbiguousServiceException($class, $services);
21+
}
22+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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\AmbiguousDefinition;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Exception\AmbiguousReferenceException;
17+
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
18+
use Symfony\Component\DependencyInjection\Reference;
19+
20+
/**
21+
* @author Martin Hasoň <martin.hason@gmail.com>
22+
*/
23+
class CheckAmbiguousReferencesPass implements CompilerPassInterface
24+
{
25+
/** @var ContainerBuilder */
26+
private $container;
27+
28+
public function process(ContainerBuilder $container)
29+
{
30+
$this->container = $container;
31+
foreach ($container->getDefinitions() as $id => $definition) {
32+
$this->processArguments($id, $definition->getArguments());
33+
$this->processArguments($id, $definition->getMethodCalls());
34+
$this->processArguments($id, $definition->getProperties());
35+
$this->processFactory($id, $definition->getFactory());
36+
}
37+
}
38+
39+
private function processArguments($id, array $arguments)
40+
{
41+
foreach ($arguments as $argument) {
42+
$definition = $argument;
43+
44+
if (is_array($argument)) {
45+
$this->processArguments($id, $argument);
46+
} elseif ($argument instanceof Reference) {
47+
try {
48+
$definition = $this->container->findDefinition((string) $argument);
49+
} catch (ServiceNotFoundException $e) {
50+
continue;
51+
}
52+
}
53+
54+
if ($definition instanceof AmbiguousDefinition) {
55+
throw new AmbiguousReferenceException($definition->getClass(), $id, $definition->getServices());
56+
}
57+
}
58+
}
59+
60+
private function processFactory($factory)
61+
{
62+
if (null === $factory || !is_array($factory) || !$factory[0] instanceof Reference) {
63+
return;
64+
}
65+
66+
$definition = $this->container->findDefinition($id = (string) $factory[0]);
67+
68+
if ($definition instanceof AmbiguousDefinition) {
69+
throw new AmbiguousReferenceException($definition->getClass(), $id, $definition->getServices());
70+
}
71+
}
72+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function __construct()
4848
new DecoratorServicePass(),
4949
new ResolveParameterPlaceHoldersPass(),
5050
new CheckDefinitionValidityPass(),
51+
new RegisterClassNamedServicesPass(),
5152
new ResolveReferencesToAliasesPass(),
5253
new ResolveInvalidReferencesPass(),
5354
new AutowirePass(),
@@ -57,6 +58,7 @@ public function __construct()
5758
);
5859

5960
$this->removingPasses = array(
61+
new CheckAmbiguousReferencesPass(),
6062
new RemovePrivateAliasesPass(),
6163
new RemoveAbstractDefinitionsPass(),
6264
new ReplaceAliasByActualDefinitionPass(),
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Compiler;
4+
5+
use Symfony\Component\DependencyInjection\Alias;
6+
use Symfony\Component\DependencyInjection\AmbiguousDefinition;
7+
use Symfony\Component\DependencyInjection\Container;
8+
use Symfony\Component\DependencyInjection\ContainerBuilder;
9+
use Symfony\Component\DependencyInjection\Definition;
10+
11+
/**
12+
* @author Martin Hasoň <martin.hason@gmail.com>
13+
*/
14+
class RegisterClassNamedServicesPass implements CompilerPassInterface
15+
{
16+
private $container;
17+
private $types;
18+
private $definedTypes;
19+
private $reflectionClasses = array();
20+
21+
public function process(ContainerBuilder $container)
22+
{
23+
$this->container = $container;
24+
$this->types = array();
25+
$this->definedTypes = array();
26+
27+
//$this->populateAvailableTypes('service_container', new Definition(Container::class));
28+
29+
foreach ($container->getDefinitions() as $id => $definition) {
30+
$this->populateAvailableTypes($id, $definition);
31+
}
32+
33+
foreach ($this->types as $type => $services) {
34+
$this->registerService($type, $services);
35+
}
36+
37+
$this->container = null;
38+
$this->types = array();
39+
$this->definedTypes = array();
40+
$this->reflectionClasses = array();
41+
}
42+
43+
private function populateAvailableTypes($id, Definition $definition)
44+
{
45+
if ($definition->isAbstract()) {
46+
return;
47+
}
48+
49+
if (!$reflectionClass = $this->getReflectionClass($id, $definition)) {
50+
return;
51+
}
52+
53+
foreach ($definition->getAutowiringTypes() as $type) {
54+
$this->definedTypes[$type] = true;
55+
$this->set($type, $id);
56+
}
57+
58+
foreach ($reflectionClass->getInterfaces() as $interfaceClass) {
59+
$this->set($interfaceClass->name, $id);
60+
}
61+
62+
do {
63+
$this->set($reflectionClass->name, $id);
64+
} while ($reflectionClass = $reflectionClass->getParentClass());
65+
}
66+
67+
private function getReflectionClass($id, Definition $definition)
68+
{
69+
if (isset($this->reflectionClasses[$id])) {
70+
return $this->reflectionClasses[$id];
71+
}
72+
73+
if (!$class = $definition->getClass()) {
74+
return;
75+
}
76+
77+
$class = $this->container->getParameterBag()->resolveValue($class);
78+
79+
try {
80+
return $this->reflectionClasses[$id] = new \ReflectionClass($class);
81+
} catch (\ReflectionException $reflectionException) {
82+
return;
83+
}
84+
}
85+
86+
private function set($type, $id)
87+
{
88+
if (isset($this->definedTypes[$type])) {
89+
return;
90+
}
91+
92+
$this->types[$type][] = $id;
93+
}
94+
95+
private function registerService($type, array $services)
96+
{
97+
if (1 === count($services)) {
98+
$service = reset($services);
99+
//$public = 'service_container' === $service ?: $this->container->getDefinition($service)->isPublic();
100+
$public = $this->container->getDefinition($service)->isPublic();
101+
$this->container->setAlias($type, new Alias($service, $public));
102+
} else {
103+
$this->container->setDefinition($type, new AmbiguousDefinition($type, $services));
104+
}
105+
}
106+
}
107+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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\Exception;
13+
14+
class AmbiguousReferenceException extends InvalidArgumentException
15+
{
16+
public function __construct($type, $service, $services)
17+
{
18+
parent::__construct(sprintf('Ambiguous services for class "%s". You should use concrete service name instead of class: "%s"', $type, implode('", "', $services)));
19+
}
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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\Exception;
13+
14+
class AmbiguousServiceException extends RuntimeException
15+
{
16+
public function __construct($type, $services)
17+
{
18+
parent::__construct(sprintf('Ambiguous services for class "%s". You should use concrete service name instead of class: "%s"', $type, implode('", "', $services)));
19+
}
20+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Reference;
16+
17+
class CheckAmbiguousReferencePassTest extends \PHPUnit_Framework_TestCase
18+
{
19+
/** @var ContainerBuilder */
20+
private $container;
21+
22+
protected function setUp()
23+
{
24+
$this->container = new ContainerBuilder();
25+
require_once __DIR__.'/../Fixtures/includes/classes2.php';
26+
}
27+
28+
/**
29+
* @expectedException Symfony\Component\DependencyInjection\Exception\AmbiguousReferenceException
30+
* @expectedExceptionMessage Ambiguous services for class "Symfony\Component\DependencyInjection\Tests\Compiler\ClassNamedServices\E". You should use concrete service name instead of class: "foo", "bar"
31+
*/
32+
public function testThrowExceptionForAmbiguousDefinitionInArguments()
33+
{
34+
$container = $this->container;
35+
$container->register('foo', ClassNamedServices\E::class);
36+
$container->register('bar', ClassNamedServices\E::class);
37+
38+
$definition = $container->register('baz', ClassNamedServices\A::class);
39+
$definition->setArguments(array(new Reference(ClassNamedServices\E::class)));
40+
41+
$container->compile();
42+
}
43+
}

0 commit comments

Comments
 (0)