@stlgaits
Sylius Key Contributor
Sylius Stack lover ❤️
Trivia & cinema quiz maniac 🎞️
Board game nerd 🎲
Ex-teacher turned developer 👩🏫
Gardening addict
@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@stlgaits
@stlgaits
CRUD
paginated, sortable and filterable data table grids
Alternative to EasyAdminBundle & Sonata
@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
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
symfony console debug:router@stlgaits
https://127.0.0.1:8000/admin/
@stlgaits
What do we need ?
@stlgaits
@stlgaits
Sylius Resource Bundle
@stlgaits
Sylius Resource Bundle
Routing & operations (CRUD)
Data model (structure)
@stlgaits
#[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
sylius_resource:
mapping:
paths:
- '%kernel.project_dir%/src/Entity'config/packages/sylius_resource.yaml
Tell Sylius Stack where they live
@stlgaits
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@stlgaits
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;
}
src/Entity/Plant.php
@stlgaits
symfony console sylius:debug:resourcesymfony console sylius:debug:resource app.plant@stlgaits
src/Entity/Plant.php
@stlgaits
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@stlgaits
symfony console debug:router@stlgaits
#[AsResource(
routePrefix: 'admin',
operations: [
new Index(),
],
)]
class Plant implements ResourceInterface
@stlgaits
Docs
@stlgaits
https://stack.sylius.com/resource/index/create_new_resource
@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
src/Entity/Plant.php
@stlgaits
/admin/plants
@stlgaits
Sylius Grid Bundle
@stlgaits
@stlgaits
paginated
data tables
filters
actions
sorting
@stlgaits
symfony console make:grid@stlgaits
#[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()
)
)
;
}
}
@stlgaits
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Component\Grid\Attribute\AsGrid;
#[AsGrid(
resourceClass: Plant::class,
name: 'app_plant',
)]
final class PlantGrid extends AbstractGriduse 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 ResourceInterfacefinal class PlantGrid extends AbstractGrid
public function __invoke(GridBuilderInterface $gridBuilder): void
{
$gridBuilder
->addActionGroup(
MainActionGroup::create(
// CreateAction::create(),
)
)src/Grid/PlantGrid.php
@stlgaits
/admin/plants
@stlgaits
https://stack.sylius.com/grid/index/fields
Create your own custom fields with
Very powerful !
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
enum Health: string
{
case EXCELLENT = 'excellent';
case GOOD = 'good';
case AVERAGE = 'average';
case POOR = 'poor';
case SICK = 'sick';
}
src/Enum/Health.php
@stlgaits
use Sylius\Bundle\GridBundle\Builder\Field\EnumField;
// ...
$gridBuilder
->withFields(
EnumField::create('health')
->setLabel('Health')
->setSortable(true),
// ...
);src/Grid/PlantGrid.php
@stlgaits
@stlgaits
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
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
{{ data }}
@stlgaits
<span class="badge {{ data.getColor() }}">
{{ data.value }}
</span>
templates/plant/grid/field/health.html.twig
@stlgaits
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
<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
= field value
@stlgaits
@stlgaits
https://stack.sylius.com/grid/index/actions
Create your own custom actions with
Item
Bulk
Main
@stlgaits
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
https://127.0.0.1:8000/admin/plants
@stlgaits
use App\Grid\Action\AddPhotoAction;
// ...
$gridBuilder
->addActionGroup(
ItemActionGroup::create(
// ...
AddPhotoAction::create(),
)
);
}
src/Grid/PlantGrid.php
@stlgaits
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
<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
Autocompletes, Entity filters and all that jazz
@stlgaits
https://stack.sylius.com/grid/index/filters#php-recommended
Or create your own custom filters with
@stlgaits
https://stack.sylius.com/grid/index/filters#string
@stlgaits
use Sylius\Bundle\GridBundle\Builder\Filter\StringFilter;
$gridBuilder->withFilters(
// ...
StringFilter::create(
name: 'Search',
fields: ['species.name', 'nickname']
)
->setFormOptions([
'type' => 'contains',
]),
);src/Grid/PlantGrid.php
src/Grid/PlantGrid.php
@stlgaits
$gridBuilder->withFields(
EnumField::create('health')
->setLabel('Health')
->setSortable(true),
);src/Grid/PlantGrid.php
@stlgaits
$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
@stlgaits
$gridBuilder->withFields(
StringField::create('species.name')
->setLabel('app.ui.species')
);src/Grid/PlantGrid.php
@stlgaits
src/Grid/PlantGrid.php
@stlgaits
$gridBuilder->withFilters(
EntityFilter::create(
name: 'species',
resourceClass: Species::class,
fields: ['species']
)
->setLabel('app.ui.species')
)
);src/Grid/PlantGrid.php
@stlgaits
@stlgaits
$gridBuilder->withFilters(
Filter::create(name: 'species', type: SpeciesFilter::class)
->setLabel('app.ui.species')
)
);src/Grid/PlantGrid.php
@stlgaits
ux_entity_autocomplete_admin:
path: '/admin/autocomplete/{alias}'
controller: 'ux.autocomplete.entity_autocomplete_controller'
config/routes/ux_autocomplete.yaml
#[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
#[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
@stlgaits
@stlgaits
Sylius Stack // API Platform
@stlgaits
repository methods
business logic & persisting
Twig, JSON, CSV, redirect...
Decorator chains@stlgaits
item providers
grid providers
processors
pagination
filtering
sorting
persisting
@stlgaits
@stlgaits
https://www.youtube.com/watch?v=KJfOgMsJgc8
@stlgaits
@stlgaits
@stlgaits
@stlgaits
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
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
https://127.0.0.1:8000/admin/plants
@stlgaits
https://127.0.0.1:8000/admin/plants/new
@stlgaits
symfony console make:form src/Form/PlantType.php
@stlgaits
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
@stlgaits
Composable UI
@stlgaits
<div class="container">
{% hook 'form' with { form } %}
</div> Alternative to Twig inheritance
@stlgaits
symfony console sylius:debug:twig-hooksimports:
- { resource: '@SyliusBootstrapAdminUiBundle/config/app.php' }
- { resource: '../sylius/' }
config/packages/sylius_bootstrap_admin_ui.yaml
@stlgaits
@stlgaits
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',
],
],
],
],
]);
};
Plant Resource
config/packages/sylius/twig_hooks/admin/plants/update.php
@stlgaits
@stlgaits
.... and a whole lot more !!!
@stlgaits
internal "HR" dialog tool between staff representatives and company execs
@stlgaits
monitoring dashboard for newspaper articles extracts sent to subscribers
@stlgaits
@stlgaits
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