2. Doctrine | SymfonyCon2019 2
Doctrine
●
Doctrine project architecture
●
Common
●
DBAL (Database Abstraction Layer)
●
ORM (Object Relationnal Mapping)
●
How to with ORM
3. Doctrine | SymfonyCon2019 3
The doctrine project
●
A bunch of projects
●
Like Symfony Components
●
Standalone
●
Some dependencies around them
●
https://www.doctrine-project.org/projects.html
4. Doctrine | SymfonyCon2019 4
●
An organisation designing libraries for modern PHP development
●
Database focused
●
Database abstraction
●
Universal component
●
Common
●
General pupose tools
●
DBAL
●
Database Abstract Layer
●
Based on, and using PDO
●
ORM
●
Object Relationnal Mapper
What is Doctrine ?
9. Doctrine | SymfonyCon2019 9
DBAL : Database Abstraction Layer
●
Uses PDO and copies its API
●
You must know PDO
●
DBAL :
●
Allows simple CRUD
●
Allows query building using OO
●
Abstracts SQL types and allow mapping them on PHP types
●
Allows to play with the DB Schema structure
●
Can log queries
17. Doctrine | SymfonyCon2019 17
ORM ?
●
System design to adapt relationnal system to OO system
●
Persistence : mecanism to flush object data into database so that
data can survive across HTTP requests by being restored
●
ORM makes use of metadata to bind OO model to relationnal
model
●
Metadata need to be in sync with the model
O.R.M
19. Doctrine | SymfonyCon2019 19
Entity
●
Business layer synchronized data object
●
Describes metadata
●
Annotations
●
Xml
●
Do not extend any class (DTO)
●
Can have getters and setters
●
Can have some business logic
●
Can be validated by Sf Validators (Sf Form usage)
●
Should not depend on any other service
20. Doctrine | SymfonyCon2019 20
Entities
namespace Entities;
/**
* @Entity
* @Table(name="my_users")
*/
class User
{
/**
* @Id
* @GeneratedValue
* @Column(type="integer", length="5")
*/
protected $id;
/**
* @Column(type="text", name="user_name")
*/
protected $name;
public function getName() {
return $this->name;
}
public function setName($name) {
$this->name = $name;
}
public function getId() {
return $this->id;
}
}
●
Can be generated from the DB schema
analysis
●
Mapping is usually described using
annotations for metadata
21. Doctrine | SymfonyCon2019 21
Types mapping
●
PHP types are mapped onto RDBM types depending on the RDBM brand
●
You can add your own types
●
DoctrineDBALTypesType
22. Doctrine | SymfonyCon2019 22
RDBM - SQL Doctrine
Metadata
●
Doctrine needs to read the metadata for each entity access or operation
●
Metadata explain Doctrine how to convert the data between formats
Entity
Metadata
Table 1
Entity
Entity
Table 2
Table 3
24. Doctrine | SymfonyCon2019 24
Metadata
●
Metadata are taken by parsing mapping infos (annot or XML)
●
They are cached
●
Using Sf Cache in Sf apps
●
Must be refreshed, updated, for each mapping change
●
Add / remove / change type of field
●
Add / remove entity or table
●
Add / remove change type of association
25. Doctrine | SymfonyCon2019 25
EntityManager
●
find()
●
clear()
●
detach()
●
persist()
●
refresh()
●
remove()
●
flush()
$user = new EntitiesUser;
$user->setName("foo") ;
$user->setAddress("somewhere") ;
$em->persist($user) ;
$em->flush($user) ;
●
persist() attaches a new entity into the IdentityMap
●
May also be used with deferred_explicit change tracking policy
●
flush() persists the whole IdentityMap (sync it with the DB)
26. Doctrine | SymfonyCon2019 26
EntityManager
●
find()
●
clear()
●
detach()
●
persist()
●
refresh()
●
remove()
●
flush()
$user = $em->find('EntitiesUsers', 3);
$user->setName("foo") ;
$em->flush($user) ;
●
find() finds by PK and attaches the found entity to the IdentityMap
●
find() actually SELECT * all fields, take care of that.
●
flush() persists the IdentityMap (performs an "update" if some fields have
been modified on entities)
27. Doctrine | SymfonyCon2019 27
EntityManager
●
find()
●
clear()
●
detach()
●
persist()
●
refresh()
●
remove()
●
flush()
$user = $em->find('EntitiesUsers', 3);
$em->remove($user) ;
$em->flush($user) ;
●
remove() marks the entity as "to be deleted" into the IdentityMap
●
flush() persists the state (issues a "delete" on the DB).
●
Cascading is honnored
28. Doctrine | SymfonyCon2019 28
EntityManager
●
find()
●
clear()
●
detach()
●
persist()
●
refresh()
●
remove()
●
flush()
$user = $em->find('EntitiesUsers', 3);
$em->detach($user) ;
●
detach() deletes the entity from the IdentityMap
●
Opposite to persist()
●
Once detached, the entity is not tracked anymore for changes
●
Cascading is honnored
29. Doctrine | SymfonyCon2019 29
EntityManager
●
find()
●
clear()
●
detach()
●
persist()
●
refresh()
●
remove()
●
flush()
$user = $em->find('EntitiesUsers', 3);
echo $user->getName() // "bar"
$user->setName("FOO") ;
echo $user->getName() // "FOO"
$em->refresh($user) ;
echo $user->getName() // "bar"
●
refresh() Cancels any modification done to the entity so far from the
IdentityMap.
●
Entity is loaded, tracked for modifications and those modifications are
cancelled by refresh().
●
Usually , an SQL query is not issued for that. ORM knows about the original
data of any Entity
●
Cascading is honnored
31. Doctrine | SymfonyCon2019 31
EntityManager in theory
●
The IdentityMap into the UnitOfWork gets filled by entities which are
queried for, or persist()ed
●
UOW then tracks modifications of those entities (by default)
●
Calling flush(), UOW computes a change matrix of what have changed on
known tracked entities
●
Computes a diff
●
Orders it
●
Uses a DB transaction to play the modifications
●
Synchronizes with DB
●
If exception is thrown, transaction is rollbacked
●
Forgetting a call to flush() means forgetting a sync
●
Calling flush() on a big IdentityMap will impact performances
33. Doctrine | SymfonyCon2019 33
EM & UOW
●
You usually don't access the UOW by yourself, but may :
●
UOW is the central object of the ORM .
●
EntityManager is just a thin layer on top of it.
●
See computeChangeSets()
$em->getUnitOfWork()
34. Doctrine | SymfonyCon2019 34
UOW Performances
●
The more entities into the IdentityMap, the slower the
computation for changes
var_dump($em->getUnitOfWork()->size());
$obj = $em->find('BazOffer', 1);
var_dump($em->getUnitOfWork()->size());
int(0)
int(3)
35. Doctrine | SymfonyCon2019 35
UOW Analysis
●
getIdentityMap() returns the IdentityMap and thus the entities actually
tracked by Doctrine ORM.
●
All dependencies are also in
$obj = $em->find('FooBar', 1);
Debug::dump($em->getUnitOfWork()->getIdentityMap());
array(3) {
["FooBar"]=>
array(1) {
[1]=>
string(8) "FooBar"
}
["FooWow"]=>
array(1) {
[1]=>
string(28) "DoctrineProxies__CG__FooWow"
}
["FooUser"]=>
array(1) {
[10]=>
string(14) "FooUser"
}
}
36. Doctrine | SymfonyCon2019 36
UOW changeset
●
getEntityChangeSet($entity) allows to see what operations are to
be sent to the DB for $entity, when flush() will come
var_dump($em->getUnitOfWork()->size());
$to = $em->find('FooBar', 1);
$to->setCurrency('BAR');
var_dump($em->getUnitOfWork()->size());
$em->getUnitOfWork()->computeChangeSets();
dump($em->getUnitOfWork()->getEntityChangeSet($to));
int(0)
int(3)
array(1) {
["currency"]=>
array(2) {
[0]=>
string(3) "foo"
[1]=>
string(3) "BAR"
}
}
37. Doctrine | SymfonyCon2019 37
Change Tracking Policy
●
Deferred implicit
●
Default mode, the more comfortable, but the less performant
●
Compare every attribute of every entities, and cascades
●
Will be very heavy on big payloads ! (foreach(){foreach(){}})
●
Deferred explicit
●
Only compare entities that are explicitely persisted back after
modification
●
The best mode, balance against performances and lines of code
●
Notify
●
User must notify the UOW about what changes it performed so that the
UOW doesn't have to compute those by itself
●
The most performant mode, but needs more code to be written
38. Doctrine | SymfonyCon2019 38
Example Deferred implicit
/**
* @ORMTable(name="User")
* @ORMEntity
*/
class User {
$user = $em->find('User', 1);
$user->setAge(30);
$em->flush();
39. Doctrine | SymfonyCon2019 39
Example Deferred explicit
●
You tell UOW what entities to track
●
Prevents the UOW from tracking a very big group of entities
/**
* @ORMTable(name="User")
* @ORMEntity
* @ORMChangeTrackingPolicy("DEFERRED_EXPLICIT")
*/
class User {
$user = $em->find('User', 1);
$user->setAge(30);
$em->persist($user);
$em->flush();
40. Doctrine | SymfonyCon2019 40
Identity map
●
Doctrine memorises entities into the IdentityMap and re-provides them when
re-queried later, not performing additionnal SQL query
●
Some cases bypass the identity map
●
DQL queries
●
Partial entities queries
●
Queries not selecting using pk
$u1 = $em->find('EntitiesUser', 1) ;
$u1->setName('foobarbaz') ;
/* ... */
$u2 = $em->find('EntitiesUser', 1) ; /* SQL is NOT re-run */
echo $u2->getName() ; /* foobarbaz */
assert($u1 === $u2) ; /* true */
44. Doctrine | SymfonyCon2019 44
Associations
●
bi-directional :
●
One User can post several trips -> OneToMany
●
Several trips can reference a same User -> ManyToOne
namespace Entities;
/** @Entity */
class User
{
/** @OneToMany(targetEntity="TripOffer", mappedBy="user") */
protected $tripOffer;
/* ... */
}
namespace Entities;
/** @Entity */
class TripOffer
{
/**@ManyToOne(targetEntity="User", inversedBy="tripOffer")
* @JoinColumn(name="Users_Id", referencedColumnName="id")
*/
protected $user;
/* ... */
}
45. Doctrine | SymfonyCon2019 45
Associations
●
ArrayCollection is used to handle the "Many" part
●
Otherwise the Entity itself
namespace Entities;
use DoctrineCommonCollectionsArrayCollection;
/** @Entity */
class User
{
/** @OneToMany(targetEntity="TripOffer", mappedBy="user") */
protected $tripOffer;
public function __construct()
{ $this->tripOffer = new ArrayCollection; }
public function getTripOffer()
{ return $this->tripOffer; }
public function addTripOffer(TripOffer $t)
{ $this->tripOffer[] = $t; }
public function removeTripOffer(TripOffer $t)
{ $this->tripOffer->removeElement($t); }
}
46. Doctrine | SymfonyCon2019 46
Proxy objects
●
By default, the "LAZY" fetch mode: dependancies are not loaded but
replaced by Proxy classes or collections :
●
Informations are read from DB only when entities are accessed
●
You can ask for a proxy explicitely :
$r = $em->find('EntitiesRating', 1);
var_dump($r) ;
object(EntitiesRating)[54]
protected 'id' => int 1
protected 'creator' =>
object(DoctrineProxiesEntitiesUserProxy)[86]
private '_entityPersister' => ...
...
$realRating = $em->find('EntitiesRating', 1);
$proxyRating = $em->getReference('EntitiesRating', 1);
47. Doctrine | SymfonyCon2019 47
Hydration modes
●
LAZY
●
Default
●
Uses Proxies and only loads the data when those are accessed
●
EAGER
●
Always loads the data (even if it is not used)
●
EXTRA_LAZY
●
Do not load the data for accesses :
●
Collection#contains($entity)
●
Collection#containsKey($key)
●
Collection#count()
●
Collection#get($key)
●
Collection#slice($offset, $length = null)
48. Doctrine | SymfonyCon2019 48
Examples hydration modes
●
EAGER
●
Association is always loaded
●
Cascaded
●
Several requests are used
namespace Entities;
/** @Entity */
class User
{
/** @OneToMany(targetEntity="TripOffer", mappedBy="user", fetch="EAGER") */
protected $tripOffer;
/* ... */
}
SELECT t0.id AS id1, t0.name AS name2, t0.userName AS userName3, t0.isvip AS isvip4
FROM users t0 WHERE t0.id = ?
SELECT t0.id AS id1, t0.SeatsOffered AS SeatsOffered2, t0.TripOfferType AS TripOfferType3, t0.Users_Id AS Users_Id4
FROM TripOffer t0 WHERE t0.Users_Id = ?
$u = $em->find('EntitiesUser', 1) ;
49. Doctrine | SymfonyCon2019 49
Examples hydration modes
●
EXTRA_LAZY
●
Like LAZY, but does not load the data if some statistical questions
are asked for it
●
User, do you own some TripOffers ?
●
No need to load all the trips to know that
namespace Entities;
/** @Entity */
class User
{
/** @OneToMany(targetEntity="TripOffer", mappedBy="user", fetch="EXTRA_LAZY") */
protected $tripOffer;
/* ... */
}
$u = $em->find('EntitiesUser', 1) ;
echo $u->getTripOffer()->count() ; /* SELECT count(*) FROM TripOffer WHERE Users_Id = ? */
/* SELECT t0.id AS id1, t0.SeatsOffered AS SeatsOffered2, t0.TripOfferType AS TripOfferType3, t0.Users_Id AS Users_Id4
FROM TripOffer t0 WHERE t0.Users_Id = ? LIMIT 8 OFFSET 3 */
$someTripOffers = $u->getTripOffer()->slice(3, 8) ;
51. Doctrine | SymfonyCon2019 51
Cascades
●
What to do with dependencies when acting on a root entity :
●
persist
●
remove
●
merge
●
detach
●
refresh
●
all
●
By default, no cascades are used
52. Doctrine | SymfonyCon2019 52
Example cascade (persist)
namespace Entities;
/** @Entity */
class User
{
/** @OneToMany(targetEntity="TripOffer", mappedBy="user", cascade={"persist"}) */
protected $tripOffer;
/* ... */
}
53. Doctrine | SymfonyCon2019 53
DQL
●
Doctrine Query Language
●
Looks like SQL but
●
Queries Entities, not tables
●
Associations are used, not foreign keys
●
Mapping informations is used to convert to SQL
●
INSERT does not exist
●
DQL is fully extensible by the user
54. Doctrine | SymfonyCon2019 54
DQL example
●
You query entities, not tables
●
createQuery() for a DQL query
●
createNamedQuery() for a pre-recorded DQL query
●
createNativeQuery() for a SQL query
●
createNamedNativeQuery() for a pre-recorded SQL query
$q = $em->createQuery('SELECT u FROM EntitiesUser u WHERE u.name = ?1');
$q->setParameter(1, 'foo');
$r = $q->getResult();
$q = $em->createQuery('SELECT u, r FROM EntitiesUser u LEFT JOIN u.ratings r');
$r = $q->getResult();
55. Doctrine | SymfonyCon2019 55
DQL and joins
namespace Entities;
use DoctrineCommonCollectionsArrayCollection;
/**
* @Table(name="my_users")
*/
class User
{
/**
* @OneToMany(targetEntity="Rating", mappedBy="userCreator")
*/
protected $ratings;
}
$q = $em->createQuery("SELECT u, r FROM EntitiesUser u JOIN u.ratings r");
$r = $q->getArrayResult();
56. Doctrine | SymfonyCon2019 56
Named queries
●
Queries recorded to be recalled / re-run later
$dql = "SELECT ... ... ..." ;
$conf = new DoctrineORMConfiguration();
$conf->addNamedQuery('search', $dql);
$q = $em->createNamedQuery('search');
$r = $q->getResult();
57. Doctrine | SymfonyCon2019 57
DQL and return values
●
By default, the return type is an array of Entities
$q = $em->createQuery('SELECT u FROM EntitiesUser u WHERE u.name = ?1');
$q->setParameter(1, 'foo');
$r = $q->getResult();
array
0 =>
object(DoctrineProxiesEntitiesUserProxy)[81]
$q = $em->createQuery('SELECT u FROM EntitiesUser u WHERE u.name = ?1');
$q->setParameter(1, 'foo');
$r = $q->getSingleResult();
object(DoctrineProxiesEntitiesUserProxy)[81]
58. Doctrine | SymfonyCon2019 58
DQL and return values
$q->getArrayResult()
array
0 => &
array
'id' => int 2
'name' => string 'foo' (length=3)
'userName' => string 'fooname' (length=7)
'vip' => int 0
SELECT u FROM EntitiesUser u WHERE u.name = 'foo'
59. Doctrine | SymfonyCon2019 59
DQL and return values
●
Use the right result type you need
●
single**() must be used on single results, if not :
●
NoResultException
●
NonUniqueResultException
SELECT COUNT(u) FROM EntitiesUser u
$q->getScalarResult()
$q->getSingleResult()
$q->getSingleScalarResult()
array
1 => string '5008'
array
0 =>
array
1 => string '5008'
string '5008'
60. Doctrine | SymfonyCon2019 60
DQL and return values
●
Often used : getResult()
●
If you select not all fields of an entity :
●
An array will be returned
●
You can ask for a partial entity
●
select('PARTIAL alias.{col1, col2}')
$q = $em->createQuery("SELECT u.name, u.userName FROM EntitiesUser u");
$r = $q->getResult();
array
0 =>
array
'name' => string 'bar' (length=3)
'userName' => string 'barname' (length=7)
1 =>
array
'name' => string 'foo' (length=3)
'userName' => string 'fooname' (length=7)
61. Doctrine | SymfonyCon2019 61
DQL and return values
$q = $em->createQuery("SELECT u, UPPER(r.comment), r.id, to.type FROM EntitiesUser u
LEFT JOIN u.ratings r LEFT JOIN u.tripOffer to");
$results = $q->getResult();
array
0 =>
array
0 => object(EntitiesUser)[103]
1 => string 'THIS IS A COMMENT' (length=17)
'id' => string '1' (length=1)
'type' => null
1 => (...)
62. Doctrine | SymfonyCon2019 62
DQL and Identity Map
●
DQL queries store into the IdentityMap but don't read from it
●
Store selected entities
●
If they are full (all fields selected)
●
If the result is asked to be an entity, and not an array
$q = $em->createQuery('SELECT r FROM FooRating r WHERE r.id=?1');
$q->setParameter(1, 1);
$result = $q->getSingleResult();
$rating1 = $em->find('FooRating', 1); /* Query is not re-played */
assert($rating1 === $result); /* that's true */
$rating1 = $em->find('FooRating', 1);
$q = $em->createQuery('SELECT r FROM FooRating r WHERE r.id=?1');
$q->setParameter(1, 1);
$result = $q->getSingleResult(); /* Query is re-played */
assert($rating1 === $result); /* that's true */
63. Doctrine | SymfonyCon2019 63
DQL and Identity Map
●
Dependancies benefit from IdentityMap
$q = $em->createQuery('SELECT u, r FROM EntitiesUser u JOIN u.ratings r');
$results = $q->getResult()
$rating1 = $em->find('FooRating', 1); /* No query played */
foreach ($results as $user) {
$user->getRatings(); /* No query played */
}
$rating1 = $em->find('FooRating', 1);
$rating1->setComment('I have changed');
$q = $em->createQuery('SELECT r FROM FooRating r WHERE r.id=?1');
$q->setParameter(1, 1);
$q->setHint(DoctrineORMQuery::HINT_REFRESH, 1);
$result = $q->getSingleResult();
assert($rating1 === $result);
assert($rating1->getComment() != 'I have changed');
64. Doctrine | SymfonyCon2019 64
DQL functions
$q = $em->createQuery("SELECT u, CONCAT('foo','bar') as baz FROM EntitiesUser u");
$r = $q->getResult();
array
0 =>
array
0 =>
object(EntitiesUser)[30]
...
'baz' => string 'foobar' (length=6)
1 =>
array ...
$rating1 = $em->find('EntitiesRating', 1) ;
$q = $em->createQuery("SELECT u FROM EntitiesUser u WHERE ?1 MEMBER OF u.ratings");
$q->setParameter(1, $rating1);
/* A sub-select is used */
65. Doctrine | SymfonyCon2019 65
Writing using DQL
●
INSERT not possible
●
DELETE and UPDATE are OK
●
Warning, this could desync the UnitOfWork
$user = $em->find('EntitiesUser', 66);
$q = $em->createQuery('DELETE EntitiesUser u WHERE u.id=66');
$q->execute();
$user->setName('hello');
$em->flush(); /* UPDATE users SET name = ? WHERE id = ? */
66. Doctrine | SymfonyCon2019 66
SQL
●
SQL can still be used, through DBAL
●
But :
●
That bypasses all the Entities and the mapping done
●
That can desync the UnitOfWork
$results = $em->getConnection()->fetchAll("/* some query here*/") ;
67. Doctrine | SymfonyCon2019 67
DQL or SQL ?
●
DQL uses Entities and mapping informations, not the DB and its tables directly
●
DQL is parsed and turned to SQL
●
This transformation should get cached
●
$query->getSQL();
●
DQL can be deeply hooked
●
DQL can return Entities
●
SQL returns arrays
68. Doctrine | SymfonyCon2019 68
Cache
●
Several caches may (must) be used
●
"Query Cache" (DQL->SQL)
●
Result Cache : caches a result from a query
●
Metadata Cache
●
Using SF, Doctrine will be bound to SF caches
69. Doctrine | SymfonyCon2019 69
Query Cache
●
Activated per query
$conf = new DoctrineORMConfiguration();
$conf->setResultCacheImpl(new DoctrineCommonCacheApcCache());
$q = $em->createQuery('SELECT u, r, r2 FROM EntitiesUser u JOIN u.ratings r
JOIN u.ratingsConcerned r2');
$q->useResultCache(1, 3600, 'foo_result') ;
$r = $q->execute();
71. Doctrine | SymfonyCon2019 71
Practice
●
Setup symfony-demo
●
Get familiar with the DB structure
●
Navigate into the BlogController and the PostRepository
> composer create-project symfony/symfony-demo some_project_dir
> bin/console s:r
73. Doctrine | SymfonyCon2019 73
Practice
●
Create a query using DQL to get 3 random posts
●
See how the authors are replaced with proxies
●
Access one author field
●
See how N+1 query is issued by Doctrine
●
Give a hint to the QueryBuilder to load partial entities
●
See how dependencies are now NULLed
●
Change the fetchmode of the author dependency to EAGER
●
See how the authors are now gathered by Doctrine using N+1
●
In every case, dump the identity map and check the state of each
entity as being STATE_MANAGED
74. Doctrine | SymfonyCon2019 74
Practice
●
Create a query using DQL to get 3 random posts
●
Get the first post from the collection
●
Modify the post content
●
Compute the changeset from the UOW
●
Dump the changeset
●
Change the tracking policy of posts to DEFERRED_EXPLICIT
●
Modify the post content
●
Compute the changeset from the UOW
●
Dump the changeset
●
What happens ? How to do ?
75. Doctrine | SymfonyCon2019 75
Practice
●
Add a new "avatar" field to the User
●
Play the migration
●
Patch the User form to add a new type to upload an avatar
●
Create an entityListener to treat the avatar
●
The DB should save the avatar file path
76. Doctrine | SymfonyCon2019 76
Practice
●
Add a new RoleType to the Type mappings
●
This type maps sf ROLE_** to an integer
77. Doctrine | SymfonyCon2019 77
Practice
●
Create a query to get the comments written by ROLE_ADMIN
●
Delete those