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-translationssymfony new pilea --version="8.0.*" --webappcd pileadocker compose up -dsymfony 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_indexDebug Operation Command

@stlgaits


symfony console debug:routerDebug 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 ResourceInterfacesrc/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 ResourceInterfacesrc/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