Symfony : la haute gastronomie en terme de développement
Symfony, c'est comme un restaurant gastronomique français où t'as un chef étoilé qui dirige tout d'une main de maître.
La brigade de cuisine
Alors qu'est-ce que c'est Symfony ? C'est un framework PHP. Un framework. Ca fait super développeur de dire ça. "Oui bonjour, je travaille avec un framework". Et mamie, elle met ses collants toute seule aussi.
Pour les gens normaux, un framework c'est comme une brigade de cuisine déjà organisée. Tu arrives, il y a déjà le chef, le second, lr commis, et toute l'équipe qui est prête. Tu leur dis "aujourd'hui on fait un site e-commerce de chaussettes pour hamsters", ils te regardent bizarrement, mais ils savent comment s'organiser.
Symfony, c'est pas juste UN truc, c'est plein de trucs qu'on appelle des "composants". Un peu comme dans une cuisine : tu as pas qu'une casserole, tu as aussi des couteaux, des planches à découper, un robot mixeur et le stagiaire qui se coupe les doigts toutes les dix minutes. Chaque composant a son boulot.
Le menu Symfony
L'hôte d'accueil : le kernel et le routing
Le Kernel de Symfony, c'est l'hôte d'accueil du resto. C'est le premier mec que tu vois quand tu passes la porte. Quelqu'un clique sur ton site, hop, le Kernel se réveille et dit "Ah, bienvenue ! Vous avez réservé ? Vous voulez voir le menu spécial ou la carte régulière ?". Il oriente les clients vers la bonne table comme un pro.
// Voilà à quoi ça ressemble, l'hôte d'accueil en code
public function handle(Request $request): Response
{
// Ici, on décide où placer le client
return $this->handleRaw($request);
}
Le Routing, c'est le plan de salle du restaurant. Quand le client débarque et dit "Je veux voir les photos de hamsters en chaussettes rouges", l'hôte d'accueil consulte son plan et sait exactement quelle table lui attribuer, quel serveur va s'occuper de lui.
# routes.yaml - Le plan de salle avec toutes les tables numérotées
hamster_chaussettes:
path: /hamster/chaussettes/{couleur}
controller: App\Controller\ChaussetteController::afficherChaussettes
methods: [GET]
requirements:
couleur: rouge|bleu|vert|arc-en-ciel
Ou alors, si t'es un maître d'hôtel moderne, tu peux faire ça directement dans le controller avec des attributs (merci PHP8) :
// Les attributs, c'est comme un plan de salle directement dessiné sur le mur
#[Route('/hamster/chaussettes/{couleur}', name: 'hamster_chaussettes', methods: ['GET'])]
Le chef de salle : le Controller
Le Controller, c'est le chef de salle. C'est lui qui supervise tout, qui va de table en table pour s'assurer que tout se passe bien. Il prend pas les commandes lui-même, il fait pas la cuisine, il dresse pas les assiettes. Non, il coordonne tout ce petit monde. Dans Symfony, le Controller reçoit la requête, demande les données, et s'occupe de les servir proprement.
// Un Controller, c'est comme un chef de salle qui fait sa ronde
#[Route('/hamster/chaussettes/{couleur}', name: 'hamster_chaussettes')]
public function afficherChaussettes(string $couleur): Response
{
// Le chef de salle vérifie que la demande est raisonnable
if ($couleur === 'transparent') {
throw $this->createNotFoundException('Des chaussettes transparentes ? T\'es sérieux ?');
}
// Il envoie un serveur chercher les produits en cuisine
$chaussettes = $this->chaussetteRepository->trouverParCouleur($couleur);
// Et il fait appel à un serveur pour présenter le plat
return $this->render('chaussettes/index.html.twig', [
'chaussettes' => $chaussettes,
'couleur' => $couleur
]);
}
Et c'est là la grande force de Symfony : le chef de salle, il fait que ça ! Il délègue tout le reste aux autres membres de l'équipe. C'est comme un chef de salle qui dit "Toi, tu prends la commande à la table 7. Toi, tu vas chercher le vin en cave. Toi, tu dresses les couverts pour le dessert." Il touche à rien lui-même, il organise. Quel beau métier.
Depuis Symfony 6, tu peux même faire des Controller ultra-spécialisés, comme des chefs de salle qui s'occupent que d'un type d'événement :
#[AsController]
class AfficherChaussettes
{
public function __construct(
private ChaussetteRepository $chaussetteRepository
) {}
#[Route('/hamster/chaussettes/{couleur}', name: 'hamster_chaussettes')]
public function __invoke(string $couleur): Response
{
return new JsonResponse(
$this->chaussetteRepository->trouverParCouleur($couleur)
);
}
}
C'est comme si tu disais "Ce soir, Paul s'occupe que des VIP, et il sait exactement ce qu'il doit faire pour eux sans qu'on ait besoin de lui expliquer en détail".
L'économe : Dotrine et la base de données
Doctrine, c'est l'économe du resto. Tu sais, le mec qui gère les stocks, qui passe les commandes aux fournisseurs, qui sait exactement ce qu'il y a dans la réserve et ce qu'il faut racheter. Dans Symfony, Doctrine s'occupe de la base de données. Tu lui demandes "T'as des chaussettes rouges ?", il te répond "Ouais, j'en ai 42 paires, taille nano, 100% coton bio, et je sais même où elles sont rangées".
// L'économe, c'est le mec qui tient l'inventaire avec une précision maniaque
#[Entity(repositoryClass: ChaussetteRepository::class)]
class Chaussette
{
#[Id]
#[GeneratedValue]
#[Column]
private ?int $id = null;
#[Column(length: 255)]
private ?string $couleur = null;
#[Column(length: 255)]
private ?string $taille = null;
#[Column]
private ?float $prix = null;
#[Column(type: 'text', nullable: true)]
private ?string $description = null;
#[Column]
private ?int $stock = null;
#[ManyToMany(targetEntity: Hamster::class, inversedBy: 'chaussettes')]
private Collection $hamsters;
public function __construct()
{
$this->hamsters = new ArrayCollection();
}
// Les getters et setters, c'est comme les bons de commande et les reçus
// Sans eux, personne sait ce qui entre et ce qui sort...
public function getId(): ?int
{
return $this->id;
}
public function getCouleur(): ?string
{
return $this->couleur;
}
public function setCouleur(string $couleur): static
{
$this->couleur = $couleur;
return $this;
}
// Et je te passe le reste, sinon on est encore là demain matin
}
Le Repository, c'est comme le carnet d'adresses de l'économe. Il sait quel fournisseur appeler pour chaque produit, il connaît tous les raccourcis pour obtenir ce dont la cuisine a besoin :
// Le carnet d'adresses de l'économe, avec tous ses petits secrets
class ChaussetteRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Chaussette::class);
}
public function trouverParCouleur(string $couleur): array
{
return $this->createQueryBuilder('c')
->andWhere('c.couleur = :couleur')
->andWhere('c.stock > 0')
->setParameter('couleur', $couleur)
->orderBy('c.prix', 'ASC')
->getQuery()
->getResult();
}
public function trouverChaussettesPopulaires(int $limit = 5): array
{
return $this->createQueryBuilder('c')
->select('c, COUNT(h.id) as nbHamsters')
->leftJoin('c.hamsters', 'h')
->groupBy('c.id')
->orderBy('nbHamsters', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
}
Et si t'es un économe flemmard (ou efficace, c'est selon), tu peux même utiliser le Maker Bundle pour générer tout ça automatiquement :
php bin/console make:entity Chaussette
php bin/console make:migration
php bin/console doctrine:migrations:migrate
C'est comme si tu arrivais dans la cuisine et que tu disais "On a besoin d'un inventaire pour les chaussettes" et que tous les formulaires, registres et systèmes de stockage se mettaient en place tout seuls. L'économe parfait quoi.
Le chef pâtissier : Twig et le Frontend
Twig, c'est le chef pâtissier. Son boulot, c'est de faire des trucs beaux et délicieux. Dans un resto, le chef pâtissier met des petites feuilles de menthe, du coulis en zigzag, et une boule de glace bien ronde. Dans Symfony, Twig prend les données et les transforme en HTML tout beau, tout appétissant.
{# Twig, c'est comme le chef pâtissier qui dresse les assiettes #}
{% extends 'base.html.twig' %}
{% block title %}Chaussettes pour Hamsters {{ couleur }}{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/chaussettes.css') }}">
{% endblock %}
{% block body %}
<div class="container">
<h1>Nos magnifiques chaussettes {{ couleur }}</h1>
{% if chaussettes|length > 0 %}
<div class="filtres">
<h2>Filtrer par taille</h2>
<div class="btn-group">
{% for taille in ['XS', 'S', 'M'] %}
<a href="{{ path('hamster_chaussettes_taille', {couleur: couleur, taille: taille}) }}"
class="btn {{ app.request.get('taille') == taille ? 'btn-primary' : 'btn-outline-primary' }}">
{{ taille }}
</a>
{% endfor %}
</div>
</div>
<div class="grid">
{% for chaussette in chaussettes %}
<div class="card">
<div class="card-header {{ chaussette.stock < 5 ? 'bg-warning' : '' }}">
{% if chaussette.stock < 5 %}
<span class="badge bg-danger">Plus que {{ chaussette.stock }} en stock !</span>
{% endif %}
</div>
<div class="card-body">
<h2>Chaussette {{ chaussette.couleur }}</h2>
<p>{{ chaussette.description|default('Pas de description disponible pour cette magnifique chaussette.') }}</p>
{% if chaussette.promotion %}
<p class="old-price">{{ chaussette.prixInitial }}€</p>
<span class="prix promo">{{ chaussette.prix }}€</span>
{% else %}
<span class="prix">{{ chaussette.prix }}€</span>
{% endif %}
</div>
<div class="card-footer">
<a href="{{ path('hamster_chaussette_detail', {id: chaussette.id}) }}" class="btn btn-info">Détails</a>
<button class="btn btn-success ajouter-panier" data-id="{{ chaussette.id }}">Ajouter au panier</button>
</div>
</div>
{% endfor %}
</div>
<div class="pagination">
{{ knp_pagination_render(chaussettes) }}
</div>
{% else %}
<div class="alert alert-warning">
<p>Désolé, on n'a plus d'chaussettes {{ couleur }}.
Votre hamster va s'les geler.</p>
<p>Consultez nos <a href="{{ path('hamster_chaussettes', {couleur: 'toutes'}) }}">autres modèles disponibles</a>.</p>
</div>
{% endif %}
</div>
<script src="{{ asset('js/chaussettes.js') }}"></script>
{% endblock %}
Et comme un bon pâtissier a ses recettes secrètes, Twig a ses extensions. Tu peux créer tes propres filtres et fonctions. Comme ça, quand tu veux une présentation spéciale, tu l'as :
// Une extension Twig, c'est comme une technique de dressage spéciale du chef
class ChaussetteExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('prix_format', [$this, 'formatPrix']),
];
}
public function formatPrix(float $prix): string
{
if ($prix < 5) {
return 'Petit prix : ' . number_format($prix, 2) . '€';
} elseif ($prix < 10) {
return 'Prix moyen : ' . number_format($prix, 2) . '€';
} else {
return 'Prix premium : ' . number_format($prix, 2) . '€';
}
}
}
Le sommelier : le composant Asset
Le composant Asset, c'est le sommelier. Il connaît toutes les bouteilles de la cave, sait exactement où elles sont rangées et quelle année est la meilleure. Dans Symfony, il gère tous les fichiers statiques comme les CSS, les JS, les images : il sait où ils sont, comment les organiser, et quelle version servir.
{# Le sommelier qui te conseille le bon millésime #}
<link href="{{ asset('css/app.css', version='v2') }}" rel="stylesheet">
<script src="{{ asset('js/chaussettes.js', version='v1.2.3') }}"></script>
<img src="{{ asset('images/hamster-chaussettes.jpg') }}" alt="Hamster stylé">
Et avec Webpack Encore, c'est comme si le sommelier avait un assistant qui prépare et classe tous les vins automatiquement :
// webpack.config.js - Le registre de la cave à vin
Encore
.setOutputPath('public/build/')
.setPublicPath('/build')
.addEntry('app', './assets/app.js')
.addStyleEntry('css/admin', './assets/styles/admin.scss')
.enableStimulusBridge('./assets/controllers.json')
.splitEntryChunks()
.enableSingleRuntimeChunk()
.cleanupOutputBeforeBuild()
.enableSourceMaps(!Encore.isProduction())
.enableVersioning(Encore.isProduction())
.enableSassLoader()
Le personnel de service
Symfony, c'est aussi tout un personnel de service qui t'aide à gérer ton resto.
Le videux : le composant Security
Le composant Security, c'est le videur du resto. Il vérifie les cartes d'identité, s'assure que t'es sur la liste des VIP, et t'envoie bouler si t'es pas habillé correctement. Dans Symfony, il gère l'authentification et les autorisations.
# La liste des VIP et le dress code, en code
security:
enable_authenticator_manager: true
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
App\Entity\User:
algorithm: auto
cost: 15
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\LoginFormAuthenticator
logout:
path: app_logout
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 semaine
# Activer le login avec différents réseaux sociaux
oauth:
resource_owners:
facebook: "/login/check-facebook"
google: "/login/check-google"
login_path: /login
use_forward: false
failure_path: /login
oauth_user_provider:
service: my.oauth_user_provider
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/profile, roles: ROLE_USER }
- { path: ^/hamster/acheter, roles: ROLE_USER }
Et pour les autorisations fines, t'as les Voters, qui sont comme les vigiles qui discutent entre eux quand il y a un client suspect :
// Un Voter, c'est comme un videur qui décide si tu peux rentrer ou pas
class ChaussetteVoter extends Voter
{
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, ['ACHETER', 'VOIR', 'MODIFIER'])
&& $subject instanceof Chaussette;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
// Si pas connecté, c'est non direct
if (!$user instanceof User) {
return false;
}
/** @var Chaussette $chaussette */
$chaussette = $subject;
return match($attribute) {
'VOIR' => true, // Tout le monde peut voir
'ACHETER' => $chaussette->getStock() > 0, // Vérifier stock
'MODIFIER' => $this->canEdit($chaussette, $user),
default => false,
};
}
private function canEdit(Chaussette $chaussette, User $user): bool
{
// Si admin, peut tout modifier
if (in_array('ROLE_ADMIN', $user->getRoles())) {
return true;
}
// Si vendeur, peut modifier que ses propres chaussettes
return $chaussette->getVendeur() === $user->getId();
}
}
Le serveur : le composant Form
Le composant Form, c'est le serveur. Il prend ta commande, vérifie que t'as pas demandé un truc qui existe pas sur la carte, et transmet tout ça en cuisine. Dans Symfony, il gère les formulaires.
// Le serveur qui note ta commande avec un stylo qui marche une fois sur deux
class CommandeChaussetteType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('nom', TextType::class, [
'label' => 'Nom de votre hamster',
'attr' => [
'placeholder' => 'Ex: Fluffy, Boule de Poils, Carnage...',
'class' => 'form-control-lg'
],
'help' => 'Nous avons besoin du nom pour personnaliser le paquet.',
'constraints' => [
new NotBlank(['message' => 'Il a bien un nom, votre hamster, non ?']),
new Length([
'min' => 2,
'max' => 50,
'minMessage' => 'Le nom doit faire au moins {{ limit }} caractères',
'maxMessage' => 'Le nom ne peut pas dépasser {{ limit }} caractères'
])
],
])
->add('taille_chaussette', ChoiceType::class, [
'label' => 'Taille des chaussettes',
'choices' => [
'Minuscule' => 'XS',
'Très petit' => 'S',
'Petit mais pas trop' => 'M',
],
'expanded' => true,
'multiple' => false,
'help' => 'En cas de doute, prenez la taille au-dessus.',
])
->add('couleurs', EntityType::class, [
'class' => Couleur::class,
'choice_label' => 'nom',
'multiple' => true,
'expanded' => true,
'label' => 'Couleurs souhaitées',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('c')
->where('c.disponible = :dispo')
->setParameter('dispo', true)
->orderBy('c.nom', 'ASC');
},
])
->add('date_livraison', DateType::class, [
'widget' => 'single_text',
'html5' => true,
'attr' => [
'min' => (new \DateTime('+2 days'))->format('Y-m-d'),
'max' => (new \DateTime('+1 month'))->format('Y-m-d'),
],
])
->add('conditions', CheckboxType::class, [
'label' => 'J\'accepte que mon hamster soit plus stylé que moi',
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'Vous devez accepter les conditions',
]),
],
])
->add('commander', SubmitType::class, [
'label' => 'Commander les chaussettes',
'attr' => [
'class' => 'btn-lg btn-success'
],
])
;
}
}
Et pour le rendu du formulaire, c'est comme si le serveur présentait la carte des desserts :
{# Le serveur qui te présente la carte des desserts avec un sourire commercial #}
{{ form_start(form, {'attr': {'class': 'form-commander'}}) }}
<div class="row">
<div class="col-md-6">
{{ form_row(form.nom) }}
</div>
<div class="col-md-6">
{{ form_row(form.taille_chaussette) }}
</div>
</div>
<div class="couleurs-section">
<h3>Couleurs disponibles</h3>
{{ form_row(form.couleurs) }}
</div>
<div class="terms">
{{ form_row(form.conditions) }}
</div>
<div class="text-center mt-4">
{{ form_row(form.commander) }}
</div>
{{ form_end(form) }}
Le caissier : le composant Validator
Le Validator, c'est le caissier. Il vérifie que t'as assez d'argent, que ta carte bancaire est pas périmée, que t'as pas essayé de commander 15 desserts alors que t'as déjà une note de 200€. Dans Symfony, il s'assure que les données sont valides avant de les traiter.
// Le caissier qui vérifie méticuleusement ton addition
#[UniqueEntity(fields: ['email'], message: 'Cet email est déjà utilisé.')]
class User
{
#[Assert\NotBlank(message: 'Le nom est obligatoire.')]
#[Assert\Length(min: 2, max: 50)]
private ?string $nom = null;
#[Assert\Email(message: 'L\'email "{{ value }}" n\'est pas valide.')]
#[Assert\NotBlank]
private ?string $email = null;
#[Assert\NotBlank]
#[Assert\Regex(
pattern: '/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/',
message: 'Le mot de passe doit contenir au moins 8 caractères, une lettre, un chiffre et un caractère spécial.'
)]
private ?string $password = null;
#[Assert\GreaterThanOrEqual(18, message: 'Vous devez avoir au moins {{ compared_value }} ans.')]
private ?int $age = null;
}
Et tu peux même créer tes propres validateurs, comme un caissier qui vérifie que t'as pas utilisé de faux billets :
// Un validateur custom, c'est comme un caissier qui passe les billets sous la lampe UV
#[Attribute(Attribute::TARGET_PROPERTY)]
class ContientHamster extends Constraint
{
public string $message = 'Le nom "{{ string }}" doit contenir le mot "hamster".';
}
class ContientHamsterValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint): void
{
if (null === $value || '' === $value) {
return;
}
if (!str_contains(strtolower($value), 'hamster')) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ string }}', $value)
->addViolation();
}
}
}
Le téléphone de la cuisine : l'Event Dispatcher
L'Event Dispatcher, c'est le téléphone interne du resto. Le maître d'hôtel appelle le chef pour lui dire "Table 9, ils ont fini l'entrée, tu peux envoyer le plat". Dans Symfony, il permet à différentes parties de l'application de communiquer sans se connaître.
// L'événement, c'est comme crier "Service !" en cuisine
namespace App\Event;
use App\Entity\Commande;
use Symfony\Contracts\EventDispatcher\Event;
class CommandeTermineeEvent extends Event
{
public const NAME = 'commande.terminee';
public function __construct(private readonly Commande $commande)
{
}
public function getCommande(): Commande
{
return $this->commande;
}
}
Et ailleurs dans ton code :
// Dispatcher un événement, c'est comme appuyer sur l'interphone
$this->dispatcher->dispatch(new CommandeTermineeEvent($commande), CommandeTermineeEvent::NAME);
Et quelqu'un d'autre dans la cuisine peut l'écouter :
// Un EventSubscriber, c'est comme un cuisinier qui tend l'oreille quand on l'appelle
namespace App\EventSubscriber;
use App\Event\CommandeTermineeEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class CommandeSubscriber implements EventSubscriberInterface
{
public function __construct(private readonly MailerInterface $mailer)
{
}
public static function getSubscribedEvents(): array
{
return [
CommandeTermineeEvent::NAME => 'onCommandeTerminee',
];
}
public function onCommandeTerminee(CommandeTermineeEvent $event): void
{
$commande = $event->getCommande();
// Envoyer un email à l'acheteur
$email = (new Email())
->from('noreply@chaussettes-hamster.com')
->to($commande->getEmail())
->subject('Votre commande de chaussettes pour hamster !')
->html('<p>Cher client, votre commande de chaussettes est en préparation...</p>');
$this->mailer->send($email);
// Mettre à jour le stock
foreach ($commande->getProduits() as $produit) {
$produit->decrementStock($commande->getQuantite());
// Enregistrer les modifications...
}
}
}
Le livreur de produits frais : l'injection de dépendances
Le Dependency Injection Container, c'est comme le fournisseur du resto. Le chef lui dit "J'ai besoin de 5kg de pommes de terre, 3kg de bœuf et 10 litres de crème", et le fournisseur s'occupe de tout apporter. Dans Symfony, il s'occupe de fournir à chaque service ce dont il a besoin.
# services.yaml - Le bon de commande aux fournisseurs
services:
_defaults:
autowire: true
autoconfigure: true
public: false
# Enregistrer tous les services automatiquement
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# Surcharger la configuration d'un service spécifique
App\Service\EmailService:
arguments:
$senderEmail: '%env(MAILER_SENDER)%'
$isDebug: '%env(bool:APP_DEBUG)%'
# Créer un alias pour un service
App\Interface\PaiementProcessorInterface: '@App\Service\StripePaiementProcessor'
Et dans ton code, tu demandes ce dont t'as besoin dans le constructeur, et comme par magie, le fournisseur livre tout :
class CommandeController extends AbstractController
{
public function __construct(
private readonly ChaussetteRepository $chaussetteRepository,
private readonly PaiementProcessorInterface $paiementProcessor,
private readonly EventDispatcherInterface $dispatcher
) {
}
// Ton controller peut maintenant utiliser tous ces ingrédients livrés frais
}
LE LIVREUR UBER EATS : LE MESSENGER COMPONENT
Le Messenger, c'est comme Deliveroo ou Uber Eats pour ton resto. Quand un client commande à emporter, le livreur prend le sac et part, pendant que le restaurant continue de servir les clients sur place. Dans Symfony, c'est un système de bus de messages asynchrones.
// Une commande à livrer
namespace App\Message;
class EnvoyerEmailConfirmation
{
private int $commandeId;
public function __construct(int $commandeId)
{
$this->commandeId = $commandeId;
}
public function getCommandeId(): int
{
return $this->commandeId;
}
}
// Le dispatch d'une livraison
use Symfony\Component\Messenger\MessageBusInterface;
class CommandeManager
{
private MessageBusInterface $bus;
public function __construct(MessageBusInterface $bus)
{
$this->bus = $bus;
}
public function finaliserCommande(Commande $commande): void
{
// Traitement synchrone immédiat...
// Envoyer une tâche asynchrone (comme un livreur qui part avec le sac)
$this->bus->dispatch(new EnvoyerEmailConfirmation($commande->getId()));
}
}
Et le handler, c'est comme le livreur qui sonne à la porte du client :
// Le livreur qui fait la livraison, même sous la pluie
namespace App\MessageHandler;
use App\Message\EnvoyerEmailConfirmation;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class EnvoyerEmailConfirmationHandler
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly MailerInterface $mailer
) {
}
public function __invoke(EnvoyerEmailConfirmation $message): void
{
$commandeId = $message->getCommandeId();
$commande = $this->entityManager->getRepository(Commande::class)->find($commandeId);
if (!$commande) {
throw new \Exception('Commande introuvable');
}
// Envoyer l'email de confirmation
$email = (new Email())
->from('noreply@chaussettes-hamster.com')
->to($commande->getEmail())
->subject('Confirmation de votre commande #' . $commande->getId())
->html('<p>Merci pour votre commande ! Votre hamster va avoir les pieds bien au chaud.</p>');
$this->mailer->send($email);
// Mettre à jour le statut
$commande->setStatut('email_envoye');
$this->entityManager->flush();
}
}
Le responsable des réservations : le composant Cache
LE RESPONSABLE DES RÉSERVATIONS : LE CACHE COMPONENT
Le Cache Component, c'est comme le responsable des réservations du resto. Quand quelqu'un appelle pour réserver, il note tout dans son carnet. Si la même personne rappelle pour vérifier l'heure, il n'a pas besoin de refaire tout le processus, il regarde juste dans son carnet. Dans Symfony, le Cache permet de stocker temporairement des données pour éviter de refaire des calculs coûteux.
// Le responsable des réservations avec son carnet bien organisé
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
class ChaussetteService
{
public function __construct(
private readonly TagAwareCacheInterface $cache,
private readonly ChaussetteRepository $repository
) {
}
public function getChaussettesPopulaires(): array
{
// Chercher dans le cache d'abord (comme regarder dans le carnet)
return $this->cache->get('chaussettes_populaires', function (ItemInterface $item) {
// Si pas trouvé, calculer (comme faire une nouvelle réservation)
$item->expiresAfter(3600); // Valide pendant 1 heure
$item->tag(['chaussettes', 'populaires']);
return $this->repository->trouverChaussettesPopulaires();
});
}
public function invalidateCache(): void
{
// Quand le stock change, on efface les entrées concernées du carnet
$this->cache->invalidateTags(['chaussettes']);
}
}
Le comptable : le Profiler et le Logger
Le Profiler et le Logger, c'est comme le comptable du resto. Il note combien de clients sont venus, combien ont commandé du vin, lequel a laissé un pourboire. Dans Symfony, ils te permettent de comprendre ce qui se passe dans ton application, de mesurer les performances et de détecter les problèmes.
// Le comptable qui note tout méticuleusement
use Psr\Log\LoggerInterface;
class CommandeController extends AbstractController
{
public function __construct(private readonly LoggerInterface $logger)
{
}
#[Route('/commande/new', name: 'commande_new')]
public function new(Request $request): Response
{
$this->logger->info('Nouvelle commande initiée', [
'ip' => $request->getClientIp(),
'user_agent' => $request->headers->get('User-Agent')
]);
try {
// Traitement de la commande...
$commande = new Commande();
// ...
$this->logger->info('Commande créée avec succès', [
'id' => $commande->getId(),
'montant' => $commande->getMontant()
]);
return $this->redirectToRoute('commande_success');
} catch (\Exception $e) {
$this->logger->error('Erreur lors de la création de commande', [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return $this->redirectToRoute('commande_error');
}
}
}
Et avec le Web Profiler, c'est comme si le comptable te montrait ses beaux tableaux Excel avec des graphiques colorés :
# config/packages/dev/web_profiler.yaml
web_profiler:
toolbar: true
intercept_redirects: false
framework:
profiler:
only_exceptions: false
collect: true
Le système de gestion de résevrations : les Bundles
Les Bundles, c'est comme si t'achetais un système de gestion de réservation tout prêt pour ton resto. T'as pas besoin de l'inventer toi-même, tu l'installes, tu le configures, et ça marche. Dans Symfony, les Bundles sont des packages réutilisables qui ajoutent des fonctionnalités.
# Installer un Bundle, c'est comme acheter un logiciel clé en main
composer require friendsofsymfony/user-bundle
# Configurer un Bundle, c'est comme paramétrer ton nouveau logiciel
# config/packages/fos_user.yaml
fos_user:
db_driver: orm
firewall_name: main
user_class: App\Entity\User
from_email:
address: "noreply@chaussettes-hamster.com"
sender_name: "Chaussettes pour Hamsters"
Et y'en a des dizaines qui font plein de trucs différents :
- EasyAdminBundle : pour créer une interface d'administration en 2 minutes
- ApiPlatform : pour créer une API RESTful complète sans effort
- Et bien d'autres...
C'est comme si tu pouvais acheter tous les départements de ton resto séparément, déjà tout prêts.
La formation du personnel : la documentation et la communauté
J'ai pas parlé de la formation ! Symfony, c'est aussi une documentation énorme, des tutoriels, des vidéos, des conférences, et une communauté active. C'est comme si chaque membre de ton personnel de resto venait avec son propre manuel d'utilisation et un groupe de support 24/7.
- La doc officielle : plus de 1000 pages de documentation détaillée
- SymfonyCasts : des vidéos et tutoriels pas à pas
- SymfonyWorld, SymfonyLive : des conférences partout dans le monde
- Slack, Discord, Stack Overflow : des communautés où tu peux poser tes questions
L'addition
On va pas se mentir, pour appréhender Symfony, il faut une bonne compréhension des bases de PHP (notamment en POO) : c'est comme un resto 3 étoiles au Guide Michelin. Si tout ce que tu veux c'est un sandwich jambon-beurre, t'as peut-être pas besoin de tout ça.
Mais si tu veux faire un site costaud, qui tient la route, qui peut évoluer sans que tout se casse la gueule, Symfony c'est un bon choix. C'est comme un grand restaurant qui a déjà formé tout son personnel avant même que t'arrives.
Alors oui, au début, c'est intimidant. Tous ces composants, toutes ces configurations... Mais une fois que tu comprends les tenants et les aboutissants de comment la cuisine fonctionne, tu peux faire des trucs incroyables.
Et si jamais tu galères, il y a toute une brigade de cuisiniers sur StackOverflow prêts à t'expliquer pourquoi ta sauce est en train de tourner.
Voilà. Symfony. La haute gastronomie du développement web. Ça coûte cher en temps d'apprentissage, mais putain, ça en vaut la peine.
Bon appétit, et n'oublie pas de laisser un pourboire à ton framework préféré en contribuant au code open-source.
Le nom des variables, des getters et setters sont volontairement en francais pour laisser un peu de compréhension à l´attention des personnes qui découvrent l´univers des frameworks.