Annexes

Les sujets à couvrir sur Doctrine sont très nombreux et variés. Nous allons aborder dans ce chapitre certains mécanismes et concepts qui peuvent se montrer très utiles dans beaucoup d’occasions.

Il n’existe pas de relations particulières entre les sujets qui sont abordés dans ce chapitre.

Support des transactions

Beaucoup de moteur de base de données fournissent le mécanisme de transaction afin d’assurer l’atomicité d’un ensemble de requêtes.

L’atomicité est une propriété qui désigne une opération ou un ensemble d’opérations en mesure de s’exécuter complétement sans interruption.

Un ensemble de requêtes sera considéré comme atomique si nous avons la garantie qu’elles seront toutes exécutées ensemble. Pour obtenir un tel résultat, Doctrine fournit une API nous permettant d’exploiter le mécanisme de transaction des bases de données relationnelles.

Transactions implicites

Lorsque que nous utilisons l'entity manager, Doctrine crée automatiquement une transaction afin d’assurer que nos opérations se déroulent correctement. Ainsi, toutes les opérations déclarées avant l’appel de la méthode flush de l'entity manager sont exécutées dans une même transaction. Nous pouvons ainsi effectuer sereinement toutes les modifications que nous voulons sur notre modèle de données avant d’effectuer les requêtes SQL associées.

<?php
# create-user-with-address.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);
$logger = $entityManager->getConfiguration()->getSQLLogger();

use Tuto\Entity\User;
use Tuto\Entity\Address;

// Instanciation de l'utilisateur

$user = new User();
$user->setFirstname("First with Address");
$user->setLastname("Lastname");
$user->setRole("admin");

// Création de l'adresse
$address = new Address();
$address->setStreet("Place Charles de Gaulle");
$address->setCity("Paris");
$address->setCountry("France");

$user->setAddress($address);

// Gestion de la persistance
/*
Le premier persist n'est pas obligatoire car il y a une opération de cascade
sur l'addresse
*/
$entityManager->persist($address);
$entityManager->persist($user);
$entityManager->flush();

// Vérification du résultats
echo $user;

echo "Nombre de requêtes : ", count($logger->queries), "\n";

foreach ($logger->queries as $query) {
    echo "- ", json_encode($query), "\n";
}

En exécutant ce code, Doctrine exécute quatre (4) requêtes dont une pour démarrer la transaction (START TRANSACTION) et une autre pour la valider (COMMIT).

User (id: 13, firstname: First with Address, lastname: Lastname, role: admin, address: Address (id: 9, street: Place Charles de Gaulle, city: Paris, country: France))
Nombre de requêtes : 4
- {"sql":"\"START TRANSACTION\"","params":null,"types":null,"executionMS":0.083003997802734}
- {"sql":"INSERT INTO addresses (street, city, country) VALUES (?, ?, ?)","params":{"1":"Place Charles de Gaulle","2":"Paris","3":"France"},"types":{"1":"string","2":"string","3":"string"},"executionMS":1.1140639781952}
- {"sql":"INSERT INTO users (firstname, lastname, role, address_id) VALUES (?, ?, ?, ?)","params":{"1":"First with Address","2":"Lastname","3":"admin","4":9},"types":{"1":"string","2":"string","3":"string","4":"integer"},"executionMS":1.9531121253967}
- {"sql":"\"COMMIT\"","params":null,"types":null,"executionMS":0.1080060005188}
[Finished in 4.7s]

Transactions explicites

Il est toujours possible d’activer les transactions à la demande. Si par exemple nous voulons supporter la suppression d’un sondage. Il serait intéressant de pouvoir faire toutes les suppressions nécessaires (le sondage, les questions, les réponses, les participations, les choix des utilisateurs liés à ce sondage) dans une même transaction.

Pour créer une transaction, nous devons utiliser directement la connexion à la base de données.

<?php
// Démarrage de la transaction
$connection = $entityManager->getConnection();
$connection->beginTransaction(); 
try {
    // Suppression du sondage et de tous les éléments liés
    $connection->commit();
} catch (Exception $e) {
    // Si il y a un problème, nous annulons la transaction
    $connection->rollBack();
}

Lorsque nous démarrons manuellement une transaction, Doctrine désactive automatiquement les transactions implicites. L’appel à la méthode flush ne créera plus de transactions.

Il y a aussi une fonction utilitaire de l'entity manager permettant d’utiliser les transactions sans passer par la connexion.

<?php
$entityManager->transactional(function($entityManager) {
    // opérations
});

Toutes les opérations dans la fonction anonyme passée à la méthode transactional seront exécutées dans une même transaction.

Les références (ou comment économiser une requête)

Supposons que nous avons dans notre base de données l’adresse Address (id: 10, street: 6 Parvis Notre-Dame - Pl. Jean-Paul II, city: Paris, country: France). N’hésitez pas à la créer pour tester par vous-même.

Nous connaissons l’identifiant de l’adresse (10) et nous voulons l’associer à un utilisateur (Id: 7) déjà existant.

Nous avons le choix entre faire une requête pour récupérer l’adresse et une autre pour récupérer l’utilisateur avant de sauvegarder nos modifications. Ou alors nous pouvons utiliser un type d’objet appelé : référence.

Les références sont des objets qui permettent d’avoir une instance d’une entité dont on connait l’identifiant sans accéder à la base de données.

Ce sont des proxies Doctrine qui permettent d’utiliser facilement une entité dans une relation.

<?php
# set-user-address.php

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

use Tuto\Entity\User;
use Tuto\Entity\Address;

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

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

$address = $entityManager->getReference(Address::class, 10);

$user->setAddress($address);

$entityManager->flush();

echo "\nPerformances:\n";
echo "Nombre de requêtes : ", count($logger->queries), "\n";

foreach ($logger->queries as $query) {
    echo "- ", json_encode($query), "\n";
}
Performances:
Nombre de requêtes : 4
- {"sql":"SELECT u0_.id AS id_0, u0_.firstname AS firstname_1, u0_.lastname AS lastname_2, u0_.role AS role_3, a1_.id AS id_4, a1_.street AS street_5, a1_.city AS city_6, a1_.country AS country_7, p2_.id AS id_8, p2_.date AS date_9, u0_.address_id AS address_id_10, p2_.user_id AS user_id_11, p2_.poll_id AS poll_id_12 FROM users u0_ LEFT JOIN addresses a1_ ON u0_.address_id = a1_.id LEFT JOIN participations p2_ ON u0_.id = p2_.user_id WHERE u0_.id = ?","params":[7],"types":["integer"],"executionMS":0.24201416969299}
- {"sql":"\"START TRANSACTION\"","params":null,"types":null,"executionMS":0.027002096176147}
- {"sql":"UPDATE users SET address_id = ? WHERE id = ?","params":[10,7],"types":["integer","integer"],"executionMS":0.020001173019409}
- {"sql":"\"COMMIT\"","params":null,"types":null,"executionMS":0.0049998760223389}

Les références sont construites autour du principe de lazy-loading. Dès que nous essayerons d’accéder ou de modifier un de ses attributs, Doctrine chargera toutes ses informations depuis la base de données. Mais vous remarquerez en consultant les logs des requêtes SQL qu’en aucun cas l’adresse n’est récupérée pour mettre à jour l’utilisateur.

Owning side - Inverse side : gestion des relations

Définition

La gestion des relations bidirectionnelles par Doctrine n’est pas une tâche anodine.

Si nous récupérons un sondage de la base de données et que nous modifions la liste des questions qui lui sont associées (en retirant une question par exemple), à la sauvegarde des modifications, la question retirée sera toujours liée au sondage.

<?php
# owning-side-inverse-side.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);
$logger = $entityManager->getConfiguration()->getSQLLogger();

use Tuto\Entity\Poll;

$pollRepo = $entityManager->getRepository(Poll::class);

$poll = $pollRepo->find(1);

echo $poll;

$questions = $poll->getQuestions();

$questions->remove(1);

$entityManager->flush();

echo "\nPerformances:\n";
echo "Nombre de requêtes : ", count($logger->queries), "\n";

foreach ($logger->queries as $query) {
    echo "- ", json_encode($query), "\n";
}
Poll (id: 1, title: Doctrine 2, ça vous dit ?, created: 2017-03-03T08:00:00+0000)

Performances:
Nombre de requêtes : 4
- {"sql":"SELECT t0.id AS id_1, t0.title AS title_2, t0.created AS created_3 FROM polls t0 WHERE t0.id = ?","params":[1],"types":["integer"],"executionMS":0.1620090007782}
- {"sql":"SELECT t0.id AS id_1, t0.wording AS wording_2, t0.poll_id AS poll_id_3 FROM questions t0 WHERE t0.poll_id = ?","params":[1],"types":["integer"],"executionMS":0.10100603103638}
- {"sql":"\"START TRANSACTION\"","params":null,"types":null,"executionMS":0.016000986099243}
- {"sql":"\"COMMIT\"","params":null,"types":null,"executionMS":0}
[Finished in 0.6s]

Aucune requête de mise à jour n’a été exécutée.

En réalité, dans toutes les relations, Doctrine ne vérifie les changements que d’un seul côté pour mettre à jour la relation. Ce côté responsable de la relation est appelé l'owning side. Réciproquement, l’autre côté de la relation est appelé l'inverse side.

Identification de l'owning side

Comment idenfier l'owning side d’une relation ?

Selon le type de relation bidirectionnelle utilisé, l'owning side peut être soit imposé par Doctrine soit choisi par nous-même.

Pour une relation ManyToOne - OneToMany, l'owning side sera toujours l’entité contenant l’annotation ManyToOne. Dans ce cas-ci, Doctrine impose le choix.

Par contre, pour les relations OneToOne et ManyToMany bidirectionnelles, nous devons nous-même choisir l'owning side en spécifiant l’attribut inversedBy.

Ainsi pour supprimer une question d’un sondage, nous devons modifier la relation en nous basant sur l’entité question qui est l'owning side de la relation car elle contient l’annotation ManyToOne.

La suppression ressemblerait donc à :

<?php
# owning-side-inverse-side.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);
$logger = $entityManager->getConfiguration()->getSQLLogger();

use Tuto\Entity\Question;

$questionRepo = $entityManager->getRepository(Question::class);

$question = $questionRepo->find(2);

echo $question;

$question->setPoll(null);

$entityManager->flush();

echo "\nPerformances:\n";
echo "Nombre de requêtes : ", count($logger->queries), "\n";

foreach ($logger->queries as $query) {
    echo "- ", json_encode($query), "\n";
}
Question (id: 2, wording: Doctrine 2 est-il un bon ORM ?)

Performances:
Nombre de requêtes : 4
- {"sql":"SELECT t0.id AS id_1, t0.wording AS wording_2, t0.poll_id AS poll_id_3 FROM questions t0 WHERE t0.id = ?","params":[2],"types":["integer"],"executionMS":0.014001131057739}
- {"sql":"\"START TRANSACTION\"","params":null,"types":null,"executionMS":0.0010001659393311}
- {"sql":"UPDATE questions SET poll_id = ? WHERE id = ?","params":[null,2],"types":["integer","integer"],"executionMS":0.35101985931396}
- {"sql":"\"COMMIT\"","params":null,"types":null,"executionMS":0.26401495933533}
[Finished in 1.2s]

Notez la présence de la requête UPDATE questions SET poll_id = ? WHERE id = ? qui permet ainsi de dissocier la question au sondage.

Contraintes

Ce mode de fonctionnement interne de Doctrine introduit quelques contraintes d’utilisation de l’ORM. Dans toutes les relations unidirectionnelles, seul l'owning side doit être configuré.

Ainsi, pour une relation 1..n unidirectionnelle, seul l’annotation ManyToOne doit être configurée.

En d’autres termes, l’annotation OneToMany ne peut être utilisée toute seule. À chaque annotation OneToMany, il doit y avoir une annotation ManyToOne correspondante.

Dans la même logique, pour une relation 1..1 unidirectionnelle, une seule entité contient l’annotation OneToOne. Doctrine créera forcément la clé étrangère dans cette entité.

C’est le cas pour notre relation entre un utilisateur et une adresse. La clé étrangère est dans la table users car l’entité utilisateur est l'owning side.

De manière générale, en plus d’être complexes, les relations bidirectionnelles sont souvent une source de problèmes de performance. Avant de les utiliser, il faut toujours évaluer la réelle nécessité d’une telle configuration.

Les événements Doctrine

Les types d’événements

Pour nous permettre d’agir sur les entités durant leur cycle de vie, l'entity manager de Doctrine génère des événements pour chaque opération qu’il effectue sur une entité. Ainsi, nous avons à notre disposition un ensemble d’événements et nous pouvons citer entre autres :

Événement Description
preRemove L’événement déclenché avant que l'entity manager supprime l' entité.
postRemove L’événement déclenché après la suppression effective de l’entité.
prePersist L’événement déclenché avant que l'entity manager sauvegarde l' entité.
postPersist L’événement déclenché après la sauvegarde effective de l’entité.
preUpdate L’événement déclenché avant la mise à jour de l’entité.
postUpdate L’événement déclenché après la mise à jour effective de l’entité.
postLoad L’événement déclenché une fois qu’une entité a été chargée depuis la base de données (ou après l’appel de la méthode refresh de l'entity manager).
preFlush L’événement déclenché juste avant la mise à jour effective de l’entité.
postFlush L’événement déclenché après la mise à jour effective de l’entité.

Ces événements ne sont pas déclenchés pour les requêtes DQL.

Les méthodes de rappel (callbacks)

Nous pouvons modifier la gestion de nos entités en nous basant sur ces événements grâce aux fonctions de rappels. Ces fonctions seront appelées automatiquement par Doctrine.

Pour la création d’un sondage, nous avions dû rajouter la date de création manuellement. Avec les événements, nous pouvons avoir une méthode simple qui modifie la date de création lorsque l’événement prePersist est déclenché.

Pour cela, nous devons d’abord spécifier que l’entité sondage utilise les événements de Doctrine avec l’annotation HasLifecycleCallbacks. Ensuite, nous pourrons utiliser les annotations pour désigner les méthodes qui seront appelées si un événement spécifique est déclenché. Ainsi pour l’entité sondage, nous aurons :

<?php
# src/Entity/Poll.php

namespace Tuto\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="polls")
* @ORM\HasLifecycleCallbacks
*/
class Poll
{
    // ...

    /**
    * @ORM\PrePersist
    */
    public function prePersist()
    {
        $this->created = new \Datetime();
    }
}
<?php
# create-poll-prepersist.php

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

use Tuto\Entity\Poll;

$poll = new Poll();

$poll->setTitle("Les événements Doctrine 2");

$entityManager->persist($poll);

$entityManager->flush();

echo $poll;

Notre méthode prePersist est appelée automatiquement par Doctrine et le sondage a ainsi une date de création.

Poll (id: 2, title: Les événements Doctrine 2, created: 2017-01-22T22:03:02+0000)

Ces événements permettent de personnaliser grandement le comportement de Doctrine. Ils sont d’ailleurs utilisés par beaucoup d’extensions pour gérer des problématiques courantes. N’hésitez donc pas les consulter pour approfondir le sujet (Atlantic18/DoctrineExtensions).

Le système d’événement peut même être poussé encore plus loin en créant par exemple nos propres événements, des classes spécialisées pour gérer les événements, etc. La documentation officielle présente bien ces sujets.


L’objectif de ce chapitre annexe est d’expliciter au mieux le fonctionnement de Doctrine et d’aborder quelques sujets intéressants que nous n’avions pas pu introduire tout au long de ce cours.

Vous pouvez vous-y référer pour éclaircir rapidement certains points mais il faut garder en tête qu’il n’est pas conçu pour être complet à 100 %.