Développez sous Sylius 2 en 40 minutes top chrono

---

---

---

---

---
# Sylius 1 vs 2
- API Platform 3 à 4
- Semantic UI vers Bootstrap
- Winzou state machine vers SF Workflow
- Utilisation des Live components
- Création des Twig Hooks
- ...
---
# Passons à l'action !
## Présenter ma collection de jeux vidéos
---
```mermaid
erDiagram
app_constructor {
int id PK "Clé primaire"
varchar(255) name "Nom du constructeur"
varchar(255) logo "Chemin du logo"
}
app_console {
int id PK "Clé primaire"
int constructor_id FK "Clé étrangère vers app_constructor"
varchar(255) name "Nom de la console"
varchar(255) logo "Chemin du logo"
}
app_game {
int id PK "Clé primaire"
varchar(255) cover "Chemin de la jaquette"
}
app_game_translation {
int id PK "Clé primaire"
int translatable_id FK "Clé étrangère vers app_game"
varchar(255) name "Nom du jeu (traduit)"
varchar(255) locale "Langue (ex: fr, en)"
}
app_constructor ||--o{ app_console : "fabrique"
app_game }o--o{ app_console : "est disponible sur"
app_game ||--o{ app_game_translation : "possède"
app_console ||--|{ app_game_console : "possède"
```
---
# Avant tout, setup du projet
---
```bash
# Makefile - Pull et build docker
cd infra/dev && docker-compose -p sylius pull
cd infra/dev && docker-compose -p sylius build --pull
```
---
```yaml
# infra/dev/docker-compose.yaml
services:
db:
image: "mysql:8"
volumes:
- "database:/var/lib/mysql:rw,cached"
ports:
- "3306:3306"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 1
MYSQL_DATABASE: "sylius_dev"
mail:
image: "monsieurbiz/mailcatcher"
ports:
- "1080:1080"
- "1025:1025"
volumes:
database: {}
```
---
``` bash
# Makefile - Liaison des domaines .wip
for domain in apps/sylius:sylius; \
do \
folder=`echo $domain | cut -d: -f 1`; \
host=`echo $domain | cut -d: -f 2 | sed 's/,/ /g'`; \
if [[ "local:proxy:domain:attach $host || true" != "" ]]; then echo
"(cd $folder && symfony local:proxy:domain:attach $host || true)";
(cd $folder && symfony local:proxy:domain:attach $host || true); else
echo "(cd apps/sylius && symfony $folder)"; (cd apps/sylius && symfony $folder);
fi; \
done;
```
---
``` bash
# Makefile - Lancement des containers et du serveur local
cd infra/dev && docker-compose -p sylius up -d
cd apps/sylius && symfony local:server:start -d
```
---
``` bash
# Makefile - Setup de la base de données
cd apps/sylius && symfony console doctrine:database:drop --if-exists --force
cd apps/sylius && symfony console doctrine:database:create --if-not-exists
cd apps/sylius && symfony console doctrine:migrations:migrate -n
```
---
``` bash
# Makefile - Installation des données de test
cd apps/sylius && symfony console sylius:fixtures:load -n default -v
```
---
``` bash
# Makefile - Setup des messages asynchrones et JWT
cd apps/sylius && symfony console messenger:setup-transports
cd apps/sylius && symfony console lexik:jwt:generate-keypair --skip-if-exists
# Makefile - Build des thèmes
cd apps/sylius && yarn encore prod
# Makefile - Installation des assets
cd apps/sylius && symfony console sylius:install:assets
cd apps/sylius && symfony console assets:install --symlink --relative
```
---

---

---
# Installation de 2 plugins

---
## Media Manager
```bash
symfony composer require
monsieurbiz/sylius-media-manager-plugin="^2.0.0"
```
---

---
## Rich Editor
```bash
# Dans le dossier apps/sylius
symfony composer require
monsieurbiz/sylius-rich-editor-plugin="^3.0.0"
```
---

---
# Passons à l'action !
## Pour de vrai
---
# Que va-t-on faire ?
---
- ✅ Setup du projet
- Resources Sylius
- Grid
- CRUD
- Fixtures
- Controller resources front
- Twig Hooks
- Et plus ?
---
# Resources Sylius
---
## Constructor
---
```mermaid
erDiagram
app_constructor {
int id PK "Clé primaire"
varchar(255) name "Nom du constructeur"
varchar(255) logo "Chemin du logo"
}
```
---
```php
// apps/sylius/src/Entity/Game/Constructor.php
namespace App\Entity\Game;
// ...
use Sylius\Component\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
// ...
#[ORM\Entity(repositoryClass: ConstructorRepository::class)]
#[ORM\Table(name: 'app_constructor')]
#[AsResource(alias: 'app.constructor')] // ✅ AsResource Sylius
class Constructor implements ResourceInterface // ✅ Implémente ResourceInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[Assert\NotBlank()]
#[ORM\Column(length: 255, nullable: true)]
private ?string $name = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $logo = null;
// ...
```
---
## Console
---
```mermaid
erDiagram
app_constructor {
int id PK "Clé primaire"
varchar(255) name "Nom du constructeur"
varchar(255) logo "Chemin du logo"
}
app_console {
int id PK "Clé primaire"
int constructor_id FK "Clé étrangère vers app_constructor"
varchar(255) name "Nom de la console"
varchar(255) logo "Chemin du logo"
}
app_constructor ||--o{ app_console : "fabrique"
```
---
```php
// apps/sylius/src/Entity/Game/Console.php
namespace App\Entity\Game;
// ...
use Sylius\Component\Resource\Model\ResourceInterface;
use Sylius\Resource\Metadata\AsResource;
// ...
#[ORM\Entity(repositoryClass: ConsoleRepository::class)]
#[ORM\Table(name: 'app_console')]
#[AsResource(alias: 'app.console')] // ✅ AsResource Sylius
class Console implements ResourceInterface // ✅ Implémente ResourceInterface
{ // ...
#[Assert\Count(min: 1)]
#[ORM\ManyToOne(targetEntity: Constructor::class, inversedBy: 'consoles')]
#[ORM\JoinColumn(name: 'constructor_id', referencedColumnName: 'id', nullable: true)]
private ?ConstructorInterface $constructor = null;
public function getConstructor(): ?ConstructorInterface { /* ... */ }
public function setConstructor(?ConstructorInterface $constructor): void { /* ... */ }
// ...
}
```
---
## Game
### Et les traductions !
---
```mermaid
erDiagram
app_constructor {
int id PK "Clé primaire"
varchar(255) name "Nom du constructeur"
varchar(255) logo "Chemin du logo"
}
app_console {
int id PK "Clé primaire"
int constructor_id FK "Clé étrangère vers app_constructor"
varchar(255) name "Nom de la console"
varchar(255) logo "Chemin du logo"
}
app_game {
int id PK "Clé primaire"
varchar(255) cover "Chemin de la jaquette"
}
app_game_console {
int game_id PK, FK "Clé primaire et étrangère vers app_game"
int console_id PK, FK "Clé primaire et étrangère vers app_console"
}
app_game_translation {
int id PK "Clé primaire"
int translatable_id FK "Clé étrangère vers app_game"
varchar(255) locale "Langue (ex: fr, en)"
varchar(255) name "Nom du jeu (traduit)"
}
app_constructor ||--o{ app_console : "fabrique"
app_game ||--|{ app_game_console : "est disponible sur"
app_game ||--o{ app_game_translation : "possède"
app_console ||--|{ app_game_console : "possède"
```
---
## GameTranslation
---
```php
// apps/sylius/src/Entity/Game/GameTranslation.php
namespace App\Entity\Game;
// ...
use Sylius\Resource\Model\AbstractTranslation;
use Sylius\Component\Resource\Model\ResourceInterface;
use Sylius\Component\Resource\Model\TranslationInterface;
//...
#[ORM\Entity]
#[ORM\Table(name: 'app_game_translation')]
// 😱 Pas de AsResource !?
// ✅ Implémente ResourceInterface et TranslationInterface
class GameTranslation extends AbstractTranslation implements TranslationInterface, ResourceInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[Assert\NotBlank()]
#[ORM\Column(length: 255, nullable: true)]
private ?string $name = null;
// ...
}
```
---
### Game
---
```php
// apps/sylius/src/Entity/Game/Game.php
namespace App\Entity\Game;
// ...
use Sylius\Component\Resource\Model\ResourceInterface;
use Sylius\Component\Resource\Model\TranslatableInterface;
// ...
#[ORM\Entity(repositoryClass: GameRepository::class)]
#[ORM\Table(name: 'app_game')]
#[AsResource(alias: 'app.game')] // ✅ AsResource Sylius
// ✅ Implémente ResourceInterface et TranslatableInterface
class Game implements ResourceInterface, TranslatableInterface
{ // ✅ Utilise le TranslatableTrait de Sylius
use TranslatableTrait {
__construct as private initializeTranslationsCollection;
getTranslation as private doGetTranslation;
}
#[Assert\Count(min: 1)]
#[ORM\JoinTable(name: 'app_game_console')]
#[ORM\ManyToMany(targetEntity: Console::class)]
private Collection $consoles;
public function __construct()
{
$this->initializeTranslationsCollection();
$this->consoles = new ArrayCollection();
}
// ...
}
```
---
## Resource avec traduction
---
```yaml
# apps/sylius/config/packages/sylius_resource.yaml
sylius_resource:
resources:
app.game:
classes:
model: App\Entity\Game\Game
# Pas encore possible en PHP Attribute
translation:
classes:
model: App\Entity\Game\GameTranslation
```
---
## Debug de resources
---
```bash
± sf console sylius:debug:resource
---------------------------------------------
Alias
---------------------------------------------
app.console
app.constructor
app.game
sylius.address
sylius.address_log_entry
sylius.adjustment
etc...
```
---
```
± sf console sylius:debug:resource app.game
Resource Metadata
-----------------
------------------------ ------------------------
Option Value
------------------------ ------------------------
alias "app.game"
section null
formType null
templatesDir null
routePrefix null
name "game"
pluralName null
applicationName "app"
identifier null
normalizationContext null
denormalizationContext null
validationContext null
class "App\Entity\Game\Game"
driver null
vars null
------------------------ ------------------------
```
---
## Services autogénérés
---
```bash
± sf console debug:container | grep game
# Entity managers
Doctrine\ORM\EntityManagerInterface $gameManager
alias for "doctrine.orm.default_entity_manager"
Doctrine\ORM\EntityManagerInterface $gameTranslationManager
alias for "doctrine.orm.default_entity_manager"
# Factories
Sylius\Component\Resource\Factory\FactoryInterface $gameFactory
alias for "app.factory.game"
Sylius\Component\Resource\Factory\FactoryInterface $gameTranslationFactory
alias for "app.factory.game_translation"
# Repositories
Sylius\Component\Resource\Repository\RepositoryInterface $gameRepository
alias for "app.repository.game"
Sylius\Component\Resource\Repository\RepositoryInterface $gameTranslationRepository
alias for "app.repository.game_translation"
# Controller
app.controller.game
Sylius\Bundle\ResourceBundle\Controller\ResourceController
```
---
## Repositories
---
```php
// apps/sylius/src/Repository/Game/GameRepository.php
namespace App\Repository\Game;
// ...
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Sylius\Component\Resource\Repository\RepositoryInterface;
// ...
// ✅ Implémente RepositoryInterface et étend ServiceEntityRepository
class GameRepository extends ServiceEntityRepository implements RepositoryInterface
{
use ResourceRepositoryTrait; // ✅ Trait Sylius pour les repositories de resources
public function __construct(ManagerRegistry $registry)
{
// On donne la classe de l'entité
parent::__construct($registry, Game::class);
}
public function createListQueryBuilder(string $localeCode): QueryBuilder
{
// Dans le cas d'une resource avec traduction, on joint la table de traduction selon la langue
return $this->createQueryBuilder('o')
->addSelect('translation')
->leftJoin('o.translations', 'translation', 'WITH', 'translation.locale = :localeCode')
->setParameter('localeCode', $localeCode)
;
}
}
```
---
# Grids
---
## Une grid Sylius c'est quoi ?
---

---

---

---
## Déclaration d'une grid
---
```php
// apps/sylius/src/Grid/Game/ConsoleGrid.php
namespace App\Grid\Game;
// ...
use Sylius\Bundle\GridBundle\Grid\AbstractGrid;
use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface;
// ...
// ✅ Étend AbstractGrid et implémente ResourceAwareGridInterface
class ConsoleGrid extends AbstractGrid implements ResourceAwareGridInterface
{
public static function getName(): string
{
// Le nom de la grid
return 'app_console';
}
public function getResourceClass(): string
{
// La classe de la resource
return Console::class;
}
public function buildGrid(GridBuilderInterface $gridBuilder): void
{
// ...
}
}
```
---
```php
// apps/sylius/src/Grid/Game/ConsoleGrid.php
public function buildGrid(GridBuilderInterface $gridBuilder): void
{
$gridBuilder
->addOrderBy('name', 'asc')
->addField(
StringField::create('name')->setLabel('app.ui.name')->setSortable(true)
)
->addField(
StringField::create('constructor')
->setPath('constructor.name')
->setLabel('app.ui.constructor')
->setSortable(true, 'constructor.name')
)
// ...
;
}
```
---
```php
// apps/sylius/src/Grid/Game/ConsoleGrid.php
public function buildGrid(GridBuilderInterface $gridBuilder): void
{
$gridBuilder
// ...
->addActionGroup(
MainActionGroup::create( CreateAction::create() )
)
->addActionGroup(
ItemActionGroup::create( UpdateAction::create(), DeleteAction::create())
)
->addActionGroup(
BulkActionGroup::create( DeleteAction::create() )
)
;
}
```
---
```php
// apps/sylius/src/Grid/Game/ConsoleGrid.php
public function buildGrid(GridBuilderInterface $gridBuilder): void
{
$gridBuilder
// ...
->addFilter( StringFilter::create('name')->setLabel('app.ui.name') )
->addFilter(
EntityFilter::create('constructor', Constructor::class)
->setLabel('app.ui.constructor')
)
;
}
```
---
## Demo
---

---
- ✅ Setup du projet
- ✅ Resources Sylius
- ✅ Grid
- CRUD
- Fixtures
- Controller resources front
- Twig Hooks
- Et plus ?
---
# CRUD
## Gérer ses contenus en BO
---
```php
// apps/sylius/src/Entity/Game/Game.php
namespace App\Entity\Game;
//...
#[AsResource]
#[SyliusCrudRoutes(
alias: 'app.game',
except: ['show'],
/** only: ['index', 'create', 'update', 'delete', 'bulk_delete'] */
form: GameType::class,
grid: GameGrid::class,
path: '/%sylius_admin.path_name%/games',
section: 'admin',
templates: '@SyliusAdmin/shared/crud',
)]
class Game implements GameInterface
{
//...
```
---
```bash
± sf console debug:router | grep game
app_admin_game_index GET ANY ANY /admin/games/
app_admin_game_create GET|POST ANY ANY /admin/games/new
app_admin_game_update GET|PUT|PATCH ANY ANY /admin/games/{id}/edit
app_admin_game_bulk_delete DELETE ANY ANY /admin/games/bulk-delete
app_admin_game_delete DELETE ANY ANY /admin/games/{id}
```
---
## Et les APIs ?
---
## Routes APIs simples
---
```yaml
app_constructor:
resource: |
alias: app.constructor
type: sylius.resource_api
```
---
```bash
± sf console debug:router | grep constructor
...
app_constructor_index GET ANY ANY /constructors/
app_constructor_create POST ANY ANY /constructors/
app_constructor_update PUT|PATCH ANY ANY /constructors/{id}
app_constructor_show GET ANY ANY /constructors/{id}
app_constructor_delete DELETE ANY ANY /constructors/{id}
```
---
## Routes APIs avec API Platform
---
```php
namespace App\Entity\Game;
use ApiPlatform\Metadata\ApiResource;
// ...
#[AsResource]
#[ApiResource]
// ...
class Constructor implements ConstructorInterface
```
---

---
## Ajouter lien du menu
---
```php
// apps/sylius/src/Ui/Menu/AdminMenuListener.php
namespace App\Ui\Menu;
// ...
use Sylius\Bundle\AdminBundle\Menu\MainMenuBuilder;
// ...
#[AsEventListener(event: MainMenuBuilder::EVENT_NAME)]
final class AdminMenuListener
{
public function __invoke(MenuBuilderEvent $event): void
{
$gameSection = $menu
->addChild('app_games')
->setLabel('app.menu.games.section')
->setLabelAttribute('icon', 'tabler:device-gamepad-2');
$gameSection
->addChild('app_constructor', ['route' => 'app_admin_constructor_index'])
->setLabel('app.menu.games.constructor');
$gameSection
->addChild('app_console', ['route' => 'app_admin_console_index'])
->setLabel('app.menu.games.console');
$gameSection
->addChild('app_game', ['route' => 'app_admin_game_index'])
->setLabel('app.menu.games.game');
}
}
```
---
## Formulaire de ressource
---
```php
// apps/sylius/src/Form/Type/Game/ConsoleType.php
namespace App\Form\Type\Game;
// ...
use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use MonsieurBiz\SyliusMediaManagerPlugin\Form\Type\ImageType;
//...
// ✅ Étend AbstractResourceType
class ConsoleType extends AbstractResourceType
{
public function __construct(
/** ✅ On autowire la classe de l'entité */
/** app.console => app.model.console.class */
#[Autowire('%app.model.console.class%')]
string $dataClass,
array $validationGroups = [],
) {
parent::__construct($dataClass, $validationGroups);
}
// ...
}
```
---
```php
// apps/sylius/src/Form/Type/Game/ConsoleType.php
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'app.ui.name',
'required' => true,
])
->add('constructor', EntityType::class, [
'class' => Constructor::class,
'label' => 'app.ui.constructor',
'required' => false,
'choice_label' => 'name',
'autocomplete' => true,
'query_builder' => function (EntityRepository $repository) {
return $repository->createQueryBuilder('o')
->orderBy('o.name', 'ASC')
;
},
])
->add('logo', ImageType::class, [ // ImageType du Media Manager
'label' => 'app.ui.logo',
'required' => false,
])
; // ...
} // ...
```
---
```php
// apps/sylius/src/Form/Type/Game/GameType.php
namespace App\Form\Type\Game;
//...
use Sylius\Bundle\ResourceBundle\Form\Type\ResourceTranslationsType;
// ...
class GameType extends AbstractResourceType
{ // ...
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// ...
->add('translations', ResourceTranslationsType::class, [
/** GameTranslationType Form Type avec un champ `name` **/
'entry_type' => GameTranslationType::class,
])
// ...
;
}
}
```
---
## Demo
---

---
# Fixtures
## De la donnée dès le départ
---
```php
// apps/sylius/src/Fixture/Game/ConsoleFixture.php
namespace App\Fixture\Game;
//...
use App\Fixture\Factory\Game\ConsoleFixtureFactory;
use Doctrine\ORM\EntityManagerInterface;
use Sylius\Bundle\CoreBundle\Fixture\AbstractResourceFixture;
// ✅ Étend AbstractResourceFixture
class ConsoleFixture extends AbstractResourceFixture
{
public function __construct(
EntityManagerInterface $consoleManager, // ✅ Manager fourni par Sylius Resource
ConsoleFixtureFactory $consoleFixtureFactory, // ✅ Notre factory à créer ensuite
) {
parent::__construct($consoleManager, $consoleFixtureFactory);
}
public function getName(): string
{ // Nom de la fixture
return 'app_console';
}
protected function configureResourceNode(ArrayNodeDefinition $resourceNode): void
{ // Format du contenu YAML
$resourceNode->children()
->scalarNode('name')->cannotBeEmpty()->end()
->scalarNode('logo')->defaultNull()->end()
->scalarNode('constructor')->defaultNull()->end()
->end();
}
}
```
---
```php
// apps/sylius/src/Fixture/Factory/Game/ConsoleFixtureFactory.php
namespace App\Fixture\Factory\Game;
// ...
// ✅ Étend AbstractExampleFactory et implémente ExampleFactoryInterface
class ConsoleFixtureFactory extends AbstractExampleFactory implements ExampleFactoryInterface
{ // ...
protected function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setRequired('name')
->setDefault('name', fn (Options $options): string => $this->faker->unique()->company)
->setAllowedTypes('name', 'string')
->setRequired('logo')
->setDefault('logo', null)
->setAllowedTypes('logo', ['null', 'string'])
->setDefault('constructor', LazyOption::randomOne($this->constructorRepository))
->setAllowedTypes('constructor', ['null', 'string', ConstructorInterface::class])
->setNormalizer('constructor', LazyOption::getOneBy($this->constructorRepository, 'name'))
;
}
// ...
}
```
---
```php
// apps/sylius/src/Fixture/Factory/Game/ConsoleFixtureFactory.php
namespace App\Fixture\Factory\Game;
// ...
class ConsoleFixtureFactory extends AbstractExampleFactory implements ExampleFactoryInterface
{
public function create(array $options = []): ConsoleInterface
{
$options = $this->optionsResolver->resolve($options);
/** @var ConsoleInterface $console */
$console = $this->consoleFactory->createNew();
$console->setName($options['name']);
$console->setLogo($options['logo']);
$console->setConstructor($options['constructor']); // Objet Constructor
return $console;
}
}
```
---
```yaml
# apps/sylius/config/fixtures/console.yaml
sylius_fixtures:
suites:
default: # Suite de fixtures par défaut dans notre exemple
fixtures:
app_console_file:
# Nom de la fixture
name: monsieurbiz_rich_editor_file
options:
files:
- source_path: 'config/fixtures/console/3ds.png'
target_path: 'gallery/images/console/3ds.png'
- source_path: 'config/fixtures/console/dreamcast.png'
target_path: 'gallery/images/console/dreamcast.png'
# ...
app_console: # Si pas de `name` la clé est prise comme nom de fixtures
options:
custom:
- name: 'NES'
constructor: 'Nintendo'
logo: 'gallery/images/console/nes.png'
- name: 'Super NES'
constructor: 'Nintendo'
logo: 'gallery/images/console/snes.png'
# ...
```
---
```yaml
# apps/sylius/config/fixtures.yaml
imports:
- { resource: 'fixtures/constructor.yaml' }
- { resource: 'fixtures/console.yaml' } # Important après constructor
- { resource: 'fixtures/game.yaml' } # Important après console
```
---
```yaml
# apps/sylius/config/fixtures/game.yaml
sylius_fixtures:
suites:
default:
fixtures:
app_game_file:
name: monsieurbiz_rich_editor_file
options:
files:
- source_path: 'config/fixtures/game/placeholder.jpg'
target_path: 'gallery/images/game/placeholder.jpg'
app_game:
options:
random: 200 # Aléatoire
prototype:
# Mais tout le monde a la même image
cover: 'gallery/images/game/placeholder.jpg'
```
---
- ✅ Setup du projet
- ✅ Resources Sylius
- ✅ Grid
- ✅ CRUD
- ✅ Fixtures
- Controller resources front
- Twig Hooks
- Et plus ? Tic tac le temps passe !
---
# Affichage en front
---
# Liste des jeux
---
## Route
---
```yaml
# apps/sylius/config/routes/game.yaml
app_game_index:
path: /{_locale}/games
methods: [ GET ]
requirements:
_locale: ^[A-Za-z]{2,4}(_([A-Za-z]{4}|[0-9]{3}))?(_([A-Za-z]{2}|[0-9]{3}))?$
defaults:
# app.controller.game : Resource controller autogénéré
# indexAction : méthode pour lister les ressources (ici les jeux)
_controller: app.controller.game::indexAction
_sylius:
grid: app_shop_game # Nom de la grid
template: 'shop/game/index.html.twig' # Template de rendu
```
---
## Grid
---
```php
// apps/sylius/src/Grid/Game/GameShopGrid.php
namespace App\Grid\Game;
// ...
class GameShopGrid extends AbstractGrid implements ResourceAwareGridInterface
{
public static function getName(): string
{
// Nom de la grille, même que dans le YAML
return 'app_shop_game';
}
public function buildGrid(GridBuilderInterface $gridBuilder): void
{
$gridBuilder
// Récupération avec la traduction avec la locale en cours
->setRepositoryMethod('createListQueryBuilder', ["expr:service('sylius.context.locale').getLocaleCode()"])
->addOrderBy('name', 'asc') // Trié par nom
->setLimits([12]) // 12 par page
;
}
public function getResourceClass(): string
{
return Game::class;
}
}
```
---
## Les Twig Hooks
---
```twig
{# apps/sylius/templates/shop/game/index.html.twig #}
{# Layout de la page #}
{% extends '@SyliusShop/shared/layout/base.html.twig' %}
{# Contenu de la page #}
{% block content %}
{% endif %}
```
---
```twig
{# apps/sylius/templates/shop/game/show/content.html.twig #}
{% set game = hookable_metadata.context.game|default %}
{% if game is not empty %}
{# Affichage de l'image #}
{% set path = game.cover|imagine_filter('app_game_image') %}
{# `resources` contient la liste des jeux #}
{# Déclenche les hooks avec le prefixe défini #}
{% hook ['app.game.index'] with { metadata, configuration, resources } %}
{% endblock %}
```
---

---

---
## Twig Hooks
---
```yaml
# apps/sylius/config/twig_hooks/shop/game_index.yaml
sylius_twig_hooks:
hooks:
'app.game.index':
breadcrumbs:
template: 'shop/game/index/breadcrumbs.html.twig'
priority: 300
title:
template: 'shop/game/index/title.html.twig'
priority: 200
content:
template: 'shop/game/index/content.html.twig'
priority: 100
pagination:
template: 'shop/game/index/pagination.html.twig'
priority: 0
```
---
## Templates
---
```twig
{# apps/sylius/templates/shop/game/index/breadcrumbs.html.twig #}
{% from '@SyliusShop/shared/breadcrumbs.html.twig' import breadcrumbs as breadcrumbs %}
{% set crumbs = [
{'label': 'sylius.ui.home'|trans, 'path': path('sylius_shop_homepage')},
{'label': 'app.ui.games'|trans, 'active': true}
] %}
{{ breadcrumbs(crumbs) }}
```
---
```twig
{# apps/sylius/templates/shop/game/index/title.html.twig #}
{# Un simple titre traduit #} {{ 'app.ui.games'|trans }}
``` --- ```twig {# apps/sylius/templates/shop/game/index/content.html.twig #} {% import '@SyliusShop/shared/messages.html.twig' as messages %} {# On récupère la liste des jeux dans les variables du hook fournies par Sylius #} {% set games = get_hookable_context().resources ?? [] %} {% if games.data|length %}
{# Boucle sur les jeux #}
{% for game in games.data %}
{% else %}
{{ messages.info('sylius.ui.no_results_to_display') }}
{% endif %}
```
---
```twig
{# apps/sylius/templates/shop/game/index/pagination.html.twig #}
{% import '@SyliusShop/shared/pagination/pagination.html.twig' as pagination %}
{% set configuration = get_hookable_context().configuration ?? null %}
{% set resources = get_hookable_context().resources ?? [] %}
{% if configuration and configuration.isPaginated %}
{{ pagination.simple(resources.data) }}
{% endif %}
```
---
## Résultat
---

---
## Debug
---

---
# Affichage d'un jeu
---
## Route
---
```yaml
# apps/sylius/config/routes/game.yaml
app_game_show:
path: /{_locale}/game/{id}
methods: [ GET ]
requirements:
_locale: ^[A-Za-z]{2,4}(_([A-Za-z]{4}|[0-9]{3}))?(_([A-Za-z]{2}|[0-9]{3}))?$
defaults:
# app.controller.game : Resource controller autogénéré
# showAction : méthode pour afficher une resource (ici un jeu)
_controller: app.controller.game::showAction
_sylius:
template: 'shop/game/show.html.twig'
repository:
method: find
arguments:
- $id
```
---
## Template
---
```twig
{# ... #}
{% block content %}
{# Affichage d'une carte de jeu en bootstrap (Image, nom, consoles) #}
{% include 'shop/game/card.html.twig' with { class: 'h-100' } %}
{% endfor %}
{% hook ['app.game.show'] with { game } %}
{% endblock %}
{# ... #}
```
---
## Twig Hooks
---
```yaml
# apps/sylius/config/twig_hooks/shop/game_show.yaml
sylius_twig_hooks:
hooks:
'app.game.show':
breadcrumbs:
template: 'shop/game/show/breadcrumbs.html.twig'
priority: 300
title:
template: 'shop/game/show/title.html.twig'
priority: 200
consoles:
template: 'shop/game/show/consoles.html.twig'
priority: 100
content:
template: 'shop/game/show/content.html.twig'
priority: 0
```
---
## Templates
---
```twig
{# apps/sylius/templates/shop/game/show/breadcrumbs.html.twig #}
{% from '@SyliusShop/shared/breadcrumbs.html.twig' import breadcrumbs as breadcrumbs %}
{# Récupération du jeu à afficher fourni par le hook #}
{% set game = hookable_metadata.context.game %}
{% set crumbs = [
{'label': 'sylius.ui.home'|trans, 'path': path('sylius_shop_homepage')},
{'label': 'app.ui.games'|trans, 'path': path('app_game_index')},
{'label': game.name, 'active': true}
] %}
{{ breadcrumbs(crumbs) }}
```
---
```twig
{# apps/sylius/templates/shop/game/show/title.html.twig #}
{% set game = hookable_metadata.context.game %}
{{ game.name }}
``` --- ```twig {# apps/sylius/templates/shop/game/show/consoles.html.twig #} {% set game = hookable_metadata.context.game %} {% set consoles = game.consoles|default([]) %} {% if consoles|length %}
{% for console in consoles %}
{{ console.name }}
{% endfor %}
{% if path is not empty %}
{% endif %}
{% endif %}
```
---
## Résultat
---

---
# Bravo !
---
- ✅ Setup du projet
- ✅ Resources Sylius
- ✅ Grid
- ✅ CRUD
- ✅ Fixtures
- ✅ Controller resources front
- ✅ Twig Hooks
- Et plus !!!!!
---
# Astuce 1 : Fil d'arianne en admin
---

---

---
```yaml
# apps/sylius/config/twig_hooks/admin/constructor.yaml
sylius_twig_hooks:
hooks:
sylius_admin.constructor.update.content.header:
breadcrumbs:
template: '@SyliusAdmin/shared/crud/show/content/header/breadcrumbs.html.twig'
configuration:
rendered_field: name # On défini le champ à afficher
priority: 0
```
---

---
# Astuce 2 : Messages flash en admin
---

---
```yaml
# apps/sylius/translations/flashes.fr.yaml
app:
constructor:
create: 'Le constructeur a été créé avec succès.'
update: 'Le constructeur a été mis à jour avec succès.'
delete: 'Le constructeur a été supprimé avec succès.'
console:
create: 'La console a été créée avec succès.'
update: 'La console a été mise à jour avec succès.'
delete: 'La console a été supprimée avec succès.'
game:
create: 'Le jeu a été créé avec succès.'
update: 'Le jeu a été mis à jour avec succès.'
delete: 'Le jeu a été supprimé avec succès.'
```
---

---
## Astuce 3 : Changement menu front
---

---
```yaml
# apps/sylius/config/twig_hooks/shop/menu.yaml
sylius_twig_hooks:
hooks:
sylius_shop.base.header.navbar:
menu:
enabled: false
game_menu:
template: 'shop/layout/menu.html.twig'
priority: 100
```
---
```twig
{# apps/sylius/templates/shop/layout/menu.html.twig #}
{# Gestion de la navbar, accessibilité et responsive #}
```
---

---
# Extra : Un form en live component
## Ajouter des consoles à un constructeur
---
```php
// apps/sylius/src/Form/Extension/Game/ConstructorExtensionType.php
namespace App\Form\Extension\Game;
///
use Symfony\UX\LiveComponent\Form\Type\LiveCollectionType;
// ...
class ConstructorExtensionType extends AbstractTypeExtension
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// ✅ LiveCollectionType de Symfony UX
$builder
->add('consoles', LiveCollectionType::class, [
'label' => 'app.ui.consoles',
'required' => true,
'entry_type' => EntityType::class,
'entry_options' => [
'label' => false,
/** ✅ Entité console */
'class' => Console::class,
'choice_label' => 'name',
'autocomplete' => true,
'query_builder' => function (ConsoleRepositoryInterface $repository) {
return $repository->createQueryBuilder('o')->orderBy('o.name', 'ASC');
},
],
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true,
'by_reference' => false,
])
;
} // ...
}
```
---

---
```yaml
# apps/sylius/config/services.yaml
services:
# ...
# Live component pour le formulaire constructeur
sylius_admin.twig.component.constructor.form:
autoconfigure: false
# ✅ ResourceFormComponent va gérer le composant pour nous
class: 'Sylius\Bundle\UiBundle\Twig\Component\ResourceFormComponent'
arguments:
- '@app.repository.constructor' # Repository de la resource
- '@form.factory' # Form factory standard de Symfony
- '%app.model.constructor.class%' # Model de la resource
- 'App\Form\Type\Game\ConstructorType' # Form type de la resource
# ✅ Tag sylius.live_component.admin + nom du component
tags:
- { name: 'sylius.live_component.admin', key: 'sylius_admin:constructor:form' }
```
---
```yaml
# apps/sylius/config/twig_hooks/admin/constructor_extension.yaml
sylius_twig_hooks:
hooks:
sylius_admin.constructor.update.content:
form:
# ✅ Nom du live component défini avant
component: 'sylius_admin:constructor:form'
props:
resource: '@=_context.resource' # Resource en cours
form: '@=_context.form' # Form en cours
# ✅ Template fourni par Sylius
template: '@SyliusAdmin/shared/crud/common/content/form.html.twig'
```
---

---
# Conclusion
---
[.column]
- ✅ Setup du projet
- ✅ Resources Sylius
- ✅ Grid
- ✅ CRUD
[.column]
- ✅ Fixtures
- ✅ Controller resources front
- ✅ Twig Hooks
- ✅ Live Component de resource
- ✅ Et quelques tips !
---
# Aller plus loin
---
- Filtres sur la liste des jeux
- Slug de jeu au lieu d'un ID dans l'URL (`/game/nom-du-jeu`)
- Plus de contenu sur la page d'affichage d'un jeu
- ...
---
# Merci / Questions
[.column]

## Donnez votre avis
[.column]

## Repository du code + slides dans la journée
---