So far in this series, we’ve been putting our route configuration in the config/routes.yaml
files. When you’re working on a local Symfony application, this is the right thing to do.
However — if you’re distributing a symfony bundle, or an entire application like Sylius, putting your routes into the config/routes.yaml
file isn’t a great option. You could do this — keep a list of routes in your README.md
and then tell users to manually copy and paste them into their own config/routes.yaml
file, (in fact, this is what was going on in the early days of Symfony 2), but that’s putting a lot of burden on the user, and making your code or system extra hard to use.
Fortunately, in 2019, Symfony offers code-distributing developers a number of systems for loading routes that will fit right into their regular workflows. Today we’re going to explore how Symfony loads routes, and take a look at how Sylius has organized their routes.
This article assumes you’ve installed Sylius using the standard edition package. Specifics may vary slightly if you’ve installed Sylius in a different way, but the concepts should stay the same.
Symfony Kernel
At the core of every Symfony application is an object called the Kernel. The Kernel object is responsible for bootstrapping Symfony’s container and a few key services, and then running the application. Two of those services are the http_kernel
and router
services — both of which are responsible for request and URL routing.
We wont get too deep into Kernel internals today, but here’s the Kernel object method where Symfony loads your system’s route configuration
#File: src/Kernel.php
protected function configureRoutes(RouteCollectionBuilder $routes): void
{
$confDir = $this->getProjectDir() . '/config';
$routes->import($confDir . '/{routes}/*' . self::CONFIG_EXTS, '/', 'glob');
$routes->import($confDir . '/{routes}/' . $this->environment . '/**/*' . self::CONFIG_EXTS, '/', 'glob');
$routes->import($confDir . '/{routes}' . self::CONFIG_EXTS, '/', 'glob');
}
Although this code is in your src/
folder, this is all from the Symfony project (i.e. not Sylius). This is the boilerplate Kernel
code generated when you create a new Symfony application.
The import
method on the RouteCollectionBuilder
object is responsible for loading the route configuration files from disk. The glob
argument above tells the RouteCollectionBuilder
that the paths we’re passing in are glob patterns. These patterns are mostly standard glob patterns, with a few custom Symfony enhancements. If we swap those variables out for their values
#File: src/Kernel.php
$routes->import("/path/to/symfony/config/{routes}/*.{php,xml,yaml,yml}", ...
$routes->import("/path/to/symfony/config/{routes}/dev/**/*.{php,xml,yaml,yml}", ...
$routes->import("/path/to/symfony/config/{routes}.{php,xml,yaml,yml}", ...
we can see the full path patterns. Symfony uses the GLOB_BRACE
constant when evaluating these glob paths, and the **
pattern will recurse into directories. This means the first path
config/{routes}/*.{php,xml,yaml,yml}
will match the following configuration files in a stock sylius standard edition system.
config/routes/liip_imagine.yaml
config/routes/sylius_admin.yaml
config/routes/sylius_admin_api.yaml
config/routes/sylius_shop.yaml
The next path
config/{routes}/dev/**/*.{php,xml,yaml,yml}
loads the following files.
config/routes/dev/twig.yaml
config/routes/dev/web_profiler.yaml
Also, notice that dev
in the glob pattern? That’s your symfony enviornment. If you’re running with a different APP_ENV
value this import will load files from a different folder.
The final pattern
config/{routes}.{php,xml,yaml,yml}
matches the following file
config/routes.yaml
You may recognize this as the main route file we’ve been working with.
Depending on extra work done in your Sylius system, or the Symfony Flex packages you’ve installed, you may have extra configuration files available to you.
Sylius Routing Files
The three files we want to concentrate on today are
config/routes/sylius_admin.yaml
config/routes/sylius_admin_api.yaml
config/routes/sylius_shop.yaml
These are Sylius’s route files. The Sylius team has placed these files here so that the Symfony Kernel will load them. If you’re debugging a page or service endpoint in a Sylius application, these files are the best place to start.
However: There’s more files in play than just these three. If we open up the sylius_shop.yaml
, we’ll see the following
#File: config/routes/sylius_shop.yaml
sylius_shop:
resource: "@SyliusShopBundle/Resources/config/routing.yml"
prefix: /{_locale}
requirements:
_locale: ^[a-z]{2}(?:_[A-Z]{2})?$
sylius_shop_payum:
resource: "@SyliusShopBundle/Resources/config/routing/payum.yml"
sylius_shop_default_locale:
path: /
methods: [GET]
defaults:
_controller: sylius.controller.shop.locale_switch:switchAction
That’s only three routes. However, thanks to our last two articles, we know that the sylius_shop
and sylius_shop_payum
routes are resource routes, which means they’re loading route files from different bundles.
resource: "@SyliusShopBundle/Resources/config/routing.yml"
resource: "@SyliusShopBundle/Resources/config/routing/payum.yml"
The @
symbol tells Symfony that the file (ex. Resources/config/routing/payum.yml
) is located in a specific bundle (ex. SyliusShopBundle
). That bundle is located here
vendor/sylius/sylius/src/Sylius/Bundle/ShopBundle
If you’d like to learn more about how Symfony’s bundles are structured, the official docs beckon.
This nested routing configuration continues — all in all (as of this writing) these three Sylius routing files ultimately load another 100+ files.
Debugging Routes
Manually following a trail of routing files can be a tedious affair. Fortunately, Symfony has mature tooling. If you’re running Sylius in developer mode, the bottom of every page should feature Symfony’s debugging toolbar.
If you open up the debugging bar’s routes section, you’ll see a list of every path rule Symfony tried to match before finding the current route
This isn’t every route in the system — but when you’re debugging routes it’s a great place to start.
Above and Beyond Core Symfony
So that’s the basics — next we need to talk about places the Sylius team went a little above and beyond standard Symfony routing.
We haven’t talked about it yet, but one of Symfony’s configuration based features allows system and application developers to create a custom route loader. Once written and configured, a custom rout loader will look for routes of a certain type
(i.e. their type:...
configuration field), and then use the value in the resource:...
configuration field to create a route.
Sylius uses a custom route loader as part of its resource configuration system. We’ll be talking about this resource configuration system in full at a later date — today we’re just going to focus on this custom route loader as a stand alone feature.
You can identify a routing configuration that takes advantage of the custom loader by looking at its type
#File: vendor/sylius/sylius/src/Sylius/Bundle/AdminApiBundle/Resources/config/routing/order.yml
sylius_admin_api_order:
resource: |
alias: sylius.order
section: admin_api
only: [show]
grid: sylius_admin_order
serialization_version: $version
type: sylius.resource_api
For example, in the above route configuration the type
is sylius.resource_api
. This type is one supported by Sylius’s custom route loader, the other type being a sylius.resource
. When Symfony is parsing the route files and encounters either of these types, it will pass the resource
value to Sylius’s custom route loader for further processing.
The resource value itself is a little strange. Let’s take a closer look.
resource: |
alias: sylius.order
section: admin_api
only: [show]
grid: sylius_admin_order
serialization_version: $version
That |
character is a YAML construct. It tells the YAML parser that the next bit of indented text should be treated as a string. Symfony passes this resource value to Sylius’s custom route loader as a raw string, and then the loader parses it as YAML in order to get a PHP data structure.
Not the most elegant of solutions, but it does allow Sylius to send a large amount of structured data to the custom route loader.
So what does the resource loader do? It automatically creates up to six routes at once for common CRUD-ish actions — specifically show
, index
, create
, update
, delete
, and bulkDelete
. The embedded yaml-string can be thought of a mini-domain-specific-language whose features include creating routes.
Consider the route with the identifier sylius_admin_api_order_show
$ php bin/console debug:router sylius_admin_api_order_show
+--------------+---------------------------------------------------------+
| Property | Value |
+--------------+---------------------------------------------------------+
| Route Name | sylius_admin_api_order_show |
| Path | /api/v{version}/orders/{id} |
| Path Regex | #^/api/v(?P<version>[^/]++)/orders/(?P<id>[^/]++)$#sD |
| Host | ANY |
| Host Regex | |
| Scheme | ANY |
| Method | GET |
| Requirements | NO CUSTOM |
| Class | Symfony\Component\Routing\Route |
| Defaults | _controller: sylius.controller.order:showAction |
| | _sylius: array ('serialization_group... |
| Options | compiler_class: Symfony\Component\Routing\RouteCompiler |
+--------------+---------------------------------------------------------+
As you can see from the debug:router
output above, this is definitely a Sylius route. However, you will not find this route’s configuration anywhere in a standard routing configuration file. That’s because it’s created by the custom route loader.
You’ll be wise to keep these auto-generated routes in mind when you’re debugging your system. In addition to the debug:router
command, Sylius also offers us a sylius:debug:resource
command, which will let us provide a resource’s alias (sylius.order
)
resource: |
alias: sylius.order
and get back all sorts of useful information about the resource.
$ php bin/console sylius:debug:resource sylius.order
+--------------------+-----------------------------------------------------+
| name | order |
| application | sylius |
| driver | doctrine/orm |
| [...].model | App\Entity\Order\Order |
| [...].controller | Sylius\Bundle\CoreBundle\Controller\OrderController |
| [...].repository | Sylius\Bundle\CoreBundle\Doctrine\ORM\OrderRepository |
| [...].interface | Sylius\Component\Order\Model\OrderInterface |
| [...].factory | Sylius\Component\Resource\Factory\Factory |
| [...].form | Sylius\Bundle\OrderBundle\Form\Type\OrderType |
+--------------------+-----------------------------------------------------+
Most of this information is NOT related to routing — as we said we’ll cover resources in full at a later date. However, we can see that this resource will always create routes that use the Sylius\Bundle\CoreBundle\Controller\OrderController
controller.
Sylius Custom Defaults
Another bit of non-standard-Symfony-but-things-symfony’s-flexibility-allows is the _sylius
default.
In our earlier articles we mentioned that Symfony’s routing configuration files are very strict with their top level keys. If you try to create an invalid configuration
route_id:
my_custom_key: ...
Symfony will reject the configuration because it contains an unknown key (my_custom_key
).
One work-around for this is the defaults
configuration.
defaults:
key: value
_format: ...
_fragment: ...
_locale: ...
_controller: ...
You may recall from last time that defaults
is a bit of a grab bag of legacy features and things that don’t fit in anywhere else. Also, unlike the top level configuration — a sub-key of defaults can have any name.
defaults:
_sylius:
//...
Sylius uses a default named _sylius
to “smuggle” extra values into the route, which also means they’ll be available from the request/controller.
Here’s one example of a _sylius
configuration from the shop bundle
#File: vendor/sylius/sylius/src/Sylius/Bundle/ShopBundle/Resources/config/routing/security.yml
sylius_shop_login:
path: /login
methods: [GET]
defaults:
_controller: sylius.controller.security:loginAction
_sylius:
template: "@SyliusShop/login.html.twig"
logged_in_route: sylius_shop_account_dashboard
and an example of some Sylius PHP code using one of these configuration keys.
#File: vendor/sylius/sylius/src/Sylius/Bundle/UiBundle/Controller/SecurityController.php
public function loginAction(Request $request): Response
{
$alreadyLoggedInRedirectRoute = $request->attributes->get('_sylius')['logged_in_route'] ?? null;
//...
}
Covering what each of these fields do is beyond the scope of this article, but keep these in mind as you explore the Sylius source and try to reason about Sylius code.
Wrap Up
Sylius’s implementation of routes show both the power, and the danger, of a configuration based MVC system. The danger is confusion — both the custom route loader and the special _sylius
variable are non-obvious. Even an experienced Symfony developer might trip over them the first time they’re encountered.
The power of this system is the Sylius team was able to build exactly the abstraction they wanted, and one that (presumably) helped their team accomplish something they wouldn’t have been able to otherwise.
Having only looked at the routing system, it’s hard to say whether this abstraction, the Sylius Resource System, will be as useful for third party developers as it’s been for the Sylius core team. In the coming articles we’ll be taking a deeper look at this system.