Skip to content

[Cookbook] [Routing] Article about custom route loaders #2339

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

Merged
merged 4 commits into from
Apr 9, 2013
Merged
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
1 change: 1 addition & 0 deletions cookbook/map.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
* :doc:`/cookbook/routing/slash_in_parameter`
* :doc:`/cookbook/routing/redirect_in_config`
* :doc:`/cookbook/routing/method_parameters`
* :doc:`/cookbook/routing/custom_route_loader`

* :doc:`/cookbook/security/index`

Expand Down
255 changes: 255 additions & 0 deletions cookbook/routing/custom_route_loader.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
.. index::
single: Routing; Custom route loader

How to Create a Custom Route Loader
Copy link
Contributor

Choose a reason for hiding this comment

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

still a paragraph needs to be added here regarding the use cases, you yourself mentioned this in your blog post answers to some of the questions there.

You can use a custom route loader for adding routes that can be
generated automatically, or when you have a bundle and don’t want
 to ask your users to add the routes to routing.yml manually.

source - http://php-and-symfony.matthiasnoback.nl/2012/01/symfony2-dynamically-add-routes/#comment-484

I would even go further with an app example idea or something
it would greatly help to see the need and best practice

Copy link
Contributor

Choose a reason for hiding this comment

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

I am further aware that some use cases are SonataAdminBundle, another use case i found is in the making of CRUD routes like in your blog post's comment, and Assetic (to be confirmed). But the idea is to expand on the cases so that it could be useful to more people in their applications.

Copy link
Member

Choose a reason for hiding this comment

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

FOSRestBundle and KnpRadBundle are other use cases: loading routes based on some convention

===================================

A custom route loader allows you to add routes to an application, without
writing them down in for instance a Yaml file. This comes in handy when
you have made a bundle and don't want to manually add the routes for the
bundle to ``app/config/routing.yml``. Especially when you want to make the
bundle reusable, or when you have open-sourced it, this would slow down
the installation process and make it error-prone.

But you can also use a custom route loader when your routes correspond
to a certain pattern and you don't want to or can't write them all out by
hand. For instance when you have CRUD controllers of which the routes all
correspond to a pattern like ``*_new``, ``*_edit``, etc.

.. note::

There are many bundles out there that use their own route loaders to
accomplish cases like those described above, for instance
`FOSRestBundle`_, `KnpRadBundle`_ and `SonataAdminBundle`_.

Loading Routes
--------------

The routes in a Symfony application are loaded by the
:class:`Symfony\\Bundle\\FrameworkBundle\\Routing\\DelegatingLoader`.
This loader uses several other loaders (delegates) to load resources of
different types, for instance Yaml files or ``@Route`` and ``@Method`` annotations
in controller files. The specialized loaders implement
:class:`Symfony\\Component\\Config\\Loader\\LoaderInterface`
and therefore have two important methods:
:method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::supports`
and :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::load`.

Take these lines from ``routing.yml``:

.. code-block:: yaml

_demo:
resource: "@AcmeDemoBundle/Controller/DemoController.php"
type: annotation
prefix: /demo
Copy link
Member

Choose a reason for hiding this comment

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

You should also add a XML and PHP example:

<import resource="@AcmeDemoBundle/Controller/DemoController.php"
    type="annotation"
    prefix="/demo"
/>

(I'm not sure about the PHP config)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think there's no need for additional examples, since it's not actually an example of how you can write config.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree and have chosen to skip this one, since it is an example and not something anyone has to implement.


The main loader tries all the delegate loaders and calls their
:method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::supports`
with the given resource (``@AcmeDemoBundle/Controller/DemoController.php``)
and type ("annotation") as arguments. When one of the loader returns ``true``,
its method :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::load`
will be called, and the loader returns a :class:`Symfony\\Component\\Routing\\RouteCollection`
Copy link
Contributor

Choose a reason for hiding this comment

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

its method ... gets called in favor of will be called ?

containing :class:`Symfony\\Component\\Routing\\Route` objects.

Creating a Custom Loader
------------------------

To load routes in another way than using annotations, Yaml or XML files,
you need to create a custom route loader. This loader should implement
:class:`Symfony\\Component\\Config\\Loader\\LoaderInterface`.

The sample loader below supports resources of type "extra". The resource
name itself is not used in the example::

namespace Acme\DemoBundle\Routing;

use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

class ExtraLoader implements LoaderInterface
{
private $loaded = false;

public function load($resource, $type = null)
{
if (true === $this->loaded) {
throw new \RuntimeException('Do not add the "extra" loader twice');
}

$routes = new RouteCollection();

// prepare a new route
$pattern = '/extra/{parameter}';
$defaults = array(
'_controller' => 'AcmeDemoBundle:Demo:extra',
);
$requirements = array(
'parameter' => '\d+',
);
$route = new Route($pattern, $defaults, $requirements);

// add the new route to the route collection:
$routeName = 'extraRoute';
$routes->add($routeName, $route);

return $routes;
}

public function supports($resource, $type = null)
{
return 'extra' === $type;
}

public function getResolver()
{
// will be explained later
}

public function setResolver(LoaderResolver $resolver)
{
// same here
}
}

.. note::

Make sure the controller you specify really exists.

Now define a service for the ``ExtraLoader``:

.. configuration-block::

.. code-block:: yaml

services:
acme_demo.routing_loader:
class: Acme\DemoBundle\Routing\ExtraLoader
tags:
- { name: routing.loader }

.. code-block:: xml

<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="acme_demo.routing_loader" class="Acme\DemoBundle\Routing\ExtraLoader">
<tag name="routing.loader" />
</service>
</services>
</container>

.. code-block:: php

use Symfony\Component\DependencyInjection\Definition;

$container
->setDefinition(
'acme_demo.routing_loader',
new Definition('Acme\DemoBundle\Routing\ExtraLoader')
)
->addTag('routing.loader')
;

Notice the tag ``routing.loader``. All services with this tag will be marked
as potential route loaders and added as specialized routers to the
:class:`Symfony\\Bundle\\FrameworkBundle\\Routing\\DelegatingLoader`.

Finally, we only need to add a few extra lines to the routing configuration:

.. configuration-block::

.. code-block:: yaml

AcmeDemoBundle_Extra:
resource: .
type: extra

.. code-block:: xml

<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

<import resource="." type="extra" />
</routes>

.. code-block:: php

use Symfony\Component\Routing\RouteCollection;

$collection = new RouteCollection();
$collection->addCollection($loader->import('.', 'extra'));

return $collection;

The important part here is the ``type`` key. Its value should be "extra".
This is the type which our ``ExtraLoader`` supports and this will make sure
its ``load()`` method gets called. The ``resource`` key is insignificant
for the ``ExtraLoader``, so we set it to ".".

.. note::
Copy link
Contributor

Choose a reason for hiding this comment

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

could we add a note that more requirements could be perhaps met by the cmf chain routing?
http://symfony.com/doc/master/cmf/components/routing.html


The routes defined using custom route loaders will be automatically
cached by the framework. So whenever you change something to the behavior
of the loader, don't forget to clear the cache.


More Advanced Loaders
---------------------

In most cases it's better not to implement
:class:`Symfony\\Component\\Config\\Loader\\LoaderInterface`
yourself, but extend from :class:`Symfony\\Component\\Config\\Loader\\Loader`.
This class knows how to use a :class:`Symfony\\Component\\Config\\Loader\\LoaderResolver`
to load secondary routing resources.

Of course you still need to implement
:method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::supports`
and :method:`Symfony\\Component\\Config\\Loader\\LoaderInterface::load`.
Whenever you want to load another resource, for instance a Yaml routing
configuration file, you can call the
:method:`Symfony\\Component\\Config\\Loader\\Loader::import` method::

namespace Acme\DemoBundle\Routing;

use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\RouteCollection;

class AdvancedLoader extends Loader
{
public function load($resource, $type = null)
{
$collection = new RouteCollection();

$resource = '@AcmeDemoBundle/Resources/config/import_routing.yml';
$type = 'yaml';

$importedRoutes = $this->import($resource, $type);

$collection->addCollection($importedRoutes);

return $collection;
}

public function supports($resource, $type = null)
{
return $type === 'advanced_extra';
}
}

.. note::

The resource name and type of the imported routing configuration can
be anything that would normally be supported by the routing configuration
loader (Yaml, XML, PHP, annotation, etc.).

.. _`FOSRestBundle`: https://github.com/FriendsOfSymfony/FOSRestBundle
.. _`KnpRadBundle`: https://github.com/KnpLabs/KnpRadBundle
.. _`SonataAdminBundle`: https://github.com/sonata-project/SonataAdminBundle
1 change: 1 addition & 0 deletions cookbook/routing/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Routing
slash_in_parameter
redirect_in_config
method_parameters
custom_route_loader