Migration winzou State Machine vers Symfony Workflow dans Sylius 2.0 : Guide Technique Approfondi
La migration de winzou/state-machine-bundle vers Symfony Workflow est l'un des chantiers les plus structurants du passage a Sylius 2.0. Chaque boutique Sylius repose sur des state machines pour les commandes, paiements, expeditions et retours. Si vous avez personnalisé ces workflows — callbacks custom, guards metier, transitions conditionnelles — ce guide technique vous accompagne etape par etape avec des exemples de code concrets.
Comprendre le changement d'architecture
winzou/state-machine-bundle et Symfony Workflow reposent sur des philosophies fondamentalement differentes. Comprendre cette distinction est essentiel avant de toucher a la moindre ligne de configuration.
winzou : l'approche config-driven
Avec winzou, tout est dans le YAML. Les callbacks (actions executees lors d'une transition) et les guards (conditions d'autorisation) sont declares directement dans la configuration, avec des references a des services Symfony et leurs methodes :
# config/packages/sylius_order.yaml (winzou)
winzou_state_machine:
sylius_order:
class: App\Entity\Order\Order
property_path: state
graph: sylius_order
states:
cart: ~
new: ~
confirmed: ~
cancelled: ~
fulfilled: ~
transitions:
confirm:
from: [new]
to: confirmed
cancel:
from: [new, confirmed]
to: cancelled
fulfill:
from: [confirmed]
to: fulfilled
callbacks:
guard:
prevent_cancel_if_shipped:
on: cancel
do: ['@App\Guard\OrderCancelGuard', 'canCancel']
args: ['object']
after:
send_confirmation_email:
on: ['confirm']
do: ['@App\Callback\OrderConfirmationCallback', 'sendEmail']
args: ['object']
update_stock:
on: ['fulfill']
do: ['@App\Callback\StockUpdateCallback', 'decrementStock']
args: ['object']
Symfony Workflow : l'approche event-driven
Symfony Workflow separe strictement la definition du workflow (places, transitions) de la logique metier (event subscribers). La configuration YAML ne contient que la structure. Toute la logique est dans des classes PHP qui ecoutent les evenements du workflow :
# config/packages/workflow.yaml (Symfony Workflow)
framework:
workflows:
sylius_order:
type: state_machine
audit_trail:
enabled: '%kernel.debug%'
marking_store:
type: method
property: state
supports:
- App\Entity\Order\Order
initial_marking: cart
places:
- cart
- new
- confirmed
- cancelled
- fulfilled
transitions:
confirm:
from: new
to: confirmed
cancel:
from: [new, confirmed]
to: cancelled
fulfill:
from: confirmed
to: fulfilled
Remarquez l'absence totale de callbacks dans la configuration. La logique est entierement decouplees dans des EventSubscribers.
Etape 1 : Activer le double adaptateur dans Sylius 1.13+
Avant de migrer, activez la cohabitation des deux systemes. Sylius 1.13 et 1.14 proposent un adaptateur configurable :
# config/packages/sylius_core.yaml
sylius_core:
state_machine:
default_adapter: winzou # garder winzou par defaut
graphs_to_adapters_mapping:
sylius_order: symfony_workflow # migrer ce graphe en premier
# sylius_payment: winzou # reste sur winzou pour l'instant
# sylius_shipping: winzou # reste sur winzou pour l'instant
Cette configuration vous permet de migrer un workflow a la fois, en production, sans interruption de service. Commencez par le workflow le moins critique pour votre metier, validez-le en staging, puis procedez aux suivants.
Etape 2 : Convertir la configuration YAML
La conversion de la structure YAML suit des regles precises. Voici un mapping complet des differences :
| winzou | Symfony Workflow |
|---|---|
class: | supports: (tableau) |
property_path: | marking_store.property: |
states: | places: |
graph: | Cle du workflow (nom) |
callbacks.guard: | EventSubscriber sur GuardEvent |
callbacks.before: | EventSubscriber sur TransitionEvent (pre) |
callbacks.after: | EventSubscriber sur CompletedEvent |
Le Sylius Upgrade Analyzer genere automatiquement la configuration Symfony Workflow À partir de vos fichiers winzou existants via son auto-fixer dedie. Lancez-le avec --fix --dry-run pour obtenir un diff de la conversion sans toucher a vos fichiers :
vendor/bin/sylius-upgrade-analyzer sylius-upgrade:analyze --fix --dry-run
Etape 3 : Migrer les callbacks vers des EventSubscribers
C'est le coeur de la migration. Chaque callback winzou devient un EventSubscriber Symfony. Voici la conversion complete du callback send_confirmation_email :
Avant : Callback winzou
// src/Callback/OrderConfirmationCallback.php
namespace App\Callback;
use App\Entity\Order\Order;
use App\Service\Mailer\OrderMailerInterface;
class OrderConfirmationCallback
{
public function __construct(
private OrderMailerInterface $mailer,
) {}
public function sendEmail(Order $order): void
{
$this->mailer->sendConfirmationEmail($order);
}
}
Apres : EventSubscriber Symfony Workflow
// src/EventSubscriber/Workflow/OrderConfirmationSubscriber.php
namespace App\EventSubscriber\Workflow;
use App\Entity\Order\Order;
use App\Service\Mailer\OrderMailerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\CompletedEvent;
class OrderConfirmationSubscriber implements EventSubscriberInterface
{
public function __construct(
private OrderMailerInterface $mailer,
) {}
public static function getSubscribedEvents(): array
{
return [
// Format : 'workflow.[nom_workflow].completed.[transition]'
'workflow.sylius_order.completed.confirm' => 'onOrderConfirmed',
];
}
public function onOrderConfirmed(CompletedEvent $event): void
{
/** @var Order $order */
$order = $event->getSubject();
$this->mailer->sendConfirmationEmail($order);
}
}
Points cles de cette conversion :
- Le callback
afterwinzou correspond a l'evenementcompletedde Symfony Workflow - Le nom de l'evenement suit le format
workflow.[workflow_name].completed.[transition_name] - Le sujet (l'entite) est accessible via
$event->getSubject() - Le contexte additionnel est disponible via
$event->getContext()
Hierarchie des evenements Symfony Workflow
Symfony Workflow dispatch plusieurs evenements dans un ordre precis pour chaque transition. Comprendre cette hierarchie est crucial pour placer votre logique au bon moment :
// Ordre d'execution des evenements lors de $workflow->apply($order, 'confirm')
1. workflow.guard // Global guard
2. workflow.sylius_order.guard // Guard pour ce workflow
3. workflow.sylius_order.guard.confirm // Guard pour cette transition
4. workflow.leave // Global leave
5. workflow.sylius_order.leave // Leave pour ce workflow
6. workflow.sylius_order.leave.new // Leave pour cette place
7. workflow.transition // Global transition
8. workflow.sylius_order.transition // Transition pour ce workflow
9. workflow.sylius_order.transition.confirm // Cette transition specifique
10. workflow.enter // Global enter
11. workflow.sylius_order.enter // Enter pour ce workflow
12. workflow.sylius_order.enter.confirmed // Enter pour cette place
13. workflow.entered // Global entered (marking mis a jour)
14. workflow.sylius_order.entered // Entered pour ce workflow
15. workflow.sylius_order.entered.confirmed // Entered pour cette place
16. workflow.completed // Global completed
17. workflow.sylius_order.completed // Completed pour ce workflow
18. workflow.sylius_order.completed.confirm // Completed pour cette transition
Etape 4 : Migrer les guards vers l'Expression Language
Les guards winzou sont des callbacks PHP. Symfony Workflow offre deux approches : l'Expression Language pour les conditions simples (directement dans le YAML), et les GuardEvent subscribers pour la logique complexe.
Guard simple avec Expression Language
# config/packages/workflow.yaml
framework:
workflows:
sylius_order:
transitions:
cancel:
from: [new, confirmed]
to: cancelled
guard: "subject.getShipmentState() !== 'shipped'"
L'Expression Language a acces a subject (l'entite) et permet d'ecrire des conditions sans creer de classe PHP. C'est ideal pour les verifications de proprietes simples.
Guard complexe avec EventSubscriber
Pour une logique metier impliquant des services externes (verification en base, appel API, regles metier complexes), utilisez un GuardEvent subscriber :
// src/EventSubscriber/Workflow/OrderCancelGuardSubscriber.php
namespace App\EventSubscriber\Workflow;
use App\Entity\Order\Order;
use App\Repository\ShipmentRepositoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\GuardEvent;
class OrderCancelGuardSubscriber implements EventSubscriberInterface
{
public function __construct(
private ShipmentRepositoryInterface $shipmentRepository,
) {}
public static function getSubscribedEvents(): array
{
return [
'workflow.sylius_order.guard.cancel' => 'guardCancel',
];
}
public function guardCancel(GuardEvent $event): void
{
/** @var Order $order */
$order = $event->getSubject();
// Verifier si des colis sont deja en transit
$inTransit = $this->shipmentRepository->findInTransitByOrder($order);
if (count($inTransit) > 0) {
// Bloquer la transition avec un message explicatif
$event->setBlocked(
true,
sprintf(
'Impossible d'annuler : %d colis en transit.',
count($inTransit)
)
);
}
}
}
Etape 5 : Gerer les cas limites
Transitions concurrentes et verrouillage
En production, deux processus peuvent tenter d'appliquer des transitions sur la meme entite simultanement (webhook de paiement + action admin). Symfony Workflow ne gere pas le locking nativement. Implementez un verrouillage optimiste avec Doctrine :
// src/Entity/Order/Order.php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Order
{
#[ORM\Version]
#[ORM\Column(type: 'integer')]
private int $version = 1;
// ... reste de l'entite
}
// src/Service/OrderWorkflowHandler.php
use Doctrine\ORM\OptimisticLockException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Workflow\WorkflowInterface;
class OrderWorkflowHandler
{
public function __construct(
private WorkflowInterface $syliusOrderWorkflow,
private LoggerInterface $logger,
) {}
public function applyTransition(Order $order, string $transition): bool
{
if (!$this->syliusOrderWorkflow->can($order, $transition)) {
$this->logger->warning('Transition {t} non autorisee pour commande {id} (etat: {s})', [
't' => $transition,
'id' => $order->getId(),
's' => $order->getState(),
]);
return false;
}
try {
$this->syliusOrderWorkflow->apply($order, $transition);
return true;
} catch (OptimisticLockException $e) {
$this->logger->error('Conflit de version sur commande {id}', [
'id' => $order->getId(),
]);
return false;
}
}
}
Rollback en cas d'echec d'un subscriber
Si un EventSubscriber leve une exception apres le changement d'etat, le marking a deja ete mis a jour en memoire. Encapsulez vos transitions critiques dans une transaction Doctrine :
// Transition dans une transaction
$this->entityManager->beginTransaction();
try {
$this->workflow->apply($order, 'confirm');
$this->entityManager->flush();
$this->entityManager->commit();
} catch (\Throwable $e) {
$this->entityManager->rollback();
// L'entite en memoire a son state modifie,
// il faut rafraichir depuis la BDD
$this->entityManager->refresh($order);
throw $e;
}
Etape 6 : Debugging visuel avec workflow:dump
Symfony fournit une commande pour generer une representation visuelle de vos workflows. C'est un outil indispensable pour valider la migration :
# Generer un fichier DOT (Graphviz)
php bin/console workflow:dump sylius_order | dot -Tpng -o order_workflow.png
# Generer du SVG pour integration web
php bin/console workflow:dump sylius_order | dot -Tsvg -o order_workflow.svg
# Avec le marking actuel d'une entite (Sylius 2.0+)
php bin/console workflow:dump sylius_order --marking=confirmed
Le Profiler Symfony (barre de debug) affiche egalement en temps reel les workflows appliques, les transitions autorisees, et les blocages de guards pour chaque requete HTTP. Activez audit_trail: enabled: true dans la config pour logguer chaque transition dans les logs Symfony.
Etape 7 : Tester vos workflows
Chaque workflow migre doit etre couvert par des tests. Voici un pattern de test complet :
// tests/Workflow/OrderWorkflowTest.php
namespace App\Tests\Workflow;
use App\Entity\Order\Order;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\WorkflowInterface;
class OrderWorkflowTest extends KernelTestCase
{
private WorkflowInterface $workflow;
protected function setUp(): void
{
self::bootKernel();
$registry = self::getContainer()->get(Registry::class);
$this->workflow = $registry->get(new Order(), 'sylius_order');
}
public function testNewOrderCanBeConfirmed(): void
{
$order = new Order();
$order->setState('new');
$this->assertTrue($this->workflow->can($order, 'confirm'));
$this->workflow->apply($order, 'confirm');
$this->assertSame('confirmed', $order->getState());
}
public function testCartCannotBeDirectlyFulfilled(): void
{
$order = new Order();
$order->setState('cart');
$this->assertFalse($this->workflow->can($order, 'fulfill'));
}
public function testCancelledOrderCannotTransition(): void
{
$order = new Order();
$order->setState('cancelled');
$transitions = $this->workflow->getEnabledTransitions($order);
$this->assertEmpty($transitions);
}
public function testGuardBlocksCancelWhenShipped(): void
{
$order = $this->createOrderWithShippedShipment();
$order->setState('confirmed');
$this->assertFalse($this->workflow->can($order, 'cancel'));
// Verifier le message de blocage
$blockers = $this->workflow->buildTransitionBlockerList($order, 'cancel');
$this->assertGreaterThan(0, count($blockers));
}
}
Checklist de migration par workflow
Appliquez cette checklist pour chaque workflow migre (sylius_order, sylius_payment, sylius_shipping, sylius_order_checkout, sylius_return) :
- Ecrire la configuration Symfony Workflow equivalente dans
config/packages/workflow.yaml - Convertir chaque callback
afteren EventSubscriber surCompletedEvent - Convertir chaque callback
beforeen EventSubscriber surTransitionEvent - Convertir chaque guard en Expression Language ou GuardEvent subscriber
- Ecrire des tests pour chaque transition et chaque guard
- Valider visuellement avec
workflow:dump - Basculer le graphe dans
graphs_to_adapters_mapping - Tester en staging avec des scenarios complets (commande de bout en bout)
- Deployer et monitorer les logs
audit_trail
Conclusion
La migration de winzou vers Symfony Workflow dans Sylius 2.0 est bien plus qu'un changement de syntaxe YAML. C'est un changement de paradigme : d'une approche monolithique config-driven vers une architecture event-driven decouplees et testable. Le travail est consequent si vous avez des workflows personnalisés, mais le resultat est un systeme plus robuste, mieux outille (Profiler, audit trail, dump visuel) et parfaitement integre a l'ecosysteme Symfony. Utilisez le Sylius Upgrade Analyzer pour generer automatiquement la configuration Symfony Workflow depuis vos fichiers winzou et identifier chaque callback a migrer.
