findBy, findOneBy, findAll en Symfony : la reference
Les methodes find, findBy, findOneBy et findAll sont les briques de base des repositories Doctrine dans Symfony. Pourtant, beaucoup de developpeurs ne maitrisent pas tous leurs parametres ou ne savent pas quand basculer vers le QueryBuilder. Ce guide de reference couvre tout ce qu'il faut savoir avec des exemples concrets sous Symfony 7.2.
Les quatre methodes de base d'un repository
Tout repository Doctrine herite de ServiceEntityRepository (ou EntityRepository), qui fournit quatre methodes principales. Voici leur signature et leur comportement :
// find(mixed $id) : ?Entity
// Trouve une entite par sa cle primaire
$product = $productRepository->find(42);
// Retourne Product|null
// findAll() : Entity[]
// Retourne TOUTES les entites de la table
$products = $productRepository->findAll();
// Retourne Product[] (peut etre vide)
// findBy(array $criteria, ?array $orderBy, ?int $limit, ?int $offset) : Entity[]
// Recherche avec criteres, tri, pagination
$products = $productRepository->findBy(
['category' => $category, 'active' => true],
['price' => 'ASC'],
10,
0
);
// Retourne Product[]
// findOneBy(array $criteria, ?array $orderBy) : ?Entity
// Retourne la premiere entite correspondant aux criteres
$user = $userRepository->findOneBy(['email' => 'pierre@exemple.fr']);
// Retourne User|null
find() - recherche par cle primaire
La methode find() est la plus simple. Elle prend un identifiant et retourne l'entite correspondante ou null. Doctrine utilise son Identity Map en interne : si l'entite a deja ete chargee dans l'UnitOfWork, aucune requete SQL supplementaire n'est executee.
<?php
namespace App\Controller;
use App\Repository\ProductRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Attribute\Route;
class ProductController
{
#[Route('/api/products/{id}', methods: ['GET'])]
public function show(int $id, ProductRepository $repo): JsonResponse
{
$product = $repo->find($id);
if ($product === null) {
throw new NotFoundHttpException(
sprintf('Produit #%d introuvable.', $id)
);
}
return new JsonResponse([
'id' => $product->getId(),
'name' => $product->getName(),
'price' => $product->getPrice(),
]);
}
}
Astuce : dans un controller Symfony, vous pouvez aussi utiliser le ParamConverter automatique en typant directement le parametre avec l'entite : public function show(Product $product). Symfony appellera find() pour vous et renverra une 404 si l'entite n'existe pas.
findAll() - recuperer toutes les entites
findAll() retourne un tableau contenant toutes les entites de la table, sans filtre ni tri. C'est pratique pour des tables de reference (pays, categories, statuts) contenant peu d'enregistrements :
// Charger toutes les categories pour un select
$categories = $categoryRepository->findAll();
// ATTENTION : sur une grande table, ceci est dangereux
// $users = $userRepository->findAll(); // 500 000 users en RAM !
Limitation importante : findAll() ne permet ni tri ni pagination. Si vous avez besoin d'un ordre specifique, utilisez findBy([], ['name' => 'ASC']) a la place. Pour les grandes collections, preferez le QueryBuilder avec pagination ou les cursors Doctrine.
findBy() - recherche avec criteres
C'est la methode la plus polyvalente des quatre. Elle accepte quatre parametres :
| Parametre | Type | Description |
|---|---|---|
$criteria | array | Paires cle/valeur pour le filtrage (WHERE) |
$orderBy | ?array | Tri : ['champ' => 'ASC|DESC'] |
$limit | ?int | Nombre maximum de resultats |
$offset | ?int | Decalage (pour la pagination) |
// Produits actifs de la categorie 5, tries par prix decroissant, page 2 (10/page)
$products = $productRepository->findBy(
['category' => 5, 'active' => true],
['price' => 'DESC'],
10, // limit
10 // offset (page 2)
);
// Recherche avec valeur null
$drafts = $articleRepository->findBy(['publishedAt' => null]);
// Recherche avec un tableau de valeurs (WHERE IN)
$products = $productRepository->findBy(['id' => [1, 5, 12, 34]]);
// Genere : WHERE id IN (1, 5, 12, 34)
Le support du WHERE IN est particulierement utile et peu connu. En passant un tableau comme valeur d'un critere, Doctrine genere automatiquement une clause IN. C'est bien plus performant que de faire plusieurs appels find() en boucle.
Limitations de findBy
Malgre sa flexibilite, findBy a des limites claires :
- Pas de conditions
OR- tous les criteres sont combines avecAND. - Pas de
LIKE,BETWEEN,>,<- uniquement des egalites strictes (etIN). - Pas de jointures - impossible de filtrer sur une relation sans la charger.
- Pas d'agregation - pas de
COUNT,SUM,AVG.
Si vous depassez ces limites, il est temps de passer au QueryBuilder. Pour des techniques avancees, consultez notre guide sur les migrations Doctrine.
findOneBy() - un seul resultat
findOneBy() retourne la premiere entite correspondant aux criteres, ou null. Elle est ideale pour rechercher par un champ unique (email, slug, token) :
// Recherche par slug unique
$article = $articleRepository->findOneBy(['slug' => 'mon-article']);
// Recherche avec tri - retourne le plus recent
$lastOrder = $orderRepository->findOneBy(
['user' => $user, 'status' => 'completed'],
['createdAt' => 'DESC']
);
// Pattern courant : findOrFail maison
public function findOneBySlugOrFail(string $slug): Article
{
$article = $this->findOneBy(['slug' => $slug]);
if ($article === null) {
throw new EntityNotFoundException(
sprintf('Article avec le slug "%s" introuvable.', $slug)
);
}
return $article;
}
Les methodes magiques findBy* et findOneBy*
Doctrine supporte aussi des methodes magiques generees a partir des noms de proprietes. Elles sont pratiques mais presentent des inconvenients :
// Methodes magiques equivalentes
$user = $userRepository->findOneByEmail('pierre@exemple.fr');
// Equivalent a : findOneBy(['email' => 'pierre@exemple.fr'])
$products = $productRepository->findByCategory($category);
// Equivalent a : findBy(['category' => $category])
// INCONVENIENTS :
// - Pas d'autocompletion IDE (sauf avec le plugin Symfony)
// - Pas de typage de retour explicite
// - Pas de parametre orderBy, limit, offset pour findBy*
// - PHPStan les signale comme erreur sans extension
Ma recommandation : evitez les methodes magiques en faveur de methodes custom dans vos repositories. C'est plus explicite, testable et compatible avec l'analyse statique.
Creer des methodes custom dans le repository
Pour des besoins recurrents, creez des methodes dediees dans vos repositories. C'est la bonne pratique recommendee par Symfony :
<?php
namespace App\Repository;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Product>
*/
class ProductRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Product::class);
}
/**
* Produits actifs d'une categorie, tries par prix.
* @return Product[]
*/
public function findActiveByCategory(
int $categoryId,
int $limit = 20
): array {
return $this->findBy(
['category' => $categoryId, 'active' => true],
['price' => 'ASC'],
$limit
);
}
/**
* Recherche avancee avec QueryBuilder (LIKE, jointures).
* @return Product[]
*/
public function search(string $query, ?int $maxPrice = null): array
{
$qb = $this->createQueryBuilder('p')
->andWhere('p.name LIKE :query OR p.description LIKE :query')
->setParameter('query', '%' . $query . '%')
->andWhere('p.active = true')
->orderBy('p.name', 'ASC');
if ($maxPrice !== null) {
$qb->andWhere('p.price <= :maxPrice')
->setParameter('maxPrice', $maxPrice);
}
return $qb->getQuery()->getResult();
}
}
Pour charger des donnees de test dans ces repositories, decouvrez notre guide des fixtures Doctrine. Et pour gerer les relations dans vos formulaires, consultez l'article sur EntityType.
Performances et bonnes pratiques
Le probleme N+1
Les methodes findBy et findAll chargent les entites sans leurs relations (lazy loading). Acceder a une relation dans une boucle declenche une requete par iteration :
// MAUVAIS : N+1 requetes
$products = $productRepository->findBy(['active' => true]);
foreach ($products as $product) {
echo $product->getCategory()->getName(); // 1 requete par produit !
}
// BON : eager loading via QueryBuilder
$products = $productRepository->createQueryBuilder('p')
->leftJoin('p.category', 'c')
->addSelect('c')
->andWhere('p.active = true')
->getQuery()
->getResult();
// 1 seule requete avec JOIN
Recapitulatif des cas d'usage
| Besoin | Methode |
|---|---|
| Par ID | find($id) |
| Par champ unique | findOneBy(['email' => $v]) |
| Liste filtree + triee | findBy($criteria, $order, $limit) |
| Tout charger (petite table) | findAll() |
| LIKE, JOIN, OR, sous-requete | QueryBuilder |
| Requetes SQL brutes | $conn->executeQuery() |
Conclusion
Les methodes de base des repositories Doctrine sont puissantes mais ont des limites claires. Utilisez find() et findOneBy() pour les recherches unitaires, findBy() pour les listes filtrees simples, et passez au QueryBuilder des que vos besoins deviennent complexes. Ecrivez des methodes custom dans vos repositories plutot que d'utiliser les methodes magiques.
Vous avez un projet Symfony qui necessite une architecture Doctrine solide ? Decouvrez mes services de developpement, consultez les tarifs ou contactez-moi directement.
