8000 feature #15742 Using a service as a router resource (weaverryan) · symfony/symfony@99745e1 · GitHub
[go: up one dir, main page]

Skip to content
8000

Commit 99745e1

Browse files
committed
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
2 parents 54e3d71 + 79e210f commit 99745e1

File tree

6 files changed

+261
-1
lines changed

6 files changed

+261
-1
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@
5050
<argument type="service" id="file_locator" />
5151
</service>
5252

53+
<service id="routing.loader.service" class="Symfony\Component\Routing\Loader\DependencyInjection\ServiceRouterLoader" public="false">
54+
<tag name="routing.loader" />
55+
<argument type="service" id="service_container" />
56+
</service>
57+
5358
<service id="routing.loader" class="%routing.loader.class%">
5459
<tag name="monolog.logger" channel="router" />
5560
<argument type="service" id="controller_name_converter" />

src/Symfony/Component/Routing/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ CHANGELOG
55
-----
66

77
* allowed specifying a directory to recursively load all routing configuration files it contains
8+
* Added ObjectRouteLoader and ServiceRouteLoader that allow routes to be loaded
9+
by calling a method on an object/service.
810

911
2.5.0
1012
-----
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Routing\Loader\DependencyInjection;
13+
14+
use Symfony\Component\Config\Loader\Loader;
15+
use Symfony\Component\DependencyInjection\ContainerInterface;
16+
use Symfony\Component\Routing\Loader\ObjectRouteLoader;
17+
18+
/**
19+
* A route loader that executes a service to load the routes.
20+
*
21+
* This depends on the DependencyInjection component.
22+
*
23+
* @author Ryan Weaver <ryan@knpuniversity.com>
24+
*/
25+
class ServiceRouterLoader extends ObjectRouteLoader
26+
{
27+
/**
28+
* @var ContainerInterface
29+
*/
30+
private $container;
31+
32+
public function __construct(ContainerInterface $container)
33+
{
34+
$this->container = $container;
35+
}
36+
37+
protected function getServiceObject($id)
38+
{
39+
return $this->container->get($id);
40+
}
41+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Routing\Loader;
13+
14+
use Symfony\Component\Config\Loader\Loader;
15+
use Symfony\Component\Config\Resource\FileResource;
16+
use Symfony\Component\Routing\RouteCollection;
17+
18+
/**
19+
* A route loader that calls a method on an object to load the routes.
20+
*
21+
* @author Ryan Weaver <ryan@knpuniversity.com>
22+
*/
23+
abstract class ObjectRouteLoader extends Loader
24+
{
25+
/**
26+
* Returns the object that the method will be called on to load routes.
27+
*
28+
* For example, if your application uses a service container,
29+
* the $id may be a service id.
30+
*
31+
* @param string $id
32+
*
33+
* @return object
34+
*/
35+
abstract protected function getServiceObject($id);
36+
37+
/**
38+
* Calls the service that will load the routes.
39+
*
40+
* @param mixed $resource Some value that will resolve to a callable
41+
* @param string|null $type The resource type
42+
*
43+
* @return RouteCollection
44+
*/
45+
public function load($resource, $type = null)
46+
{
47+
$parts = explode(':', $resource);
48+
if (count($parts) != 2) {
49+
throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the "service" route loader: use the format "service_name:methodName"', $resource));
50+
}
51+
52+
$serviceString = $parts[0];
53+
$method = $parts[1];
54+
55+
$loaderObject = $this->getServiceObject($serviceString);
56+
57+
if (!is_object($loaderObject)) {
58+
throw new \LogicException(sprintf('%s:getServiceObject() must return an object: %s returned', get_class($this), gettype($loaderObject)));
59+
}
60+
61+
if (!method_exists($loaderObject, $method)) {
62+
throw new \BadMethodCallException(sprintf('Method "%s" not found on "%s" when importing routing resource "%s"', $method, get_class($loaderObject), $resource));
63+
}
64+
65+
$routeCollection = call_user_func(array($loaderObject, $method), $this);
66+
67+
if (!$routeCollection instanceof RouteCollection) {
68+
$type = is_object($routeCollection) ? get_class($routeCollection) : gettype($routeCollection);
69+
70+
throw new \LogicException(sprintf('The %s::%s method must return a RouteCollection: %s returned', get_class($loaderObject), $method, $type));
71+
}
72+
73+
// make the service file tracked so that if it changes, the cache rebuilds
74+
$this->addClassResource(new \ReflectionClass($loaderObject), $routeCollection);
75+
76+
return $routeCollection;
77+
}
78+
79+
/**
80+
* {@inheritdoc}
81+
*
82+
* @api
83+
*/
84+
public function supports($resource, $type = null)
85+
{
86+
return 'service' === $type;
87+
}
88+
89+
private function addClassResource(\ReflectionClass $class, RouteCollection $collection)
90+
{
91+
do {
92+
$collection->addResource(new FileResource($class->getFileName()));
93+
} while ($class = $class->getParentClass());
94+
}
95+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Routing\Tests\Loader;
13+
14+
use Symfony\Component\Routing\Loader\ObjectRouteLoader;
15+
use Symfony\Component\Routing\Route;
16+
use Symfony\Component\Routing\RouteCollection;
17+
18+
class ObjectRouteLoaderTest extends \PHPUnit_Framework_TestCase
19+
{
20+
public function testLoadCallsServiceAndReturnsCollection()
21+
{
22+
$loader = new ObjectRouteLoaderForTest();
23+
24+
// create a basic collection that will be returned
25+
$collection = new RouteCollection();
26+
$collection->add('foo', new Route('/foo'));
27+
28+
// create some callable object
29+
$service = $this->getMockBuilder('stdClass')
30+
->setMethods(array('loadRoutes'))
31+
->getMock();
32+
$service->expects($this->once())
33+
->method('loadRoutes')
34+
->with($loader)
35+
->will($this->returnValue($collection));
36+
37+
$loader->loaderMap = array(
38+
'my_route_provider_service' => $service,
39+
);
40+
41+
$actualRoutes = $loader->load(
42+
'my_route_provider_service:loadRoutes',
43+
'service'
44+
);
45+
46+
$this->assertSame($collection, $actualRoutes);
47+
// the service file should be listed as a resource
48+
$this->assertNotEmpty($actualRoutes->getResources());
49+
}
50+
51+
/**
52+
* @expectedException \InvalidArgumentException
53+
* @dataProvider getBadResourceStrings
54+
*/
55+
public function testExceptionWithoutSyntax($resourceString)
56+
{
57+
$loader = new ObjectRouteLoaderForTest();
58+
$loader->load($resourceString);
59+
}
60+
61+
public function getBadResourceStrings()
62+
{
63+
return array(
64+
array('Foo'),
65+
array('Bar::baz'),
66+
array('Foo:Bar:baz'),
67+
);
68+
}
69+
70+
/**
71+
* @expectedException \LogicException
72+
*/
73+
public function testExceptionOnNoObjectReturned()
74+
{
75+
$loader = new ObjectRouteLoaderForTest();
76+
$loader->loaderMap = array('my_service' => 'NOT_AN_OBJECT');
77+
$loader->load('my_service:method');
78+
}
79+
80+
/**
81+
* @expectedException \BadMethodCallException
82+
*/
83+
public function testExceptionOnBadMethod()
84+
{
85+
$loader = new ObjectRouteLoaderForTest();
86+
$loader->loaderMap = array('my_service' => new \stdClass());
87+
$loader->load('my_service:method');
88+
}
89+
90+
/**
91+
* @expectedException \LogicException
92+
*/
93+
public function testExceptionOnMethodNotReturningCollection()
94+
{
95+
$service = $this->getMockBuilder('stdClass')
96+
->setMethods(array('loadRoutes'))
97+
->getMock();
98+
$service->expects($this->once())
99+
->method('loadRoutes')
100+
->will($this->returnValue('NOT_A_COLLECTION'));
101+
102+
$loader = new ObjectRouteLoaderForTest();
103+
$loader->loaderMap = array('my_service' => $service);
104+
$loader->load('my_service:loadRoutes');
105+
}
106+
}
107+
108+
class ObjectRouteLoaderForTest extends ObjectRouteLoader
109+
{
110+
public $loaderMap = array();
111+
112+
protected function getServiceObject($id)
113+
{
114+
return isset($this->loaderMap[$id]) ? $this->loaderMap[$id] : null;
115+
}
116+
}

src/Symfony/Component/Routing/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"symfony/config": "For using the all-in-one router or any loader",
3636
"symfony/yaml": "For using the YAML loader",
3737
"symfony/expression-language": "For using expression matching",
38-
"doctrine/annotations": "For using the annotation loader"
38+
"doctrine/annotations": "For using the annotation loader",
39+
"symfony/dependency-injection": "For loading routes from a service"
3940
},
4041
"autoload": {
4142
"psr-4": { "Symfony\\Component\\Routing\\": "" }

0 commit comments

Comments
 (0)
0