PrestaConcept
Nos réalisations
Nos métiers
  • Découvrez nos métiers
  • Développement web sur mesure

    Nous développons en méthode agile des back sous le framework PHP Symfony, des front en Angular.

  • Maintenance d'applications

    Maintien en condition opérationnelle de votre plateforme Symfony.

  • Expertise Symfony

    Coaching, formation, audit et conseil.

  • Hébergement et Infogérance

    Une expertise de l'hébergement depuis plus de 15 ans et l’infogérance de centaines de machines en production actuellement.

  • Qui sommes nous
  • Découvrez Prestaconcept
  • PrestaConcept

    Notre histoire, nos convictions, notre vision... Découvrez ce qui nous anime !

  • L'équipe

    Plus de 15 ans minimum d'expérience sur Symfony.

  • Nos engagements RSE

    Une société engagée pour un numérique responsable.

  • Nos convictions

    Co-construction, transparence.. Les principes qui guident nos collaborations !

  • Nous rejoindre

    Envie de nous rejoindre ? Consultez nos offres !

  • Blog
    J'ai un projet Nous contacter
    J'ai un projet Nous contacter Menu
    • Accueil
    • Blog
    • Tech
    • Le pattern Décorateur avec Symfony

    Blog

    Le pattern Décorateur avec Symfony

    design pattern php snippet symfony
    Yann Eugoné
    Yann Eugoné CTO
    Publié le mercredi 16 mars 2022

    Apprenez à découper votre code devenu trop complexe avec le pattern décorateur, en vous aidant de Symfony.

    Le pattern Décorateur

    Ce design pattern propose une façon de découper et d'agencer un ensemble de classe répondant à un même contrat, et œuvrant ensemble dans le même objectif.
    L'architecture propose d'imbriquer les objets les uns dans les autres (à la manière de poupées russes), chaque couche supplémentaire ajoutant sa spécificité.

    L'intérêt de ce découpage est de proposer un ensemble de classe avec des spécificités, facilement composables, et respectant le principe Ouvert/Fermé : le comportement du code est modifié, mais pas le code en lui-même.

    Exemple concret

    Disons que votre application doit chercher des informations à propos de ses utilisateurs sur un service tiers (peu importe la technologie).
    Vous avez créé un service répondant à ce besoin.

    final class UserFetcher
    {
        public function fetch(string $username): array
        {
            // ...
        }
    }
    

    Mais voilà, on vous demande d'ajouter quelques capacités annexes au code que vous avez produit :

    • logger les appels qui sont faits
    • ajouter des informations par défaut lorsque la récupération échoue
    • mettre en cache les informations reçues
    • chronométrer les appels qui sont faits

    Alors évidemment, vous pourriez tout simplement modifier le code du service que vous avez déjà écrit, et lui ajouter en une seule fois toutes ces choses.
    Seulement voilà, ça fait beaucoup de responsabilités pour une seule et même classe (c'est même une violation du principe de responsabilité unique).

    Mise en oeuvre avec Symfony

    Ce n'est pas une obligation du pattern, mais vous gagnerez vraiment à exposer un contrat pour ce composant, et ici une interface sera le candidat idéal.
    Il suffit d'extraire depuis le service que vous avez déjà écrit la signature de la méthode principale :

    namespace App\User;
    
    interface UserFetcherInterface
    {
        public function fetch(string $username): array;
    }
    

    Commençons déjà par poser l'interface sur votre service :

    namespace App\User;
    
    final class UserFetcher implements UserFetcherInterface
    {
        public function fetch(string $username): array
        {
            // ...
        }
    }
    

    Il s'agit maintenant d'écrire chacun des comportements décrits plus haut, en respectant le contrat.
    De façon générale, un décorateur a toujours plus ou moins la même structure :

    • Il implémente l'interface du composant qu'il décore
    • Il reçoit à la construction un autre élément de la même interface
    • Il "entoure" l'appel à l'élément décoré de sa logique
    namespace App\User;
    
    final class WhateverFetcher implements UserFetcherInterface
    {
        public function __construct(
            private UserFetcherInterface $decorated,
        ) {
        }
    
        public function fetch(string $username): array
        {
            // quelque chose à faire avant ?
            $result = $this->decorated->fetch($username);
            // quelque chose à faire après ?
    
            return $result;
        }
    }
    

    Mettre en cache les informations

    Ce décorateur nécessite symfony/cache et autorise la mise en cache du résultat de l'appel à l'instance qu'il décore.

    namespace App\User;
    
    use Symfony\Contracts\Cache\CacheInterface;
    
    final class CacheUserFetcher implements UserFetcherInterface
    {
        public function __construct(
            private UserFetcherInterface $decorated,
            private CacheInterface $cache,
        ) {
        }
    
        public function fetch(string $username): array
        {
            return $this->cache->get(
                'fetch_user_' . $username,
                fn () => $this->decorated->fetch($username)
            );
        }
    }
    

    Résultat par défaut en cas d'échec

    Cette implémentation permet seulement d'éviter qu'une erreur déclenchée par l'instance décorée ne vienne bloquer le process, et propose une valeur de remplacement lorsque cela arrive.

    namespace App\User;
    
    final class DefaultsUserFetcher implements UserFetcherInterface
    {
        public function __construct(
            private UserFetcherInterface $decorated,
            private array $defaults,
        ) {
        }
    
        public function fetch(string $username): array
        {
            try {
                return $this->decorated->fetch($username);
            } catch (\Throwable) {
                return $this->defaults;
            }
        }
    }
    

    Logger les appels et les erreurs

    Cette implémentation nécessite un logger (généralement grâce à symfony/monolog-bundle) qui sera utilisé pour tracer chaque appel à l'implémentation décorée ainsi que les erreurs qu'elle pourrait émettre.

    namespace App\User;
    
    use Psr\Log\LoggerInterface;
    
    final class LogUserFetcher implements UserFetcherInterface
    {
        public function __construct(
            private UserFetcherInterface $decorated,
            private LoggerInterface $logger,
        ) {
        }
    
        public function fetch(string $username): array
        {
            $this->logger->info(
                'Fetching user info from API client.',
                ['username' => $username, 'client' => \get_class($this->decorated)]
            );
    
            try {
                return $this->decorated->fetch($username);
            } catch (\Throwable $error) {
                $this->logger->error(
                    'An error occurred while fetching info from API client.',
                    ['error' => $error, 'client' => \get_class($this->decorated)]
                );
    
                throw $error;
            }
        }
    }
    

    Chronométrer les appels

    Cette implémentation utilise symfony/stopwatch qui sera appelé pour profiler le code de l'instance décorée.

    namespace App\User;
    
    use Symfony\Component\Stopwatch\Stopwatch;
    
    final class TraceUserFetcher implements UserFetcherInterface
    {
        public function __construct(
            private UserFetcherInterface $decorated,
            private Stopwatch $stopwatch,
        ) {
        }
    
        public function fetch(string $username): array
        {
            $this->stopwatch->start('fetch_user_' . $username);
    
            try {
                return $this->decorated->fetch($username);
            } finally {
                $this->stopwatch->stop('fetch_user_' . $username);
            }
        }
    }
    

    Déclarer les services

    Maintenant que nos classes sont créées, nous allons pouvoir nous attaquer aux services associés.

    Notre objectif reste de pouvoir continuer à injecter le user fetcher par autowiring, sans avoir à se poser la question l'instance réelle que l'on reçoit.
    Pour ça, on va utiliser un alias Interface => Classe qui permettra à Symfony de savoir que dès lors qu'un service demande l'interface de notre user fetcher, il doit injecter tel service à la place.

    services:
        App\User\UserFetcherInterface: '@App\User\UserFetcher'
    

    Maintenant, nous allons pouvoir nous attaquer à la décoration à proprement parler.
    De ce côté là, c'est parfait car Symfony dispose d'une mécanique intégrée au composant DependencyInjection.

    Nous allons donc utiliser les options decorates & decoration_priority pour contrôler la fabrication de l'ensemble de nos services.

    services:
        _defaults:
            autowire: true
            autoconfigure: true
    
        App\User\CacheUserFetcher:
            decoration_priority: 2
            decorates: App\User\UserFetcher
    
        App\User\DefaultsUserFetcher:
            decoration_priority: 1
            decorates: App\User\UserFetcher
            arguments:
                $defaults:
                    firstName: John
                    lastName: Doe
    
        App\User\LogUserFetcher:
            decoration_priority: 4
            decorates: App\User\UserFetcher
    
        App\User\TraceUserFetcher:
            decoration_priority: 3
            decorates: App\User\UserFetcher
    

    Avec cette configuration, on obtient la construction suivante :

    new LogUserFetcher(
        new TraceUserFetcher(
            new CacheUserFetcher(
                new DefaultsUserFetcher(
                    new UserFetcher(),
                    ...
                ),
                ...
            ),
            ...
        ),
        ...
    );
    

    Mais comme vous l'avez compris, en jouant avec decoration_priority, vous pouvez fabriquer n'importe quelle combinaison de classes.
    C'est d'ailleurs pour cette raison que le contrat est important, car sans lui, la structure serait figée.

    Conclusion

    Ce pattern peut être mis en place dès que vous repérez une classe ayant trop de dépendances, notamment techniques.
    Mais de façon générale, c'est un très bon pattern de structure qui convient à bien des moments.

    Grâce à la mise en place de ce pattern, vous obtiendrez des structures de classes flexibles où l'on peut changer le comportement du code sans changer le code lui-même.
    Cette flexibilité, apportée notamment par la mise en place d'une interface, rend notre code plus robuste et plus facile à tester.
    Et comme la mise en place dans Symfony est très simple, c'est un pattern dont nous pouvons abuser.

    Blog

    Pour continuer votre lecture ...

    Tech

    Le pattern Stratégie avec Symfony

    Par Maximilien Delangle 09/02/2023

    C'est un modèle de conception de logiciel qui permet de séparer l'algorithme d'une classe de son exécution. Il est utilisé pour résoudre les problèmes de complexité et de maintenance dans les applications.

    Lire la suite
    Tech

    Le pattern Chaîne de responsabilité avec Symfony

    Par Maximilien Delangle 20/07/2022

    Apprenez à rendre votre code plus léger et maintenable avec le design pattern Chaîne de responsabilité avec Symfony

    Lire la suite
    Tech

    Comment désactiver certains listeners lors de certaines commandes

    Par Yann Eugoné 11/01/2022

    Une solution simple et élégante, utilisant l'injection de services tagués, pour vous donner la possibilité de désactiver certains listeners lors de l'exécution de certaines commandes.

    Lire la suite

    Vous avez un projet Laravel ?

    Nous sommes spécialisés en Symfony, et grâce à Web^ID, l’agence sœur du groupe Agile Invest, nous couvrons aussi toute l’expertise Laravel.

    Découvrir Web^ID

    Une question, un projet ?
    Planifiez un échange avec nous !

    Choisissez votre date
    PrestaConcept - Groupe Agile Invest
    5, imp. Morel, 69003 Lyon +33 (0)4 78 54 45 45
    Suivez-nous
    Ecoindex B

    Ce site internet est un site basse consommation. En savoir plus sur l'Ecoindex

    Nos réalisations

  • Logiciel de mise en conformité réglementaire
  • Application de suivi de production des centrales éoliennes
  • Outil d'aide à la décision
  • Portail client
  • Nos métiers

  • Développement sur-mesure
  • Reprise d'application Symfony
  • Expertise Symfony
  • Hébergement & Infogérance
  • Qui sommes-nous

  • PrestaConcept
  • Groupe Agile Invest
  • L'équipe
  • Engagement RSE
  • Blog

  • Tech
  • Méthodologie
  • PrestaConcept
  • RSE
  • © 2025 PrestaConcept
    Mentions légales Politique de confidentialité 🍪
    Retour en haut de page