Skip to content

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy