Symfony Avancé

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.

Preparetoi.academy 30 min

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

Accédez à des centaines d'examens QCM — Découvrir les offres Premium