Commit 0cdef8e5 authored by Romain Neutron's avatar Romain Neutron Committed by Fabien Potencier

Converters as service

parent 86fb50c1
......@@ -5,6 +5,7 @@ Changelog
------------------
* Only convert attributes on the request that actually exist.
* Add support for using a service method as a converter.
1.1.0 (2013-07-04)
------------------
......
......@@ -349,6 +349,42 @@ The converter callback also receives the ``Request`` as its second argument::
// ...
})->convert('post', $callback);
A converter can also be defined as a service. For example, here is a user
converter based on Doctrine ObjectManager::
use Doctrine\Common\Persistence\ObjectManager
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class UserConverter
{
private $om;
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function convert($id)
{
if (null === $user = $this->om->find('User', (int) $id)) {
throw new NotFoundHttpException(sprintf('User %d does not exist', $id));
}
return $user;
}
}
The service will now be registered in the application, and the
convert method will be used as converter::
$app['converter.user'] = $app->share(function () {
return new UserConverter();
});
$app->get('/user/{user}', function (User $user) {
// ...
})->convert('user', 'converter.user:convert');
Requirements
~~~~~~~~~~~~
......
......@@ -100,12 +100,16 @@ class Application extends \Pimple implements HttpKernelInterface, TerminableInte
}
$dispatcher->addSubscriber(new ResponseListener($app['charset']));
$dispatcher->addSubscriber(new MiddlewareListener($app));
$dispatcher->addSubscriber(new ConverterListener($app['routes']));
$dispatcher->addSubscriber(new ConverterListener($app['routes'], $app['callback_resolver']));
$dispatcher->addSubscriber(new StringToResponseListener());
return $dispatcher;
});
$this['callback_resolver'] = $this->share(function () use ($app) {
return new CallbackResolver($app);
});
$this['resolver'] = $this->share(function () use ($app) {
return new ControllerResolver($app, $app['logger']);
});
......
<?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;
class CallbackResolver
{
const SERVICE_PATTERN = "/[A-Za-z0-9\._\-]+:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/";
private $app;
public function __construct(\Pimple $app)
{
$this->app = $app;
}
/**
* Returns true if the string is a valid service method representation.
*
* @param string $name
*
* @return Boolean
*/
public function isValid($name)
{
return is_string($name) && preg_match(static::SERVICE_PATTERN, $name);
}
/**
* Returns a callable given its string representation.
*
* @param string $name
*
* @return array A callable array
*
* @throws \InvalidArgumentException In case the method does not exist.
*/
public function getCallback($name)
{
list($service, $method) = explode(':', $name, 2);
if (!isset($this->app[$service])) {
throw new \InvalidArgumentException(sprintf('Service "%s" does not exist.', $service));
}
return array($this->app[$service], $method);
}
}
......@@ -11,6 +11,7 @@
namespace Silex\EventListener;
use Silex\CallbackResolver;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -24,15 +25,17 @@ use Symfony\Component\Routing\RouteCollection;
class ConverterListener implements EventSubscriberInterface
{
protected $routes;
protected $callbackResolver;
/**
* Constructor.
*
* @param RouteCollection $routes A RouteCollection instance
*/
public function __construct(RouteCollection $routes)
public function __construct(RouteCollection $routes, CallbackResolver $callbackResolver)
{
$this->routes = $routes;
$this->callbackResolver = $callbackResolver;
}
/**
......@@ -47,6 +50,7 @@ class ConverterListener implements EventSubscriberInterface
if ($route && $converters = $route->getOption('_converters')) {
foreach ($converters as $name => $callback) {
if ($request->attributes->has($name)) {
$callback = $this->callbackResolver->isValid($callback) ? $this->callbackResolver->getCallback($callback) : $callback;
$request->attributes->set($name, call_user_func($callback, $request->attributes->get($name), $request));
}
}
......
......@@ -20,7 +20,7 @@ class ServiceControllerServiceProvider implements ServiceProviderInterface
public function register(Application $app)
{
$app['resolver'] = $app->share($app->extend('resolver', function ($resolver, $app) {
return new ServiceControllerResolver($resolver, $app);
return new ServiceControllerResolver($resolver, $app['callback_resolver']);
}));
}
......
......@@ -11,7 +11,6 @@
namespace Silex;
use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
......@@ -22,21 +21,19 @@ use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
*/
class ServiceControllerResolver implements ControllerResolverInterface
{
const SERVICE_PATTERN = "/[A-Za-z0-9\._\-]+:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/";
protected $resolver;
protected $app;
protected $controllerResolver;
protected $callbackResolver;
/**
* Constructor.
*
* @param ControllerResolverInterface $resolver A ControllerResolverInterface instance to delegate to
* @param Application $app An Application instance
* @param ControllerResolverInterface $controllerResolver A ControllerResolverInterface instance to delegate to
* @param CallbackResolver $callbackResolver A service resolver instance
*/
public function __construct(ControllerResolverInterface $resolver, Application $app)
public function __construct(ControllerResolverInterface $controllerResolver, CallbackResolver $callbackResolver)
{
$this->resolver = $resolver;
$this->app = $app;
$this->controllerResolver = $controllerResolver;
$this->callbackResolver = $callbackResolver;
}
/**
......@@ -46,17 +43,11 @@ class ServiceControllerResolver implements ControllerResolverInterface
{
$controller = $request->attributes->get('_controller', null);
if (!is_string($controller) || !preg_match(static::SERVICE_PATTERN, $controller)) {
return $this->resolver->getController($request);
}
list($service, $method) = explode(':', $controller, 2);
if (!isset($this->app[$service])) {
throw new \InvalidArgumentException(sprintf('Service "%s" does not exist.', $service));
if (!$this->callbackResolver->isValid($controller)) {
return $this->controllerResolver->getController($request);
}
return array($this->app[$service], $method);
return $this->callbackResolver->getCallback($controller);
}
/**
......@@ -64,6 +55,6 @@ class ServiceControllerResolver implements ControllerResolverInterface
*/
public function getArguments(Request $request, $controller)
{
return $this->resolver->getArguments($request, $controller);
return $this->controllerResolver->getArguments($request, $controller);
}
}
<?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\Tests;
use Silex\CallbackResolver;
class CallbackResolverTest extends \PHPUnit_Framework_Testcase
{
public function setup()
{
$this->app = new \Pimple();
$this->resolver = new CallbackResolver($this->app);
}
public function testShouldResolveCallback()
{
$this->app['some_service'] = function() { return new \stdClass(); };
$this->assertTrue($this->resolver->isValid('some_service:methodName'));
$this->assertEquals(
array($this->app['some_service'], 'methodName'),
$this->resolver->getCallback('some_service:methodName')
);
}
public function testNonStringsAreNotValid()
{
$this->assertFalse($this->resolver->isValid(null));
$this->assertFalse($this->resolver->isValid('some_service::methodName'));
}
/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Service "some_service" does not exist.
*/
public function testShouldThrowAnExceptionIfServiceIsMissing()
{
$this->resolver->getCallback('some_service:methodName');
}
}
......@@ -26,71 +26,58 @@ class ServiceControllerResolverTest extends \PHPUnit_Framework_Testcase
$this->mockResolver = $this->getMockBuilder('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface')
->disableOriginalConstructor()
->getMock();
$this->mockCallbackResolver = $this->getMockBuilder('Silex\CallbackResolver')
->disableOriginalConstructor()
->getMock();
$this->app = new Application();
$this->resolver = new ServiceControllerResolver($this->mockResolver, $this->app);
$this->resolver = new ServiceControllerResolver($this->mockResolver, $this->mockCallbackResolver);
}
public function testShouldResolveServiceController()
{
$this->app['some_service'] = function() { return new \stdClass(); };
$this->mockCallbackResolver->expects($this->once())
->method('isValid')
->will($this->returnValue(true));
$req = Request::create('/');
$req->attributes->set('_controller', 'some_service:methodName');
$this->mockCallbackResolver->expects($this->once())
->method('getCallback')
->with('some_service:methodName')
->will($this->returnValue(array('callback')));
$this->assertEquals(
array($this->app['some_service'], 'methodName'),
$this->resolver->getController($req)
);
}
$this->app['some_service'] = function() { return new \stdClass(); };
public function testShouldDelegateNonStrings()
{
$req = Request::create('/');
$req->attributes->set('_controller', function() {});
$this->mockResolver->expects($this->once())
->method('getController')
->with($req)
->will($this->returnValue(123));
$req->attributes->set('_controller', 'some_service:methodName');
$this->assertEquals(123, $this->resolver->getController($req));
$this->assertEquals(array('callback'), $this->resolver->getController($req));
}
/**
* Note: This doesn't test the regex extensively, just a common use case
*/
public function testShouldDelegateNonMatchingSyntax()
public function testShouldUnresolvedControllerNames()
{
$req = Request::create('/');
$req->attributes->set('_controller', 'some_class::methodName');
$this->mockCallbackResolver->expects($this->once())
->method('isValid')
->with('some_class::methodName')
->will($this->returnValue(false));
$this->mockResolver->expects($this->once())
->method('getController')
->with($req)
->will($this->returnValue(123));
->method('getController')
->with($req)
->will($this->returnValue(123));
$this->assertEquals(123, $this->resolver->getController($req));
}
/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Service "some_service" does not exist.
*/
public function testShouldThrowIfServiceIsMissing()
{
$req = Request::create('/');
$req->attributes->set('_controller', 'some_service:methodName');
$this->resolver->getController($req);
}
public function testShouldDelegateGetArguments()
{
$req = Request::create('/');
$this->mockResolver->expects($this->once())
->method('getArguments')
->with($req)
->will($this->returnValue(123));
->method('getArguments')
->with($req)
->will($this->returnValue(123));
$this->assertEquals(123, $this->resolver->getArguments($req, function() {}));
}
......
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