Symfony ExpressionLanguage : Comment utiliser ce composant ?
Le composant Symfony ExpressionLanguage : qu'est-ce que c'est ? Quand et comment l'utiliser ? Comment créer des expressions lors de cas plus complexes ?
Progression
Nous y voilà enfin ! Je vous conseille de faire une pause pour aller lire l'article sur le typage générique en PHP pour un peu de théorie sur le pourquoi du comment des generics.
Ça y est ? Alors c'est parti !
Avant toute chose, modifiez votre fichier phpstan.neon
pour qu'il ressemble à cela :
parameters: level: max paths: - bin/ - config/ - public/ - src/
Puis lançons notre commande PHPStan pour voir ce qu'il en retourne :
$ ./vendor/bin/phpstan
Vous devriez avoir tout plein d'erreurs. Il est l'heure de se retrousser les manches et de s'en occuper.
Tout d'abord, il faut créer notre nouveau type générique, que nous nommons par convention T
.
Avec le tag @template
, nous le déclarons au plus haut niveau possible de notre imbrication de repositories, ici BaseRepositoryDoctrine
:
/** @template T of object */
abstract class BaseRepositoryDoctrine
{
// ...
}
...et son interface BaseRepositoryInterface
:
<?php
declare(strict_types=1);
namespace App\Repository;
/** @template T of object */
interface BaseRepositoryInterface
{
// ...
}
La notation of object
signifie que notre type T
est quoiqu'il arrive de type object
.
À présent, typons le corps de notre BaseRepositoryDoctrine
pas-à-pas.
Commençons avec la propriété Repository
:
/** @var ObjectRepository<T> */
protected ObjectRepository $repository;
Cela est possible car l'interface ObjectRepository
contient elle même un tag @template T of object
. Ce faisant, nous indiquons à notre analyseur que nous remplaçons le type T
par le type que nous définissons entre les chevrons. Ici, nous avons remis notre T
à nous. Aucun changement me direz-vous.
Mais vous imaginez bien que le tour de magie n'est pas terminé.
Ajoutons les tags sur nos méthodes :
/** @param T $object */
public function store(object $object): void
{
$this->entityManager->persist($object);
}
/** @return ?T */
public function find(int $id): ?object
{
return $this->repository->find($id);
}
Taggez également les méthodes de la
BaseRepositoryInterface
de la même manière !
Ici nous indiquons à notre cher PHPStan de remplacer nos types object
par notre nouveau type T
. Cela fonctionne car rappelez-vous, T
ne peut de toute manière n'être qu'un object
.
Enfin, dernière petite astuce, vous pouvez faire ceci au niveau du constructeur :
/** @param class-string<T> $className */
public function __construct(protected EntityManagerInterface $entityManager, string $className)
{
$this->repository = $this->entityManager->getRepository($className);
}
Le type class-string
permet de limiter le type string
à des valeurs très précises : ce ne pourra être que des noms de classe valides, pas une chaîne de caractère classique. Or c'est bien ce que l'on veut ici ; un nom de classe pour créer la bonne instance de repository. Vous trouverez la doc de ce tag ici.
Important
C'est bon, notre repository de base a été générisé, nous pouvons à présent en tirer profit dans nos repositories Doctrine.
Par exemple avec le PostRepositoryDoctrine
:
/** @extends BaseRepositoryDoctrine<Post> */
class PostRepositoryDoctrine extends BaseRepositoryDoctrine implements PostRepositoryInterface
{
public function __construct(protected EntityManagerInterface $entityManager)
{
parent::__construct($entityManager, Post::class);
}
/** @return array<Post> */
public function findPostsAboutPhp(): array
{
// Whatever...
return [];
}
}
Le seul changement utile et important ici est le tag @extends
juste au-dessus de la classe. C'est ici que tout ce passe.
On indique ici que nous étendons notre BaseRepositoryDoctrine
, mais avec une info supplémentaire, la présence des chevrons <Post>
. En faisant cela, on explique à PHPStan que nous souhaitons remplacer, dans cette classe, tous nos types génériques T
par Post
. Et par User
dans notre Repository User en faisant la manipulation équivalente.
Notez également la notation @return array<Post>
au-dessus de la seule méthode de notre repository. Cela permet d'indiquer que nous ne pouvons retourner qu'un tableau composé d'objets Post
. En fonction de comment vous implémentez la méthode, PHPStan vous remonte une erreur si ce n'est pas le cas.
Note
array<Post>
peut aussi être noté Post[]
si vous préférez.
Testons d'ailleurs si tout ce petit monde fonctionne correctement. Créons un Controller, et mettons-y ce code :
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Post;
use App\Repository\UserRepositoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class BaseController
{
#[Route(path: '/', name: 'index', methods: [Request::METHOD_GET])]
public function index(UserRepositoryInterface $userRepository): Response
{
$post = new Post();
$userRepository->store($post);
return new Response();
}
}
Puis, lançons notre commande PHPStan. Vous devriez recevoir cette erreur :
$ Parameter 1 $object of method App\Repository\BaseRepositoryInterface<App\Entity\User>::store() expects App\Entity\User, App\Entity\Post given.
Eh oui, car nous essayons de passer un objet de type Post
dans la méthode store()
du UserRepository
. PHPStan est capable de le détecter grâce à nos quelques annotations, et nous produit cette erreur.
Remplacez l'argument par un objet de type User
, et tout rentre dans l'ordre.
Et voilà, vous en avez fini avec les types génériques !
Auteur(s)
Arthur Jacquemin
Développeur de contenu + ou - pertinent @ ElevenLabs_🚀
Vous souhaitez en savoir plus sur le sujet ?
Organisons un échange !
Notre équipe d'experts répond à toutes vos questions.
Nous contacterDécouvrez nos autres contenus dans le même thème
Le composant Symfony ExpressionLanguage : qu'est-ce que c'est ? Quand et comment l'utiliser ? Comment créer des expressions lors de cas plus complexes ?
Découvrez comment réaliser du typage générique en PHP : introduction et définition du concept, conseils et explications pas-à-pas de cas pratique.
Découvrez un cas d'usage d'intégration d'un CRM avec une application e-commerce, en asynchrone, avec Hubspot et RabbitMQ