Extending Doctrine 2 for your Domain Model
Upcoming SlideShare
Loading in...5
×

Like this? Share it with your network

Share

Extending Doctrine 2 for your Domain Model

  • 10,648 views
Uploaded on

UPDATE: My thoughts have changed a bit since I originally presented this talk. It's could still be a useful intro to some of Doctrine's intermediate features (although the docs continue to improve. ...

UPDATE: My thoughts have changed a bit since I originally presented this talk. It's could still be a useful intro to some of Doctrine's intermediate features (although the docs continue to improve. However, I would recommend you take a look at a new talk I've done since then "Models & Service Layers; Hemoglobin & Hobgoblins" which I think does a better job of actually talking about architecture and domain models than this talk does. Thanks! http://www.slideshare.net/rosstuck/models-and-service-layers-hemoglobin-and-hobgoblins

As presented at the Dutch PHP Conference 2012.

Sure, it can save models and do relations but Doctrine 2 can do a whole lot more. This talk introduces features like Events and Filters that can keep your code clean and organized. If you like the idea of automatically updating your solr index or adding audit logs without changing any existing code, this is the talk for you.

The emphasis is on practical application of the lesser known but powerful features. The talk is geared toward people who are curious what Doctrine 2 can offer over a standard database layer or have used it previously.

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
  • @HakanAkta All of the images seem to be in place, the arrow on slide #114 was part of a joke in the presentation where it was supposed to point to the speaker (me). Thanks for pointing it out though!
    Are you sure you want to
    Your message goes here
  • I think the some of the images in the slides are remote and I think they have deleted. For example slide #114 only has an arrow.
    Are you sure you want to
    Your message goes here
No Downloads

Views

Total Views
10,648
On Slideshare
10,174
From Embeds
474
Number of Embeds
8

Actions

Shares
Downloads
93
Comments
2
Likes
35

Embeds 474

http://www.scoop.it 405
https://twitter.com 31
http://protalk.me 26
http://www.php-talks.com 5
http://symfony2developer.com 3
http://getpocket.com 2
https://si0.twimg.com 1
http://dev.symfony2developer.com 1

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. Extending Doctrine 2For Your Domain Model Ross Tuck June 9th, DPC
  • 2. Who Am I?
  • 3. Ross Tuck
  • 4. Team Lead at IbuildingsCodemonkeyREST nutHat guyToken Foreigner America, &@* ! Yeah
  • 5. @rosstuck#dashinglyHandsome
  • 6. Quick But Necessary
  • 7. Doctrine 101
  • 8. Doctrine 102
  • 9. Doctrine 102.5
  • 10. Model /** @Entity */ class Article { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="text") */ protected $content; }
  • 11. Controller $article = $em->find(Article, 1); $article->setTitle(Awesome New Story); $em->flush();
  • 12. Filters
  • 13. TPS RequestWe need a way to approve commentsbefore they appear to all users.
  • 14. Controller $comments = $em->getRepository(Comment)->findAll(); foreach($comments as $comment) { echo $comment->getContent()."n"; }Output:Doug isnt humanDoug is the best
  • 15. Controller $comments = $em->getRepository(Comment)->findAll(); foreach($comments as $comment) { echo $comment->getContent()."n"; }
  • 16. Controller $comments = $em->createQuery(SELECT c FROM Comment c); foreach($comments->getResult() as $comment) { echo $comment->getContent()."n"; }
  • 17. Controller $comments = $em->find(Article, 1)->getComments(); foreach($comments as $comment) { echo $comment->getContent()."n"; } ! ce s in your code Approx 100 pla
  • 18. FiltersNew in 2.2
  • 19. Filter use DoctrineORMQueryFilterSQLFilter; class CommentFilter extends SQLFilter { public function addFilterConstraint($entityInfo, $alias) { if ($entityInfo->name !== Comment) { return ""; } return $alias.".approved = 1"; } }
  • 20. Bootstrap y Ha ndy ke $em->getConfiguration() ->addFilter(approved_comments, CommentFilter); Class name
  • 21. Bootstrap $em->getConfiguration() ->addFilter(approved_comments, CommentFilter); $em->getFilters()->enable(approved_comments);
  • 22. Bootstrap if ($this->isNormalUser()) { $em->getConfiguration() ->addFilter(approved_comments, CommentFilter); $em->getFilters()->enable(approved_comments); }
  • 23. Controller $comments = $em->getRepository(Comment)->findAll(); foreach($comments as $comment) { echo $comment->getContent()."n"; } al UserOutput: A s NormDoug is the best
  • 24. Controller $comments = $em->getRepository(Comment)->findAll(); foreach($comments as $comment) { echo $comment->getContent()."n"; } inOutput: As the AdmDoug isnt humanDoug is the best
  • 25. Parameters
  • 26. Bootstrap $filter = $em->getFilters()->getFilter(approved_comments); $filter->setParameter(level, $this->getUserLevel());
  • 27. Filter use DoctrineORMQueryFilterSQLFilter; class CommentFilter extends SQLFilter { public function addFilterConstraint($entityInfo, $alias) { if ($entityInfo->name !== Comment) { return ""; } $level = $this->getParameter(level); return $alias.".approved = 1"; } }
  • 28. Filter use DoctrineORMQueryFilterSQLFilter; class CommentFilter extends SQLFilter { public function addFilterConstraint($entityInfo, $alias) { if ($entityInfo->name !== Comment) { return ""; } $level = $this->getParameter(level); return $alias.".approved = ".$level; } }
  • 29. Filter use DoctrineORMQueryFilterSQLFilter; class CommentFilter extends SQLFilter { public function addFilterConstraint($entityInfo, $alias) { if ($entityInfo->name !== Comment) { return ""; } Already escaped $level = $this->getParameter(level); return $alias.".approved = ".$level; } }
  • 30. Limitations
  • 31. Events
  • 32. Inser tprePersist postLoadpostPersist loadClassMetadatapreUpdate preFlushpostUpdate onFlushpreRemove postFlushpostRemove onClear
  • 33. Callbacks ListenersOn the model External objects
  • 34. Lifecycle Callbacks
  • 35. Model /** @Entity */ class Article { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="text") */ protected $content; }
  • 36. Controller $article = $em->find(Article, 1); $article->setTitle(Awesome New Story); $em->flush();
  • 37. TPS RequestArticles must record the last date theywere modified in any way.
  • 38. Controller $article = $em->find(Article, 1); $article->setTitle(Awesome New Story); $article->setUpdatedAt(new DateTime(now)); + 100 other files $em->flush(); + testing + missed bugs
  • 39. Model /** @Entity */ class Article { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; }
  • 40. Model /** @Entity */ class Article { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="datetime") */ protected $updatedAt; }
  • 41. Model /** @Entity */ class Article { // ... public function markAsUpdated() { $this->updatedAt = new Datetime(now); } }
  • 42. Model /** @Entity @HasLifecycleCallbacks */ class Article { // ... Event mapping /** @PreUpdate */ public function markAsUpdated() { $this->updatedAt = new Datetime(now); } }
  • 43. Controller $article = $em->find(Article, 1); $article->setTitle(Awesome New Story); No Changes! $em->flush(); echo $article->getUpdatedAt()->format(c); // 2012-05-29T10:48:00+02:00
  • 44. Model /** @Entity @HasLifecycleCallbacks */ class Article { // ... /** @PreUpdate */ public function markAsUpdated() { $this->updatedAt = new Datetime(now); } }
  • 45. What else can I do?
  • 46. prePersist postLoadpostPersist loadClassMetadatapreUpdate preFlushpostUpdate onFlushpreRemove postFlushpostRemove onClear
  • 47. prePersist postLoadpostPersist loadClassMetadatapreUpdate preFlushpostUpdate onFlushpreRemove postFlushpostRemove onClear
  • 48. Protip:Only fires if youre dirty
  • 49. new MudWrestling();
  • 50. Controller $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 2X
  • 51. Controller $article = $em->find(Article, 1); $article->setTitle(Awesome New Story); $em->flush(); Awesome New Story === Awesome New Story No update
  • 52. Controller $article = $em->find(Article, 1); $article->setTitle(Fantabulous Updated Story); $em->flush(); Awesome New Story !== Fantabulous Updated Story Update, yeah!
  • 53. Controller $article = $em->find(Article, 1); $article->setTitle(Awesome New Project.uniqid()); $em->flush();
  • 54. So, heres the thing.
  • 55. The Thing
  • 56. Events are cool.
  • 57. Callbacks?
  • 58. Eeeeeeeeeeeeeeeeeeh.
  • 59. • Limited to model dependencies• Do you really need that function?• Protected function? Silent error• Repeated Code• Performance implications
  • 60. Listeners
  • 61. Listener class UpdateTimeListener { public function preUpdate($event) { } }
  • 62. Bootstrap $em->getEventManager()->addEventListener( array(DoctrineORMEvents::preUpdate), new UpdateTimeListener() );
  • 63. Invert
  • 64. Invert
  • 65. Bootstrap $em->getEventManager()->addEventListener( array(DoctrineORMEvents::preUpdate), new UpdateTimeListener() );
  • 66. Bootstrap $em->getEventManager()->addEventSubscriber( new UpdateTimeListener() );
  • 67. Listener class UpdateTimeListener { public function preUpdate($event) { } public function getSubscribedEvents() { return array(Events::preUpdate); } }
  • 68. Listener use DoctrineCommonEventSubscriber; class UpdateTimeListener implements EventSubscriber { public function preUpdate($event) { } public function getSubscribedEvents() { return array(Events::preUpdate); } }
  • 69. Functionally? No differenceDesign? Subscriber
  • 70. Bootstrap $em->getEventManager()->addEventSubscriber( new ChangeMailListener() );
  • 71. Bootstrap $em->getEventManager()->addEventSubscriber( new ChangeMailListener($mailer) );
  • 72. Listener class UpdateTimeListener implements EventSubscriber { public function getSubscribedEvents() { return array(Events::preUpdate); } public function preUpdate($event) { } }
  • 73. Listener public function preUpdate($event) { $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 ); }
  • 74. Controller $article = $em->find(Article, 1); $article->setTitle(Awesome New Story); $em->flush(); echo $article->getUpdatedAt()->format(c); // 2012-05-29T10:48:00+02:00
  • 75. Whoopity-doo
  • 76. Theory Land interface LastUpdatedInterface { public function setUpdatedAt(Datetime $date); public function getUpdatedAt(); }
  • 77. Listener public function preUpdate($event) { $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 ); }
  • 78. Listener public function preUpdate($event) { $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 ); }
  • 79. Listener public function preUpdate($event) { $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 ); }
  • 80. The POWAH
  • 81. Flushing And Multiple Events
  • 82. OnFlush
  • 83. TPS RequestAfter every update to an article, I wantan email of the changes sent to me.Also, bring me more lettuce.
  • 84. Listener class ChangeMailListener implements EventSubscriber { protected $mailMessage; public function getSubscribedEvents() { return array(Events::onFlush, Events::postFlush); } }
  • 85. Listener public function onFlush($event) { $uow = $event->getEntityManager()->getUnitOfWork(); foreach($uow->getScheduledEntityUpdates() as $model) { $changeset = $uow->getEntityChangeSet($model); } }
  • 86. array(1) { ["title"]=> array(2) { [0]=> string(16) "Boring Old Title" [1]=> string(16) "Great New Title!" }}
  • 87. Listener public function onFlush($event) { $uow = $event->getEntityManager()->getUnitOfWork(); foreach($uow->getScheduledEntityUpdates() as $model) { $changeset = $uow->getEntityChangeSet($model); $this->formatAllPrettyInMail($model, $changeset); } }
  • 88. 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); }
  • 89. Thats really all there is to it.
  • 90. 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]} nn"; } $this->mailMessage .= $msg; }
  • 91. Advice:Treat your listeners like controllers
  • 92. Keep it thin
  • 93. Crazy Town
  • 94. TPS RequestWrite the change messages to thedatabase instead of emailing them.
  • 95. Model /** @Entity */ class ChangeLog { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; /** @Column(type="text") */ protected $description; }
  • 96. Listener class ChangeLogger implements EventSubscriber { public function getSubscribedEvents() { return array(Events::onFlush); } public function onFlush($event) { } }
  • 97. 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 ); }
  • 98. Shiny
  • 99. Yes, I really used PHPMyAdmin there
  • 100. Shiny
  • 101. Also, wow, it worked!
  • 102. UnitOfWork API $uow->getScheduledEntityInsertions(); $uow->getScheduledEntityUpdates(); $uow->getScheduledEntityDeletions(); $uow->getScheduledCollectionUpdates(); $uow->getScheduledCollectionDeletions();
  • 103. UnitOfWork API $uow->scheduleForInsert(); $uow->scheduleForUpdate(); $uow->scheduleExtraUpdate(); $uow->scheduleForDelete();
  • 104. UnitOfWork API And many more...
  • 105. postLoad
  • 106. Model /** @Entity */ class Article { //... /** @Column(type="integer") */ protected $viewCount; }
  • 107. +MySQL Redis
  • 108. TPS RequestMove view counts out of the databaseinto a faster system.And dont break everything.
  • 109. Controller $article = $em->find(Article, 1); echo $article->getViewCount();
  • 110. Model /** @Entity */ class Article { //... /** @Column(type="integer") */ protected $viewCount; }
  • 111. Listener class ViewCountListener implements EventSubscriber { public function getSubscribedEvents() { return DoctrineORMEvents::postLoad; } public function postLoad($event) { $model = $event->getEntity(); if (!$model instanceof Article) { return; } $currentRank = $this->getCountFromRedis($model); $model->setViewCount($currentRank); } }
  • 112. Controller $article = $em->find(Article, 1); echo $article->getViewCount();
  • 113. Many, many other uses.
  • 114. Class Metadata
  • 115. Model /** @Entity */ class Article { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; Where do /** @Column(type="string") */ they go? protected $title; /** @Column(type="datetime") */ protected $updatedAt; }
  • 116. Model /** @Entity */ class Article { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="datetime") */ protected $updatedAt; }
  • 117. DoctrineORMMappingClassMetadata
  • 118. ??? $metadata = $em->getClassMetadata(Article);
  • 119. ??? $metadata = $em->getClassMetadata(Article); echo $metadata->getTableName();Output:Article
  • 120. ??? $metadata = $em->getClassMetadata(Article); echo $metadata->getTableName();Output:articles
  • 121. Doctrine 2.3 will bring NamingStrategy
  • 122. ??? $conn = $em->getConnection(); $tableName = $metadata->getQuotedTableName($conn); $results = $conn->query(SELECT * FROM .$tableName);
  • 123. Tip of the Iceberg
  • 124. ??? $metadata->getFieldMapping(title);array(8) { fieldName => "title" type => "string" length => NULL precision => 0 scale => 0 nullable => false unique => false columnName => "title"}
  • 125. ??? $metadata->getFieldMapping(title); $metadata->getFieldNames(); $metadata->getReflectionClass(); $metadata->getReflectionProperty(title); $metadata->getColumnName(title); $metadata->getTypeOfField(title);
  • 126. Simple Example
  • 127. Symple Example
  • 128. Form in Symfony2 class ArticleType extends AbstractType { public function buildForm($builder, array $options) { $builder ->add(title) ->add(content); } }
  • 129. Controller in Symfony2 $entity = new Article(); $form = $this->createForm(new ArticleType(), $entity);
  • 130. Form in Symfony2 class ArticleType extends AbstractType { public function buildForm($builder, array $options) { $builder ->add(title) ->add(content); } }
  • 131. 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, ...); }
  • 132. Cool, huh?
  • 133. Cool, huh?
  • 134. ??? $metadata->getAssociationMapping(comments);array(15) { fieldName => "comments" mappedBy => "article" targetEntity => "Comment" cascade => array(0) {} fetch => 2 type => 4 isOwningSide => false sourceEntity => "Article" ...
  • 135. ??? $metadata->getAssociationMapping(comments); $metadata->getAssociationMappings(); $metadata->isSingleValuedAssociation(comments); $metadata->isCollectionValuedAssociation(comments); $metadata->getAssociationTargetClass(comments);
  • 136. Form in Symfony2 class ArticleType extends AbstractType { public function buildForm($builder, array $options) { $builder ->add(title) ->add(content) ->add(comments); } }
  • 137. Oh yeah.
  • 138. You can set all of this stuff.
  • 139. 100~ public functions
  • 140. But is there a good reason?Unless youre writing a driver, probably not.
  • 141. Listener class MyListener { public function loadClassMetadata($event) { echo $event->getClassMetadata()->getName(); } }
  • 142. Wheres the manatee?
  • 143. Youre the manatee.
  • 144. Epilogue & Service Layers
  • 145. ModelController View
  • 146. ModelService Layer Controller View
  • 147. Be smart.
  • 148. Be smart.
  • 149. Be simple
  • 150. Dont overuse this.
  • 151. Test
  • 152. Test test test
  • 153. test test test test test test test test test testtest test test test test test test test test testtest test test test test test test test test testtest test test test test test test test test testtest test test test test test test test test testtest test test test test test test test test testtest test test test test test test test test testtest test test test test test test test test testtest test test test test test test test test testtest test test test test test test test test testtest test test test test test test test test test
  • 154. RTFM.
  • 155. Go forth and rock.
  • 156. Youre the manatee.
  • 157. Thanks to:@boekkooi#doctrine (freenode)
  • 158. Wikipedia OwnMoment (sxc.hu)
  • 159. https://joind.in/6251