Développez sous Sylius 2 en 40 minutes top chrono

![](img/carte-forum2025.png) --- ![](img/sylius.png) --- ![fit](img/unsupported.png) --- ![120%](img/releases.png) --- ![fit](img/versions.png) --- # 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 ``` --- ![fit](img/shop.png) --- ![fit](img/admin.png) --- # Installation de 2 plugins ![inline fill](img/mbiz_logo.svg) --- ## Media Manager ```bash symfony composer require monsieurbiz/sylius-media-manager-plugin="^2.0.0" ``` --- ![fit](video/demo-media-manager.mov) --- ## Rich Editor ```bash # Dans le dossier apps/sylius symfony composer require monsieurbiz/sylius-rich-editor-plugin="^3.0.0" ``` --- ![fit](video/demo-rich-editor.mov) --- # 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 ? --- ![fit](img/grid-fields.jpg) --- ![fit](img/grid-actions.jpg) --- ![fit](img/grid-filters.jpg) --- ## 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 --- ![fit](video/demo-grids.mov) --- - ✅ 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 ``` --- ![fit](img/api-platform-constructor.jpg) --- ## 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 --- ![fit](video/demo-forms.mov) --- # 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 %}
{# `resources` contient la liste des jeux #} {# Déclenche les hooks avec le prefixe défini #} {% hook ['app.game.index'] with { metadata, configuration, resources } %}
{% endblock %} ``` --- ![fit](img/game-index-empty.jpg) --- ![100%](img/debug-twig-hooks.jpg) --- ## 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 %}
{# Affichage d'une carte de jeu en bootstrap (Image, nom, consoles) #} {% include 'shop/game/card.html.twig' with { class: 'h-100' } %}
{% endfor %}
{% 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 --- ![fit](video/game-index-demo.mov) --- ## Debug --- ![fit](img/debug-hook-html.jpg) --- # 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 %}
{% 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 %}
{% 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') %}
{% if path is not empty %} {{ game.name }} {% endif %}
{% endif %} ``` --- ## Résultat --- ![fit](img/game-show.jpg) --- # Bravo ! --- - ✅ Setup du projet - ✅ Resources Sylius - ✅ Grid - ✅ CRUD - ✅ Fixtures - ✅ Controller resources front - ✅ Twig Hooks - Et plus !!!!! --- # Astuce 1 : Fil d'arianne en admin --- ![fit](img/breadcrumbs-edited.jpg) --- ![fit](img/breadcrumbs.jpg) --- ```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 ``` --- ![fit](img/breadcrumbs-edited.jpg) --- # Astuce 2 : Messages flash en admin --- ![fit](img/flash.jpg) --- ```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.' ``` --- ![fit](img/flash-edited.jpg) --- ## Astuce 3 : Changement menu front --- ![fit](img/basic-menu.jpg) --- ```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 #}
``` --- ![fit](img/shop-menu.png) --- # 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, ]) ; } // ... } ``` --- ![fit](video/live-collection-not-working.mov) --- ```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' ``` --- ![fit](video/live-collection-working.mov) --- # 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] ![inline](img/openfeedback-code.png) ## Donnez votre avis [.column] ![inline](img/qr-code-article.png) ## Repository du code + slides dans la journée ---