Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Extending Doctrine 2 for your Domain Model

Ross Tuck
June 09, 2012
2.9k

Extending Doctrine 2 for your Domain Model

As presented at the Dutch PHP Conference 2012

Ross Tuck

June 09, 2012
Tweet

Transcript

  1. /** @Entity */ class Article { Model /** @Id @GeneratedValue

    * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="text") */ protected $content; }
  2. use Doctrine\ORM\Query\Filter\SQLFilter; class CommentFilter extends SQLFilter { public function addFilterConstraint($entityInfo,

    $alias) { if ($entityInfo->name !== 'Comment') { return ""; } return $alias.".approved = 1"; } } Filter
  3. Controller As the Admin Output: Doug isn't human Doug is

    the best $comments = $em->getRepository('Comment')->findAll(); foreach($comments as $comment) { echo $comment->getContent()."\n"; }
  4. use Doctrine\ORM\Query\Filter\SQLFilter; class CommentFilter extends SQLFilter { public function addFilterConstraint($entityInfo,

    $alias) { if ($entityInfo->name !== 'Comment') { return ""; } $level = $this->getParameter('level'); return $alias.".approved = 1"; } } Filter
  5. use Doctrine\ORM\Query\Filter\SQLFilter; class CommentFilter extends SQLFilter { public function addFilterConstraint($entityInfo,

    $alias) { if ($entityInfo->name !== 'Comment') { return ""; } $level = $this->getParameter('level'); return $alias.".approved = ".$level; } } Filter
  6. use Doctrine\ORM\Query\Filter\SQLFilter; class CommentFilter extends SQLFilter { public function addFilterConstraint($entityInfo,

    $alias) { if ($entityInfo->name !== 'Comment') { return ""; } $level = $this->getParameter('level'); return $alias.".approved = ".$level; } } Filter Already escaped
  7. /** @Entity */ class Article { Model /** @Id @GeneratedValue

    * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="text") */ protected $content; }
  8. /** @Entity */ class Article { Model /** @Id @GeneratedValue

    * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; }
  9. /** @Entity */ class Article { Model /** @Id @GeneratedValue

    * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="datetime") */ protected $updatedAt; }
  10. /** @Entity */ class Article { Model // ... public

    function markAsUpdated() { $this->updatedAt = new \Datetime('now'); } }
  11. /** @Entity @HasLifecycleCallbacks */ class Article { Model // ...

    /** @PreUpdate */ public function markAsUpdated() { $this->updatedAt = new \Datetime('now'); } } Event mapping
  12. /** @Entity @HasLifecycleCallbacks */ class Article { Model // ...

    /** @PreUpdate */ public function markAsUpdated() { $this->updatedAt = new \Datetime('now'); } }
  13. • Limited to model dependencies • Do you really need

    that function? • Protected function? Silent error • Repeated Code • Performance implications
  14. class UpdateTimeListener { Listener public function preUpdate($event) { } public

    function getSubscribedEvents() { return array(Events::preUpdate); } }
  15. use Doctrine\Common\EventSubscriber; class UpdateTimeListener implements EventSubscriber { Listener public function

    preUpdate($event) { } public function getSubscribedEvents() { return array(Events::preUpdate); } }
  16. class UpdateTimeListener implements EventSubscriber { Listener public function getSubscribedEvents() {

    return array(Events::preUpdate); } public function preUpdate($event) { } }
  17. public function preUpdate($event) { Listener $em = $event->getEntityManager(); $model =

    $event->getEntity(); if (!$model instanceof Article) { return; } $model->setUpdatedAt(new \Datetime('now')); $uow = $event->getEntityManager()->getUnitOfWork(); $uow->recomputeSingleEntityChangeSet( $em->getClassMetadata('Article'), $model ); }
  18. public function preUpdate($event) { Listener $em = $event->getEntityManager(); $model =

    $event->getEntity(); if (!$model instanceof Article) { return; } $model->setUpdatedAt(new \Datetime('now')); $uow = $event->getEntityManager()->getUnitOfWork(); $uow->recomputeSingleEntityChangeSet( $em->getClassMetadata('Article'), $model ); }
  19. public function preUpdate($event) { Listener $em = $event->getEntityManager(); $model =

    $event->getEntity(); if (!$model instanceof LastUpdatedInterface) { return; } $model->setUpdatedAt(new \Datetime('now')); $uow = $event->getEntityManager()->getUnitOfWork(); $uow->recomputeSingleEntityChangeSet( $em->getClassMetadata('Article'), $model ); }
  20. public function preUpdate($event) { Listener $em = $event->getEntityManager(); $model =

    $event->getEntity(); if (!$model instanceof LastUpdatedInterface) { return; } $model->setUpdatedAt(new \Datetime('now')); $uow = $event->getEntityManager()->getUnitOfWork(); $uow->recomputeSingleEntityChangeSet( $em->getClassMetadata(get_class($model)), $model ); }
  21. After every update to an article, I want an email

    of the changes sent to me. Also, bring me more lettuce. TPS Request
  22. Listener class ChangeMailListener implements EventSubscriber { protected $mailMessage; public function

    getSubscribedEvents() { return array(Events::onFlush, Events::postFlush); } }
  23. Listener public function onFlush($event) { $uow = $event->getEntityManager()->getUnitOfWork(); foreach($uow->getScheduledEntityUpdates() as

    $model) { $changeset = $uow->getEntityChangeSet($model); $this->formatAllPrettyInMail($model, $changeset); } }
  24. Listener public function onFlush($event) { $uow = $event->getEntityManager()->getUnitOfWork(); foreach($uow->getScheduledEntityUpdates() as

    $model) { $changeset = $uow->getEntityChangeSet($model); $this->formatAllPrettyInMail($model, $changeset); } } public function postFlush($event) { if (!$this->hasMessage()) { return; } $this->mailTheBoss($this->message); }
  25. Listener public function formatAllPrettyInMail($model, $changes) { if (!$model instanceof Article)

    { return; } $msg = ""; foreach($changes as $field => $values) { $msg .= "{$field} ----- \n". "old: {$values[0]} \n". "new: {$values[1]} \n\n"; } $this->mailMessage .= $msg; }
  26. Model /** @Entity */ class ChangeLog { /** @Id @GeneratedValue

    * @Column(type="integer") */ protected $id; /** @Column(type="text") */ protected $description; }
  27. Listener class ChangeLogger implements EventSubscriber { public function getSubscribedEvents() {

    return array(Events::onFlush); } public function onFlush($event) { } }
  28. Listener::onFlush public function onFlush($event) { $em = $event->getEntityManager(); $uow =

    $em->getUnitOfWork(); foreach($uow->getScheduledEntityUpdates() as $model) { if (!$model instanceof Article) { continue; } $log = new ChangeLog(); $log->setDescription($this->meFormatPrettyOneDay()); $em->persist($log); $uow->computeChangeSet( $em->getClassMetadata('ChangeLog'), $log ); }
  29. Move view counts out of the database into a faster

    system. And don't break everything. TPS Request
  30. Listener class ViewCountListener implements EventSubscriber { public function getSubscribedEvents() {

    return \Doctrine\ORM\Events::postLoad; } public function postLoad($event) { $model = $event->getEntity(); if (!$model instanceof Article) { return; } $currentRank = $this->getCountFromRedis($model); $model->setViewCount($currentRank); } }
  31. /** @Entity */ class Article { Model /** @Id @GeneratedValue

    * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="datetime") */ protected $updatedAt; } Where do they go?
  32. /** @Entity */ class Article { Model /** @Id @GeneratedValue

    * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="datetime") */ protected $updatedAt; }
  33. ??? array(8) { fieldName => "title" type => "string" length

    => NULL precision => 0 scale => 0 nullable => false unique => false columnName => "title" } $metadata->getFieldMapping('title');
  34. class ArticleType extends AbstractType { public function buildForm($builder, array $options)

    { $builder ->add('title') ->add('content'); } } Form in Symfony2
  35. class ArticleType extends AbstractType { public function buildForm($builder, array $options)

    { $builder ->add('title') ->add('content'); } } Form in Symfony2
  36. Symfony Doctrine Bridge switch ($metadata->getTypeOfField($property)) { case 'string': return new

    TypeGuess('text', ...); case 'boolean': return new TypeGuess('checkbox', ...); case 'integer': case 'bigint': case 'smallint': return new TypeGuess('integer', ...); case 'text': return new TypeGuess('textarea', ...); default: return new TypeGuess('text', ...); }
  37. ??? array(15) { fieldName => "comments" mappedBy => "article" targetEntity

    => "Comment" cascade => array(0) {} fetch => 2 type => 4 isOwningSide => false sourceEntity => "Article" ... $metadata->getAssociationMapping('comments');
  38. class ArticleType extends AbstractType { public function buildForm($builder, array $options)

    { $builder ->add('title') ->add('content') ->add('comments'); } } Form in Symfony2
  39. test test test test test test test test test test

    test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test