Maîtriser le Kernel de Symfony : Architecture Interne et Optimisation Performance
Plongez dans les mécanismes profonds du Kernel Symfony pour comprendre le cycle de vie complet des requêtes et optimiser vos applications. Découvrez les techniques avancées de debugging, de performance et les pièges courants que seuls les experts connaissent.
Le Cycle de Vie Complet du Kernel Symfony
Définition
Le Kernel Symfony est le cœur de l'application qui orchestre l'ensemble du cycle de vie d'une requête HTTP, de sa réception à l'envoi de la réponse. C'est une machine d'état sophistiquée qui gère les événements du système de manière décentralisée.
Explication Détaillée
Le Kernel Symfony fonctionne selon un modèle événementiel strict. Quand une requête arrive, le Kernel crée d'abord une instance de Request, puis déclenche une série d'événements dans un ordre prédéfini : kernel.request, kernel.controller, kernel.view, kernel.response, et kernel.terminate. Chaque événement peut être écouté par plusieurs listeners qui peuvent modifier la requête, le contrôleur ou la réponse. Le Kernel maintient également un état interne avec des informations sur l'environnement (dev, prod, test), le debug mode et les bundles chargés.
La phase de bootstrap est critique : le Kernel charge le container DI, initialise les bundles, configure les services et construit le cache. En mode prod, ce bootstrap est caché et réutilisé. En mode dev, il y a des vérifications supplémentaires pour la cohérence du cache.
<?php
// kernel/Kernel.php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\RequestEvent;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
public function registerBundles(): iterable
{
return [
new FrameworkBundle(),
// Autres bundles...
];
}
protected function configureContainer(ContainerBuilder $container): void
{
$container->import('../config/{packages}/*.yaml');
$container->import('../config/services.yaml');
}
public function boot(): void
{
if ($this->booted) {
return;
}
// Initialisation personnalisée avant le bootstrap
parent::boot();
// Initialisation personnalisée après le bootstrap
$this->container->get('logger')->info('Kernel booted');
}
}
// Exemple d'event listener avancé
class KernelRequestListener
{
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return; // Ignorer les sous-requêtes
}
// Logique métier complexe
$event->setResponse(new Response('Cached'));
}
}
| Événement Kernel | Ordre | Modifiable | Cas d'usage |
|---|---|---|---|
| kernel.request | 1er | Requête | Auth, logging, redirection |
| kernel.controller | 2e | Contrôleur | Décoration, aspect ratio |
| kernel.controller_arguments | 3e | Arguments | Injection personnalisée |
| kernel.view | 4e | Réponse | Transformation données |
| kernel.response | 5e | Réponse | Headers, compression |
| kernel.terminate | 6e | Aucune | Tâches asynchrones |
Astuce Expert
En mode prod, désactivez le debug du Kernel et utilisez une classe Kernel précompilée pour accélérer le bootstrap de 30-40%. Vérifiez aussi que le var/cache est sur un stockage rapide (SSD) car le Kernel lit le container sérialisé à chaque requête en dev.
⚠️ Attention Critique
Ne modifiez jamais directement le container après que le Kernel soit démarré en prod. Tous les listeners kernel.request doivent être ultra-rapides car ils s'exécutent à CHAQUE requête. Évitez les appels base de données côté kernel.request; utilisez plutôt un évènement EventSubscriber lazy-loaded.
Optimisation du Container DI et Lazy-Loading
Définition
Le Container d'Injection de Dépendances (DI) est un registre de services gérés par Symfony qui instancie les objets à la demande. Le lazy-loading permet de différer l'instanciation des services jusqu'à leur première utilisation réelle.
Explication Détaillée
En mode prod, le container est compilé en fichier PHP statique dans var/cache/prod/. Ce processus de compilation supprime les services inutilisés (dead code elimination) et crée une hiérarchie d'instanciation optimisée. Cependant, tous les services ne sont pas compilés de la même manière.
Les proxies lazy sont des objets virtuels qui retardent l'instanciation du vrai service. Quand vous accédez à une propriété ou méthode du proxy, il instancie le service réel en coulisse. C'est transparent pour le développeur mais génère des gains énormes de performance, surtout pour les services lourds (connexions DB, clients HTTP) rarement utilisés.
La autoconfiguration est une fonctionnalité puissante qui ajoute automatiquement les tags appropriés basés sur les interfaces/traits. Par exemple, un EventSubscriber implémentant EventSubscriberInterface est automatiquement tagué et enregistré auprès du dispatcher sans configuration manuelle.
<?php
// config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
bind:
$projectDir: '%kernel.project_dir%'
App\:
resource: '../src'
exclude: '../src/{DependencyInjection,Entity,Migrations}'
# Service avec lazy-loading explicite
App\Service\HeavyExpensiveService:
lazy: true
arguments:
- '@database_connection'
- '@redis'
# Service factory avec lazy-loading
App\Service\ReportGenerator:
factory: ['App\Factory\ReportGeneratorFactory', 'create']
lazy: true
tags:
- name: 'app.report_handler'
priority: 100
# Configurator avancé
App\Service\ConfigurableService:
class: App\Service\ConfigurableService
configurator: ['@App\Configurator\ServiceConfigurator', 'configure']
arguments:
- '%env(json:DATABASE_CONFIG)%'
// Exemple de classe avec injection intelligente
namespace App\Service;
class OptimizedService
{
private $heavyService; // Lazy-loaded si possible
public function __construct(
private HeavyExpensiveService $expensive, // Lazy
private string $projectDir,
private LoggerInterface $logger
) {}
public function process(): void
{
// Heavy service instancié seulement ici
$this->expensive->run();
}
}
// Compilation personnalisée du Container
namespace App\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class OptimizationCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
// Récupère tous les services taggés
$handlers = $container->findTaggedServiceIds('app.handler');
foreach ($handlers as $id => $tags) {
$definition = $container->getDefinition($id);
// Marque comme lazy si pas d'accès fréquent
if ($this->shouldBeLazy($id)) {
$definition->setLazy(true);
}
// Optimise les arguments
$this->optimizeArguments($definition);
}
}
private function shouldBeLazy(string $serviceId): bool
{
// Logique basée sur les patterns
return strpos($serviceId, 'External') !== false ||
strpos($serviceId, 'Cache') === false;
}
private function optimizeArguments($definition): void
{
// Remplace les références non utilisées
}
}
| Feature DI | Gain Performance | Risque | Recommandation |
|---|---|---|---|
| Lazy-loading | +20-40% startup | Overhead runtime petit | Tous les services externes |
| Autoconfigure | -50% config | Magique difficile à debug | Activé par défaut |
| Binding global | +10% clarity | Conflicts possibles | Utilisez avec modération |
| Compiler passes | +30% optimisation | Complexité accrue | Mode prod uniquement |
| Private services | +15% memory | Non-injectable | Par défaut, changez si besoin |
Astuce Expert
Utilisez bin/console debug:container avec l'option --format=json pour analyser le graph de dépendances en profondeur. Combinez avec bin/console container:lint pour vérifier les services non utilisés. Pour déboguer les lazy proxies, inspectez les fichiers générés dans var/cache/prod/Symfony/Component/DependencyInjection/Dumper/.
⚠️ Attention Critique
Les services public: false (par défaut) ne peuvent pas être accessibles via $container->get(). Cela force l'injection explicite, ce qui est bon pour la maintenabilité mais peut créer des services inaccessibles. Les circular dependencies dans les services lazy peuvent causer des erreurs subtiles en runtime, non détectées en compilation. Testez en prod avant de déployer.
Gestion Avancée des Événements et Listeners
Définition
Le système d'événements de Symfony est un dispatcher centralisé (Event Dispatcher) qui gère la publication et la souscription aux événements. Les listeners et subscribers écoutent les événements spécifiques et exécutent du code réactif.
Explication Détaillée
Il existe deux patterns pour écouter les événements : les listeners et les subscribers. Les listeners sont simples mais demandent une configuration manuelle pour chaque événement. Les subscribers implémentent EventSubscriberInterface et retournent un tableau des événements qu'ils écoutent, avec la priorité optionnelle.
La priorité des listeners est cruciale. Un listener avec une priorité 100 s'exécute avant un listener avec priorité 50. Les priorités négatives s'exécutent après les positives. Symfony utilise lui-même des priorités spécifiques : les listeners de sécurité à 8, les listeners de routage à 32. Comprendre l'ordre d'exécution est essentiel pour déboguer les problèmes complexes.
Les événements immutables (events) permettent ou non les modifications selon leur type. Par exemple, kernel.request peut être modifié, mais kernel.exception ne peut pas l'être après qu'une réponse soit définie. Les événements stoppables ont une méthode stopPropagation() qui empêche les listeners suivants de s'exécuter.
<?php
// src/EventListener/SecurityAuditListener.php
namespace App\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Psr\Log\LoggerInterface;
class SecurityAuditListener implements EventSubscriberInterface
{
public function __construct(
private LoggerInterface $logger,
private AuditRepository $auditRepo,
private ?string $enabledForUsers = null // Lazy configuration
) {}
public static function getSubscribedEvents(): array
{
return [
// Priorité élevée : s'exécute AVANT les autres
KernelEvents::REQUEST => ['onKernelRequest', 256],
KernelEvents::RESPONSE => ['onKernelResponse', -100],
KernelEvents::EXCEPTION => ['onKernelException', -50],
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return; // Ignore les sous-requêtes
}
// Audit de sécurité de la requête
$request = $event->getRequest();
$user = $request->attributes->get('_user');
// Filtre : audit uniquement certains utilisateurs
if ($this->enabledForUsers && !in_array($user?->getId(),
json_decode($this->enabledForUsers, true))) {
return;
}
$audit = new AuditLog(
userId: $user?->getId(),
method: $request->getMethod(),
path: $request->getPathInfo(),
ip: $request->getClientIp(),
timestamp: new \DateTimeImmutable(),
userAgent: $request->headers->get('User-Agent')
);
// Persistance asynchrone via une queue
$this->auditRepo->queueAudit($audit);
}
public function onKernelResponse(ResponseEvent $event): void
{
// Inject des headers de sécurité
$response = $event->getResponse();
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
}
public function onKernelException(\Symfony\Component\HttpKernel\Event\ExceptionEvent $event): void
{
// Log l'exception avant le handler de Symfony
$this->logger->critical('Unhandled exception', [
'exception' => $event->getThrowable(),
'uri' => $event->getRequest()->getUri()
]);
}
}
// Configuration du listener
// config/services.yaml
services:
App\EventListener\SecurityAuditListener:
tags:
- { name: kernel.event_subscriber }
bind:
$enabledForUsers: '%env(AUDIT_USERS)%'
// Dispatching custom d'événements
namespace App\Service;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use App\Event\OrderPlacedEvent;
class OrderService
{
public function __construct(
private EventDispatcherInterface $dispatcher
) {}
public function placeOrder(Order $order): void
{
// Logique métier
$order->markAsPlaced();
// Dispatch l'événement custom
$event = new OrderPlacedEvent($order);
$this->dispatcher->dispatch($event, OrderPlacedEvent::NAME);
}
}
// Event custom stoppable
namespace App\Event;
use Symfony\Contracts\EventDispatcher\Event;
class OrderPlacedEvent extends Event
{
public const NAME = 'order.placed';
private bool $processingAllowed = true;
public function __construct(
private Order $order
) {}
public function getOrder(): Order
{
return $this->order;
}
public function stopProcessing(): void
{
$this->processingAllowed = false;
$this->stopPropagation();
}
public function isProcessingAllowed(): bool
{
return $this->processingAllowed;
}
}
| Type Listener | Priorité Défaut | Performance | Cas d'Usage |
|---|---|---|---|
| EventSubscriber | Définie | Meilleure | Logique complexe, plusieurs événements |
| Listener kernel | 0 | Variable | Modification requête/réponse |
| Listener doctrine | -10 | Bonne | Hooks ORM |
| Custom events | Variable | Excellente | Domain events, processus métier |
| Stoppable events | N/A | Bonne | Short-circuiting, early exit |
Astuce Expert
Pour déboguer l'ordre des listeners, utilisez bin/console debug:event-dispatcher kernel.request pour voir tous les listeners dans l'ordre exact. Si vous avez des race conditions, cela indique que les priorités ne sont pas définies correctement. Utilisez des priorities espacées (100, 90, 80...) pour éviter les collisions. Pour les événements custom, envisagez de les dispatcher de manière asynchrone via Messenger si le traitement est lourd.
⚠️ Attention Critique
Les listeners qui lèvent des exceptions interrompent la chaîne et peuvent causer des erreurs 500 inattendues. Wrappez toujours vos listeners dans des try-catch. Les listeners kernel.terminate s'exécutent APRÈS que la réponse soit envoyée au client, donc toute modification est ignorée. Les événements dispatcher ne sont pas persistants : si personne n'écoute, rien ne se passe silencieusement (difficile à déboguer). Testez toujours que vos listeners personnalisés sont bien enregistrés via bin/console.
Debugging et Profiling en Profondeur
Définition
Le Symfony Profiler est un outil de diagnostic intégré qui capture des informations détaillées sur chaque requête : temps d'exécution, requêtes SQL, appels de service, événements dispatchés. Le debugging avancé consiste à exploiter ces données pour optimiser les goulots d'étranglement.
Explication Détaillée
Le Profiler fonctionne via un kernel listener spécial qui enregistre les données pendant l'exécution. Toutes les requêtes sont profilees en mode dev et cachées dans var/cache/profile/. Le token unique permet de récupérer les données ultérieurement. En production, le Profiler peut être activé sélectivement via des conditions (IP spécifique, utilisateur admin) sans impacter les performances.
Les panels du profiler fournissent des insights différents : le panel "Time" montre la timeline précise de chaque étape, "Doctrine" liste les requêtes SQL avec les slowqueries, "Events" affiche tous les événements dispatchés, "Services" montre les services instanciés. Le panel personnalisé permet de tracker des métriques custom.
Le VarDumper de Symfony avec dump() et dd() est bien plus puissant que var_dump(). Il fournit une UI interactive, un formatage colorisé, et envoie les données au serveur VarDumper pour une inspection distante. Combined avec xdebug, c'est une combinaison killer.
<?php
// config/services.yaml - Enable profiler selectively
when@dev:
framework:
profiler:
enabled: true
collect: true
when@prod:
framework:
profiler:
enabled: true # Activé mais...
collect: false # ...ne collect que si condition
// Custom profiler condition
namespace App\Profiler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ConditionalProfiler
{
private array $allowedIps = ['127.0.0.1', '::1'];
public function shouldProfile(Request $request, Response $response): bool
{
// Profile seulement les admins
return in_array($request->getClientIp(), $this->allowedIps) ||
$request->attributes->get('_profiler_enabled');
}
}
// Custom profiler panel
namespace App\Profiler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Profiler\ProfilerAwareTraitInterface;
use Symfony\Component\HttpKernel\Profiler\Profiler;
class CachePerformancePanel extends \Symfony\Component\HttpKernel\Profiler\AbstractProfilerPanel
{
private array $cacheMetrics = [];
public function __construct(?Profiler $profiler = null)
{
parent::__construct($profiler);
$this->cacheMetrics = [
'hits' => 0,
'misses' => 0,
'sets' => 0,
'time' => 0.0
];
}
public function collect(Request $request, Response $response, \Throwable $exception = null): void
{
$this->data = [
'metrics' => $this->cacheMetrics,
'hit_ratio' => $this->calculateHitRatio(),
'slowest_operations' => $this->slowestOps(),
];
}
public function recordCacheHit(string $key, float $time): void
{
$this->cacheMetrics['hits']++;
$this->cacheMetrics['time'] += $time;
}
public function recordCacheMiss(string $key, float $time): void
{
$this->cacheMetrics['misses']++;
$this->cacheMetrics['time'] += $time;
}
private function calculateHitRatio(): float
{
$total = $this->cacheMetrics['hits'] + $this->cacheMetrics['misses'];
return $total === 0 ? 0 : ($this->cacheMetrics['hits'] / $total) * 100;
}
private function slowestOps(): array
{
// Implémentation simplifiée
return [];
}
public function getName(): string
{
return 'cache_performance';
}
public function getTemplate(): string
{
return '@WebProfiler/Collector/cache_performance.html.twig';
}
}
// Utilisation du VarDumper avancé
namespace App\Controller;
use Symfony\Component\VarDumper\VarDumper;
class DebugController
{
public function debug(): void
{
$complexData = [
'users' => $this->fetchUsers(),
'stats' => $this->computeStats(),
'config' => $_ENV
];
// Dump avec options avancées
dump($complexData);
// Dump et die (fréquent en debug)
// dd($complexData);
// Custom handler pour redirection distante
VarDumper::setHandler(function ($var) {
$dumped = VarDumper::dump($var);
error_log($dumped); // Log au lieu d'afficher
});
}
}
// Web link et symfony toolbar personnalisée
namespace App\Middleware;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\WebLink\Link;
use Symfony\Component\WebLink\LinkCollector;
class WebLinkMiddleware
{
public function __invoke(Request $request): void
{
if (!$this->shouldProfile($request)) {
return;
}
$links = [
(new Link('preload', '/js/critical.js'))->withAttribute('as', 'script'),
(new Link('dns-prefetch', 'https://cdn.example.com')),
];
// Expose via Link header pour HTTP/2 push
$collector = $request->attributes->get('_link_collector');
foreach ($links as $link) {
$collector->addLink($request, $link);
}
}
}
| Tool Debug | Cas d'usage | Overhead Performance | Environnement |
|---|---|---|---|
| Web Profiler | Analyse requête complète | +50ms | Dev |
| VarDumper | Inspection rapide | Minimal | All |
| xdebug Profiler | Hotspot detection | +100-200ms | Dev |
| Blackfire.io | APM production | +2-5% | Prod (optionnel) |
| Custom panels | Métriques métier | Variable | All |
Astuce Expert
Combinez bin/console debug:router avec le Profiler pour matcher les routes exactes. Pour les slow queries, activez le slow query log dans config/services.yaml avec doctrine.dbal.logger_chain et une threshold en millisecondes. Pour analyser les memory leaks, utilisez le Memory panel du Profiler avec des techniques de diff : profilez la même page avec/sans cache et comparez l'utilisation mémoire.
⚠️ Attention Critique
Le Profiler ne doit JAMAIS être activé en production sans filtres stricts. Les fichiers de profiling accumulés dans var/cache/profile/ peuvent exploser et remplir le disque. Gardez seulement les 1000 derniers profils via une task CRON. Les données du Profiler peuvent contenir des informations sensibles (connexions DB, tokens) : soyez vigilant avant de partager les outputs. Les custom panels mal implémentés peuvent ralentir toutes les requêtes même quand ils ne sont pas affichés.
Techniques Avancées d'Optimisation et Edge Cases
Définition
Les edge cases en Symfony sont des situations extrêmes qui exposent les limites du framework : très grand nombre de services, dépendances circulaires masquées, handlers d'exception imbriqués, ou requêtes concurrent au bootstrap. Les techniques d'optimisation consistent à les identifier et les mitiger.
Explication Détaillée
La compilation du container en prod génère un fichier unique (Container.php) qui peut devenir très volumineux si vous avez des centaines de services. Symfony fournit des techniques pour scinder ce fichier : les InterfaceLoaderInterface et MultiServiceLocator permettent de charger les services par groupes. Le preloading PHP 7.4+ charge certaines classes en mémoire au démarrage du serveur pour éviter les disk I/O répétés.
Les dépendances circulaires sont le fléau des larges applications. Si le Service A dépend de B qui dépend de C qui dépend de A, Symfony les détecte à la compilation et lève une exception claire. Mais si ces services sont lazy ou utilisent des service locators, le problème peut rester caché jusqu'au runtime. Pour déboguer, utilisez bin/console debug:container --format=json | grep -A5 circular.
Le garbage collection en PHP est important en production avec les long-running processes (Symfony workers). Les listeners qui accumulent des références sans les nettoyer peuvent causer des memory leaks. Utilisez gc_collect_cycles() periodiquement et le flag --memory-limit des workers.
<?php
// config/packages/framework.yaml - Advanced optimization
framework:
cache:
pools:
cache.app:
adapter: cache.adapter.apcu # En-mémoire, ultra-rapide
provider: 'apcu://'
cache.system:
adapter: cache.adapter.redis
provider: 'redis://localhost'
options:
lazy: true # Lazy connection
http_cache:
enabled: true
debug: '%kernel.debug%'
default_ttl: 3600
session:
handler_id: session.handler.redis
cookie_secure: true
cookie_samesite: 'lax'
# Deprecated services warning
# validation: { enable_annotations: false } # ⚠️ Remove in 7.0
// Exemple de Memory-efficient Service Locator
namespace App\Service;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ServiceLocator;
class HandlerRegistry
{
private ContainerInterface $handlers;
public function __construct(
#[TaggedIterator('app.handler', defaultIndexMethod: 'getAlias')]
iterable $handlers
) {
// Convertit en service locator lazy
$this->handlers = new ServiceLocator(
array_flip(iterator_to_array($handlers))
);
}
public function getHandler(string $type): HandlerInterface
{
if (!$this->handlers->has($type)) {
throw new \InvalidArgumentException("Unknown handler: $type");
}
return $this->handlers->get($type);
}
}
// Preloading optimization
// public/index.php
<?php
// Preload classes critiques avant l'autoloading Composer
if (\function_exists('opcache_compile_file')) {
@opcache_compile_file(__DIR__ . '/../src/Kernel.php');
// Preload d'autres classes fréquentes
$criticalClasses = [
__DIR__ . '/../src/Controller/HomeController.php',
__DIR__ . '/../vendor/symfony/http-foundation/Request.php',
];
foreach ($criticalClasses as $file) {
if (file_exists($file)) {
@opcache_compile_file($file);
}
}
}
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
// Memory-safe worker pour long-running processes
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'app:worker', description: 'Long-running worker')]
class WorkerCommand extends Command
{
public function __construct(
private QueueService $queue,
private EntityManagerInterface $em,
private LoggerInterface $logger
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$iteration = 0;
$maxIterations = 1000; // Redémarrage tous les 1000 jobs
while (true) {
try {
$job = $this->queue->fetch();
if (!$job) {
sleep(1);
continue;
}
$this->processJob($job);
// Memory cleanup
$iteration++;
if ($iteration % 100 === 0) {
$this->em->clear(); // Vide l'UnitOfWork
gc_collect_cycles(); // Force garbage collection
$memory = memory_get_usage(true) / 1024 / 1024;
$this->logger->info("Memory usage: {$memory}MB");
}
// Graceful restart after X iterations
if ($iteration >= $maxIterations) {
$this->logger->info('Restarting worker for safety');
return Command::SUCCESS;
}
} catch (\Throwable $e) {
$this->logger->error('Worker exception', ['exception' => $e]);
// Continue ou exit selon la severité
}
}
return Command::SUCCESS;
}
private function processJob(Job $job): void
{
// Traiter le job
}
}
// Gestion des dépendances circulaires avec setter injection
namespace App\Service;
class ServiceA
{
private ServiceB $serviceB;
public function __construct(private DependencyX $depX) {}
public function setServiceB(ServiceB $serviceB): void
{
$this->serviceB = $serviceB;
}
}
class ServiceB
{
public function __construct(private ServiceA $serviceA) {}
}
// Configuration pour résoudre la circulaire
// config/services.yaml
services:
App\Service\ServiceA:
arguments:
- '@App\DependencyX'
calls:
- setServiceB: ['@App\Service\ServiceB']
App\Service\ServiceB:
arguments:
- '@App\Service\ServiceA'
| Optimisation | Gain | Complexité | Production-Ready |
|