Commit fd1fb2ae authored by Fabien Potencier's avatar Fabien Potencier

feature #1161 View Listener (simensen, davedevelopment)

This PR was merged into the 1.3 branch.

Discussion
----------

View Listener

Previously #1062 and #863

TODO

- [x] Documentation

Commits
-------

fa14af74 Remove incorrect examples and fix grammar
f0d7d7c2 Basic documentation
5407435c Use PHPUnit annotation to skip test on <5.4
b4446482 Document as mixed, may be a string intended to be resolved from the CallbackResolver
f2724fd2 Revert the CS fix and make a valid docComment
ca9fe47b More CS fixes
4e6591c3 CS Fixes
974b49d1 Allow view listeners to be "skipped" by returning null
50150276 Restricted visibility
c86b6f59 Fix docblock type
a7768253 Remove callable hint for 5.3 tests
65a97117 Avoid mutating state
f31bb299 Fix test
3a8e2dd5 Remove callable type hint from chain test for 5.3
a940fbe3 Clarify docblock
0bce9898 Add request to signature to test call
6bddf29e Add ViewListenerWrapper and tests
86c02016 Add view.
parents 77f308ca fa14af74
...@@ -546,6 +546,48 @@ early:: ...@@ -546,6 +546,48 @@ early::
return new Response(...); return new Response(...);
}); });
View Handlers
-------------
View Handlers allow you to intercept a controller result that is not a
``Response`` and transform it before it gets returned to the kernel.
To register a view handler, pass a callable (or string that can be resolved to a
callable) to the view method. The callable should accept some sort of result
from the controller::
$app->view(function (array $controllerResult) use ($app) {
return $app->json($controllerResult);
});
View Handlers also receive the ``Request`` as their second argument,
making them a good candidate for basic content negotiation::
$app->view(function (array $controllerResult, Request $request) use ($app) {
$acceptHeader = $request->headers->get('Accept');
$bestFormat = $app['negotiator']->getBestFormat($acceptHeader, array('json', 'xml'));
if ('json' === $bestFormat) {
return new JsonResponse($controllerResult);
}
if ('xml' === $bestFormat) {
return $app['serializer.xml']->renderResponse($controllerResult);
}
return $controllerResult;
});
View Handlers will be examined in the order they are added to the application
and Silex will use type hints to determine if a view handler should be used for
the current result, continously using the return value of the last view handler
as the input for the next.
.. note::
You must ensure that Silex receives a ``Response`` or a string as the result of
the last view handler (or controller) to be run.
Redirects Redirects
--------- ---------
......
...@@ -90,7 +90,7 @@ class Application extends \Pimple implements HttpKernelInterface, TerminableInte ...@@ -90,7 +90,7 @@ class Application extends \Pimple implements HttpKernelInterface, TerminableInte
$this['dispatcher_class'] = 'Symfony\\Component\\EventDispatcher\\EventDispatcher'; $this['dispatcher_class'] = 'Symfony\\Component\\EventDispatcher\\EventDispatcher';
$this['dispatcher'] = $this->share(function () use ($app) { $this['dispatcher'] = $this->share(function () use ($app) {
/** /**
* @var EventDispatcherInterface * @var EventDispatcherInterface $dispatcher
*/ */
$dispatcher = new $app['dispatcher_class'](); $dispatcher = new $app['dispatcher_class']();
...@@ -417,6 +417,23 @@ class Application extends \Pimple implements HttpKernelInterface, TerminableInte ...@@ -417,6 +417,23 @@ class Application extends \Pimple implements HttpKernelInterface, TerminableInte
$this->on(KernelEvents::EXCEPTION, new ExceptionListenerWrapper($this, $callback), $priority); $this->on(KernelEvents::EXCEPTION, new ExceptionListenerWrapper($this, $callback), $priority);
} }
/**
* Registers a view handler.
*
* View handlers are simple callables which take a controller result and the
* request as arguments, whenever a controller returns a value that is not
* an instance of Response. When this occurs, all suitable handlers will be
* called, until one returns a Response object.
*
* @param mixed $callback View handler callback
* @param int $priority The higher this value, the earlier an event
* listener will be triggered in the chain (defaults to 0)
*/
public function view($callback, $priority = 0)
{
$this->on(KernelEvents::VIEW, new ViewListenerWrapper($this, $callback), $priority);
}
/** /**
* Flushes the controller collection. * Flushes the controller collection.
* *
......
<?php
/*
* This file is part of the Silex framework.
*
* (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 Silex;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
/**
* Wraps view listeners.
*
* @author Dave Marshall <dave@atst.io>
*/
class ViewListenerWrapper
{
private $app;
private $callback;
/**
* Constructor.
*
* @param Application $app An Application instance
* @param mixed $callback
*/
public function __construct(Application $app, $callback)
{
$this->app = $app;
$this->callback = $callback;
}
public function __invoke(GetResponseForControllerResultEvent $event)
{
$controllerResult = $event->getControllerResult();
$callback = $this->app['callback_resolver']->resolveCallback($this->callback);
if (!$this->shouldRun($callback, $controllerResult)) {
return;
}
$response = call_user_func($callback, $controllerResult, $event->getRequest());
if ($response instanceof Response) {
$event->setResponse($response);
} elseif (null !== $response) {
$event->setControllerResult($response);
}
}
private function shouldRun($callback, $controllerResult)
{
if (is_array($callback)) {
$callbackReflection = new \ReflectionMethod($callback[0], $callback[1]);
} elseif (is_object($callback) && !$callback instanceof \Closure) {
$callbackReflection = new \ReflectionObject($callback);
$callbackReflection = $callbackReflection->getMethod('__invoke');
} else {
$callbackReflection = new \ReflectionFunction($callback);
}
if ($callbackReflection->getNumberOfParameters() > 0) {
$parameters = $callbackReflection->getParameters();
$expectedControllerResult = $parameters[0];
if ($expectedControllerResult->getClass() && (!is_object($controllerResult) || !$expectedControllerResult->getClass()->isInstance($controllerResult))) {
return false;
}
if ($expectedControllerResult->isArray() && !is_array($controllerResult)) {
return false;
}
if (method_exists($expectedControllerResult, 'isCallable') && $expectedControllerResult->isCallable() && !is_callable($controllerResult)) {
return false;
}
}
return true;
}
}
...@@ -559,6 +559,127 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase ...@@ -559,6 +559,127 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$response = $app->handle(Request::create('/foo')); $response = $app->handle(Request::create('/foo'));
$this->assertEquals(301, $response->getStatusCode()); $this->assertEquals(301, $response->getStatusCode());
} }
public function testBeforeFilterOnMountedControllerGroupIsolatedToGroup()
{
$app = new Application();
$app->match('/', function () { return new Response('ok'); });
$mounted = $app['controllers_factory'];
$mounted->before(function () { return new Response('not ok'); });
$app->mount('/group', $mounted);
$response = $app->handle(Request::create('/'));
$this->assertEquals('ok', $response->getContent());
}
public function testViewListenerWithPrimitive()
{
$app = new Application();
$app->get('/foo', function () { return 123; });
$app->view(function ($view, Request $request) {
return new Response($view);
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('123', $response->getContent());
}
public function testViewListenerWithArrayTypeHint()
{
$app = new Application();
$app->get('/foo', function () { return array('ok'); });
$app->view(function (array $view) {
return new Response($view[0]);
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('ok', $response->getContent());
}
public function testViewListenerWithObjectTypeHint()
{
$app = new Application();
$app->get('/foo', function () { return (object) array('name' => 'world'); });
$app->view(function (\stdClass $view) {
return new Response('Hello '.$view->name);
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('Hello world', $response->getContent());
}
/**
* @requires PHP 5.4
*/
public function testViewListenerWithCallableTypeHint()
{
$app = new Application();
$app->get('/foo', function () { return function () { return 'world'; }; });
$app->view(function (callable $view) {
return new Response('Hello '.$view());
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('Hello world', $response->getContent());
}
public function testViewListenersCanBeChained()
{
$app = new Application();
$app->get('/foo', function () { return (object) array('name' => 'world'); });
$app->view(function (\stdClass $view) {
return array('msg' => 'Hello '.$view->name);
});
$app->view(function (array $view) {
return $view['msg'];
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('Hello world', $response->getContent());
}
public function testViewListenersAreIgnoredIfNotSuitable()
{
$app = new Application();
$app->get('/foo', function () { return 'Hello world'; });
$app->view(function (\stdClass $view) {
throw new \Exception('View listener was called');
});
$app->view(function (array $view) {
throw new \Exception('View listener was called');
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('Hello world', $response->getContent());
}
public function testViewListenersResponsesAreNotUsedIfNull()
{
$app = new Application();
$app->get('/foo', function () { return 'Hello world'; });
$app->view(function ($view) {
return 'Hello view listener';
});
$app->view(function ($view) {
return;
});
$response = $app->handle(Request::create('/foo'));
$this->assertEquals('Hello view listener', $response->getContent());
}
} }
class FooController class FooController
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment