Hello ! Symfony 4 étant mon outil de travail au quotidien, j’aimerais vous faire part de quelques trucs et astuces que je considère comme étant de bonnes pratiques.

Remarque: Contrairement à Laravel, sur Symfony vous n’avez pas de façades pour injecter vos dépendances. Convention over configuration n’est absolument pas la philosophie des développeurs Symfony qui préfèrent écrire des fichiers de configurations en yaml avec de la documentation souvent dépreciées. Ceci dit, je salue l’initiative Flex qui permet au moins d’avoir des fichiers de configurations basiques à l’installation de votre bundle préféré. En 2019 c’est appréciable.

Bon j’arrête de râler, sans plus tarder parlons de l’injection de dépendances par l’utilisation de traits.

Traitons le cas suivant ! Vous voulez injecter le validateur symfony et l’entity manager de Doctrine dans un controller. Vous pouvez injecter les dépendances en passant ces derniers en paramètres du constructeur de votre controller.

<?php

namespace App\Controller;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class UserController extends AbstractController {
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    /**
     * @var ValidatorInterface
     */
    private $validator;

    public function __construct(EntityManagerInterface $entityManager, ValidatorInterface $validator) {
        $this->entityManager = $entityManager;
        $this->validator = $validator;
    }
}

Bien sûr, n’oublions de préciser ceci dans service.yaml pour que Symfony initialise les dépendances à injecter. N’oublions pas qu’avec Symfony, on aime bien écrire de la configuration. :)

    App\Controller\:
        resource: '../src/Controller'
        tags: ['controller.service_arguments']

Vous pouvez aussi préciser ce comportement par défaut:

    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

À partir de là, il est possible de créer une méthode dans notre controller utilisant l’entity manager et le validateur.

    ...
    /**
     * Sans utiliser de mécanisme de réflexion mais directement la request
     */
    public function postHelloWorld(Request $request): Response {
        $content = json_decode($request->getContent());
        $errors = $this->validator->validate($content);

        if (count($errors) > 0) {
            throw new BadRequestException($errors);
        }
        
        /* J'imagine que le constructeur de l'entité HelloWord 
           prend en paramètre un object stdClass et map chaque champ
           avec ceux de l'entité mais idéalement il faudrait un Data Mapper
        */
        $this->entityManager->persist(new HelloWord($content));
        $this->entityManager->flush();

        return JsonResponse(null, 201);
    }
    ...

Mais voilà, si jamais on veut injecter plus de dépendances et initialiser plus d’attributs, avec cette méthodes on risque de se retrouver une plétore d’arguments à passer au constructeur de la classe.

Une solution plutôt élégante consiste à créer un trait ou plusieurs traits pour injecter les dépendances. La documentation nous apprends qu’il est possible d’initialiser un attribut dynamiquement en utilisant une annotation sur un setter.

class RandomClass {
    private $logger;

    /**
     * @required
     */
    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
}

Du coup si on veut factoriser son code et éviter les problèmes de conceptions liés à l’héritage, le mieux est de créer un trait. Pour rappel, en Php (comme en Java) vous ne pouvez hériter que d’une seule classe.

Remarque: Si vous avez une bonne conception, en théorie vous n’avez pas besoin de l’héritage multiple. Méfiez-vous des langages qui permettent l’héritage multiple. Avoir un sous-type C de A et B, c’est prendre le risque de créer des incohérences dans C mais aussi de briser le concept de Single Responsability (premier principe de SOLID) et en même temps, vous prenez le risque de créer des comportements inattendus lors de la factorisation du code. (avec une fonction nécessitant un objet de type A et qui reçoit un sous-type C qui, dans le carde de l’héritage multiple, peut être aussi un sous-type de B) Dans ce cas c’est le principe de Substitution de Liskov (SOLID) qui peut être compromis.

Les traits sont utiliser pour déléguer des comportements et ne doivent en aucun cas être utilisés pour gérér des états. Un trait n’a pas d’état, on ne peut pas l’instancier. (tout comme une classe abstraite ou une interface)

Par contre, peut instancier une classe qui aura des propriétés déléguées par un ou plusieurs traits. Les traits doivent respecter le principe de Ségrégation des Interfaces. (SOLID) C’est pourquoi vous devez privilégier la création de plusieurs petits traits plutôt que la création de gros traits avec 26 champs et fonctions.

<?php

namespace App\Helper;

use Doctrine\ORM\EntityManagerInterface;

trait EntityManagerTrait {
    /**
     * @var EntityManagerInterface
     */
    protected $entityManager;

    /**
     * @required
     */
    public function setEntityManager(EntityManagerInterface $entityManager): void {
        $this->entityManager = $entityManager;
    }
}

De cette façon on initialise le champ protected (ou private mais il faudra créer un getter public ou protected pour que la classe qui utilisera le trait puisse accéder à l’attribut) $entityManager de type EntityManagerInterface (toujours privilégier une interface, rappeler vous du principe de Substitution de Liskov cité un peu plus haut)

Et ensuite on importe ce trait dans notre controller

class UserController extends AbstractController {
    use EntityManagerTrait; 
}

Il est ensuite possible d’utiliser l’entity manager à partir de $this. Le constructeur n’a plus besoin d’avoir un paramètre de type EntityManagerInterface en paramètre.

Mais là où les traits sont particulièrement intéressant, c’est pour gérer des traitements particulier sur des Entity. Comme par exemple, ajouter les champs createdAt, updatedAt et deletedAt.

<?php

namespace App\Helper\ORM;

use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;

/**
 * Trait TimestampableTrait
 * @ORM\HasLifecycleCallbacks
 */
trait TimestampableTrait
{
    /**
     * @var datetime
     * @Gedmo\Timestampable(on="create")
     * @ORM\Column(name="created_at", type="datetime", nullable=true)
     */
    protected $createdAt;

    /**
     * @var datetime
     * @Gedmo\Timestampable(on="update")
     * @ORM\Column(name="updated_at", type="datetime", nullable=true)
     */
    protected $updatedAt;


    /**
     * @param $createdAt
     * @return self
     */
    public function setCreatedAt($createdAt): self
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    /**
     * Get createdAt.
     * @return \DateTime
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }


    /**
     * Set updated.
     * @param \DateTime $updatedAt
     * @return self
     */
    public function setUpdatedAt($updatedAt): self
    {
        $this->updatedAt = $updatedAt;

        return $this;
    }

    /**
     * Get updatedAt.
     * @return \DateTime
     */
    public function getUpdatedAt(): ?datetime
    {
        return $this->updatedAt;
    }
}

Ensuite si on veut un soft delete on créé un second trait.

<?php

namespace App\Helper\ORM;

use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;

/**
 * Trait SoftDeleteTrait
 * @package App\Helper\ORM
 */
trait SoftDeleteTrait
{
    /**
     * @var datetime
     * @ORM\Column(name="deleted_at", type="datetime", nullable=true)
     */
    protected $deletedAt;

    /**
     * Set deletedAt.
     * @param \DateTime $deletedAt
     * @return self
     */
    public function setDeletedAt($deletedAt): self
    {
        $this->deletedAt = $deletedAt;

        return $this;
    }

    /**
     * Get deletedAt.
     * @return \DateTime
     */
    public function getDeletedAt(): ?datetime
    {
        return $this->deletedAt;
    }
}

De cette façon, je peux attribuer un champ createdAt et updatedAt et si besoin, un deletedAt.

<?php

namespace App\Entity;

use App\Helper\ORM\TimestampableTrait;
use App\Helper\ORM\SoftDeleteTrait;
use Doctrine\ORM\Mapping as ORM;

class HelloWorld {
    use TimestampableTrait, SoftDeleteTrait;

    /**
     * @var int 
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     * @ORM\Column(name="message", type="string", nullable=true)
     */
    private $message;
}

Et ainsi notre classe peut encore hériter d’une autre classe tout en ayant les champs nécessaires pour implémenter un mécanisme de soft delete.

Conclusion

Les traits c’est bien pratiques ! Ceci dit parfois il est préférable d’implémenter des façades pour l’injection de dépendances. En effet une classe A qui utilise le trait T se voit instancier avec les champs et fonctions de T.

Pour le peu que vous utiliser plusieurs traits vous pouvez vous retrouver avec une instances assez énorme. En utilisant des façades, vous ne surchargez pas votre classe de fonctions et attributs qui ne seront peut être pas si utilisés que cela.