Skip to content

Using new routing system from Sylius resource bundle #18212

@loic425

Description

@loic425

Description

We need to use the new routing system using new metadata with operations instead of the legacy ResourceController.

The new "MainController" has 60 lines of code (580 for the ResourceController). It's based on the same technical architecture as API-Platform provider/processor system.

This new approach brings the routing logic closer to the resource definitions (similar to API Platform), eliminates boilerplate, and supports both RAD and advanced DDD usage patterns. It also removes the coupled legacy ResourceController pattern which has been hard to extend and understand for many contributors.

Before
The legacy ResourceController

Image

After

final class MainController
{
    public function __construct(
        private HttpOperationInitiatorInterface $operationInitiator,
        private RequestContextInitiatorInterface $requestContextInitiator,
        private ProviderInterface $provider,
        private ProcessorInterface $processor,
    ) {
    }

    public function __invoke(Request $request): Response
    {
        $operation = $this->operationInitiator->initializeOperation($request);

        if (null === $operation) {
            throw new RuntimeException('Operation should not be null.');
        }

        $context = $this->requestContextInitiator->initializeContext($request);

        if (null === $operation->canWrite()) {
            $operation = $operation->withWrite(!$request->isMethodSafe());
        }

        $data = $this->provider->provide($operation, $context);

        $valid = $request->attributes->getBoolean('is_valid', true);
        if (!$valid) {
            $operation = $operation->withWrite(false);
        }

        return $this->processor->process($data, $operation, $context);
    }
}

With this change, routing definitions become fully driven by metadata and operations — removing the need for Symfony routing files and the legacy ResourceController. This reduces duplication, standardizes route behavior, and opens the path to more flexible, decoupled resources across both Admin and Shop interfaces.

Before After
Legacy routing via ResourceController (~580 LOC) Metadata-driven routing via operations (~60 LOC)
Duplication and generated controllers Unified system like API Platform
Coupled to Doctrine entities Works with any PHP classes

Why this change matters

The legacy ResourceController adds hundreds of lines of duplicate routing logic and diverges from the resource-metadata driven model that SyliusResourceBundle now provides. Moving to a unified routing system reduces complexity, improves maintainability, and allows us to leverage the full expressiveness of operation metadata (like API-Platform).

Bc-layer

We try to provide the best bc-layer as possible that will allow you to migrate slowly and prepare the moment when the ResourceController will be removed in the future (Sylius 3.x probably).

bc-layer example

sylius_core:
    routing:
        bc_layer:
            enabled: true
            routes:
                admin_admin_user: false
                admin_catalog_promotion: false
                admin_channel: false   
                admin_product: false
                shop_product_review: false

The bc_layer allows selectively disabling legacy routes, letting developers migrate incrementally. Setting a route to false disables its legacy variant while keeping the new metadata-driven routes active.

Routing converter

We are trying to provide a best-effort tool to convert your existing custom routes and Sylius built-in routes' customization.

Convert this legacy routing with YAML configuration

sylius_admin_product:
    resource: |
        alias: sylius.product
        section: admin
        templates: "@SyliusAdmin\\shared\\crud"
        redirect: update
        grid: sylius_admin_product
        form:
            type: Sylius\Bundle\AdminBundle\Form\Type\ProductType
        permission: true
    type: sylius.resource

With the brand new routing configuration

declare(strict_types=1);

use Sylius\Bundle\AdminBundle\Form\Type\ProductType;
use Sylius\Resource\Metadata\BulkDelete;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Operations;
use Sylius\Resource\Metadata\ResourceMetadata;
use Sylius\Resource\Metadata\Show;
use Sylius\Resource\Metadata\Update;

return (new ResourceMetadata())
    ->withClass('%sylius.model.product.class%')
    ->withRoutePrefix('/admin')
    ->withSection('admin')
    ->withTemplatesDir('@SyliusAdmin/shared/crud')
    ->withFormType(ProductType::class)
    ->withOperations(new Operations([
        new Create(
            redirectToRoute: 'sylius_admin_product_update',
        ),
        new Update(
            redirectToRoute: 'sylius_admin_product_update',
        ),
        new Delete(),
        new BulkDelete(),
        new Index(
            grid: 'sylius_admin_product',
        ),
        new Show(),
    ]))
;

DDD support

The new routing system is more powerful and not coupled to Doctrine entities.
You can use it with any php classes.

<?php

declare(strict_types=1);

namespace App\BookStore\Infrastructure\Sylius\Resource;

use ApiPlatform\Metadata\ApiProperty;
use App\BookStore\Domain\Model\Book;
use App\BookStore\Infrastructure\Sylius\Grid\BookGrid;
use App\BookStore\Infrastructure\Sylius\State\Processor\CreateBookProcessor;
use App\BookStore\Infrastructure\Sylius\State\Processor\DeleteBookProcessor;
use App\BookStore\Infrastructure\Sylius\State\Processor\UpdateBookProcessor;
use App\BookStore\Infrastructure\Sylius\State\Provider\BookBulkItemsProvider;
use App\BookStore\Infrastructure\Sylius\State\Provider\BookItemProvider;
use App\BookStore\Infrastructure\Symfony\Form\BookResourceType;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\BulkDelete;
use Sylius\Resource\Metadata\Create;
use Sylius\Resource\Metadata\Delete;
use Sylius\Resource\Metadata\Index;
use Sylius\Resource\Metadata\Update;
use Sylius\Resource\Model\ResourceInterface;
use Symfony\Component\Uid\AbstractUid;
use Symfony\Component\Validator\Constraints as Assert;

#[AsResource(
    section: 'admin',
    formType: BookResourceType::class,
    templatesDir: '@SyliusAdminUi/crud',
    routePrefix: '/admin',
    driver: false,
    operations: [
        new Create(
            processor: CreateBookProcessor::class,
        ),
        new Update(
            provider: BookItemProvider::class,
            processor: UpdateBookProcessor::class,
        ),
        new Delete(
            provider: BookItemProvider::class,
            processor: DeleteBookProcessor::classRelat,
        ),
        new BulkDelete(
            provider: BookBulkItemsProvider::class,
            processor: DeleteBookProcessor::class,
        ),
        new Index(
            grid: BookGrid::class,
        ),
    ],
)]
final class BookResource implements ResourceInterface
{
    public function __construct(
        #[ApiProperty(identifier: true, readable: false, writable: false)]
        public ?AbstractUid $id = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\Length(min: 1, max: 255, groups: ['create', 'Default'])]
        public ?string $name = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\Length(min: 1, max: 1023, groups: ['create', 'Default'])]
        public ?string $description = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\Length(min: 1, max: 255, groups: ['create', 'Default'])]
        public ?string $author = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\Length(min: 1, max: 65535, groups: ['create', 'Default'])]
        public ?string $content = null,

        #[Assert\NotNull(groups: ['create'])]
        #[Assert\PositiveOrZero(groups: ['create', 'Default'])]
        public ?int $price = null,
    ) {
    }

    public function getId(): ?AbstractUid
    {
        return $this->id;
    }

    public static function fromModel(Book $book): self
    {
        return new self(
            $book->id()->value,
            $book->name()->value,
            $book->description()->value,
            $book->author()->value,
            $book->content()->value,
            $book->price()->amount,
        );
    }
}

This example shows defining the resource entirely via attributes and operations — without relying on Doctrine — enabling DDD-friendly APIs and custom workflows out of the box.

Documentation

See also the Resource Bundle documentation for more details on metadata and operations: https://docs.sylius.com/sylius-stack/resource/index

Main PR

Related PRs on Resource package

Related PRs

ADMIN

Missing route conversions:

SHOP

Shop routes:

BC_LAYER

The PoC was here
#17695

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions