Programmation PHP avec Symfony/Doctrine
Installation
[modifier | modifier le wikicode]Doctrine est l'ORM par défaut de Symfony. Il utilise PDO. Son langage PHP traduit en SQL est appelé DQL, et utilise le principe de la chaîne de responsabilité.
Installation en SF4[1] :
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
- doctrine/doctrine-bundle
- doctrine/doctrine-migrations-bundle
- doctrine/orm
- symfony/proxy-manager-bridge
Commandes Doctrine
[modifier | modifier le wikicode]Exemples de commandes :
php bin/console doctrine:query:sql "SELECT * FROM ma_table" php bin/console doctrine:query:sql "$(< mon_fichier.sql)" # Ces deux commandes sont équivalentes des précédentes php bin/console dbal:run-sql "SELECT * FROM ma_table" php bin/console dbal:run-sql "$(< mon_fichier.sql)" php bin/console doctrine:cache:clear-metadata php bin/console doctrine:cache:clear-query php bin/console doctrine:cache:clear-result
Entity
[modifier | modifier le wikicode]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 getters et setters respectifs. Pour en générer une :
php bin/console generate:doctrine:entity
Cette association est définie par des annotations Doctrine. Pour vérifier les annotations :
php bin/console doctrine:schema:validate
Exemple
[modifier | modifier le wikicode]Voici par exemple plusieurs types d'attributs :
#[ORM\Table(name: 'word')]
#[ORM\Entity(repositoryClass: WordRepository::class)]
class Word
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(name: 'id', type: 'integer', nullable: false)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: 'Language')]
#[ORM\JoinColumn(name: 'language_id', referencedColumnName: 'id', nullable: false)]
private ?Language $language = null;
#[ORM\Column(name: 'spelling', type: 'string', nullable: false)]
private ?string $spelling = null;
#[ORM\Column(name: 'pronunciation', type: 'string', nullable: true)]
private ?string $pronunciation = null;
#[ORM\OneToMany(targetEntity: 'Homophon', cascade: ['persist', 'remove'])]
private ?Collection $homophons;
Format avant PHP 8
/**
* @ORM\Table(name="word")
* @ORM\Entity(repositoryClass="App\Repository\WordRepository")
*/
class Word
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer", nullable=false)
* @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;
Et leurs modificateurs (getters et setters) :
public function __construct()
{
$this->homophons = new ArrayCollection();
}
public function setSpelling($p): self
{
$this->spelling = $p;
return $this;
}
public function getSpelling(): ?string
{
return $this->spelling;
}
public function setPronunciation($p): self
{
$this->pronunciation = $p;
return $this;
}
public function getPronunciation(): ?string
{
return $this->pronunciation;
}
public function setLanguage($l): self
{
$this->language = $l;
return $this;
}
public function getLanguage(): ?Language
{
return $this->language;
}
public function addHomophons($homophon): self
{
if (!$this->homophons->contains($homophon)) {
$this->homophons->add($homophon);
$homophon->setWord($this);
}
return $this;
}
}
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 $word->setLanguage($language)
.
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 @ORM\JoinColumn
mentionnant la clé étrangère en base de données (et pas en PHP).
Dans les relations *toMany :
- il faut initialiser l'attribut dans le constructeur en
ArrayCollection()
. - on peut avoir une méthode ->set(ArrayCollection) mais le plus souvent on utilise ->add(un seul élément)
- cette méthode add() doit idéalement contenir le set() de l'entité cible vers la courante (pour ne pas avoir à l'ajouter après chaque appel).
NB : par défaut la longueur des types "string" est 255, on peut l'écraser ou la retirer avec length=0
[2]. Le type "text" par contre n'a pas de limite.
#[ORM\Table(name: 'word')]
(anciennement l'annotation @ORM\Table(name="word")
) était facultative dans cet exemple, car le nom de la table peut être déduit du nom de l'entité.L'annotation code>@ORM\Table peut servir à définir des clés composites :
* @ORM\Table(uniqueConstraints={
* @ORM\UniqueConstraint(name="spelling-pronunciation", columns={"spelling", "pronunciation"})
* })
ArrayCollection
[modifier | modifier le wikicode]Cet objet itérable peut être converti en tableau avec ->toArray().
Pour le trier :
- Dans une entité :
@ORM\OrderBy({"sort_order" = "ASC"})
- Sinon, instancier un critère :
$sort = new Criteria(null, ['slug' => Criteria::ASC]);
$services = $maCollection->matching($sort);
GeneratedValue
[modifier | modifier le wikicode]L'annotation GeneratedValue peut valoir "AUTO", "SEQUENCE", "TABLE", "IDENTITY", "NONE", "UUID", "CUSTOM".
Dans le cas du CUSTOM, un setId() réaliser avant le persist() sera écrasé par la génération d'un nouvel ID[3]. Ce nouvel ID peut être écrasé à son tour, mais si l'entité possède des liens vers d'autres, c'est l'ID custom qui est utilisé comme clé (on a alors une erreur Integrity constraint violation puisque la clé générée n'est pas retenue). Pour éviter cela (par exemple dans des tests automatiques), il faut désactiver la génération à la volée :
$metadata = $this->em->getClassMetadata(get_class($entity));
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
$metadata->setIdGenerator(new AssignedGenerator());
$entity->setId(static::TEST_ID);
Triggers
[modifier | modifier le wikicode]Les opérations en cascade sont définies sous deux formes d'attributs :
#[ORM\OneToMany(cascade: ['persist', 'remove'])]
: au niveau ORM.#[ORM\JoinColumn(onDelete: 'CASCADE')]
: au niveau base de données.
Ainsi, quand on supprime l'entité contenant un cascade remove, cela supprime aussi ses entités liées par cette relation.
Concepts avancés
[modifier | modifier le wikicode]Pour utiliser une entité depuis une autre, alors qu'elles n'ont pas de liaison SQL, il existe l'interface ObjectManagerAware[4].
Les types des attributs peuvent être quelque peu différents du SGBD[5].
Dans le cas de jointure vers une entité d'un autre espace de nom (par exemple une table d'une autre base), il faut indiquer son namespace complet dans l'annotation Doctrine (car elle ne tient pas compte des "use").
L'autojointure est appelé self-referencing association mapping par Doctrine[6]).
Héritage
[modifier | modifier le wikicode]Une entité peut hériter d'une classe si celle-ci contient l'annotation suivante[7] :
/** @MappedSuperclass */
class MyEntityParent
...
EntityManager
[modifier | modifier le wikicode]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 :
- persist() : prépare un INSERT SQL (rattache une entité à un entity manager).
- remove() : prépare un DELETE SQL.
- flush() : exécute le code SQL préparé.
Il existe aussi les méthodes suivantes :
- merge() : fusionne une entité absent de l'em dedans.
- refresh() : rafraichit l'entité PHP à partir de la base de données. C'est utile par exemple pour tenir compte des résultats d'un trigger after insert sur le SGBD. Exemple si le trigger ajoute une date de création après le persist, à écraser par
$createdDate
:
$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
[modifier | modifier le wikicode]On appelle "repository" les classes PHP qui contiennent les requêtes pour la base de données. Elles héritent de Doctrine\ORM\EntityRepository
. Chacune permet de récupérer une entité associée en base de données. Les repo doivent donc être nommés NomDeLEntitéRepository.
CarFactory
fera un new Car()
mais aussi créera et lui associera ses composants : new Motor()
...@ORM\Entity(repositoryClass="App\Repository\WordRepository")
SQL
[modifier | modifier le wikicode]Depuis Doctrine
[modifier | modifier le wikicode]$rsm = new ResultSetMapping();
$this->_em->createNativeQuery('call my_stored_procedure', $rsm)->getResult();
Sans Doctrine
[modifier | modifier le wikicode]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 ('@database_connection'
) :
use Doctrine\DBAL\Connection;
...
return $this->dbalConnection->fetchAll($sql);
DQL
[modifier | modifier le wikicode]Méthodes magiques
[modifier | modifier le wikicode]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 :
$repo->find($id)
: cherche par la clé primaire définie dans l'entité.$repo->findAll()
: récupère tous les enregistrements (sans clauseWHERE
).$repo->findById($id)
: engendre automatiquement unSELECT * WHERE id = $id
dans la table associée au repo.$repo->findBy(['lastname' => $lastname, 'firstname' => $firstname])
engendre automatiquement unSELECT * WHERE lastname = $lastname AND firstname = $firstname
.$repo->findOneById($id)
: engendre automatiquement unSELECT * WHERE id = $id LIMIT 1
.$repo->findOneBy(['lastname' => $lastname, 'firstname' => $firstname])
: engendre automatiquement unSELECT * WHERE lastname = $lastname AND firstname = $firstname LIMIT 1
.
Lors des tests unitaires PHPUnit, il est probable qu'une erreur survienne sur l'inexistence de méthode "findById
" pour le mock du repository (du fait qu'elle est magique). Il vaut donc mieux utiliser findBy()
.
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
);
createQuery
[modifier | modifier le wikicode]DQL possède une syntaxe proche du SQL, si ce n'est qu'il faut convertir les entités jointes en ID avec IDENTITY()
pour les jointures. Ex :
public function findComplicatedStuff() { $em = $this->getEntityManager(); $query = $em->createQuery(" SELECT u.last_name, u.first_name FROM App\Entity\Users u INNER JOIN App\Entity\Invoices i WITH u.id = IDENTITY(i.users) WHERE i.status='waiting' "); return $query->getResult(); }
createQueryBuilder
[modifier | modifier le wikicode]L'autre syntaxe du DQL est en POO. Les méthodes des repos font appel createQueryBuilder()
:
public function findAllWithCalculus()
{
return $this->createQueryBuilder('mon_entité')
->where('id < 3')
->getQuery()
->getResult()
;
}
Pour éviter le SELECT *
dans cet exemple, on peut y ajouter la méthode ->select()
.
Pour afficher la requête SQL générée par le DQL, remplacer "->getResult()" par "->getQuery()".
Jointures
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Doctrine peut renvoyer avec :
getResult()
: un objet ArrayCollection (iterable, pour rechercher dedans :->contains()
), d'objets (du type de l'entité) avec leurs méthodes get (pas set) ;getArrayResult()
ougetScalarResult()
: un tableau de tableaux (entité normalisée) ;getSingleColumnResult()
: un tableau unidimensionnel.
Cache
[modifier | modifier le wikicode]Configuration globale
[modifier | modifier le wikicode]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.
Configuration par entité
[modifier | modifier le wikicode]Par contre pour ceux de requêtes et de résultats, il faut les définir pour chaque entité, soit :
- Dans l'entité, avec une annotation
@ORM\Cache(usage="READ_ONLY", region="write_rare")
[8], utilisant la configuration doctrine.yaml :
doctrine: orm: second_level_cache: enabled: true regions: write_rare: lifetime: 864000 cache_driver: { type: service, id: cache.app }
- Dans le 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
[modifier | modifier le wikicode]Pour ajouter une expression en DQL, utilise $qb->expr()
. Ex[9] :
$qb->expr()->count('u.id')
$qb->expr()->between('u.id', 2, 10)
(entre 2 et 10)$qb->expr()->gte('u.id', 2)
(plus grand ou égal à 2)$qb->expr()->like('u.name', '%son')
$qb->expr()->lower('u.name')
$qb->expr()->substring('u.name', 0, 1)
Injection de dépendances
[modifier | modifier le wikicode]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 :
- Étendre une classe qui étend ServiceEntityRepository.
- Le redéfinir dans services.yaml.
- Utiliser un trait.
Transactions
[modifier | modifier le wikicode]Pour garantir d'intégrité d'une transaction[10] :
$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
[modifier | modifier le wikicode]Pour ajouter des triggers sur la mise à jour d'une table, ajouter dans son entité l'attribut #[ORM\HasLifecycleCallbacks]
(anciennement l'annotation @ORM\HasLifecycleCallbacks()
). Voici les évènements utilisables ensuite (dans les listeners / subscribers) :
prePersist
[modifier | modifier le wikicode]Se produit avant la persistance d'une entité (paramètre : PrePersistEventArgs $args
).
postPersist
[modifier | modifier le wikicode]Se produit après la persistance d'une entité (PostPersistEventArgs $args
).
preUpdate
[modifier | modifier le wikicode]Se produit avant l'update d'une entité (PreUpdateEventArgs $args
).
postUpdate
[modifier | modifier le wikicode]Se produit après l'update d'une entité (PostUpdateEventArgs $args
).
preRemove
[modifier | modifier le wikicode]Se produit avant l'update d'une entité (PreRemoveEventArgs $args
).
postRemove
[modifier | modifier le wikicode]Se produit après l'update d'une entité (PostRemoveEventArgs $args
).
preFlush
[modifier | modifier le wikicode]Se produit avant la sauvegarde d'une entité (PreFlushEventArgs $args
).
- Dans cet évènement, les attributs en lazy loading de l'entité flushée s'ils sont appelés, sont issus de la base de données et donc correspondent aux données écrasées (et pas aux nouvelles flushées).
- Si on flush l'entité qui déclenche cet évènement il faut penser à un dispositif anti-boucle infinie (ex : variable d'instance).
- Dans le cas d'un new sur une entité, le persist ne suffit pas pour préparer sa sauvegarde. Il faut alors appeler
$unitOfWork->computeChangeSet($classMetadata, $entity)
[11].
Ex :
$uow = $em->getUnitOfWork(); $uow->computeChangeSets(); if ($uow->isEntityScheduled($myEntity)) { //... }
LifecycleEventArgs $args
dans ces fonctions.
Parfois le $object::class
peut renvoyer Proxies\__CG__\App\Entity\MyEntity
au lieu de App\Entity\MyEntity
, selon le cache utilisé.
postFlush
[modifier | modifier le wikicode]Se produit après la sauvegarde d'une entité (PostFlushEventArgs $args
).
Migrations
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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 migration_versions
enregistre leur succession).
Exemple SQL
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]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();
}
}
Cette technique est déconseillée car les entités peuvent évoluer indépendamment de la migration. Mais elle peut s'avérer utile pour stocker des données dépendantes de l'environnement.
$this->containergetParameter()
ne fonctionne pas sur la valeur du paramètre quand elle doit être remplacée par une variable d'environnement. Par exemple $_SERVER['SUBAPI_URI']
renvoie la variable d'environnement et $this->containergetParameter('env(SUBAPI_URI)')
sa valeur par défaut (définie dans services.yaml).
Exécution
[modifier | modifier le wikicode]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
# ou si "migrations_paths" dans doctrine_migrations.yaml contient le namespace :
php bin/console doctrine:migrations:execute --up "App\Migrations\Version20170321095644"
# ou encore :
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 --no-interaction
(ou -n
).
Pour voir le code SQL au lieu de l'exécuter : --write-sql
.
Sur plusieurs bases de données
[modifier | modifier le wikicode]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'
# custom_template: 'src/DoctrineMigrations/migration.tpl'
Synchronisation
[modifier | modifier le wikicode]Vers le code
[modifier | modifier le wikicode]Vers les entités
[modifier | modifier le wikicode]php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity
Ce script ne fonctionne pas avec les attributs PHP8. Donc pour créer une nouvelle entité à partir d'une table, utiliser un filtre et passer Rector pour convertir les annotations. Ex :
php bin/console doctrine:mapping:import App\\Entity annotation --path=src/Entity --filter=myNewTable vendor/bin/rector process src/Entity/MyNewEntity.php
Vers les migrations
[modifier | modifier le wikicode]Pour créer la migration permettant de parvenir à la base de données actuelle :
php bin/console doctrine:migrations:diff
Vers la base
[modifier | modifier le wikicode]À 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
[modifier | modifier le wikicode]Il existe plusieurs bibliothèques pour créer des fixtures, dont une de Doctrine[13] :
composer require --dev orm-fixtures
Pour charger les fixtures du code dans la base :
php bin/console doctrine:fixtures:load -n
Types de champ
[modifier | modifier le wikicode]La liste des types de champ Doctrine se trouve dans Doctrine\DBAL\Types
. 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 JsonType
pour surcharger le type JSON par défaut afin de lui faire faire json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
automatiquement.
Ou encore, pour y stocker du code de configuration désérialisé dans une colonne[14].
Réplication SQL
[modifier | modifier le wikicode]Anciennement appelée MasterSlaveConnection, la réplication entre une base de données accessible en écriture et ses réplicas accessibles en lecture par l'application, est prise en charge par Doctrine qui effectuera automatiquement les SELECT vers les réplicas pour soulager la base principale. Il suffit juste d'indiquer les adresses des réplicas dans doctrine.yml. Ex[15] :
doctrine: dbal: url: '%env(resolve:DATABASE_URL)%' server_version: '8.0.35' replicas: replica1: url: '%env(resolve:REPLICA_DATABASE_URL)%'
Critique
[modifier | modifier le wikicode]- Il faut revenir en SQL si les performances sont limites (ex : un million de lignes avec jointures) ou si on veut tronquer une table.
- Si les valeurs d'une table jointe n'apparaissent pas tout le temps, vérifier que le lazy loading est contourné par au choix :
- Avant l'appel null, un
ObjetJoint->get()
. - Dans l'entité, un
@ManyToOne(…, fetch="EAGER")
. - Dans le repository, un
$this->queryBuilder->addSelect()
.
- Avant l'appel null, un
- Pas de HAVING MAX car il n'est pas connu lors de la construction dans la chaine de responsabilité
- Pas de FULL OUTER JOIN ou RIGHT JOIN (que "leftJoin" et "innerJoin")
- Attention aux
$this->queryBuilder->setMaxResults()
et$this->queryBuilder->setFirstResult()
en cas de jointure, car elles ne conservent que le nombre d'enregistrements de la première table (à l'instar duLIMIT
SQL). La solution consiste à ajouter un paginateur[16]. - L'annotation @ORM/JOIN TABLE crée une table vide et ne permet pas d'y placer des fixtures lors de sa construction.
- Pas de hints.
- Bug des
UNION ALL
quand on joint deux entités non liées dans le repo.
Références
[modifier | modifier le wikicode]- ↑ https://symfony.com/doc/current/doctrine.html
- ↑ https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html#string
- ↑ https://stackoverflow.com/questions/31594338/overriding-default-identifier-generation-strategy-has-no-effect-on-associations
- ↑ https://www.doctrine-project.org/api/persistence/1.0/Doctrine/Common/Persistence/ObjectManagerAware.html
- ↑ https://www.doctrine-project.org/projects/doctrine-dbal/en/2.8/reference/types.html#mapping-matrix
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/association-mapping.html#many-to-many-self-referencing
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/reference/inheritance-mapping.html
- ↑ https://medium.com/@dotcom.software/using-doctrines-l2-cache-in-symfony-eba300ab1e6
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.12/reference/query-builder.html#the-expr-class
- ↑ https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html#approach-2-explicitly
- ↑ https://stackoverflow.com/questions/37831828/symfony-onflush-doctrine-listener
- ↑ https://stackoverflow.com/questions/10800178/how-to-check-if-entity-changed-in-doctrine-2
- ↑ https://symfony.com/doc/current/bundles/DoctrineFixturesBundle/index.html
- ↑ https://speakerdeck.com/lyrixx/doctrine-objet-type-et-colonne-json?slide=23
- ↑ https://medium.com/@dominykasmurauskas1/how-to-add-read-write-replicas-on-symfony-6-using-doctrine-bundle-a46447449f35
- ↑ https://stackoverflow.com/questions/50199102/setmaxresults-does-not-works-fine-when-doctrine-query-has-join/50203939