3
votes

I have a current project that has to displays both defined pages with specific entities, what is very easy to manage with Symfony2, and content pages on different layouts, what is - I guess - a bit less common.

I get in trouble trying to build the routing system.

For instance, if I have to display a page with some news, I would like to update the router of my bundle with a new route like :

my_bundle_news_page:
    pattern: /news
    defaults:
        _controller: MyBundle:NewsController:indexAction

But how to manage a dynamic router that could have a totally custom URL on many levels ?

Let's imagine I've got a "Page" Entity, that is self-references for an optionnal "parent-child" relation. I don't think I can just use any config YAML file for this specific routing ?!

my_bundle_custom_page:
    pattern: /{slug}
    defaults:
        _controller: MyBundle:PageController:showAction

This would bind all the first-level pages:

/projects

/about

/contact

/our-project

What about a page that would be displayed with, for instance, a slug like:

/our-project/health

In fact any URL...

/{slug-level1}/{slug-level2}/{slug-level3} etc.

Cause the pages are supposed to change and be updated from webmastering.

I guess the best way would be to have a router that compare the {slug} with a database field (entity property)

I read in the Symfony-CMF doc that it is possible to write a service based a route provider:

namespace MyBundle\Routing;

use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route as SymfonyRoute;

use MyBundle\Entity\PageRepository;

class RouteProvider extends PageRepository {
    public function findPageBySlug($slug)
    {
        // Find a page by slug property
        $page = $this->findOneBySlug($slug);

        if (!$page) {
            // Maybe any custom Exception
            throw $this->createNotFoundException('The page you are looking for does not exists.');
        }

        $pattern = $page->getUrl(); // e.g. "/first-level/second-level/third-level"

        $collection = new RouteCollection();

        // create a new Route and set our page as a default
        // (so that we can retrieve it from the request)
        $route = new SymfonyRoute($pattern, array(
            'page' => $page,
        ));

        // add the route to the RouteCollection using a unique ID as the key.
        $collection->add('page_'.uniqid(), $route);

        return $collection;
    }
}

But how to set it up as a service ? Are there some requirements ? How could this kind of thing work, does it add a route to the RouteCollection when request is called ?

And will I be able to bind any route in this way ?

EDIT : services.yml of my bundle

parameters:
    cmf_routing.matcher.dummy_collection.class: Symfony\Component\Routing\RouteCollection
    cmf_routing.matcher.dummy_context.class: Symfony\Component\Routing\RequestContext
    cmf_routing.generator.class: Symfony\Cmf\Bundle\RoutingBundle\Routing\ContentAwareGenerator
    cmf_routing.nested_matcher.class:  Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
    cmf_routing.url_matcher.class:  Symfony\Cmf\Component\Routing\NestedMatcher\NestedMatcher
    fsbcms.chain_router.class: Symfony\Cmf\Component\Routing\ChainRouter
    fsbcms.route_provider.class: FSB\CMSBundle\Routing\RouteProvider
    fsbcms.dynamic_router.class: Symfony\Cmf\Component\Routing\DynamicRouter
    fsbcms.route_entity.class: null

services:
    fsbcms.router:
        class: %fsbcms.chain_router.class%
        arguments:
            - "@logger"
        calls:
            - [setContext, ["router.request_context"]]
    fsbcms.route_provider:
        class: "%fsbcms.route_provider.class%"
        arguments:
            - "@doctrine"
    cmf_routing.matcher.dummy_collection:
        class: "%cmf_routing.matcher.dummy_collection.class%"
        public: "false"
    cmf_routing.matcher.dummy_context:
        class: "%cmf_routing.matcher.dummy_context.class%"
        public: false
    cmf_routing.generator:
        class: "%cmf_routing.generator.class%"
        arguments:
            - "@fsbcms.route_provider"
            - "@logger"
        calls:
            - [setContainer, ["service_container"]]
            - [setContentRepository, ["cmf_routing.content_repository"]]
    cmf_routing.url_matcher:
        class: "%cmf_routing.url_matcher.class%"
        arguments: ["@cmf_routing.matcher.dummy_collection", "@cmf_routing.matcher.dummy_context"]
    cmf_routing.nested_matcher:
        class: "%cmf_routing.nested_matcher.class%"
        arguments: ["@fsbcms.route_provider"]
        calls:
            - [setFinalMatcher, ["cmf_routing.url_matcher"]]
    fsbcms.dynamic_router:
        class: "%fsbcms.dynamic_router.class%"
        arguments:
            - "@router.request_context"
            - "@cmf_routing.nested_matcher"
            - "@cmf_routing.generator"
        tags:
            - { name: router, priority: 300 }
1

1 Answers

3
votes

I suggest taking a look at the Symfony CMF routing component and the CmfRoutingBundle (to implement the component in symfony).

The Routing component uses a chain router, which is irrelevant for this question but it's good to know. The chain router chains over a queue of routers. The component provides a DynamicRouter that uses a NestedMatcher. That's exactly what you want.

The NestedMatcher uses a Route provider to get the routes from a dynamic source (e.g. a database). You are showing an example of a Route provider in your question.

Furthermore, it uses a FinalMatcher to match the route. You can just pass an instance of Symfony\Cmf\Component\Routing\NestedMatcher\UrlMatcher, as you are doing not too difficult things.

Take a look at the docs of the RoutingBundle to learn how to activate the chain router and then create a route provider which loads the routes, make a service:

acme_routing.route_provider:
    class: Acme\RoutingBundle\Provider\DoctrineOrmProvider
    arguments: ["@doctrine"]

Now, you can create a NestedMatcher service:

acme_routing.url_matcher:
    class: Symfony\Cmf\Component\Routing\NestedMatcher\UrlMatcher
    arguments: ["@cmf_routing.matcher.dummy_collection", "@cmf_routing.matcher.dummy_context"]

acme_routing.nested_matcher:
    class: Symfony\Cmf\Component\Routing\NestedMatcher
    arguments: ["@acme_routing.route_provider"]
    calls:
        - [setFinalMatcher, ["acme_routing.url_matcher"]]

Now, register the DynamicRouter and put it in the chain:

acme_routing.dynamic_router:
    class: Symfony\Cmf\Component\Routing\DynamicRouter
    arguments:
        - "@router.request_context"
        - "@acme_routing.nested_matcher"
        - "@cmf_routing.generator"
    tags:
        - { name: router, priority: 300 }

Now, it should work and should load the routes from the database and match them against the request.