Browse Source

refactored the controller resolver (+ made a small routing optimization)

Fabien Potencier 15 năm trước cách đây
mục cha
commit
485400dd51

+ 131 - 0
src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameConverter.php

@@ -0,0 +1,131 @@
+<?php
+
+namespace Symfony\Bundle\FrameworkBundle\Controller;
+
+use Symfony\Framework\Kernel;
+use Symfony\Components\HttpKernel\LoggerInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * ControllerNameConverter converts controller from the short notation a:b:c
+ * (BlogBundle:Post:index) to a fully-qualified class::method string
+ * (Bundle\BlogBundle\Controller\PostController::indexAction); and the other
+ * way around.
+ *
+ * @author Fabien Potencier <fabien.potencier@symfony-project.com>
+ */
+class ControllerNameConverter
+{
+    protected $kernel;
+    protected $logger;
+    protected $namespaces;
+
+    /**
+     * Constructor.
+     *
+     * @param Kernel          $kernel A Kernel instance
+     * @param LoggerInterface $logger A LoggerInterface instance
+     */
+    public function __construct(Kernel $kernel, LoggerInterface $logger = null)
+    {
+        $this->kernel = $kernel;
+        $this->logger = $logger;
+        $this->namespaces = array_keys($kernel->getBundleDirs());
+    }
+
+    /**
+     * Converts a class::method string to the short notation a:b:c.
+     *
+     * @param string $controller A controler (class::method)
+     *
+     * @return string A short notation controller (a:b:c)
+     */
+    public function toShortNotation($controller)
+    {
+        if (2 != count($parts = explode('::', $controller))) {
+            throw new \InvalidArgumentException(sprintf('The "%s" controller is not a valid class::method controller string.', $controller));
+        }
+
+        list($class, $method) = $parts;
+
+        if (!preg_match('/Action$/', $method)) {
+            throw new \InvalidArgumentException(sprintf('The "%s::%s" method does not look like a controller action (it does not end with Action)', $class, $method));
+        }
+        $action = substr($method, 0, -6);
+
+        if (!preg_match('/Controller\\\(.*)Controller$/', $class, $match)) {
+            throw new \InvalidArgumentException(sprintf('The "%s" class does not look like a controller class (it does not end with Controller)', $class));
+        }
+        $controller = $match[1];
+
+        $bundle = null;
+        $namespace = substr($class, 0, strrpos($class, '\\'));
+        foreach ($this->namespaces as $prefix) {
+            if (0 === $pos = strpos($namespace, $prefix)) {
+                // -11 to remove the \Controller suffix (11 characters)
+                $bundle = substr($namespace, strlen($prefix) + 1, -11);
+            }
+        }
+
+        if (null === $bundle) {
+            throw new \InvalidArgumentException(sprintf('The "%s" class does not belong to a known bundle namespace.', $class));
+        }
+
+        return $bundle.':'.$controller.':'.$action;
+    }
+
+    /**
+     * Converts a short notation a:b:c ro a class::method.
+     *
+     * @param string $controller A short notation controller (a:b:c)
+     *
+     * @param string A controler (class::method)
+     */
+    public function fromShortNotation($controller)
+    {
+        if (3 != count($parts = explode(':', $controller))) {
+            throw new \InvalidArgumentException(sprintf('The "%s" controller is not a valid a:b:c controller string.', $controller));
+        }
+
+        list($bundle, $controller, $action) = $parts;
+        $bundle = strtr($bundle, array('/' => '\\'));
+        $class = null;
+        $logs = array();
+        foreach ($this->namespaces as $namespace) {
+            $try = $namespace.'\\'.$bundle.'\\Controller\\'.$controller.'Controller';
+            if (!class_exists($try)) {
+                if (null !== $this->logger) {
+                    $logs[] = sprintf('Failed finding controller "%s:%s" from namespace "%s" (%s)', $bundle, $controller, $namespace, $try);
+                }
+            } else {
+                if (!$this->kernel->isClassInActiveBundle($try)) {
+                    throw new \LogicException(sprintf('To use the "%s" controller, you first need to enable the Bundle "%s" in your Kernel class.', $try, $namespace.'\\'.$bundle));
+                }
+
+                $class = $try;
+
+                break;
+            }
+        }
+
+        if (null === $class) {
+            if (null !== $this->logger) {
+                foreach ($logs as $log) {
+                    $this->logger->info($log);
+                }
+            }
+
+            throw new \InvalidArgumentException(sprintf('Unable to find controller "%s:%s".', $bundle, $controller));
+        }
+
+        return $class.'::'.$action.'Action';
+    }
+}

+ 33 - 102
src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php

@@ -3,7 +3,7 @@
 namespace Symfony\Bundle\FrameworkBundle\Controller;
 
 use Symfony\Components\HttpKernel\LoggerInterface;
-use Symfony\Components\HttpKernel\Controller\ControllerResolverInterface;
+use Symfony\Components\HttpKernel\Controller\ControllerResolver as BaseControllerResolver;
 use Symfony\Components\HttpKernel\HttpKernelInterface;
 use Symfony\Components\HttpFoundation\Request;
 use Symfony\Components\EventDispatcher\Event;
@@ -21,19 +21,47 @@ use Symfony\Components\DependencyInjection\ContainerInterface;
 /**
  * ControllerResolver.
  *
- * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  */
-class ControllerResolver implements ControllerResolverInterface
+class ControllerResolver extends BaseControllerResolver
 {
     protected $container;
-    protected $logger;
     protected $esiSupport;
 
+    /**
+     * Constructor.
+     *
+     * @param ContainerInterface $container A ContainerInterface instance
+     * @param LoggerInterface    $logger    A LoggerInterface instance
+     */
     public function __construct(ContainerInterface $container, LoggerInterface $logger = null)
     {
         $this->container = $container;
-        $this->logger = $logger;
         $this->esiSupport = $container->has('esi') && $container->getEsiService()->hasSurrogateEsiCapability($container->getRequestService());
+
+        parent::__construct($logger);
+    }
+
+    /**
+     * Returns a callable for the given controller.
+     *
+     * @param string $controller A Controller string
+     *
+     * @return mixed A PHP callable
+     */
+    protected function createController($controller)
+    {
+        if (false === strpos($controller, '::')) {
+            throw new \InvalidArgumentException(sprintf('Unable to find controller "%s".', $controller));
+        }
+
+        list($class, $method) = explode('::', $controller);
+
+        if (!class_exists($class)) {
+            throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
+        }
+
+        return array(new $class($this->container), $method);
     }
 
     /**
@@ -111,103 +139,6 @@ class ControllerResolver implements ControllerResolverInterface
         }
     }
 
-    /**
-     * Returns the Controller instance associated with a Request.
-     *
-     * This method looks for a '_controller' request parameter that represents
-     * the controller name (a string like BlogBundle:Post:index).
-     *
-     * @param Request $request A Request instance
-     *
-     * @return mixed|Boolean A PHP callable representing the Controller,
-     *                       or false if this resolver is not able to determine the controller
-     *
-     * @throws \InvalidArgumentException|\LogicException If the controller can't be found
-     */
-    public function getController(Request $request)
-    {
-        if (!$controller = $request->attributes->get('_controller')) {
-            if (null !== $this->logger) {
-                $this->logger->err('Unable to look for the controller as the "_controller" parameter is missing');
-            }
-
-            return false;
-        }
-
-        list($bundle, $controller, $action) = explode(':', $controller);
-        $bundle = strtr($bundle, array('/' => '\\'));
-        $class = null;
-        $logs = array();
-        foreach (array_keys($this->container->getParameter('kernel.bundle_dirs')) as $namespace) {
-            $try = $namespace.'\\'.$bundle.'\\Controller\\'.$controller.'Controller';
-            if (!class_exists($try)) {
-                if (null !== $this->logger) {
-                    $logs[] = sprintf('Failed finding controller "%s:%s" from namespace "%s" (%s)', $bundle, $controller, $namespace, $try);
-                }
-            } else {
-                if (!in_array($namespace.'\\'.$bundle.'\\'.strtr($bundle, array('\\' => '')), array_map(function ($bundle) { return get_class($bundle); }, $this->container->getKernelService()->getBundles()))) {
-                    throw new \LogicException(sprintf('To use the "%s" controller, you first need to enable the Bundle "%s" in your Kernel class.', $try, $namespace.'\\'.$bundle));
-                }
-
-                $class = $try;
-
-                break;
-            }
-        }
-
-        if (null === $class) {
-            if (null !== $this->logger) {
-                foreach ($logs as $log) {
-                    $this->logger->info($log);
-                }
-            }
-
-            throw new \InvalidArgumentException(sprintf('Unable to find controller "%s:%s".', $bundle, $controller));
-        }
-
-        $controller = new $class($this->container);
-
-        $method = $action.'Action';
-        if (!method_exists($controller, $method)) {
-            throw new \InvalidArgumentException(sprintf('Method "%s::%s" does not exist.', $class, $method));
-        }
-
-        if (null !== $this->logger) {
-            $this->logger->info(sprintf('Using controller "%s::%s"%s', $class, $method, isset($file) ? sprintf(' from file "%s"', $file) : ''));
-        }
-
-        return array($controller, $method);
-    }
-
-    /**
-     * Returns the arguments to pass to the controller.
-     *
-     * @param Request $request    A Request instance
-     * @param mixed   $controller A PHP callable
-     *
-     * @throws \RuntimeException When value for argument given is not provided
-     */
-    public function getArguments(Request $request, $controller)
-    {
-        $attributes = $request->attributes->all();
-
-        list($controller, $method) = $controller;
-
-        $r = new \ReflectionObject($controller);
-        $arguments = array();
-        foreach ($r->getMethod($method)->getParameters() as $param) {
-            if (array_key_exists($param->getName(), $attributes)) {
-                $arguments[] = $attributes[$param->getName()];
-            } elseif ($param->isDefaultValueAvailable()) {
-                $arguments[] = $param->getDefaultValue();
-            } else {
-                throw new \RuntimeException(sprintf('Controller "%s::%s()" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', get_class($controller), $method, $param->getName()));
-            }
-        }
-
-        return $arguments;
-    }
-
     /**
      * Generates an internal URI for a given controller.
      *

+ 3 - 1
src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml

@@ -6,7 +6,7 @@
 
     <parameters>
         <parameter key="router.class">Symfony\Components\Routing\Router</parameter>
-        <parameter key="routing.loader.class">Symfony\Components\Routing\Loader\DelegatingLoader</parameter>
+        <parameter key="routing.loader.class">Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader</parameter>
         <parameter key="routing.resolver.class">Symfony\Bundle\FrameworkBundle\Routing\LoaderResolver</parameter>
         <parameter key="routing.loader.xml.class">Symfony\Components\Routing\Loader\XmlFileLoader</parameter>
         <parameter key="routing.loader.yml.class">Symfony\Components\Routing\Loader\YamlFileLoader</parameter>
@@ -34,6 +34,8 @@
         </service>
 
         <service id="routing.loader" class="%routing.loader.class%">
+            <argument type="service" id="controller_name_converter" />
+            <argument type="service" id="logger" on-invalid="null" />
             <argument type="service" id="routing.resolver" />
         </service>
 

+ 7 - 1
src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml

@@ -7,14 +7,20 @@
     <parameters>
         <parameter key="request_listener.class">Symfony\Bundle\FrameworkBundle\RequestListener</parameter>
         <parameter key="controller_resolver.class">Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver</parameter>
+        <parameter key="controller_name_converter.class">Symfony\Bundle\FrameworkBundle\Controller\ControllerNameConverter</parameter>
         <parameter key="response_listener.class">Symfony\Components\HttpKernel\ResponseListener</parameter>
         <parameter key="exception_listener.class">Symfony\Bundle\FrameworkBundle\Controller\ExceptionListener</parameter>
-        <parameter key="exception_listener.controller">FrameworkBundle:Exception:exception</parameter>
+        <parameter key="exception_listener.controller">Symfony\Bundle\FrameworkBundle\Controller\ExceptionController::exceptionAction</parameter>
         <parameter key="esi.class">Symfony\Components\HttpKernel\Cache\Esi</parameter>
         <parameter key="esi_listener.class">Symfony\Components\HttpKernel\Cache\EsiListener</parameter>
     </parameters>
 
     <services>
+        <service id="controller_name_converter" class="%controller_name_converter.class%">
+            <argument type="service" id="kernel" />
+            <argument type="service" id="logger" on-invalid="ignore" />
+        </service>
+
         <service id="controller_resolver" class="%controller_resolver.class%">
             <argument type="service" id="service_container" />
             <argument type="service" id="logger" on-invalid="ignore" />

+ 72 - 0
src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace Symfony\Bundle\FrameworkBundle\Routing;
+
+use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameConverter;
+use Symfony\Components\Routing\Loader\DelegatingLoader as BaseDelegatingLoader;
+use Symfony\Components\Routing\Loader\LoaderResolverInterface;
+use Symfony\Components\HttpKernel\LoggerInterface;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * DelegatingLoader delegates route loading to other loaders using a loader resolver.
+ *
+ * This implementation resolves the _controller attribute from the short notation
+ * to the fully-qualified form (from a:b:c to class:method).
+ *
+ * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
+ */
+class DelegatingLoader extends BaseDelegatingLoader
+{
+    protected $converter;
+    protected $logger;
+
+    /**
+     * Constructor.
+     *
+     * @param ControllerNameConverter $converter A ControllerNameConverter instance
+     * @param LoggerInterface         $logger    A LoggerInterface instance
+     * @param LoaderResolverInterface $resolver  A LoaderResolverInterface instance
+     */
+    public function __construct(ControllerNameConverter $converter, LoggerInterface $logger = null, LoaderResolverInterface $resolver)
+    {
+        $this->converter = $converter;
+        $this->logger = $logger;
+
+        parent::__construct($resolver);
+    }
+
+    /**
+     * Loads a resource.
+     *
+     * @param  mixed $resource A resource
+     *
+     * @return RouteCollection A RouteCollection instance
+     */
+    public function load($resource)
+    {
+        $collection = parent::load($resource);
+
+        foreach ($collection->getRoutes() as $name => $route) {
+            if ($controller = $route->getDefault('_controller')) {
+                try {
+                    $controller = $this->converter->fromShortNotation($controller);
+                } catch (\Exception $e) {
+                    throw new \RuntimeException(sprintf('%s (for route "%s" in resource "%s")', $e->getMessage(), $name, is_string($resource) ? $resource : 'RESOURCE'), $e->getCode(), $e);
+                }
+
+                $route->setDefault('_controller', $controller);
+            }
+        }
+
+        return $collection;
+    }
+}

+ 84 - 0
src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameConverterTest.php

@@ -0,0 +1,84 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\Tests\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
+use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameConverter;
+use Symfony\Bundle\FrameworkBundle\Tests\Logger;
+use Symfony\Bundle\FrameworkBundle\Tests\Kernel;
+
+require_once __DIR__.'/../Kernel.php';
+require_once __DIR__.'/../Logger.php';
+
+class ControllerNameConverterTest extends TestCase
+{
+    public function testToShortNotation()
+    {
+        $kernel = new Kernel();
+        $kernel->boot();
+        $converter = new ControllerNameConverter($kernel);
+
+        $this->assertEquals('FooBundle:Foo:index', $converter->toShortNotation('Symfony\Bundle\FooBundle\Controller\FooController::indexAction'), '->toShortNotation() converts a class::method string to the short a:b:c notation');
+
+        try {
+            $converter->toShortNotation('foo');
+            $this->fail('->toShortNotation() throws an \InvalidArgumentException if the controller is not a class::method string');
+        } catch (\Exception $e) {
+            $this->assertInstanceOf('\InvalidArgumentException', $e, '->toShortNotation() throws an \InvalidArgumentException if the controller is not a class::method string');
+        }
+
+        try {
+            $converter->toShortNotation('Symfony\Bundle\FooBundle\Controller\FooController::bar');
+            $this->fail('->toShortNotation() throws an \InvalidArgumentException if the method does not end with Action');
+        } catch (\Exception $e) {
+            $this->assertInstanceOf('\InvalidArgumentException', $e, '->toShortNotation() throws an \InvalidArgumentException if the method does not end with Action');
+        }
+
+        try {
+            $converter->toShortNotation('Symfony\Bundle\FooBundle\FooController::barAction');
+            $this->fail('->toShortNotation() throws an \InvalidArgumentException if the class does not end with Controller');
+        } catch (\Exception $e) {
+            $this->assertInstanceOf('\InvalidArgumentException', $e, '->toShortNotation() throws an \InvalidArgumentException if the class does not end with Controller');
+        }
+
+        try {
+            $converter->toShortNotation('FooBar\Bundle\FooBundle\Controller\FooController::barAction');
+            $this->fail('->toShortNotation() throws an \InvalidArgumentException if the class does not belongs to a known namespace');
+        } catch (\Exception $e) {
+            $this->assertInstanceOf('\InvalidArgumentException', $e, '->toShortNotation() throws an \InvalidArgumentException if the class does not belongs to a known namespace');
+        }
+    }
+
+    public function testFromShortNotation()
+    {
+        $kernel = new Kernel();
+        $kernel->boot();
+        $logger = new Logger();
+        $converter = new ControllerNameConverter($kernel, $logger);
+
+        $this->assertEquals('Symfony\Bundle\FrameworkBundle\Controller\DefaultController::indexAction', $converter->fromShortNotation('FrameworkBundle:Default:index'), '->fromShortNotation() converts a short a:b:c notation string to a class::method string');
+
+        try {
+            $converter->fromShortNotation('foo:');
+            $this->fail('->fromShortNotation() throws an \InvalidArgumentException if the controller is not an a:b:c string');
+        } catch (\Exception $e) {
+            $this->assertInstanceOf('\InvalidArgumentException', $e, '->toShortNotation() throws an \InvalidArgumentException if the controller is not an a:b:c string');
+        }
+
+        try {
+            $converter->fromShortNotation('FooBundle:Default:index');
+            $this->fail('->fromShortNotation() throws a \InvalidArgumentException if the class is found but does not exist');
+        } catch (\Exception $e) {
+            $this->assertInstanceOf('\InvalidArgumentException', $e, '->fromShortNotation() throws a \LogicException if the class is found but does not exist');
+        }
+    }
+}

+ 61 - 0
src/Symfony/Bundle/FrameworkBundle/Tests/Kernel.php

@@ -0,0 +1,61 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\Tests;
+
+use Symfony\Framework\Kernel as BaseKernel;
+use Symfony\Components\DependencyInjection\Loader\LoaderInterface;
+use Symfony\Bundle\FrameworkBundle\Util\Filesystem;
+
+class Kernel extends BaseKernel
+{
+    public function __construct()
+    {
+        $this->tmpDir = sys_get_temp_dir().'/sf2_'.rand(1, 9999);
+
+        parent::__construct('env', true);
+    }
+
+    public function __destruct()
+    {
+        $fs = new Filesystem();
+        $fs->remove($this->tmpDir);
+    }
+
+    public function registerRootDir()
+    {
+        return $this->tmpDir;
+    }
+
+    public function registerBundles()
+    {
+        return array(
+            new \Symfony\Framework\KernelBundle(),
+            new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
+        );
+    }
+
+    public function registerBundleDirs()
+    {
+        return array(
+            'Application'     => __DIR__.'/../src/Application',
+            'Bundle'          => __DIR__.'/../src/Bundle',
+            'Symfony\\Bundle' => __DIR__.'/../src/vendor/symfony/src/Symfony/Bundle',
+        );
+    }
+
+    public function registerContainerConfiguration(LoaderInterface $loader)
+    {
+        $loader->load(function ($container) {
+            $container->setParameter('kernel.compiled_classes', array());
+        });
+    }
+}

+ 88 - 0
src/Symfony/Bundle/FrameworkBundle/Tests/Logger.php

@@ -0,0 +1,88 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\Tests;
+
+use Symfony\Components\HttpKernel\LoggerInterface;
+
+class Logger implements LoggerInterface
+{
+    protected $logs;
+
+    public function __construct()
+    {
+        $this->clear();
+    }
+
+    public function getLogs($priority = false)
+    {
+        return false === $priority ? $this->logs : $this->logs[$priority];
+    }
+
+    public function clear()
+    {
+        $this->logs = array(
+            'emerg' => array(),
+            'alert' => array(),
+            'crit' => array(),
+            'err' => array(),
+            'warn' => array(),
+            'notice' => array(),
+            'info' => array(),
+            'debug' => array(),
+        );
+    }
+
+    public function log($message, $priority)
+    {
+        $this->logs[$priority][] = $message;
+    }
+
+    public function emerg($message)
+    {
+        $this->log($message, 'emerg');
+    }
+
+    public function alert($message)
+    {
+        $this->log($message, 'alert');
+    }
+
+    public function crit($message)
+    {
+        $this->log($message, 'crit');
+    }
+
+    public function err($message)
+    {
+        $this->log($message, 'err');
+    }
+
+    public function warn($message)
+    {
+        $this->log($message, 'warn');
+    }
+
+    public function notice($message)
+    {
+        $this->log($message, 'notice');
+    }
+
+    public function info($message)
+    {
+        $this->log($message, 'info');
+    }
+
+    public function debug($message)
+    {
+        $this->log($message, 'debug');
+    }
+}

+ 126 - 0
src/Symfony/Components/HttpKernel/Controller/ControllerResolver.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace Symfony\Components\HttpKernel\Controller;
+
+use Symfony\Components\HttpKernel\LoggerInterface;
+use Symfony\Components\HttpFoundation\Request;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * ControllerResolver.
+ *
+ * This implementation uses the '_controller' request attribute to determine
+ * the controller to execute and uses the request attributes to determine
+ * the controller method arguments.
+ *
+ * @author Fabien Potencier <fabien.potencier@symfony-project.com>
+ */
+class ControllerResolver implements ControllerResolverInterface
+{
+    protected $logger;
+
+    /**
+     * Constructor.
+     *
+     * @param LoggerInterface $logger A LoggerInterface instance
+     */
+    public function __construct(LoggerInterface $logger = null)
+    {
+        $this->logger = $logger;
+    }
+
+    /**
+     * Returns the Controller instance associated with a Request.
+     *
+     * This method looks for a '_controller' request attribute that represents
+     * the controller name (a string like ClassName:::MethodName).
+     *
+     * @param Request $request A Request instance
+     *
+     * @return mixed|Boolean A PHP callable representing the Controller,
+     *                       or false if this resolver is not able to determine the controller
+     *
+     * @throws \InvalidArgumentException|\LogicException If the controller can't be found
+     */
+    public function getController(Request $request)
+    {
+        if (!$controller = $request->attributes->get('_controller')) {
+            if (null !== $this->logger) {
+                $this->logger->err('Unable to look for the controller as the "_controller" parameter is missing');
+            }
+
+            return false;
+        }
+
+        list($controller, $method) = $this->createController($controller);
+
+        if (!method_exists($controller, $method)) {
+            throw new \InvalidArgumentException(sprintf('Method "%s::%s" does not exist.', get_class($controller), $method));
+        }
+
+        if (null !== $this->logger) {
+            $this->logger->info(sprintf('Using controller "%s::%s"', get_class($controller), $method));
+        }
+
+        return array($controller, $method);
+    }
+
+    /**
+     * Returns the arguments to pass to the controller.
+     *
+     * @param Request $request    A Request instance
+     * @param mixed   $controller A PHP callable
+     *
+     * @throws \RuntimeException When value for argument given is not provided
+     */
+    public function getArguments(Request $request, $controller)
+    {
+        $attributes = $request->attributes->all();
+
+        list($controller, $method) = $controller;
+
+        $r = new \ReflectionObject($controller);
+        $arguments = array();
+        foreach ($r->getMethod($method)->getParameters() as $param) {
+            if (array_key_exists($param->getName(), $attributes)) {
+                $arguments[] = $attributes[$param->getName()];
+            } elseif ($param->isDefaultValueAvailable()) {
+                $arguments[] = $param->getDefaultValue();
+            } else {
+                throw new \RuntimeException(sprintf('Controller "%s::%s()" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', get_class($controller), $method, $param->getName()));
+            }
+        }
+
+        return $arguments;
+    }
+
+    /**
+     * Returns a callable for the given controller.
+     *
+     * @param string $controller A Controller string
+     *
+     * @return mixed A PHP callable
+     */
+    protected function createController($controller)
+    {
+        if (false === strpos($controller, '::')) {
+            throw new \InvalidArgumentException(sprintf('Unable to find controller "%s".', $controller));
+        }
+
+        list($class, $method) = explode('::', $controller);
+
+        if (!class_exists($class)) {
+            throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
+        }
+
+        return array(new $class(), $method);
+    }
+}

+ 105 - 0
tests/Symfony/Tests/Components/HttpKernel/Controller/ControllerResolverTest.php

@@ -0,0 +1,105 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Tests\Components\HttpKernel;
+
+use Symfony\Components\HttpKernel\Controller\ControllerResolver;
+use Symfony\Components\HttpFoundation\Request;
+
+require_once __DIR__.'/../Logger.php';
+
+class ControllerResolverTest extends \PHPUnit_Framework_TestCase
+{
+    public function testGetController()
+    {
+        $logger = new Logger();
+        $resolver = new ControllerResolver($logger);
+
+        $request = Request::create('/');
+        $this->assertFalse($resolver->getController($request), '->getController() returns false when the request has no _controller attribute');
+        $this->assertEquals(array('Unable to look for the controller as the "_controller" parameter is missing'), $logger->getLogs('err'));
+
+        $request->attributes->set('_controller', 'Symfony\Tests\Components\HttpKernel\ControllerResolverTest::testGetController');
+        $controller = $resolver->getController($request);
+        $this->assertInstanceOf('Symfony\Tests\Components\HttpKernel\ControllerResolverTest', $controller[0], '->getController() returns a PHP callable');
+        $this->assertEquals(array('Using controller "Symfony\Tests\Components\HttpKernel\ControllerResolverTest::testGetController"'), $logger->getLogs('info'));
+
+        $request->attributes->set('_controller', 'foo');
+        try {
+            $resolver->getController($request);
+            $this->fail('->getController() throws an \InvalidArgumentException if the _controller attribute is not well-formatted');
+        } catch (\Exception $e) {
+            $this->assertInstanceOf('\InvalidArgumentException', $e, '->getController() throws an \InvalidArgumentException if the _controller attribute is not well-formatted');
+        }
+
+        $request->attributes->set('_controller', 'foo::bar');
+        try {
+            $resolver->getController($request);
+            $this->fail('->getController() throws an \InvalidArgumentException if the _controller attribute contains a non-existent class');
+        } catch (\Exception $e) {
+            $this->assertInstanceOf('\InvalidArgumentException', $e, '->getController() throws an \InvalidArgumentException if the _controller attribute contains a non-existent class');
+        }
+
+        $request->attributes->set('_controller', 'Symfony\Tests\Components\HttpKernel\ControllerResolverTest::bar');
+        try {
+            $resolver->getController($request);
+            $this->fail('->getController() throws an \InvalidArgumentException if the _controller attribute contains a non-existent method');
+        } catch (\Exception $e) {
+            $this->assertInstanceOf('\InvalidArgumentException', $e, '->getController() throws an \InvalidArgumentException if the _controller attribute contains a non-existent method');
+        }
+    }
+
+    public function testGetArguments()
+    {
+        $resolver = new ControllerResolver();
+
+        $request = Request::create('/');
+        $controller = array(new self(), 'testGetArguments');
+        $this->assertEquals(array(), $resolver->getArguments($request, $controller), '->getArguments() returns an empty array if the method takes no arguments');
+
+        $request = Request::create('/');
+        $request->attributes->set('foo', 'foo');
+        $controller = array(new self(), 'controllerMethod1');
+        $this->assertEquals(array('foo'), $resolver->getArguments($request, $controller), '->getArguments() returns an array of arguments for the controller method');
+
+        $request = Request::create('/');
+        $request->attributes->set('foo', 'foo');
+        $controller = array(new self(), 'controllerMethod2');
+        $this->assertEquals(array('foo', null), $resolver->getArguments($request, $controller), '->getArguments() uses default values if present');
+
+        $request->attributes->set('bar', 'bar');
+        $this->assertEquals(array('foo', 'bar'), $resolver->getArguments($request, $controller), '->getArguments() overrides default values if provided in the request attributes');
+
+        $request = Request::create('/');
+        $request->attributes->set('foo', 'foo');
+        $request->attributes->set('foobar', 'foobar');
+        $controller = array(new self(), 'controllerMethod3');
+
+        try {
+            $resolver->getArguments($request, $controller);
+            $this->fail('->getArguments() throws a \RuntimeException exception if it cannot determine the argument value');
+        } catch (\Exception $e) {
+            $this->assertInstanceOf('\RuntimeException', $e, '->getArguments() throws a \RuntimeException exception if it cannot determine the argument value');
+        }
+    }
+
+    protected function controllerMethod1($foo)
+    {
+    }
+
+    protected function controllerMethod2($foo, $bar = null)
+    {
+    }
+
+    protected function controllerMethod3($foo, $bar = null, $foobar)
+    {
+    }
+}

+ 88 - 0
tests/Symfony/Tests/Components/HttpKernel/Logger.php

@@ -0,0 +1,88 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Tests\Components\HttpKernel;
+
+use Symfony\Components\HttpKernel\LoggerInterface;
+
+class Logger implements LoggerInterface
+{
+    protected $logs;
+
+    public function __construct()
+    {
+        $this->clear();
+    }
+
+    public function getLogs($priority = false)
+    {
+        return false === $priority ? $this->logs : $this->logs[$priority];
+    }
+
+    public function clear()
+    {
+        $this->logs = array(
+            'emerg' => array(),
+            'alert' => array(),
+            'crit' => array(),
+            'err' => array(),
+            'warn' => array(),
+            'notice' => array(),
+            'info' => array(),
+            'debug' => array(),
+        );
+    }
+
+    public function log($message, $priority)
+    {
+        $this->logs[$priority][] = $message;
+    }
+
+    public function emerg($message)
+    {
+        $this->log($message, 'emerg');
+    }
+
+    public function alert($message)
+    {
+        $this->log($message, 'alert');
+    }
+
+    public function crit($message)
+    {
+        $this->log($message, 'crit');
+    }
+
+    public function err($message)
+    {
+        $this->log($message, 'err');
+    }
+
+    public function warn($message)
+    {
+        $this->log($message, 'warn');
+    }
+
+    public function notice($message)
+    {
+        $this->log($message, 'notice');
+    }
+
+    public function info($message)
+    {
+        $this->log($message, 'info');
+    }
+
+    public function debug($message)
+    {
+        $this->log($message, 'debug');
+    }
+}