symfony-cmf / symfony-cmf

Meta package tying together all the key packages of the Symfony CMF project.
http://cmf.symfony.com
Other
729 stars 94 forks source link

Ability to have multiple basepaths #182

Closed benglass closed 10 years ago

benglass commented 10 years ago

Currently the cmf assumes there is a single basepath for various types of documents (routes, content, menus, etc.). However in a multi-domain/website configuration this may not be true. One approach to handling this is having a service that reconfigures the basepaths in various services based on the current website. This can be problematic in the case of generating a menu or loading content or generating a url or accessing any content outside of the current basepath. Reconfiguring the basepaths dynamically is a fragile approach in this case for reasons outlined in https://github.com/symfony-cmf/RoutingBundle/issues/178#issuecomment-30635352 (this also applies to generating menus in addition to route urls outside the current basepath context).

I think it would be a great feature of the cmf (especially for multi-site functionality) to allow multiple basepaths for content, menus, routes, etc. in addition to handling URL generation to automatically include the domain when generating URLs outside of the current basepath.

Obviously this is a potentially major change that effects multiple bundles (Menu, Routing, Content, etc.) and I'd love to hear what the core devs have to say about it.

One possible approach would be centralizing the knowledge of basepaths into a BasepathProviderInterface. There could be multiple services that provide an implementation of this such as cmf_menu.basepath_provider and cmf_content.basepath_provider, these could all even be instances of the same class just with different array of basepaths configured.

Some issues that discuss this situation: https://github.com/symfony-cmf/RoutingBundle/issues/178 https://github.com/symfony-cmf/RoutingBundle/issues/149

dbu commented 10 years ago

i was pondering the multiple basepath options as well, but mostly for better supporting simple cms without duplicating services. it could also be used to have a common collection for a menu / routes that are available on all sites of a multisite installation. as routes can also store criteria on the domain, even a mixed setup would be possible.

however, i don't see a reason to on one site want to match a route of a different site, or to render the menu of a different site. i am thinking about a scenario where i have a separate route tree per site. as said in the discussion on the routing bundle, i think what we need is a generator that knows about the locations of routes per domain and is able to build the route with the correct domain name (and protocoll and all), even when i just call path it still needs to generate an absolute route. that would be a separate route generator and should not be stuffed into the normal cmf generator. when this generator gets hold of the route object (either through the provider or its passed the object directly) it can inspect the route id and configure the right prefix, make sure its embedded core generator has the right domain and then generate the absolute url.

the menu should not need anything else than a list of base paths to work.

jjanvier commented 10 years ago

Registering to this discussion. Could be useful for Sylius :)

danrot commented 10 years ago

Just FYI: In @sulu-cmf we are planning to have one PHPCR workspace for each site, that would solve the problem you are describing, as every site could use the same paths. However, I don't know too much about every bundle, so I don't know if it is too hard to build. But it sounds like it is a big task anyway.

lsmith77 commented 10 years ago

@danrot just FYI .. you cannot use PHPCR references across workspaces .. though you can of course still store UUIDs pointing to other workspaces and then resolve the references manually but f.e. referrers are harder to implement in user land. one could use a query but these are again limited to one workspace at a time.

benglass commented 10 years ago

@dbu regarding the DomainAwareGenerator I think I could use a little assistance understanding the structure of generators vs the route matchers and how we can add another generator without adding matcher. I will confess that the nested matchers and generators and their relationships to the chain router are an area of the cmf I have not fully grasped so any insight into how to add a generator to the cmf without replacing the existing generators and without effecting the way that url-based route matching is done would be helpful.

benglass commented 10 years ago

@dbu As I think I understand it the ChainRouter aggregates routers for both url matching and url generation, the relevant methods being doMatch (https://github.com/symfony-cmf/Routing/blob/1cbe6927b9f21c7879dc525d2c3981980f402827/ChainRouter.php#L169) for url matching and generate (https://github.com/symfony-cmf/Routing/blob/1cbe6927b9f21c7879dc525d2c3981980f402827/ChainRouter.php#L211) for url generation.

We accomplish the creation of a DomainAwareGenerator by implementing Symfony/Component/Routing/RouterInterface and add an instance of the DomainAwareGenerator to the ChainRouter

If this is the case, then we need to ensure that the match() method never matches since this generator is not a true router in that it cant do url matching, only generation (or rather it could do generation but only if domain is known and that is not our use case).

So when match is called, do we throw ResourceNotFoundException?

Also, we would have to provide a getRouteCollection method, this should generate absolute URLs for every item in every basepath? Is just used for the console commands like router:debug/dump?

Please let me know if this is in line with what you were thinking for a DomainAwareUrlGenerator or if im looking the wrong area.

dbu commented 10 years ago

sorry, pretty busy here before the christmas office closing... exactly, that is what i would do. the DomainAwareGenerator would extend the ContentAwareGenerator, implement RouterInterface but implement the match method to simply throw the ResourceNotFoundException, and return an empty RouteCollection. it will be given a special Provider (maybe extending the standard provider is enough) that detects out-of-basepath situations and makes sure those routes are always created including domains.

from how i understand ProviderBasedGenerator::generate and UrlGenerator::doGenerate the url will become absolute if the host of the url is different from the host we currently are. if that is enough, we just have to make sure the DomainAwareProvider sets the right host on the route, and that the request context provides the correct different host. doGenerate looks rather cryptic however, you will need to dig through it a big i fear. though maybe the symfony doc explains it, as well. (whether you configure a route with routing.yml or in the object directly maps quite straightforward)

dbu commented 10 years ago

@danrot i guess then you do not care about sharing content between sites, or do you? if you do, it will become quite difficult to manage. if you don't its a neat separation that won't accidentally be broken. david's model says you should use workspaces only like git branches. magnolia cms uses them for separating media files, content, metadata and other types of data but keep all sites of a multisite system together. there again, they care about sharing data between sites.

benglass commented 10 years ago

@dbu thanks for the review I will work on this but to be honest I think if you have a routing capable of generating a route from a basepath and it knows what domain to use this router should also be able to do the matching based on the domain as well. This would simplify multi-site handling and also be more logical/symmetrical. I will start with the url generation though.

Part of the DomainAwareGenerator is going to also have to be an IdPrefixListener that supports multiple basepaths because otherwise when you try to load the document via the DocumentManager you will get an exception that the configured prefix is not in the id of the route you loaded. I am thinking there should be an alternate IdPrefixListener called MultipleIdPrefixListener or the IdPrefixListener should just be fixed to support multiple prefixes.

dbu commented 10 years ago

@benglass i have no experience with multidomain symfony sites. (the same code running multiple domains). i know a route can have a host criteria. would your vision be to have a route provider that also puts a requirement on host and then only if you are on the right host you will get a match? you would want the RouteProvider to only propose routes from domains that actually could match, otherwise you potentially load a lot of routes that you could already know don't match, which would be inefficient. but then, would this not just move that logic (what domain we currently are on, and what base paths to use for that domain) deep into the routing, instead of configuring your system for the current domain? this is an open question, i don't know what the best approach is.

the IdPrefixListener reads like this:

    // only update route objects and only if the prefix can match, to allow
    // for more than one listener and more than one route root
    if (($doc instanceof PrefixInterface)
        && ! strncmp($this->idPrefix, $doc->getId(), strlen($this->idPrefix))
    ) {
        $doc->setPrefix($this->idPrefix);
    }

it does not throw any exception if things do not match. i would propose that the logic about the many domains is in a DomainAwareProvider that has multiple roots and knows which of them are domain specific and which are not. then it can set the right prefix, and - if necessary - set the domain as well. hm, or indeed we could make the IdPrefixListener aware of multiple prefixes, and of the domains. (that would also help to get rid of the simplecms special services mess that you stumbled into). instead of one idprefix, it would be many, and each could potentially be associated with a host pattern to be set if it is the prefix taken. compared to a special route provider, this would work however the route is loaded.

i still think having two providers, one for matching that is set to the current domains, and one for generating that knows all roots makes sense.

danrot commented 10 years ago

@dbu Well, we don't want to share data between different sites, but we plan to have some global workspaces, which will then be accessed by all the other ones, and I guess we will handle this by weak references (as @lsmith77) described above. Or don't you think this is a good way?

dbu commented 10 years ago

@danrot that should be possible as well, yes. but you will need manual steps to link documents from different namespaces and can't automate everything with doctrine. and you will need multiple dynamic router instances, one for each workspace, if you want routing to use several workspaces.

of course, you could also try to use the global workspace as template and sync it into the various site workspaces, to have the merge logic and all that in a cronjob rather than on each request. i would then mark such documents as "coming from the global defaults" so that you can also remove/update those that where not changed but keep things that a site customized. lots of tricky and interesting questions to solve ;-)

multisite is something that should be really possible with the cmf, but we did not do it yet, so we need the input of all people trying to do that and figure out what works best for what situations. (multisite can mean different things as well, from totally separate sites running on the same installation to tightly integrated multidomain websites managed by the same people)

dbu commented 10 years ago

trying to implement this in https://github.com/symfony-cmf/RoutingBundle/pull/210

dbu commented 10 years ago

this is now implemented in the RoutingBundle