Skip to content

Commit 4c056d9

Browse files
[DI] Add context to service-not-found exceptions thrown by service locators
1 parent 22a6a7e commit 4c056d9

File tree

7 files changed

+75
-21
lines changed

7 files changed

+75
-21
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ protected function processValue($value, $isRoot = false)
9494
throw new InvalidArgumentException(sprintf('Service %s not exist in the map returned by "%s::getSubscribedServices()" for service "%s".', $message, $class, $this->currentId));
9595
}
9696

97-
$value->addTag('container.service_subscriber.locator', array('id' => (string) ServiceLocatorTagPass::register($this->container, $subscriberMap)));
97+
$value->addTag('container.service_subscriber.locator', array('id' => (string) ServiceLocatorTagPass::register($this->container, $subscriberMap, $this->currentId)));
9898

9999
return parent::processValue($value);
100100
}

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

+13-1
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,13 @@ protected function processValue($value, $isRoot = false)
7272
/**
7373
* @param ContainerBuilder $container
7474
* @param Reference[] $refMap
75+
* @param string|null $callerId
7576
*
7677
* @return Reference
78+
*
79+
* @final since version 3.4
7780
*/
78-
public static function register(ContainerBuilder $container, array $refMap)
81+
public static function register(ContainerBuilder $container, array $refMap/*, string $callerId = null*/)
7982
{
8083
foreach ($refMap as $id => $ref) {
8184
if (!$ref instanceof Reference) {
@@ -94,6 +97,15 @@ public static function register(ContainerBuilder $container, array $refMap)
9497
$container->setDefinition($id, $locator);
9598
}
9699

100+
if (null !== $callerId = 2 < func_num_args() ? func_get_arg(2) : null) {
101+
$locatorId = $id;
102+
$container->register($id .= '.'.$callerId, ServiceLocator::class)
103+
->setPublic(false)
104+
->setFactory(array(new Reference($locatorId), 'withContext'))
105+
->addArgument($callerId)
106+
->addArgument(new Reference('service_container'));
107+
}
108+
97109
return new Reference($id);
98110
}
99111
}

src/Symfony/Component/DependencyInjection/ServiceLocator.php

+53-6
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
namespace Symfony\Component\DependencyInjection;
1313

1414
use Psr\Container\ContainerInterface as PsrContainerInterface;
15+
use Symfony\Component\DependencyInjection\Container;
1516
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
1617
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
18+
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
1719

1820
/**
1921
* @author Robin Chalas <robin.chalas@gmail.com>
@@ -22,6 +24,9 @@
2224
class ServiceLocator implements PsrContainerInterface
2325
{
2426
private $factories;
27+
private $loading = array();
28+
private $externalId;
29+
private $container;
2530

2631
/**
2732
* @param callable[] $factories
@@ -31,6 +36,15 @@ public function __construct(array $factories)
3136
$this->factories = $factories;
3237
}
3338

39+
public function withContext($externalId, Container $container)
40+
{
41+
$locator = clone $this;
42+
$locator->externalId = $externalId;
43+
$locator->container = $container;
44+
45+
return $locator;
46+
}
47+
3448
/**
3549
* {@inheritdoc}
3650
*/
@@ -45,23 +59,56 @@ public function has($id)
4559
public function get($id)
4660
{
4761
if (!isset($this->factories[$id])) {
48-
throw new ServiceNotFoundException($id, null, null, array_keys($this->factories));
62+
throw new ServiceNotFoundException($id, end($this->loading) ?: null, null, array(), $this->createServiceNotFoundMessage($id));
4963
}
5064

51-
if (true === $factory = $this->factories[$id]) {
52-
throw new ServiceCircularReferenceException($id, array($id, $id));
65+
if (isset($this->loading[$id])) {
66+
$ids = array_values($this->loading);
67+
$ids = array_slice($this->loading, array_search($id, $ids));
68+
$ids[] = $id;
69+
70+
throw new ServiceCircularReferenceException($id, $ids);
5371
}
5472

55-
$this->factories[$id] = true;
73+
$this->loading[$id] = $id;
5674
try {
57-
return $factory();
75+
return $this->factories[$id]();
5876
} finally {
59-
$this->factories[$id] = $factory;
77+
unset($this->loading[$id]);
6078
}
6179
}
6280

6381
public function __invoke($id)
6482
{
6583
return isset($this->factories[$id]) ? $this->get($id) : null;
6684
}
85+
86+
private function createServiceNotFoundMessage($id)
87+
{
88+
if ($this->loading) {
89+
$msg = sprintf('The service "%s" has a dependency on a non-existent service "%s".', end($this->loading), $id);
90+
$msg .= sprintf(' This locator only knows about "%s"', implode('", "', array_keys($this->factories)));
91+
} else {
92+
$class = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 3);
93+
$class = isset($class['object'][2]) ? get_class($class['object'][2]) : null;
94+
$externalId = $this->externalId ?: $class;
95+
96+
$msg = sprintf('Service "%s" not found: ', $id);
97+
98+
if ($this->container && ($this->container->has($id) || isset($this->container->getRemovedIds()[$id]))) {
99+
$msg .= 'even though it exists in the app\'s container, ';
100+
}
101+
if ($externalId) {
102+
$msg .= sprintf('"%s" only knows about pre-listed services "%s".', $externalId, implode('", "', array_keys($this->factories)));
103+
} else {
104+
$msg .= sprintf('the current locator only knows about "%s".', implode('", "', array_keys($this->factories)));
105+
}
106+
107+
if ($class && is_subclass_of($class, ServiceSubscriberInterface::class)) {
108+
$msg .= sprintf(' Did you forget to declare the depencency using "%s::getSubscribedServices()"?', preg_replace('/([^\\\\]++\\\\)++/', '', $class));
109+
}
110+
}
111+
112+
return $msg;
113+
}
67114
}

src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public function testNoAttributes()
8585
'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, TestServiceSubscriber::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
8686
);
8787

88-
$this->assertEquals($expected, $locator->getArgument(0));
88+
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
8989
}
9090

9191
public function testWithAttributes()
@@ -115,7 +115,7 @@ public function testWithAttributes()
115115
'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, TestServiceSubscriber::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
116116
);
117117

118-
$this->assertEquals($expected, $locator->getArgument(0));
118+
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
119119
}
120120

121121
/**

src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public function getRemovedIds()
4545
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
4646
'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true,
4747
'service_locator.jmktfsv' => true,
48+
'service_locator.jmktfsv.foo_service' => true,
4849
);
4950
}
5051

@@ -82,15 +83,15 @@ protected function getTestServiceSubscriberService()
8283
*/
8384
protected function getFooServiceService()
8485
{
85-
return $this->services['foo_service'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber(new \Symfony\Component\DependencyInjection\ServiceLocator(array('Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => function () {
86+
return $this->services['foo_service'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber(call_user_func(array(new \Symfony\Component\DependencyInjection\ServiceLocator(array('Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => function () {
8687
$f = function (\Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition $v = null) { return $v; }; return $f(${($_ = isset($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition']) ? $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] : $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition()) && false ?: '_'});
8788
}, 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber' => function () {
8889
$f = function (\Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber $v) { return $v; }; return $f(${($_ = isset($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber']) ? $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] : $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber()) && false ?: '_'});
8990
}, 'bar' => function () {
9091
$f = function (\Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition $v) { return $v; }; return $f(${($_ = isset($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber']) ? $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] : $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber()) && false ?: '_'});
9192
}, 'baz' => function () {
9293
$f = function (\Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition $v = null) { return $v; }; return $f(${($_ = isset($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition']) ? $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] : $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition()) && false ?: '_'});
93-
})));
94+
})), 'withContext'), 'foo_service', $this));
9495
}
9596

9697
/**

src/Symfony/Component/DependencyInjection/Tests/ServiceLocatorTest.php

+2-8
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function testGetDoesNotMemoize()
5959

6060
/**
6161
* @expectedException \Psr\Container\NotFoundExceptionInterface
62-
* @expectedExceptionMessage You have requested a non-existent service "dummy". Did you mean one of these: "foo", "bar"?
62+
* @expectedExceptionMessage Service "dummy" not found: "Symfony\Component\DependencyInjection\Tests\ServiceLocatorTest" only knows about pre-listed services "foo", "bar".
6363
*/
6464
public function testGetThrowsOnUndefinedService()
6565
{
@@ -68,13 +68,7 @@ public function testGetThrowsOnUndefinedService()
6868
'bar' => function () { return 'baz'; },
6969
));
7070

71-
try {
72-
$locator->get('dummy');
73-
} catch (ServiceNotFoundException $e) {
74-
$this->assertSame(array('foo', 'bar'), $e->getAlternatives());
75-
76-
throw $e;
77-
}
71+
$locator->get('dummy');
7872
}
7973

8074
public function testInvoke()

src/Symfony/Component/HttpKernel/HttpKernel.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
150150
$arguments = $event->getArguments();
151151

152152
// call controller
153-
$response = call_user_func_array($controller, $arguments);
153+
$response = \call_user_func_array($controller, $arguments);
154154

155155
// view
156156
if (!$response instanceof Response) {

0 commit comments

Comments
 (0)