Struggling with intricacies of RouteBundle/document managers

144 views
Skip to first unread message

Lars Janssen

unread,
Jun 4, 2013, 11:57:46 AM6/4/13
to symfony-...@googlegroups.com
Hi all,

I wonder if anyone can help me to understand how the CMF routing works. I am still trying to make the RoutingBundle work with my preview/published workspaces but there are so many variables to consider.

I am trying to make the following steps possible; in a nutshell, I want to be able to rename a page/route in the preview workspace, without affecting the published workspace:

1. Load some fixtures
    - Including a route, for example /cms/routes/about and including a reference to a page /cms/pages/about
    - Loaded into the preview workspace, cloned into published workspace
    - I am clearing the cache before loading the fixtures

    - RoutingBundle should load the routes from the published workspace and pass the referenced page over to the controller
    - BlockBundle should load all child blocks from the published workspace

3. Change the URL of the "about" page in the preview workspace
    - This is done using the admin panel
    - Page is changed from /cms/pages/about to /cms/pages/aboutx
    - Route is changed from /cms/routes/about to /cms/routes/aboutx, and still references the page (by uuid)

4. Visit the URL http://localhost/app_dev.php/about again
    - This page should load exactly as in step 2
    - Meanwhile, visiting http://localhost/app_dev.php/aboutx should create a 404 error

5. Publish the site
    - Triggered from the admin panel
    - Does a workspace->update in the published workspace on the /cms node, so now the content of both workspaces is identical again
    - This action also clears the cache

6. Visit the URLs again
    - http://localhost/app_dev.php/about should now give a 404 error
    - http://localhost/app_dev.php/aboutx should now display the page and its blocks

As discussed previously, this needs to be achieved by telling the RoutingBundle (and BlockBundle) which document manager to use at each stage, overriding this config value:

    cmf_routing:
        dynamic:
            manager_name: <preview or published?>

In theory, it should be possible to set the manager_name either way around, and then override it in the other cases. In practice, I have not been able to get either way to work yet.

Here I am looking at the case where the configured document manager is "preview"; i.e. it's all set for loading the fixtures and the admin site, but I need to set it to "published" in order to view the site.

1. Load some fixtures

This seems to work. I have a route like this in both workspaces:

    about:
      - addTrailingSlash =
      - jcr:uuid = e0ad6cf1-f7b5-4f78-a64a-fb184e0bbbf7
      - routeContent =
       - uuid: 0fed5b90-d50a-46b3-88ed-80440c751fdb
      - jcr:mixinTypes = Array(    [0] => phpcr:managed    [1] => mix:referenceable)
      - defaults = Array(    [0] => PwnContentViewBundle:Default:index)
      - jcr:primaryType = nt:unstructured
      - phpcr:class = Symfony\Cmf\Bundle\RoutingBundle\Document\Route
      - phpcr:classparents = Array(    [0] => Symfony\Component\Routing\Route)
      - requirements = Array()
      - defaultsKeys = Array(    [0] => _controller)
      - host =
      - addFormatPattern =
      - options = Array()


This can seem to work correctly, but only because preview/published are currently the same. So, we'll need to jump further ahead to prove it, but I'll add the service now:

services.yml:

    pwn_content_view.controller.workspace_listener:
        class: Pwn\ContentViewBundle\EventListener\WorkspaceListener
        calls:
            - [ setContainer, [ @service_container ] ]
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 50 }

service class:

    public function onKernelRequest(GetResponseEvent $getResponseEvent)
    {
        // @todo - find a better way to keep it from affecting the admin pages
        if (0 === strpos($getResponseEvent->getRequest()->getPathInfo(), '/admin')) {
            return;
        }

        // not sure if this is needed?
        $contentRepository = $this->container->get('cmf_routing.default_content_repository');
        $contentRepository->setManagerName('published');

        $routeProvider = $this->container->get('cmf_routing.default_route_provider');
        $routeProvider->setManagerName('published');
    }

This now gives me the error:

    Unable to find the controller for path "/about". Maybe you forgot to add the matching route in your routing configuration?

Through various hacks (e.g. temporarily changing the manager_name config or hard wiring things in vendor) I've managed to get it further, but never quite working through all the steps. Here are some areas I've been looking at:

Symfony\Cmf\Bundle\RoutingBundle\CmfRoutingBundle
    - adds the DoctrinePhpcrMappingsPass compiler pass
    - specifies the config item to use: "cmf_routing.manager_name"

Doctrine\Bundle\PHPCRBundle\DependencyInjection\Compiler\RegisterMappingsPass
    - getChainDriverServiceName() gets the service name based on the specified config item (e.g. cmf_routing.manager_name)
    - this results in a service name like doctrine_phpcr.odm.preview_metadata_driver
    - process() gets the container definition based on that generated service name, but where was this service defined?
    - I am struggling to find how the metadata_driver relates to/uses document managers, if at all. If it does use them, and that config item is significant, should there be a way to dynamically change it?

Symfony\Cmf\Bundle\RoutingBundle\Document\RouteProvider
    - getRouteCollectionForRequest() uses the currently set (by name) document manager to fetch the routes
    - it's difficult to see if this is working correctly without understanding the "Unable to find the controller ..." error.

Can anyone shed any light on this, and are there any other areas I should be looking at?

Thanks,

Lars.

David Buchmann

unread,
Jun 4, 2013, 12:47:32 PM6/4/13
to symfony-...@googlegroups.com, Lars Janssen
hi lars,

i think your workflow does make sense, so lets try to figure out how to
make it happen.

> In theory, it should be possible to set the manager_name either way
> around, and then override it in the other cases.

ack. if you set it before any of the routing happens, it does not matter
which is the default as long as you catch all non-default cases.
i would feel more safe to have the published workspace be the default.

> Unable to find the controller for path "/about". Maybe you forgot to
> add the matching route in your routing configuration?

are you sure the document that route points too actually has a
controller configured? maybe to isolate issues you could write the
_controller into that /about route?


sonata admin never goes through the router. so would it not be enough to
have sonata look at preview workspace and leave routing in published? or
is this about having previews?

> Doctrine\Bundle\PHPCRBundle\DependencyInjection\Compiler\RegisterMappingsPass
> - getChainDriverServiceName() gets the service name based on the
> specified config item (e.g. cmf_routing.manager_name)
> - this results in a service name
> like doctrine_phpcr.odm.preview_metadata_driver
> - process() gets the container definition based on that generated
> service name, but where was this service defined?
> - I am struggling to find how the metadata_driver relates to/uses
> document managers, if at all. If it does use them, and that config item
> is significant, should there be a way to dynamically change it?

indeed there might be an issue here. we only register the mappings for
the base Symfony\Component\Routing\Route class with the manager that is
set in the configuration.
maybe you need to add another compiler pass (or actually another
instance of the same pass again with the name of the preview workspace
config parameter) to have that recognized. otherwise properties of
Symfony\Component\Routing\Route will simply be ignored.

> Symfony\Cmf\Bundle\RoutingBundle\Document\RouteProvider
> - getRouteCollectionForRequest() uses the currently set (by name)
> document manager to fetch the routes
> - it's difficult to see if this is working correctly without
> understanding the "Unable to find the controller ..." error.

so this part should work. maybe you can set up some functional tests to
see if the behaviour you expect actually works? trying to debug with a
full request makes it hard to see what is going on.

cheers,david

Lars Janssen

unread,
Jun 5, 2013, 7:23:53 AM6/5/13
to symfony-...@googlegroups.com, Lars Janssen
Hi David,

> In theory, it should be possible to set the manager_name either way
> around, and then override it in the other cases.

ack. if you set it before any of the routing happens, it does not matter
which is the default as long as you catch all non-default cases.
i would feel more safe to have the published workspace be the default.

I guess it depends which is the biggest danger in the event of a coding error: accidentally modifying the published content, or accidentally revealing the preview content.

I will probably try to use the most common case as the default, but it's not clear which one is most common yet.

>     Unable to find the controller for path "/about". Maybe you forgot to
> add the matching route in your routing configuration?

are you sure the document that route points too actually has a
controller configured? maybe to isolate issues you could write the
_controller into that /about route?

I always had the controller configured on the routes, not the pages. Not sure if that's the best way or not. Here is how my routes are created in the fixtures:

    $route = new Route();
    $route->setPosition($parentNode, $newNodeName);
    $route->setDefault('_controller', 'PwnContentViewBundle:Default:index');
    $route->setRouteContent($sourceNode);
    $this->manager->persist($route);
    $this->manager->flush();

This is done in the preview workspace, before cloning the entire repository over to published in one operation.

sonata admin never goes through the router. so would it not be enough to
have sonata look at preview workspace and leave routing in published? or
is this about having previews?

Actually, we are not fully using Sonata admin, so this is done with a custom controller action. Here is the action that gets executed when the user clicks the "publish" button (this is a global publish action acting on the whole repository until we sort out the cascade issues):

    public function publishAction()
    {
        $previewManager = $this->get('doctrine_phpcr.odm.preview_document_manager');
        $previewWorkspaceName = $previewManager->getPhpcrSession()->getWorkspace()->getName();

        $publishedManager = $this->get('doctrine_phpcr.odm.published_document_manager');
        $rootDocument = $publishedManager->find(null, $this->container->getParameter('pwn_content.global_root_node'));
        $rootDocument->update($previewWorkspaceName);

        $this->get('session')->getFlashBag()->add(self::DASHBOARD_NOTICES, 'The site has been published.');
        return $this->redirect($this->generateUrl('sonata_admin_dashboard'));
    }

> Doctrine\Bundle\PHPCRBundle\DependencyInjection\Compiler\RegisterMappingsPass
>     - getChainDriverServiceName() gets the service name based on the
> specified config item (e.g. cmf_routing.manager_name)
>     - this results in a service name
> like doctrine_phpcr.odm.preview_metadata_driver
>     - process() gets the container definition based on that generated
> service name, but where was this service defined?
>     - I am struggling to find how the metadata_driver relates to/uses
> document managers, if at all. If it does use them, and that config item
> is significant, should there be a way to dynamically change it?

indeed there might be an issue here. we only register the mappings for
the base Symfony\Component\Routing\Route class with the manager that is
set in the configuration.

Where is the manager specified in the current mappings? I could find the part where the manager name is used (from the config item "cmf_routing.manager_name"). Is there any mention of the manager in the XML mappings, or elsewhere?

maybe you need to add another compiler pass (or actually another
instance of the same pass again with the name of the preview workspace
config parameter) to have that recognized. otherwise properties of
Symfony\Component\Routing\Route will simply be ignored.

Assuming I can figure out the mappings above, is it safe to have two compiler passes? I would not want a route from the preview workspace to be picked up if there is no matching route in published, or anything like that.

> Symfony\Cmf\Bundle\RoutingBundle\Document\RouteProvider
>     - getRouteCollectionForRequest() uses the currently set (by name)
> document manager to fetch the routes
>     - it's difficult to see if this is working correctly without
> understanding the "Unable to find the controller ..." error.

so this part should work. maybe you can set up some functional tests to
see if the behaviour you expect actually works? trying to debug with a
full request makes it hard to see what is going on.
 
Sure, I'll try and set something up later...

Thanks for your help. :)

Lars.

David Buchmann

unread,
Jun 5, 2013, 8:00:33 AM6/5/13
to symfony-...@googlegroups.com
> > In theory, it should be possible to set the manager_name either way
> > around, and then override it in the other cases.
>
> ack. if you set it before any of the routing happens, it does not matter
> which is the default as long as you catch all non-default cases.
> i would feel more safe to have the published workspace be the default.
>
>
> I guess it depends which is the biggest danger in the event of a coding
> error: accidentally modifying the published content, or accidentally
> revealing the preview content.
>
> I will probably try to use the most common case as the default, but it's
> not clear which one is most common yet.

i would tend to leave the default on the published, so all bundles that
might read and whatever do not need to know at all.
writing is quite contained so should be easier to be sure it always goes
to the right workspace.

> I always had the controller configured on the routes, not the pages. Not
> sure if that's the best way or not. Here is how my routes are created in
> the fixtures:
>
> $route = new Route();
> $route->setPosition($parentNode, $newNodeName);
> $route->setDefault('_controller', 'PwnContentViewBundle:Default:index');
> $route->setRouteContent($sourceNode);
> $this->manager->persist($route);
> $this->manager->flush();

ah then really its most likely the mapping pass issue you are seeing.

i much prefer configuring the controller (or template if i need no
specific controller) in the bundle config by content document class. its
much easier to fix if you for example refactor a name. if the name is
stored in the db, you might not refactor because you don't want to
migrate the data, leaving your application less understandable than it
otherwise would be...

> Where is the manager specified in the current mappings? I could find the
> part where the manager name is used (from the config item
> "cmf_routing.manager_name"). Is there any mention of the manager in the
> XML mappings, or elsewhere?
>
> Assuming I can figure out the mappings above, is it safe to have two
> compiler passes? I would not want a route from the preview workspace to
> be picked up if there is no matching route in published, or anything
> like that.

i'll try to explain. the mapping pass has nothing at all to do with
routing. when phpcr-odm loads documents, it needs metadata. the mapping
pass is used to tell doctrine where to find that.
the compiler pass does not add the information to all doctrine manager
instances, but only to one. when you look at the CmfRoutingBundle bundle
class, you see how we instantiate the compiler pass for the base routing
class. we pass it cmf_routing.manager_name to find the manager - you can
see this name in DependencyInjection\Configuration.php - if that is not
defined, we fall back to the param that defines the global default
manager name of phpcr-odm.

you can safely create another compiler pass where you pass the name of a
parameter that tells the manager name for the draft workspace. then the
draft thing should persist route defaults and parameters as well.

note that right now we are only talking about the mapping for the base
Symfony\Component\Routing but i plan to move the Route document to Model
and then use the compiler pass also for the
Symfony\Cmf\Bundle\RoutingBundle\Model\Route class.

cheers,david
--
Liip AG // Agile Web Development // T +41 26 422 25 11
CH-1700 Fribourg // PGP 0xA581808B // www.liip.ch

Lars Janssen

unread,
Jun 5, 2013, 12:44:16 PM6/5/13
to symfony-...@googlegroups.com
On Wed, Jun 5, 2013 at 1:00 PM, David Buchmann wrote:
i would tend to leave the default on the published, so all bundles that
might read and whatever do not need to know at all.
writing is quite contained so should be easier to be sure it always goes
to the right workspace.

Good point, so I will probably switch things around soon. Still, it should work both ways around so I will try just a little longer on the current setup (so long as it doesn't prove too troublesome).

> I always had the controller configured on the routes, not the pages. Not
> sure if that's the best way or not. Here is how my routes are created in
> the fixtures:
>
>     $route = new Route();
>     $route->setPosition($parentNode, $newNodeName);
>     $route->setDefault('_controller', 'PwnContentViewBundle:Default:index');
>     $route->setRouteContent($sourceNode);
>     $this->manager->persist($route);
>     $this->manager->flush();

ah then really its most likely the mapping pass issue you are seeing.

i much prefer configuring the controller (or template if i need no
specific controller) in the bundle config by content document class. its
much easier to fix if you for example refactor a name. if the name is
stored in the db, you might not refactor because you don't want to
migrate the data, leaving your application less understandable than it
otherwise would be...

Ah, good point, I hadn't tried doing it that way, although I am trying it now without success.

For now, I have taken this out of my route fixtures:

    $route->setDefault('_controller', 'PwnContentViewBundle:Default:index');

and added this to my config.yml:

    generic_controller: PwnContentViewBundle:Default:index

This is giving me the "Unable to find the controller for path..." error even when I keep everything to the same workspace.

I notice in the docs the controllers are defined as services. Is that required, or just preferable for unrelated reasons?

i'll try to explain. the mapping pass has nothing at all to do with
routing. when phpcr-odm loads documents, it needs metadata. the mapping
pass is used to tell doctrine where to find that.
the compiler pass does not add the information to all doctrine manager
instances, but only to one. when you look at the CmfRoutingBundle bundle
class, you see how we instantiate the compiler pass for the base routing
class. we pass it cmf_routing.manager_name to find the manager - you can
see this name in DependencyInjection\Configuration.php - if that is not
defined, we fall back to the param that defines the global default
manager name of phpcr-odm.

Ok, I think that makes sense - I've been through this code a few times and see that it picks up a service such as "doctrine_phpcr.odm.published_metadata_driver", although I'm not sure where that service is generated in the first place.

you can safely create another compiler pass where you pass the name of a
parameter that tells the manager name for the draft workspace. then the
draft thing should persist route defaults and parameters as well.

This seems to have come much closer to working. I've copied it into my own bundle and needed to change a couple of lines:

    $arguments = array(array(realpath(__DIR__ . '/../../../vendor/symfony-cmf/routing-bundle/Symfony/Cmf/Bundle/RoutingBundle/Resources/config/doctrine-base')), '.phpcr.xml');

and then the config parameter:

    array('pwn_content_view.manager_name'),

However, something is still not quite right. If I do the following (all in the preview workspace):

Rename page /about to /aboutx
Rename page /other-page to /about

Now, when I view the public site, I see the content of /other-page when viewing /about (this should not happen until I publish the changes).

It's very possibly a bug in my code/configuration of course.

Perhaps to narrow this down I need to get the "generic_controller" config above working and then, as you say try and add some functional tests.

After that, I will try things the other way around (with published as the default, only setting 'preview' in my admin and fixtures operations).

Thanks again,

Lars

David Buchmann

unread,
Jun 6, 2013, 7:46:24 AM6/6/13
to symfony-...@googlegroups.com
> For now, I have taken this out of my route fixtures:
>
> $route->setDefault('_controller', 'PwnContentViewBundle:Default:index');
>
> and added this to my config.yml:
>
> generic_controller: PwnContentViewBundle:Default:index

i think we have to make things work with setDefault too (though i think
the mapping is superior).

what you do here can not work. the generic_controller is only used if a
_template default or an entry in templates_by_class is found. but you
don't want your special controller to be the default controller for all
content. if you need a controller and not just a special template, you
want to configure controllers_by_class

controllers_by_class:
Your\Project\Document: \
PwnContentViewBundle:Default:index

see also
http://symfony.com/doc/master/cmf/getting_started/routing.html#getting-the-controller-and-template

> I notice in the docs the controllers are defined as services. Is that
> required, or just preferable for unrelated reasons?

its generally more flexible as you can then inject parameters into the
controller rather than pull everything in using $this->get. but both
should work with RoutingBundle.

> i'll try to explain. the mapping pass has nothing at all to do with
> routing. when phpcr-odm loads documents, it needs metadata. the mapping
> pass is used to tell doctrine where to find that.
> the compiler pass does not add the information to all doctrine manager
> instances, but only to one. when you look at the CmfRoutingBundle bundle
> class, you see how we instantiate the compiler pass for the base routing
> class. we pass it cmf_routing.manager_name to find the manager - you can
> see this name in DependencyInjection\Configuration.php - if that is not
> defined, we fall back to the param that defines the global default
> manager name of phpcr-odm.
>
>
> Ok, I think that makes sense - I've been through this code a few times
> and see that it picks up a service such as
> "doctrine_phpcr.odm.published_metadata_driver", although I'm not sure
> where that service is generated in the first place.

thats the DependencyInjection class of DoctrinePHPCRBundle. its creating
a lot of service definitions programmatically, not easy to read indeed.

> you can safely create another compiler pass where you pass the name of a
> parameter that tells the manager name for the draft workspace. then the
> draft thing should persist route defaults and parameters as well.
>
>
> This seems to have come much closer to working. I've copied it into my
> own bundle and needed to change a couple of lines:
>
> $arguments = array(array(realpath(__DIR__ .
> '/../../../vendor/symfony-cmf/routing-bundle/Symfony/Cmf/Bundle/RoutingBundle/Resources/config/doctrine-base')),
> '.phpcr.xml');
>
> and then the config parameter:
>
> array('pwn_content_view.manager_name'),

that sounds not too wrong yes. don't know why you encounter the issues
afterwards. are you sure everything gets edited and flushed on the right
workspace? to debug you could register a listener on the default
namespace to detect flushes and throw an exception to know the wrong
workspace got flushed...

Lars Janssen

unread,
Jun 7, 2013, 8:07:06 AM6/7/13
to symfony-...@googlegroups.com

On Thu, Jun 6, 2013 at 12:46 PM, David Buchmann wrote:
> For now, I have taken this out of my route fixtures:
>
>     $route->setDefault('_controller', 'PwnContentViewBundle:Default:index');
>
> and added this to my config.yml:
>
>     generic_controller: PwnContentViewBundle:Default:index

i think we have to make things work with setDefault too (though i think
the mapping is superior).

This appears to be working now, so long as I keep the second compiler pass.

what you do here can not work. the generic_controller is only used if a
_template default or an entry in templates_by_class is found. but you
don't want your special controller to be the default controller for all
content. if you need a controller and not just a special template, you
want to configure controllers_by_class

controllers_by_class:
    Your\Project\Document: \
       PwnContentViewBundle:Default:index

see also
http://symfony.com/doc/master/cmf/getting_started/routing.html#getting-the-controller-and-template

Ah, ok, it works fine with controllers_by_class (this in itself doesn't solve my preview/publish workflow, see below, but it means I am at least starting with something that works!)
 
that sounds not too wrong yes. don't know why you encounter the issues
afterwards. are you sure everything gets edited and flushed on the right
workspace? to debug you could register a listener on the default
namespace to detect flushes and throw an exception to know the wrong
workspace got flushed...

Sorry for my ignorance here, but how would I register an event listener on the namespace?

Anyway, I think I narrowed the issue down to something that has given me a problem in the past, and is still not solved: if I rename a node, then the reference is lost during the workspace update operation. Since the route has a reference to the page, if the page loses its reference then clearly we're going to see an error.

Here's that topic again on the Jackrabbit list:

I don't think I have reported this issue on the Apache Jira yet, so I will try and create a test case and do that.

Thanks,

Lars.

David Buchmann

unread,
Jun 7, 2013, 8:12:44 AM6/7/13
to symfony-...@googlegroups.com

> afterwards. are you sure everything gets edited and flushed on the right
> workspace? to debug you could register a listener on the default
> namespace to detect flushes and throw an exception to know the wrong
> workspace got flushed...
>
>
> Sorry for my ignorance here, but how would I register an event listener
> on the namespace?

wanted to say workspace. so basically register a listener on the
doctrine manager for the workspace that should not be written to.

Lars Janssen

unread,
Jun 10, 2013, 12:14:03 PM6/10/13
to symfony-...@googlegroups.com
Hi,

On Friday, 7 June 2013 13:12:44 UTC+1, David Buchmann wrote:
> Sorry for my ignorance here, but how would I register an event listener
> on the namespace?

wanted to say workspace. so basically register a listener on the
doctrine manager for the workspace that should not be written to.

Ok thanks, I've just made my first event listener. :) From monitoring preFlush and postPersist it seemed to always have the event manager I expected.

So to summarise, for the case where cmf_routing -> dynamic -> manager_name is "preview":

  - I can set a default controller on the route document, but this requires me to add my own compiler pass for the "published" document manager using DoctrinePhpcrMappingsPass. This also needs me to put a config item in my bundle somewhere (e.g. "manager_name") and set this to "published"

  - Using cmf_routing -> dyanmic -> controllers_by_class is more maintainable (if the controllers change later), and also seems to work without the need for adding the above compiler pass.

  - I still see a problem when renaming a document and then publishing (update cloned node), where the document references (from nodes to pages in this case) are missing in the destination workspace. I will look into this issue separately.

Now, having gone through the above, I have tried to implement David's suggestion and set cmf_routing -> dynamic -> manager_name to "published":

  - Initially this gave the problem I've mentioned at the start of this topic: some properties on the route node, such as phpcr:classparents, are not set. Fixing this requires me to add my a compiler pass using DoctrinePhpcrMappingsPass again, but with the related config item set to "preview"

  - The rename/publish bug mentioned above affects this configuration too.

After all this, I'm wondering if RoutingBundle could prevent the need for the user to create these compiler passes? For example, could it automatically loop through all the managers specified in doctrine_phpcr -> odm ?

Or could be a manual setting?

    cmf_routing:
        dynamic:
            manager_name: published
            mapped_managers: [ preview, published ]

Although, I think it might be best to minimise the amount of PHPCR-related config in bundles such as Routing, Block etc. so would prefer some kind of automation (the built-in compiler pass is already checking for the existence of the DoctrinePhpcrMappingsPass class).

Thanks,

Lars.

David Buchmann

unread,
Jun 13, 2013, 10:25:17 AM6/13/13
to symfony-...@googlegroups.com
you opened https://github.com/symfony-cmf/RoutingBundle/issues/111 -
lets continue the discussion there.
Reply all
Reply to author
Forward
0 new messages