SlideShare une entreprise Scribd logo
1  sur  54
Télécharger pour lire hors ligne
Doctrine en dehors
des sentiers ba!us
Romaric Drigon,
Développeur et consultant à netinfluence
@romaricdrigon / romaric@netinfluence.ch
❗
= à utiliser avec précaution
SELECTQue se passe-t-il lorsque je requête une entité?
Une requête basique
$blog = $entityManager->getRepository(Blog::class)->find(1);
Une requête basique
$blog = $entityManager->getRepository(Blog::class)->find(1);
— Doctrine ORM regarde dans le mapping (ClassMetadata) quel est
l'identifiant (ID) de notre entité
Une requête basique
$blog = $entityManager->getRepository(Blog::class)->find(1);
— Doctrine ORM regarde dans le mapping (ClassMetadata) quel est
l'identifiant (ID) de notre entité
— dans le cas d'une requête par ID, il regarde dans l'identityMap si
l'entité n'a pas déjà été chargée
Une requête basique
$blog = $entityManager->getRepository(Blog::class)->find(1);
— Doctrine ORM regarde dans le mapping (ClassMetadata) quel est
l'identifiant (ID) de notre entité
— dans le cas d'une requête par ID, il regarde dans l'identityMap si
l'entité n'a pas déjà été chargée
— si non, Doctrine va générer une requête SQL
Une requête basique
$blog = $entityManager->getRepository(Blog::class)->find(1);
— Doctrine ORM regarde dans le mapping (ClassMetadata) quel est
l'identifiant (ID) de notre entité
— dans le cas d'une requête par ID, il regarde dans l'identityMap si
l'entité n'a pas déjà été chargée
— si non, Doctrine va générer une requête SQL
— Doctrine DBAL va l'exécuter
Une requête basique
$blog = $entityManager->getRepository(Blog::class)->find(1);
— Doctrine ORM regarde dans le mapping (ClassMetadata) quel est
l'identifiant (ID) de notre entité
— dans le cas d'une requête par ID, il regarde dans l'identityMap si
l'entité n'a pas déjà été chargée
— si non, Doctrine va générer une requête SQL
— Doctrine DBAL va l'exécuter
— Doctrine ORM va construire un objet à partir du résultat
(hydratation)
Une requête basique
$blog = $entityManager->getRepository(Blog::class)->find(1);
— Doctrine ORM regarde dans le mapping (ClassMetadata) quel est
l'identifiant (ID) de notre entité
— dans le cas d'une requête par ID, il regarde dans l'identityMap si
l'entité n'a pas déjà été chargée
— si non, Doctrine va générer une requête SQL
— Doctrine DBAL va l'exécuter
— Doctrine ORM va construire un objet à partir du résultat
(hydratation)
— elle sera ajoutée à l'identityMap, puis retournée
Changer l'hydratation
Plusieurs modes sont disponibles :
$result = query->getResult(Query::HYDRATE_OBJECT); // Blog, objet construit par Reflection
$result = query->getResult(Query::HYDRATE_ARRAY); // tableau associatif avec id, name...
$result = query->getResult(Query::HYDRATE_SCALAR); // tableaux avec b_id, b_name... (non-dédupliqué !)
$result = query->getResult(Query::HYDRATE_SINGLE_SCALAR); // non supporté ici
$result = query->getResult(Query::HYDRATE_SIMPLEOBJECT); // Blog mais sans objets joints (1-to-1) (!)
Pourquoi?
Principalement pour des raisons de performance, l'hydratation en
objet, surtout avec des objets associés, est très lourde.
❗
pour HYDRATE_SIMPLEOBJECT, surtout si vos getters sont typés
Une autre optimisation : partial object
// Dans BlogRepository
$query = $this->createQueryBuilder('b')
->select('PARTIAL b.{id, name}')
->where('b.name = :name')
->setParameter('name', 'Romaric')
->getQuery();
$blogs = query->getResult();
dump($blogs[0] instanceof Blog); // true
dump($blogs[0]->getId()); // 1
dump($blogs[0]->getName()); // Romaric
dump($blogs[0]->getDescription()); // null
❗
Tous les autres champs seront null, les associations et collections
auront des proxys non initialisés / vides.
Ou encore : les Partial References
Lorsqu'on veut une entité que pour son ID, il est possible d'utiliser un
autre type d'objet partiel, une référence :
$blog1 = $entityManager->getPartialReference(Blog::class, 1);
dump($blog1 instanceof Blog); // true
dump($blog1->getId()); // 1
$articles = $entityManager->getRepository(Article::class)
->findBy(['blog' => $blog1]);
Relations / associations : base
Dans une relation, il y a l'owning side (requis), et l'inverse side.
/** @ORMEntity */
class Article
{
/**
* @ORMManyToOne(targetEntity="Blog", inversedBy="articles")
*/
private $blog;
}
/** @ORMEntity */
class Blog
{
/**
* @ORMOneToMany(targetEntity="Article", mappedBy="blog",
* cascade={"remove"},
* onDelete="CASCADE",
* orphanRemoval=true)
*/
private $articles;
}
Relations / associations : base
Dans une relation, il y a l'owning side (requis), et l'inverse side.
/** @ORMEntity */
class Article
{
/**
* @ORMManyToOne(targetEntity="Blog", inversedBy="articles")
*/
private $blog;
}
/** @ORMEntity */
class Blog
{
/**
* @ORMOneToMany(targetEntity="Article", mappedBy="blog",
* cascade={"remove"},
* onDelete="CASCADE",
* orphanRemoval=true)
*/
private $articles;
}
Relations / associations : base
Dans une relation, il y a l'owning side (requis), et l'inverse side.
/** @ORMEntity */
class Article
{
/**
* @ORMManyToOne(targetEntity="Blog", inversedBy="articles")
*/
private $blog;
}
/** @ORMEntity */
class Blog
{
/**
* @ORMOneToMany(targetEntity="Article", mappedBy="blog",
* cascade={"remove"},
* onDelete="CASCADE",
* orphanRemoval=true)
*/
private $articles;
}
Relations / associations : base
Dans une relation, il y a l'owning side (requis), et l'inverse side.
/** @ORMEntity */
class Article
{
/**
* @ORMManyToOne(targetEntity="Blog", inversedBy="articles")
*/
private $blog;
}
/** @ORMEntity */
class Blog
{
/**
* @ORMOneToMany(targetEntity="Article", mappedBy="blog",
* cascade={"remove"},
* onDelete="CASCADE",
* orphanRemoval=true)
*/
private $articles;
}
Relations et proxy
Dans la mesure du possible, Doctrine propose du lazy
loading, c'est-à-dire de mettre soit un proxy soit une
PersistentCollection à la place de(s) entité(s) jointe(s).
Type Côté Peut être proxy ?
One-to-One Owning Oui (ou null)
One-to-One Inverse Jamais ❗
Many-to-One Owning Oui (ou null)
Many-to-One (One-to-Many) Inverse Oui, Collection
Many-to-Many Owning Oui, Collection
Many-to-Many Inverse Oui, Collection
Lazy-loading...
$blogs = $entityManager->getRepository(Blog::class)->findAll();
// Chaque Blog a des articles (One-to-Many),
// et des contributeurs/authors (One-to-Many)
foreach ($blogs as $blog) {
foreach ($blog->getArticles() as $article) {
$title = $article->getTitle();
// ...
}
foreach ($blog->getAuthors() as $author) {
$name = $author->getName();
// ...
}
}
...et kaboom, problème du N+1
N+1 : une possible solution
On peut demander à Doctrine de récupérer en même temps les
articles et les contributeurs :
$blogs = $this->createQueryBuilder('blog')
->addSelect('article, author')
->join('blog.articles', 'article')
->join('blog.authors', 'author')
->getQuery()
->getResult();
❗
on garde le problème du coût de l'hydratation (coût en O(n*m*q) ici,
avec n blogs, m articles et q contributeurs).
Solution2
: multi-step hydratation
$blogs = $entityManager->createQuery('
SELECT blog, article
FROM Blog blog
LEFT JOIN blog.articles article
')
->getResult();
$entityManager->createQuery('
SELECT PARTIAL blog.{id}, author
FROM Blog blog
LEFT JOIN blog.authors author
')
->getResult(); // Résultat inutile
$blogs[0]->getArticles()->first()->getTitle(); // Ne déclenche pas de requête
2 
Plus de détails sur le blog de Marco Pivetta (Ocramius), exemples sur Github ici
❗
Note sur les proxies
// Un Blog a un Logo (One-To-One, owning side)
$logo = $blog->getLogo();
// Les proxies supportent mal la sérialisation
$str = serialize($logo);
$logo2 = unserialize($str); //
!
Error at offset 0...
// À la place, soit charger le proxy
if ($logo instanceof DoctrineORMProxyProxy) {
$logo->__load();
}
$logo2 = unserialize(serialize($logo)); //
// Soit utiliser l'identité
$logoId = unserialize(serialize($logo->getId()));
Requêtage, astuce : les Criteria
class Blog
{
public function getDraftArticles(): Collection
{
$criteria = Criteria::create()
->where(Criteria::expr()->eq('status', 'draft'))
->orderBy(['position' => Criteria::ASC]);
return $this->articles->matching($criteria);
}
}
Si la collection n'est pas chargée, une requête SQL avec un WHERE sera
générée, sinon le filtrage aura lieu sur les éléments en mémoire.
Astuce 2 : appliquer un filtre à toutes les requêtes
class NoDraftFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $entityMetadata, $targetTableAlias)
{
if (Article::class !== $entityMetadata->reflClass->getName()) {
return '';
}
return $targetTableAlias.'.status != 'draft''; // DQL qui sera injecté dans le WHERE
}
}
orm:
entity_managers:
default:
filters:
draft_filter:
class: AppFilterNoDraftFilter
enabled: true
$publishedArticles = $entityManager->getRepository(Article::class)->findAll();
$entityManager->getFilters()->disable('draft_filter');
$allArticles = $entityManager->getRepository(Article::class)->findAll();
Astuce 3 : organiser ses repositories...
class ArticleRepository
{
public function findOnlineArticlesIWroteOnBlog(User $user, Blog $blog)
{
$queryBuilder = $this->createQueryBuilder('a');
self::withIsOnline($queryBuilder, 'a');
self::withIWrote($queryBuilder, 'a', $user);
self::withFromBlog($queryBuilder, 'a', $blog);
return $queryBuilder->getQuery()->getResult();
}
private static function withIsOnline(QueryBuilder $queryBuilder, string $alias)
{
$queryBuilder
->andWhere($alias.'.status != 'draft'')
->andWhere($alias.'.publishOn >= CURRENT_TIMESTAMP()')
;
}
// withIWrote(), withFromBlog(), etc
}
...ou construire des requêtes complexes
class ArticleQueryBuilderBuilder
{
private $queryBuilder;
private $tableAlias;
public function __construct(QueryBuilder $queryBuilder, string $tableAlias)
{
$this->queryBuilder = $queryBuilder;
$this->tableAlias = $tableAlias;
}
public function withIsOnline()
{
$this->queryBuilder
->andWhere($this->tableAlias.'.status != 'draft'')
->andWhere($this->tableAlias.'.publishOn >= CURRENT_TIMESTAMP()');
}
// withIWrote(), withFromBlog(), etc
}
$articles = $entityManager->getRepository(Article::class)
->getArticleQueryBuilderBuilder() // À ajouter dans ArticleRepository
->withIsOnline()
->withIWrote($user)
->withFromBlog($blog)
->getQueryBuilder()->getQuery()->getResult();
[intermède]
INSERTQue se passe-t-il quand j'insère une nouvelle entité ?
Cycle de Vie d'un persist()
$article = new Article();
// Doctrine détecte qu'il ne connaît pas l'entité,
// il va l'ajouter dans UnitOfWork::entityInsertions
$entityManager->persist($article);
// Maintenant, Doctrine va synchroniser l'UnitOfWork avec la BDD :
// il regarde s'il y a de nouvelles entités,
// ouvre une transaction,
// génère et exécute un INSERT SQL,
// puis commit et l'UnitOfWork se "nettoie"
$entityManager->flush();
Cycle de Vie d'un persist()
$article = new Article();
// Doctrine détecte qu'il ne connaît pas l'entité,
// il va l'ajouter dans UnitOfWork::entityInsertions
$entityManager->persist($article);
// Maintenant, Doctrine va synchroniser l'UnitOfWork avec la BDD :
// il regarde s'il y a de nouvelles entités,
// ouvre une transaction,
// génère et exécute un INSERT SQL,
// puis commit et l'UnitOfWork se "nettoie"
$entityManager->flush();
Cycle de Vie d'un persist()
$article = new Article();
// Doctrine détecte qu'il ne connaît pas l'entité,
// il va l'ajouter dans UnitOfWork::entityInsertions
$entityManager->persist($article);
// Maintenant, Doctrine va synchroniser l'UnitOfWork avec la BDD :
// il regarde s'il y a de nouvelles entités,
// ouvre une transaction,
// génère et exécute un INSERT SQL,
// puis commit et l'UnitOfWork se "nettoie"
$entityManager->flush();
Évènements
Action Quand ? Évènements
Nouvelle entité EM:flush() prePersist et postPersist
Mise à jour EM:flush() preUpdate et postUpdate
Suppression EM:flush() preRemove et postRemove
Toujours EM:flush() preFlush, onFlush et postFlush
Lecture de la BDD find()... postLoad
Première opération find(), EM:persist()... loadClassMetadata
Nettoyage EM:clear() onClear
Lifecycle callbacks (1/3)
use DoctrineCommonPersistenceEventPreUpdateEventArgs;
/**
* @ORMEntity
* @ORMHasLifecycleCallbacks
*/
class Blog
{
/** @ORMPreUpdate */
public function onPreUpdate(PreUpdateEventArgs $event)
{
$this->updatedAt = new DateTimeImmutable();
}
}
Events listeners/subscriber (2/3)
use DoctrineORMEvents;
use DoctrineCommonEventSubscriber;
use DoctrineCommonPersistenceEventLifecycleEventArgs;
class LocalizationPersister implements EventSubscriber
{
public function getSubscribedEvents()
{
return [Events::prePersist];
}
public function onPrePersist(LifecycleEventArgs $args)
{
if (!$args->getObject() instanceof Article) {
return;
}
$args->getObject()->setLocale('fr'); // ou injectée...
}
}
Entity listeners3
⭐
(3/3)
use DoctrineCommonPersistenceEventLifecycleEventArgs;
class BlogLogoListener
{
public function preRemove(Logo $logo, LifecycleEventArgs $args)
{
unlink($logo->getPath());
}
}
services:
blog_logo_listener:
class: AppListenerBlogLogoListener
tags:
- { name: doctrine.orm.entity_listener, event: preRemove, entity: AppEntityLogo }
3 
Syntaxe avec Doctrine 2.5+.
❗
La documentation n'est pas très claire pour l'instant.
UPDATEEt lors de la mise à jour d'une entité ?
Lors du flush()
$article->setTitle('Retex SymfonyLive 2019');
$entityManager->flush($article); // Ou flush() tout court
Par défaut, pas besoin de persist() !
Doctrine va regarder si chaque champ de chaque entité a été modifié.
Il est possible de changer cela, c'est-à-dire la tracking policy.
Les tracking policies
Deferred Implicit : stratégie par défaut. Doctrine garde en mémoire les
valeurs récupérées de la BDD, et lors du flush() va comparer chaque
champ des entités qu'il connaît pour voir s'il a été modifié.
❗
Peut utiliser beaucoup de ressource.
Deferred Explicit : seules les entités explicitement persistées
($entityManager->persist($article)) ont leurs champs comparés.
❗
Utilise moins de ressources, mais attention aux cascades.
Notify : chaque entité doit signaler ses modifications à un listener.
Le plus optimisé, mais lourd à mettre en place.
Exemple avec Deferred explicit
/**
* @ORMEntity
* @ORMChangeTrackingPolicy("DEFERRED_EXPLICIT")
*/
class Article
{
// ...
}
$article->setTitle('Hello World');
$entityManager->persist($article);
$entityManager->flush();
❗
Il faut appeler exactement $entityManager->persist() sur chaque
entité, cascade: {"persist"} dans les annotations ne suffit pas. Donc il
faut pouvoir/vouloir accéder à chaque entité depuis son contrôleur...
Listeners : particularité de l'update
use DoctrineCommonPersistenceEventPreUpdateEventArgs;
class BlogListener
{
public function preUpdate(Blog $blog, PreUpdateEventArgs $args)
{
if ($eventArgs->hasChangedField('name') && $eventArgs->getNewValue('name')) {
// Attention de cette manière seuls les champs déjà modifiés peuvent être remodifiés
$eventArgs->setNewValue('name', 'Nouveau nom: '.$eventArgs->getNewValue());
$blog->setSlug(canonicalize($blog->getName()));
// Si on souhaite modifier une autre propriété, il faut lancer une recomparaison
$classMetadata = $args->getEntityManager()->getClassMetadata(Blog::class);
$args->getEntityManager()->getUnitOfWork()->recomputeSingleEntityChangeSet($classMetadata, $blog);
}
}
}
❗
On ne peut pas créer de nouvelles entités ici.
Listeners : créer de nouvelles entités
class BlogSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return [Events::onFlush];
}
public function onFlush(OnFlushEventArgs $args)
{
$entityManager = $args->getEntityManager();
$unitOfWork = $entityManager->getUnitOfWork();
foreach ($unitOfWork->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof Blog) {
$changeset = $unitOfWork->getEntityChangeSet($entity);
if (isset($changeset['name'])) {
$notification = new Notification(); // etc
$entityManager->persist($notification);
// On ne peut pas déclencher de flush() - on doit recompute "à la main"
$classMetadata = $entityManager->getClassMetadata(Notification::class);
$unitOfWork->computeChangeSet($classMetadata, $notification);
}
}
}
}
}
Un piège : suivi des objets
Par défaut, Doctrine compare les valeurs des propriétés pour détecter
si elles ont été modifiées.
❗
Cela ne marche pas avec les objets: UploadedFile, DateTime...
Il faut alors soit modifier la valeur d'une autre propriété, d'un type
primitif, soit utiliser des objets immutables.
class Article
{
/**
* @var DateTimeImmutable
* @ORMColumn(type="datetime_immutable")
*/
private $updatedAt;
}
MAPPING& Configuration & dernière astuces
Embeddables
/** @ORMEmbeddable */
class Address
{
/** @ORMColumn() */
private $street;
/** @ORMColumn() */
private $city;
}
/** @ORMEntity */
class Blog
{
/** @ORMEmbedded(class="Address") */
private $address;
public function __construct()
{
$this->address = new Address();
}
}
// Pour requêter en DQL, on pourra écrire :
// SELECT b FROM Blog b WHERE b.address.city = ...
Ajouter un type Doctrine (1/2)
use DoctrineDBALPlatformsAbstractPlatform;
use DoctrineDBALTypesConversionException;
use DoctrineDBALTypesType;
class DatetimeUtcType extends Type
{
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getDateTimeTypeDeclarationSQL($fieldDeclaration);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if (null === $value) {
return $value;
}
if (!$value instanceof DateTime) {
throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'DateTime']);
}
$value->setTimezone(new DateTimeZone('UTC'));
return $value->format($platform->getDateTimeFormatString());
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if ($value === null || $value instanceof DateTime) {
return $value;
}
$val = DateTime::createFromFormat($platform->getDateTimeFormatString(), $value, new DateTimeZone('UTC'));
if (!$val) {
throw ConversionException::conversionFailedFormat($value, $this->getName(), $platform->getDateTimeFormatString());
}
return $val;
}
public function getName()
{
return 'datetime_utc';
}
}
Ajouter un type Doctrine (2/2)
doctrine:
dbal:
types:
datetime_utc: AppTypeDatetimeUtcType
Exemple, pour stocker une date avec un timezone (en MySQL...) :
/** ORMEntity */
class Article
{
/** ORMColumn(type="datetime_utc") */
private $utcDate;
/** ORMColumn(type="string") */
private $timezone;
public function getDate()
{
return (clone $this->utcDate)->setTimezone(new DateTimeZone($this->timezone));
}
public function setDate(DateTime $date)
{
$this->timezone = $date->getTimezone()->getName();
$this->utcDate = clone $date;
}
}
Héritage
Cas typique : définir dans un bundle une entité qui sera étendue.
/** @ORMMappedSuperclass */ /** @ORMEntity */
abstract class AbstractUser class User extends AbstractUser
{ {
/** @ORMColumn() */ /** @ORMId @ORMColumn(type="integer") */
protected $email; private $id;
/** @ORMColumn() */ /** @ORMColumn() */
protected $username; private $myfield;
} }
❗
aux limites (associations...)
Doctrine supporte les Traits, cela est généralement plus simple.
❗ ❗ ❗
pour Single Table Inheritance et Multiple Table Inheritance
Caches
doctrine:
orm:
# Ces 2 caches sont INDISPENSABLES en "prod" :
# (et activés par Symfony Flex par défaut)
metadata_cache_driver: apcu # cache les annotations
query_cache_driver: apcu # cache le DQL généré
# Optionnel et manuel :
result_cache_driver: apcu
Le Result cache devra être rempli manuellement :
$query = $em->createQuery('SELECT b FROM AppEntityBlog b');
$query->useResultCache(true, 3600); // 1 heure
Second-level cache1
doctrine:
orm:
entity_managers:
default:
second_level_cache:
enabled: true
region_cache_driver: apcu
regions:
my_entity_region:
type: apcu
/**
* @ORMEntity
* @ORMCache(usage="READ_ONLY", region="my_entity_region")
*/
class Blog
{
// ...
}
1 
Il y a un nombre important d'options, voir la documentation
Étendre Doctrine
Le plus connu, les Doctrine extensions :
Sortable, SoftDeleteable, Sluggable...
⭐ ⭐
steevanb/DoctrineStatsBundle
⭐ ⭐
Des fonctions DQL supplémentaires :
- spécifiques à chaque plate-forme
- pour requêter/manipuler du JSON
- pour les types spatiaux
Des utilitaires pour les batchs
Conclusion & questions
@romaricdrigon / romaric@netinfluence.ch

Contenu connexe

Tendances

RESTful API 설계
RESTful API 설계RESTful API 설계
RESTful API 설계Jinho Yoo
 
React for Dummies
React for DummiesReact for Dummies
React for DummiesMitch Chen
 
Web Development with Laravel 5
Web Development with Laravel 5Web Development with Laravel 5
Web Development with Laravel 5Soheil Khodayari
 
jQuery - Chapter 3 - Effects
jQuery - Chapter 3 - Effects  jQuery - Chapter 3 - Effects
jQuery - Chapter 3 - Effects WebStackAcademy
 
jpa-hibernate-presentation
jpa-hibernate-presentationjpa-hibernate-presentation
jpa-hibernate-presentationJohn Slick
 
Spring boot Introduction
Spring boot IntroductionSpring boot Introduction
Spring boot IntroductionJeevesh Pandey
 
Use Symfony Messenger Component and CQRS!
Use Symfony Messenger Component and CQRS!Use Symfony Messenger Component and CQRS!
Use Symfony Messenger Component and CQRS!Žilvinas Kuusas
 
파이썬 플라스크 이해하기
파이썬 플라스크 이해하기 파이썬 플라스크 이해하기
파이썬 플라스크 이해하기 Yong Joon Moon
 
Michael Bayer Introduction to SQLAlchemy @ Postgres Open
Michael Bayer Introduction to SQLAlchemy @ Postgres OpenMichael Bayer Introduction to SQLAlchemy @ Postgres Open
Michael Bayer Introduction to SQLAlchemy @ Postgres OpenPostgresOpen
 
Introduction to the Web API
Introduction to the Web APIIntroduction to the Web API
Introduction to the Web APIBrad Genereaux
 
Lets make a better react form
Lets make a better react formLets make a better react form
Lets make a better react formYao Nien Chung
 
Hibernate
HibernateHibernate
HibernateAjay K
 
JavaScript Event Loop
JavaScript Event LoopJavaScript Event Loop
JavaScript Event LoopDesignveloper
 
Laravel introduction
Laravel introductionLaravel introduction
Laravel introductionSimon Funk
 
Angular interview questions
Angular interview questionsAngular interview questions
Angular interview questionsGoa App
 
Html and css presentation
Html and css presentationHtml and css presentation
Html and css presentationumesh patil
 
Docker란 무엇인가? : Docker 기본 사용법
Docker란 무엇인가? : Docker 기본 사용법Docker란 무엇인가? : Docker 기본 사용법
Docker란 무엇인가? : Docker 기본 사용법pyrasis
 
PHP File Handling
PHP File Handling PHP File Handling
PHP File Handling Degu8
 

Tendances (20)

RESTful API 설계
RESTful API 설계RESTful API 설계
RESTful API 설계
 
React for Dummies
React for DummiesReact for Dummies
React for Dummies
 
Web Development with Laravel 5
Web Development with Laravel 5Web Development with Laravel 5
Web Development with Laravel 5
 
Web API Basics
Web API BasicsWeb API Basics
Web API Basics
 
jQuery - Chapter 3 - Effects
jQuery - Chapter 3 - Effects  jQuery - Chapter 3 - Effects
jQuery - Chapter 3 - Effects
 
jpa-hibernate-presentation
jpa-hibernate-presentationjpa-hibernate-presentation
jpa-hibernate-presentation
 
Spring boot Introduction
Spring boot IntroductionSpring boot Introduction
Spring boot Introduction
 
Use Symfony Messenger Component and CQRS!
Use Symfony Messenger Component and CQRS!Use Symfony Messenger Component and CQRS!
Use Symfony Messenger Component and CQRS!
 
파이썬 플라스크 이해하기
파이썬 플라스크 이해하기 파이썬 플라스크 이해하기
파이썬 플라스크 이해하기
 
Michael Bayer Introduction to SQLAlchemy @ Postgres Open
Michael Bayer Introduction to SQLAlchemy @ Postgres OpenMichael Bayer Introduction to SQLAlchemy @ Postgres Open
Michael Bayer Introduction to SQLAlchemy @ Postgres Open
 
Introduction to the Web API
Introduction to the Web APIIntroduction to the Web API
Introduction to the Web API
 
Lets make a better react form
Lets make a better react formLets make a better react form
Lets make a better react form
 
Swoole w PHP. Czy to ma sens?
Swoole w PHP. Czy to ma sens?Swoole w PHP. Czy to ma sens?
Swoole w PHP. Czy to ma sens?
 
Hibernate
HibernateHibernate
Hibernate
 
JavaScript Event Loop
JavaScript Event LoopJavaScript Event Loop
JavaScript Event Loop
 
Laravel introduction
Laravel introductionLaravel introduction
Laravel introduction
 
Angular interview questions
Angular interview questionsAngular interview questions
Angular interview questions
 
Html and css presentation
Html and css presentationHtml and css presentation
Html and css presentation
 
Docker란 무엇인가? : Docker 기본 사용법
Docker란 무엇인가? : Docker 기본 사용법Docker란 무엇인가? : Docker 기본 사용법
Docker란 무엇인가? : Docker 기본 사용법
 
PHP File Handling
PHP File Handling PHP File Handling
PHP File Handling
 

Similaire à Doctrine en dehors des sentiers battus

Modèle de domaine riche dans une application métier complexe un exemple pratique
Modèle de domaine riche dans une application métier complexe un exemple pratiqueModèle de domaine riche dans une application métier complexe un exemple pratique
Modèle de domaine riche dans une application métier complexe un exemple pratiqueVladyslav Riabchenko
 
S2-02-PHP-objet.pptx
S2-02-PHP-objet.pptxS2-02-PHP-objet.pptx
S2-02-PHP-objet.pptxkohay75604
 
Les bonnes pratiques de l'architecture en général
Les bonnes pratiques de l'architecture en généralLes bonnes pratiques de l'architecture en général
Les bonnes pratiques de l'architecture en généralGeoffrey Bachelet
 
Draft - Developper Sur Elgg
Draft - Developper Sur ElggDraft - Developper Sur Elgg
Draft - Developper Sur ElggBrice Gaillard
 
Programmation STUPID vs SOLID (PHP Meetup)
Programmation STUPID vs SOLID (PHP Meetup)Programmation STUPID vs SOLID (PHP Meetup)
Programmation STUPID vs SOLID (PHP Meetup)Arnaud Langlade
 
Open close principle, on a dit étendre, pas extends !
Open close principle, on a dit étendre, pas extends !Open close principle, on a dit étendre, pas extends !
Open close principle, on a dit étendre, pas extends !Engineor
 
Programmation orientée objet en PHP 5
Programmation orientée objet en PHP 5Programmation orientée objet en PHP 5
Programmation orientée objet en PHP 5Kristen Le Liboux
 
CocoaHeads Toulouse - Getting to the core of Core Data
CocoaHeads Toulouse - Getting to the core of Core DataCocoaHeads Toulouse - Getting to the core of Core Data
CocoaHeads Toulouse - Getting to the core of Core DataCocoaHeads France
 
Cours yeoman backbone box2d
Cours yeoman backbone box2dCours yeoman backbone box2d
Cours yeoman backbone box2dhugomallet
 
Tirer parti des décorateurs de Zend_Form
Tirer parti des décorateurs de Zend_FormTirer parti des décorateurs de Zend_Form
Tirer parti des décorateurs de Zend_FormMickael Perraud
 
Meet up symfony 11 octobre 2016 - Les formulaire
Meet up symfony 11 octobre 2016 - Les formulaireMeet up symfony 11 octobre 2016 - Les formulaire
Meet up symfony 11 octobre 2016 - Les formulaireJulien Vinber
 
Formulaires Symfony2 - Cas pratiques et explications
Formulaires Symfony2 - Cas pratiques et explicationsFormulaires Symfony2 - Cas pratiques et explications
Formulaires Symfony2 - Cas pratiques et explicationsAlexandre Salomé
 
Quelle place pour le framework Rails dans le développement d'application web
Quelle place pour le framework Rails dans le développement d'application webQuelle place pour le framework Rails dans le développement d'application web
Quelle place pour le framework Rails dans le développement d'application web5pidou
 
Drupalcamp Nantes - Adapter Drupal
Drupalcamp Nantes - Adapter DrupalDrupalcamp Nantes - Adapter Drupal
Drupalcamp Nantes - Adapter DrupalArtusamak
 

Similaire à Doctrine en dehors des sentiers battus (20)

Modèle de domaine riche dans une application métier complexe un exemple pratique
Modèle de domaine riche dans une application métier complexe un exemple pratiqueModèle de domaine riche dans une application métier complexe un exemple pratique
Modèle de domaine riche dans une application métier complexe un exemple pratique
 
S2-02-PHP-objet.pptx
S2-02-PHP-objet.pptxS2-02-PHP-objet.pptx
S2-02-PHP-objet.pptx
 
Les bonnes pratiques de l'architecture en général
Les bonnes pratiques de l'architecture en généralLes bonnes pratiques de l'architecture en général
Les bonnes pratiques de l'architecture en général
 
Apprenez le jQuery
Apprenez le jQueryApprenez le jQuery
Apprenez le jQuery
 
Draft - Developper Sur Elgg
Draft - Developper Sur ElggDraft - Developper Sur Elgg
Draft - Developper Sur Elgg
 
Programmation STUPID vs SOLID (PHP Meetup)
Programmation STUPID vs SOLID (PHP Meetup)Programmation STUPID vs SOLID (PHP Meetup)
Programmation STUPID vs SOLID (PHP Meetup)
 
De legacy à symfony
De legacy à symfonyDe legacy à symfony
De legacy à symfony
 
Open close principle, on a dit étendre, pas extends !
Open close principle, on a dit étendre, pas extends !Open close principle, on a dit étendre, pas extends !
Open close principle, on a dit étendre, pas extends !
 
Programmation orientée objet en PHP 5
Programmation orientée objet en PHP 5Programmation orientée objet en PHP 5
Programmation orientée objet en PHP 5
 
Android ORMLite
Android   ORMLiteAndroid   ORMLite
Android ORMLite
 
CocoaHeads Toulouse - Getting to the core of Core Data
CocoaHeads Toulouse - Getting to the core of Core DataCocoaHeads Toulouse - Getting to the core of Core Data
CocoaHeads Toulouse - Getting to the core of Core Data
 
Cours yeoman backbone box2d
Cours yeoman backbone box2dCours yeoman backbone box2d
Cours yeoman backbone box2d
 
Tirer parti des décorateurs de Zend_Form
Tirer parti des décorateurs de Zend_FormTirer parti des décorateurs de Zend_Form
Tirer parti des décorateurs de Zend_Form
 
Presentation JPA
Presentation JPAPresentation JPA
Presentation JPA
 
Cours PHP avancé
Cours PHP avancéCours PHP avancé
Cours PHP avancé
 
Meet up symfony 11 octobre 2016 - Les formulaire
Meet up symfony 11 octobre 2016 - Les formulaireMeet up symfony 11 octobre 2016 - Les formulaire
Meet up symfony 11 octobre 2016 - Les formulaire
 
Formulaires Symfony2 - Cas pratiques et explications
Formulaires Symfony2 - Cas pratiques et explicationsFormulaires Symfony2 - Cas pratiques et explications
Formulaires Symfony2 - Cas pratiques et explications
 
Quelle place pour le framework Rails dans le développement d'application web
Quelle place pour le framework Rails dans le développement d'application webQuelle place pour le framework Rails dans le développement d'application web
Quelle place pour le framework Rails dans le développement d'application web
 
Playing With PHP 5.3
Playing With PHP 5.3Playing With PHP 5.3
Playing With PHP 5.3
 
Drupalcamp Nantes - Adapter Drupal
Drupalcamp Nantes - Adapter DrupalDrupalcamp Nantes - Adapter Drupal
Drupalcamp Nantes - Adapter Drupal
 

Doctrine en dehors des sentiers battus

  • 1. Doctrine en dehors des sentiers ba!us
  • 2. Romaric Drigon, Développeur et consultant à netinfluence @romaricdrigon / romaric@netinfluence.ch
  • 3. ❗ = à utiliser avec précaution
  • 4. SELECTQue se passe-t-il lorsque je requête une entité?
  • 5. Une requête basique $blog = $entityManager->getRepository(Blog::class)->find(1);
  • 6. Une requête basique $blog = $entityManager->getRepository(Blog::class)->find(1); — Doctrine ORM regarde dans le mapping (ClassMetadata) quel est l'identifiant (ID) de notre entité
  • 7. Une requête basique $blog = $entityManager->getRepository(Blog::class)->find(1); — Doctrine ORM regarde dans le mapping (ClassMetadata) quel est l'identifiant (ID) de notre entité — dans le cas d'une requête par ID, il regarde dans l'identityMap si l'entité n'a pas déjà été chargée
  • 8. Une requête basique $blog = $entityManager->getRepository(Blog::class)->find(1); — Doctrine ORM regarde dans le mapping (ClassMetadata) quel est l'identifiant (ID) de notre entité — dans le cas d'une requête par ID, il regarde dans l'identityMap si l'entité n'a pas déjà été chargée — si non, Doctrine va générer une requête SQL
  • 9. Une requête basique $blog = $entityManager->getRepository(Blog::class)->find(1); — Doctrine ORM regarde dans le mapping (ClassMetadata) quel est l'identifiant (ID) de notre entité — dans le cas d'une requête par ID, il regarde dans l'identityMap si l'entité n'a pas déjà été chargée — si non, Doctrine va générer une requête SQL — Doctrine DBAL va l'exécuter
  • 10. Une requête basique $blog = $entityManager->getRepository(Blog::class)->find(1); — Doctrine ORM regarde dans le mapping (ClassMetadata) quel est l'identifiant (ID) de notre entité — dans le cas d'une requête par ID, il regarde dans l'identityMap si l'entité n'a pas déjà été chargée — si non, Doctrine va générer une requête SQL — Doctrine DBAL va l'exécuter — Doctrine ORM va construire un objet à partir du résultat (hydratation)
  • 11. Une requête basique $blog = $entityManager->getRepository(Blog::class)->find(1); — Doctrine ORM regarde dans le mapping (ClassMetadata) quel est l'identifiant (ID) de notre entité — dans le cas d'une requête par ID, il regarde dans l'identityMap si l'entité n'a pas déjà été chargée — si non, Doctrine va générer une requête SQL — Doctrine DBAL va l'exécuter — Doctrine ORM va construire un objet à partir du résultat (hydratation) — elle sera ajoutée à l'identityMap, puis retournée
  • 12. Changer l'hydratation Plusieurs modes sont disponibles : $result = query->getResult(Query::HYDRATE_OBJECT); // Blog, objet construit par Reflection $result = query->getResult(Query::HYDRATE_ARRAY); // tableau associatif avec id, name... $result = query->getResult(Query::HYDRATE_SCALAR); // tableaux avec b_id, b_name... (non-dédupliqué !) $result = query->getResult(Query::HYDRATE_SINGLE_SCALAR); // non supporté ici $result = query->getResult(Query::HYDRATE_SIMPLEOBJECT); // Blog mais sans objets joints (1-to-1) (!) Pourquoi? Principalement pour des raisons de performance, l'hydratation en objet, surtout avec des objets associés, est très lourde. ❗ pour HYDRATE_SIMPLEOBJECT, surtout si vos getters sont typés
  • 13. Une autre optimisation : partial object // Dans BlogRepository $query = $this->createQueryBuilder('b') ->select('PARTIAL b.{id, name}') ->where('b.name = :name') ->setParameter('name', 'Romaric') ->getQuery(); $blogs = query->getResult(); dump($blogs[0] instanceof Blog); // true dump($blogs[0]->getId()); // 1 dump($blogs[0]->getName()); // Romaric dump($blogs[0]->getDescription()); // null ❗ Tous les autres champs seront null, les associations et collections auront des proxys non initialisés / vides.
  • 14. Ou encore : les Partial References Lorsqu'on veut une entité que pour son ID, il est possible d'utiliser un autre type d'objet partiel, une référence : $blog1 = $entityManager->getPartialReference(Blog::class, 1); dump($blog1 instanceof Blog); // true dump($blog1->getId()); // 1 $articles = $entityManager->getRepository(Article::class) ->findBy(['blog' => $blog1]);
  • 15. Relations / associations : base Dans une relation, il y a l'owning side (requis), et l'inverse side. /** @ORMEntity */ class Article { /** * @ORMManyToOne(targetEntity="Blog", inversedBy="articles") */ private $blog; } /** @ORMEntity */ class Blog { /** * @ORMOneToMany(targetEntity="Article", mappedBy="blog", * cascade={"remove"}, * onDelete="CASCADE", * orphanRemoval=true) */ private $articles; }
  • 16. Relations / associations : base Dans une relation, il y a l'owning side (requis), et l'inverse side. /** @ORMEntity */ class Article { /** * @ORMManyToOne(targetEntity="Blog", inversedBy="articles") */ private $blog; } /** @ORMEntity */ class Blog { /** * @ORMOneToMany(targetEntity="Article", mappedBy="blog", * cascade={"remove"}, * onDelete="CASCADE", * orphanRemoval=true) */ private $articles; }
  • 17. Relations / associations : base Dans une relation, il y a l'owning side (requis), et l'inverse side. /** @ORMEntity */ class Article { /** * @ORMManyToOne(targetEntity="Blog", inversedBy="articles") */ private $blog; } /** @ORMEntity */ class Blog { /** * @ORMOneToMany(targetEntity="Article", mappedBy="blog", * cascade={"remove"}, * onDelete="CASCADE", * orphanRemoval=true) */ private $articles; }
  • 18. Relations / associations : base Dans une relation, il y a l'owning side (requis), et l'inverse side. /** @ORMEntity */ class Article { /** * @ORMManyToOne(targetEntity="Blog", inversedBy="articles") */ private $blog; } /** @ORMEntity */ class Blog { /** * @ORMOneToMany(targetEntity="Article", mappedBy="blog", * cascade={"remove"}, * onDelete="CASCADE", * orphanRemoval=true) */ private $articles; }
  • 19. Relations et proxy Dans la mesure du possible, Doctrine propose du lazy loading, c'est-à-dire de mettre soit un proxy soit une PersistentCollection à la place de(s) entité(s) jointe(s). Type Côté Peut être proxy ? One-to-One Owning Oui (ou null) One-to-One Inverse Jamais ❗ Many-to-One Owning Oui (ou null) Many-to-One (One-to-Many) Inverse Oui, Collection Many-to-Many Owning Oui, Collection Many-to-Many Inverse Oui, Collection
  • 20. Lazy-loading... $blogs = $entityManager->getRepository(Blog::class)->findAll(); // Chaque Blog a des articles (One-to-Many), // et des contributeurs/authors (One-to-Many) foreach ($blogs as $blog) { foreach ($blog->getArticles() as $article) { $title = $article->getTitle(); // ... } foreach ($blog->getAuthors() as $author) { $name = $author->getName(); // ... } }
  • 22. N+1 : une possible solution On peut demander à Doctrine de récupérer en même temps les articles et les contributeurs : $blogs = $this->createQueryBuilder('blog') ->addSelect('article, author') ->join('blog.articles', 'article') ->join('blog.authors', 'author') ->getQuery() ->getResult(); ❗ on garde le problème du coût de l'hydratation (coût en O(n*m*q) ici, avec n blogs, m articles et q contributeurs).
  • 23. Solution2 : multi-step hydratation $blogs = $entityManager->createQuery(' SELECT blog, article FROM Blog blog LEFT JOIN blog.articles article ') ->getResult(); $entityManager->createQuery(' SELECT PARTIAL blog.{id}, author FROM Blog blog LEFT JOIN blog.authors author ') ->getResult(); // Résultat inutile $blogs[0]->getArticles()->first()->getTitle(); // Ne déclenche pas de requête 2  Plus de détails sur le blog de Marco Pivetta (Ocramius), exemples sur Github ici
  • 24. ❗ Note sur les proxies // Un Blog a un Logo (One-To-One, owning side) $logo = $blog->getLogo(); // Les proxies supportent mal la sérialisation $str = serialize($logo); $logo2 = unserialize($str); // ! Error at offset 0... // À la place, soit charger le proxy if ($logo instanceof DoctrineORMProxyProxy) { $logo->__load(); } $logo2 = unserialize(serialize($logo)); // // Soit utiliser l'identité $logoId = unserialize(serialize($logo->getId()));
  • 25. Requêtage, astuce : les Criteria class Blog { public function getDraftArticles(): Collection { $criteria = Criteria::create() ->where(Criteria::expr()->eq('status', 'draft')) ->orderBy(['position' => Criteria::ASC]); return $this->articles->matching($criteria); } } Si la collection n'est pas chargée, une requête SQL avec un WHERE sera générée, sinon le filtrage aura lieu sur les éléments en mémoire.
  • 26. Astuce 2 : appliquer un filtre à toutes les requêtes class NoDraftFilter extends SQLFilter { public function addFilterConstraint(ClassMetadata $entityMetadata, $targetTableAlias) { if (Article::class !== $entityMetadata->reflClass->getName()) { return ''; } return $targetTableAlias.'.status != 'draft''; // DQL qui sera injecté dans le WHERE } } orm: entity_managers: default: filters: draft_filter: class: AppFilterNoDraftFilter enabled: true $publishedArticles = $entityManager->getRepository(Article::class)->findAll(); $entityManager->getFilters()->disable('draft_filter'); $allArticles = $entityManager->getRepository(Article::class)->findAll();
  • 27. Astuce 3 : organiser ses repositories... class ArticleRepository { public function findOnlineArticlesIWroteOnBlog(User $user, Blog $blog) { $queryBuilder = $this->createQueryBuilder('a'); self::withIsOnline($queryBuilder, 'a'); self::withIWrote($queryBuilder, 'a', $user); self::withFromBlog($queryBuilder, 'a', $blog); return $queryBuilder->getQuery()->getResult(); } private static function withIsOnline(QueryBuilder $queryBuilder, string $alias) { $queryBuilder ->andWhere($alias.'.status != 'draft'') ->andWhere($alias.'.publishOn >= CURRENT_TIMESTAMP()') ; } // withIWrote(), withFromBlog(), etc }
  • 28. ...ou construire des requêtes complexes class ArticleQueryBuilderBuilder { private $queryBuilder; private $tableAlias; public function __construct(QueryBuilder $queryBuilder, string $tableAlias) { $this->queryBuilder = $queryBuilder; $this->tableAlias = $tableAlias; } public function withIsOnline() { $this->queryBuilder ->andWhere($this->tableAlias.'.status != 'draft'') ->andWhere($this->tableAlias.'.publishOn >= CURRENT_TIMESTAMP()'); } // withIWrote(), withFromBlog(), etc } $articles = $entityManager->getRepository(Article::class) ->getArticleQueryBuilderBuilder() // À ajouter dans ArticleRepository ->withIsOnline() ->withIWrote($user) ->withFromBlog($blog) ->getQueryBuilder()->getQuery()->getResult();
  • 30. INSERTQue se passe-t-il quand j'insère une nouvelle entité ?
  • 31. Cycle de Vie d'un persist() $article = new Article(); // Doctrine détecte qu'il ne connaît pas l'entité, // il va l'ajouter dans UnitOfWork::entityInsertions $entityManager->persist($article); // Maintenant, Doctrine va synchroniser l'UnitOfWork avec la BDD : // il regarde s'il y a de nouvelles entités, // ouvre une transaction, // génère et exécute un INSERT SQL, // puis commit et l'UnitOfWork se "nettoie" $entityManager->flush();
  • 32. Cycle de Vie d'un persist() $article = new Article(); // Doctrine détecte qu'il ne connaît pas l'entité, // il va l'ajouter dans UnitOfWork::entityInsertions $entityManager->persist($article); // Maintenant, Doctrine va synchroniser l'UnitOfWork avec la BDD : // il regarde s'il y a de nouvelles entités, // ouvre une transaction, // génère et exécute un INSERT SQL, // puis commit et l'UnitOfWork se "nettoie" $entityManager->flush();
  • 33. Cycle de Vie d'un persist() $article = new Article(); // Doctrine détecte qu'il ne connaît pas l'entité, // il va l'ajouter dans UnitOfWork::entityInsertions $entityManager->persist($article); // Maintenant, Doctrine va synchroniser l'UnitOfWork avec la BDD : // il regarde s'il y a de nouvelles entités, // ouvre une transaction, // génère et exécute un INSERT SQL, // puis commit et l'UnitOfWork se "nettoie" $entityManager->flush();
  • 34. Évènements Action Quand ? Évènements Nouvelle entité EM:flush() prePersist et postPersist Mise à jour EM:flush() preUpdate et postUpdate Suppression EM:flush() preRemove et postRemove Toujours EM:flush() preFlush, onFlush et postFlush Lecture de la BDD find()... postLoad Première opération find(), EM:persist()... loadClassMetadata Nettoyage EM:clear() onClear
  • 35. Lifecycle callbacks (1/3) use DoctrineCommonPersistenceEventPreUpdateEventArgs; /** * @ORMEntity * @ORMHasLifecycleCallbacks */ class Blog { /** @ORMPreUpdate */ public function onPreUpdate(PreUpdateEventArgs $event) { $this->updatedAt = new DateTimeImmutable(); } }
  • 36. Events listeners/subscriber (2/3) use DoctrineORMEvents; use DoctrineCommonEventSubscriber; use DoctrineCommonPersistenceEventLifecycleEventArgs; class LocalizationPersister implements EventSubscriber { public function getSubscribedEvents() { return [Events::prePersist]; } public function onPrePersist(LifecycleEventArgs $args) { if (!$args->getObject() instanceof Article) { return; } $args->getObject()->setLocale('fr'); // ou injectée... } }
  • 37. Entity listeners3 ⭐ (3/3) use DoctrineCommonPersistenceEventLifecycleEventArgs; class BlogLogoListener { public function preRemove(Logo $logo, LifecycleEventArgs $args) { unlink($logo->getPath()); } } services: blog_logo_listener: class: AppListenerBlogLogoListener tags: - { name: doctrine.orm.entity_listener, event: preRemove, entity: AppEntityLogo } 3  Syntaxe avec Doctrine 2.5+. ❗ La documentation n'est pas très claire pour l'instant.
  • 38. UPDATEEt lors de la mise à jour d'une entité ?
  • 39. Lors du flush() $article->setTitle('Retex SymfonyLive 2019'); $entityManager->flush($article); // Ou flush() tout court Par défaut, pas besoin de persist() ! Doctrine va regarder si chaque champ de chaque entité a été modifié. Il est possible de changer cela, c'est-à-dire la tracking policy.
  • 40. Les tracking policies Deferred Implicit : stratégie par défaut. Doctrine garde en mémoire les valeurs récupérées de la BDD, et lors du flush() va comparer chaque champ des entités qu'il connaît pour voir s'il a été modifié. ❗ Peut utiliser beaucoup de ressource. Deferred Explicit : seules les entités explicitement persistées ($entityManager->persist($article)) ont leurs champs comparés. ❗ Utilise moins de ressources, mais attention aux cascades. Notify : chaque entité doit signaler ses modifications à un listener. Le plus optimisé, mais lourd à mettre en place.
  • 41. Exemple avec Deferred explicit /** * @ORMEntity * @ORMChangeTrackingPolicy("DEFERRED_EXPLICIT") */ class Article { // ... } $article->setTitle('Hello World'); $entityManager->persist($article); $entityManager->flush(); ❗ Il faut appeler exactement $entityManager->persist() sur chaque entité, cascade: {"persist"} dans les annotations ne suffit pas. Donc il faut pouvoir/vouloir accéder à chaque entité depuis son contrôleur...
  • 42. Listeners : particularité de l'update use DoctrineCommonPersistenceEventPreUpdateEventArgs; class BlogListener { public function preUpdate(Blog $blog, PreUpdateEventArgs $args) { if ($eventArgs->hasChangedField('name') && $eventArgs->getNewValue('name')) { // Attention de cette manière seuls les champs déjà modifiés peuvent être remodifiés $eventArgs->setNewValue('name', 'Nouveau nom: '.$eventArgs->getNewValue()); $blog->setSlug(canonicalize($blog->getName())); // Si on souhaite modifier une autre propriété, il faut lancer une recomparaison $classMetadata = $args->getEntityManager()->getClassMetadata(Blog::class); $args->getEntityManager()->getUnitOfWork()->recomputeSingleEntityChangeSet($classMetadata, $blog); } } } ❗ On ne peut pas créer de nouvelles entités ici.
  • 43. Listeners : créer de nouvelles entités class BlogSubscriber implements EventSubscriber { public function getSubscribedEvents() { return [Events::onFlush]; } public function onFlush(OnFlushEventArgs $args) { $entityManager = $args->getEntityManager(); $unitOfWork = $entityManager->getUnitOfWork(); foreach ($unitOfWork->getScheduledEntityUpdates() as $entity) { if ($entity instanceof Blog) { $changeset = $unitOfWork->getEntityChangeSet($entity); if (isset($changeset['name'])) { $notification = new Notification(); // etc $entityManager->persist($notification); // On ne peut pas déclencher de flush() - on doit recompute "à la main" $classMetadata = $entityManager->getClassMetadata(Notification::class); $unitOfWork->computeChangeSet($classMetadata, $notification); } } } } }
  • 44. Un piège : suivi des objets Par défaut, Doctrine compare les valeurs des propriétés pour détecter si elles ont été modifiées. ❗ Cela ne marche pas avec les objets: UploadedFile, DateTime... Il faut alors soit modifier la valeur d'une autre propriété, d'un type primitif, soit utiliser des objets immutables. class Article { /** * @var DateTimeImmutable * @ORMColumn(type="datetime_immutable") */ private $updatedAt; }
  • 45.
  • 46. MAPPING& Configuration & dernière astuces
  • 47. Embeddables /** @ORMEmbeddable */ class Address { /** @ORMColumn() */ private $street; /** @ORMColumn() */ private $city; } /** @ORMEntity */ class Blog { /** @ORMEmbedded(class="Address") */ private $address; public function __construct() { $this->address = new Address(); } } // Pour requêter en DQL, on pourra écrire : // SELECT b FROM Blog b WHERE b.address.city = ...
  • 48. Ajouter un type Doctrine (1/2) use DoctrineDBALPlatformsAbstractPlatform; use DoctrineDBALTypesConversionException; use DoctrineDBALTypesType; class DatetimeUtcType extends Type { public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) { return $platform->getDateTimeTypeDeclarationSQL($fieldDeclaration); } public function convertToDatabaseValue($value, AbstractPlatform $platform) { if (null === $value) { return $value; } if (!$value instanceof DateTime) { throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'DateTime']); } $value->setTimezone(new DateTimeZone('UTC')); return $value->format($platform->getDateTimeFormatString()); } public function convertToPHPValue($value, AbstractPlatform $platform) { if ($value === null || $value instanceof DateTime) { return $value; } $val = DateTime::createFromFormat($platform->getDateTimeFormatString(), $value, new DateTimeZone('UTC')); if (!$val) { throw ConversionException::conversionFailedFormat($value, $this->getName(), $platform->getDateTimeFormatString()); } return $val; } public function getName() { return 'datetime_utc'; } }
  • 49. Ajouter un type Doctrine (2/2) doctrine: dbal: types: datetime_utc: AppTypeDatetimeUtcType Exemple, pour stocker une date avec un timezone (en MySQL...) : /** ORMEntity */ class Article { /** ORMColumn(type="datetime_utc") */ private $utcDate; /** ORMColumn(type="string") */ private $timezone; public function getDate() { return (clone $this->utcDate)->setTimezone(new DateTimeZone($this->timezone)); } public function setDate(DateTime $date) { $this->timezone = $date->getTimezone()->getName(); $this->utcDate = clone $date; } }
  • 50. Héritage Cas typique : définir dans un bundle une entité qui sera étendue. /** @ORMMappedSuperclass */ /** @ORMEntity */ abstract class AbstractUser class User extends AbstractUser { { /** @ORMColumn() */ /** @ORMId @ORMColumn(type="integer") */ protected $email; private $id; /** @ORMColumn() */ /** @ORMColumn() */ protected $username; private $myfield; } } ❗ aux limites (associations...) Doctrine supporte les Traits, cela est généralement plus simple. ❗ ❗ ❗ pour Single Table Inheritance et Multiple Table Inheritance
  • 51. Caches doctrine: orm: # Ces 2 caches sont INDISPENSABLES en "prod" : # (et activés par Symfony Flex par défaut) metadata_cache_driver: apcu # cache les annotations query_cache_driver: apcu # cache le DQL généré # Optionnel et manuel : result_cache_driver: apcu Le Result cache devra être rempli manuellement : $query = $em->createQuery('SELECT b FROM AppEntityBlog b'); $query->useResultCache(true, 3600); // 1 heure
  • 52. Second-level cache1 doctrine: orm: entity_managers: default: second_level_cache: enabled: true region_cache_driver: apcu regions: my_entity_region: type: apcu /** * @ORMEntity * @ORMCache(usage="READ_ONLY", region="my_entity_region") */ class Blog { // ... } 1  Il y a un nombre important d'options, voir la documentation
  • 53. Étendre Doctrine Le plus connu, les Doctrine extensions : Sortable, SoftDeleteable, Sluggable... ⭐ ⭐ steevanb/DoctrineStatsBundle ⭐ ⭐ Des fonctions DQL supplémentaires : - spécifiques à chaque plate-forme - pour requêter/manipuler du JSON - pour les types spatiaux Des utilitaires pour les batchs
  • 54. Conclusion & questions @romaricdrigon / romaric@netinfluence.ch