Skip to content

Commit

Permalink
Using a service as a router resource
Browse files Browse the repository at this point in the history
  • Loading branch information
weaverryan authored and fabpot committed Oct 1, 2015
1 parent dbb099d commit 79e210f
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 1 deletion.
Expand Up @@ -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" />
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Routing/CHANGELOG.md
Expand Up @@ -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
-----
Expand Down
@@ -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);
}
}
95 changes: 95 additions & 0 deletions src/Symfony/Component/Routing/Loader/ObjectRouteLoader.php
@@ -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());
}
}
116 changes: 116 additions & 0 deletions src/Symfony/Component/Routing/Tests/Loader/ObjectRouteLoaderTest.php
@@ -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;
}
}
3 changes: 2 additions & 1 deletion src/Symfony/Component/Routing/composer.json
Expand Up @@ -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\\": "" }
Expand Down

0 comments on commit 79e210f

Please sign in to comment.