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