Browse Source

simple timestampable listener

gediminasm 14 years ago
parent
commit
f3fd75f423

+ 18 - 0
lib/DoctrineExtensions/Timestampable/Timestampable.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace DoctrineExtensions\Timestampable;
+
+/**
+ * This interface must be implemented for all entities
+ * to activate the Timestampable behavior
+ * 
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package DoctrineExtensions.Timestampable
+ * @subpackage Timestampable
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+interface Timestampable
+{
+    // timestampable expects, fields: modified, created or updated
+}

+ 122 - 0
lib/DoctrineExtensions/Timestampable/TimestampableListener.php

@@ -0,0 +1,122 @@
+<?php
+
+namespace DoctrineExtensions\Timestampable;
+
+use Doctrine\Common\EventSubscriber,
+    Doctrine\ORM\Events,
+    Doctrine\ORM\Event\LifecycleEventArgs,
+    Doctrine\ORM\Event\OnFlushEventArgs,
+    Doctrine\ORM\EntityManager;
+
+/**
+ * The Timestampable listener handles the update of
+ * dates on creation and update of entity.
+ * 
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package DoctrineExtensions.Timestampable
+ * @subpackage TimestampableListener
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class TimestampableListener implements EventSubscriber
+{   
+    /**
+     * List of types which are valid for timestamp
+     * 
+     * @var array
+     */
+    private $_validTypes = array(
+        'date',
+        'time',
+        'datetime'
+    );
+    
+    /**
+     * Specifies the list of events to listen
+     * 
+     * @return array
+     */
+    public function getSubscribedEvents()
+    {
+        return array(
+            Events::prePersist,
+            Events::onFlush
+        );
+    }
+    
+    /**
+     * Looks for Timestampable entities being updated
+     * to update modification date
+     * 
+     * @param OnFlushEventArgs $args
+     * @return void
+     */
+    public function onFlush(OnFlushEventArgs $args)
+    {
+        $em = $args->getEntityManager();
+        $uow = $em->getUnitOfWork();
+        // check all scheduled updates
+        foreach ($uow->getScheduledEntityUpdates() as $entity) {
+            if ($entity instanceof Timestampable) {
+                $meta = $em->getClassMetadata(get_class($entity));
+                $needChanges = false;
+                
+                if ($this->_isFieldAvailable($em, $entity, 'updated')) {
+                    $needChanges = true;
+                    $meta->getReflectionProperty('updated')->setValue($entity, new \DateTime('now'));
+                } elseif ($this->_isFieldAvailable($em, $entity, 'modified')) {
+                    $needChanges = true;
+                    $meta->getReflectionProperty('modified')->setValue($entity, new \DateTime('now'));
+                }
+                
+                if ($needChanges) {
+                    $uow->recomputeSingleEntityChangeSet($meta, $entity);
+                }
+            }
+        }
+    }
+    
+    /**
+     * Checks for persisted Timestampable entities
+     * to update creation and modification dates
+     * 
+     * @param LifecycleEventArgs $args
+     * @return void
+     */
+    public function prePersist(LifecycleEventArgs $args)
+    {
+        $em = $args->getEntityManager();
+        $entity = $args->getEntity();
+        $uow = $em->getUnitOfWork();
+        
+        if ($entity instanceof Timestampable) {
+            $meta = $em->getClassMetadata(get_class($entity));
+            if ($this->_isFieldAvailable($em, $entity, 'created')) {
+                $meta->getReflectionProperty('created')->setValue($entity, new \DateTime('now'));
+            }
+            if ($this->_isFieldAvailable($em, $entity, 'updated')) {
+                $meta->getReflectionProperty('updated')->setValue($entity, new \DateTime('now'));
+            } elseif ($this->_isFieldAvailable($em, $entity, 'modified')) {
+                $meta->getReflectionProperty('modified')->setValue($entity, new \DateTime('now'));
+            }
+        }
+    }
+    
+    /**
+     * Checks if $field exists on entity and
+     * if it is in right type
+     * 
+     * @param EntityManager $em
+     * @param Timestampable $entity
+     * @param string $field
+     * @return boolean
+     */
+    protected function _isFieldAvailable(EntityManager $em, Timestampable $entity, $field)
+    {
+        $meta = $em->getClassMetadata(get_class($entity));
+        if ($meta->hasField($field) && in_array($meta->getTypeOfField($field), $this->_validTypes)) {
+            return true;
+        }
+        return false;
+    }
+}

+ 83 - 0
tests/DoctrineExtensions/Timestampable/Fixture/Article.php

@@ -0,0 +1,83 @@
+<?php
+namespace Timestampable\Fixture;
+
+use DoctrineExtensions\Timestampable\Timestampable;
+
+/**
+ * @Entity
+ */
+class Article implements Timestampable
+{
+    /** @Id @GeneratedValue @Column(type="integer") */
+    private $id;
+
+    /**
+     * @Column(name="title", type="string", length=128)
+     */
+    private $title;
+    
+    /**
+     * @OneToMany(targetEntity="Timestampable\Fixture\Comment", mappedBy="article")
+     */
+    private $comments;
+    
+    /**
+     * @var datetime $created
+     *
+     * @Column(name="created", type="date")
+     */
+    private $created;
+    
+    /**
+     * @var datetime $updated
+     *
+     * @Column(name="updated", type="datetime")
+     */
+    private $updated;
+
+    public function getId()
+    {
+        return $this->id;
+    }
+    
+    public function setTitle($title)
+    {
+        $this->title = $title;
+    }
+
+    public function getTitle()
+    {
+        return $this->title;
+    }
+    
+    public function addComment(Comment $comment)
+    {
+        $comment->setArticle($this);
+        $this->comments[] = $comment;
+    }
+
+    public function getComments()
+    {
+        return $this->comments;
+    }
+    
+    /**
+     * Get created
+     *
+     * @return datetime $created
+     */
+    public function getCreated()
+    {
+        return $this->created;
+    }
+    
+    /**
+     * Get updated
+     *
+     * @return datetime $updated
+     */
+    public function getUpdated()
+    {
+        return $this->updated;
+    }
+}

+ 61 - 0
tests/DoctrineExtensions/Timestampable/Fixture/Comment.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace Timestampable\Fixture;
+
+use DoctrineExtensions\Timestampable\Timestampable;
+
+/**
+ * @Entity
+ */
+class Comment implements Timestampable
+{
+    /** @Id @GeneratedValue @Column(type="integer") */
+    private $id;
+
+    /**
+     * @Column(name="message", type="text")
+     */
+    private $message;
+    
+    /**
+     * @ManyToOne(targetEntity="Timestampable\Fixture\Article", inversedBy="comments")
+     */
+    private $article;
+    
+    /**
+     * @var datetime $modified
+     *
+     * @Column(name="modified", type="time")
+     */
+    private $modified;
+
+    public function setArticle($article)
+    {
+        $this->article = $article;
+    }
+
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    public function setMessage($message)
+    {
+        $this->message = $message;
+    }
+
+    public function getMessage()
+    {
+        return $this->message;
+    }
+    
+    /**
+     * Get modified
+     *
+     * @return datetime $modified
+     */
+    public function getModified()
+    {
+        return $this->modified;
+    }
+}

+ 104 - 0
tests/DoctrineExtensions/Timestampable/TimestampableTest.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace DoctrineExtensions\Timestampable;
+
+use Doctrine\Common\Util\Debug,
+    Timestampable\Fixture\Article,
+    Timestampable\Fixture\Comment;
+
+/**
+ * These are tests for Tree behavior
+ * 
+ * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
+ * @package DoctrineExtensions.Tree
+ * @link http://www.gediminasm.org
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+class TimestampableTest extends \PHPUnit_Framework_TestCase
+{
+    const TEST_ENTITY_ARTICLE = "Timestampable\Fixture\Article";
+    const TEST_ENTITY_COMMENT = "Timestampable\Fixture\Comment";
+    private $em;
+
+    public function setUp()
+    {
+        $classLoader = new \Doctrine\Common\ClassLoader('Timestampable\Fixture', __DIR__ . '/../');
+        $classLoader->register();
+        
+        $config = new \Doctrine\ORM\Configuration();
+        $config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache);
+        $config->setQueryCacheImpl(new \Doctrine\Common\Cache\ArrayCache);
+        $config->setProxyDir(__DIR__ . '/temp');
+        $config->setProxyNamespace('DoctrineExtensions\Timestampable\Proxies');
+        $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver());
+
+        $conn = array(
+            'driver' => 'pdo_sqlite',
+            'memory' => true,
+        );
+
+        //$config->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger());
+        
+        $evm = new \Doctrine\Common\EventManager();
+        $timestampableListener = new TimestampableListener();
+        $evm->addEventSubscriber($timestampableListener);
+        $this->em = \Doctrine\ORM\EntityManager::create($conn, $config, $evm);
+
+        $schemaTool = new \Doctrine\ORM\Tools\SchemaTool($this->em);
+        $schemaTool->dropSchema(array());
+        $schemaTool->createSchema(array(
+            $this->em->getClassMetadata(self::TEST_ENTITY_ARTICLE),
+            $this->em->getClassMetadata(self::TEST_ENTITY_COMMENT)
+        ));
+    }
+    
+    public function testTimestampable()
+    {        
+        $sport = new Article();
+        $sport->setTitle('Sport');
+        
+        $this->assertTrue($sport instanceof Timestampable);
+        
+        $sportComment = new Comment();
+        $sportComment->setMessage('hello');
+        $sportComment->setArticle($sport);
+        
+        $this->assertTrue($sportComment instanceof Timestampable);
+        
+        $date = new \DateTime('now');
+        $this->em->persist($sport);
+        $this->em->persist($sportComment);
+        $this->em->flush();
+        $this->em->clear();
+        
+        $sport = $this->em->getRepository(self::TEST_ENTITY_ARTICLE)->findOneByTitle('Sport');
+        $this->assertEquals(
+            $date->format('Y-m-d 00:00:00'), 
+            $sport->getCreated()->format('Y-m-d H:i:s')
+        );
+        $this->assertEquals(
+            $date->format('Y-m-d H:i:s'), 
+            $sport->getUpdated()->format('Y-m-d H:i:s')
+        );
+        
+        $sportComment = $this->em->getRepository(self::TEST_ENTITY_COMMENT)->findOneByMessage('hello');
+        $this->assertEquals(
+            $date->format('H:i:s'), 
+            $sportComment->getModified()->format('H:i:s')
+        );
+        
+        sleep(1);
+        
+        $sport->setTitle('Updated');
+        $date = new \DateTime('now');
+        $this->em->persist($sport);
+        $this->em->flush();
+        $this->em->clear();
+        
+        $sport = $this->em->getRepository(self::TEST_ENTITY_ARTICLE)->findOneByTitle('Updated');
+        $this->assertEquals(
+            $date->format('Y-m-d H:i:s'), 
+            $sport->getUpdated()->format('Y-m-d H:i:s')
+        );
+    }
+}

+ 101 - 0
tests/DoctrineExtensions/Tree/temp/TreeFixtureBehavioralCategoryProxy.php

@@ -0,0 +1,101 @@
+<?php
+
+namespace DoctrineExtensions\Tree\Proxies;
+
+/**
+ * THIS CLASS WAS GENERATED BY THE DOCTRINE ORM. DO NOT EDIT THIS FILE.
+ */
+class TreeFixtureBehavioralCategoryProxy extends \Tree\Fixture\BehavioralCategory implements \Doctrine\ORM\Proxy\Proxy
+{
+    private $_entityPersister;
+    private $_identifier;
+    public $__isInitialized__ = false;
+    public function __construct($entityPersister, $identifier)
+    {
+        $this->_entityPersister = $entityPersister;
+        $this->_identifier = $identifier;
+    }
+    private function _load()
+    {
+        if (!$this->__isInitialized__ && $this->_entityPersister) {
+            $this->__isInitialized__ = true;
+            if ($this->_entityPersister->load($this->_identifier, $this) === null) {
+                throw new \Doctrine\ORM\EntityNotFoundException();
+            }
+            unset($this->_entityPersister, $this->_identifier);
+        }
+    }
+
+    
+    public function getId()
+    {
+        $this->_load();
+        return parent::getId();
+    }
+
+    public function getSlug()
+    {
+        $this->_load();
+        return parent::getSlug();
+    }
+
+    public function setTitle($title)
+    {
+        $this->_load();
+        return parent::setTitle($title);
+    }
+
+    public function getTitle()
+    {
+        $this->_load();
+        return parent::getTitle();
+    }
+
+    public function setParent(\Tree\Fixture\BehavioralCategory $parent)
+    {
+        $this->_load();
+        return parent::setParent($parent);
+    }
+
+    public function getParent()
+    {
+        $this->_load();
+        return parent::getParent();
+    }
+
+    public function getTreeConfiguration()
+    {
+        $this->_load();
+        return parent::getTreeConfiguration();
+    }
+
+    public function getSluggableConfiguration()
+    {
+        $this->_load();
+        return parent::getSluggableConfiguration();
+    }
+
+    public function getTranslatableFields()
+    {
+        $this->_load();
+        return parent::getTranslatableFields();
+    }
+
+    public function getTranslatableLocale()
+    {
+        $this->_load();
+        return parent::getTranslatableLocale();
+    }
+
+    public function getTranslationEntity()
+    {
+        $this->_load();
+        return parent::getTranslationEntity();
+    }
+
+
+    public function __sleep()
+    {
+        return array('__isInitialized__', 'id', 'title', 'lft', 'rgt', 'parent', 'children', 'slug');
+    }
+}

+ 71 - 0
tests/DoctrineExtensions/Tree/temp/TreeFixtureCategoryProxy.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace DoctrineExtensions\Tree\Proxies;
+
+/**
+ * THIS CLASS WAS GENERATED BY THE DOCTRINE ORM. DO NOT EDIT THIS FILE.
+ */
+class TreeFixtureCategoryProxy extends \Tree\Fixture\Category implements \Doctrine\ORM\Proxy\Proxy
+{
+    private $_entityPersister;
+    private $_identifier;
+    public $__isInitialized__ = false;
+    public function __construct($entityPersister, $identifier)
+    {
+        $this->_entityPersister = $entityPersister;
+        $this->_identifier = $identifier;
+    }
+    private function _load()
+    {
+        if (!$this->__isInitialized__ && $this->_entityPersister) {
+            $this->__isInitialized__ = true;
+            if ($this->_entityPersister->load($this->_identifier, $this) === null) {
+                throw new \Doctrine\ORM\EntityNotFoundException();
+            }
+            unset($this->_entityPersister, $this->_identifier);
+        }
+    }
+
+    
+    public function getId()
+    {
+        $this->_load();
+        return parent::getId();
+    }
+
+    public function setTitle($title)
+    {
+        $this->_load();
+        return parent::setTitle($title);
+    }
+
+    public function getTitle()
+    {
+        $this->_load();
+        return parent::getTitle();
+    }
+
+    public function setParent(\Tree\Fixture\Category $parent)
+    {
+        $this->_load();
+        return parent::setParent($parent);
+    }
+
+    public function getParent()
+    {
+        $this->_load();
+        return parent::getParent();
+    }
+
+    public function getTreeConfiguration()
+    {
+        $this->_load();
+        return parent::getTreeConfiguration();
+    }
+
+
+    public function __sleep()
+    {
+        return array('__isInitialized__', 'id', 'title', 'lft', 'rgt', 'parent', 'children', 'comments');
+    }
+}

+ 3 - 0
tests/phpunit.dist.xml

@@ -10,6 +10,9 @@
         <testsuite name="Tree Extension">
             <directory suffix=".php">./DoctrineExtensions/Tree/</directory>
         </testsuite>
+        <testsuite name="Timestampable Extension">
+            <directory suffix=".php">./DoctrineExtensions/Timestampable/</directory>
+        </testsuite>
     </testsuites>
     <php>
         <const name="DOCTRINE_LIBRARY_PATH" value="/path/to/library/where/Doctrine/located"/>