Skip to content

Using a service as a router resource #15742

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't put this inside FrameworkBundle because it's perfectly usable by someone like Drupal. It's really a "Bridge" class, but there's no proper bridge to put it into.

{
/**
* @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
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was caught between calling this "object" (which would look so generic in a routing.yml file) and service.

}

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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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