feature #21723 [Routing][DX] Add full route definition for invokable controller/class (yceruto)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[Routing][DX] Add full route definition for invokable controller/class

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| License       | MIT
| Doc PR        | _not yet_

Currently the [`@Route`][1] annotation can be set on the class (for global parameters only). This PR allows you to define the full route annotation for _single_ controllers on the class.

Here a common use case of [ADR pattern][3] applied to Symfony:

**Before:**
```
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route(service="AppBundle\Controller\Hello")
 */
class Hello
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * @Route("/hello/{name}", name="hello")
     */
    public function __invoke($name = 'World')
    {
        $this->logger->info('log entry...');

        return new Response(sprintf('Hello %s!', $name));
    }
}
```

**After:**
```
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route("/hello/{name}", name="hello", service="AppBundle\Controller\Hello")
 */
class Hello
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function __invoke($name = 'World')
    {
        $this->logger->info('log entry...');

        return new Response(sprintf('Hello %s!', $name));
    }
}
```

This feature does not break any behavior before and works under these conditions:
 * The class cannot contain other methods with `@Route` annotation (otherwise, this works as before: used for global parameters).
 *  <del>The class `@Route` must have the `name` option defined (otherwise, the route is ignored).</del> This one is auto-generated if `null`.
 * The class must be invokable: [`__invoke()` method][2] (otherwise, the route is ignored).

Btw, this PR fix the inconsistency with other route definitions (xml, yml) where the `_controller` parameter points to the class name only (i.e. without method).

  [1]: https://github.com/symfony/symfony/tree/master/src/Symfony/Component/Routing/Annotation/Route.php
  [2]: http://php.net/manual/en/language.oop5.magic.php#object.invoke
  [3]: https://github.com/pmjones/adr

Commits
-------

34e360ade3 Add full route definition to invokable class
This commit is contained in:
Fabien Potencier 2017-02-28 13:57:59 -08:00
commit 4a70919cea
4 changed files with 92 additions and 1 deletions

View File

@ -127,6 +127,11 @@ abstract class AnnotationClassLoader implements LoaderInterface
}
}
if (0 === $collection->count() && $class->hasMethod('__invoke') && $annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) {
$globals['path'] = '';
$this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke'));
}
return $collection;
}

View File

@ -0,0 +1,19 @@
<?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\Routing\Tests\Fixtures\AnnotatedClasses;
class BazClass
{
public function __invoke()
{
}
}

View File

@ -180,6 +180,73 @@ class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
$this->assertEquals(array_merge($classRouteData['methods'], $methodRouteData['methods']), $route->getMethods(), '->load merges class and method route methods');
}
public function testInvokableClassRouteLoad()
{
$classRouteData = array(
'name' => 'route1',
'path' => '/',
'schemes' => array('https'),
'methods' => array('GET'),
);
$this->reader
->expects($this->exactly(2))
->method('getClassAnnotation')
->will($this->returnValue($this->getAnnotatedRoute($classRouteData)))
;
$this->reader
->expects($this->once())
->method('getMethodAnnotations')
->will($this->returnValue(array()))
;
$routeCollection = $this->loader->load('Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BazClass');
$route = $routeCollection->get($classRouteData['name']);
$this->assertSame($classRouteData['path'], $route->getPath(), '->load preserves class route path');
$this->assertEquals(array_merge($classRouteData['schemes'], $classRouteData['schemes']), $route->getSchemes(), '->load preserves class route schemes');
$this->assertEquals(array_merge($classRouteData['methods'], $classRouteData['methods']), $route->getMethods(), '->load preserves class route methods');
}
public function testInvokableClassWithMethodRouteLoad()
{
$classRouteData = array(
'name' => 'route1',
'path' => '/prefix',
'schemes' => array('https'),
'methods' => array('GET'),
);
$methodRouteData = array(
'name' => 'route2',
'path' => '/path',
'schemes' => array('http'),
'methods' => array('POST', 'PUT'),
);
$this->reader
->expects($this->once())
->method('getClassAnnotation')
->will($this->returnValue($this->getAnnotatedRoute($classRouteData)))
;
$this->reader
->expects($this->once())
->method('getMethodAnnotations')
->will($this->returnValue(array($this->getAnnotatedRoute($methodRouteData))))
;
$routeCollection = $this->loader->load('Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BazClass');
$route = $routeCollection->get($classRouteData['name']);
$this->assertNull($route, '->load ignores class route');
$route = $routeCollection->get($methodRouteData['name']);
$this->assertSame($classRouteData['path'].$methodRouteData['path'], $route->getPath(), '->load concatenates class and method route path');
$this->assertEquals(array_merge($classRouteData['schemes'], $methodRouteData['schemes']), $route->getSchemes(), '->load merges class and method route schemes');
$this->assertEquals(array_merge($classRouteData['methods'], $methodRouteData['methods']), $route->getMethods(), '->load merges class and method route methods');
}
private function getAnnotatedRoute($data)
{
return new Route($data);

View File

@ -29,7 +29,7 @@ class AnnotationDirectoryLoaderTest extends AbstractAnnotationLoaderTest
public function testLoad()
{
$this->reader->expects($this->exactly(2))->method('getClassAnnotation');
$this->reader->expects($this->exactly(4))->method('getClassAnnotation');
$this->reader
->expects($this->any())