DEV Community

Cover image for 🏗️ Où doit vivre le code natif ? Phalcon, Symfony et TrueAsync à différentes couches de la pile
Mathieu Ledru
Mathieu Ledru

Posted on

🏗️ Où doit vivre le code natif ? Phalcon, Symfony et TrueAsync à différentes couches de la pile

Pendant des années, Phalcon a été une réponse rationnelle pour les plateformes PHP à fort trafic : un framework livré en extension C, peu gourmand, MVC familier.

Récemment, lors d’un POC d’architecture pour un client, je me suis retrouvé face à une situation assez représentative des plateformes PHP matures : une base existante performante, des contraintes de maintenance, des workflows devenus plus complexes que le cycle requête-réponse, et la tentation de répondre trop vite par un choix de framework. La question visible était : faut-il rester proche d’un modèle Phalcon ou reconstruire autour de Symfony ? La question réelle était plus profonde : où doivent vivre la performance native, les boundaries métier et l’orchestration des workflows ?

Phalcon, Symfony et TrueAsync ne sont pas des concurrents directs : ils optimisent des contraintes différentes, à des couches différentes de la pile. Cet article part de ce constat terrain pour proposer une lecture plus large - sans présupposer qu’une migration est toujours la bonne réponse.

Introduction - La question n’est plus « quel framework est plus rapide ? »

Ce POC n’était pas isolé. Sur une plateforme read-heavy, le dilemme se reformule souvent en termes binaires : Phalcon d’un côté - extension C, faible overhead, MVC éprouvé —, Symfony de l’autre - écosystème, structure, recrutement plus simple —, avec TrueAsync en toile de fond comme promesse de runtime async. Chacune de ces options défendait quelque chose de légitime. Aucune ne répondait seule à la question qui s’imposait peu à peu : à quelle couche doit-on adresser la performance, et à quelle couche l’orchestration ?

La discussion a vite dépassé le choix de framework. Faut-il rester sur une architecture proche de Phalcon, où le natif vit au niveau MVC ? Migrer vers Symfony pour la maintenabilité et l’interopérabilité ? Introduire TrueAsync comme levier runtime, indépendamment de la décision web ? Ou confondre migration framework et redessin des workflows - la erreur la plus fréquente que nous voyons sur ce type de plateforme ?

La question affichée était : « Migre-t-on de Phalcon vers Symfony ? » La question de fond est devenue : où doit vivre le code natif - et où doivent vivre les boundaries et les pipelines métier ?

Trois réponses coexistent pour le natif, chacune à une couche différente :

  • Couche framework - Phalcon optimise routeur, dispatcher, ORM, cache via une extension C
  • Couche application - Symfony structure l’application en PHP pur : DI, sĂ©curitĂ©, bundles, conventions d’organisation
  • Couche runtime - TrueAsync vise l’exĂ©cution concurrente et l’I/O non bloquante via ext-async

Ces trois approches ne s’excluent pas mutuellement. Mais le POC a surtout révélé un quatrième axe, souvent négligé dans les débats « framework A vs framework B » : où vivent les workflows, et comment séparer la logique métier du modèle d’exécution qui la porte. Boundaries, orchestration et runtime sont trois décisions indépendantes. Les confondre - comme le suggérait la formulation initiale du projet - mène aux migrations les plus coûteuses, et les moins efficaces.

Chez Darkwood, c’est cette lentille que nous essayons d’appliquer : l’industrie a longtemps optimisé les frameworks ; les systèmes complexes exposent aujourd’hui un autre goulot - la définition des boundaries et l’orchestration explicite. Le runtime natif reste un détail d’implémentation, pas l’architecture. L’article qui suit généralise ce que ce POC a cristallisé en une lecture applicable au-delà d’un cas particulier.

Pourquoi Phalcon a eu du sens - et en a encore

Phalcon est un framework PHP full-stack dont le cœur est compilé. Écrit en Zephir, transpilé vers du C via le projet cphalcon, il s’utilise comme n’importe quel framework PHP : controllers, services, modules. La différence est dans la pile : une requête HTTP arrive dans PHP-FPM, l’extension est déjà chargée, et les primitives critiques - routeur, dispatcher, ORM, cache - vivent dans la couche native. Le code métier reste en PHP ; le squelette framework est optimisé en amont.

Pour une plateforme éditoriale, média ou e-commerce, les avantages restent tangibles sur le bon profil. Faible overhead sur le chemin MVC. Modèle cohérent : modules, injection de dépendances, ORM, PHQL, Volt, cache. APIs read-heavy alimentées par des documents pré-calculés. Monolithe multi-surfaces sans empiler plusieurs stacks.

Phalcon n’était pas « plus moderne » que Symfony. Il était plus natif au bon endroit - au niveau framework - pour un profil de charge précis. Et pour les équipes qui maîtrisent cette stack, qui atteignent leurs SLOs, et dont le produit évolue peu, cet avantage n’a pas disparu du jour au lendemain.

Phalcon en 2026 : maintenance, direction, crédibilité

Une lecture honnête de Phalcon en 2026 commence par les faits - pas par l’hypothèse d’un framework en déclin.

La branche 5.x est activement maintenue. La version 5.16.0 a été publiée en juin 2026, après une cadence de releases accélérée au second trimestre. Les commits sur la branche 5.0.x sont fréquents ; le changelog documente des évolutions substantielles : couche Phalcon\Contracts, nouveau container DI, composant Auth, couche Queue (Beanstalk, Redis, Memory, Stream), support PIE pour l’installation, migration vers Zephir 1.0, outillage qualité (Infection, Sonar). Ce n’est pas la maintenance minimale d’un projet figé.

L’écosystème dispose d’un socle de crédibilité documenté. Le fichier BACKERS.md du dépôt cphalcon liste des soutiens industriels - dont Cloudflare (également référencé sur la page des sponsorships Cloudflare), Algolia et DigitalOcean (partenariat open source documenté pour benchmarks et infrastructure). Open Collective et GitHub Sponsors alimentent un financement communautaire modeste mais réel. La gouvernance repose sur une équipe core concentrée - Nikolaos Dimopoulos et Anton Vasiliev en tête - avec des processus de contribution structurés.

Ces éléments ne prouvent pas que Phalcon est « meilleur » que Symfony. Ils prouvent qu’il ne s’agit pas d’un écosystème abandonné. Un sponsor industriel n’est pas une garantie technique ; c’est un signal que le projet reste visible et soutenu par des acteurs qui croient à sa pérennité.

Sur le plan architectural, cphalcon reste fondamentalement Zephir/C : plus de mille fichiers .zep, distribution en extension via PIE ou PECL. La prochaine ligne majeure en préparation est la branche 7.0.x (PHP 8.3+), pas une branche v6 dans ce dépôt. Les discussions publiques et le travail sur un dépôt PHP compagnon (phalcon/phalcon) suggèrent une évolution vers un modèle dual-track : implémentation de référence en PHP, extension optionnelle pour les zones les plus coûteuses - identifiées par mesure. C’est une maturation architecturale, pas un enterrement du natif.

Dimension Phalcon (v5, 2026) Symfony
Distribution Extension C (PIE / PECL) Packages Composer, PHP pur
Levier principal Overhead framework minimal Écosystème, structure, interop
Recrutement Profils plus rares Marché large
Montée de version PHP Rebuild / repinning extension Semver Composer
Maintenance actuelle Releases fréquentes, features nouvelles Releases fréquentes, LTS
Meilleur fit Équipe experte, SLOs tenus, perf MVC critique Équipe en croissance, intégrations nombreuses, churn produit

Le tableau ne désigne pas de vainqueur. Il cartographie des contraintes.

Phalcon n’est pas obsolète - la cible d’optimisation a changé

La mauvaise question est : « Phalcon est-il dépassé ? » La bonne question est : « Quelle optimisation compte le plus maintenant ? »

Pendant longtemps, les priorités étaient claires :

  • RĂ©duire le coĂ»t du dispatch MVC et du bootstrap framework
  • Minimiser l’overhead mĂ©moire et CPU sur le chemin requĂŞte → rĂ©ponse
  • Servir des APIs read-heavy depuis un monolithe performant sans multiplier les runtimes

Phalcon répondait à ces priorités de manière cohérente. Le framework natif était le bon levier.

Les priorités ont évolué - pas parce que Phalcon a « raté » quelque chose, mais parce que les systèmes ont grossi et les goulots ont bougé :

  • ComplexitĂ© distribuĂ©e - bus de messages, workers, modèles dĂ©rivĂ©s, invalidation cache Ă  grande Ă©chelle
  • Goulots I/O - agrĂ©gation HTTP, fichiers, attentes rĂ©seau dans les workers, pas seulement le temps de bootstrap
  • Orchestration - chaĂ®nes de handlers, cron, scripts CLI : la logique workflow Ă©chappe au MVC
  • Onboarding et interop - recrutement, bundles, standards de sĂ©curitĂ©, intĂ©grations tierces
  • ÉvolutivitĂ© organisationnelle - bounded contexts, extraction progressive, coexistence de stacks

Sur beaucoup de plateformes matures, le routeur natif n’est plus le bottleneck mesuré. L’I/O bloquante dans les workers, la complexité des workflows, ou le coût humain de la maintenance l’est. C’est ce déplacement - pas un verdict de valeur - qui change la conversation architecturale.

Phalcon reste pertinent quand la contrainte dominante est encore le chemin MVC synchrone et que l’équipe maîtrise la stack. Symfony devient plus pertinent quand la contrainte dominante est l’organisation du code et l’écosystème sur dix ans. TrueAsync entre en jeu quand la contrainte dominante est l’attente I/O dans le runtime - indépendamment du framework choisi.

OĂą doit vivre le code natif ?

Revenons Ă  la question centrale. Trois couches, trois logiques :

flowchart TB
  subgraph layers [Couches de la pile PHP]
    FrameworkLayer["Couche framework\nPhalcon extension C"]
    AppLayer["Couche application\nSymfony structure"]
    RuntimeLayer["Couche runtime\nTrueAsync ext-async"]
  end
  Question["OĂą vit le code natif ?"]
  Question --> FrameworkLayer
  Question --> AppLayer
  Question --> RuntimeLayer
Enter fullscreen mode Exit fullscreen mode
Phalcon Symfony TrueAsync
Couche Framework Application Runtime
Cible Routeur, ORM, MVC, cache Structure, DI, sécurité, conventions Coroutines, I/O, pools
Question Comment rendre le framework plus rapide ? Comment organiser l’application sur le long terme ? Comment exécuter plus de travail I/O sans bloquer ?
Distribution Extension framework (PIE / PECL) Composer, PHP pur Fork PHP + ext-async
Maturité (2026) v5 maintenue, v7 en préparation Production standard Expérimental

Phalcon et TrueAsync partagent une intuition - rapprocher le travail critique du natif - mais pas le même problème. Phalcon optimise l’entrée dans le framework. TrueAsync optimise l’attente une fois dans le code métier. Symfony, lui, ne mise pas sur le natif framework : il mise sur la structure et l’interopérabilité.

Le placement du natif est un choix d’architecture, pas une question de mode. On peut avoir du natif à la couche framework sans natif au runtime. On peut introduire du runtime natif sans quitter Phalcon. On peut structurer en Symfony sans TrueAsync. Les combinaisons dépendent des contraintes mesurées - pas d’un narratif « old vs new ».

Symfony - une autre optimisation, pas un remplacement universel

Symfony est une base structurelle : HttpKernel, routing, DI, security, console, cache, Messenger, intégration Doctrine, tooling mature. Pour une plateforme qui doit documenter, auditer et faire évoluer son code pendant des années, c’est un socle organisateur crédible.

L’argument n’est pas seulement technique. C’est aussi organisationnel : recrutement plus simple, documentation abondante, intégrations standards, trajectoire claire sur plusieurs versions de PHP sans dépendre d’une extension framework compilée pour chaque montée.

Mais Symfony optimise une autre contrainte que Phalcon. Il ne promet pas un routeur en C. Il promet une application maintenable, testable, intégrable - avec un écosystème qui absorbe une grande partie de la complexité transverse (auth, queues, observabilité, validation).

Symfony devient la bonne réponse quand le coût dominant n’est plus le microsecond de bootstrap MVC, mais :

  • la vĂ©locitĂ© de livraison sur un produit en Ă©volution
  • la capacitĂ© Ă  recruter et former
  • l’intĂ©gration avec des services et des standards externes
  • la clartĂ© des boundaries applicatives sur le long terme

Symfony n’est pas automatiquement meilleur. C’est une autre optimisation - et elle a un coût de migration réel qu’il ne faut pas sous-estimer.

TrueAsync - runtime natif, expérimental, non automatique

TrueAsync joue un troisième rôle, indépendant du choix framework. Extension PHP expérimentale (ext-async), intégration au moteur, scheduler libuv, coroutines, I/O non bloquante sur des fonctions familières (curl, fichiers, PDO dans certains cas). L’API propose spawn(), await(), delay() - sans imposer une « couleur async » à chaque fonction du codebase.

Restons mesurés sur la maturité. TrueAsync est expérimental : PHP 8.6+, build custom, API en évolution, RFC en cours. Ce n’est ni un standard PHP officiel, ni un substitut drop-in à Swoole ou Amp. Ce n’est pas non plus « le futur » de PHP - c’est une direction à évaluer sur des cas précis.

TrueAsync peut s’appliquer derrière un monolithe Phalcon aussi bien que derrière Symfony. Le runtime natif ne présuppose pas une migration framework. Le CRUD synchrone d’un back-office n’en a généralement pas besoin. Les workers I/O-heavy, les agrégations HTTP parallèles, les pipelines de fichiers - peut-être, si les mesures le justifient et si l’exploitation d’un build custom est acceptable.

Chez Darkwood, nous traitons TrueAsync comme une stratégie d’exécution - interchangeable selon le contexte - et non comme une décision architecturale fondatrice. L’architecture, c’est le workflow et ses boundaries. Le driver async n’est que la façon dont on l’exécute.

Quand ne pas migrer hors de Phalcon

La migration n’est pas toujours souhaitable. Certaines équipes devraient rester sur Phalcon - et c’est un choix rationnel, pas un aveu d’échec.

Restez si :

  • L’équipe maĂ®trise Phalcon et la productivitĂ© est haute. Le coĂ»t de rĂ©apprentissage et de réécriture dĂ©passe le gain attendu
  • Les SLOs sont atteints - latence, dĂ©bit, stabilitĂ© - et les profils de charge n’ont pas fondamentalement changĂ©
  • Le produit Ă©volue peu - corrections, maintenance, pas de refonte majeure ni d’intĂ©grations Ă©cosystème nombreuses
  • Le coĂ»t migration > valeur business - rewrite, double run, risque de rĂ©gression, gel des features pendant des mois
  • La modernisation incrĂ©mentale suffit - montĂ©e vers Phalcon 5, adoption PIE, usage des nouveaux composants (Queue, Container, Contracts) sans changer de framework

Dans ces cas, migrer vers Symfony parce que « c’est ce que font les autres » est une erreur stratégique. Phalcon v5 en 2026 n’est pas la même chose que Phalcon 3 gelé sur PHP 7 : le projet bouge, l’écosystème est soutenu, et le levier framework natif reste valide tant que c’est lui le bottleneck.

Quatre trajectoires possibles

Pour une plateforme PHP à grande échelle, quatre issues sont crédibles - aucune n’est la « bonne » par défaut :

1. Rester sur Phalcon et moderniser. Montée v5, adoption des nouveaux composants, refactor interne des workflows, amélioration de l’observabilité. TrueAsync optionnel sur les workers I/O-heavy si le build custom est acceptable.

2. Migrer vers Symfony. Quand les contraintes écosystème, recrutement ou évolution produit dominent. Migration progressive (strangler), pas big bang. TrueAsync optionnel en phase ultérieure.

3. Architecture hybride. Phalcon conserve le trafic critique ou les APIs à très faible latence ; Symfony porte les nouveaux bounded contexts ou les surfaces en évolution. Coexistence assumée, avec des contrats API explicites entre les deux mondes.

4. Introduire le runtime natif indépendamment du framework. TrueAsync (ou une alternative async) sur les workers et pipelines I/O, sans toucher au framework web. Phalcon ou Symfony en couche HTTP ; le runtime async en couche exécution.

TrueAsync n’est automatiquement partie de la réponse dans aucune trajectoire sauf la quatrième - et même là, seulement après mesure. Ce n’est pas un prérequis de migration.

Boundaries avant frameworks

Voici la thèse que nous défendons chez Darkwood et que nous retrouvons sur presque toutes les missions d’architecture : beaucoup de « migrations framework » sont en réalité des problèmes de boundaries mal définies.

Un monolithe legacy ne souffre pas toujours parce que son routeur est lent. Il souffre parce que trois langages métier cohabitent sans frontières claires : l’ingestion (recevoir et valider un événement), la publication (persister et notifier), la lecture (servir une vue optimisée). Dans le code, ces trois préoccupations partagent les mêmes handlers, les mêmes services, les mêmes scripts cron. Changer de framework sans redessiner ces boundaries ne fait que déplacer la dette.

Quelques concepts nous aident à nommer ce problème - sans transformer l’article en catalogue DDD :

Bounded contexts. Chaque zone métier a son vocabulaire et ses invariants. Sur une plateforme éditoriale, « article publié » ne signifie pas la même chose côté ingestion (événement à traiter) et côté lecture (document pré-calculé à servir). Les confondre dans un seul module produit les handlers enchevêtrés que nous voyons partout.

Services de domaine vs services d’application. Les règles métier (valider un contenu, calculer un statut de publication) ne devraient pas vivre au même endroit que la coordination technique (invalider un cache, appeler un bus, reconstruire un read model). Quand tout est un « service », rien n’est testable isolément.

Couche orchestration vs couche domaine. L’orchestration enchaîne les étapes ; le domaine décide. « Persister puis invalider puis reconstruire » est de l’orchestration. « Ce contenu est-il publiable ? » est du domaine. Les mélanger dans un controller ou un handler message rend les deux impossibles à faire évoluer séparément.

CQRS et read models. Sur les plateformes read-heavy, le chemin d’écriture (stockage canonique) et le chemin de lecture (modèles dérivés, cache, documents agrégés) ont des contraintes opposées. Forcer une seule couche ORM à servir les deux crée des compromis coûteux. Séparer explicitement write path et read path n’est pas du dogme - c’est une réponse naturelle à un profil de charge mesuré.

Event-driven et cohérence éventuelle. Après une persistence, l’invalidation cache et la reconstruction du read model peuvent être asynchrones. C’est acceptable - à condition de nommer la frontière où la cohérence forte s’arrête et où la cohérence éventuelle commence.

flowchart TB
  subgraph sync_boundary [Boundary synchrone]
    Validate[Validation]
    Persist[Persistence canonique]
    Validate --> Persist
  end
  subgraph async_boundary [Boundary eventual]
    Invalidate[Invalidation]
    Rebuild[Rebuild read model]
    Invalidate --> Rebuild
  end
  Persist -->|"événement"| Invalidate
Enter fullscreen mode Exit fullscreen mode

La boundary synchrone - validation et persistence - doit rester ACID : soit le contenu est accepté et stocké, soit l’opération échoue proprement. La boundary éventuelle - invalidation et reconstruction - peut tolérer un délai, à condition que le système de lecture le assume (stale read acceptable, ou mécanisme de versioning).

Quand nous auditons une plateforme en vue de migration, la première question n’est pas « Phalcon ou Symfony ? ». C’est : où sont les boundaries aujourd’hui, et où devraient-elles être ? Le framework n’est qu’un contenant. Un mauvais contenant propre reste préférable à un bon contenant qui recèle la même confusion métier.

Archetype PEFT - pattern plateforme éditoriale read-heavy

Pour ancrer le raisonnement sans coller à une architecture réelle, imaginons la PEFT (Plateforme Éditoriale à Fort Trafic) - un archetype, pas un cas client.

PEFT combine des traits communs aux grandes plateformes de contenu :

  • Chemin de lecture dominant - les APIs et le web public servent des vues prĂ©-calculĂ©es, pas la base relationnelle Ă  chaud
  • Ingestion Ă©vĂ©nementielle - un système amont Ă©met des Ă©vĂ©nements de publication ; des workers consomment, valident, persistent
  • Modèles dĂ©rivĂ©s - après persistence, invalidation et reconstruction de structures de lecture optimisĂ©es (cache, documents agrĂ©gĂ©s)
  • Monolithe multi-surfaces - web, APIs, traitements batch partagent une base de code, souvent organisĂ©e en modules
  • Extraction progressive - certains bounded contexts commencent Ă  se dĂ©tacher, mais le cĹ“ur porte encore une part significative du trafic
flowchart LR
  CMS[Source de contenu] --> Bus[Bus de messages]
  Bus --> Workers[Workers ingestion]
  Workers --> Store[(Stockage canonique)]
  Workers --> Cache[(Modèles dérivés)]
  Cache --> APIs[APIs read-heavy]
  APIs --> Clients[Clients]
Enter fullscreen mode Exit fullscreen mode

Lue avec la lentille des boundaries, PEFT révèle trois bounded contexts implicites :

  1. Ingestion - recevoir, valider, normaliser les événements entrants
  2. Publication - persister l’état canonique, émettre les signaux de changement
  3. Lecture - servir des read models optimisés, indépendamment du schéma d’écriture

Le pattern CQRS y est naturel : le stockage canonique (write model) et les modèles dérivés (read models) ont des cycles de vie différents. L’invalidation et la reconstruction introduisent une cohérence éventuelle assumée entre écriture et lecture - acceptable tant que les SLOs de fraîcheur sont explicites.

Sur cet archetype, les décisions architecturales ne se jouent pas sur le choix du routeur. Elles se jouent sur :

  • oĂą vit la logique d’ingestion et d’invalidation (handlers enchevĂŞtrĂ©s vs workflows explicites)
  • oĂą passe la frontière entre domaine et orchestration
  • si le bottleneck est le MVC synchrone ou l’I/O des workers
  • si le produit exige une rĂ©organisation profonde ou une stabilisation longue

PEFT peut rester sur Phalcon si les SLOs tiennent et l’équipe est stable. PEFT peut migrer vers Symfony si l’écosystème et le churn produit le commandent. PEFT peut hybrider. TrueAsync n’entre en scène que si les workers I/O deviennent le goulot mesuré - quel que soit le framework. Mais dans tous les cas, redessiner les boundaries précède le choix technologique.

Retours d’expérience - trois archétypes de POC

Le POC évoqué en introduction - plateforme PHP à fort trafic, héritage performant, workflows qui débordent du MVC - illustre un premier archétype. Ce n’est pas le seul. Dans nos missions de conseil et validations techniques, nous retrouvons régulièrement trois profils où le bon premier mouvement dépend rarement du framework. Il dépend du type de dette dominante.

Archétype 1 - Monolithe legacy, règles métier denses. C’est le cas du POC d’ouverture : plateforme mature, logique métier enchevêtrée, peu de marge pour un rewrite. L’exercice utile n’a pas été de comparer Phalcon et Symfony sur le papier, mais de cartographier les bounded contexts implicites, d’identifier les frontières de cohérence synchrone, et de proposer une extraction progressive (strangler) contexte par contexte. Le framework - Phalcon ou Symfony - est devenu secondaire : l’enjeu était de sortir les règles métier du magma des handlers.

Archétype 2 - Greenfield Symfony. Nouveau produit, équipe en croissance, intégrations nombreuses prévues. Ici, la priorité était la modélisation du domaine et des modules clairs - pas l’async, pas le runtime natif. Nous avons structuré les boundaries tôt (domain services, application services, couches de lecture distinctes) pour éviter de recréer, en Symfony, le monolithe spaghetti qu’on fuyait ailleurs. Messenger a suffi pour les premiers workflows ; l’orchestration explicite n’est venue qu’avec la complexité.

Archétype 3 - Automatisation et workflows I/O-heavy. Chaînes de traitement (enrichissement de contenu, appels API multiples, pipelines de fichiers - profils proches de certaines chaînes IA). Le goulot n’était ni le routeur ni le modèle de domaine : c’était l’attente I/O et l’absence de modèle d’exécution explicite. Nous avons d’abord rendu le workflow lisible - étapes nommées, erreurs isolées, boundaries sync/async identifiées - puis seulement évalué TrueAsync, Amp ou Fiber selon l’environnement. Introduire le runtime async avant d’avoir un workflow explicite aurait masqué le vrai problème.

Ces trois archétypes ne produisent pas le même playbook. Ils partagent une discipline : diagnostiquer la contrainte dominante avant de choisir la technologie. C’est ce que nous appliquons chez Darkwood - sur Phalcon comme sur Symfony, avec ou sans TrueAsync.

L’orchestration comme préoccupation de premier ordre

Les frameworks web - Phalcon, Symfony, Laravel - sont optimisés pour un cycle : requête HTTP → traitement → réponse. C’est leur force. C’est aussi leur angle mort.

Les systèmes matures dépassent vite ce cycle. Un événement de publication déclenche une chaîne : validation, persistence, invalidation cache, reconstruction de read model, notification, accusé vers un bus. Cette chaîne ne tient pas dans un controller. Elle vit éclatée entre handlers message, services, scripts CLI et tâches cron - parfois sur des années de développement, parfois sous des pressions de mise en production qui ont privilégié la vitesse sur la structure.

Nous appelons cela un problème d’orchestration : la coordination explicite d’étapes métier qui dépassent le cycle requête-réponse. Et nous pensons que ce problème mérite le même niveau de réflexion architecturale que le choix du framework.

Une architecture workflow-centric ne remplace pas le MVC. Elle le complète. Le controller reste fin : il reçoit, délègue, répond. Le workflow porte la logique multi-étapes : il nomme les étapes, gère les erreurs par étape, définit les boundaries de cohérence, et - idéalement - sépare la description du pipeline du modèle d’exécution qui le fait tourner.

C’est là que nous divergeons des approches « tout dans Messenger » ou « tout dans un service god-object ». Messenger transporte des messages ; il n’orchestrre pas nativement un pipeline multi-étapes avec stratégies d’exécution interchangeables. Un service de 800 lignes qui fait tout orchestre, mais de façon opaque et non testable.

Chez Darkwood, nous plaçons l’orchestration au même niveau que les boundaries et le choix framework. Pas parce qu’un seul outil résout tout - mais parce que ne pas nommer l’orchestration est la cause la plus fréquente des migrations ratées. On change de framework ; les handlers restent enchevêtrés ; la dette suit.

Flow - séparer le workflow du modèle d’exécution

C’est cette observation qui nous a conduits à développer Flow - un composant open source, pas un framework de remplacement.

Flow existe parce que nous avons vu, sur des migrations réelles, le même pattern se répéter : la logique workflow n’a pas de maison naturelle dans Symfony ou Phalcon. Elle finit dans des handlers anonymes, des scripts cron ou des services fourre-tout. Flow lui donne une place architecturale : un pipeline explicite composé de Flow (l’étape), Job (le travail unitaire) et Ip (le jeton de données qui traverse le pipeline).

Mais le différenciateur architectural le plus important, pour nous, est DriverInterface.

Même workflow. Modèle d’exécution interchangeable.

Un pipeline de publication - valider, persister, invalider, reconstruire - peut s’exécuter en synchrone pur, en fibers PHP, en coroutines Amp, sur une event loop ReactPHP, dans un worker Swoole, ou via TrueAsync. Le métier ne change pas. Seul le modèle d’exécution change. C’est une décision d’infrastructure, pas de domaine.

Flow propose aujourd’hui plusieurs drivers : FiberDriver (défaut, pas d’extension), AmpDriver, ReactDriver, SwooleDriver, SpatieDriver, ParallelDriver, et TrueAsyncDriver (expérimental, ext-async). Chacun répond à un contexte d’exploitation différent - intégration existante, worker dédié, I/O natif expérimental.

use Flow\Driver\FiberDriver;
use Flow\Driver\TrueAsyncDriver;
use Flow\Flow\Flow;
use Flow\Ip;

// Le workflow est stable ; seul le driver change selon l'environnement
$driver = TrueAsyncDriver::isSupported()
    ? new TrueAsyncDriver()
    : new FiberDriver();

$flow = (new Flow(job: new ValidateMessage(), driver: $driver))
    ->fn(new Persist())            // domaine : boundary synchrone
    ->fn(new InvalidateCache())    // orchestration
    ->fn(new RebuildReadModel());  // I/O potentiellement async

$flow(new Ip($message));
$flow->await();
Enter fullscreen mode Exit fullscreen mode

Symfony reçoit l’événement - via Messenger ou un endpoint interne - et délègue à Flow. Chaque fn() est une étape nommée ; les erreurs peuvent être isolées par errorJob. On ne commence pas par TrueAsync : FiberDriver par défaut, mesure, puis opt-in sur le pipeline qui le justifie.

Points d’attention que nous appliquons en production : ne pas mélanger FiberDriver et TrueAsyncDriver dans le même processus (TrueAsync bloque les fibers userland tant qu’il est actif). Ne pas introduire threads, channels ou TaskGroup dans un premier temps - complexité d’exploitation sans besoin démontré.

Flow n’est pas la seule réponse possible. Symfony Messenger, un pipeline maison, ou une orchestration par événements peuvent suffire sur des workflows simples. Flow explore une direction plus ambitieuse : faire de l’orchestration une primitive architecturale de premier ordre, avec séparation explicite entre le quoi (le pipeline) et le comment (le driver).

Nous ne disons pas que Flow est le futur. Nous disons que l’orchestration explicite - avec la possibilité de changer de stratégie d’exécution sans réécrire le métier - est une direction que les systèmes complexes nous ont appris à prendre au sérieux.

Si vous choisissez de migrer - principes, pas playbook

Quand la trajectoire « migrer vers Symfony » est choisie - et seulement alors - quelques principes valent mieux qu’un plan en cinq phases rigide.

Mesurer avant de bouger. Identifier les vrais bottlenecks : MVC, workflows, I/O workers, couplage Volt/PHQL, boundaries floues. Ne pas migrer un framework performant pour résoudre un problème d’orchestration ou de modélisation.

Redessiner les boundaries d’abord. Avant de réécrire un module, nommer ses bounded contexts, ses read models, ses frontières de cohérence. Le strangler fonctionne par contexte, pas par couche technique.

Strangler, pas big bang. Nouvelle application Symfony en parallèle ; routage progressif endpoint par endpoint. Préserver les contrats API pendant la coexistence.

Sync d’abord, async ensuite. Extraire les workflows en synchrone. Rendre la chaîne lisible et testable avant d’introduire TrueAsync ou tout autre runtime async.

Opt-in sur l’async. TrueAsync sur les pipelines I/O-heavy uniquement, en environnement dédié, avec repli possible.

Les patterns legacy se déplacent différemment selon leur nature : les handlers en chaîne vers un workflow explicite ; les modules applicatifs vers des bundles ou dossiers Feature/ ; l’invalidation cache ad hoc vers un pipeline nommé ; l’I/O bloquante dans les workers vers un runtime async opt-in ; l’ORM/PHQL vers Doctrine - souvent la migration la plus coûteuse, à traiter à part.

Un endpoint CRUD se porte en controller Symfony simple. Une chaîne ingestion → cache → modèle dérivé mérite une réflexion workflow - et probablement des boundaries redessinées avant tout choix d’outil.

Ce qu’il ne faut pas faire

Certaines erreurs reviennent - y compris quand la migration n’est pas nécessaire.

Migrer par défaut parce que Phalcon « fait vieux » - alors que v5 est maintenue, les SLOs tiennent et l’équipe est productive.

Ignorer Phalcon v5 moderne - Queue, Container, Contracts, PIE - et traiter le framework comme figé sur une version ancienne.

Changer de framework sans redessiner les boundaries - le monolithe spaghetti survit au changement de namespace.

Déployer TrueAsync partout - ignore sa maturité expérimentale et alourdit l’exploitation.

Confondre concurrence I/O et parallélisme CPU - mène à de mauvais choix, quel que soit le framework.

Transformer une lib d’orchestration en framework - pipeline systématique sur chaque feature remplace un anti-pattern par un autre.

Présenter TrueAsync comme prêt pour la production universelle - en 2026, c’est une piste à cadrer et à tester.

Introduire l’async avant d’avoir un workflow explicite - masque la dette d’orchestration derrière une couche d’exécution.

Risques réalistes

Risque Mitigation
Migration sous-estimée Strangler ; périmètre par endpoint ; boundaries d’abord
Boundaries mal définies Cartographie contexts avant rewrite ; CQRS explicite si read-heavy
API TrueAsync instable Driver optionnel ; repli Fiber ; version épinglée
Build PHP custom Limiter aux workers / environnements dédiés
Doctrine + concurrence EM par unité de travail ; transactions courtes
Coexistence Phalcon / Symfony Contrats API explicites ; routage clair ; observabilité
Sur-ingénierie orchestration Pipelines sur workflows réels uniquement

Conclusion - au-delà du framework, l’orchestration explicite

Phalcon n’a jamais été une erreur. Pour des plateformes read-heavy, optimiser le framework en C était une réponse cohérente - et reste défendable quand la contrainte dominante est encore le chemin MVC.

Symfony n’est pas automatiquement meilleur. C’est une optimisation différente : structure, écosystème, maintenabilité organisationnelle. Pertinent quand ces contraintes dominent le coût du framework natif.

TrueAsync n’est pas automatiquement le futur. C’est une optimisation runtime expérimentale - pertinente sur certains profils I/O, indépendante du choix framework, pas un prérequis de modernisation.

Phalcon, Symfony et TrueAsync placent le natif - ou la structure, ou l’async - à des couches différentes de la pile. Mais la compétence architecturale, en 2026, ne s’arrête pas là.

L’industrie a passé des années à optimiser les frameworks. La frontière suivante, pour les systèmes complexes, n’est pas seulement la performance framework : c’est l’orchestration explicite, les boundaries de domaine clairement tracées, et les stratégies d’exécution interchangeables selon le contexte. Le runtime natif est un levier d’implémentation. L’orchestration est une décision d’architecture.

Chez Darkwood, nous construisons Flow parce que les systèmes matures nous ont appris une chose : quand les workflows dépassent le cycle requête-réponse, il faut leur donner une place architecturale - et séparer la logique d’exécution du métier qu’elle coordonne. Flow explore une direction où l’orchestration devient une primitive de premier ordre, pas un assemblage de handlers et de cron.

Le choix framework reste important. Il n’est qu’une partie de l’équation. La vraie question n’est pas seulement « où vit le code natif ? » - c’est aussi où vivent les workflows, où passent les boundaries, et qui porte la responsabilité de l’orchestration dans une architecture qui doit tenir dix ans.

Sources

Cet article s’appuie sur plusieurs sources publiques, expérimentations personnelles et travaux open source autour de l’architecture PHP moderne, de l’exécution asynchrone et de l’orchestration de workflows.

Frameworks et runtime

Références Darkwood

Une partie des réflexions présentées ici provient de travaux menés chez Darkwood autour de la séparation entre workflow métier et modèle d’exécution.

  • Flow - Darkwood orchestration library : ImplĂ©mentation open source de pipelines mĂ©tier avec drivers d’exĂ©cution interchangeables (Fiber, Amp, ReactPHP, Swoole, Parallel, TrueAsync). Le support expĂ©rimental de TrueAsyncDriver y est disponible comme preuve de concept pour explorer l’orchestration dĂ©couplĂ©e du runtime.
  • Slidewire presentation source : Les slides de la prĂ©sentation associĂ©e Ă  cet article, construites avec Slidewire, sont disponibles publiquement dans ce dĂ©pĂ´t.

Top comments (0)