ソースを参照

Fix Slugged behavior to allow for non-deprecated and custom slugger.

mscherer 7 年 前
コミット
a93e3cf18d

+ 14 - 3
docs/Behavior/Slugged.md

@@ -44,6 +44,7 @@ A CakePHP behavior to automatically create and store slugs.
                     <li> <b>url: </b> returns a slug appropriate to put in a URL </li>
                     <li> <b>class: </b> a dummy mode which returns a slug appropriate to put in a html class (there are no restrictions) </li>
                     <li> <b>id: </b> returns a slug appropriate to use in a html id </li>
+                    <li> <b>{callable}: </b> Use your custom callable to pass in your slugger method </li>
                 </ul>
             </td>
         </tr>
@@ -96,17 +97,17 @@ A CakePHP behavior to automatically create and store slugs.
         <tr>
             <td>    replace  </td>
             <td>    </td>
-            <td>    custom replacements as array.     </td>
+            <td>    Custom replacements as array. `Set to null` to disable.    </td>
         </tr>
         <tr>
             <td>    on  </td>
             <td>    </td>
-            <td>    beforeSave or beforeValidate.     </td>
+            <td>    `beforeSave` or `beforeMarshal` or `beforeRules`.     </td>
         </tr>
         <tr>
             <td>    scope  </td>
             <td>    </td>
-            <td>    certain conditions to use as scope.     </td>
+            <td>    Certain conditions to use as scope.     </td>
         </tr>
         <tr>
             <td>    tidy  </td>
@@ -153,3 +154,13 @@ $this->addBehavior('Tools.Slugged',
 Note that we don't need "unique" either then.
 
 Each save now re-triggers the slug generation.
+
+### Using a custom slugger
+You can pass your own callable for slugging into the `mode` config.
+And you can even use a static method on any class this way (given it has a static `slug()` method):
+```
+$this->addBehavior('Tools.Slugged', ['mode' => [MySlugger::class, 'slug']]);
+```
+
+Tip: Use `'mode' => [Text::class, 'slug']` if you want to avoid using the deprecated `Inflector::slug()` method.
+Don't forget the use statement at the top of the file, though (`use Tools\Utility\Text;`).

+ 12 - 3
src/Model/Behavior/SluggedBehavior.php

@@ -34,11 +34,12 @@ class SluggedBehavior extends Behavior {
 	 * - field: The slug field name
 	 * - overwriteField: The boolean field to trigger overwriting if "overwrite" is false
 	 * - mode: has the following values
-	 *     ascii - retuns an ascii slug generated using the core Inflector::slug() function
+	 *     ascii - returns an ascii slug generated using the core Inflector::slug() function
 	 *     display - a dummy mode which returns a slug legal for display - removes illegal (not unprintable) characters
 	 *     url - returns a slug appropriate to put in a URL
 	 *     class - a dummy mode which returns a slug appropriate to put in a html class (there are no restrictions)
 	 *     id - retuns a slug appropriate to use in a html id
+	 *     OR pass it a callable as custom method to be invoked
 	 * - separator: The separator to use
 	 * - length:
 	 *  Set to 0 for no length. Will be auto-detected if possible via schema.
@@ -46,7 +47,7 @@ class SluggedBehavior extends Behavior {
 	 *     false - once the slug has been saved, do not change it (use if you are doing lookups based on slugs)
 	 *     true - if the label field values change, regenerate the slug (use if you are the slug is just window-dressing)
 	 * - unique: has 2 values
-	 *     false - will not enforce a unique slug, whatever the label is is direclty slugged without checking for duplicates
+	 *     false - will not enforce a unique slug, whatever the label is is directly slugged without checking for duplicates
 	 *     true - use if you are doing lookups based on slugs (see overwrite)
 	 * - case: has the following values
 	 *     null - don't change the case of the slug
@@ -262,7 +263,15 @@ class SluggedBehavior extends Behavior {
 		if ($replace) {
 			$string = str_replace(array_keys($replace), array_values($replace), $string);
 		}
-		if ($this->_config['mode'] === 'ascii') {
+
+		if (!is_string($this->_config['mode'])) {
+			$callable = $this->_config['mode'];
+			if (!is_callable($callable)) {
+				throw new RuntimeException('Invalid callable passed as mode.');
+			}
+			$slug = $callable($string);
+
+		} elseif ($this->_config['mode'] === 'ascii') {
 			$slug = Inflector::slug($string, $separator);
 		} else {
 			$regex = $this->_regex($this->_config['mode']);

+ 33 - 39
src/View/Helper/FormatHelper.php

@@ -7,6 +7,7 @@ use Cake\Utility\Inflector;
 use Cake\View\Helper;
 use Cake\View\StringTemplate;
 use Cake\View\View;
+use RuntimeException;
 
 /**
  * Format helper with basic html snippets
@@ -62,7 +63,8 @@ class FormatHelper extends Helper {
 		'templates' => [
 			'icon' => '<i class="{{class}}"{{attributes}}></i>',
 			'ok' => '<span class="ok-{{type}}" style="color:{{color}}"{{attributes}}>{{content}}</span>'
-		]
+		],
+		'slugger' => null,
 	];
 
 	/**
@@ -108,25 +110,6 @@ class FormatHelper extends Helper {
 	 * @return string
 	 */
 	public function neighbors(array $neighbors, $field, array $options = []) {
-		$alias = null;
-		if (mb_strpos($field, '.') !== false) {
-			$fieldArray = explode('.', $field, 2);
-			$alias = $fieldArray[0];
-			$field = $fieldArray[1];
-		}
-
-		if (empty($alias)) {
-			if (!empty($neighbors['prev'])) {
-				$modelNames = array_keys($neighbors['prev']);
-				$alias = $modelNames[0];
-			} elseif (!empty($neighbors['next'])) {
-				$modelNames = array_keys($neighbors['next']);
-				$alias = $modelNames[0];
-			}
-		}
-		if (empty($field)) {
-		}
-
 		$name = 'Record'; // Translation further down!
 		if (!empty($options['name'])) {
 			$name = ucfirst($options['name']);
@@ -135,22 +118,15 @@ class FormatHelper extends Helper {
 		$prevSlug = $nextSlug = null;
 		if (!empty($options['slug'])) {
 			if (!empty($neighbors['prev'])) {
-				$prevSlug = Inflector::slug($neighbors['prev'][$alias][$field], '-');
+				$prevSlug = $this->slug($neighbors['prev'][$field]);
 			}
 			if (!empty($neighbors['next'])) {
-				$nextSlug = Inflector::slug($neighbors['next'][$alias][$field], '-');
+				$nextSlug = $this->slug($neighbors['next'][$field]);
 			}
 		}
-		$titleAlias = $alias;
 		$titleField = $field;
 		if (!empty($options['titleField'])) {
-			if (mb_strpos($options['titleField'], '.') !== false) {
-				$fieldArray = explode('.', $options['titleField'], 2);
-				$titleAlias = $fieldArray[0];
-				$titleField = $fieldArray[1];
-			} else {
-				$titleField = $options['titleField'];
-			}
+			$titleField = $options['titleField'];
 		}
 		if (!isset($options['escape']) || $options['escape'] === false) {
 			$titleField = h($titleField);
@@ -158,39 +134,38 @@ class FormatHelper extends Helper {
 
 		$ret = '<div class="next-prev-navi nextPrevNavi">';
 		if (!empty($neighbors['prev'])) {
-			$url = [$neighbors['prev'][$alias]['id'], $prevSlug];
+			$url = [$neighbors['prev']['id'], $prevSlug];
 			if (!empty($options['url'])) {
 				$url += $options['url'];
 			}
 
-			// ICON_PREV, false
 			$ret .= $this->Html->link(
 				$this->icon('prev') . '&nbsp;' . __d('tools', 'prev' . $name),
 				$url,
-				['escape' => false, 'title' => $neighbors['prev'][$titleAlias][$titleField]]
+				['escape' => false, 'title' => $neighbors['prev'][$titleField]]
 			);
 		} else {
-			//ICON_PREV_DISABLED, __d('tools', 'noPrev' . $name)) . '&nbsp;' . __d('tools', 'prev' . $name
 			$ret .= $this->icon('prev');
 		}
+
 		$ret .= '&nbsp;&nbsp;';
 		if (!empty($neighbors['next'])) {
-			$url = [$neighbors['next'][$alias]['id'], $prevSlug];
+			$url = [$neighbors['next']['id'], $nextSlug];
 			if (!empty($options['url'])) {
 				$url += $options['url'];
 			}
 
-			// ICON_NEXT, false
 			$ret .= $this->Html->link(
 				$this->icon('next') . '&nbsp;' . __d('tools', 'next' . $name),
 				$url,
-				['escape' => false, 'title' => $neighbors['next'][$titleAlias][$titleField]]
+				['escape' => false, 'title' => $neighbors['next'][$titleField]]
 			);
 		} else {
-			// ICON_NEXT_DISABLED, __d('tools', 'noNext' . $name)
 			$ret .= $this->icon('next') . '&nbsp;' . __d('tools', 'next' . $name);
 		}
+
 		$ret .= '</div>';
+
 		return $ret;
 	}
 
@@ -349,7 +324,7 @@ class FormatHelper extends Helper {
 
 		$type = pathinfo($icon, PATHINFO_FILENAME);
 		$title = ucfirst($type);
-		$alt = Inflector::slug($title);
+		$alt = $this->slug($title);
 		if ($translate !== false) {
 			$title = __($title);
 			$alt = __($alt);
@@ -727,4 +702,23 @@ class FormatHelper extends Helper {
 		return $table;
 	}
 
+	/**
+	 * @param string $string
+	 *
+	 * @return string
+	 * @throws \RuntimeException
+	 */
+	public function slug($string) {
+		if ($this->_config['slugger']) {
+			$callable = $this->_config['slugger'];
+			if (!is_callable($callable)) {
+				throw new RuntimeException('Invalid callable passed as slugger.');
+			}
+
+			return $callable($string);
+		}
+
+		return Inflector::slug($string);
+	}
+
 }

+ 49 - 1
tests/TestCase/Model/Behavior/SluggedBehaviorTest.php

@@ -6,6 +6,7 @@ use Cake\Core\Configure;
 use Cake\ORM\Entity;
 use Cake\ORM\TableRegistry;
 use Tools\TestSuite\TestCase;
+use Tools\Utility\Text;
 
 /**
  * SluggedBehaviorTest
@@ -612,7 +613,7 @@ class SluggedBehaviorTest extends TestCase {
 	 *
 	 * @return void
 	 */
-	public function testSlugGenerationWithVirualField() {
+	public function testSlugGenerationWithVirtualField() {
 		$this->articles->removeBehavior('Slugged');
 		$this->articles->setEntityClass('\App\Model\Entity\SluggedArticle');
 		$this->articles->addBehavior('Tools.Slugged', [
@@ -631,6 +632,53 @@ class SluggedBehaviorTest extends TestCase {
 	}
 
 	/**
+	 * Test slug generation works with new slugger.
+	 *
+	 * @return void
+	 */
+	public function testSlugGenerationWithNewSlugger() {
+		$this->articles->removeBehavior('Slugged');
+		$this->articles->addBehavior('Tools.Slugged', [
+			'mode' => [Text::class, 'slug'],
+		]);
+
+		$data = ['title' => 'Some Article 12345'];
+
+		$article = $this->articles->newEntity($data);
+		$result = $this->articles->save($article);
+		$this->assertTrue((bool)$result);
+		$this->assertEquals('Some-Article-12345', $result['slug']);
+	}
+
+	/**
+	 * Test slug generation works with custom slugger.
+	 *
+	 * @return void
+	 */
+	public function testSlugGenerationWithCustomSlugger() {
+		$this->articles->removeBehavior('Slugged');
+		$this->articles->addBehavior('Tools.Slugged', [
+			'mode' => [$this, '_customSluggerMethod'],
+		]);
+
+		$data = ['title' => 'Some Article 12345'];
+
+		$article = $this->articles->newEntity($data);
+		$result = $this->articles->save($article);
+		$this->assertTrue((bool)$result);
+		$this->assertEquals('some article 12345', $result['slug']);
+	}
+
+	/**
+	 * @param string $name
+	 *
+	 * @return string
+	 */
+	public function _customSluggerMethod($name) {
+		return mb_strtolower($name);
+	}
+
+	/**
 	 * Get a new Entity
 	 *
 	 * @param string|null $title

+ 47 - 1
tests/TestCase/View/Helper/FormatHelperTest.php

@@ -5,6 +5,7 @@ namespace Tools\Test\TestCase\View\Helper;
 use Cake\Core\Configure;
 use Cake\View\View;
 use Tools\TestSuite\TestCase;
+use Tools\Utility\Text;
 use Tools\View\Helper\FormatHelper;
 
 /**
@@ -250,8 +251,36 @@ class FormatHelperTest extends TestCase {
 	}
 
 	/**
-	 * FormatHelperTest::testConfigure()
+	 * @return void
+	 */
+	public function testSlug() {
+		$result = $this->Format->slug('A Baz D & Foo');
+		$this->assertSame('A-Baz-D-Foo', $result);
+
+		$this->Format->setConfig('slugger', [Text::class, 'slug']);
+		$result = $this->Format->slug('A Baz D & Foo');
+		$this->assertSame('A-Baz-D-Foo', $result);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testSlugCustomObject() {
+		$this->Format->setConfig('slugger', [$this, '_testSlugger']);
+		$result = $this->Format->slug('A Baz D & Foo');
+		$this->assertSame('a baz d & foo', $result);
+	}
+
+	/**
+	 * @param string $name
 	 *
+	 * @return string
+	 */
+	public function _testSlugger($name) {
+		return mb_strtolower($name);
+	}
+
+	/**
 	 * @return void
 	 */
 	public function testNeighbors() {
@@ -268,6 +297,23 @@ class FormatHelperTest extends TestCase {
 	}
 
 	/**
+	 * Test slug generation works with new slugger.
+	 *
+	 * @return void
+	 */
+	public function testSlugGenerationWithNewSlugger() {
+		$neighbors = [
+			'prev' => ['id' => 1, 'foo' => 'My Foo'],
+			'next' => ['id' => 2, 'foo' => 'My FooBaz'],
+		];
+
+		$result = $this->Format->neighbors($neighbors, 'foo', ['slug' => true]);
+
+		$expected = '<div class="next-prev-navi nextPrevNavi"><a href="/index/1/My-Foo" title="My Foo"><i class="icon icon-prev fa fa-prev" title="Prev" data-placement="bottom" data-toggle="tooltip"></i>&nbsp;prevRecord</a>&nbsp;&nbsp;<a href="/index/2/My-FooBaz" title="My FooBaz"><i class="icon icon-next fa fa-next" title="Next" data-placement="bottom" data-toggle="tooltip"></i>&nbsp;nextRecord</a></div>';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
 	 * FormatHelperTest::testTab2space()
 	 *
 	 * @return void