Doctrine | SymfonyCon2019 1
Doctrine
-
Doctrine ORM
Doctrine | SymfonyCon2019 2
Doctrine
●
Doctrine project architecture
●
Common
●
DBAL (Database Abstraction Layer)
●
ORM (Object Relationnal Mapping)
●
How to with ORM
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
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 ?
Doctrine | SymfonyCon2019 5
Doctrine project
Doctrine | SymfonyCon2019 6
What we'll study
Doctrine | SymfonyCon2019 7
Doctrine-Annotations
use DoctrineCommonAnnotationsAnnotation;
class MyAnnot extends Annotation
{
public $something;
public $hey = array();
}
/**
* @MyAnnot(something="foo", hey="hola")
*/
class Foo
{ }
$an = new DoctrineCommonAnnotationsAnnotationReader();
var_dump($an->getClassAnnotations(new ReflectionClass('Foo')));
Annotation ::= "@" AnnotationName ["(" [Values] ")"]
AnnotationName ::= QualifiedName | SimpleName
QualifiedName ::= NameSpacePart "" {NameSpacePart ""}* SimpleName
NameSpacePart ::= identifier | null | false | true
SimpleName ::= identifier | null | false | true
array
0 =>
object(MyAnnot)[32]
public 'something' => string 'foo'
public 'hey' => string 'hola'
public 'value' => null
Doctrine | SymfonyCon2019 8
Doctrine - DBAL
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
Doctrine | SymfonyCon2019 10
DBAL : Connection
●
The main element
Doctrine | SymfonyCon2019 11
DBAL : connection
●
Main element
●
Allows to drive the whole DB
●
Like :
●
Connect() - close()
●
Insert()
●
Update() - executeUpdate()
●
Delete()
●
Query() - executeQuery()- exec() - prepare()
●
fetchAll() - fetchArray() - fetchColumn() - fetchRow()
●
BeginTransaction() - commit() - rollback()
Doctrine | SymfonyCon2019 12
DBAL : Easy
$stmt = $con->query("SELECT id, title, comment FROM Ratings");
while($result = $stmt->fetch()) {
vprintf("%d, %s, %s", $result);
}
$params = array('unix_socket'=>'/var/run/mysqld/mysqld.sock',
'user'=>'foobar',
'password'=>'secret',
'driver'=>'pdo_mysql',
'dbname'=>'foobar');
$con = DoctrineDBALDriverManager::getConnection($params);
Doctrine | SymfonyCon2019 13
DBAL looks like PDO
●
DBAL has a very similar interface that PDO
●
But it overpowers it significantly
$stmt = $con->executeQuery("SELECT id, text FROM Ratings WHERE InsertTimeStamp > ?
AND id IN(?)",
[new DateTime("now"), [2,3,4]],
['datetime', DoctrineDBALConnection::PARAM_INT_ARRAY];
while($result = $stmt->fetch()) {
vprintf("%d, %s", $result);
}
$con->delete('Ratings', ['id' => 3] ) ;
$con->update('Ratings', ['comment'=>'hey !!'] ,['id' => 3] ) ;
$con->insert('Ratings', [/* Columns detail */]) ;
Doctrine | SymfonyCon2019 14
DBAL QueryBuilder
●
Build and practice queries with OO API
●
An ExpressionBuilder also exists
$query = $con->createQueryBuilder() ;
$query->from('User', 'u')
->select('u.id')
->addSelect("DATE_FORMAT(User.regdate, '%Y%m%d') as DATESORT")
->add('join', [
'c' => [
'joinType' => 'straight',
'joinTable' => 'Comments',
'joinAlias' => 'comment',
'joinCondition' => 'u.comment=c.id'
]
], true)
/* … etc … */
echo $query; /* __toString() */
Doctrine | SymfonyCon2019 15
DBAL and schema introspection
●
API :
●
listDatabases()
●
listFunctions() - listSequences()
●
listTableColumns($tableName)
●
listTableConstraints($tableName)
●
listTableDetails($tableName)
●
listTableForeignKeys($tableName)
●
listTableIndexes($tableName)
●
listTables() platform = $con->getDatabasePlatform();
$schema = new DoctrineDBALSchemaSchema();
$myTable = $schema->createTable("Users");
$myTable->addColumn("id", "integer", ["unsigned" => true]);
$myTable->addColumn("username", "string", ["length" => 32]);
$myTable->setPrimaryKey(["id"]);
$queries = $schema->toSql($platform);
Doctrine | SymfonyCon2019 16
Doctrine - ORM
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
Doctrine | SymfonyCon2019 18
Doctrine ORM
Entities
Repository
EntityManager
UnitOfWork
DBAL Connection
PDO
ClassMetadata
IdentityMap
DataPersisters
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
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
Doctrine | SymfonyCon2019 21
Types mapping
●
PHP types are mapped onto RDBM types depending on the RDBM brand
●
You can add your own types
●
DoctrineDBALTypesType
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
Doctrine | SymfonyCon2019 23
metadata
var_dump($em->getClassMetadata('FooBar::class'));
object(DoctrineORMMappingClassMetadata)#321 (36) {
["name"]=>
string(7) "FooBar"
["namespace"]=>
string(3) "Foo"
["rootEntityName"]=>
string(7) "FooBar"
["customGeneratorDefinition"]=>
NULL
["customRepositoryClassName"]=>
NULL
["isMappedSuperclass"]=>
bool(false)
["parentClasses"]=>
array(0) {
}
["subClasses"]=>
array(0) {
}
["namedQueries"]=>
array(0) {
}
["namedNativeQueries"]=>
array(0) {
}
...
...
["sqlResultSetMappings"]=>
array(0) {
}
["identifier"]=>
array(1) {
[0]=>
string(2) "id"
}
["inheritanceType"]=>
int(1)
["generatorType"]=>
int(4)
["fieldMappings"]=>
array(8) {
["id"]=>
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
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)
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)
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
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
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
Doctrine | SymfonyCon2019 30
EntityManager
●
find()
●
clear()
●
detach()
●
persist()
●
refresh()
●
remove()
●
flush()
$user = $em->find('EntitiesUsers', 3);
$user = $em->find('EntitiesUsers', 4);
$user = $em->find('EntitiesUsers', 5);
$em->clear() ;
●
clear() empties the IdentityMap in the UnitOfWork
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
Doctrine | SymfonyCon2019 32
Doctrine ORM
Entities
Repository
EntityManager
UnitOfWork
DBAL Connection
PDO
ClassMetadata
IdentityMap
DataPersisters
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()
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)
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"
}
}
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"
}
}
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
Doctrine | SymfonyCon2019 38
Example Deferred implicit
/**
* @ORMTable(name="User")
* @ORMEntity
*/
class User {
$user = $em->find('User', 1);
$user->setAge(30);
$em->flush();
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();
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 */
Doctrine | SymfonyCon2019 41
Repository
●
Place where you write queries concerning an entity
Doctrine | SymfonyCon2019 42
Repository API
●
find(), findAll(), findBy(), findOneBy()
●
Only find() makes use of the IdentityMap
/* findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */
$results = $repo->findBy(array('name'=>'foo'), array('name'=>'asc'), 2, 3);
Doctrine | SymfonyCon2019 43
Association mapping
●
Relations ?
●
OneToOne
●
OneToMany
●
ManyToOne
●
ManyToMany
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;
/* ... */
}
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); }
}
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);
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)
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) ;
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) ;
Doctrine | SymfonyCon2019 50
Collections and hydration
$u = $em->find('EntitiesUser', 1);
$tripStatus = $u->getTripOffer()->get(0)->getTrips()->get(0)->getStatus();
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
Doctrine | SymfonyCon2019 52
Example cascade (persist)
namespace Entities;
/** @Entity */
class User
{
/** @OneToMany(targetEntity="TripOffer", mappedBy="user", cascade={"persist"}) */
protected $tripOffer;
/* ... */
}
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
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();
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();
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();
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]
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'
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'
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)
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 => (...)
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 */
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');
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 */
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 = ? */
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*/") ;
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
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
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();
Doctrine | SymfonyCon2019 70
Q/A
●
?
●
?
●
?
●
?
●
?
●
?
●
?
●
?
●
?
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
Doctrine | SymfonyCon2019 72
Practice
●
Create a query using DQL to get 3 random posts
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
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 ?
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
Doctrine | SymfonyCon2019 76
Practice
●
Add a new RoleType to the Type mappings
●
This type maps sf ROLE_** to an integer
Doctrine | SymfonyCon2019 77
Practice
●
Create a query to get the comments written by ROLE_ADMIN
●
Delete those

Doctrine with Symfony - SymfonyCon 2019

  • 1.
    Doctrine | SymfonyCon20191 Doctrine - Doctrine ORM
  • 2.
    Doctrine | SymfonyCon20192 Doctrine ● Doctrine project architecture ● Common ● DBAL (Database Abstraction Layer) ● ORM (Object Relationnal Mapping) ● How to with ORM
  • 3.
    Doctrine | SymfonyCon20193 The doctrine project ● A bunch of projects ● Like Symfony Components ● Standalone ● Some dependencies around them ● https://www.doctrine-project.org/projects.html
  • 4.
    Doctrine | SymfonyCon20194 ● 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 ?
  • 5.
    Doctrine | SymfonyCon20195 Doctrine project
  • 6.
    Doctrine | SymfonyCon20196 What we'll study
  • 7.
    Doctrine | SymfonyCon20197 Doctrine-Annotations use DoctrineCommonAnnotationsAnnotation; class MyAnnot extends Annotation { public $something; public $hey = array(); } /** * @MyAnnot(something="foo", hey="hola") */ class Foo { } $an = new DoctrineCommonAnnotationsAnnotationReader(); var_dump($an->getClassAnnotations(new ReflectionClass('Foo'))); Annotation ::= "@" AnnotationName ["(" [Values] ")"] AnnotationName ::= QualifiedName | SimpleName QualifiedName ::= NameSpacePart "" {NameSpacePart ""}* SimpleName NameSpacePart ::= identifier | null | false | true SimpleName ::= identifier | null | false | true array 0 => object(MyAnnot)[32] public 'something' => string 'foo' public 'hey' => string 'hola' public 'value' => null
  • 8.
    Doctrine | SymfonyCon20198 Doctrine - DBAL
  • 9.
    Doctrine | SymfonyCon20199 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
  • 10.
    Doctrine | SymfonyCon201910 DBAL : Connection ● The main element
  • 11.
    Doctrine | SymfonyCon201911 DBAL : connection ● Main element ● Allows to drive the whole DB ● Like : ● Connect() - close() ● Insert() ● Update() - executeUpdate() ● Delete() ● Query() - executeQuery()- exec() - prepare() ● fetchAll() - fetchArray() - fetchColumn() - fetchRow() ● BeginTransaction() - commit() - rollback()
  • 12.
    Doctrine | SymfonyCon201912 DBAL : Easy $stmt = $con->query("SELECT id, title, comment FROM Ratings"); while($result = $stmt->fetch()) { vprintf("%d, %s, %s", $result); } $params = array('unix_socket'=>'/var/run/mysqld/mysqld.sock', 'user'=>'foobar', 'password'=>'secret', 'driver'=>'pdo_mysql', 'dbname'=>'foobar'); $con = DoctrineDBALDriverManager::getConnection($params);
  • 13.
    Doctrine | SymfonyCon201913 DBAL looks like PDO ● DBAL has a very similar interface that PDO ● But it overpowers it significantly $stmt = $con->executeQuery("SELECT id, text FROM Ratings WHERE InsertTimeStamp > ? AND id IN(?)", [new DateTime("now"), [2,3,4]], ['datetime', DoctrineDBALConnection::PARAM_INT_ARRAY]; while($result = $stmt->fetch()) { vprintf("%d, %s", $result); } $con->delete('Ratings', ['id' => 3] ) ; $con->update('Ratings', ['comment'=>'hey !!'] ,['id' => 3] ) ; $con->insert('Ratings', [/* Columns detail */]) ;
  • 14.
    Doctrine | SymfonyCon201914 DBAL QueryBuilder ● Build and practice queries with OO API ● An ExpressionBuilder also exists $query = $con->createQueryBuilder() ; $query->from('User', 'u') ->select('u.id') ->addSelect("DATE_FORMAT(User.regdate, '%Y%m%d') as DATESORT") ->add('join', [ 'c' => [ 'joinType' => 'straight', 'joinTable' => 'Comments', 'joinAlias' => 'comment', 'joinCondition' => 'u.comment=c.id' ] ], true) /* … etc … */ echo $query; /* __toString() */
  • 15.
    Doctrine | SymfonyCon201915 DBAL and schema introspection ● API : ● listDatabases() ● listFunctions() - listSequences() ● listTableColumns($tableName) ● listTableConstraints($tableName) ● listTableDetails($tableName) ● listTableForeignKeys($tableName) ● listTableIndexes($tableName) ● listTables() platform = $con->getDatabasePlatform(); $schema = new DoctrineDBALSchemaSchema(); $myTable = $schema->createTable("Users"); $myTable->addColumn("id", "integer", ["unsigned" => true]); $myTable->addColumn("username", "string", ["length" => 32]); $myTable->setPrimaryKey(["id"]); $queries = $schema->toSql($platform);
  • 16.
    Doctrine | SymfonyCon201916 Doctrine - ORM
  • 17.
    Doctrine | SymfonyCon201917 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
  • 18.
    Doctrine | SymfonyCon201918 Doctrine ORM Entities Repository EntityManager UnitOfWork DBAL Connection PDO ClassMetadata IdentityMap DataPersisters
  • 19.
    Doctrine | SymfonyCon201919 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 | SymfonyCon201920 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 | SymfonyCon201921 Types mapping ● PHP types are mapped onto RDBM types depending on the RDBM brand ● You can add your own types ● DoctrineDBALTypesType
  • 22.
    Doctrine | SymfonyCon201922 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
  • 23.
    Doctrine | SymfonyCon201923 metadata var_dump($em->getClassMetadata('FooBar::class')); object(DoctrineORMMappingClassMetadata)#321 (36) { ["name"]=> string(7) "FooBar" ["namespace"]=> string(3) "Foo" ["rootEntityName"]=> string(7) "FooBar" ["customGeneratorDefinition"]=> NULL ["customRepositoryClassName"]=> NULL ["isMappedSuperclass"]=> bool(false) ["parentClasses"]=> array(0) { } ["subClasses"]=> array(0) { } ["namedQueries"]=> array(0) { } ["namedNativeQueries"]=> array(0) { } ... ... ["sqlResultSetMappings"]=> array(0) { } ["identifier"]=> array(1) { [0]=> string(2) "id" } ["inheritanceType"]=> int(1) ["generatorType"]=> int(4) ["fieldMappings"]=> array(8) { ["id"]=>
  • 24.
    Doctrine | SymfonyCon201924 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 | SymfonyCon201925 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 | SymfonyCon201926 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 | SymfonyCon201927 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 | SymfonyCon201928 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 | SymfonyCon201929 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
  • 30.
    Doctrine | SymfonyCon201930 EntityManager ● find() ● clear() ● detach() ● persist() ● refresh() ● remove() ● flush() $user = $em->find('EntitiesUsers', 3); $user = $em->find('EntitiesUsers', 4); $user = $em->find('EntitiesUsers', 5); $em->clear() ; ● clear() empties the IdentityMap in the UnitOfWork
  • 31.
    Doctrine | SymfonyCon201931 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
  • 32.
    Doctrine | SymfonyCon201932 Doctrine ORM Entities Repository EntityManager UnitOfWork DBAL Connection PDO ClassMetadata IdentityMap DataPersisters
  • 33.
    Doctrine | SymfonyCon201933 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 | SymfonyCon201934 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 | SymfonyCon201935 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 | SymfonyCon201936 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 | SymfonyCon201937 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 | SymfonyCon201938 Example Deferred implicit /** * @ORMTable(name="User") * @ORMEntity */ class User { $user = $em->find('User', 1); $user->setAge(30); $em->flush();
  • 39.
    Doctrine | SymfonyCon201939 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 | SymfonyCon201940 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 */
  • 41.
    Doctrine | SymfonyCon201941 Repository ● Place where you write queries concerning an entity
  • 42.
    Doctrine | SymfonyCon201942 Repository API ● find(), findAll(), findBy(), findOneBy() ● Only find() makes use of the IdentityMap /* findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ $results = $repo->findBy(array('name'=>'foo'), array('name'=>'asc'), 2, 3);
  • 43.
    Doctrine | SymfonyCon201943 Association mapping ● Relations ? ● OneToOne ● OneToMany ● ManyToOne ● ManyToMany
  • 44.
    Doctrine | SymfonyCon201944 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 | SymfonyCon201945 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 | SymfonyCon201946 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 | SymfonyCon201947 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 | SymfonyCon201948 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 | SymfonyCon201949 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) ;
  • 50.
    Doctrine | SymfonyCon201950 Collections and hydration $u = $em->find('EntitiesUser', 1); $tripStatus = $u->getTripOffer()->get(0)->getTrips()->get(0)->getStatus();
  • 51.
    Doctrine | SymfonyCon201951 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 | SymfonyCon201952 Example cascade (persist) namespace Entities; /** @Entity */ class User { /** @OneToMany(targetEntity="TripOffer", mappedBy="user", cascade={"persist"}) */ protected $tripOffer; /* ... */ }
  • 53.
    Doctrine | SymfonyCon201953 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 | SymfonyCon201954 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 | SymfonyCon201955 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 | SymfonyCon201956 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 | SymfonyCon201957 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 | SymfonyCon201958 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 | SymfonyCon201959 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 | SymfonyCon201960 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 | SymfonyCon201961 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 | SymfonyCon201962 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 | SymfonyCon201963 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 | SymfonyCon201964 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 | SymfonyCon201965 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 | SymfonyCon201966 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 | SymfonyCon201967 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 | SymfonyCon201968 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 | SymfonyCon201969 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();
  • 70.
    Doctrine | SymfonyCon201970 Q/A ● ? ● ? ● ? ● ? ● ? ● ? ● ? ● ? ● ?
  • 71.
    Doctrine | SymfonyCon201971 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
  • 72.
    Doctrine | SymfonyCon201972 Practice ● Create a query using DQL to get 3 random posts
  • 73.
    Doctrine | SymfonyCon201973 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 | SymfonyCon201974 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 | SymfonyCon201975 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 | SymfonyCon201976 Practice ● Add a new RoleType to the Type mappings ● This type maps sf ROLE_** to an integer
  • 77.
    Doctrine | SymfonyCon201977 Practice ● Create a query to get the comments written by ROLE_ADMIN ● Delete those