Skip to content
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

[HttpKernel] Add a better error messages when passing a private or non-tagged controller #25201

Conversation

@Simperfit
Copy link
Contributor

commented Nov 29, 2017

Q A
Branch? 3.4
Bug fix? no
New feature? no
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #25192
License MIT
Doc PR none
  • Add more tests
return parent::instantiateController($class);
} catch (FatalThrowableError $e) {
if ($this->container instanceof ContainerBuilder) {
var_dump($this->container->getRemovedIds());

This comment has been minimized.

Copy link
@sroze
@Simperfit

This comment has been minimized.

Copy link
Contributor Author

commented Nov 29, 2017

}
return parent::instantiateController($class);
} catch (FatalThrowableError $e) {

This comment has been minimized.

Copy link
@sroze

sroze Nov 29, 2017

Member

You need to be more specific than that. First of all: isn't it on the $this->container->get(...) that you want to catch the exception? Also, can you use the exception specific to this error, not a Debug exception?

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 29, 2017

Member

Actually, I'm not sure: this is Debug turning an E_RECOVERABLE_ERROR into an exception.
There is no way to catch this error without the debug component.
And any other exception might be unrelated in fact.
So this looks specific to me: improves DX by throwing to the user when applicable.

This comment has been minimized.

Copy link
@Vinorcola

Vinorcola Nov 29, 2017

The problem I encountered into issue #25192 is that the Controller is not registered into the container, and the parent::instantiateController() method is called, which instantiate the controller without any argument.

So the try catch should only be for the return parent::instantiateController($class);.

Can we not use introspection in this method (instantiateController) to check that the constructor does not require arguments, and throw an exception here if it does?

This comment has been minimized.

Copy link
@sroze

sroze Nov 29, 2017

Member

So it is maybe something to change on the ControllerResolver directly?

This comment has been minimized.

Copy link
@Vinorcola

Vinorcola Nov 29, 2017

I think I would go for that and throw an exception telling something similar to

"Your controller constructor requires argument but is neither declared as a public service nor tagged with controller.service_arguments"

But having instrospection is heavy work so I don't know if it worth it...

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 29, 2017

Member

the parent doens't know anything about the container, so here is the correct place, isn't it?
catching only the parent call looks sensible to me also

This comment has been minimized.

Copy link
@Vinorcola

Vinorcola Nov 29, 2017

@nicolas-grekas I think the point here is that ControllerResolver::instantiateController() cannot instantiate controllers requiring parameters in the constructor. So this method should check that the controller's constructor does not require any arguments and throw an exception if it does. This is not related to container.

If a controller is correctly configured as a public service or tagged with controller.service_arguments, then the controller will not be instantiate, and pulled from the container so the ControllerResolver::instantiateController() method will never be called, so no problem here...

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 29, 2017

Member

So this method should check that the controller's constructor does not require any arguments and throw an exception if it does.

This supposes that the check is free, which is not the case. Here, we're in a performance critical code path. There is no better way to do this check than the current one: try, and catch any error.

@sroze

This comment has been minimized.

Copy link
Member

commented Nov 29, 2017

Status: Needs Work

@@ -13,6 +13,9 @@
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Debug\Exception\FatalThrowableError;
use Symfony\Component\DependencyInjection\ContainerBuilder;

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 29, 2017

Member

Why ContainerBuilder, and not Container?

This comment has been minimized.

Copy link
@Simperfit

Simperfit Nov 29, 2017

Author Contributor

ill use Container

@@ -175,7 +196,7 @@ protected function createControllerResolver(LoggerInterface $logger = null, Cont
protected function createMockContainer()
{
return $this->getMockBuilder(ContainerInterface::class)->getMock();
return $this->getMockBuilder(ContainerBuilder::class)->getMock();

This comment has been minimized.

Copy link
@nicolas-grekas

This comment has been minimized.

Copy link
@Simperfit

Simperfit Nov 29, 2017

Author Contributor

Ill revert that.

@Simperfit Simperfit force-pushed the Simperfit:feature/add-dx-when-passing-a-private-controller branch 2 times, most recently from a0c7d55 to 193a461 Nov 29, 2017

@nicolas-grekas

This comment has been minimized.

Copy link
Member

commented Nov 29, 2017

to be rebased on 3.4 btw

@nicolas-grekas nicolas-grekas added this to the 3.4 milestone Nov 29, 2017

@Simperfit Simperfit force-pushed the Simperfit:feature/add-dx-when-passing-a-private-controller branch from 193a461 to 1f14f4d Nov 29, 2017

@Simperfit Simperfit changed the base branch from 4.0 to 3.4 Nov 29, 2017

@Simperfit Simperfit force-pushed the Simperfit:feature/add-dx-when-passing-a-private-controller branch 2 times, most recently from 7ce94f3 to 35ab6dd Nov 29, 2017

@@ -82,10 +84,25 @@ protected function createController($controller)
*/
protected function instantiateController($class)
{
if ($this->container->has($class)) {
return $this->container->get($class);
try {

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 29, 2017

Member

Should be moved below, see comments

@Simperfit Simperfit force-pushed the Simperfit:feature/add-dx-when-passing-a-private-controller branch 3 times, most recently from 9ddc5c9 to 828d8c4 Nov 29, 2017

} catch (\ArgumentCountError $e) {
}
if (null !== $e) {

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 29, 2017

Member

$e should be initialized, is it? This "if" should be merged with the next one also.

if (null !== $e) {
if ($this->container instanceof Container) {
if (in_array($class, $this->container->getRemovedIds())) {

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 29, 2017

Member

A bit unrelated but maybe an opportunity to catch character case mismatches also here? Supposes that we can prevent the parent from failing hard in that case.

@Simperfit Simperfit force-pushed the Simperfit:feature/add-dx-when-passing-a-private-controller branch 3 times, most recently from 7cb768b to 6345f3b Nov 30, 2017

@@ -86,6 +94,18 @@ protected function instantiateController($class)
return $this->container->get($class);
}
return parent::instantiateController($class);
$e = null;

This comment has been minimized.

Copy link
@ogizanagi

ogizanagi Nov 30, 2017

Member

Could be removed and use isset($e) in if below ?

This comment has been minimized.

Copy link
@Simperfit

Simperfit Nov 30, 2017

Author Contributor

isn't better for performance to do this instead of isset ?

This comment has been minimized.

Copy link
@ogizanagi

ogizanagi Nov 30, 2017

Member

Hmm, no idea. Actually I thought we already used isset($e) for such cases in many places. But it seems I was wrong. So fine 👍

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 30, 2017

Member

Setting to null is the CS we use in the code base.

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 30, 2017

Member

oh, in fact, this can be removed, the variable is not needed: if we reach L99, $e will always be non null
which means the null !== $e check can be removed from the below if also

} catch (\ArgumentCountError $e) {
}
if (null !== $e && $this->container instanceof Container && in_array($class, $this->container->getRemovedIds())) {

This comment has been minimized.

Copy link
@ogizanagi

ogizanagi Nov 30, 2017

Member

Shouldn't this be in the Fwb ControllerResolver instead?

This comment has been minimized.

Copy link
@ogizanagi

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 30, 2017

Member

this would limit the behavior to ppl using the fwb, I don't see any reason to do so

@Simperfit Simperfit force-pushed the Simperfit:feature/add-dx-when-passing-a-private-controller branch from 6345f3b to 210016f Nov 30, 2017

->will($this->returnValue(array(ImpossibleConstructController::class)))
;
if (method_exists($this, 'expectException')) {

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 30, 2017

Member

Could we use an annotation instead?

This comment has been minimized.

Copy link
@Simperfit

Simperfit Nov 30, 2017

Author Contributor

Ill try but in other test we use this instead of annot, it's maybe because of lower version of php using lower version of phpunit.

}
if (null !== $e && $this->container instanceof Container && in_array($class, $this->container->getRemovedIds())) {
throw new \InvalidArgumentException(sprintf('The controller %s seems to be removed from the container, which means that he maybe private, add the tag \'controller.service_arguments\' or made it public.', $class));

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 30, 2017

Member

RuntimeException? The argument is valid, the config is not.
Also pass $e as previous exception to the constructor?

@Simperfit Simperfit force-pushed the Simperfit:feature/add-dx-when-passing-a-private-controller branch 2 times, most recently from b0d2aba to 1a872d1 Nov 30, 2017

}
if (null !== $e && $this->container instanceof Container && in_array($class, $this->container->getRemovedIds())) {
throw new \RuntimeException(sprintf('The controller %s seems to be removed from the container, which means that he maybe private, add the tag \'controller.service_arguments\' or made it public.', $class), 0, $e);

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 30, 2017

Member

I propose this:
new \LogicException(sprintf('Controller "%s" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?', $class), 0, $e);

} catch (\ArgumentCountError $e) {
}
if (null !== $e && $this->container instanceof Container && in_array($class, $this->container->getRemovedIds())) {

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 30, 2017

Member

null !== $e && to be removed, see comment above

if (null !== $e && $this->container instanceof Container && in_array($class, $this->container->getRemovedIds())) {
throw new \RuntimeException(sprintf('The controller %s seems to be removed from the container, which means that he maybe private, add the tag \'controller.service_arguments\' or made it public.', $class), 0, $e);
}

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 30, 2017

Member

else throw $e

@Simperfit Simperfit force-pushed the Simperfit:feature/add-dx-when-passing-a-private-controller branch 2 times, most recently from 2d29a78 to 83c0784 Nov 30, 2017

}
if ($this->container instanceof Container && in_array($class, $this->container->getRemovedIds())) {
throw new \LogicException(sprintf('Controller "%s" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?', $class), 0, $e);

This comment has been minimized.

Copy link
@dunglas

dunglas Nov 30, 2017

Member

It looks to weird from a DX POV to recommend to add this tag instead of just recommending to make the service public.

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Nov 30, 2017

Member

I crafted the message so that, from DX pov, the best experience is suggested by default (allowing services as arguments, ie the tag)
and from a more exprienced DX pov, the word "private" hints the advanced dev about "public" being just missing.
If one doesn't know the difference, then we should definitely suggest the tag. Hence the current message.

@Simperfit Simperfit force-pushed the Simperfit:feature/add-dx-when-passing-a-private-controller branch 2 times, most recently from a784e86 to 92e67e6 Nov 30, 2017

@dunglas
Copy link
Member

left a comment

👍

@Simperfit Simperfit force-pushed the Simperfit:feature/add-dx-when-passing-a-private-controller branch 4 times, most recently from f5709c5 to 5824d18 Nov 30, 2017

@Simperfit Simperfit force-pushed the Simperfit:feature/add-dx-when-passing-a-private-controller branch from 5824d18 to b1173f3 Nov 30, 2017

@nicolas-grekas

This comment has been minimized.

Copy link
Member

commented Nov 30, 2017

Thank you @Simperfit.

@nicolas-grekas nicolas-grekas merged commit b1173f3 into symfony:3.4 Nov 30, 2017

1 of 3 checks passed

continuous-integration/appveyor/pr AppVeyor build cancelled
Details
continuous-integration/travis-ci/pr The Travis CI build could not complete due to an error
Details
fabbot.io Your code looks good.
Details
nicolas-grekas added a commit that referenced this pull request Nov 30, 2017
bug #25201 [HttpKernel] Add a better error messages when passing a pr…
…ivate or non-tagged controller (Simperfit)

This PR was merged into the 3.4 branch.

Discussion
----------

[HttpKernel] Add a better error messages when passing a private or non-tagged controller

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | no
| New feature?  | no <!-- don't forget to update src/**/CHANGELOG.md files -->
| BC breaks?    | no
| Deprecations? | no <!-- don't forget to update UPGRADE-*.md files -->
| Tests pass?   | yes
| Fixed tickets | #25192 <!-- #-prefixed issue number(s), if any -->
| License       | MIT
| Doc PR        | none

- [x] Add more tests

Commits
-------

b1173f3 [HttpKernel] Add a better error messages when passing a private or non-tagged controller
This was referenced Nov 30, 2017

@sroze sroze deleted the Simperfit:feature/add-dx-when-passing-a-private-controller branch Dec 5, 2017

fabpot added a commit that referenced this pull request Dec 7, 2017
minor #25339 [DX][HttpKernel] Throw a sensible exception when control…
…ler has been removed (sroze)

This PR was merged into the 3.4 branch.

Discussion
----------

[DX][HttpKernel] Throw a sensible exception when controller has been removed

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #25335
| License       | MIT
| Doc PR        | ø

Following on #25201, we need to throw the same kind of sensible exception when the controller service is not found.

Commits
-------

458d63f Throw a sensible exception when controller has been removed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants
You can’t perform that action at this time.