Extending Doctrine 2 for your Domain Model

C26bfcbd5f786591e036fa0958a11e8b?s=47 Ross Tuck
June 09, 2012
2.1k

Extending Doctrine 2 for your Domain Model

As presented at the Dutch PHP Conference 2012

C26bfcbd5f786591e036fa0958a11e8b?s=128

Ross Tuck

June 09, 2012
Tweet

Transcript

  1. Ross Tuck Extending Doctrine 2 For Your Domain Model June

    9th, DPC
  2. None
  3. Who Am I?

  4. Ross Tuck

  5. Team Lead at Ibuildings Codemonkey REST nut Hat guy Token

    Foreigner America, &@*! Yeah
  6. @rosstuck #dashinglyHandsome

  7. Quick But Necessary

  8. Doctrine 101

  9. Doctrine 102

  10. Doctrine 102.5

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

    * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="text") */ protected $content; }
  12. $article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $em->flush(); Controller

  13. Filters

  14. None
  15. We need a way to approve comments before they appear

    to all users. TPS Request
  16. $comments = $em->getRepository('Comment')->findAll(); foreach($comments as $comment) { echo $comment->getContent()."\n"; }

    Controller Output: Doug isn't human Doug is the best
  17. $comments = $em->getRepository('Comment')->findAll(); foreach($comments as $comment) { echo $comment->getContent()."\n"; }

    Controller
  18. $comments = $em->createQuery('SELECT c FROM Comment c'); foreach($comments->getResult() as $comment)

    { echo $comment->getContent()."\n"; } Controller
  19. $comments = $em->find('Article', 1)->getComments(); foreach($comments as $comment) { echo $comment->getContent()."\n";

    } Controller Approx 100 places in your code!
  20. Filters New in 2.2

  21. 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
  22. $em->getConfiguration() ->addFilter('approved_comments', 'CommentFilter'); Bootstrap Handy key Class name

  23. $em->getConfiguration() ->addFilter('approved_comments', 'CommentFilter'); $em->getFilters()->enable('approved_comments'); Bootstrap

  24. if ($this->isNormalUser()) { $em->getConfiguration() ->addFilter('approved_comments', 'CommentFilter'); $em->getFilters()->enable('approved_comments'); } Bootstrap

  25. $comments = $em->getRepository('Comment')->findAll(); foreach($comments as $comment) { echo $comment->getContent()."\n"; }

    Controller As Normal User Output: Doug is the best
  26. 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"; }
  27. Parameters

  28. $filter = $em->getFilters()->getFilter('approved_comments'); $filter->setParameter('level', $this->getUserLevel()); Bootstrap

  29. 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
  30. 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
  31. 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
  32. Limitations

  33. Events

  34. prePersist postPersist preUpdate postUpdate preRemove postRemove postLoad loadClassMetadata preFlush onFlush

    postFlush onClear Insert
  35. Callbacks Listeners On the model External objects

  36. Lifecycle Callbacks

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

    * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="text") */ protected $content; }
  38. $article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $em->flush(); Controller

  39. None
  40. Articles must record the last date they were modified in

    any way. TPS Request
  41. $article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $article->setUpdatedAt(new DateTime('now')); $em->flush();

    Controller + 100 other files + testing + missed bugs
  42. /** @Entity */ class Article { Model /** @Id @GeneratedValue

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

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

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

    /** @PreUpdate */ public function markAsUpdated() { $this->updatedAt = new \Datetime('now'); } } Event mapping
  46. $article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $em->flush(); echo $article->getUpdatedAt()->format('c');

    // 2012-05-29T10:48:00+02:00 Controller No Changes!
  47. /** @Entity @HasLifecycleCallbacks */ class Article { Model // ...

    /** @PreUpdate */ public function markAsUpdated() { $this->updatedAt = new \Datetime('now'); } }
  48. What else can I do?

  49. prePersist postPersist preUpdate postUpdate preRemove postRemove postLoad loadClassMetadata preFlush onFlush

    postFlush onClear
  50. prePersist postPersist preUpdate postUpdate preRemove postRemove postLoad loadClassMetadata preFlush onFlush

    postFlush onClear
  51. Protip: Only fires if you're dirty

  52. new MudWrestling();

  53. $article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $em->flush(); // 2012-05-29T10:48:00+02:00

    // 2012-05-29T10:48:00+02:00 Controller 2X
  54. $article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $em->flush(); Controller 'Awesome

    New Story' === 'Awesome New Story' No update
  55. $article = $em->find('Article', 1); $article->setTitle('Fantabulous Updated Story'); $em->flush(); Controller 'Awesome

    New Story' !== 'Fantabulous Updated Story' Update, yeah!
  56. $article = $em->find('Article', 1); $article->setTitle('Awesome New Project'.uniqid()); $em->flush(); Controller

  57. So, here's the thing.

  58. The Thing

  59. Events are cool.

  60. Callbacks?

  61. Eeeeeeeeeeeeeeeeeeh.

  62. • Limited to model dependencies • Do you really need

    that function? • Protected function? Silent error • Repeated Code • Performance implications
  63. Listeners

  64. class UpdateTimeListener { Listener public function preUpdate($event) { } }

  65. $em->getEventManager()->addEventListener( array(Doctrine\ORM\Events::preUpdate), new UpdateTimeListener() ); Bootstrap

  66. Invert

  67. Invert

  68. $em->getEventManager()->addEventListener( array(Doctrine\ORM\Events::preUpdate), new UpdateTimeListener() ); Bootstrap

  69. $em->getEventManager()->addEventSubscriber( new UpdateTimeListener() ); Bootstrap

  70. class UpdateTimeListener { Listener public function preUpdate($event) { } public

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

    preUpdate($event) { } public function getSubscribedEvents() { return array(Events::preUpdate); } }
  72. Functionally? Design? No difference Subscriber

  73. $em->getEventManager()->addEventSubscriber( new ChangeMailListener() ); Bootstrap

  74. $em->getEventManager()->addEventSubscriber( new ChangeMailListener($mailer) ); Bootstrap

  75. class UpdateTimeListener implements EventSubscriber { Listener public function getSubscribedEvents() {

    return array(Events::preUpdate); } public function preUpdate($event) { } }
  76. 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 ); }
  77. $article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $em->flush(); echo $article->getUpdatedAt()->format('c');

    // 2012-05-29T10:48:00+02:00 Controller
  78. Whoopity-doo

  79. Theory Land interface LastUpdatedInterface { public function setUpdatedAt(\Datetime $date); public

    function getUpdatedAt(); }
  80. 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 ); }
  81. 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 ); }
  82. 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 ); }
  83. The POWAH

  84. Flushing And Multiple Events

  85. OnFlush

  86. None
  87. After every update to an article, I want an email

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

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

    $model) { $changeset = $uow->getEntityChangeSet($model); } }
  90. array(1) { ["title"]=> array(2) { [0]=> string(16) "Boring Old Title"

    [1]=> string(16) "Great New Title!" } }
  91. Listener public function onFlush($event) { $uow = $event->getEntityManager()->getUnitOfWork(); foreach($uow->getScheduledEntityUpdates() as

    $model) { $changeset = $uow->getEntityChangeSet($model); $this->formatAllPrettyInMail($model, $changeset); } }
  92. 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); }
  93. That's really all there is to it.

  94. 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; }
  95. Advice: Treat your listeners like controllers

  96. Keep it thin

  97. Crazy Town

  98. None
  99. Write the change messages to the database instead of emailing

    them. TPS Request
  100. Model /** @Entity */ class ChangeLog { /** @Id @GeneratedValue

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

    return array(Events::onFlush); } public function onFlush($event) { } }
  102. 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 ); }
  103. Shiny

  104. Yes, I really used PHPMyAdmin there

  105. Shiny

  106. Also, wow, it worked!

  107. None
  108. UnitOfWork API $uow->getScheduledEntityInsertions(); $uow->getScheduledEntityUpdates(); $uow->getScheduledEntityDeletions(); $uow->getScheduledCollectionUpdates(); $uow->getScheduledCollectionDeletions();

  109. UnitOfWork API $uow->scheduleForInsert(); $uow->scheduleForUpdate(); $uow->scheduleExtraUpdate(); $uow->scheduleForDelete();

  110. UnitOfWork API And many more...

  111. postLoad

  112. None
  113. /** @Entity */ class Article { Model //... /** @Column(type="integer")

    */ protected $viewCount; }
  114. None
  115. MySQL Redis +

  116. Move view counts out of the database into a faster

    system. And don't break everything. TPS Request
  117. $article = $em->find('Article', 1); echo $article->getViewCount(); Controller

  118. /** @Entity */ class Article { Model //... /** @Column(type="integer")

    */ protected $viewCount; }
  119. 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); } }
  120. $article = $em->find('Article', 1); echo $article->getViewCount(); Controller

  121. Many, many other uses.

  122. Class Metadata

  123. /** @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?
  124. /** @Entity */ class Article { Model /** @Id @GeneratedValue

    * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="datetime") */ protected $updatedAt; }
  125. Doctrine\ORM\Mapping\ClassMetadata

  126. ??? $metadata = $em->getClassMetadata('Article');

  127. ??? $metadata = $em->getClassMetadata('Article'); echo $metadata->getTableName(); Output: Article

  128. ??? $metadata = $em->getClassMetadata('Article'); echo $metadata->getTableName(); Output: articles

  129. Doctrine 2.3 will bring NamingStrategy

  130. ??? $conn = $em->getConnection(); $tableName = $metadata->getQuotedTableName($conn); $results = $conn->query('SELECT

    * FROM '.$tableName);
  131. Tip of the Iceberg

  132. ??? array(8) { fieldName => "title" type => "string" length

    => NULL precision => 0 scale => 0 nullable => false unique => false columnName => "title" } $metadata->getFieldMapping('title');
  133. ??? $metadata->getFieldMapping('title'); $metadata->getFieldNames(); $metadata->getReflectionClass(); $metadata->getReflectionProperty('title'); $metadata->getColumnName('title'); $metadata->getTypeOfField('title');

  134. Simple Example

  135. Symple Example

  136. class ArticleType extends AbstractType { public function buildForm($builder, array $options)

    { $builder ->add('title') ->add('content'); } } Form in Symfony2
  137. $entity = new Article(); $form = $this->createForm(new ArticleType(), $entity); Controller

    in Symfony2
  138. None
  139. class ArticleType extends AbstractType { public function buildForm($builder, array $options)

    { $builder ->add('title') ->add('content'); } } Form in Symfony2
  140. None
  141. 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', ...); }
  142. Cool, huh?

  143. Cool, huh?

  144. ??? array(15) { fieldName => "comments" mappedBy => "article" targetEntity

    => "Comment" cascade => array(0) {} fetch => 2 type => 4 isOwningSide => false sourceEntity => "Article" ... $metadata->getAssociationMapping('comments');
  145. ??? $metadata->getAssociationMapping('comments'); $metadata->getAssociationMappings(); $metadata->isSingleValuedAssociation('comments'); $metadata->isCollectionValuedAssociation('comments'); $metadata->getAssociationTargetClass('comments');

  146. class ArticleType extends AbstractType { public function buildForm($builder, array $options)

    { $builder ->add('title') ->add('content') ->add('comments'); } } Form in Symfony2
  147. None
  148. Oh yeah.

  149. You can set all of this stuff.

  150. 100~ public functions

  151. But is there a good reason? Unless you're writing a

    driver, probably not.
  152. Listener class MyListener { public function loadClassMetadata($event) { echo $event->getClassMetadata()->getName();

    } }
  153. Where's the manatee?

  154. You're the manatee.

  155. Epilogue & Service Layers

  156. Model Controller View

  157. Model Service Layer Controller View

  158. Be smart.

  159. Be smart.

  160. Be simple

  161. Don't overuse this.

  162. Test

  163. Test test test

  164. 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
  165. RTFM.

  166. Go forth and rock.

  167. None
  168. You're the manatee.

  169. Thanks to: @boekkooi #doctrine (freenode)

  170. Wikipedia OwnMoment (sxc.hu)

  171. https://joind.in/6251