Quand on doit filtrer une collection Doctrine, le premier réflexe est d’utiliser la méthode filter(). Simple et pratique, le code est plus court et lisible qu’une boucle traditionnelle.
#[ORM\Entity]
class Project
{
#[ORM\OneToMany(targetEntity: Task::class)]
private Collection $tasks;
public function getPendingTasks(): Collection
{
return $this->tasks->filter(
static fn (Task $task): bool => !$task->isDone(),
);
}
}Néanmoins cette méthode de filtrage peut ne pas être sans conséquences.
Les collections Doctrine sont rarement utilisées comme bibliothèques de collections. Elles sont généralement utilisées avec le composant ORM pour manipuler la base de données. Si l’on reprend le code précédent, $project est une entité Doctrine qui possède une relation avec une entité Task. Or si un projet est associé à de nombreuses tâches, la méthode filter() peut être inefficace car elle va nécessiter la récupération et l’hydratation de toutes les données (potentiellement des milliers) avant de les parcourir une à une pour filtrer les tâches non terminées. Cela peut s’avérer être un désastre pour les performances.
C’est un des problèmes de gestion des collections de données avec les ORM bien moins connu que le problème N+1 car il est plus discret puisqu’il n’y a qu’une seule requête SQL qui est exécutée. Le reste se passe en mémoire.
Il est alors tentant d’extraire cette logique de filtrage dans une méthode de repository pour éviter de charger toutes les tâches en mémoire et de le faire via une requête SQL. C’est dommage, surtout lorsque l’on souhaite conserver la sémantique objet de notre appel et permettre de récupérer toutes les tâches non terminées d’un projet à partir de l’objet Project lui-même.
Il existe pourtant une seconde méthode des collections Doctrine (si la collection implémente l’interface Doctrine\Common\Collections\Selectable) permettant de filtrer les éléments, mais qui elle est bien moins connue : la méthode matching. Cette dernière prend en paramètre un objet Doctrine\Common\Collections\Criteria qui permet de définir les conditions de filtrage.
use Doctrine\Common\Collections\Criteria;
public function getPendingTasks(): Collection
{
$criteria = Criteria::create()
->where(Criteria::expr()->eq('done', false));
return $this->tasks->matching($criteria);
}La différence est que Criteria n’est pas du code exécutable, mais la description d’un filtre. De ce fait, si l’on utilise une collection mémoire de type Doctrine\Common\Collections\ArrayCollection, la méthode matching va fonctionner de la même manière que la méthode filter. Par contre, dans un contexte ORM, les collections sont généralement des objets de type Doctrine\Common\Collections\PersistentCollection, la méthode matching va exécuter une requête SQL pour filtrer les éléments directement depuis la base de données.
Il y a néanmoins quelques points d’attention à avoir en utilisant matching avec PersistentCollection :
- l’exécution SQL ne sera déclenchée que si la collection
Project::$tasksn’a pas encore été initialisée. Dans le cas contraire, la méthodematchingitérera sur la collection en mémoire, - la méthode
matchingretournera une nouvelle collection détachée.
Maintenant, vous vous demandez peut-être s’il est encore utile d’utiliser filter ? La réponse que j’ai envie de donner est : “ça dépend”. Tout dépendra de la donnée qui est manipulée. Faire un matching avec la légère complexité que cela implique est pertinent sur des grosses collections gérées par Doctrine ORM. Sur des collections hors ORM ou sur des petites collections persistées filter pourra faire l’affaire.