Slide 1

Slide 1 text

Ross Tuck Extending Doctrine 2 For Your Domain Model June 9th, DPC

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Who Am I?

Slide 4

Slide 4 text

Ross Tuck

Slide 5

Slide 5 text

Team Lead at Ibuildings Codemonkey REST nut Hat guy Token Foreigner America, &@*! Yeah

Slide 6

Slide 6 text

@rosstuck #dashinglyHandsome

Slide 7

Slide 7 text

Quick But Necessary

Slide 8

Slide 8 text

Doctrine 101

Slide 9

Slide 9 text

Doctrine 102

Slide 10

Slide 10 text

Doctrine 102.5

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

$article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $em->flush(); Controller

Slide 13

Slide 13 text

Filters

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

We need a way to approve comments before they appear to all users. TPS Request

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

$comments = $em->createQuery('SELECT c FROM Comment c'); foreach($comments->getResult() as $comment) { echo $comment->getContent()."\n"; } Controller

Slide 19

Slide 19 text

$comments = $em->find('Article', 1)->getComments(); foreach($comments as $comment) { echo $comment->getContent()."\n"; } Controller Approx 100 places in your code!

Slide 20

Slide 20 text

Filters New in 2.2

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

$em->getConfiguration() ->addFilter('approved_comments', 'CommentFilter'); Bootstrap Handy key Class name

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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"; }

Slide 27

Slide 27 text

Parameters

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Limitations

Slide 33

Slide 33 text

Events

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Callbacks Listeners On the model External objects

Slide 36

Slide 36 text

Lifecycle Callbacks

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

$article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $em->flush(); Controller

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

Articles must record the last date they were modified in any way. TPS Request

Slide 41

Slide 41 text

$article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $article->setUpdatedAt(new DateTime('now')); $em->flush(); Controller + 100 other files + testing + missed bugs

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

$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!

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

What else can I do?

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Protip: Only fires if you're dirty

Slide 52

Slide 52 text

new MudWrestling();

Slide 53

Slide 53 text

$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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

$article = $em->find('Article', 1); $article->setTitle('Awesome New Project'.uniqid()); $em->flush(); Controller

Slide 57

Slide 57 text

So, here's the thing.

Slide 58

Slide 58 text

The Thing

Slide 59

Slide 59 text

Events are cool.

Slide 60

Slide 60 text

Callbacks?

Slide 61

Slide 61 text

Eeeeeeeeeeeeeeeeeeh.

Slide 62

Slide 62 text

• Limited to model dependencies • Do you really need that function? • Protected function? Silent error • Repeated Code • Performance implications

Slide 63

Slide 63 text

Listeners

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

Invert

Slide 67

Slide 67 text

Invert

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

Functionally? Design? No difference Subscriber

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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 ); }

Slide 77

Slide 77 text

$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

Slide 78

Slide 78 text

Whoopity-doo

Slide 79

Slide 79 text

Theory Land interface LastUpdatedInterface { public function setUpdatedAt(\Datetime $date); public function getUpdatedAt(); }

Slide 80

Slide 80 text

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 ); }

Slide 81

Slide 81 text

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 ); }

Slide 82

Slide 82 text

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 ); }

Slide 83

Slide 83 text

The POWAH

Slide 84

Slide 84 text

Flushing And Multiple Events

Slide 85

Slide 85 text

OnFlush

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 text

After every update to an article, I want an email of the changes sent to me. Also, bring me more lettuce. TPS Request

Slide 88

Slide 88 text

Listener class ChangeMailListener implements EventSubscriber { protected $mailMessage; public function getSubscribedEvents() { return array(Events::onFlush, Events::postFlush); } }

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

array(1) { ["title"]=> array(2) { [0]=> string(16) "Boring Old Title" [1]=> string(16) "Great New Title!" } }

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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); }

Slide 93

Slide 93 text

That's really all there is to it.

Slide 94

Slide 94 text

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; }

Slide 95

Slide 95 text

Advice: Treat your listeners like controllers

Slide 96

Slide 96 text

Keep it thin

Slide 97

Slide 97 text

Crazy Town

Slide 98

Slide 98 text

No content

Slide 99

Slide 99 text

Write the change messages to the database instead of emailing them. TPS Request

Slide 100

Slide 100 text

Model /** @Entity */ class ChangeLog { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; /** @Column(type="text") */ protected $description; }

Slide 101

Slide 101 text

Listener class ChangeLogger implements EventSubscriber { public function getSubscribedEvents() { return array(Events::onFlush); } public function onFlush($event) { } }

Slide 102

Slide 102 text

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 ); }

Slide 103

Slide 103 text

Shiny

Slide 104

Slide 104 text

Yes, I really used PHPMyAdmin there

Slide 105

Slide 105 text

Shiny

Slide 106

Slide 106 text

Also, wow, it worked!

Slide 107

Slide 107 text

No content

Slide 108

Slide 108 text

UnitOfWork API $uow->getScheduledEntityInsertions(); $uow->getScheduledEntityUpdates(); $uow->getScheduledEntityDeletions(); $uow->getScheduledCollectionUpdates(); $uow->getScheduledCollectionDeletions();

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

UnitOfWork API And many more...

Slide 111

Slide 111 text

postLoad

Slide 112

Slide 112 text

No content

Slide 113

Slide 113 text

/** @Entity */ class Article { Model //... /** @Column(type="integer") */ protected $viewCount; }

Slide 114

Slide 114 text

No content

Slide 115

Slide 115 text

MySQL Redis +

Slide 116

Slide 116 text

Move view counts out of the database into a faster system. And don't break everything. TPS Request

Slide 117

Slide 117 text

$article = $em->find('Article', 1); echo $article->getViewCount(); Controller

Slide 118

Slide 118 text

/** @Entity */ class Article { Model //... /** @Column(type="integer") */ protected $viewCount; }

Slide 119

Slide 119 text

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); } }

Slide 120

Slide 120 text

$article = $em->find('Article', 1); echo $article->getViewCount(); Controller

Slide 121

Slide 121 text

Many, many other uses.

Slide 122

Slide 122 text

Class Metadata

Slide 123

Slide 123 text

/** @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?

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

Doctrine\ORM\Mapping\ClassMetadata

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

Doctrine 2.3 will bring NamingStrategy

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

Tip of the Iceberg

Slide 132

Slide 132 text

??? array(8) { fieldName => "title" type => "string" length => NULL precision => 0 scale => 0 nullable => false unique => false columnName => "title" } $metadata->getFieldMapping('title');

Slide 133

Slide 133 text

??? $metadata->getFieldMapping('title'); $metadata->getFieldNames(); $metadata->getReflectionClass(); $metadata->getReflectionProperty('title'); $metadata->getColumnName('title'); $metadata->getTypeOfField('title');

Slide 134

Slide 134 text

Simple Example

Slide 135

Slide 135 text

Symple Example

Slide 136

Slide 136 text

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

Slide 137

Slide 137 text

$entity = new Article(); $form = $this->createForm(new ArticleType(), $entity); Controller in Symfony2

Slide 138

Slide 138 text

No content

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

No content

Slide 141

Slide 141 text

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', ...); }

Slide 142

Slide 142 text

Cool, huh?

Slide 143

Slide 143 text

Cool, huh?

Slide 144

Slide 144 text

??? array(15) { fieldName => "comments" mappedBy => "article" targetEntity => "Comment" cascade => array(0) {} fetch => 2 type => 4 isOwningSide => false sourceEntity => "Article" ... $metadata->getAssociationMapping('comments');

Slide 145

Slide 145 text

??? $metadata->getAssociationMapping('comments'); $metadata->getAssociationMappings(); $metadata->isSingleValuedAssociation('comments'); $metadata->isCollectionValuedAssociation('comments'); $metadata->getAssociationTargetClass('comments');

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

No content

Slide 148

Slide 148 text

Oh yeah.

Slide 149

Slide 149 text

You can set all of this stuff.

Slide 150

Slide 150 text

100~ public functions

Slide 151

Slide 151 text

But is there a good reason? Unless you're writing a driver, probably not.

Slide 152

Slide 152 text

Listener class MyListener { public function loadClassMetadata($event) { echo $event->getClassMetadata()->getName(); } }

Slide 153

Slide 153 text

Where's the manatee?

Slide 154

Slide 154 text

You're the manatee.

Slide 155

Slide 155 text

Epilogue & Service Layers

Slide 156

Slide 156 text

Model Controller View

Slide 157

Slide 157 text

Model Service Layer Controller View

Slide 158

Slide 158 text

Be smart.

Slide 159

Slide 159 text

Be smart.

Slide 160

Slide 160 text

Be simple

Slide 161

Slide 161 text

Don't overuse this.

Slide 162

Slide 162 text

Test

Slide 163

Slide 163 text

Test test test

Slide 164

Slide 164 text

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

Slide 165

Slide 165 text

RTFM.

Slide 166

Slide 166 text

Go forth and rock.

Slide 167

Slide 167 text

No content

Slide 168

Slide 168 text

You're the manatee.

Slide 169

Slide 169 text

Thanks to: @boekkooi #doctrine (freenode)

Slide 170

Slide 170 text

Wikipedia OwnMoment (sxc.hu)

Slide 171

Slide 171 text

https://joind.in/6251