SlideShare a Scribd company logo
1 of 108
Download to read offline
Architectes techniques chez
depuis 2011
10 ans d’expérience cumulée sur une vingtaine de projets Symfony2
de tous types (système d’information, site web grand public …)
1
1 Produit 1 120,53 16 15
2 Produit 2 53,69 23 NULL
3 Produit 3 27,17 39 20
Produit 1 120,53
Produit 2 NULL
Produit 3 297,73
title, pretax_price * (in_stock – on_order) AS total
products;
Produit 1 120,53
Produit 2 1 234,87
Produit 3 297,73
title, pretax_price * (in_stock – IFNULL(on_order, 0)) AS total
products;
# Doctrine Configuration
doctrine:
orm:
entity_managers:
default:
...
dql:
string_functions:
IFNULL: MyProjectMyBundleDoctrineIfNull
use DoctrineORMQueryASTFunctionsFunctionNode,
DoctrineORMQueryLexer;
class IfNull extends FunctionNode
{
private $expr1;
private $expr2;
public function parse(DoctrineORMQueryParser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->expr1 = $parser->ArithmeticExpression();
$parser->match(Lexer::T_COMMA);
$this->expr2 = $parser->ArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(DoctrineORMQuerySqlWalker $sqlWalker)
{
return 'IFNULL('
. $sqlWalker->walkArithmeticPrimary($this->expr1) . ', '
. $sqlWalker->walkArithmeticPrimary($this->expr2) . ')';
}
}
use DoctrineORMQueryASTFunctionsFunctionNode,
DoctrineORMQueryLexer;
class IfNull extends FunctionNode
{
private $expr1;
private $expr2;
public function parse(DoctrineORMQueryParser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->expr1 = $parser->ArithmeticExpression();
$parser->match(Lexer::T_COMMA);
$this->expr2 = $parser->ArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(DoctrineORMQuerySqlWalker $sqlWalker)
{
return 'IFNULL('
. $sqlWalker->walkArithmeticPrimary($this->expr1) . ', '
. $sqlWalker->walkArithmeticPrimary($this->expr2) . ')';
}
}
use DoctrineORMQueryASTFunctionsFunctionNode,
DoctrineORMQueryLexer;
class IfNull extends FunctionNode
{
private $expr1;
private $expr2;
public function parse(DoctrineORMQueryParser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->expr1 = $parser->ArithmeticExpression();
$parser->match(Lexer::T_COMMA);
$this->expr2 = $parser->ArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(DoctrineORMQuerySqlWalker $sqlWalker)
{
return 'IFNULL('
. $sqlWalker->walkArithmeticPrimary($this->expr1) . ', '
. $sqlWalker->walkArithmeticPrimary($this->expr2) . ')';
}
}
use DoctrineORMQueryASTFunctionsFunctionNode,
DoctrineORMQueryLexer;
class IfNull extends FunctionNode
{
private $expr1;
private $expr2;
public function parse(DoctrineORMQueryParser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->expr1 = $parser->ArithmeticExpression();
$parser->match(Lexer::T_COMMA);
$this->expr2 = $parser->ArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(DoctrineORMQuerySqlWalker $sqlWalker)
{
return 'IFNULL('
. $sqlWalker->walkArithmeticPrimary($this->expr1) . ', '
. $sqlWalker->walkArithmeticPrimary($this->expr2) . ')';
}
}
use DoctrineORMQueryASTFunctionsFunctionNode,
DoctrineORMQueryLexer;
class IfNull extends FunctionNode
{
private $expr1;
private $expr2;
public function parse(DoctrineORMQueryParser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->expr1 = $parser->ArithmeticExpression();
$parser->match(Lexer::T_COMMA);
$this->expr2 = $parser->ArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(DoctrineORMQuerySqlWalker $sqlWalker)
{
return 'IFNULL('
. $sqlWalker->walkArithmeticPrimary($this->expr1) . ', '
. $sqlWalker->walkArithmeticPrimary($this->expr2) . ')';
}
}
use DoctrineORMQueryASTFunctionsFunctionNode,
DoctrineORMQueryLexer;
class IfNull extends FunctionNode
{
private $expr1;
private $expr2;
public function parse(DoctrineORMQueryParser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->expr1 = $parser->ArithmeticExpression();
$parser->match(Lexer::T_COMMA);
$this->expr2 = $parser->ArithmeticExpression();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(DoctrineORMQuerySqlWalker $sqlWalker)
{
return 'IFNULL('
. $sqlWalker->walkArithmeticPrimary($this->expr1) . ', '
. $sqlWalker->walkArithmeticPrimary($this->expr2) . ')';
}
}
$this->createQueryBuilder('p')
->select('p.title')
->addSelect('p.pretaxPrice * (p.inStock - IFNULL(p.onOrder, 0)) AS total')
->getQuery()
->getArrayResult();
2
 prePersist / postPersist
 preUpdate / postUpdate
 preRemove / postRemove
 postLoad
 loadClassMetadata
 preFlush / onFlush / postFlush
/**
* Article
*
* @ORMTable(name="article")
* @ORMEntity()
* @ORMHasLifecycleCallbacks()
*/
class Article
{
/**
* Date de création
* @var DateTime $createdAt
*
* @ORMColumn(name="created_at", type="datetime", nullable=true)
*/
private $createdAt;
/**
* Article
*
* @ORMTable(name="article")
* @ORMEntity()
* @ORMHasLifecycleCallbacks()
*/
class Article
{
(...)
/**
* Callback sur la création d'un article
* @ORMPrePersist
*/
public function onCreate()
{
$this->createdAt = new DateTime();
}
 Restreint à l’instance de l’entité elle-même
 Duplication de code pour les comportements récurrents
 Les modifications sur les collections ne sont pas gérées
 L’ajout d’une nouvelle entité n’est pas géré
 Impossible d'accéder aux services de l’application
 Difficile à utiliser sur les entités éventuelles de bundles tiers
services:
app.subscriber.entity_timestamper.:
class: AppBundleEventListenerEntityTimestamperSubscriber
tags:
- { name: doctrine.event_subscriber, connection: default }
/**
* Décrit une entité dont la date de création peut être mise à jour automatiquement
*
* @package AppBundleEntity
*/
interface CreationTimestampableEntityInterface {
/**
* Permet de déterminer la date de création de l'entité
*
* @param DateTime $createdAt
* @return mixed
*/
public function setCreatedAt(DateTime $createdAt);
}
/**
* Décrit une entité dont la date de création peut être mise à jour automatiquement
*
* @package AppBundleEntity
*/
interface CreationTimestampableEntityInterface {
/**
* Permet de déterminer la date de création de l'entité
*
* @param DateTime $createdAt
* @return mixed
*/
public function setCreatedAt(DateTime $createdAt);
}
class Article implements CreationTimestampableEntityInterface
class EntityTimestamperSubscriber implements EventSubscriber {
(...)
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof CreationTimestampableEntityInterface) {
return;
}
$entity->setCreatedAt(new DateTime());
}
class EntityTimestamperSubscriber implements EventSubscriber {
(...)
public function preUpdate(PreUpdateEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof UpdateTimestampableEntityInterface) {
return;
}
$entity->setUpdatedAt(new DateTime());
$args->getEntityManager()->getUnitOfWork()->recomputeSingleEntityChangeSet(
$args->getEntityManager()->getClassMetadata(get_class($entity)),
$entity
);
}
class EntityTimestamperSubscriber implements EventSubscriber {
(...)
public function preUpdate(PreUpdateEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof UpdateTimestampableEntityInterface) {
return;
}
$entity->setUpdatedAt(new DateTime());
$args->getEntityManager()->getUnitOfWork()->recomputeSingleEntityChangeSet(
$args->getEntityManager()->getClassMetadata(get_class($entity)),
$entity
);
}
class EntityTimestamperSubscriber implements EventSubscriber {
(...)
public function preUpdate(PreUpdateEventArgs $args)
{
// Récupérer l'ensemble des modifications sur l'entité
$changes = $args->getEntityChangeSet();
// Modifier une valeur précédemment modifiée
if ($args->hasChangedField('title')) {
$args->setNewValue('title', $args->getNewValue('title') . ' (edit)');
}
}
class EntityTimestamperSubscriber implements EventSubscriber {
(...)
public function preUpdate(PreUpdateEventArgs $args)
{
// Récupérer l'ensemble des modifications sur l'entité
$changes = $args->getEntityChangeSet();
// Modifier une valeur précédemment modifiée
if ($args->hasChangedField('title')) {
$args->setNewValue('title', $args->getNewValue('title') . ' (edit)');
}
}
class EntityTimestamperSubscriber implements EventSubscriber {
(...)
public function preUpdate(PreUpdateEventArgs $args)
{
// Récupérer l'ensemble des modifications sur l'entité
$changes = $args->getEntityChangeSet();
// Modifier une valeur précédemment modifiée
if ($args->hasChangedField('title')) {
$args->setNewValue('title', $args->getNewValue('title') . ' (edit)');
}
}
 Mutualisation du code pour les comportements récurrents
 Accès à l’ensemble des opérations de l’application, y compris sur
les entités de bundles tiers
 Accès aux services de l’application si besoin
 La modification des collections de l’entité n’est pas prise en
compte
 L’ajout/suppression d’une entité ne sont pas gérés
 Le subscriber est appelé pour chaque entité modifiée/ajoutée
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
// ....
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
// ...
}
foreach ($uow->getScheduledEntityDeletions() as $entity) {
// ....
}
foreach ($uow->getScheduledCollectionDeletions() as $col) {
// .....
}
foreach ($uow->getScheduledCollectionUpdates() as $col) {
// .....
}
}
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
// ....
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
// ...
}
foreach ($uow->getScheduledEntityDeletions() as $entity) {
// ....
}
foreach ($uow->getScheduledCollectionDeletions() as $col) {
// .....
}
foreach ($uow->getScheduledCollectionUpdates() as $col) {
// .....
}
}
/**
* Log les modifications sur les entités
*
* @package AppBundleEventListener
*/
class UpdatedEntityLoggerSubscriber implements EventSubscriber
{
…
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
// ....
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
// ...
}
foreach ($uow->getScheduledEntityDeletions() as $entity) {
// ....
}
foreach ($uow->getScheduledCollectionDeletions() as $col) {
// .....
}
foreach ($uow->getScheduledCollectionUpdates() as $col) {
// .....
}
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if (!$entity instanceof LoggableEntityInterface) {
return;
}
$log = new EntityLog();
$log->setUpdatedFields($uow->getEntityChangeSet($entity));
$log->setTargetId($entity->getId());
$log->setType($entity->getEntityName());
$em->persist($log);
}
$uow->computeChangeSets();
3
1 Produit x 120,53 16 …
… … … … …
30000 Produit y 27,17 39 …
$results = $this->createQueryBuilder('p')
->getQuery()
->getResult();
foreach ($results as $result) {
$result->setPretaxPrice(
$result->getPretaxPrice() - $this->getProductDiscount($result->getId())
);
}
$this->getEntityManager()->flush();
23 435 218,75
$i = 0;
$batchSize = 20;
$results = $this->createQueryBuilder('p')
->getQuery()
->getResult();
foreach ($results as $result) {
$result->setPretaxPrice(
$result->getPretaxPrice() - $this->getProductDiscount($result->getId())
);
if (($i % $batchSize) === 0) {
$this->getEntityManager()->flush();
}
++$i;
}
$this->getEntityManager()->flush();
$this->getEntityManager()->clear();
1 419 137 237,50
// Désactivation des logs SQL
$this->getEntityManager()->getConnection()->getConfiguration()->setSQLLogger(null);
1 020 377 117,25
114.7528 705
public function importPrice()
{
$em = $this->getEntityManager();
$connection = $em->getConnection();
$connection->getConfiguration()->setSQLLogger(null);
$results = $this->createQueryBuilder('p')->getQuery();
foreach ($results->iterate() as $result) {
$result[0]->setPretaxPrice(
$result[0]->getPretaxPrice() - $this->getProductDiscount($result->getId())
);
$em->flush($result[0]);
$em->detach($result[0]);
}
}
8,50482 825
public function importPrice()
{
$em = $this->getEntityManager();
$connection = $em->getConnection();
$connection->getConfiguration()->setSQLLogger(null);
$results = $this->createQueryBuilder('p')->getQuery();
$connection->beginTransaction();
foreach ($results->iterate() as $result) {
$result[0]->setPretaxPrice(
$result[0]->getPretaxPrice() - $this->getProductDiscount($result->getId())
);
$em->flush($result[0]);
$em->detach($result[0]);
}
$connection->commit();
}
173,0818 373
public function importPrice()
{
$em = $this->getEntityManager();
$connection = $em->getConnection();
$connection->getConfiguration()->setSQLLogger(null);
$results = $this->createQueryBuilder('p')->getQuery();
$connection->beginTransaction();
foreach ($results->iterate() as $result) {
$connection->update(
$em->getClassMetadata('AppBundle:Product')->getTableName(),
array(
'pretax_price' => $result[0]->getPretaxPrice() –
$this->getProductDiscount($result->getId())
),
array('id' => $result[0]->getId())
);
$em->detach($result[0]);
}
$connection->commit();
}
8,256 453
public function importPrice()
{
$this->getEntityManager()->createQueryBuilder()
->update('AppBundle:Product', 'p')
->set('p.pretaxPrice', 'p.pretaxPrice * 0.9')
->getQuery()
->execute();
}
4,50158
public function getAllProductsNoPartial()
{
return $this->createQueryBuilder('p')
->select('p, c')
->leftJoin('p.category', 'c')
->getQuery()
->getResult();
}
p0_.id AS id_0,
p0_.title AS title_1,
p0_.vat_rate AS vat_rate_2,
p0_.pretax_price AS pretax_price_3,
p0_.in_stock AS in_stock_4,
p0_.on_order AS on_order_5,
p0_.created_at AS created_at_6,
p0_.updated_at AS updated_at_7,
p0_.deleted AS deleted_8,
c1_.id AS id_9,
c1_.title AS title_10,
c1_.created_at AS created_at_11,
c1_.updated_at AS updated_at_12,
p0_.category_id AS category_id_13
product p0_
category c1_ ON p0_.category_id = c1_.id;
public function getAllProductsNoPartial()
{
return $this->createQueryBuilder('p')
->select('p.id, p.title, p.pretaxPrice, p.vatRate')
->addSelect('c.id, c.title')
->leftJoin('p.category', 'c')
->getQuery()
->getResult();
}
p0_.id AS id_0,
p0_.title AS title_1,
p0_.vat_rate AS vat_rate_2,
p0_.pretax_price AS pretax_price_3,
c1_.id AS id_4,
c1_.title AS title_5,
product p0_
category c1_ ON p0_.category_id = c1_.id;
/**
* Product
*
* @ORMTable(name="product")
* @ORMEntity()
*/
class Product
{
/**
* Get fullTaxPrice
*
* @return float
*/
public function getFullTaxPrice()
{
return $this->vatRate * $this->pretaxPrice / 100 + $this->pretaxPrice;
}
public function getAllProductsPartial()
{
return $this->createQueryBuilder('p')
->select('partial p.{id, title, vatRate, pretaxPrice}')
->addSelect('partial c.{id, title}')
->leftJoin('p.category', 'c')
->getQuery()
->getResult();
}
p0_.id AS id_0,
p0_.title AS title_1,
p0_.vat_rate AS vat_rate_2,
p0_.pretax_price AS pretax_price_3,
c1_.id AS id_4,
c1_.title AS title_5,
p0_.category_id AS category_id_6
product p0_
category c1_ ON p0_.category_id = c1_.id ;
 Les requêtes partielles sont à utiliser avec précautions
 Eviter que les instances partielles ne soient transmises à de
grandes portions de code pour éviter tout effet de bord
 Limiter leurs utilisations sur des pages où les performances sont à
privilégier.
4
 Query::HYDRATE_OBJECT
 Query::HYDRATE_ARRAY
 Query::HYDRATE_SCALAR
 Query::HYDRATE_SINGLE_SCALAR
use DoctrineORMInternalHydrationAbstractHydrator;
use PDO;
class KeyValueListHydrator extends AbstractHydrator
{
protected function hydrateAllData()
{
$results = array();
foreach ($this->_stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$this->hydrateRowData($row, $results);
}
return $results;
}
protected function hydrateRowData(array $data, array &$result)
{
$keys = array_keys($data);
$keyCount = count($keys);
// La première colonne est considérée comme étant la clé
$key = $data[$keys[0]];
if ($keyCount == 2) {
// Si deux colonnes alors
// la seconde colonne est considéré comme la valeur
$value = $data[$keys[1]];
} else {
// Sinon le reste des colonnes excepté la premières
// sont considréré comme la talbeau de valeur
array_shift($data);
$value = array_values($data);
}
$result[$key] = $value;
}
protected function hydrateRowData(array $data, array &$result)
{
$keys = array_keys($data);
$keyCount = count($keys);
// La première colonne est considérée comme étant la clé
$key = $data[$keys[0]];
if ($keyCount == 2) {
// Si deux colonnes alors
// la seconde colonne est considéré comme la valeur
$value = $data[$keys[1]];
} else {
// Sinon le reste des colonnes excepté la premières
// sont considréré comme la talbeau de valeur
array_shift($data);
$value = array_values($data);
}
$result[$key] = $value;
}
protected function hydrateRowData(array $data, array &$result)
{
$keys = array_keys($data);
$keyCount = count($keys);
// La première colonne est considérée comme étant la clé
$key = $data[$keys[0]];
if ($keyCount == 2) {
// Si deux colonnes alors
// la seconde colonne est considéré comme la valeur
$value = $data[$keys[1]];
} else {
// Sinon le reste des colonnes excepté la premières
// sont considréré comme la talbeau de valeur
array_shift($data);
$value = array_values($data);
}
$result[$key] = $value;
}
protected function hydrateRowData(array $data, array &$result)
{
$keys = array_keys($data);
$keyCount = count($keys);
// La première colonne est considérée comme étant la clé
$key = $data[$keys[0]];
if ($keyCount == 2) {
// Si deux colonnes alors
// la seconde colonne est considéré comme la valeur
$value = $data[$keys[1]];
} else {
// Sinon le reste des colonnes excepté la premières
// sont considréré comme la talbeau de valeur
array_shift($data);
$value = array_values($data);
}
$result[$key] = $value;
}
protected function hydrateRowData(array $data, array &$result)
{
$keys = array_keys($data);
$keyCount = count($keys);
// La première colonne est considérée comme étant la clé
$key = $data[$keys[0]];
if ($keyCount == 2) {
// Si deux colonnes alors
// la seconde colonne est considéré comme la valeur
$value = $data[$keys[1]];
} else {
// Sinon le reste des colonnes excepté la premières
// sont considréré comme la talbeau de valeur
array_shift($data);
$value = array_values($data);
}
$result[$key] = $value;
}
doctrine:
orm:
hydrators:
KeyValueListHydrator: AppBundleDoctrineHydratorsKeyValueListHydrator
public function getCategoriesList()
{
return $this->createQueryBuilder('c')
->select('c.id, c.title')
->getQuery()
->getResult(‘KeyValueListHydrator');
}
1
Changement des codes
nucléaires
273 … 9
2 Opération militaire en cours Urg … 7
3
Fuite dans les toilettes du
RDC
Une fui … 1
$query = $this->createQueryBuilder('n')
->where('n.accreditationLevel <= :userLevel')
->setParameter('userLevel', $user->getAccreditationLevel())
->getQuery();
namespace AppBundleDoctrineFilter;
use DoctrineORMQueryFilterSQLFilter;
use DoctrineORMMappingClassMetadata;
/**
* Filtre les actualités selon le niveau d'accréditation du lecteur
*/
class NewsAccreditationFilter extends SQLFilter
{
/**
* Gets the SQL query part to add to a query.
*
* @param ClassMetaData $targetEntity
* @param string $targetTableAlias
*
* @return string The constraint SQL if there is available, empty string otherwise.
*/
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
}
}
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if ($targetEntity->name !== 'AppBundleEntityNews') {
return '';
}
try {
$accreditationLevel = $this->getParameter('accreditationLevel');
} catch (InvalidArgumentException $e) {
return '';
}
return sprintf(
'%s.%s <= %s',
$targetTableAlias,
$targetEntity->getColumnName('accreditationLevel'),
$accreditationLevel
);
}
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if ($targetEntity->name !== 'AppBundleEntityNews') {
return '';
}
try {
$accreditationLevel = $this->getParameter('accreditationLevel');
} catch (InvalidArgumentException $e) {
return '';
}
return sprintf(
'%s.%s <= %s',
$targetTableAlias,
$targetEntity->getColumnName('accreditationLevel'),
$accreditationLevel
);
}
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if ($targetEntity->name !== 'AppBundleEntityNews') {
return '';
}
try {
$accreditationLevel = $this->getParameter('accreditationLevel');
} catch (InvalidArgumentException $e) {
return '';
}
return sprintf(
'%s.%s <= %s',
$targetTableAlias,
$targetEntity->getColumnName('accreditationLevel'),
$accreditationLevel
);
}
# Doctrine Configuration
doctrine:
# ..
orm:
# ..
filters:
news_accreditation_filter:
class: AppBundleDoctrineFilterNewsAccreditationFilter
enabled: false
# Doctrine Configuration
doctrine:
# ..
orm:
# ..
filters:
news_accreditation_filter:
class: AppBundleDoctrineFilterNewsAccreditationFilter
enabled: false
# Services Declaration
services:
app.filter.configurator:
class: AppBundleEventListenerFilterConfiguratorListener
arguments:
- "@doctrine.orm.entity_manager"
- "@security.token_storage"
tags:
- { name: kernel.event_listener, event: kernel.request }
namespace AppBundleEventListener;
use SymfonyComponentSecurityCoreAuthenticationTokenStorageTokenStorageInterface;
use DoctrineCommonPersistenceObjectManager;
use DoctrineCommonAnnotationsReader;
/**
* Configuration des filtres Doctrine
*/
class FilterConfiguratorListener
{
/**
* @var DoctrineCommonPersistenceObjectManager
*/
protected $em;
/**
* @var
SymfonyComponentSecurityCoreAuthenticationTokenStorageTokenStorageInterface
*/
protected $tokenStorage;
public function __construct(ObjectManager $em, TokenStorageInterface $tokenStorage)
{
$this->em = $em;
$this->tokenStorage = $tokenStorage;
}
}
/**
* Configuration des filtres Doctrine
*/
class FilterConfiguratorListener
{
// ...
public function onKernelRequest()
{
$filter = $this->em->getFilters()->enable('news_accreditation_filter');
if ($user = $this->getUser()) {
$accreditationLevel = $user->getAccreditationLevel();
}
else {
$accreditationLevel = 0;
}
$filter->setParameter('accreditationLevel', $accreditationLevel);
}
private function getUser()
{
$token = $this->tokenStorage->getToken();
if (!$token || !$user = $token->getUser()) {
return NULL;
}
return $user;
}
}
/**
* Configuration des filtres Doctrine
*/
class FilterConfiguratorListener
{
// ...
public function onKernelRequest()
{
$filter = $this->em->getFilters()->enable('news_accreditation_filter');
if ($user = $this->getUser()) {
$accreditationLevel = $user->getAccreditationLevel();
}
else {
$accreditationLevel = 0;
}
$filter->setParameter('accreditationLevel', $accreditationLevel);
}
private function getUser()
{
$token = $this->tokenStorage->getToken();
if (!$token || !$user = $token->getUser()) {
return NULL;
}
return $user;
}
}
t0.id id_1, t0.title title_2, t0.body body_3,
t0.accreditation_level accreditation_level_4
news t0
((t0.accreditation_level <= '1'))
$news = $em->getRepository('AppBundle:News')->findAll();
t0.id id_1, t0.title title_2, t0.body body_3,
t0.accreditation_level accreditation_level_4
news t0
((t0.accreditation_level <= ‘9'))
<?php
namespace AppBundleDoctrineAnnotation;
use DoctrineCommonAnnotationsAnnotation;
use DoctrineCommonAnnotationsAnnotationTarget;
/**
* Décrit une entité qui peut être supprimée logiquement
*
* @Annotation
* @Target("CLASS")
*/
final class SoftDeletable
{
public $deletionField = 'deleted';
}
namespace AppBundleEntity;
use DoctrineORMMapping as ORM;
use AppBundleDoctrineAnnotationSoftDeletable;
/**
* @SoftDeletable()
* @ORMTable(name="product")
* @ORMEntity(repositoryClass="AppBundleRepositoryProductRepository")
*/
class Product
{
// ...
/**
* Statut de suppression logique du produit
* @var boolean $deleted
*
* @ORMColumn(name="deleted", type="boolean", nullable=true)
*/
private $deleted = false;
// ...
}
# Doctrine Configuration
doctrine:
# ..
orm:
# ..
filters:
soft_deleted_filter:
class: AppBundleDoctrineFilterSoftDeletedFilter
enabled: false
# Doctrine Configuration
doctrine:
# ..
orm:
# ..
filters:
soft_deleted_filter:
class: AppBundleDoctrineFilterSoftDeletedFilter
enabled: false
# Services Declaration
services:
app.filter.configurator:
class: AppBundleEventListenerFilterConfiguratorListener
arguments:
- "@doctrine.orm.entity_manager"
- "@security.token_storage"
- "@annotation_reader"
tags:
- { name: kernel.event_listener, event: kernel.request }
# Doctrine Configuration
doctrine:
# ..
orm:
# ..
filters:
soft_deleted_filter:
class: AppBundleDoctrineFilterSoftDeletedFilter
enabled: false
# Services Declaration
services:
app.filter.configurator:
class: AppBundleEventListenerFilterConfiguratorListener
arguments:
- "@doctrine.orm.entity_manager"
- "@security.token_storage"
- "@annotation_reader"
tags:
- { name: kernel.event_listener, event: kernel.request }
/**
* Configuration des filtres Doctrine
*/
class FilterConfiguratorListener
{
public function onKernelRequest()
{
// Configuration du filtre SoftDeletable
$filter = $this->em->getFilters()->enable('soft_deleted_filter');
$filter->setAnnotationReader($this->annotationReader);
namespace AppBundleDoctrineFilter;
use DoctrineORMQueryFilterSQLFilter;
use DoctrineORMMappingClassMetadata;
use DoctrineCommonAnnotationsReader;
/**
* Filtre les entités supprimées logiquement
*
* @package AppBundleDoctrineFilter
*/
class SoftDeletedFilter extends SQLFilter
{
/**
* Lecteur d'annotation
* @var DoctrineCommonAnnotationsReader
*/
protected $annotationReader;
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
}
public function setAnnotationReader(Reader $annotationReader)
{
$this->annotationReader = $annotationReader;
}
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if (empty($this->annotationReader)) {
return '';
}
// La requête en cours concerne t'elle une entité supprimable logiquement ?
$softDeletable = $this->annotationReader->getClassAnnotation(
$targetEntity->getReflectionClass(),
'AppBundleDoctrineAnnotationSoftDeletable'
);
if (!$softDeletable || empty($softDeletable->deletionField)) {
return '';
}
return sprintf('%s.%s IS NULL', $targetTableAlias, $softDeletable->deletionField);
}
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if (empty($this->annotationReader)) {
return '';
}
// La requête en cours concerne t'elle une entité supprimable logiquement ?
$softDeletable = $this->annotationReader->getClassAnnotation(
$targetEntity->getReflectionClass(),
'AppBundleDoctrineAnnotationSoftDeletable'
);
if (!$softDeletable || empty($softDeletable->deletionField)) {
return '';
}
return sprintf('%s.%s = 0', $targetTableAlias, $softDeletable->deletionField);
}
namespace AppBundleRepository;
class ProductRepository extends EntityRepository
{
/**
* Retourne les produits supprimés
* @return array
*/
public function getDeletedProducts()
{
$em = $this->getEntityManager();
$em->getFilters()->disable('soft_deleted_filter');
$products = $this->findBy(array('deleted' => true));
$em->getFilters()->enable('soft_deleted_filter');
return $products;
}
}
class SoftDeletedFilter extends SQLFilter
{
/**
* Permet de désactiver le filtre temporairement
* @var bool
*/
protected $disabled = false;
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if ($this->disabled || empty($this->annotationReader)) {
return '';
}
// ...
}
public function disable()
{
$this->disabled = true;
}
public function enable()
{
$this->disabled = false;
}
}
namespace AppBundleRepository;
class ProductRepository extends EntityRepository
{
/**
* Retourne les produits supprimés
* @return array
*/
public function getDeletedProducts()
{
$em = $this->getEntityManager();
if ($em->getFilters()->isEnabled('soft_deleted_filter')) {
$em->getFilters()->getFilter('soft_deleted_filter')->disable();
}
$products = $this->findBy(array('deleted' => true));
if ($em->getFilters()->isEnabled('soft_deleted_filter')) {
$em->getFilters()->getFilter('soft_deleted_filter')->enable();
}
return $products;
}
}


More Related Content

What's hot

Юнит тестирование в Zend Framework 2.0
Юнит тестирование в Zend Framework 2.0Юнит тестирование в Zend Framework 2.0
Юнит тестирование в Zend Framework 2.0zfconfua
 
modern javascript, unobtrusive javascript, jquery
modern javascript, unobtrusive javascript, jquerymodern javascript, unobtrusive javascript, jquery
modern javascript, unobtrusive javascript, jqueryAdam Zygadlewicz
 
JAVA Program in NetBeans
JAVA Program in NetBeansJAVA Program in NetBeans
JAVA Program in NetBeansHimanshiSingh71
 
RxSwift 예제로 감잡기
RxSwift 예제로 감잡기RxSwift 예제로 감잡기
RxSwift 예제로 감잡기Yongha Yoo
 
EJEMPLOS DESARROLLADOS
EJEMPLOS DESARROLLADOSEJEMPLOS DESARROLLADOS
EJEMPLOS DESARROLLADOSDarwin Durand
 
Practical JavaScript Programming - Session 3/8
Practical JavaScript Programming - Session 3/8Practical JavaScript Programming - Session 3/8
Practical JavaScript Programming - Session 3/8Wilson Su
 
OpenResty/Lua 70+ Advanced Programming Skills and Optimization tips
OpenResty/Lua 70+ Advanced Programming Skills and Optimization tipsOpenResty/Lua 70+ Advanced Programming Skills and Optimization tips
OpenResty/Lua 70+ Advanced Programming Skills and Optimization tipsHo Kim
 
jQuery sans jQuery
jQuery sans jQueryjQuery sans jQuery
jQuery sans jQuerygoldoraf
 
Assalamualaykum warahmatullahi wabarakatuu
Assalamualaykum warahmatullahi wabarakatuuAssalamualaykum warahmatullahi wabarakatuu
Assalamualaykum warahmatullahi wabarakatuuiswan_di
 
Palestra PythonBrasil[8]
Palestra PythonBrasil[8]Palestra PythonBrasil[8]
Palestra PythonBrasil[8]Thiago Da Silva
 
Hacer una calculadora en Java y en Visual Basic
Hacer una calculadora en Java y en Visual BasicHacer una calculadora en Java y en Visual Basic
Hacer una calculadora en Java y en Visual BasicHumbertoWuwu
 
Practical JavaScript Programming - Session 2/8
Practical JavaScript Programming - Session 2/8Practical JavaScript Programming - Session 2/8
Practical JavaScript Programming - Session 2/8Wilson Su
 
An introduction to functional programming with Go [redux]
An introduction to functional programming with Go [redux]An introduction to functional programming with Go [redux]
An introduction to functional programming with Go [redux]Eleanor McHugh
 
Java AWT Calculadora
Java AWT CalculadoraJava AWT Calculadora
Java AWT Calculadorajubacalo
 
Propuesta..sujei
Propuesta..sujeiPropuesta..sujei
Propuesta..sujeigersonjack
 

What's hot (20)

Sis quiz
Sis quizSis quiz
Sis quiz
 
Юнит тестирование в Zend Framework 2.0
Юнит тестирование в Zend Framework 2.0Юнит тестирование в Zend Framework 2.0
Юнит тестирование в Zend Framework 2.0
 
modern javascript, unobtrusive javascript, jquery
modern javascript, unobtrusive javascript, jquerymodern javascript, unobtrusive javascript, jquery
modern javascript, unobtrusive javascript, jquery
 
Aplicacion turbogenerador java
Aplicacion turbogenerador javaAplicacion turbogenerador java
Aplicacion turbogenerador java
 
JAVA Program in NetBeans
JAVA Program in NetBeansJAVA Program in NetBeans
JAVA Program in NetBeans
 
RxSwift 예제로 감잡기
RxSwift 예제로 감잡기RxSwift 예제로 감잡기
RxSwift 예제로 감잡기
 
EJEMPLOS DESARROLLADOS
EJEMPLOS DESARROLLADOSEJEMPLOS DESARROLLADOS
EJEMPLOS DESARROLLADOS
 
Taller1
Taller1Taller1
Taller1
 
Practical JavaScript Programming - Session 3/8
Practical JavaScript Programming - Session 3/8Practical JavaScript Programming - Session 3/8
Practical JavaScript Programming - Session 3/8
 
Danna y felix 10°
Danna y felix 10°Danna y felix 10°
Danna y felix 10°
 
OpenResty/Lua 70+ Advanced Programming Skills and Optimization tips
OpenResty/Lua 70+ Advanced Programming Skills and Optimization tipsOpenResty/Lua 70+ Advanced Programming Skills and Optimization tips
OpenResty/Lua 70+ Advanced Programming Skills and Optimization tips
 
jQuery sans jQuery
jQuery sans jQueryjQuery sans jQuery
jQuery sans jQuery
 
Assalamualaykum warahmatullahi wabarakatuu
Assalamualaykum warahmatullahi wabarakatuuAssalamualaykum warahmatullahi wabarakatuu
Assalamualaykum warahmatullahi wabarakatuu
 
Palestra PythonBrasil[8]
Palestra PythonBrasil[8]Palestra PythonBrasil[8]
Palestra PythonBrasil[8]
 
JQuery
JQueryJQuery
JQuery
 
Hacer una calculadora en Java y en Visual Basic
Hacer una calculadora en Java y en Visual BasicHacer una calculadora en Java y en Visual Basic
Hacer una calculadora en Java y en Visual Basic
 
Practical JavaScript Programming - Session 2/8
Practical JavaScript Programming - Session 2/8Practical JavaScript Programming - Session 2/8
Practical JavaScript Programming - Session 2/8
 
An introduction to functional programming with Go [redux]
An introduction to functional programming with Go [redux]An introduction to functional programming with Go [redux]
An introduction to functional programming with Go [redux]
 
Java AWT Calculadora
Java AWT CalculadoraJava AWT Calculadora
Java AWT Calculadora
 
Propuesta..sujei
Propuesta..sujeiPropuesta..sujei
Propuesta..sujei
 

Aller plus loin avec Doctrine2

  • 1.
  • 2.
  • 3.
  • 4. Architectes techniques chez depuis 2011 10 ans d’expérience cumulée sur une vingtaine de projets Symfony2 de tous types (système d’information, site web grand public …)
  • 5. 1
  • 6.
  • 7. 1 Produit 1 120,53 16 15 2 Produit 2 53,69 23 NULL 3 Produit 3 27,17 39 20
  • 8. Produit 1 120,53 Produit 2 NULL Produit 3 297,73 title, pretax_price * (in_stock – on_order) AS total products;
  • 9. Produit 1 120,53 Produit 2 1 234,87 Produit 3 297,73 title, pretax_price * (in_stock – IFNULL(on_order, 0)) AS total products;
  • 11. use DoctrineORMQueryASTFunctionsFunctionNode, DoctrineORMQueryLexer; class IfNull extends FunctionNode { private $expr1; private $expr2; public function parse(DoctrineORMQueryParser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $this->expr1 = $parser->ArithmeticExpression(); $parser->match(Lexer::T_COMMA); $this->expr2 = $parser->ArithmeticExpression(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } public function getSql(DoctrineORMQuerySqlWalker $sqlWalker) { return 'IFNULL(' . $sqlWalker->walkArithmeticPrimary($this->expr1) . ', ' . $sqlWalker->walkArithmeticPrimary($this->expr2) . ')'; } }
  • 12. use DoctrineORMQueryASTFunctionsFunctionNode, DoctrineORMQueryLexer; class IfNull extends FunctionNode { private $expr1; private $expr2; public function parse(DoctrineORMQueryParser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $this->expr1 = $parser->ArithmeticExpression(); $parser->match(Lexer::T_COMMA); $this->expr2 = $parser->ArithmeticExpression(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } public function getSql(DoctrineORMQuerySqlWalker $sqlWalker) { return 'IFNULL(' . $sqlWalker->walkArithmeticPrimary($this->expr1) . ', ' . $sqlWalker->walkArithmeticPrimary($this->expr2) . ')'; } }
  • 13. use DoctrineORMQueryASTFunctionsFunctionNode, DoctrineORMQueryLexer; class IfNull extends FunctionNode { private $expr1; private $expr2; public function parse(DoctrineORMQueryParser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $this->expr1 = $parser->ArithmeticExpression(); $parser->match(Lexer::T_COMMA); $this->expr2 = $parser->ArithmeticExpression(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } public function getSql(DoctrineORMQuerySqlWalker $sqlWalker) { return 'IFNULL(' . $sqlWalker->walkArithmeticPrimary($this->expr1) . ', ' . $sqlWalker->walkArithmeticPrimary($this->expr2) . ')'; } }
  • 14. use DoctrineORMQueryASTFunctionsFunctionNode, DoctrineORMQueryLexer; class IfNull extends FunctionNode { private $expr1; private $expr2; public function parse(DoctrineORMQueryParser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $this->expr1 = $parser->ArithmeticExpression(); $parser->match(Lexer::T_COMMA); $this->expr2 = $parser->ArithmeticExpression(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } public function getSql(DoctrineORMQuerySqlWalker $sqlWalker) { return 'IFNULL(' . $sqlWalker->walkArithmeticPrimary($this->expr1) . ', ' . $sqlWalker->walkArithmeticPrimary($this->expr2) . ')'; } }
  • 15. use DoctrineORMQueryASTFunctionsFunctionNode, DoctrineORMQueryLexer; class IfNull extends FunctionNode { private $expr1; private $expr2; public function parse(DoctrineORMQueryParser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $this->expr1 = $parser->ArithmeticExpression(); $parser->match(Lexer::T_COMMA); $this->expr2 = $parser->ArithmeticExpression(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } public function getSql(DoctrineORMQuerySqlWalker $sqlWalker) { return 'IFNULL(' . $sqlWalker->walkArithmeticPrimary($this->expr1) . ', ' . $sqlWalker->walkArithmeticPrimary($this->expr2) . ')'; } }
  • 16. use DoctrineORMQueryASTFunctionsFunctionNode, DoctrineORMQueryLexer; class IfNull extends FunctionNode { private $expr1; private $expr2; public function parse(DoctrineORMQueryParser $parser) { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); $this->expr1 = $parser->ArithmeticExpression(); $parser->match(Lexer::T_COMMA); $this->expr2 = $parser->ArithmeticExpression(); $parser->match(Lexer::T_CLOSE_PARENTHESIS); } public function getSql(DoctrineORMQuerySqlWalker $sqlWalker) { return 'IFNULL(' . $sqlWalker->walkArithmeticPrimary($this->expr1) . ', ' . $sqlWalker->walkArithmeticPrimary($this->expr2) . ')'; } }
  • 17. $this->createQueryBuilder('p') ->select('p.title') ->addSelect('p.pretaxPrice * (p.inStock - IFNULL(p.onOrder, 0)) AS total') ->getQuery() ->getArrayResult();
  • 18.
  • 19.
  • 20. 2
  • 21.  prePersist / postPersist  preUpdate / postUpdate  preRemove / postRemove  postLoad
  • 22.  loadClassMetadata  preFlush / onFlush / postFlush
  • 23. /** * Article * * @ORMTable(name="article") * @ORMEntity() * @ORMHasLifecycleCallbacks() */ class Article { /** * Date de création * @var DateTime $createdAt * * @ORMColumn(name="created_at", type="datetime", nullable=true) */ private $createdAt;
  • 24. /** * Article * * @ORMTable(name="article") * @ORMEntity() * @ORMHasLifecycleCallbacks() */ class Article { (...) /** * Callback sur la création d'un article * @ORMPrePersist */ public function onCreate() { $this->createdAt = new DateTime(); }
  • 25.  Restreint à l’instance de l’entité elle-même  Duplication de code pour les comportements récurrents  Les modifications sur les collections ne sont pas gérées  L’ajout d’une nouvelle entité n’est pas géré  Impossible d'accéder aux services de l’application  Difficile à utiliser sur les entités éventuelles de bundles tiers
  • 27. /** * Décrit une entité dont la date de création peut être mise à jour automatiquement * * @package AppBundleEntity */ interface CreationTimestampableEntityInterface { /** * Permet de déterminer la date de création de l'entité * * @param DateTime $createdAt * @return mixed */ public function setCreatedAt(DateTime $createdAt); }
  • 28. /** * Décrit une entité dont la date de création peut être mise à jour automatiquement * * @package AppBundleEntity */ interface CreationTimestampableEntityInterface { /** * Permet de déterminer la date de création de l'entité * * @param DateTime $createdAt * @return mixed */ public function setCreatedAt(DateTime $createdAt); } class Article implements CreationTimestampableEntityInterface
  • 29. class EntityTimestamperSubscriber implements EventSubscriber { (...) public function prePersist(LifecycleEventArgs $args) { $entity = $args->getEntity(); if (!$entity instanceof CreationTimestampableEntityInterface) { return; } $entity->setCreatedAt(new DateTime()); }
  • 30. class EntityTimestamperSubscriber implements EventSubscriber { (...) public function preUpdate(PreUpdateEventArgs $args) { $entity = $args->getEntity(); if (!$entity instanceof UpdateTimestampableEntityInterface) { return; } $entity->setUpdatedAt(new DateTime()); $args->getEntityManager()->getUnitOfWork()->recomputeSingleEntityChangeSet( $args->getEntityManager()->getClassMetadata(get_class($entity)), $entity ); }
  • 31. class EntityTimestamperSubscriber implements EventSubscriber { (...) public function preUpdate(PreUpdateEventArgs $args) { $entity = $args->getEntity(); if (!$entity instanceof UpdateTimestampableEntityInterface) { return; } $entity->setUpdatedAt(new DateTime()); $args->getEntityManager()->getUnitOfWork()->recomputeSingleEntityChangeSet( $args->getEntityManager()->getClassMetadata(get_class($entity)), $entity ); }
  • 32. class EntityTimestamperSubscriber implements EventSubscriber { (...) public function preUpdate(PreUpdateEventArgs $args) { // Récupérer l'ensemble des modifications sur l'entité $changes = $args->getEntityChangeSet(); // Modifier une valeur précédemment modifiée if ($args->hasChangedField('title')) { $args->setNewValue('title', $args->getNewValue('title') . ' (edit)'); } }
  • 33. class EntityTimestamperSubscriber implements EventSubscriber { (...) public function preUpdate(PreUpdateEventArgs $args) { // Récupérer l'ensemble des modifications sur l'entité $changes = $args->getEntityChangeSet(); // Modifier une valeur précédemment modifiée if ($args->hasChangedField('title')) { $args->setNewValue('title', $args->getNewValue('title') . ' (edit)'); } }
  • 34. class EntityTimestamperSubscriber implements EventSubscriber { (...) public function preUpdate(PreUpdateEventArgs $args) { // Récupérer l'ensemble des modifications sur l'entité $changes = $args->getEntityChangeSet(); // Modifier une valeur précédemment modifiée if ($args->hasChangedField('title')) { $args->setNewValue('title', $args->getNewValue('title') . ' (edit)'); } }
  • 35.  Mutualisation du code pour les comportements récurrents  Accès à l’ensemble des opérations de l’application, y compris sur les entités de bundles tiers  Accès aux services de l’application si besoin  La modification des collections de l’entité n’est pas prise en compte  L’ajout/suppression d’une entité ne sont pas gérés  Le subscriber est appelé pour chaque entité modifiée/ajoutée
  • 36. public function onFlush(OnFlushEventArgs $eventArgs) { $em = $eventArgs->getEntityManager(); $uow = $em->getUnitOfWork(); foreach ($uow->getScheduledEntityInsertions() as $entity) { // .... } foreach ($uow->getScheduledEntityUpdates() as $entity) { // ... } foreach ($uow->getScheduledEntityDeletions() as $entity) { // .... } foreach ($uow->getScheduledCollectionDeletions() as $col) { // ..... } foreach ($uow->getScheduledCollectionUpdates() as $col) { // ..... } }
  • 37. public function onFlush(OnFlushEventArgs $eventArgs) { $em = $eventArgs->getEntityManager(); $uow = $em->getUnitOfWork(); foreach ($uow->getScheduledEntityInsertions() as $entity) { // .... } foreach ($uow->getScheduledEntityUpdates() as $entity) { // ... } foreach ($uow->getScheduledEntityDeletions() as $entity) { // .... } foreach ($uow->getScheduledCollectionDeletions() as $col) { // ..... } foreach ($uow->getScheduledCollectionUpdates() as $col) { // ..... } }
  • 38. /** * Log les modifications sur les entités * * @package AppBundleEventListener */ class UpdatedEntityLoggerSubscriber implements EventSubscriber { …
  • 39. public function onFlush(OnFlushEventArgs $eventArgs) { $em = $eventArgs->getEntityManager(); $uow = $em->getUnitOfWork(); foreach ($uow->getScheduledEntityInsertions() as $entity) { // .... } foreach ($uow->getScheduledEntityUpdates() as $entity) { // ... } foreach ($uow->getScheduledEntityDeletions() as $entity) { // .... } foreach ($uow->getScheduledCollectionDeletions() as $col) { // ..... } foreach ($uow->getScheduledCollectionUpdates() as $col) { // ..... } }
  • 40. foreach ($uow->getScheduledEntityUpdates() as $entity) { if (!$entity instanceof LoggableEntityInterface) { return; } $log = new EntityLog(); $log->setUpdatedFields($uow->getEntityChangeSet($entity)); $log->setTargetId($entity->getId()); $log->setType($entity->getEntityName()); $em->persist($log); } $uow->computeChangeSets();
  • 41. 3
  • 42. 1 Produit x 120,53 16 … … … … … … 30000 Produit y 27,17 39 …
  • 43. $results = $this->createQueryBuilder('p') ->getQuery() ->getResult(); foreach ($results as $result) { $result->setPretaxPrice( $result->getPretaxPrice() - $this->getProductDiscount($result->getId()) ); } $this->getEntityManager()->flush();
  • 45. $i = 0; $batchSize = 20; $results = $this->createQueryBuilder('p') ->getQuery() ->getResult(); foreach ($results as $result) { $result->setPretaxPrice( $result->getPretaxPrice() - $this->getProductDiscount($result->getId()) ); if (($i % $batchSize) === 0) { $this->getEntityManager()->flush(); } ++$i; } $this->getEntityManager()->flush(); $this->getEntityManager()->clear();
  • 46. 1 419 137 237,50
  • 47. // Désactivation des logs SQL $this->getEntityManager()->getConnection()->getConfiguration()->setSQLLogger(null);
  • 48. 1 020 377 117,25
  • 50. public function importPrice() { $em = $this->getEntityManager(); $connection = $em->getConnection(); $connection->getConfiguration()->setSQLLogger(null); $results = $this->createQueryBuilder('p')->getQuery(); foreach ($results->iterate() as $result) { $result[0]->setPretaxPrice( $result[0]->getPretaxPrice() - $this->getProductDiscount($result->getId()) ); $em->flush($result[0]); $em->detach($result[0]); } }
  • 52. public function importPrice() { $em = $this->getEntityManager(); $connection = $em->getConnection(); $connection->getConfiguration()->setSQLLogger(null); $results = $this->createQueryBuilder('p')->getQuery(); $connection->beginTransaction(); foreach ($results->iterate() as $result) { $result[0]->setPretaxPrice( $result[0]->getPretaxPrice() - $this->getProductDiscount($result->getId()) ); $em->flush($result[0]); $em->detach($result[0]); } $connection->commit(); }
  • 54. public function importPrice() { $em = $this->getEntityManager(); $connection = $em->getConnection(); $connection->getConfiguration()->setSQLLogger(null); $results = $this->createQueryBuilder('p')->getQuery(); $connection->beginTransaction(); foreach ($results->iterate() as $result) { $connection->update( $em->getClassMetadata('AppBundle:Product')->getTableName(), array( 'pretax_price' => $result[0]->getPretaxPrice() – $this->getProductDiscount($result->getId()) ), array('id' => $result[0]->getId()) ); $em->detach($result[0]); } $connection->commit(); }
  • 56. public function importPrice() { $this->getEntityManager()->createQueryBuilder() ->update('AppBundle:Product', 'p') ->set('p.pretaxPrice', 'p.pretaxPrice * 0.9') ->getQuery() ->execute(); }
  • 58.
  • 59. public function getAllProductsNoPartial() { return $this->createQueryBuilder('p') ->select('p, c') ->leftJoin('p.category', 'c') ->getQuery() ->getResult(); }
  • 60. p0_.id AS id_0, p0_.title AS title_1, p0_.vat_rate AS vat_rate_2, p0_.pretax_price AS pretax_price_3, p0_.in_stock AS in_stock_4, p0_.on_order AS on_order_5, p0_.created_at AS created_at_6, p0_.updated_at AS updated_at_7, p0_.deleted AS deleted_8, c1_.id AS id_9, c1_.title AS title_10, c1_.created_at AS created_at_11, c1_.updated_at AS updated_at_12, p0_.category_id AS category_id_13 product p0_ category c1_ ON p0_.category_id = c1_.id;
  • 61.
  • 62. public function getAllProductsNoPartial() { return $this->createQueryBuilder('p') ->select('p.id, p.title, p.pretaxPrice, p.vatRate') ->addSelect('c.id, c.title') ->leftJoin('p.category', 'c') ->getQuery() ->getResult(); }
  • 63. p0_.id AS id_0, p0_.title AS title_1, p0_.vat_rate AS vat_rate_2, p0_.pretax_price AS pretax_price_3, c1_.id AS id_4, c1_.title AS title_5, product p0_ category c1_ ON p0_.category_id = c1_.id;
  • 64.
  • 65. /** * Product * * @ORMTable(name="product") * @ORMEntity() */ class Product { /** * Get fullTaxPrice * * @return float */ public function getFullTaxPrice() { return $this->vatRate * $this->pretaxPrice / 100 + $this->pretaxPrice; }
  • 66. public function getAllProductsPartial() { return $this->createQueryBuilder('p') ->select('partial p.{id, title, vatRate, pretaxPrice}') ->addSelect('partial c.{id, title}') ->leftJoin('p.category', 'c') ->getQuery() ->getResult(); }
  • 67. p0_.id AS id_0, p0_.title AS title_1, p0_.vat_rate AS vat_rate_2, p0_.pretax_price AS pretax_price_3, c1_.id AS id_4, c1_.title AS title_5, p0_.category_id AS category_id_6 product p0_ category c1_ ON p0_.category_id = c1_.id ;
  • 68.
  • 69.  Les requêtes partielles sont à utiliser avec précautions  Eviter que les instances partielles ne soient transmises à de grandes portions de code pour éviter tout effet de bord  Limiter leurs utilisations sur des pages où les performances sont à privilégier.
  • 70. 4
  • 71.
  • 72.  Query::HYDRATE_OBJECT  Query::HYDRATE_ARRAY  Query::HYDRATE_SCALAR  Query::HYDRATE_SINGLE_SCALAR
  • 73.
  • 74. use DoctrineORMInternalHydrationAbstractHydrator; use PDO; class KeyValueListHydrator extends AbstractHydrator { protected function hydrateAllData() { $results = array(); foreach ($this->_stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { $this->hydrateRowData($row, $results); } return $results; }
  • 75. protected function hydrateRowData(array $data, array &$result) { $keys = array_keys($data); $keyCount = count($keys); // La première colonne est considérée comme étant la clé $key = $data[$keys[0]]; if ($keyCount == 2) { // Si deux colonnes alors // la seconde colonne est considéré comme la valeur $value = $data[$keys[1]]; } else { // Sinon le reste des colonnes excepté la premières // sont considréré comme la talbeau de valeur array_shift($data); $value = array_values($data); } $result[$key] = $value; }
  • 76. protected function hydrateRowData(array $data, array &$result) { $keys = array_keys($data); $keyCount = count($keys); // La première colonne est considérée comme étant la clé $key = $data[$keys[0]]; if ($keyCount == 2) { // Si deux colonnes alors // la seconde colonne est considéré comme la valeur $value = $data[$keys[1]]; } else { // Sinon le reste des colonnes excepté la premières // sont considréré comme la talbeau de valeur array_shift($data); $value = array_values($data); } $result[$key] = $value; }
  • 77. protected function hydrateRowData(array $data, array &$result) { $keys = array_keys($data); $keyCount = count($keys); // La première colonne est considérée comme étant la clé $key = $data[$keys[0]]; if ($keyCount == 2) { // Si deux colonnes alors // la seconde colonne est considéré comme la valeur $value = $data[$keys[1]]; } else { // Sinon le reste des colonnes excepté la premières // sont considréré comme la talbeau de valeur array_shift($data); $value = array_values($data); } $result[$key] = $value; }
  • 78. protected function hydrateRowData(array $data, array &$result) { $keys = array_keys($data); $keyCount = count($keys); // La première colonne est considérée comme étant la clé $key = $data[$keys[0]]; if ($keyCount == 2) { // Si deux colonnes alors // la seconde colonne est considéré comme la valeur $value = $data[$keys[1]]; } else { // Sinon le reste des colonnes excepté la premières // sont considréré comme la talbeau de valeur array_shift($data); $value = array_values($data); } $result[$key] = $value; }
  • 79. protected function hydrateRowData(array $data, array &$result) { $keys = array_keys($data); $keyCount = count($keys); // La première colonne est considérée comme étant la clé $key = $data[$keys[0]]; if ($keyCount == 2) { // Si deux colonnes alors // la seconde colonne est considéré comme la valeur $value = $data[$keys[1]]; } else { // Sinon le reste des colonnes excepté la premières // sont considréré comme la talbeau de valeur array_shift($data); $value = array_values($data); } $result[$key] = $value; }
  • 81. public function getCategoriesList() { return $this->createQueryBuilder('c') ->select('c.id, c.title') ->getQuery() ->getResult(‘KeyValueListHydrator'); }
  • 82.
  • 83.
  • 84. 1 Changement des codes nucléaires 273 … 9 2 Opération militaire en cours Urg … 7 3 Fuite dans les toilettes du RDC Une fui … 1
  • 85. $query = $this->createQueryBuilder('n') ->where('n.accreditationLevel <= :userLevel') ->setParameter('userLevel', $user->getAccreditationLevel()) ->getQuery();
  • 86. namespace AppBundleDoctrineFilter; use DoctrineORMQueryFilterSQLFilter; use DoctrineORMMappingClassMetadata; /** * Filtre les actualités selon le niveau d'accréditation du lecteur */ class NewsAccreditationFilter extends SQLFilter { /** * Gets the SQL query part to add to a query. * * @param ClassMetaData $targetEntity * @param string $targetTableAlias * * @return string The constraint SQL if there is available, empty string otherwise. */ public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) { } }
  • 87. public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) { if ($targetEntity->name !== 'AppBundleEntityNews') { return ''; } try { $accreditationLevel = $this->getParameter('accreditationLevel'); } catch (InvalidArgumentException $e) { return ''; } return sprintf( '%s.%s <= %s', $targetTableAlias, $targetEntity->getColumnName('accreditationLevel'), $accreditationLevel ); }
  • 88. public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) { if ($targetEntity->name !== 'AppBundleEntityNews') { return ''; } try { $accreditationLevel = $this->getParameter('accreditationLevel'); } catch (InvalidArgumentException $e) { return ''; } return sprintf( '%s.%s <= %s', $targetTableAlias, $targetEntity->getColumnName('accreditationLevel'), $accreditationLevel ); }
  • 89. public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) { if ($targetEntity->name !== 'AppBundleEntityNews') { return ''; } try { $accreditationLevel = $this->getParameter('accreditationLevel'); } catch (InvalidArgumentException $e) { return ''; } return sprintf( '%s.%s <= %s', $targetTableAlias, $targetEntity->getColumnName('accreditationLevel'), $accreditationLevel ); }
  • 90. # Doctrine Configuration doctrine: # .. orm: # .. filters: news_accreditation_filter: class: AppBundleDoctrineFilterNewsAccreditationFilter enabled: false
  • 91. # Doctrine Configuration doctrine: # .. orm: # .. filters: news_accreditation_filter: class: AppBundleDoctrineFilterNewsAccreditationFilter enabled: false # Services Declaration services: app.filter.configurator: class: AppBundleEventListenerFilterConfiguratorListener arguments: - "@doctrine.orm.entity_manager" - "@security.token_storage" tags: - { name: kernel.event_listener, event: kernel.request }
  • 92. namespace AppBundleEventListener; use SymfonyComponentSecurityCoreAuthenticationTokenStorageTokenStorageInterface; use DoctrineCommonPersistenceObjectManager; use DoctrineCommonAnnotationsReader; /** * Configuration des filtres Doctrine */ class FilterConfiguratorListener { /** * @var DoctrineCommonPersistenceObjectManager */ protected $em; /** * @var SymfonyComponentSecurityCoreAuthenticationTokenStorageTokenStorageInterface */ protected $tokenStorage; public function __construct(ObjectManager $em, TokenStorageInterface $tokenStorage) { $this->em = $em; $this->tokenStorage = $tokenStorage; } }
  • 93. /** * Configuration des filtres Doctrine */ class FilterConfiguratorListener { // ... public function onKernelRequest() { $filter = $this->em->getFilters()->enable('news_accreditation_filter'); if ($user = $this->getUser()) { $accreditationLevel = $user->getAccreditationLevel(); } else { $accreditationLevel = 0; } $filter->setParameter('accreditationLevel', $accreditationLevel); } private function getUser() { $token = $this->tokenStorage->getToken(); if (!$token || !$user = $token->getUser()) { return NULL; } return $user; } }
  • 94. /** * Configuration des filtres Doctrine */ class FilterConfiguratorListener { // ... public function onKernelRequest() { $filter = $this->em->getFilters()->enable('news_accreditation_filter'); if ($user = $this->getUser()) { $accreditationLevel = $user->getAccreditationLevel(); } else { $accreditationLevel = 0; } $filter->setParameter('accreditationLevel', $accreditationLevel); } private function getUser() { $token = $this->tokenStorage->getToken(); if (!$token || !$user = $token->getUser()) { return NULL; } return $user; } }
  • 95. t0.id id_1, t0.title title_2, t0.body body_3, t0.accreditation_level accreditation_level_4 news t0 ((t0.accreditation_level <= '1')) $news = $em->getRepository('AppBundle:News')->findAll(); t0.id id_1, t0.title title_2, t0.body body_3, t0.accreditation_level accreditation_level_4 news t0 ((t0.accreditation_level <= ‘9'))
  • 96. <?php namespace AppBundleDoctrineAnnotation; use DoctrineCommonAnnotationsAnnotation; use DoctrineCommonAnnotationsAnnotationTarget; /** * Décrit une entité qui peut être supprimée logiquement * * @Annotation * @Target("CLASS") */ final class SoftDeletable { public $deletionField = 'deleted'; }
  • 97. namespace AppBundleEntity; use DoctrineORMMapping as ORM; use AppBundleDoctrineAnnotationSoftDeletable; /** * @SoftDeletable() * @ORMTable(name="product") * @ORMEntity(repositoryClass="AppBundleRepositoryProductRepository") */ class Product { // ... /** * Statut de suppression logique du produit * @var boolean $deleted * * @ORMColumn(name="deleted", type="boolean", nullable=true) */ private $deleted = false; // ... }
  • 98. # Doctrine Configuration doctrine: # .. orm: # .. filters: soft_deleted_filter: class: AppBundleDoctrineFilterSoftDeletedFilter enabled: false
  • 99. # Doctrine Configuration doctrine: # .. orm: # .. filters: soft_deleted_filter: class: AppBundleDoctrineFilterSoftDeletedFilter enabled: false # Services Declaration services: app.filter.configurator: class: AppBundleEventListenerFilterConfiguratorListener arguments: - "@doctrine.orm.entity_manager" - "@security.token_storage" - "@annotation_reader" tags: - { name: kernel.event_listener, event: kernel.request }
  • 100. # Doctrine Configuration doctrine: # .. orm: # .. filters: soft_deleted_filter: class: AppBundleDoctrineFilterSoftDeletedFilter enabled: false # Services Declaration services: app.filter.configurator: class: AppBundleEventListenerFilterConfiguratorListener arguments: - "@doctrine.orm.entity_manager" - "@security.token_storage" - "@annotation_reader" tags: - { name: kernel.event_listener, event: kernel.request }
  • 101. /** * Configuration des filtres Doctrine */ class FilterConfiguratorListener { public function onKernelRequest() { // Configuration du filtre SoftDeletable $filter = $this->em->getFilters()->enable('soft_deleted_filter'); $filter->setAnnotationReader($this->annotationReader);
  • 102. namespace AppBundleDoctrineFilter; use DoctrineORMQueryFilterSQLFilter; use DoctrineORMMappingClassMetadata; use DoctrineCommonAnnotationsReader; /** * Filtre les entités supprimées logiquement * * @package AppBundleDoctrineFilter */ class SoftDeletedFilter extends SQLFilter { /** * Lecteur d'annotation * @var DoctrineCommonAnnotationsReader */ protected $annotationReader; public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) { } public function setAnnotationReader(Reader $annotationReader) { $this->annotationReader = $annotationReader; }
  • 103. public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) { if (empty($this->annotationReader)) { return ''; } // La requête en cours concerne t'elle une entité supprimable logiquement ? $softDeletable = $this->annotationReader->getClassAnnotation( $targetEntity->getReflectionClass(), 'AppBundleDoctrineAnnotationSoftDeletable' ); if (!$softDeletable || empty($softDeletable->deletionField)) { return ''; } return sprintf('%s.%s IS NULL', $targetTableAlias, $softDeletable->deletionField); }
  • 104. public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) { if (empty($this->annotationReader)) { return ''; } // La requête en cours concerne t'elle une entité supprimable logiquement ? $softDeletable = $this->annotationReader->getClassAnnotation( $targetEntity->getReflectionClass(), 'AppBundleDoctrineAnnotationSoftDeletable' ); if (!$softDeletable || empty($softDeletable->deletionField)) { return ''; } return sprintf('%s.%s = 0', $targetTableAlias, $softDeletable->deletionField); }
  • 105. namespace AppBundleRepository; class ProductRepository extends EntityRepository { /** * Retourne les produits supprimés * @return array */ public function getDeletedProducts() { $em = $this->getEntityManager(); $em->getFilters()->disable('soft_deleted_filter'); $products = $this->findBy(array('deleted' => true)); $em->getFilters()->enable('soft_deleted_filter'); return $products; } }
  • 106. class SoftDeletedFilter extends SQLFilter { /** * Permet de désactiver le filtre temporairement * @var bool */ protected $disabled = false; public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) { if ($this->disabled || empty($this->annotationReader)) { return ''; } // ... } public function disable() { $this->disabled = true; } public function enable() { $this->disabled = false; } }
  • 107. namespace AppBundleRepository; class ProductRepository extends EntityRepository { /** * Retourne les produits supprimés * @return array */ public function getDeletedProducts() { $em = $this->getEntityManager(); if ($em->getFilters()->isEnabled('soft_deleted_filter')) { $em->getFilters()->getFilter('soft_deleted_filter')->disable(); } $products = $this->findBy(array('deleted' => true)); if ($em->getFilters()->isEnabled('soft_deleted_filter')) { $em->getFilters()->getFilter('soft_deleted_filter')->enable(); } return $products; } }
  • 108.