· 14 min read

Un Workflow de pro avec Symfony 5 !

Dans cet article nous allons découvrir le composant Workflow de Symfony. Ce composant va nous permettre de mettre en place facilement et rapidement un workflow dans une application Symfony.

Dans cet article nous allons découvrir le composant Workflow de Symfony. Ce composant va nous permettre de mettre en place facilement et rapidement un workflow dans une application Symfony.

Introduction

Dans cet article nous allons découvrir le composant Workflow de Symfony. Ce composant va nous permettre de mettre en place facilement et rapidement un workflow dans une application Symfony.

Nous ne rentrerons pas dans le détail de ce qu’est un “workflow” vs une “state machine” (les deux sont possible avec ce composant), mais vous pouvez en apprendre davantage à ce sujet dans la documentation Symfony. Dans notre exemple nous utiliserons un “Workflow”, ce modèle permet en effet plus de possibilités que la “state machine”.

Notre objectif : Achète-moi un cadeau ;-)

Afin de rendre la découverte de ce composant plus simple, nous allons l’étudier au travers d’un exemple d’application toute simple que nous allons développer ensemble.

Nous souhaitons donc mettre en place une application permettant à un enfant de demander un cadeau à ses parents. Chacun des parents doit donner sa validation, puis l’un des deux peut passer la commande, et enfin valider la bonne réception du cadeau (et comme on est gentil, on ne prévoit pas la possibilité que les parents refusent !).

Concrètement, nous allons mettre en place :

  • Trois rôles : KID, DAD et MUM
  • Deux URLs : /kid (pour formuler la demande) et /parents (pour faire avancer la demande)
  • La possibilité de s’inscrire, d’ouvrir et fermer une session
  • D’être notifié par mail au fur et à mesure des étapes

Initialisation du projet Symfony

Créons un nouveau projet Symfony.

symfony new ToyRequest --full
cd ToyRequest

Pour simplifier le tuto, passons en base de données SQLite dans le fichier .env.

###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
# DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8"
###< doctrine/doctrine-bundle ###

Et créons la base de données.

symfony console d:d:c

Gestions des mails avec Docker

Dans notre application nous allons envoyer des mails, mettons en place un conteneur Docker qui va intercepter nos mails et nous les afficher dans un webmail. Pour cela créons un fichier docker-compose.yml à la racine du projet.

version: '3'
 
services:
    mailer:
      image: schickling/mailcatcher
      ports: [1025, 1080]

Démarrons le conteneur, et le serveur interne Symfony.

docker-compose up -d
symfony serve -d

Ouvrez votre application Symfony dans un navigateur, et vérifiez que le conteneur Webmail est bien détecté par le serveur Symfony.

Vous pouvez cliquer sur le lien “Open” à côté de Webmail dans la debug barre pour ouvrir le Webmail.

Gestion des Utilisateurs et des rôles

Créons nos utilisateurs, générons la migration et appliquons-la.

symfony console make:user User

 Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
 > yes

 Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [email]:
 > email

 Will this app need to hash/check user passwords? Choose No if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server).

 Does this app need to hash/check user passwords? (yes/no) [yes]:
 > yes

symfony console make:migration

symfony console d:m:m

Mettons en place notre processus d’authentification.

symfony console make:auth

 What style of authentication do you want? [Empty authenticator]:
  [0] Empty authenticator
  [1] Login form authenticator
 > 1

 The class name of the authenticator to create (e.g. AppCustomAuthenticator):
 > AppAuthenticator

 Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
 > SecurityController

 Do you want to generate a '/logout' URL? (yes/no) [yes]:
 > yes

Mettons en place notre formulaire de création de compte.

symfony console make:registration-form

 Creating a registration form for App\Entity\User

 Do you want to add a @UniqueEntity validation annotation on your User class to make sure duplicate accounts aren't created? (yes/no) [yes]:
 > yes

 Do you want to send an email to verify the user's email address after registration? (yes/no) [yes]:
 > no

 Do you want to automatically authenticate the user after registration? (yes/no) [yes]:
 > yes

Modifions notre formulaire de création de compte pour lui permettre de sélectionner un “rôles” dans un liste déroulante. Pour cela éditons le fichier /src/Form/RegistrationFormType.php. Nous allons devoir mettre en place un “Data Transformer”.

<?php

namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('email')
            ->add('roles', ChoiceType::class, [
                'required' => true,
                'multiple' => false,
                'expanded' => false,
                'choices'  => [
                  'Kid' => 'ROLE_KID',
                  'Dad' => 'ROLE_DAD',
                  'Mum' => 'ROLE_MUM',
                ],
            ])
            ->add('agreeTerms', CheckboxType::class, [
                'mapped' => false,
                'constraints' => [
                    new IsTrue([
                        'message' => 'You should agree to our terms.',
                    ]),
                ],
            ])
            ->add('plainPassword', PasswordType::class, [
                // instead of being set onto the object directly,
                // this is read and encoded in the controller
                'mapped' => false,
                'constraints' => [
                    new NotBlank([
                        'message' => 'Please enter a password',
                    ]),
                    new Length([
                        'min' => 6,
                        'minMessage' => 'Your password should be at least {{ limit }} characters',
                        // max length allowed by Symfony for security reasons
                        'max' => 4096,
                    ]),
                ],
            ])
        ;

        // Data transformer
        $builder->get('roles')
            ->addModelTransformer(new CallbackTransformer(
                function ($rolesArray) {
                     // transform the array to a string
                     return count($rolesArray)? $rolesArray[0]: null;
                },
                function ($rolesString) {
                     // transform the string back to an array
                     return [$rolesString];
                }
        ));
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => User::class,
        ]);
    }
}

Modifions le fichier twig /templates/registration/register.html.twig pour gérer notre liste de choix de rôles.

{% extends 'base.html.twig' %}

{% block title %}Register{% endblock %}

{% block body %}
    {% for flashError in app.flashes('verify_email_error') %}
        <div class="alert alert-danger" role="alert">{{ flashError }}</div>
    {% endfor %}

    <h1>Register</h1>

    {{ form_start(registrationForm) }}
        {{ form_row(registrationForm.email) }}
        {{ form_row(registrationForm.roles) }}
        {{ form_row(registrationForm.plainPassword, {
            label: 'Password'
        }) }}
        {{ form_row(registrationForm.agreeTerms) }}

        <button type="submit" class="btn btn-primary">Register</button>
    {{ form_end(registrationForm) }}
{% endblock %}

Et comme nous sommes pour la parité femme/homme, nous modifions le fichier /config/packages/secutiy.yaml pour y ajouter un rôle “ROLE_PARENT” dont hériterons “ROLE_DAD” et “ROLE_MUM”. Pas de jaloux ;-)

security:
    encoders:
        App\Entity\User:
            algorithm: auto

    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

    role_hierarchy:
        ROLE_DAD: [ROLE_PARENT]
        ROLE_MUM: [ROLE_PARENT]
        
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: true
            lazy: true
            provider: app_user_provider
            guard:
                authenticators:
                    - App\Security\AppAuthenticator
            logout:
                path: app_logout
                # where to redirect after logout
                # target: app_any_route

            # activate different ways to authenticate
            # https://symfony.com/doc/current/security.html#firewalls-authentication

            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        # - { path: ^/admin, roles: ROLE_ADMIN }
        # - { path: ^/profile, roles: ROLE_USER }

Mettons un peu de style avec Bootstrap 5 (via cdn)

Pour que notre exemple soit plus agréable à manipuler, nous allons mettre en place Bootstrap via un CDN (nous aurions pu le faire de manière plus “pro”, vous pouvez jeter un coup d’œil à ce tuto qui expose comment faire).

Éditons le fichier /templates/base.html.twig pour y ajouter Bootstrap 5, une Navabar et un bout de code pour afficher des “Flash Messages”.

<!DOCTYPE html>
<html>

	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>
			{% block title %}Welcome!
			{% endblock %}
		</title>
		{% block stylesheets %}<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
		{% endblock %}
	</head>

	<body>

		<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
			<div class="container-fluid">
				<a class="navbar-brand" href="/">ToyRequest</a>
				<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
					<span class="navbar-toggler-icon"></span>
				</button>

				<div class="collapse navbar-collapse" id="navbarsExampleDefault">
					<ul class="navbar-nav me-auto mb-2 mb-md-0">
						<li class="nav-item active">
							<a class="nav-link" href="#">NEW</a>
						</li>
						<li class="nav-item">
							<a class="nav-link" href="#">PARENT</a>
						</li>
					</ul>
				</div>
			</div>
		</nav>

        <main class="container pt-5 mt-5">
			{% for label, messages in app.flashes(['success']) %}
				{% for message in messages %}
					<div class="alert alert-success">
						{{ message }}
					</div>
				{% endfor %}
			{% endfor %}

	        {% block body %}{% endblock %}

        </main>

		{% block javascripts %}
			<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
		{% endblock %}

	</body>
</html>

Indiquons également à Twig qu’il doit styliser nos formulaires avec Bootstrap, en modifiant le fichier /config/packages/twig.yaml.

twig:
    default_path: '%kernel.project_dir%/templates'
    form_themes: ['bootstrap_4_layout.html.twig']

Créons une page d’accueil

Générons un contrôleur “HomeController”.

symfony console make:controller HomeController

Éditons le fichier src/Controller/HomeController.php pour y modifier la route sur ”/“.

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class HomeController extends AbstractController
{
<?php
    /**
     * @Route("/", name="app_home")
     */
    public function index(): Response
    {
        return $this->render('home/index.html.twig');
    }
}

Puis ajoutons du contenu sur la page en modifiant le fichier templates/home/index.html.twig.

{% extends 'base.html.twig' %}

{% block title %}ToyRequest!
{% endblock %}

{% block body %}
	<section class="py-5 text-center container">
		<div class="row py-lg-5">
			<div class="col-lg-6 col-md-8 mx-auto">
				<h1 class="fw-light">ToyRequest</h1>
				<p class="lead text-muted">Papa, Maman, je veux un jouet ! Promis j'ai été super sage et j'ai fait tous mes devoirs !</p>
				<p>
					<a href="#" class="btn btn-primary my-2">Demander un jouet</a>
					<a href="#" class="btn btn-secondary my-2">Traiter les demandes</a>
				</p>
			</div>
		</div>
	</section>
{% endblock %}

Et testons!

Modifions également le fichier src/Security/AppAuthenticator.php pour faire en sorte qu’après une authentification nos utilisateurs arrivent sur cette route.

<?php

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

        return new RedirectResponse($this->urlGenerator->generate('app_home'));
    }

Créons notre entité ToyRequest

Utilisons-le maker de Symfony pour créer notre entité, générerons la migration et appliquons-la.

symfony console make:entity ToyRequest

 New property name (press <return> to stop adding fields):
 > user

 Field type (enter ? to see all types) [string]:
 > relation

 What class should this entity be related to?:
 > User

 Relation type? [ManyToOne, OneToMany, ManyToMany, OneToOne]:
 > ManyToOne

 Is the ToyRequest.user property allowed to be null (nullable)? (yes/no) [yes]:
 > yes

 Do you want to add a new property to User so that you can access/update ToyRequest objects from it - e.g. $user->getToyRequests()? (yes/no) [yes]:
 > yes

 A new property will also be added to the User class so that you can access the related ToyRequest objects from it.

 New field name inside User [toyRequests]:
 > toyRequests

 updated: src/Entity/ToyRequest.php
 updated: src/Entity/User.php

 Add another property? Enter the property name (or press <return> to stop adding fields):
 > name

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 255

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 New property name (press <return> to stop adding fields):
 > status

 Field type (enter ? to see all types) [string]:
 > array

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

symfony console make:migration

symfony console d:m:m

Mettons en place le Workplace (enfin !)

Commençons par installer le composant.

composer require symfony/workflow

Décrivons-le dans le fichier /config/packages/workflow.yaml.

framework:
    workflows:
        toy_request:
            type: 'workflow'
            audit_trail:
                enabled: true
            marking_store:
                type: 'method'
                property: 'status'
            supports:
                - App\Entity\ToyRequest
            initial_marking: request
            places:
                - request
                - dad_validation_pending
                - dad_ok
                - mum_validation_pending
                - mum_ok
                - order
                - ordered
                - received

            transitions:
                to_pending:
                    from: request
                    to:   [dad_validation_pending, mum_validation_pending]
                to_dad_ok:
                    guard: "is_granted('ROLE_DAD')"
                    from: dad_validation_pending
                    to: dad_ok
                to_mum_ok:
                    guard: "is_granted('ROLE_MUM')"
                    from: mum_validation_pending
                    to: mum_ok
                to_order:
                    guard: "is_granted('ROLE_PARENT')"
                    from: [dad_ok, mum_ok]
                    to:   order
                to_ordered:
                    guard: "is_granted('ROLE_PARENT')"
                    from: order
                    to:   ordered
                to_received:
                    guard: "is_granted('ROLE_PARENT')"
                    from: ordered
                    to:   received

Faisons un dump du workflow pour vérifier qu’il est conforme à notre scénario de départ. (Il faut installer au préalable l’outil Graphviz tel que précisé dans la documentation de Symfony).

php bin/console workflow:dump toy_request | dot -Tpng -o graph.png

Un fichier graph.png devrait être désormais disponible à la racine de votre projet, avec une représentation graphique de votre Workflow.

Mettons en place le formulaire d’ajout

Première chose à faire, générer le formulaire avec la CLI de Symfony.

symfony console make:form ToyRequestType

 The name of Entity or fully qualified model class name that the new form will be bound to (empty for none):
 > ToyRequest

Éditons le fichier src/Form/ToyRequestType.php pour l’adapter à nos besoins.

<?php

namespace App\Form;

use App\Entity\ToyRequest;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ToyRequestType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class)
            ->add('submit', SubmitType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => ToyRequest::class,
        ]);
    }
}

Puis créons un contrôleur pour les “ToyRequest”.

symfony console make:controller ToyRequestController

Éditons le fichier src/Controller/ToyRequestController.php pour y ajouter le formulaire, et activer la transition du “to_pending” de notre Workflow.

<?php

namespace App\Controller;

use App\Entity\ToyRequest;
use App\Form\ToyRequestType;
use Doctrine\ORM\EntityManagerInterface;
use LogicException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Workflow\WorkflowInterface;

class ToyRequestController extends AbstractController
{
    private $toyRequestWorkflow;

    public function __construct(WorkflowInterface $toyRequestWorkflow)
    {
        $this->toyRequestWorkflow = $toyRequestWorkflow;
    }
    /**
     * @Route("/new", name="app_new")
     */
    public function index(Request $request, EntityManagerInterface $entityManager): Response
    {
        $toy = new ToyRequest();

        $toy->setUser($this->getUser());

        $form = $this->createForm(ToyRequestType::class, $toy);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {

            $toy = $form->getData();

            try {
                $this->toyRequestWorkflow->apply($toy, 'to_pending');
            } catch (LogicException $exception) {
                //
            }

            $entityManager->persist($toy);
            $entityManager->flush();

            $this->addFlash('success', 'Demande enregistrée !');

            return $this->redirectToRoute('app_new');
        }

        return $this->render('toy_request/index.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

Et limitons l’accès à cette page au ROLE_KID dans le fichier /config/services.yaml.

access_control:
        - { path: ^/new, roles: ROLE_KID }

Créons-nous un compte avec le rôle KID.

Et soumettons une demande de jouet ;-)

Mettons en place la page de validation des parents

Avançons dans la mise en place de notre Workflow et permettons aux parents de valider les étapes d’une demande.

Dans notre contrôleur src/Controller/ToyRequestController.php ajoutons une nouvelle route /parent, et envoyons à une vue templates/toy_request/parent.html.twig toutes les ToyRequest.

<?php
    /**
     * @Route("/parent", name="app_parent")
     */
    public function parent(ToyRequestRepository $toyRequestRepository): Response
    {
        return $this->render('toy_request/parent.html.twig', [
            'toys' => $toyRequestRepository->findAll(),
        ]);
    }

Mettons en place une route dédiée aux changements de status de notre workflow, toujours dans le fichier src/Controller/ToyRequestController.php.

<?php

    /**
     * @Route("/change/{id}/{to}", name="app_change")
     */
    public function change(ToyRequest $toyRequest, String $to, EntityManagerInterface $entityManager): Response
    {
        try {
            $this->toyRequestWorkflow->apply($toyRequest, $to);
        } catch (LogicException $exception) {
            //
        }

        $entityManager->persist($toyRequest);
        $entityManager->flush();

        $this->addFlash('success', 'Action enregistrée !');

        return $this->redirectToRoute('app_parent');
    }

Protégeons ces deux routes, et ne les rendons accessibles qu’aux parents en modifiant le fichier config/packages/security.yaml.

    access_control:
        - { path: ^/new, roles: ROLE_KID }
        - { path: ^/parent, roles: ROLE_PARENT }
        - { path: ^/change, roles: ROLE_PARENT }

Il ne nous reste plus qu’à mettre en place le fichier /templates/toy_request/parent.html.twig.

{% extends 'base.html.twig' %}

{% block title %}ToyRequest - Validation des parents
{% endblock %}

{% block body %}

	{% for label, messages in app.flashes(['success']) %}
		{% for message in messages %}
			<div class="flash-{{ label }}">
				{{ message }}
			</div>
		{% endfor %}
	{% endfor %}

	{% for toy in toys %}

		<h2>{{toy.name}}</h2>
        <p>Demande de {{ toy.user.email }}</p>

		{% if workflow_can(toy, 'to_dad_ok') %}
			<a type="button" class="btn btn-primary btn-lg" href="{{ path('app_change', {'id': toy.id, 'to': 'to_dad_ok'}) }}">Papa valide</a>
		{% endif %}

        {% if workflow_can(toy, 'to_mum_ok') %}
			<a type="button" class="btn btn-primary btn-lg" href="{{ path('app_change', {'id': toy.id, 'to': 'to_mum_ok'}) }}">Maman valide</a>
		{% endif %}

        {% if workflow_can(toy, 'to_order') %}
			<a type="button" class="btn btn-primary btn-lg" href="{{ path('app_change', {'id': toy.id, 'to': 'to_order'}) }}">Passer la commande</a>
		{% endif %}

        {% if workflow_can(toy, 'to_ordered') %}
			<a type="button" class="btn btn-primary btn-lg" href="{{ path('app_change', {'id': toy.id, 'to': 'to_ordered'}) }}">La commande est en cours de livraison</a>
		{% endif %}

        {% if workflow_can(toy, 'to_received') %}
			<a type="button" class="btn btn-primary btn-lg" href="{{ path('app_change', {'id': toy.id, 'to': 'to_received'}) }}">Valider la reception de la commande</a>
		{% endif %}

		<hr>

	{% endfor %}

{% endblock %}

Créons-nous deux comptes : mum@test.com et dad@test.com et faisons des essais !

Mettons en place les notifications par mails

Commençons par créer un répertoire EventSubscriber dans /src/EventSubscriber, puis créer un fichier WorkflowSubscriber.php. Ce fichieri va :

  • Écouter un événement émis par le workflow quand il quitte la place “request” (La toute première) et envoyer un email
  • Écouter un événement émis par le workflow quand il est pleinement dans la place “received”(La toute dernière) et envoyer un email.
<?php
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Workflow\Event\Event;

class WorkflowSubscriber implements EventSubscriberInterface
{
    private $mailer;

    public function __construct(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }

    public function newToyRequest(Event $event)
    {
        $email = (new Email())
            ->from($event->getSubject()->getUser()->getEmail())
            ->to('dad@test.com')
            ->addTo('mum@test.com')
            ->subject('Demande de jouet - ' . $event->getSubject()->getName())
            ->text('Bonjour Maman et Papa, merci de me commander le jouet : ' . $event->getSubject()->getName());

        $this->mailer->send($email);
    }

    public function toyReceived(Event $event)
    {
        $email = (new Email())
            ->from('papa.noel@laponie.fr')
            ->to($event->getSubject()->getUser()->getEmail())
            ->subject('Ton jouet est la, oh oh oh !')
            ->text('Ton jouet est arrivé, amuse toi bien !');

        $this->mailer->send($email);
    }

    public static function getSubscribedEvents()
    {
        return [
            'workflow.toy_request.leave.request' => 'newToyRequest',
            'workflow.toy_request.entered.received' => 'toyReceived',
        ];
    }
}

Et profitons d’être dans le code pour mettre à jour la NavBar dans le fichier /templates/base.html/twig.

        <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
			<div class="container-fluid">
				<a class="navbar-brand" href="/">ToyRequest</a>
				<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
					<span class="navbar-toggler-icon"></span>
				</button>

				<div class="collapse navbar-collapse" id="navbarsExampleDefault">
					<ul class="navbar-nav me-auto mb-2 mb-md-0">
						<li class="nav-item active">
							<a class="nav-link" href="{{ path('app_new') }}">NEW</a>
						</li>
						<li class="nav-item">
							<a class="nav-link" href="{{ path('app_parent') }}">PARENT</a>
						</li>
					</ul>
				</div>
			</div>
		</nav>

Et modifions également la homepage, dans /templates/home/index.html.twig.

{% extends 'base.html.twig' %}

{% block title %}ToyRequest!
{% endblock %}

{% block body %}
	<section class="py-5 text-center container">
		<div class="row py-lg-5">
			<div class="col-lg-6 col-md-8 mx-auto">
				<h1 class="fw-light">ToyRequest</h1>
				<p class="lead text-muted">Papa, Maman, je veux un jouet ! Promis j'ai été super sage et j'ai fait tous mes devoirs !</p>
				<p>
					<a href="{{ path('app_new') }}" class="btn btn-primary my-2">Demander un jouet</a>
					<a href="{{ path('app_parent') }}" class="btn btn-secondary my-2">Traiter les demandes</a>
				</p>
			</div>
		</div>
	</section>
{% endblock %}

Conclusions et dépôt GitLab

Le composant Workflow de Symfony offre de nombreuses possibilités, et peut s’adapter à de nombreux usages. Le découplage du code en utilisant les Events offre aussi une approche efficace et durable pour faire évoluer votre code.

Les sources de ce projet sont disponible sur ce dépôt GitLab.

Back to Blog