Programmation PHP avec Symfony/Doctrine
Installation.
 est l'ORM par défaut de Symfony. Il utilise . Son langage PHP traduit en SQL est appelé DQL, et utilise le principe de la chaîne de responsabilité.
Installation en SF4 :
 composer require symfony/orm-pack
 composer require symfony/maker-bundle --dev
Renseigner l'accès au SGBD dans le .env :
 DATABASE_URL="mysql://mon_login:mon_mot_de_passe@127.0.0.1:3306/ma_base"
Ensuite la base de données doit être créée avec :
 php bin/console doctrine:database:create
Commandes Doctrine.
Exemples de commandes :
 php bin/console dbal:run-sql "SELECT * FROM ma_table"
 php bin/console doctrine:query:sql "$(< mon_fichier.sql)"
Vider les caches :
 php bin/console doctrine:cache:clear-metadata
 php bin/console doctrine:cache:clear-query 
 php bin/console doctrine:cache:clear-result
D'autres sont expliquées ci-dessous.
Entity.
Une entité est une classe PHP associée à une table de la base de données. Elle est composée d'un attribut par colonne, et de leurs s et s respectifs. Pour les générer à partir d'une base de données :
 php bin/console doctrine:mapping:import "App\Entity" annotation --path=src/Entity
Cette association est définie par des annotations Doctrine. Pour vérifier les annotations :
 php bin/console doctrine:schema:validate
Exemple.
Voici par exemple plusieurs types d'attributs :
class Word
 #[ORM\Id]
 #[ORM\Column(name: 'id', type: 'integer', nullable: false, options=['unsigned'])]
 #[ORM\GeneratedValue(strategy: 'IDENTITY')]
 private $id;
 #[ORM\Column(name: 'spelling', type: 'string', nullable: false)]
 private $spelling;
 #[ORM\Column(name: 'pronunciation', type: 'string', nullable: true)]
 private $pronunciation;
 #[ORM\ManyToOne(targetEntity: 'Language')]
 #[ORM\JoinColumn(name: 'language_id', referencedColumnName: 'id', nullable: false)]
 private ?Language $language = null;
 #[ORM\OneToMany(targetEntity: Homophon::class, mappedBy: 'word', cascade: ['persist', 'remove'], orphanRemoval: true)]
 private ArrayCollection $homophons;
 public function __construct()
 $this->homophons = new ArrayCollection();
 public function setSpelling($p)
 $this->spelling = $p;
 return $this;
 public function getSpelling()
 return $this->spelling;
 public function setPronunciation($p)
 $this->pronunciation = $p;
 return $this;
 public function getPronunciation()
 return $this->pronunciation;
 public function setLanguage($l)
 $this->language = $l;
 return $this;
 public function getLanguage()
 return $this->language;
 public function addHomophons($homophon)
 if (!$this->homophons->contains($homophon)) {
 $this->homophons->add($homophon);
 $homophon->setWord($this);
 return $this;
On utilise les annotations au lieu des attributs :
class Word
 /**
 * @ORM\Id
 * @ORM\Column(name="id", type="integer", nullable=false, options={"unsigned": true})
 * @ORM\GeneratedValue(strategy="IDENTITY")
 private $id;
 * @ORM\Column(name="spelling", type="string", length=255, nullable=false)
 private $spelling;
 * @ORM\Column(name="pronunciation", type="string", length=255, nullable=true)
 private $pronunciation;
 * @var Language
 * @ORM\ManyToOne(targetEntity="Language", inversedBy="words")
 * @ORM\JoinColumn(name="language_id", referencedColumnName="id")
 protected $language;
 * @var ArrayCollection
 * @ORM\OneToMany(targetEntity="Homophon", mappedBy="word", cascade={"persist", "remove"})
 private $homophons;
On voit ici que la table "word" possède trois champs : "id" (clé primaire), "pronunciation" (chaine de caractère) et "language_id" (clé étrangère vers la table "language"). Doctrine stockera automatiquement l'id de la table "language" dans la troisième colonne quand on associera une entité "Language" à une "Word" avec codice_1.
Le quatrième attribut permet juste de récupérer les enregistrements de la table "homophon" ayant une clé étrangère pointant vers "word".
Par ailleurs, en relation "OneToMany", c'est toujours l'entité ciblée par le "Many" qui définit la relation car elle contient la clé étrangère. Elle contient donc l'attribut "inversedBy=", alors que celle ciblée par "One" contient "mappedBy=". Elle contient aussi une deuxième annotation codice_2 mentionnant la clé étrangère en base de données (et pas en PHP).
NB : par défaut la longueur des types "string" est 255, on peut l'écraser ou la retirer avec codice_3. Le type "text" par contre n'a pas de limite.
L'annotation codice_4 peut servir à définir des clés composites :
 * @ORM\Table(uniqueConstraints={
 * @ORM\UniqueConstraint(name="spelling-pronunciation", columns={"spelling", "pronunciation"})
ArrayCollection.
Cet objet itérable peut être converti en tableau avec ->toArray().
Pour le trier :
 $sort = new Criteria(null, ['slug' => Criteria::ASC]);
 $services = $maCollection->matching($sort);
GeneratedValue.
L'annotation "GeneratedValue" peut valoir "AUTO", "SEQUENCE", "TABLE", "IDENTITY", "NONE", "UUID", "CUSTOM".
Triggers.
Les opérations en cascade sont définies sous deux formes d'annotations :
Concepts avancés.
Pour utiliser une entité depuis une autre, alors qu'elles n'ont pas de liaison SQL, il existe l'interface ObjectManagerAware.
L'autojointure est appelé "self-referencing association mapping" par Doctrine).
Héritage.
Une entité peut hériter d'une classe si celle-ci contient l'annotation suivante :
/** @MappedSuperclass */
class MyEntityParent
EntityManager.
L'EntityManager (em) est l'objet qui synchronise les entités avec la base de données. Une application doit en avoir un par base de données, définis dans doctrine.yaml.
Il possède trois méthodes pour cela :
Il existe aussi les méthodes suivantes :
 $entity = new MyEntity();
 $em->persist($entity);
 $em->flush($entity);
 // Trigger SGBD déclenché ici en parallèle
 $em->refresh($entity);
 $entity->setCreatedDate($createdDate);
 $em->flush($entity);
Repository.
On appelle "repository" les classes PHP qui contiennent les requêtes pour la base de données. Elles héritent de codice_9. Chacune permet de récupérer une entité associée en base de données. Les repo doivent donc être nommés "NomDeLEntitéRepository".
SQL.
Depuis Doctrine.
$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('call my_stored_procedure', $rsm)->getResult();
Sans Doctrine.
Pour exécuter du SQL natif dans Symfony sans Doctrine, il faut créer un service de connexion, par exemple qui appelle PDO en utilisant les identifiants du .env, puis l'injecter dans les repos (dans chaque constructeur ou par une classe mère commune) : 
return $this->connection->fetchAll($sql);
Depuis un repository Doctrine, tout ceci est déjà fait et les deux techniques sont disponibles :
1. Par l'attribut "entity manager" ("em", ou "_em" pour les anciennes versions) hérité de la classe mère (le "use" permettra ici d'appeler des constantes pour paramétrer le résultat) :
use Doctrine\DBAL\Connection;
$statement = $this->_em->getConnection()->executeQuery($sql);
$statement->fetchAll(\PDO::FETCH_KEY_PAIR);
$statement->closeCursor();
$this->_em->getConnection()->close();
return $statement;
2. En injectant le service de connexion dans le constructeur (codice_10) :
use Doctrine\DBAL\Connection;
return $this->dbalConnection->fetchAll($sql);
DQL.
L'avantage par rapport au SQL est que la requête renvoyée contient des objets PHP, instances des entités.
Constructeur.
<?php
namespace App\Repository;
use App\Entity\Word;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class WordRepository extends ServiceEntityRepository
 public function __construct(ManagerRegistry $registry)
 parent::__construct($registry, Word::class);
Méthodes magiques.
Doctrine peut ensuite générer des requêtes SQL à partir du nom d'une méthode PHP appelée mais non écrite dans les repository (car ils en héritent). Ex :
Par ailleurs, on peut compléter les requêtes avec des paramètres supplémentaires. Ex :
$repo->findBy(
 ['lastname' => $lastname], // where
 ['lastname' => 'ASC'], // order by
 10, // limit
 0, // offset
Mais cela reste plus limité que le DQL ou SQL, car par exemple on ne peut pas demander une recherche des enregistrements contenant un champ différent d'une valeur.
Requête.
Si les méthodes magiques ne suffisent pas, on peut créer sa propre méthode dans le repository de l'entité, avec des requêtes DQL plus complexes. Ex :
 $query = $em->createQuery("
 SELECT u.id, c.id
 FROM
 App\Entity\User u
 LEFT JOIN App\Entity\Company c WITH IDENTITY(u.company) = c.id 
 WHERE
 u.id < :maxId
 ORDER BY u.id DESC
 ->setParameter('maxId', 3);
 return $query->getResult();
Fonctions.
Voici quelques fonctions DQL :
D'autres sont ajoutables dans doctrine.yaml.
QueryBuilder.
Doctrine permet aussi d'écrire des requêtes DQL en syntaxe de programmation objet. Les méthodes des repos font alors appel à codice_22 :
public function findAllWithCalculus(): ?array
 return $this->createQueryBuilder('mon_entité')
 ->where('id < 3')
 ->getQuery()
 ->getResult()
Pour éviter le codice_23 dans cet exemple, on peut y ajouter la méthode codice_24.
Pour afficher la requête SQL générée par le DQL, remplacer "->getResult()" par "->getQuery()".
Jointures.
Quand deux entités sont reliées entre elle, on peut utiliser une méthode pour définir leur jointure (inner, left ou right). Par exemple pour des utilisateurs avec leurs sociétés :
 return $this->createQueryBuilder('u')
 ->innerJoin('u.company', 'c')
 ->addSelect('c')
 ->where('u.isActive = true')
 ->andWhere('c.isClosed = false')
 ->orderBy('u.lastName', 'ASC')
 ->addOrderBy('c.firstName', 'ASC')
On peut même préciser les champs à sélectionner et la clé de jointure :
 return $this->createQueryBuilder('u')
 ->select(['u.lastName as name', 'u.company'])
 ->innerJoin('u.company', 'c', 'u.company = c.id')
 ->addSelect(['c.id', 'c.name'])
 ->where('u.isActive = true')
 ->andWhere('c.isClosed = false')
 ->orderBy('u.lastName', 'ASC')
 ->addOrderBy('c.firstName', 'ASC')
Quand deux entités ne sont pas reliées entre elles, on peut tout de même lancer une jointure en DQL :
use Doctrine\ORM\Query\Expr\Join;
 ->join('AcmeCategoryBundle:Category', 'c', Expr\Join::WITH, 'v.id = c.id')
Résultats.
Doctrine renvoie des objets avec leurs méthodes (get pas set) avec getResult, ou un tableau avec getArrayResult, ou 2D avec getScalar.
Cache.
Doctrine propose trois caches pour ses requêtes : celui de métadonnées, de requête et de résultats. Il faut d'abord définir les pools dans cache.yaml :
framework:
 cache:
 pools:
 doctrine.metadata_cache_pool:
 adapter: cache.system
 doctrine.query_cache_pool:
 adapter: cache.system
 doctrine.result_cache_pool:
 adapter: cache.app
Puis dans doctrine.yaml, les utiliser :
doctrine:
 orm:
 metadata_cache_driver:
 type: pool
 pool: doctrine.metadata_cache_pool
 query_cache_driver:
 type: pool
 pool: doctrine.query_cache_pool
 result_cache_driver:
 type: pool
 pool: doctrine.result_cache_pool
À partir de là le cache des métadonnées est utilisé partout.
Par contre pour ceux de requêtes et de résultats, il faut les définir pour chaque query builder de chaque repository :
$query
 ->useQueryCache($hasQueryCache)
 ->setQueryCacheLifetime($lifetime)
 ->enableResultCache($lifetime)
Dans cet exemple, on n'utilise pas cache.system pour le cache de résultats pour ne pas saturer le serveur qui héberge le code. cache.app pointe donc vers une autre machine, par exemple Redis, ce qui nécessite un appel réseau supplémentaire, et n'améliore donc pas forcément les performances selon la requête.
Expressions.
Pour ajouter une expression en DQL, utilise codice_27. Ex :
Injection de dépendances.
Les repository DQL deoivent "ServiceEntityRepository" :
namespace App\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
class WordRepository extends ServiceEntityRepository
 public function __construct(ManagerRegistry $registry)
 parent::__construct($registry, Word::class);
Mais parfois on souhaite injecter un service dans un repository. Pour ce faire il y a plusieurs solutions :
Transactions.
Pour garantir d'intégrité d'une transaction :
 $connection = $this->entityManager->getConnection();
 $connection->beginTransaction();
 try {
 $this->persist($myEntity);
 $this->flush();
 $connection->commit();
 } catch (Exception $e) {
 $connection->rollBack();
 throw $e;
Il existe aussi une syntaxe alternative :
 $em->transactional(function($em, $myEntity) {
 $em->persist($myEntity);
Évènements.
Pour ajouter des triggers sur la mise à jour d'une table, ajouter dans son entité :
 #[Entity, HasLifecycleCallbacks]
Ou en PHP < 8 :
 @ORM\HasLifecycleCallbacks()
Voici les évènements utilisables ensuite :
prePersist.
S'exécute avant la persistance d'une entité (son INSERT en SQL).
postPersist.
Après la persistance d'une entité.
preUpdate.
Avant la mise à jour d'une entité (son UPDATE en SQL).
postUpdate.
Après la mise à jour d'une entité.
preFlush.
Avant la sauvegarde d'une entité.
onFlush.
Pendant la sauvegarde d'une entité.
postFlush.
Après la sauvegarde d'une entité.
preRemove.
Avant la suppression d'une entité (son DELETE en SQL).
postRemove.
Après la suppression d'une entité.
Migrations.
Pour modifier la base de données avec une commande, par exemple pour ajouter une colonne à une table ou modifier une procédure stockée, il existe une bibliothèque qui s'installe comme suit :
composer require doctrine/doctrine-migrations-bundle
Création.
Ensuite, on peut créer un squelette de "migration" :
php bin/console doctrine:migrations:generate
Cette classe comporte une méthode "up()" qui réalise la modification en SQL ou DQL, et une "down()" censée faire l'inverse à des fins de rollback. De plus, on ne peut pas lancer deux fois de suite le "up()" sans un "down()" entre les deux (une table nommée codice_34 enregistre leur succession).
Exemple SQL.
final class Version20210719125146 extends AbstractMigration
 public function up(Schema $schema) : void
 $this->connection->fetchAll('SHOW DATABASES;');
 $this->addSql(«<SQL
 CREATE TABLE ma_table(ma_colonne VARCHAR(255) NOT NULL);
SQL);
 public function down(Schema $schema) : void
 $this->addSql('DROP TABLE ma_table');
Exemple DQL.
final class Version20210719125146 extends AbstractMigration
 public function up(Schema $schema) : void
 $table = $schema->createTable('ma_table');
 $table->addColumn('ma_colonne', 'string');
 public function down(Schema $schema) : void
 $schema->dropTable('ma_table');
Exemple PHP.
final class Version20210719125146 extends AbstractMigration implements ContainerAwareInterface
 use ContainerAwareTrait;
 public function up(Schema $schema) : void
 $em = $this->container->get('doctrine.orm.entity_manager');
 $monEntite = new MonEntite();
 $em->persist($monEntite);
 $em->flush();
Exécution.
La commande suivante exécute toutes les migrations qui n'ont pas encore été lancées dans une base :
php bin/console doctrine:migrations:migrate
Sinon, on peut les exécuter une par une selon le paramètre, avec la partie variable du nom du fichier de la classe (timestamp) :
php bin/console doctrine:migrations:execute --up 20170321095644
php bin/console doctrine:migrations:execute --up "App\Migrations\Version20170321095644"
php bin/console doctrine:migrations:execute --up App\\Migrations\\Version20170321095644
Pour le rollback :
php bin/console doctrine:migrations:execute --down 20170321095644
Pour éviter que Doctrine pose des questions durant les migrations, ajouter codice_35 (ou codice_36).
Pour voir le code SQL au lieu de l'exécuter : codice_37.
Sur plusieurs bases de données.
Pour exécuter sur plusieurs bases :
 php bin/console doctrine:migrations:migrate --em=em1 --configuration=src/DoctrineMigrations/Base1/migrations.yaml
 php bin/console doctrine:migrations:migrate --em=em2 --configuration=src/DoctrineMigrations/Base2/migrations.yaml
Avec des migrations.yaml de type :
name: 'Doctrine Migrations base 1'
migrations_namespace: 'App\DoctrineMigrations\Base1'
migrations_directory: 'src/DoctrineMigrations/Base1'
table_name: 'migration_versions'
Synchronisation.
Vers le code.
Vers les entités.
 php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity
Vers les migrations.
Pour créer la migration permettant de parvenir à la base de données actuelle :
 php bin/console doctrine:migrations:diff
Vers la base.
À contrario, pour mettre à jour la BDD à partir des entités :
 php bin/console doctrine:schema:update --force
Pour le prévoir dans une migration :
 php bin/console doctrine:schema:update --dump-sql
Fixtures.
Il existe plusieurs bibliothèques pour créer des s, dont une de Doctrine :
composer require --dev orm-fixtures
Pour charger les fixtures du code dans la base :
php bin/console doctrine:fixtures:load -n
Types de champ.
La liste des types de champ Doctrine se trouve dans codice_38. Toutefois, il est possible d'en créer des nouveaux pour définir des comportements particuliers quand on lit ou écrit en base.
Par exemple on peut étendre codice_39 pour surcharger le type JSON par défaut afin de lui faire faire codice_40 automatiquement.
Ou encore, pour y stocker du code de configuration désérialisé dans une colonne.