Categories


Archives


Recent Posts


Categories


Basic and Advanced Sylius Routing

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

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.

Series Navigation<< Symfony Routing Configuration Keys

Copyright © Alan Storm 1975 – 2019 All Rights Reserved

Originally Posted: 7th May 2019