Bladeren bron

[sluggable] take into account persisted slugs, closes #45

Gediminas Morkevicius 14 jaren geleden
bovenliggende
commit
9af57579da
2 gewijzigde bestanden met toevoegingen van 99 en 50 verwijderingen
  1. 81 48
      lib/Gedmo/Sluggable/SluggableListener.php
  2. 18 2
      tests/Gedmo/Sluggable/SluggableTest.php

+ 81 - 48
lib/Gedmo/Sluggable/SluggableListener.php

@@ -2,11 +2,11 @@
 
 namespace Gedmo\Sluggable;
 
-use Doctrine\Common\EventArgs,
-    Doctrine\Common\Persistence\ObjectManager,
-    Doctrine\Common\Persistence\Mapping\ClassMetadata,
-    Gedmo\Mapping\MappedEventSubscriber,
-    Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
+use Doctrine\Common\EventArgs;
+use Doctrine\Common\Persistence\ObjectManager;
+use Doctrine\Common\Persistence\Mapping\ClassMetadata;
+use Gedmo\Mapping\MappedEventSubscriber;
+use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
 
 /**
  * The SluggableListener handles the generation of slugs
@@ -24,20 +24,6 @@ use Doctrine\Common\EventArgs,
  */
 class SluggableListener extends MappedEventSubscriber
 {
-    /**
-     * Specifies the list of events to listen
-     *
-     * @return array
-     */
-    public function getSubscribedEvents()
-    {
-        return array(
-            'prePersist',
-            'onFlush',
-            'loadClassMetadata'
-        );
-    }
-
     /**
      * The power exponent to jump
      * the slug unique number by tens.
@@ -53,6 +39,28 @@ class SluggableListener extends MappedEventSubscriber
      */
     private $transliterator = array('Gedmo\Sluggable\Util\Urlizer', 'transliterate');
 
+    /**
+     * List of inserted slugs for each object class.
+     * This is needed in case there are identical slug
+     * composition in number of persisted objects
+     *
+     * @var array
+     */
+    private $persistedSlugs = array();
+
+    /**
+     * Specifies the list of events to listen
+     *
+     * @return array
+     */
+    public function getSubscribedEvents()
+    {
+        return array(
+            'onFlush',
+            'loadClassMetadata'
+        );
+    }
+
     /**
      * Set the transliteration callable method
      * to transliterate slugs
@@ -68,33 +76,25 @@ class SluggableListener extends MappedEventSubscriber
     }
 
     /**
-     * Mapps additional metadata
+     * Get currently used transliterator callable
      *
-     * @param EventArgs $eventArgs
-     * @return void
+     * @return callable
      */
-    public function loadClassMetadata(EventArgs $eventArgs)
+    public function getTransliterator()
     {
-        $ea = $this->getEventAdapter($eventArgs);
-        $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
+        return $this->transliterator;
     }
 
     /**
-     * Checks for persisted object to specify slug
+     * Mapps additional metadata
      *
-     * @param EventArgs $args
+     * @param EventArgs $eventArgs
      * @return void
      */
-    public function prePersist(EventArgs $args)
+    public function loadClassMetadata(EventArgs $eventArgs)
     {
-        $ea = $this->getEventAdapter($args);
-        $om = $ea->getObjectManager();
-        $object = $ea->getObject();
-        $meta = $om->getClassMetadata(get_class($object));
-
-        if ($config = $this->getConfiguration($om, $meta->name)) {
-            $this->generateSlug($ea, $object, false);
-        }
+        $ea = $this->getEventAdapter($eventArgs);
+        $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
     }
 
     /**
@@ -110,13 +110,25 @@ class SluggableListener extends MappedEventSubscriber
         $om = $ea->getObjectManager();
         $uow = $om->getUnitOfWork();
 
+        // process all objects being inserted, using scheduled insertions instead
+        // of prePersist in case if record will be changed before flushing this will
+        // ensure correct result. No additional overhead is encoutered
+        foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
+            $meta = $om->getClassMetadata(get_class($object));
+            if ($config = $this->getConfiguration($om, $meta->name)) {
+                // generate first to exclude this object from similar persisted slugs result
+                $this->generateSlug($ea, $object);
+                $slug = $meta->getReflectionProperty($config['slug'])->getValue($object);
+                $this->persistedSlugs[$meta->name][] = $slug;
+            }
+        }
         // we use onFlush and not preUpdate event to let other
         // event listeners be nested together
         foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
             $meta = $om->getClassMetadata(get_class($object));
             if ($config = $this->getConfiguration($om, $meta->name)) {
                 if ($config['updatable']) {
-                    $this->generateSlug($ea, $object, $ea->getObjectChangeSet($uow, $object));
+                    $this->generateSlug($ea, $object);
                 }
             }
         }
@@ -135,18 +147,16 @@ class SluggableListener extends MappedEventSubscriber
      *
      * @param SluggableAdapter $ea
      * @param object $object
-     * @param mixed $changeSet
-     *      case array: the change set array
-     *      case boolean(false): object is not managed
      * @throws UnexpectedValueException - if parameters are missing
      *      or invalid
      * @return void
      */
-    protected function generateSlug(SluggableAdapter $ea, $object, $changeSet)
+    private function generateSlug(SluggableAdapter $ea, $object)
     {
         $om = $ea->getObjectManager();
         $meta = $om->getClassMetadata(get_class($object));
         $uow = $om->getUnitOfWork();
+        $changeSet = $ea->getObjectChangeSet($uow, $object);
         $config = $this->getConfiguration($om, $meta->name);
 
         // sort sluggable fields by position
@@ -162,7 +172,7 @@ class SluggableListener extends MappedEventSubscriber
         $slug = '';
         $needToChangeSlug = false;
         foreach ($fields as $sluggableField) {
-            if ($changeSet === false || isset($changeSet[$sluggableField['field']])) {
+            if (isset($changeSet[$sluggableField['field']])) {
                 $needToChangeSlug = true;
             }
             $slug .= $meta->getReflectionProperty($sluggableField['field'])->getValue($object) . ' ';
@@ -210,10 +220,8 @@ class SluggableListener extends MappedEventSubscriber
         }
         // set the final slug
         $meta->getReflectionProperty($config['slug'])->setValue($object, $slug);
-        // recompute changeset if object is managed
-        if ($changeSet !== false) {
-            $ea->recomputeSingleObjectChangeSet($uow, $meta, $object);
-        }
+        // recompute changeset
+        $ea->recomputeSingleObjectChangeSet($uow, $meta, $object);
     }
 
     /**
@@ -224,7 +232,7 @@ class SluggableListener extends MappedEventSubscriber
      * @param string $preferedSlug
      * @return string - unique slug
      */
-    protected function makeUniqueSlug(SluggableAdapter $ea, $object, $preferedSlug)
+    private function makeUniqueSlug(SluggableAdapter $ea, $object, $preferedSlug)
     {
         $om = $ea->getObjectManager();
         $meta = $om->getClassMetadata(get_class($object));
@@ -232,6 +240,8 @@ class SluggableListener extends MappedEventSubscriber
 
         // search for similar slug
         $result = $ea->getSimilarSlugs($object, $meta, $config, $preferedSlug);
+        // add similar persisted slugs into account
+        $result += $this->getSimilarPersistedSlugs($meta->name, $preferedSlug);
 
         if ($result) {
             $generatedSlug = $preferedSlug;
@@ -259,4 +269,27 @@ class SluggableListener extends MappedEventSubscriber
         }
         return $preferedSlug;
     }
-}
+
+    /**
+     * In case if any number of records are persisted instantly
+     * and they contain same slugs. This method will filter those
+     * identical slugs specialy for persisted objects. Returns
+     * array of similar slugs found
+     *
+     * @param string $class
+     * @param string $preferedSlug
+     * @return array
+     */
+    private function getSimilarPersistedSlugs($class, $preferedSlug)
+    {
+        $result = array();
+        if (isset($this->persistedSlugs[$class])) {
+            array_walk($this->persistedSlugs[$class], function($val) use ($preferedSlug, &$result) {
+                if (preg_match("/{$preferedSlug}.*/smi", $val)) {
+                    $result[] = array('slug' => $val);
+                }
+            });
+        }
+        return $result;
+    }
+}

+ 18 - 2
tests/Gedmo/Sluggable/SluggableTest.php

@@ -4,8 +4,7 @@ namespace Gedmo\Sluggable;
 
 use Doctrine\Common\EventManager;
 use Tool\BaseTestCaseORM;
-use Doctrine\Common\Util\Debug,
-    Sluggable\Fixture\Article;
+use Sluggable\Fixture\Article;
 
 /**
  * These are tests for sluggable behavior
@@ -111,6 +110,23 @@ class SluggableTest extends BaseTestCaseORM
         $this->assertEquals($article->getSlug(), 'the-title-updated-my-code');
     }
 
+    public function testGithubIssue45()
+    {
+        $article = new Article;
+        $article->setTitle('test');
+        $article->setCode('code');
+        $this->em->persist($article);
+
+        $article2 = new Article;
+        $article2->setTitle('test');
+        $article2->setCode('code');
+        $this->em->persist($article2);
+
+        $this->em->flush();
+        $this->assertEquals('test-code', $article->getSlug());
+        $this->assertEquals('test-code-1', $article2->getSlug());
+    }
+
     protected function getUsedEntityFixtures()
     {
         return array(