feature #15742 Using a service as a router resource (weaverryan)

This PR was squashed before being merged into the 2.8 branch (closes #15742).

Discussion
----------

Using a service as a router resource

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | almost
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | not yet...

Hi guys!

This adds the ability to use a service as a routing resource. In other words, instead of loading `routing.yml`, you could load `my_route_loader`, and then a method would be called on your service to return a RouteCollection.

Specifically, I'm interested in this because it would allow a user to point their main router resource to the kernel itself, making it possible to load routes inside the kernel (making a single-file full-stack app more possible).

Thanks!

Commits
-------

79e210f Using a service as a router resource
This commit is contained in:
Fabien Potencier 2015-10-01 22:34:27 +02:00
commit 99745e12a7
6 changed files with 261 additions and 1 deletions

View File

@ -50,6 +50,11 @@
<argument type="service" id="file_locator" />
</service>
<service id="routing.loader.service" class="Symfony\Component\Routing\Loader\DependencyInjection\ServiceRouterLoader" public="false">
<tag name="routing.loader" />
<argument type="service" id="service_container" />
</service>
<service id="routing.loader" class="%routing.loader.class%">
<tag name="monolog.logger" channel="router" />
<argument type="service" id="controller_name_converter" />

View File

@ -5,6 +5,8 @@ CHANGELOG
-----
* allowed specifying a directory to recursively load all routing configuration files it contains
* Added ObjectRouteLoader and ServiceRouteLoader that allow routes to be loaded
by calling a method on an object/service.
2.5.0
-----

View File

@ -0,0 +1,41 @@
<?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\Loader\DependencyInjection;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Loader\ObjectRouteLoader;
/**
* A route loader that executes a service to load the routes.
*
* This depends on the DependencyInjection component.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
class ServiceRouterLoader extends ObjectRouteLoader
{
/**
* @var ContainerInterface
*/
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
protected function getServiceObject($id)
{
return $this->container->get($id);
}
}

View File

@ -0,0 +1,95 @@
<?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\Loader;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\RouteCollection;
/**
* A route loader that calls a method on an object to load the routes.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*/
abstract class ObjectRouteLoader extends Loader
{
/**
* Returns the object that the method will be called on to load routes.
*
* For example, if your application uses a service container,
* the $id may be a service id.
*
* @param string $id
*
* @return object
*/
abstract protected function getServiceObject($id);
/**
* Calls the service that will load the routes.
*
* @param mixed $resource Some value that will resolve to a callable
* @param string|null $type The resource type
*
* @return RouteCollection
*/
public function load($resource, $type = null)
{
$parts = explode(':', $resource);
if (count($parts) != 2) {
throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the "service" route loader: use the format "service_name:methodName"', $resource));
}
$serviceString = $parts[0];
$method = $parts[1];
$loaderObject = $this->getServiceObject($serviceString);
if (!is_object($loaderObject)) {
throw new \LogicException(sprintf('%s:getServiceObject() must return an object: %s returned', get_class($this), gettype($loaderObject)));
}
if (!method_exists($loaderObject, $method)) {
throw new \BadMethodCallException(sprintf('Method "%s" not found on "%s" when importing routing resource "%s"', $method, get_class($loaderObject), $resource));
}
$routeCollection = call_user_func(array($loaderObject, $method), $this);
if (!$routeCollection instanceof RouteCollection) {
$type = is_object($routeCollection) ? get_class($routeCollection) : gettype($routeCollection);
throw new \LogicException(sprintf('The %s::%s method must return a RouteCollection: %s returned', get_class($loaderObject), $method, $type));
}
// make the service file tracked so that if it changes, the cache rebuilds
$this->addClassResource(new \ReflectionClass($loaderObject), $routeCollection);
return $routeCollection;
}
/**
* {@inheritdoc}
*
* @api
*/
public function supports($resource, $type = null)
{
return 'service' === $type;
}
private function addClassResource(\ReflectionClass $class, RouteCollection $collection)
{
do {
$collection->addResource(new FileResource($class->getFileName()));
} while ($class = $class->getParentClass());
}
}

View File

@ -0,0 +1,116 @@
<?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\Loader;
use Symfony\Component\Routing\Loader\ObjectRouteLoader;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class ObjectRouteLoaderTest extends \PHPUnit_Framework_TestCase
{
public function testLoadCallsServiceAndReturnsCollection()
{
$loader = new ObjectRouteLoaderForTest();
// create a basic collection that will be returned
$collection = new RouteCollection();
$collection->add('foo', new Route('/foo'));
// create some callable object
$service = $this->getMockBuilder('stdClass')
->setMethods(array('loadRoutes'))
->getMock();
$service->expects($this->once())
->method('loadRoutes')
->with($loader)
->will($this->returnValue($collection));
$loader->loaderMap = array(
'my_route_provider_service' => $service,
);
$actualRoutes = $loader->load(
'my_route_provider_service:loadRoutes',
'service'
);
$this->assertSame($collection, $actualRoutes);
// the service file should be listed as a resource
$this->assertNotEmpty($actualRoutes->getResources());
}
/**
* @expectedException \InvalidArgumentException
* @dataProvider getBadResourceStrings
*/
public function testExceptionWithoutSyntax($resourceString)
{
$loader = new ObjectRouteLoaderForTest();
$loader->load($resourceString);
}
public function getBadResourceStrings()
{
return array(
array('Foo'),
array('Bar::baz'),
array('Foo:Bar:baz'),
);
}
/**
* @expectedException \LogicException
*/
public function testExceptionOnNoObjectReturned()
{
$loader = new ObjectRouteLoaderForTest();
$loader->loaderMap = array('my_service' => 'NOT_AN_OBJECT');
$loader->load('my_service:method');
}
/**
* @expectedException \BadMethodCallException
*/
public function testExceptionOnBadMethod()
{
$loader = new ObjectRouteLoaderForTest();
$loader->loaderMap = array('my_service' => new \stdClass());
$loader->load('my_service:method');
}
/**
* @expectedException \LogicException
*/
public function testExceptionOnMethodNotReturningCollection()
{
$service = $this->getMockBuilder('stdClass')
->setMethods(array('loadRoutes'))
->getMock();
$service->expects($this->once())
->method('loadRoutes')
->will($this->returnValue('NOT_A_COLLECTION'));
$loader = new ObjectRouteLoaderForTest();
$loader->loaderMap = array('my_service' => $service);
$loader->load('my_service:loadRoutes');
}
}
class ObjectRouteLoaderForTest extends ObjectRouteLoader
{
public $loaderMap = array();
protected function getServiceObject($id)
{
return isset($this->loaderMap[$id]) ? $this->loaderMap[$id] : null;
}
}

View File

@ -35,7 +35,8 @@
"symfony/config": "For using the all-in-one router or any loader",
"symfony/yaml": "For using the YAML loader",
"symfony/expression-language": "For using expression matching",
"doctrine/annotations": "For using the annotation loader"
"doctrine/annotations": "For using the annotation loader",
"symfony/dependency-injection": "For loading routes from a service"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Routing\\": "" }