· 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.
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.