Extending Doctrine 2
For Your Domain Model


                     Ross Tuck
                  June 9th, DPC
Who Am I?
Ross Tuck
Team Lead at Ibuildings
Codemonkey
REST nut
Hat guy
Token Foreigner America,
               &@* ! Yeah
@rosstuck
#dashinglyHandsome
Quick But Necessary
Doctrine 101
Doctrine 102
Doctrine 102.5
Model


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


        /** @Column(type="string") */
        protected $title;


        /** @Column(type="text") */
        protected $content;




  }
Controller




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


   $em->flush();
Filters
TPS Request
We need a way to approve comments
before they appear to all users.
Controller




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



Output:
Doug isn't human
Doug is the best
Controller




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




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




   $comments = $em->find('Article', 1)->getComments();
   foreach($comments as $comment) {
        echo $comment->getContent()."n";
   }
                                             !
                            ce s in your code
             Approx 100 pla
Filters
New in 2.2
Filter



  use DoctrineORMQueryFilterSQLFilter;


  class CommentFilter extends SQLFilter {


         public function addFilterConstraint($entityInfo, $alias) {
             if ($entityInfo->name !== 'Comment') {
                 return "";
             }
             return $alias.".approved = 1";
         }


  }
Bootstrap




                         y
               Ha ndy ke


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




                                         Class name
Bootstrap




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




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




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


                                                     al User
Output:                                     A s Norm
Doug is the best
Controller




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


                                                       in
Output:
                                            As the Adm
Doug isn't human
Doug is the best
Parameters
Bootstrap




   $filter = $em->getFilters()->getFilter('approved_comments');
   $filter->setParameter('level', $this->getUserLevel());
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";
         }


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


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


  }
Limitations
Events
Inser
          t
prePersist    postLoad
postPersist   loadClassMetadata
preUpdate     preFlush
postUpdate    onFlush
preRemove     postFlush
postRemove    onClear
Callbacks        Listeners
On the model   External objects
Lifecycle Callbacks
Model


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


        /** @Column(type="string") */
        protected $title;


        /** @Column(type="text") */
        protected $content;




  }
Controller




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


   $em->flush();
TPS Request
Articles must record the last date they
were modified in any way.
Controller




   $article = $em->find('Article', 1);
   $article->setTitle('Awesome New Story');
   $article->setUpdatedAt(new DateTime('now'));

                                         + 100 other files
   $em->flush();                         + testing
                                         + missed bugs
Model


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


        /** @Column(type="string") */
        protected $title;




  }
Model


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


        /** @Column(type="string") */
        protected $title;


        /** @Column(type="datetime") */
        protected $updatedAt;



  }
Model


  /** @Entity */
  class Article {
        // ...



        public function markAsUpdated() {
            $this->updatedAt = new Datetime('now');
        }




  }
Model


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




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


  /** @Entity @HasLifecycleCallbacks */
  class Article {
        // ...


        /** @PreUpdate */
        public function markAsUpdated() {
            $this->updatedAt = new Datetime('now');
        }




  }
What else can I do?
prePersist    postLoad
postPersist   loadClassMetadata
preUpdate     preFlush
postUpdate    onFlush
preRemove     postFlush
postRemove    onClear
prePersist    postLoad
postPersist   loadClassMetadata
preUpdate     preFlush
postUpdate    onFlush
preRemove     postFlush
postRemove    onClear
Protip:
Only fires if you're dirty
new MudWrestling();
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
Controller




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


   $em->flush();

             'Awesome New Story' === 'Awesome New Story'
                           No update
Controller




   $article = $em->find('Article', 1);
   $article->setTitle('Fantabulous Updated Story');


   $em->flush();

     'Awesome New Story' !== 'Fantabulous Updated Story'
                    Update, yeah!
Controller




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


   $em->flush();
So, here's the thing.
The Thing
Events are cool.
Callbacks?
Eeeeeeeeeeeeeeeeeeh.
•   Limited to model dependencies
•   Do you really need that function?
•   Protected function? Silent error
•   Repeated Code
•   Performance implications
Listeners
Listener




   class UpdateTimeListener {



           public function preUpdate($event) {


           }




   }
Bootstrap




   $em->getEventManager()->addEventListener(
        array(DoctrineORMEvents::preUpdate),
        new UpdateTimeListener()
   );
Invert
Invert
Bootstrap




   $em->getEventManager()->addEventListener(
        array(DoctrineORMEvents::preUpdate),
        new UpdateTimeListener()
   );
Bootstrap




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




   class UpdateTimeListener {


           public function preUpdate($event) {


           }


           public function getSubscribedEvents() {
               return array(Events::preUpdate);
           }



   }
Listener


   use DoctrineCommonEventSubscriber;
   class UpdateTimeListener implements EventSubscriber {


           public function preUpdate($event) {


           }


           public function getSubscribedEvents() {
               return array(Events::preUpdate);
           }



   }
Functionally?   No difference
Design?         Subscriber
Bootstrap




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




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

   class UpdateTimeListener implements EventSubscriber {


           public function getSubscribedEvents() {
               return array(Events::preUpdate);
           }


           public function preUpdate($event) {


           }




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

   }
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
Whoopity-doo
Theory Land




   interface LastUpdatedInterface {
        public function setUpdatedAt(Datetime $date);
        public function getUpdatedAt();
   }
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
           );

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

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

   }
The POWAH
Flushing And Multiple Events
OnFlush
TPS Request
After every update to an article, I want
an email of the changes sent to me.

Also, bring me more lettuce.
Listener


   class ChangeMailListener implements EventSubscriber {
           protected $mailMessage;


           public function getSubscribedEvents()   {
               return array(Events::onFlush, Events::postFlush);
           }
   }
Listener


   public function onFlush($event) {
           $uow = $event->getEntityManager()->getUnitOfWork();


           foreach($uow->getScheduledEntityUpdates() as $model) {
               $changeset = $uow->getEntityChangeSet($model);


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


   public function onFlush($event) {
           $uow = $event->getEntityManager()->getUnitOfWork();


           foreach($uow->getScheduledEntityUpdates() as $model) {
               $changeset = $uow->getEntityChangeSet($model);
               $this->formatAllPrettyInMail($model, $changeset);
           }
   }
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);
   }
That's really all there is to it.
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;
   }
Advice:
Treat your listeners like controllers
Keep it thin
Crazy Town
TPS Request
Write the change messages to the
database instead of emailing them.
Model

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


        /** @Column(type="text") */
        protected $description;
  }
Listener



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


           public function onFlush($event) {


           }
   }
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
              );
        }
Shiny
Yes, I really used PHPMyAdmin there
Shiny
Also, wow, it worked!
UnitOfWork API




                 $uow->getScheduledEntityInsertions();
                 $uow->getScheduledEntityUpdates();
                 $uow->getScheduledEntityDeletions();


                 $uow->getScheduledCollectionUpdates();
                 $uow->getScheduledCollectionDeletions();
UnitOfWork API




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




                 And many more...
postLoad
Model


  /** @Entity */
  class Article {
        //...


        /** @Column(type="integer") */
        protected $viewCount;




  }
+
MySQL       Redis
TPS Request
Move view counts out of the database
into a faster system.

And don't break everything.
Controller




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


  /** @Entity */
  class Article {
        //...


        /** @Column(type="integer") */
        protected $viewCount;




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




   $article = $em->find('Article', 1);
   echo $article->getViewCount();
Many, many other uses.
Class Metadata
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;


  }
Model


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


        /** @Column(type="string") */
        protected $title;


        /** @Column(type="datetime") */
        protected $updatedAt;


  }
DoctrineORMMappingClassMetadata
???




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




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




Output:
Article
???




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




Output:
articles
Doctrine 2.3 will bring NamingStrategy
???




      $conn = $em->getConnection();
      $tableName = $metadata->getQuotedTableName($conn);


      $results = $conn->query('SELECT * FROM '.$tableName);
Tip of the Iceberg
???




      $metadata->getFieldMapping('title');


array(8) {
  fieldName     =>   "title"
  type          =>   "string"
  length        =>   NULL
  precision     =>   0
  scale         =>   0
  nullable      =>   false
  unique        =>   false
  columnName    =>   "title"
}
???




      $metadata->getFieldMapping('title');
      $metadata->getFieldNames();


      $metadata->getReflectionClass();
      $metadata->getReflectionProperty('title');


      $metadata->getColumnName('title');
      $metadata->getTypeOfField('title');
Simple Example
Symple Example
Form in Symfony2




   class ArticleType extends AbstractType {


        public function buildForm($builder, array $options) {
             $builder
                   ->add('title')
                   ->add('content');
        }


   }
Controller in Symfony2




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




   class ArticleType extends AbstractType {


        public function buildForm($builder, array $options) {
             $builder
                   ->add('title')
                   ->add('content');
        }


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




      $metadata->getAssociationMapping('comments');


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




      $metadata->getAssociationMapping('comments');
      $metadata->getAssociationMappings();


      $metadata->isSingleValuedAssociation('comments');
      $metadata->isCollectionValuedAssociation('comments');


      $metadata->getAssociationTargetClass('comments');
Form in Symfony2




   class ArticleType extends AbstractType {


        public function buildForm($builder, array $options) {
             $builder
                   ->add('title')
                   ->add('content')
                   ->add('comments');
        }


   }
Oh yeah.
You can set all of this stuff.
100~ public functions
But is there a good reason?
Unless you're writing a driver, probably not.
Listener




   class MyListener {


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


   }
Where's the manatee?
You're the manatee.
Epilogue & Service Layers
Model




Controller




  View
Model




Service Layer




 Controller




    View
Be smart.
Be smart.
Be simple
Don't overuse this.
Test
Test test test
test test test test test test test test test test
test test test test test test test test test test
test test test test test test test test test test
test test test test test test test test test test
test test test test test test test test test test
test test test test test test test test test test
test test test test test test test test test test
test test test test test test test test test test
test test test test test test test test test test
test test test test test test test test test test
test test test test test test test test test test
RTFM.
Go forth and rock.
You're the manatee.
Thanks to:

@boekkooi
#doctrine (freenode)
Wikipedia




            OwnMoment (sxc.hu)
https://joind.in/6251

Extending Doctrine 2 for your Domain Model

  • 1.
    Extending Doctrine 2 ForYour Domain Model Ross Tuck June 9th, DPC
  • 3.
  • 4.
  • 5.
    Team Lead atIbuildings Codemonkey REST nut Hat guy Token Foreigner America, &@* ! Yeah
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
    Model /**@Entity */ class Article { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="text") */ protected $content; }
  • 12.
    Controller $article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $em->flush();
  • 13.
  • 15.
    TPS Request We needa way to approve comments before they appear to all users.
  • 16.
    Controller $comments = $em->getRepository('Comment')->findAll(); foreach($comments as $comment) { echo $comment->getContent()."n"; } Output: Doug isn't human Doug is the best
  • 17.
    Controller $comments = $em->getRepository('Comment')->findAll(); foreach($comments as $comment) { echo $comment->getContent()."n"; }
  • 18.
    Controller $comments = $em->createQuery('SELECT c FROM Comment c'); foreach($comments->getResult() as $comment) { echo $comment->getContent()."n"; }
  • 19.
    Controller $comments = $em->find('Article', 1)->getComments(); foreach($comments as $comment) { echo $comment->getContent()."n"; } ! ce s in your code Approx 100 pla
  • 20.
  • 21.
    Filter useDoctrineORMQueryFilterSQLFilter; class CommentFilter extends SQLFilter { public function addFilterConstraint($entityInfo, $alias) { if ($entityInfo->name !== 'Comment') { return ""; } return $alias.".approved = 1"; } }
  • 22.
    Bootstrap y Ha ndy ke $em->getConfiguration() ->addFilter('approved_comments', 'CommentFilter'); Class name
  • 23.
    Bootstrap $em->getConfiguration() ->addFilter('approved_comments', 'CommentFilter'); $em->getFilters()->enable('approved_comments');
  • 24.
    Bootstrap if ($this->isNormalUser()) { $em->getConfiguration() ->addFilter('approved_comments', 'CommentFilter'); $em->getFilters()->enable('approved_comments'); }
  • 25.
    Controller $comments = $em->getRepository('Comment')->findAll(); foreach($comments as $comment) { echo $comment->getContent()."n"; } al User Output: A s Norm Doug is the best
  • 26.
    Controller $comments = $em->getRepository('Comment')->findAll(); foreach($comments as $comment) { echo $comment->getContent()."n"; } in Output: As the Adm Doug isn't human Doug is the best
  • 27.
  • 28.
    Bootstrap $filter = $em->getFilters()->getFilter('approved_comments'); $filter->setParameter('level', $this->getUserLevel());
  • 29.
    Filter useDoctrineORMQueryFilterSQLFilter; class CommentFilter extends SQLFilter { public function addFilterConstraint($entityInfo, $alias) { if ($entityInfo->name !== 'Comment') { return ""; } $level = $this->getParameter('level'); return $alias.".approved = 1"; } }
  • 30.
    Filter useDoctrineORMQueryFilterSQLFilter; class CommentFilter extends SQLFilter { public function addFilterConstraint($entityInfo, $alias) { if ($entityInfo->name !== 'Comment') { return ""; } $level = $this->getParameter('level'); return $alias.".approved = ".$level; } }
  • 31.
    Filter useDoctrineORMQueryFilterSQLFilter; class CommentFilter extends SQLFilter { public function addFilterConstraint($entityInfo, $alias) { if ($entityInfo->name !== 'Comment') { return ""; } Already escaped $level = $this->getParameter('level'); return $alias.".approved = ".$level; } }
  • 32.
  • 33.
  • 34.
    Inser t prePersist postLoad postPersist loadClassMetadata preUpdate preFlush postUpdate onFlush preRemove postFlush postRemove onClear
  • 35.
    Callbacks Listeners On the model External objects
  • 36.
  • 37.
    Model /**@Entity */ class Article { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="text") */ protected $content; }
  • 38.
    Controller $article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $em->flush();
  • 40.
    TPS Request Articles mustrecord the last date they were modified in any way.
  • 41.
    Controller $article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $article->setUpdatedAt(new DateTime('now')); + 100 other files $em->flush(); + testing + missed bugs
  • 42.
    Model /**@Entity */ class Article { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; }
  • 43.
    Model /**@Entity */ class Article { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="datetime") */ protected $updatedAt; }
  • 44.
    Model /**@Entity */ class Article { // ... public function markAsUpdated() { $this->updatedAt = new Datetime('now'); } }
  • 45.
    Model /**@Entity @HasLifecycleCallbacks */ class Article { // ... Event mapping /** @PreUpdate */ public function markAsUpdated() { $this->updatedAt = new Datetime('now'); } }
  • 46.
    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
  • 47.
    Model /**@Entity @HasLifecycleCallbacks */ class Article { // ... /** @PreUpdate */ public function markAsUpdated() { $this->updatedAt = new Datetime('now'); } }
  • 48.
  • 49.
    prePersist postLoad postPersist loadClassMetadata preUpdate preFlush postUpdate onFlush preRemove postFlush postRemove onClear
  • 50.
    prePersist postLoad postPersist loadClassMetadata preUpdate preFlush postUpdate onFlush preRemove postFlush postRemove onClear
  • 51.
  • 52.
  • 53.
    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
  • 54.
    Controller $article = $em->find('Article', 1); $article->setTitle('Awesome New Story'); $em->flush(); 'Awesome New Story' === 'Awesome New Story' No update
  • 55.
    Controller $article = $em->find('Article', 1); $article->setTitle('Fantabulous Updated Story'); $em->flush(); 'Awesome New Story' !== 'Fantabulous Updated Story' Update, yeah!
  • 56.
    Controller $article = $em->find('Article', 1); $article->setTitle('Awesome New Project'.uniqid()); $em->flush();
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
    Limited to model dependencies • Do you really need that function? • Protected function? Silent error • Repeated Code • Performance implications
  • 63.
  • 64.
    Listener class UpdateTimeListener { public function preUpdate($event) { } }
  • 65.
    Bootstrap $em->getEventManager()->addEventListener( array(DoctrineORMEvents::preUpdate), new UpdateTimeListener() );
  • 66.
  • 67.
  • 68.
    Bootstrap $em->getEventManager()->addEventListener( array(DoctrineORMEvents::preUpdate), new UpdateTimeListener() );
  • 69.
    Bootstrap $em->getEventManager()->addEventSubscriber( new UpdateTimeListener() );
  • 70.
    Listener class UpdateTimeListener { public function preUpdate($event) { } public function getSubscribedEvents() { return array(Events::preUpdate); } }
  • 71.
    Listener use DoctrineCommonEventSubscriber; class UpdateTimeListener implements EventSubscriber { public function preUpdate($event) { } public function getSubscribedEvents() { return array(Events::preUpdate); } }
  • 72.
    Functionally? No difference Design? Subscriber
  • 73.
    Bootstrap $em->getEventManager()->addEventSubscriber( new ChangeMailListener() );
  • 74.
    Bootstrap $em->getEventManager()->addEventSubscriber( new ChangeMailListener($mailer) );
  • 75.
    Listener class UpdateTimeListener implements EventSubscriber { public function getSubscribedEvents() { return array(Events::preUpdate); } public function preUpdate($event) { } }
  • 76.
    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 ); }
  • 77.
    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
  • 78.
  • 79.
    Theory Land interface LastUpdatedInterface { public function setUpdatedAt(Datetime $date); public function getUpdatedAt(); }
  • 80.
    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 ); }
  • 81.
    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 ); }
  • 82.
    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 ); }
  • 83.
  • 84.
  • 85.
  • 87.
    TPS Request After everyupdate to an article, I want an email of the changes sent to me. Also, bring me more lettuce.
  • 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 allthere 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]} nn"; } $this->mailMessage .= $msg; }
  • 95.
  • 96.
  • 97.
  • 99.
    TPS Request Write thechange messages to the database instead of emailing them.
  • 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.
  • 104.
    Yes, I reallyused PHPMyAdmin there
  • 105.
  • 106.
  • 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.
  • 113.
    Model /**@Entity */ class Article { //... /** @Column(type="integer") */ protected $viewCount; }
  • 115.
    + MySQL Redis
  • 116.
    TPS Request Move viewcounts out of the database into a faster system. And don't break everything.
  • 117.
    Controller $article = $em->find('Article', 1); echo $article->getViewCount();
  • 118.
    Model /**@Entity */ class Article { //... /** @Column(type="integer") */ protected $viewCount; }
  • 119.
    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); } }
  • 120.
    Controller $article = $em->find('Article', 1); echo $article->getViewCount();
  • 121.
  • 122.
  • 123.
    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; }
  • 124.
    Model /**@Entity */ class Article { /** @Id @GeneratedValue * @Column(type="integer") */ protected $id; /** @Column(type="string") */ protected $title; /** @Column(type="datetime") */ protected $updatedAt; }
  • 125.
  • 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 willbring NamingStrategy
  • 130.
    ??? $conn = $em->getConnection(); $tableName = $metadata->getQuotedTableName($conn); $results = $conn->query('SELECT * FROM '.$tableName);
  • 131.
    Tip of theIceberg
  • 132.
    ??? $metadata->getFieldMapping('title'); array(8) { fieldName => "title" type => "string" length => NULL precision => 0 scale => 0 nullable => false unique => false columnName => "title" }
  • 133.
    ??? $metadata->getFieldMapping('title'); $metadata->getFieldNames(); $metadata->getReflectionClass(); $metadata->getReflectionProperty('title'); $metadata->getColumnName('title'); $metadata->getTypeOfField('title');
  • 134.
  • 135.
  • 136.
    Form in Symfony2 class ArticleType extends AbstractType { public function buildForm($builder, array $options) { $builder ->add('title') ->add('content'); } }
  • 137.
    Controller in Symfony2 $entity = new Article(); $form = $this->createForm(new ArticleType(), $entity);
  • 139.
    Form in Symfony2 class ArticleType extends AbstractType { public function buildForm($builder, array $options) { $builder ->add('title') ->add('content'); } }
  • 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.
  • 143.
  • 144.
    ??? $metadata->getAssociationMapping('comments'); array(15) { fieldName => "comments" mappedBy => "article" targetEntity => "Comment" cascade => array(0) {} fetch => 2 type => 4 isOwningSide => false sourceEntity => "Article" ...
  • 145.
    ??? $metadata->getAssociationMapping('comments'); $metadata->getAssociationMappings(); $metadata->isSingleValuedAssociation('comments'); $metadata->isCollectionValuedAssociation('comments'); $metadata->getAssociationTargetClass('comments');
  • 146.
    Form in Symfony2 class ArticleType extends AbstractType { public function buildForm($builder, array $options) { $builder ->add('title') ->add('content') ->add('comments'); } }
  • 148.
  • 149.
    You can setall of this stuff.
  • 150.
  • 151.
    But is therea good reason? Unless you're writing a driver, probably not.
  • 152.
    Listener class MyListener { public function loadClassMetadata($event) { echo $event->getClassMetadata()->getName(); } }
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
    test test testtest test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test 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.
  • 166.
  • 168.
  • 169.
  • 170.
    Wikipedia OwnMoment (sxc.hu)
  • 171.