Skip to content

[DI] Add context to service-not-found exceptions thrown by service locators #25381

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 11, 2017

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Dec 7, 2017

Q A
Branch? 3.4
Bug fix? yes (DX)
New feature? yes (...)
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #25342, #25196
License MIT
Doc PR -

Here hopefully is the fully-context aware message you're looking for @weaverryan @curry684.

Service "foo" not found: even though it exists in the app's container, the container inside "caller" is a smaller service locator that only knows about the "bar" service. Unless you need extra laziness, try using dependency injection instead. Otherwise, you need to declare it using "SomeServiceSubscriber::getSubscribedServices()".

Copy link
Contributor

@curry684 curry684 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brilliant!

@nicolas-grekas nicolas-grekas force-pushed the di-dx-locator branch 2 times, most recently from 3afa88b to 4c056d9 Compare December 7, 2017 17:19
@@ -31,6 +36,15 @@ public function __construct(array $factories)
$this->factories = $factories;
}

public function withContext($externalId, Container $container)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@internal?

@@ -94,6 +97,15 @@ public static function register(ContainerBuilder $container, array $refMap)
$container->setDefinition($id, $locator);
}

if (null !== $callerId = 2 < func_num_args() ? func_get_arg(2) : null) {
$locatorId = $id;
$container->register($id .= '.'.$callerId, ServiceLocator::class)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you hate it when I suggest this, but a comment above this explaining what's going on would help. I can't locate where and why registering this service is significant.

{
if ($this->loading) {
$msg = sprintf('The service "%s" has a dependency on a non-existent service "%s".', end($this->loading), $id);
$msg .= sprintf(' This locator only knows about "%s"', implode('", "', array_keys($this->factories)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this situation happen? Once I use $this->container->get('foo'), if foo depends on a non-existent bar service, wouldn't the container have already exploded at build time?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see new test case :)

} else {
$class = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 3);
$class = isset($class['object'][2]) ? get_class($class['object'][2]) : null;
$externalId = $this->externalId ?: $class;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would $externalId be set versus not set? It obviously makes a difference in the error message below, so just curious.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eg the locator is used from a function.
But in practice, should not happen to users.

$msg .= 'even though it exists in the app\'s container, ';
}
if ($externalId) {
$msg .= sprintf('"%s" only knows about pre-listed services "%s".', $externalId, implode('", "', array_keys($this->factories)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe:

The container inside "%s" is a smaller service locator that only contains the following services: "%s".

}

if ($class && is_subclass_of($class, ServiceSubscriberInterface::class)) {
$msg .= sprintf(' Did you forget to declare the depencency using "%s::getSubscribedServices()"?', preg_replace('/([^\\\\]++\\\\)++/', '', $class));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to go too crazy, but if the actual class that implements ServiceSubscriberInterface is a vendor, then this error message doesn't make much sense. The screenshot of the error in the PR is a good example of this - the correct solution for a controller is probably not to override getSubscribedServices().

For the controller situations specifically (which probably would work for any case where getSubscribedServices() is actually implemented by a vendor class), the better message might be:

Instead of using the container, try using dependency injection to get the service.

We could go even more bonkers and do something (for AbstractController) about type-hinting something on the controller... but probably that's overboard. We just need to say "Hey! Stop using the container! Do what you see recommended everywhere in the docs instead".

*/
public static function register(ContainerBuilder $container, array $refMap)
public static function register(ContainerBuilder $container, array $refMap/*, string $callerId = null*/)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the class is final (not even @final but actually final), so you can add the new argument without this magic

}

if ($class && is_subclass_of($class, ServiceSubscriberInterface::class)) {
$msg .= sprintf(' Did you forget to declare the depencency using "%s::getSubscribedServices()"?', preg_replace('/([^\\\\]++\\\\)++/', '', $class));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo here depencency vs dependency

@@ -150,7 +150,7 @@ private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
$arguments = $event->getArguments();

// call controller
$response = call_user_func_array($controller, $arguments);
$response = \call_user_func_array($controller, $arguments);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest submitting this to 2.7 instead. It is unrelated to this PR.

@nicolas-grekas
Copy link
Member Author

@stof @weaverryan thanks for the review, comments addressed.

@nicolas-grekas nicolas-grekas force-pushed the di-dx-locator branch 8 times, most recently from 6d2f355 to d34e760 Compare December 8, 2017 12:36
@nicolas-grekas
Copy link
Member Author

PR green and ready.
We could even improve the message further, by suggesting what to type-hint for.
But this requires some code infrastructure that we don't have right now, so for 4.1 if someone wants to make it happen after merge.

Status: needs review

@nicolas-grekas nicolas-grekas force-pushed the di-dx-locator branch 2 times, most recently from 28e1964 to 60cd0f6 Compare December 9, 2017 09:06
Copy link
Contributor

@ro0NL ro0NL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

next level error messages :) i like it.

return sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $this->formatAlternatives());
}

$class = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 3);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with DEBUG_BACKTRACE_IGNORE_ARGS or is it negligible?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, added

Copy link
Member

@chalasr chalasr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

if ($this->loading) {
return sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $this->formatAlternatives());
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra spaces

Copy link
Member

@weaverryan weaverryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for the messaging as shown on the test cases. The technical details are a bit complex, but this only touches the messaging and is well-covered with test cases.

@fabpot
Copy link
Member

fabpot commented Dec 11, 2017

Thank you @nicolas-grekas.

@fabpot fabpot merged commit 9512f26 into symfony:3.4 Dec 11, 2017
fabpot added a commit that referenced this pull request Dec 11, 2017
… service locators (nicolas-grekas)

This PR was merged into the 3.4 branch.

Discussion
----------

[DI] Add context to service-not-found exceptions thrown by service locators

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes (DX)
| New feature?  | yes (...)
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #25342, #25196
| License       | MIT
| Doc PR        | -

Here hopefully is the fully-context aware message you're looking for @weaverryan @curry684.

![image](https://user-images.githubusercontent.com/243674/33726013-1db38a34-db74-11e7-91dd-ca9d53e58891.png)

Commits
-------

9512f26 [DI] Add context to service-not-found exceptions thrown by service locators
@nicolas-grekas nicolas-grekas deleted the di-dx-locator branch December 11, 2017 20:32
This was referenced Dec 15, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants