Tous droits réservés

Récupérer des entités avec Doctrine

Maintenant que nous sommes en mesure de sauvegarder des informations dans notre base de données, il est légitime de se poser la question suivante :

Comment lire les données dans une base avec Doctrine ?

Là aussi, l'entity manager sera notre partenaire privilégié pour réaliser cette opération. Comme pour la sauvegarde des données, nous n’allons manipuler que des objets PHP.

Doctrine effectuera pour nous automatiquement la récupération des données, l’instanciation et l’hydratation de nos classes.

Récupérer une entité avec la clé primaire

Pour accéder aux informations grâce à une clé primaire, nous devons juste renseigner deux paramètres :

  • la classe de l’entité que nous voulons récupérer ;
  • et l’identifiant de celle-ci.

L'entity manager se charge alors de faire la requête SQL et d’instancier notre classe. Un exemple valant mieux que mille discours, nous allons récupérer l’utilisateur que nous venons de créer.

<?php
# get-user.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);

use Tuto\Entity\User;

$user = $entityManager->find(User::class, 1);

echo sprintf(
    "User (id: %s, firstname: %s, lastname: %s, role: %s)", 
    $user->getId(), $user->getFirstname(), $user->getLastname(), $user->getRole()
);

En exécutant ce code, nous obtenons comme réponse :

User (id: 1, firstname: First, lastname: LAST, role: admin)

Si l’identifiant n’existe pas, la variable $user vaudra null.

Pour la suite, nous allons rajouter une méthode __toString à nos entités pour pouvoir les afficher plus facilement. Vous pouvez donc dés à présent rajouter dans l’entité utilisateur, ce code :

<?php
public function __toString()
{
    $format = "User (id: %s, firstname: %s, lastname: %s, role: %s)\n";
    return sprintf($format, $this->id, $this->firstname, $this->lastname, $this->role);
}

Récupérer une ou plusieurs entités selon des critères différents

L’intérêt de Doctrine ne se limite pas qu’à la récupération d’une entité en se basant sur la clé primaire. Grâce aux métadonnées que nous avons rajoutées à notre entité, nous sommes maintenant en mesure de faire plusieurs recherches sur les utilisateurs en utilisant tous ses attributs.

Nous pouvons nativement faire des recherches sur le nom, le prénom ou encore sur deux ou plusieurs champs en même temps et ce, sans écrire aucune ligne de code personnalisée.

Les repositories

Les repositories (entrepôts) sont des classes spécialisées qui nous permettent de récupérer nos entités. Chaque repository permet ainsi d’interagir avec un type d’entité et faire la liaison entre notre code et la base de données.

Pour, par exemple, lire les informations sur les utilisateurs, nous aurons un repository dédié à l’entité utilisateur. C’est une bonne pratique de travailler avec les repositories car elle facilite grandement les interactions avec nos entités.

En utilisant un repository pour les utilisateurs, notre exemple précédent devient :

<?php
# get-user.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);

use Tuto\Entity\User;

$userRepo = $entityManager->getRepository(User::class);

$user = $userRepo->find(1);

echo $user;

L'entity manager reste l’élément central et c’est grâce à lui que nous récupérons notre repository. Avec la méthode find, nous ne sommes plus obligés de préciser la classe de l’entité que nous cherchons car le repository ne gère qu’une seule classe : ici les utilisateurs.

Les méthodes du repository

Les méthodes find, findAll, findBy et findOneBy

En accédant au repository, nous disposons de plusieurs méthodes simples permettant de lire des données.

Voici un petit tableau descriptif de chacune de ses méthodes simples.

Méthode Description
find Récupère une entité grâce à sa clé primaire
findAll Récupère toutes les entités
findBy Récupère une liste d’entités selon un ensemble de critères
findOneBy Récupère une entité selon un ensemble de critères

Pour tester toutes ces méthodes, nous allons d’abord créer plusieurs utilisateurs.

<?php
# create-users.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);

use Tuto\Entity\User;

foreach (range(1, 10) as $index) {
    $user = new User();
    $user->setFirstname("First ".$index);
    $user->setLastname("LAST ".$index);
    $user->setRole("user");
    $entityManager->persist($user);
}

$entityManager->flush();
Jeu de test pour la lecture
Jeu de test pour la lecture

Nous ne sommes pas obligés d’appeler la méthode flush après chaque persist. Dans l’extrait de code, nous donnons d’abord à Doctrine toutes les entités à sauvegarder avant de faire le flush.

Voici maintenant un ensemble d’exemples de requêtes de lecture :

<?php
# get-user.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);

use Tuto\Entity\User;

$userRepo = $entityManager->getRepository(User::class);

$user = $userRepo->find(1);
echo "User by primary key:\n";
echo $user;

$allUsers = $userRepo->findAll();
echo "All users:\n";
foreach ($allUsers as $user) {
    echo $user;
}

$usersByRole = $userRepo->findBy(["role" => "admin"]);
echo "Users by role:\n";
foreach ($usersByRole as $user) {
    echo $user;
}

$usersByRoleAndFirstname = $userRepo->findBy(["role" => "user", "firstname" => "First 2"]);
echo "Users by role and firstname:\n";
foreach ($usersByRoleAndFirstname as $user) {
    echo $user;
}

Le résultat est assez explicite :

User by primary key:
User (id: 1, firstname: First, lastname: LAST, role: admin)

All users:
User (id: 1, firstname: First, lastname: LAST, role: admin)
User (id: 2, firstname: First 1, lastname: LAST 1, role: user)
User (id: 3, firstname: First 2, lastname: LAST 2, role: user)
User (id: 4, firstname: First 3, lastname: LAST 3, role: user)
User (id: 5, firstname: First 4, lastname: LAST 4, role: user)
User (id: 6, firstname: First 5, lastname: LAST 5, role: user)
User (id: 7, firstname: First 6, lastname: LAST 6, role: user)
User (id: 8, firstname: First 7, lastname: LAST 7, role: user)
User (id: 9, firstname: First 8, lastname: LAST 8, role: user)
User (id: 10, firstname: First 9, lastname: LAST 9, role: user)
User (id: 11, firstname: First 10, lastname: LAST 10, role: user)

Users by role:
User (id: 1, firstname: First, lastname: LAST, role: admin)

Users by role and firstname:
User (id: 3, firstname: First 2, lastname: LAST 2, role: user)

Les méthodes les plus intéressantes ici sont le findBy et le findOneBy. Elles prennent toutes les deux un tableau associatif avec comme clé le nom des attributs de notre entité (id, firstname, lastname, role) et comme valeur l’information que nous voulons chercher.

Nous devons toujours mettre le nom des attributs des entités dans nos filtres et pas celui des colonnes dans la base de données. En utilisant l’ORM, nous devons faire fi de la configuration réelle de la base de données. Doctrine s’occupe en interne de la correspondance entre un attribut de notre classe et une colonne de la base de données.

Si nous avons deux ou plusieurs clés dans le tableau comme ["role" => "user", "firstname" => "First 2"], Doctrine va chercher tous les utilisateurs ayant comme rôle user et comme prénom First 2.

Du coup, comment faire pour récupérer les utilisateurs ayant comme rôle user ou un prénom valant First 2 ?

Il existe d’autres moyens pour récupérer des données avec des critères beaucoup plus avancés que ceux proposés nativement par le repository. Nous aurons l’occasion de tous les voir dans la suite de ce cours.

Mais avant cela, nous pouvons tester les filtres natifs comme Order By, Limit, Offset qui permettent d’affiner un tant soit peu les résultats de nos requêtes.

<?php
# get-user.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);

use Tuto\Entity\User;

$userRepo = $entityManager->getRepository(User::class);

$limit = 4;
$offset = 2;
$orderBy = ["firstname" => "DESC"];
$usersByRoleWithFilters = $userRepo->findBy(["role" => "user"], $orderBy, $limit, $offset);
echo "Users by role with filters:\n";
foreach ($usersByRoleWithFilters as $user) {
    echo $user;
}

Avec cette requête, nous récupérons une liste d’utilisateur en les triant par ordre décroissant du prénom (["firstname" => "DESC"]) et en limitant le nombre de résultats grâce à l’offset (2) et la limite (4).

Le résultat d’une telle requête est :

Users by role with filters:
User (id: 8, firstname: First 7, lastname: LAST 7, role: user)
User (id: 7, firstname: First 6, lastname: LAST 6, role: user)
User (id: 6, firstname: First 5, lastname: LAST 5, role: user)
User (id: 5, firstname: First 4, lastname: LAST 4, role: user)

Le filtre Order By peut avoir comme valeur ASC pour un trie par ordre croissant et DESC pour un trie par ordre décroissant.

Les méthodes findByXXX et findOneByXXX

En plus des méthodes simples findBy et findOneBy, nous avons une panoplie de méthodes qui permettent de faire une recherche.

Ce sont des raccourcis qui rajoutent du sucre syntaxique aux méthodes simples mais restent néanmoins moins complets.

Pour chacune de nos entités, Doctrine gère grâce aux méthodes magiques plusieurs variantes de méthodes de sélection.

Si nous prenons le cas de notre utilisateur, l’attribut role permet d’avoir ainsi deux méthodes dans le repository: findByRole et findOneByRole. Ces deux méthodes représentent respectivement les méthodes findBy(["role" => "XXX"]) et findOneBy(["role" => "XXX"]).

Donc l’exemple précédent peut devenir :

<?php
# get-user.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);

use Tuto\Entity\User;

$userRepo = $entityManager->getRepository(User::class);

//$usersByRole = $userRepo->findBy(["role" => "admin"]);
$usersByRole = $userRepo->findByRole("admin");
echo "Users by role:\n";
foreach ($usersByRole as $user) {
    echo $user;
}

Ces méthodes magiques sont moins complètes car elles ne supportent pas les filtres natifs (Order By, Limit, etc.).

Les index

Nous avons actuellement une base de données peu volumineuse mais la quantité de données dans une application en production peut rapidement croître.

Ainsi les performances des requêtes de lecture sont à prendre en compte dès la conception du modèle de données. Avec Doctrine, nous pouvons mettre en place des index afin d’améliorer ceux-ci. Les index SQL sont des structures permettant aux bases de données de référencer une ou plusieurs colonnes et ainsi accélérer les recherches effectuées sur celles-ci.

Pour déclarer des index grâce à Doctrine, nous pouvons utiliser l’annotation Index. Elle permet de déclarer un index en spécifiant son nom et la liste des colonnes indexées.

Les noms des colonnes doivent être ceux dans la table SQL et non pas le nom des attributs de la classe.

Si par exemple, nous voulions rajouter un index sur le nom et prénom des utilisateurs et un autre sur le rôle de ceux-ci, nous aurions comme configuration :

<?php
# src/Entity/User.php

namespace Tuto\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(
    name="users",
    indexes={
        @ORM\Index(name="search_firstname_lastname", columns={"firstname", "lastname"}),
        @ORM\Index(name="search_role", columns={"role"})
    }
 )
*/
class User
{
   // ...
}

Bien sûr pour que les index soient créés, il faut mettre à jour la base de données avec la commande :

-- vendor/bin/doctrine orm:schema-tool:update --dump-sql --force
CREATE INDEX search_firstname_lastname ON users (firstname, lastname);
CREATE INDEX search_role ON users (role);

Doctrine se base sur nos entités pour nous offrir une API de lecture très simple et intuitive.

Nous avons ainsi pu créer et lire des données sans nous préoccuper de l’infrastructure derrière. Avec la configuration actuelle, on aurait pu avoir aussi bien une base SQLite ou PostgreSQL, Doctrine gérerait nativement toute la persistance des données grâce à la couche d’abstraction qu’il apporte.

L’étape deux (2) du CRUD (Créer, Lire, Mettre à jour et Supprimer) est maintenant atteinte.