8000 Add the kernel.controller_arguments event by stof · Pull Request #18440 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

Add the kernel.controller_arguments event #18440

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
Apr 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/Symfony/Component/HttpKernel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ CHANGELOG
* added `Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface`
* added `Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface` as argument to `HttpKernel`
* added `Symfony\Component\HttpKernel\Controller\ArgumentResolver`
* added the `kernel.controller_arguments` event, triggered after controller arguments have been resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

This sentence sounds more like a commit message which is is trying to be as short as possible. If you would change it slightly, it would sound a lot better:

  • added the kernel.controller_arguments event, which is triggered after the controller arguments have been resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

#18412 has just been merged, you need to rebase because of the changelog though.


3.0.0
-----
Expand All @@ -22,8 +23,8 @@ CHANGELOG
* removed `Symfony\Component\HttpKernel\EventListener\RouterListener::setRequest()`
* removed `Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelRequest()`
* removed `Symfony\Component\HttpKernel\Fragment\FragmentHandler::setRequest()`
* removed `Symfony\Component\HttpKernel\HttpCache\Esi::hasSurrogateEsiCapability()`
* removed `Symfony\Component\HttpKernel\HttpCache\Esi::addSurrogateEsiCapability()`
* removed `Symfony\Component\HttpKernel\HttpCache\Esi::hasSurrogateEsiCapability()`
* removed `Symfony\Component\HttpKernel\HttpCache\Esi::addSurrogateEsiCapability()`
* removed `Symfony\Component\HttpKernel\HttpCache\Esi::needsEsiParsing()`
* removed `Symfony\Component\HttpKernel\HttpCache\HttpCache::getEsi()`
* removed `Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ protected function preDispatch($eventName, Event $event)
protected function postDispatch($eventName, Event $event)
{
switch ($eventName) {
case KernelEvents::CONTROLLER:
case KernelEvents::CONTROLLER_ARGUMENTS:
Copy link
Contributor

Choose a reason for hiding this comment

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

you're removing the stopwatch start on controller events then, to apply it only on controller_arguments events ?

Copy link
Member Author

Choose a reason for hiding this comment

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

@Taluu this is about starting the stopwatch for the controller execution itself. As there is now an event after the resolution of arguments, I starting it there now, as it makes the timing more accurate (in existing versions, the argument resolution is included in the controller time, even though it is also timed on its own)

$this->stopwatch->start('controller', 'section');
break;
case KernelEvents::RESPONSE:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpKernel\Event;

use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Request;

/**
* Allows filtering of controller arguments.
*
* You can call getController() to retrieve the controller and getArguments
* to retrieve the current arguments. With setArguments() you can replace
* arguments that are used to call the controller.
*
* Arguments set in the event must be compatible with the signature of the
* controller.
*
* @author Christophe Coevoet <stof@notk.org>
*/
class FilterControllerArgumentsEvent extends FilterControllerEvent
{
private $arguments;

public function __construct(HttpKernelInterface $kernel, callable $controller, array $arguments, Request $request, $requestType)
{
parent::__construct($kernel, $controller, $request, $requestType);

$this->arguments = $arguments;
}

/**
* @return array
*/
public function getArguments()
{
return $this->arguments;
}

/**
* @param array $arguments
*/
public function setArguments(array $arguments)
{
$this->arguments = $arguments;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a benefit of making the arguments mutable? Personally I would make this a constructor argument only until other cases really need this. If people want to alter the arguments, they should create an ArgumentResolverValue in my opinion, this makes it a lot easier to trace back what creates the arguments and prevents weird mutations that might alter the amount of arguments.

Copy link
Member

Choose a reason for hiding this comment

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

@iltar I'm not sure about that. Don't you think it would complicate things unnecessarily?

Copy link
Contributor

Choose a reason for hiding this comment

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

Imagine a case where I get 3 arguments:

[ 0 => 'foo', 1 => 'bar', 2 => 'baz' ]

I want to update the last element but I did $arguments[3] = 'foo'; instead of $arguments[2] = 'foo';, I would end up with 1 argument more and no clear trace of where what happened.

Another issue is that you can't really do much at this point with those arguments. You don't have access to the metadata so if you want to do anything with introspection, you would have to (again) create the metadata or do it manually with reflection. Until there's a cached variant, the first is really not something I would advice.

This would not be the correct hook to modify the arguments. I would rather get rid of the setController() in the FilterControllerEvent as well and use a ControllerFactory for the controller event (on my todo list). This means you have 1 fixed point of turning _controller (or whatever else) into something callable, just like you have 1 fixed point of setting the argument at this point.

Copy link
Member Author

Choose a reason for hiding this comment

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

forbidding to alter the controller in a kernel.controller event would be a BC break though (and btw, the description of the kernel.controller event describes this as the use case of the event since Symfony 2.0).

Btw, there are some cases where being able to alter arguments after they are resolved might be much easier than registering a value resolver, as it would mean that your value resolver has to duplicate the original resolution logic to add its own logic.

Copy link
Contributor

Choose a reason for hiding this comment

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

the description of the kernel.controller event describes this as the use case of the event since Symfony 2.0

My guess is that's because there's no other way to hook in on the controller generation at the moment

there are some cases where being able to alter arguments after they are resolved might be much easier than registering a value resolver, as it would mean that your value resolver has to duplicate the original resolution logic to add its own logic.

Do you have an example? I've been thinking of cases where this would be a viable alternative but I can't think of any

Copy link
Member Author

Choose a reason for hiding this comment

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

My guess is that's because there's no other way to hook in on the controller generation at the moment

@iltar replacing the controller factory is possible since 2.0 thanks to dependency injection.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, you can implement your own ControllerResolver, but to add your own notation while supporting the existing ones, you'd have to extend the existing classes + service definitions etc, which is quite cumbersome. Anyways, this is getting off-topic.

I really don't mind if you keep the setArguments() as you have a good reason to add it: Another extension point which is easy to implement. I'm just afraid it will get confusing if you have 2 extension points.

Would it be possible to log it if arguments have changed (even if only in dev)?

Copy link
Member Author

Choose a reason for hiding this comment

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

@iltar the usage of replacing the callable is not about introducing custom notations, but more about customizing the behavior of the controller callable, for instance by replacing it with a proxy object performing extra work before and after the action (JMSDiExtraBundle has such a use case, even though it decided to overwrite the ControllerResolver in a way making it unfriendly for people wanting to overwrite the resolver logic and using the listener would have been a better idea)

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ public function getController()
* Sets a new controller.
*
* @param callable $controller
*
* @throws \LogicException
*/
public function setController(callable $controller)
{
Expand Down
6 changes: 6 additions & 0 deletions src/Symfony/Component/HttpKernel/HttpKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
Expand Down Expand Up @@ -138,6 +139,11 @@ private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
// controller arguments
$arguments = $this->argumentResolver->getArguments($request, $controller);

$event = new FilterControllerArgumentsEvent($this, $controller, $arguments, $request, $type);
$this->dispatcher->dispatch(KernelEvents::CONTROLLER_ARGUMENTS, $event);
$controller = $event->getController();
$arguments = $event->getArguments();

// call controller
$response = call_user_func_array($controller, $arguments);

Expand Down
13 changes: 13 additions & 0 deletions src/Symfony/Component/HttpKernel/KernelEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ final class KernelEvents
*/
const CONTROLLER = 'kernel.controller';

/**
* The CONTROLLER_ARGUMENTS event occurs once controller arguments have been resolved.
*
* This event allows you to change the arguments that will be passed to
* the controller. The event listener method receives a
* Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent instance.
*
* @Event
*
* @var string
*/
const CONTROLLER_ARGUMENTS = 'kernel.controller_arguments';

/**
* The RESPONSE event occurs once a response was created for
* replying to a request.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public function testStopwatchSections()
'__section__',
'kernel.request',
'kernel.controller',
'kernel.controller_arguments',
'controller',
'kernel.response',
'kernel.terminate',
Expand Down
41 changes: 39 additions & 2 deletions src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
Expand Down Expand Up @@ -233,6 +234,42 @@ public function testHandleWithAResponseListener()
$this->assertEquals('foo', $kernel->handle(new Request())->getContent());
}

public function testHandleAllowChangingControllerArguments()
{
$dispatcher = new EventDispatcher();
$dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS, function (FilterControllerArgumentsEvent $event) {
$event->setArguments(array('foo'));
});

$kernel = $this->getHttpKernel($dispatcher, function ($content) { return new Response($content); });

$this->assertResponseEquals(new Response('foo'), $kernel->handle(new Request()));
}

public function testHandleAllowChangingControllerAndArguments()
{
$dispatcher = new EventDispatcher();
$dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS, function (FilterControllerArgumentsEvent $event) {
$oldController = $event->getController();
$oldArguments = $event->getArguments();

$newController = function ($id) use ($oldController, $oldArguments) {
$response = call_user_func_array($oldController, $oldArguments);

$response->headers->set('X-Id', $id);

return $response;
};

$event->setController($newController);
$event->setArguments(array('bar'));
});

$kernel = $this->getHttpKernel($dispatcher, function ($content) { return new Response($content); }, null, array('foo'));

$this->assertResponseEquals(new Response('foo', 200, array('X-Id' => 'bar')), $kernel->handle(new Request()));
}

public function testTerminate()
{
$dispatcher = new EventDispatcher();
Expand Down Expand Up @@ -265,7 +302,7 @@ public function testVerifyRequestStackPushPopDuringHandle()
$kernel->handle($request, HttpKernelInterface::MASTER_REQUEST);
}

private function getHttpKernel(EventDispatcherInterface $eventDispatcher, $controller = null, RequestStack $requestStack = null)
private function getHttpKernel(EventDispatcherInterface $eventDispatcher, $controller = null, RequestStack $requestStack = null, array $arguments = array())
{
if (null === $controller) {
$controller = function () { return new Response('Hello'); };
Expand All @@ -281,7 +318,7 @@ private function getHttpKernel(EventDispatcherInterface $eventDispatcher, $contr
$argumentResolver
->expects($this->any())
->method('getArguments')
->will($this->returnValue(array()));
->will($this->returnValue($arguments));

return new HttpKernel($eventDispatcher, $controllerResolver, $requestStack, $argumentResolver);
}
Expand Down
0