The problem: routing /blogs, not just /blogs/ in Silex
Route mounting in the Silex PHP framework allows conveniently grouping controllers per feature in separate files, then using mounting them on some prefix path like $app->mount('/blogs')
. This will prepend the prefix "/blogs" to the paths defined in the the feature controller, effectively delegating to it all the /blogs/*
routes. However, as the Silex documentation claims:
When mounting a route collection under /blog, it is not possible to define a route for the /blog URL. The shortest possible URL is /blog/.
This means handling the route mount point has to be done by a route outside the mounted feature, which makes it slightly less clean, as you have to do something like:
But is there really no workaround for this limitation ? Sure there is!
The solution
Turns out this limitation is not completely real, thanks to the way the UrlMatcher
from the Symfony Routing component works. To use the canonical example from the Silex documentation, let us define routes
At this point, accessing /blogs
will actually work, but serving blog entry 1 using the $blogController->showAction(1)
, instead of using $blogController->indexAction()
thanks to the default value()
provided by the blog_show
. Not what we are looking for, but notice how we are already serving /blogs
from a route defined below the mount point, within the blogs controllers list.
Since multiple routes can handle the same path and will resolve in their order of appearance in the source file, all it takes is adding another more specific route for the same path, not conflicting with the blog_show
route, in this case the blog_mount
route:
Now when a GET request comes in for /blogs
, it matches either blog_mount
and blog_show
. But since blog_mount
is listed before blog_show
, it will match first using the default "xyzzy" argument, satisfying its assert()
requirement, and pass the request to the indexAction
controller.
That'all it takes.
Why it works
To understand why things work that way, look at:
vendor/symfony/routing/Matcher/UrlMatcher::matchCollection()
: the call to$route->compile()
is where the magic lies, as it builds a match pattern which will handle the mount point itself thanks for the default value provided in thevalue()
call.vendor/symfony/routing/RouteCompiler::compile()<code> actually performs this in its <code>compilePattern()
method.