Introduction to the Sylius Stack

@stlgaits

Sylius Key Contributor

Sylius Stack lover ❤️

Trivia & cinema quiz maniac 🎞️

Board game nerd 🎲

Ex-teacher turned developer 👩‍🏫

Gardening addict

Estelle Gaits

 

 

@stlgaits

 

2 Sylius Key Contributors

Sylius partner since 2022

Sylius, Symfony, AI and a whole lot more 🦕

 

@stlgaits

https://www.youtube.com/watch?v=ObgFab4rLVE

My little plant problem

Plant management dashboard

@stlgaits

Sylius Stack to the rescue !

@stlgaits

CRUD

paginated, sortable and filterable data table grids

Alternative to EasyAdminBundle & Sonata

 

But, I thought Sylius was an e-commerce project ?

 

 

Sylius Stack

@stlgaits

                   

Sylius Grid Bundle

Sylius Resource Bundle

Twig Hooks

Bootstrap Admin UI

Symfony UX & Asset Mapper

Doctrine ORM & DBAL Drivers

State Providers & Processors system

@stlgaits

Install

composer require -W \
  doctrine/orm \
  doctrine/doctrine-bundle \
  pagerfanta/doctrine-orm-adapter \
  symfony/asset-mapper \
  sylius/bootstrap-admin-ui \
  sylius/ui-translations
symfony new pilea --version="8.0.*" --webapp
cd pilea
docker compose up -d
symfony serve -d

@stlgaits

Routes

symfony console debug:router

@stlgaits

https://127.0.0.1:8000/admin/

@stlgaits

Plant CRUD

What do we need ?

@stlgaits

routing & operations

a data model

a data provider 

a Sylius Grid definition for the index operation

Sylius Stack must-haves !

@stlgaits

Resources

Sylius Resource Bundle

@stlgaits

Resources : 2 responsibilities

Sylius Resource Bundle

Routing & operations (CRUD)

Data model (structure)

@stlgaits

Doctrine Entity + Repository

#[ORM\Entity(repositoryClass: PlantRepository::class)]
class Plant
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(type: Types::SIMPLE_ARRAY)]
    private array $commonNames = [];

    #[ORM\Column(length: 255)]
    private ?string $species = null;

    #[ORM\Column(length: 255, nullable: true)]
    private ?string $nickname = null;
symfony console make:entity

@stlgaits

Resources config

 

sylius_resource:
    mapping:
        paths:
            - '%kernel.project_dir%/src/Entity'
config/packages/sylius_resource.yaml

Tell Sylius Stack where they live

@stlgaits

Turn your Entity into a Sylius Resource

use Sylius\Resource\Metadata\AsResource;

#[ORM\Entity(repositoryClass: PlantRepository::class)]
#[AsResource]
class Plant
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;
src/Entity/Plant.php

@stlgaits

 symfony console sylius:debug:resource

Debug Resources

@stlgaits

Turn your Entity into a Resource

src/Entity/Plant.php

@stlgaits

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;

#[ORM\Entity(repositoryClass: PlantRepository::class)]
#[AsResource]
class Plant implements ResourceInterface
{
    public function getId(): ?int
    {
        return $this->id;
    }

Implement ResourceInterface

src/Entity/Plant.php

@stlgaits

 symfony console sylius:debug:resource

Debug Resources Command

symfony console sylius:debug:resource app.plant

@stlgaits

Configure operations

src/Entity/Plant.php

@stlgaits

Index operation

use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Index;

#[AsResource(
    operations: [
        new Index(),
    ],
)]
#[ORM\Entity(repositoryClass: PlantRepository::class)]
class Plant implements ResourceInterface
src/Entity/Plant.php

@stlgaits

symfony console sylius:debug:resource app.plant app_plant_index

Debug  Operation Command

@stlgaits

symfony console debug:router

Debug  Symfony Router

@stlgaits

Route Prefix

#[AsResource(
    routePrefix: 'admin',
    operations: [
        new Index(),
    ],
)]
class Plant implements ResourceInterface

@stlgaits

https://127.0.0.1:8000/admin/plants

Repository

Docs

@stlgaits

https://stack.sylius.com/resource/index/create_new_resource

Repository : CreatePaginatorTrait

@stlgaits

use Sylius\Bundle\ResourceBundle\Doctrine\ORM\CreatePaginatorTrait;

/**
 * @extends ServiceEntityRepository<Plant>
 */
class PlantRepository extends ServiceEntityRepository
{
    use CreatePaginatorTrait;
    
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Plant::class);
    }
src/Repository/PlantRepository.php
#[AsResource(
    templatesDir: '@SyliusAdminUi/crud',
    routePrefix: 'admin',
    operations: [
        new Index(),
    ],
)]
class Plant implements ResourceInterface

Twig Templates Directory

src/Entity/Plant.php

@stlgaits

/admin/plants

@stlgaits

Grids

Sylius Grid Bundle

@stlgaits

Grids

@stlgaits

Grid

paginated

data tables

filters

actions

sorting

@stlgaits

Add a Grid to Index Operation

 

symfony console make:grid

@stlgaits

Grid

 

#[AsGrid(
    resourceClass: Plant::class,
    name: 'app_plant',
)]
final class PlantGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->withFields(
                StringField::create('species')
                    ->setLabel('Species')
                    ->setSortable(true),
                StringField::create('nickname')
                    ->setLabel('Nickname')
                    ->setSortable(true),
            )
            ->addActionGroup(
                MainActionGroup::create(
                    CreateAction::create(),
                )
            )
            ->addActionGroup(
                ItemActionGroup::create(
                    // ShowAction::create(),
                    UpdateAction::create(),
                    DeleteAction::create()
                )
            )
            ->addActionGroup(
                BulkActionGroup::create(
                    DeleteAction::create()
                )
            )
        ;
    }
}

Fields

Filters

Actions

@stlgaits

#[AsGrid] attribute

 

use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;

#[AsGrid(
    resourceClass: Plant::class,
    name: 'app_plant',
)]
final class PlantGrid extends AbstractGrid

Resource : Link up the Grid to Index Operation

use App\Grid\PlantGrid;
use Sylius\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
use Sylius\Resource\Metadata\Index;

#[AsResource(
    operations: [
        new Index(
            grid: PlantGrid::class,
        ),
    ],
)]
#[ORM\Entity(repositoryClass: PlantRepository::class)]
class Plant implements ResourceInterface

Comment out all unused actions for now

final class PlantGrid extends AbstractGrid

	public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
         ->addActionGroup(
                MainActionGroup::create(
            //    CreateAction::create(),
                )
            )
src/Grid/PlantGrid.php

@stlgaits

/admin/plants

Grid

@stlgaits

Grid Fields

 

https://stack.sylius.com/grid/index/fields

String  

DateTime

Enum

Callable

Twig

Create your own custom fields with

 

#[AsField]

Very powerful  !

 

String field

 

use Sylius\Bundle\GridBundle\Builder\Field\StringField;

public function __invoke(GridBuilderInterface $gridBuilder): void
 {
     $gridBuilder
         ->withFields(
         	StringField::create('nickname')
                 ->setLabel('Nickname')
            // ...
      );
}
src/Grid/PlantGrid.php

@stlgaits

Health Enum 

 

enum Health: string
{
    case EXCELLENT = 'excellent';
    case GOOD = 'good';
    case AVERAGE = 'average';
    case POOR = 'poor';
    case SICK = 'sick';
}
src/Enum/Health.php

@stlgaits

Enum field

 

use Sylius\Bundle\GridBundle\Builder\Field\EnumField;

// ... 
 
     $gridBuilder
         ->withFields(
         	EnumField::create('health')
                 ->setLabel('Health')
                 ->setSortable(true),
            // ...
      );
src/Grid/PlantGrid.php

@stlgaits

Make our health field colourful

 

@stlgaits

Health Enum 

 

enum Health: string
{
    case EXCELLENT = 'excellent';
    case GOOD = 'good';
    case AVERAGE = 'average';
    case POOR = 'poor';
    case SICK = 'sick';

    public function getColor(): string
    {
        return match($this) {
            self::EXCELLENT => 'bg-green-lt',
            self::GOOD => 'bg-lime-lt',
            self::AVERAGE => 'bg-yellow-lt',
            self::POOR => 'bg-orange-lt',
            self::SICK => 'bg-red-lt',
        };
    }
}
src/Enum/Health.php

Bootstrap 5 classes

Replace Enum field with Twig field

 

use Sylius\Bundle\GridBundle\Builder\Field\EnumField;

public function __invoke(GridBuilderInterface $gridBuilder): void
 {
     $gridBuilder
         ->withFields(
  				TwigField::create(
                	name: 'health',
                	template: 'plant/grid/field/health.html.twig'
                ),
            // ...
      );
}
src/Grid/PlantGrid.php

Accessing grid field data in Twig template

 

{{ data }}

@stlgaits

Health Enum Twig template

 

<span class="badge {{ data.getColor() }}">
	{{ data.value }}
</span>
templates/plant/grid/field/health.html.twig

case value

@stlgaits

Colours !

 

Twig field : images

 

use Sylius\Bundle\GridBundle\Builder\Field\TwigField;

public function __invoke(GridBuilderInterface $gridBuilder): void
 {
     $gridBuilder
         ->withFields(
   	           TwigField::create(
               		name: 'photos',
               		template: 'plant/grid/field/image.html.twig'
               ) 
             	->setLabel('Photos'),
            // ...
      );
}
src/Grid/PlantGrid.php

Twig field : template

 

<div style="width: 200px;">
{% for photo in data %}
        <img src="{{ vich_uploader_asset(photo, 'imageFile') }}"
             alt="Plant photo"
             class="img-thumbnail img-fluid"
        />
{% endfor %}
</div>
templates/plant/grid/field/image.html.twig

data

= field value

@stlgaits

Twig field for photo column

 

@stlgaits

Actions

 

https://stack.sylius.com/grid/index/actions

Create

Delete

Update

Show

ApplyTransition

Create your own custom actions  with

 

ActionInterface

Item

Bulk

Main

Delete

@stlgaits

Item Actions

 

use Sylius\Bundle\GridBundle\Builder\ActionGroup\ItemActionGroup;

final class PlantGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
            ->addActionGroup(
                ItemActionGroup::create(
                 	ShowAction::create(),
                    UpdateAction::create(),
                    DeleteAction::create(),
                )
            );
    }
}
src/Grid/PlantGrid.php

@stlgaits

Item Actions

 

https://127.0.0.1:8000/admin/plants

@stlgaits

Custom item action : add photo

 

use App\Grid\Action\AddPhotoAction;

// ... 
        $gridBuilder
            ->addActionGroup(
                ItemActionGroup::create(
                // ...
                    AddPhotoAction::create(),
                )
            );
    }
src/Grid/PlantGrid.php

@stlgaits

Custom item action : add photo

 

use Sylius\Bundle\GridBundle\Builder\Action\Action;
use Sylius\Bundle\GridBundle\Builder\Action\ActionInterface;

class AddPhotoAction
{
    public static function create(): ActionInterface
    {
        $action = Action::create('add_photo', 'add_photo');
        $action->setLabel('app.ui.add_photo');
        $action->setTemplate('plant/photo/action/add_photo.html.twig');

        return $action;
    }
}
src/Grid/Action/AddPhotoAction.php

@stlgaits

Custom item action : add photo

 

<a title="details"
	href="{{ path('app_photo_create', {plant: data.id}) }}"
	class="btn btn-icon"
	data-bs-toggle="tooltip" 
	data-bs-title="Add photo">
    {{ ux_icon(action.icon|default('ic:round-add-a-photo')) }}
</a>
templates/plant/photo/action/add_photo.html.twig

@stlgaits

Filters

Autocompletes, Entity filters and all that jazz

@stlgaits

Filters

 

https://stack.sylius.com/grid/index/filters#php-recommended

String  with search options

Date

Select

Exists

Money

Entity

Boolean

Or create your own custom filters with 

 

#[AsFilter]

@stlgaits

Search with StringFilter

 

https://stack.sylius.com/grid/index/filters#string

@stlgaits

Search filter

 

use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter;

$gridBuilder->withFilters(
	// ...
     StringFilter::create(
     	name: 'Search', 
     	fields: ['species.name', 'nickname']
     )
     	->setFormOptions([
        	'type' => 'contains',
        ]),
);
src/Grid/PlantGrid.php

pick search options

search across multiple fields

src/Grid/PlantGrid.php

@stlgaits

Plant health : EnumField

 

$gridBuilder->withFields(

	EnumField::create('health')
		->setLabel('Health')
		->setSortable(true),
  
);
src/Grid/PlantGrid.php

@stlgaits

Filtering plants by health : EnumFilter

 

$gridBuilder->withFilters(

 EnumFilter::create(name: 'health', enumClass: Health::class, field: 'health')
  ->addFormOption(
  	option:'choice_label',
  	value: fn (Health $choice): string => ucfirst($choice->value)
  )
  ->setLabel('Health')
  
);
src/Grid/PlantGrid.php

@stlgaits

Filtering plants by health : EnumFilter

 

@stlgaits

Species

 

$gridBuilder->withFields(
  StringField::create('species.name')
     ->setLabel('app.ui.species')
);
src/Grid/PlantGrid.php

@stlgaits

Filtering plants by species : EntityFilter

 

src/Grid/PlantGrid.php

@stlgaits

Filtering plants by species : EntityFilter

 

$gridBuilder->withFilters(
	EntityFilter::create(
		name: 'species',
		resourceClass: Species::class,
		fields: ['species']
    	)
     ->setLabel('app.ui.species')
    )
);
src/Grid/PlantGrid.php

@stlgaits

EntityFilter

 

@stlgaits

Autocomplete EntityFilter

 

$gridBuilder->withFilters(
     Filter::create(name: 'species', type: SpeciesFilter::class)
     ->setLabel('app.ui.species')
    )
);
src/Grid/PlantGrid.php

@stlgaits

Entity autocomplete route

 

ux_entity_autocomplete_admin:
    path: '/admin/autocomplete/{alias}'
    controller: 'ux.autocomplete.entity_autocomplete_controller'
config/routes/ux_autocomplete.yaml

Autocomplete Symfony Form Type

 

#[AsEntityAutocompleteField(
    alias: 'app_admin_species',
    route: 'ux_entity_autocomplete_admin',
)]
class SpeciesAutocompleteType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'class' => Species::class,
            'choice_label' => 'name',
            'placeholder' => 'sylius.ui.all',
        ]);
    }

    public function getParent(): string
    {
        return BaseEntityAutocompleteType::class;
    }
}
src/Form/SpeciesAutocompleteType.php

Autocomplete EntityFilter

 

#[AsFilter(
    formType: SpeciesAutocompleteType::class,
    template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig',
)]
final readonly class SpeciesFilter implements FilterInterface
{
    public function __construct(
        private EntityFilter $entityFilter,
    ) {
    }

    public function apply(DataSourceInterface $dataSource, string $name, mixed $data, array $options): void
    {
        $this->entityFilter->apply($dataSource, $name, $data, $options);
    }
}
src/Grid/Filter/SpeciesFilter.php

Entity auto

complete filter

 

@stlgaits

routing & operations

a data model

a data provider 

a Sylius Grid definition for the index operation

Sylius Stack must-haves !

@stlgaits

Providers & Processors

Sylius Stack // API Platform 

State

@stlgaits

Resource Lifecycle

Request

Routing & operation metadata

Providers

Processors 

Responder 

Main Controller

repository methods

business logic & persisting

Twig, JSON, CSV, redirect...

Decorator chains

Providers & processors

@stlgaits

Default Doctrine ORM

item providers

grid providers

processors

pagination

filtering

sorting

persisting

Doctrine Data

Provider is not

compulsory

@stlgaits

@stlgaits

Don't need Doctrine ?

Want a custom API data provider ?

 

https://www.youtube.com/watch?v=KJfOgMsJgc8

@stlgaits

Species

Pl@ntNet

API Provider

@stlgaits

@stlgaits

Adding a new plant : create operation

@stlgaits

Add create action link in grid

use Sylius\Bundle\GridBundle\Builder\Action\CreateAction;

#[AsGrid(
    resourceClass: Plant::class,
)]
final class PlantGrid extends AbstractGrid
{
    public function __invoke(GridBuilderInterface $gridBuilder): void
    {
        $gridBuilder
          ->addActionGroup(
                MainActionGroup::create(
                    CreateAction::create(),
                )
            ) 
     }
}
src/Grid/PlantGrid.php

@stlgaits

Add create operation in resource

use Sylius\Resource\Metadata\Create;

#[AsResource(
    templatesDir: '@SyliusAdminUi/crud',
    routePrefix: 'admin',
    operations: [
        new Show(),
        new Create(),
    ],
)]
#[ORM\Entity(repositoryClass: PlantRepository::class)]
class Plant implements ResourceInterface
src/Entity/Plant.php

@stlgaits

Create button in Grid

https://127.0.0.1:8000/admin/plants

@stlgaits

Default form with validation

https://127.0.0.1:8000/admin/plants/new

@stlgaits

Create Symfony Form Type

symfony console make:form 
src/Form/PlantType.php

@stlgaits

Add Form Type to Resource

use App\Form\PlantType;

#[AsResource(
    formType: PlantType::class,
    templatesDir: '@SyliusAdminUi/crud',
    routePrefix: 'admin',
    operations: [
        new Create(),
    ],
)]
#[ORM\Entity(repositoryClass: PlantRepository::class)]
class Plant implements ResourceInterface
src/Entity/Plant.php

@stlgaits

UI: let's customise breadcrumbs

@stlgaits

Twig Hooks

Composable UI 

@stlgaits

Twig Hooks

  • Twig templates
  • Symfony UX Twig / Live Components

 

 

<div class="container">
	{% hook 'form' with { form } %}
</div>    

Alternative to Twig inheritance

 

 

Hookables

 

 

Hooks

 

 

Twig Hooks accessible from SF profliler

@stlgaits

Twig Hooks debug command

  

symfony console sylius:debug:twig-hooks

Import your custom Twig Hooks config

imports:
    - { resource: '@SyliusBootstrapAdminUiBundle/config/app.php' }
    - { resource: '../sylius/' }
config/packages/sylius_bootstrap_admin_ui.yaml

@stlgaits

@stlgaits

Twig Hooks: customising breadcrumbs

return static function (ContainerConfigurator $container): void {
    $container->extension('sylius_twig_hooks', [
        'hooks' => [
            'sylius_admin.plant.update.content.header' => [
                'breadcrumbs' => [
                    'template' => '@SyliusBootstrapAdminUi/shared/crud/update/content/header/breadcrumbs.html.twig',
                    'configuration' => [
                        'rendered_field' => 'nickname',
                    ],
                ],
            ],
         ],
    ]);
};

rendered_field

Plant Resource

config/packages/sylius/twig_hooks/admin/plants/update.php

@stlgaits

Twig Hooks: customising breadcrumbs

@stlgaits

Going further...

  • Twig hooks tricks
  • customising the sidebar menu
  • State machines
  • Custom (API) data providers and filtering
  • Twig Components
  • Live Components
  • custom redirect routes in Resource

 

.... and a whole lot more !!! 

@stlgaits

Real life example 1

internal "HR" dialog tool between staff representatives and company execs

 

 

 

 

 

@stlgaits

monitoring dashboard for newspaper articles extracts sent to subscribers

 

 

 

 

 

@stlgaits

Wanna help ? Please contribute !

https://stack.sylius.com/

 

https://github.com/Sylius/Stack

https://github.com/Sylius/SyliusResourceBundle

https://github.com/Sylius/SyliusGridBundle

@stlgaits

Thanks ! Want moar ?!

Demo project for this talk

https://github.com/stlgaits/pilea

Sylius Stack blog article by Florian Merle (Sylius Key Contributor)

https://www.akawaka.fr/bring-sylius-components-to-your-project/

https://github.com/Florian-Merle/sylius-stack-demo/

Sylius Stack Documentation

https://stack.sylius.com

Latest release

https://github.com/Sylius/Stack/releases/tag/v0.12.2

Pole Position on the Grid - Loïc Frémont & Estelle Gaits : a Sylius Stack API-based project (without Doctrine !)

https://www.youtube.com/watch?v=KJfOgMsJgc8

Introduction to the Sylius Stack - phpday Verona 2026

By Estelle Gaits

Introduction to the Sylius Stack - phpday Verona 2026

Ever heard of Sylius, the increasingly popular eCommerce framework? Well, forget everything you know about shopping carts for a moment and let’s look under the hood. Let me introduce you to the Sylius Stack, the new admin bundle in town ! It is a set of powerful components and bundles that form the foundation of Sylius itself, but work brilliantly for any (non-eCommerce) project. By leveraging its powerful Resource and Grid systems, you can build a lean, mean, and highly customizable admin panel UI in record time. In this session, we will explore how to fetch your data with or without the Doctrine ORM via the Providers and Processors system. We will turn entities into full-featured CRUD interface and use these core building blocks to define complex, filterable data tables through simple configuration and awesome command line tools.

  • 72