소스 검색

sluggable behavior optimizations

gediminasm 14 년 전
부모
커밋
9880129958

+ 2 - 0
README.markdown

@@ -205,6 +205,8 @@ want and on flush everything magicaly will be created and updated..
 
 ### Recent Updates
 
+- Optimized to support unique index on slug, except concurent update flush. Now it takes less
+ queries to perform the slug operations.
 - Implemented more abstracted functionality for getting and setting field properties.
 - Implemented slug type case and separator configuration. Example camel cased and underscored
 slug: Example_Slug, default: example-slug.

+ 6 - 1
lib/DoctrineExtensions/Sluggable/Exception.php

@@ -45,6 +45,11 @@ class Exception extends \Exception
     
     static public function slugFieldIsUnique($slugField)
     {
-        return new self("SluggableListener cannot support unique slug field '{$slugField}' yet, make an index on it.");
+        return new self("SluggableListener cannot support unique slug field '{$slugField}' during concurent updates, make an index on it.");
+    }
+    
+    static public function slugIsEmpty()
+    {
+    	return new self("SluggableListener was unable to find any non empty sluggable fields, make sure they have something at least.");
     }
 }

+ 86 - 67
lib/DoctrineExtensions/Sluggable/SluggableListener.php

@@ -32,12 +32,12 @@ class SluggableListener implements EventSubscriber
 	protected $_configurations = array();
 	
 	/**
-	 * List of entities which needs to be processed
-	 * after the insertion operations, because
-	 * query executions will be needed
-	 * 
-	 * @var array
-	 */
+     * List of entities which needs to be processed
+     * after the insertion operations, because
+     * query executions will be needed
+     * 
+     * @var array
+     */
 	protected $_pendingEntities = array();
 	
 	/**
@@ -48,8 +48,9 @@ class SluggableListener implements EventSubscriber
     public function getSubscribedEvents()
     {
         return array(
-            Events::onFlush,
-            Events::postPersist
+            Events::prePersist,
+            Events::postPersist,
+            Events::onFlush
         );
     }
 		
@@ -67,32 +68,20 @@ class SluggableListener implements EventSubscriber
 		}
 		return $this->_configurations[$entityClass];
 	}
-	
-	/**
-	 * Looks for Sluggable entities being inserted or updated
-	 * for further processing
-	 * 
-	 * @param OnFlushEventArgs $args
-	 * @return void
-	 */
-    public function onFlush(OnFlushEventArgs $args)
+    
+    /**
+     * Checks for persisted entity to specify slug
+     * 
+     * @param LifecycleEventArgs $args
+     * @return void
+     */
+    public function prePersist(LifecycleEventArgs $args)
     {
         $em = $args->getEntityManager();
-        $uow = $em->getUnitOfWork();
-        // check all scheduled inserts for Translatable entities
-        foreach ($uow->getScheduledEntityInsertions() as $entity) {
-            if ($entity instanceof Sluggable) {
-                $this->_generateSlug($em, $entity, true);
-            }
-        }
-        // check all scheduled updates for Translatable entities
-        foreach ($uow->getScheduledEntityUpdates() as $entity) {
-            if ($entity instanceof Sluggable) {
-            	$config = $this->getConfiguration($entity);
-            	if ($config->isUpdatable()) {
-                    $this->_generateSlug($em, $entity, false);
-            	}
-            }
+        $entity = $args->getEntity();
+
+        if ($entity instanceof Sluggable) {
+		    $this->_generateSlug($em, $entity, false);
         }
     }
     
@@ -110,16 +99,40 @@ class SluggableListener implements EventSubscriber
         // there can be other entities being inserted because
         // unitofwork does inserts by class ordered chunks
         if (!$uow->hasPendingInsertions()) {
-	        while ($entity = array_shift($this->_pendingEntities)) {
-	        	$config = $this->getConfiguration($entity);
-	        	if ($config->isUnique()) {
-		            $slugField = $config->getSlugField();
-		        	$slug = $this->_makeUniqueSlug($em, $entity);
-		        	$uow->scheduleExtraUpdate($entity, array(
-			            $slugField => array(null, $slug)
-			        ));
-	        	}
-	        }
+            while ($entity = array_shift($this->_pendingEntities)) {
+            	// we know that this slug must be unique and
+            	// it was preprocessed allready
+                $config = $this->getConfiguration($entity);
+                $slugField = $config->getSlugField();
+                $slug = $this->_makeUniqueSlug($em, $entity);
+                $uow->scheduleExtraUpdate($entity, array(
+                    $slugField => array(null, $slug)
+                ));
+            }
+        }
+    }
+    
+    /**
+     * Generate slug on entities being updated during flush
+     * if they require changing
+     * 
+     * @param OnFlushEventArgs $args
+     * @return void
+     */
+    public function onFlush(OnFlushEventArgs $args)
+    {
+        $em = $args->getEntityManager();
+        $uow = $em->getUnitOfWork();
+        
+        // we use onFlush and not preUpdate event to let other
+        // event listeners be nested together
+        foreach ($uow->getScheduledEntityUpdates() as $entity) {
+            if ($entity instanceof Sluggable) {
+            	$config = $this->getConfiguration($entity);
+            	if ($config->isUpdatable()) {
+                    $this->_generateSlug($em, $entity, $uow->getEntityChangeSet($entity));
+            	}
+            }
         }
     }
     
@@ -128,34 +141,26 @@ class SluggableListener implements EventSubscriber
      * 
      * @param EntityManager $em
      * @param Sluggable $entity
-     * @param boolean $isInsert
+     * @param mixed $changeSet
+     *      case array: the change set array
+     *      case boolean(false): entity is not managed
      * @throws Sluggable\Exception if parameters are missing
      *      or invalid
      * @return void
      */
-    protected function _generateSlug(EntityManager $em, Sluggable $entity, $isInsert)
+    protected function _generateSlug(EntityManager $em, Sluggable $entity, $changeSet)
     {
     	$entityClass = get_class($entity);
     	$uow = $em->getUnitOfWork();
         $entityClassMetadata = $em->getClassMetadata($entityClass);
         $config = $this->getConfiguration($entity);
         
-        // if entity is not updatable and it is update, no need processing
-        if (!$isInsert && !$config->isUpdatable()) {
-        	return; // nothing to do
-        }
-        
         // check if slug field exists
         $slugField = $config->getSlugField();
         if (!$entityClassMetadata->hasField($slugField)) {
         	throw Exception::cannotFindSlugField($slugField);
         }
         
-        // @todo: make support for unique field metadata
-        if ($entityClassMetadata->isUniqueField($slugField)) {
-        	throw Exception::slugFieldIsUnique($slugField);
-        }
-        
         // check if slug metadata is valid
         $preferedLength = $config->getLength();
         $mapping = $entityClassMetadata->getFieldMapping($slugField);
@@ -174,12 +179,11 @@ class SluggableListener implements EventSubscriber
         // collect the slug from fields
         $slug = '';
         $needToChangeSlug = false;
-        $changeSet = $uow->getEntityChangeSet($entity);
         foreach ($sluggableFields as $sluggableField) {
         	if (!$entityClassMetadata->hasField($sluggableField)) {
         		throw Exception::cannotFindFieldToSlug($sluggableField);
         	}
-        	if (isset($changeSet[$sluggableField])) {
+        	if ($changeSet === false || isset($changeSet[$sluggableField])) {
         		$needToChangeSlug = true;
         	}
         	$slug .= $entityClassMetadata->getReflectionProperty($sluggableField)->getValue($entity) . ' ';
@@ -188,6 +192,11 @@ class SluggableListener implements EventSubscriber
         if (!$needToChangeSlug) {
         	return; // nothing to do
         }
+        
+        if (!strlen(trim($slug))) {
+            throw Exception::slugIsEmpty();
+        }
+        
         // build the slug
         $builder = $config->getSlugBuilder();
         $separator = $config->getSeparator();
@@ -206,25 +215,35 @@ class SluggableListener implements EventSubscriber
                     $slug
                 );
                 break;
+                
+        	default:
+        	    // leave it as is
+        	    break;
         }
         
         // cut slug if exceeded in length
         if ($preferedLength && strlen($slug) > $preferedLength) {
             $slug = substr($slug, 0, $preferedLength);
         }
-        
-        // set the slug
-        $entityClassMetadata->getReflectionProperty($slugField)->setValue($entity, $slug);
-        if ($config->isUnique() && ($isInsert || $uow->hasPendingInsertions())) {
-        	// leave for further processing after insertion
-            $this->_pendingEntities[spl_object_hash($entity)] = $entity;
-        } elseif ($config->isUnique()) {
-        	// make slug unique
-            $slug = $this->_makeUniqueSlug($em, $entity);
+
+        // make unique slug if requested
+        if ($config->isUnique() && !$uow->hasPendingInsertions()) {
+        	// set the slug for further processing
         	$entityClassMetadata->getReflectionProperty($slugField)->setValue($entity, $slug);
+            $slug = $this->_makeUniqueSlug($em, $entity);
+        }
+        // set the final slug
+        $entityClassMetadata->getReflectionProperty($slugField)->setValue($entity, $slug);
+        // recompute changeset if entity is managed
+        if ($changeSet !== false) {
+            $uow->recomputeSingleEntityChangeSet($entityClassMetadata, $entity);
+        } elseif ($config->isUnique() && $uow->hasPendingInsertions()) {
+            // @todo: make support for unique field metadata
+            if ($entityClassMetadata->isUniqueField($slugField)) {
+                throw Exception::slugFieldIsUnique($slugField);
+            }
+            $this->_pendingEntities[] = $entity;
         }
-        // update the changset
-        $uow->recomputeSingleEntityChangeSet($entityClassMetadata, $entity);
     }
     
     /**

+ 1 - 1
tests/DoctrineExtensions/Sluggable/SluggableTest.php

@@ -143,7 +143,7 @@ class Article implements Sluggable
     private $code;
     
     /**
-     * @Column(name="slug", type="string", length=128)
+     * @Column(name="slug", type="string", length=128, unique=true)
      */
     private $slug;