4

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 }
Community
  • 1
  • 1
Flo Schild
  • 5,104
  • 4
  • 40
  • 55

1 Answers1

3

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.

Wouter J
  • 41,455
  • 15
  • 107
  • 112
  • Thanks, I can understand a bit more the way to do. Do you recommend me to implement the CMF RoutingBundle or write my own based on this exemple ? And do you think it's possible, with such feature, to add some fields for the template and the Controller in the "Page" entity, to write some specific features ? Or is it better, if any page has specific needs, to develop it by my own as a classic Symfony2 route with a defined controller and template ? – Flo Schild Jul 14 '13 at 17:12
  • 1
    1) This example is based on the component, the component provides the basics and you need to create the implementations (or use the PHPCR ODM implementation of the bundle), but you always need the bundle; 2) You create your own route entity, so yes it's possible. As long as your route entity extends the Symfony route it works – Wouter J Jul 14 '13 at 17:32
  • I think I successfully set up my services, but I'm getting a Fatal Error : "Catchable Fatal Error: Argument 2 passed to Doctrine\ORM\EntityRepository::__construct() must be an instance of Doctrine\ORM\Mapping\ClassMetadata, none given" It seems to be my RouteProvider, given in the post, that is firing the problem. Did I miss anything ? I've updated my post with the services.yml. – Flo Schild Jul 15 '13 at 08:53