From a93e3cf18d592154990a6835b1bc56e7ceac8b73 Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 26 Nov 2018 19:13:10 +0100 Subject: [PATCH] Fix Slugged behavior to allow for non-deprecated and custom slugger. --- docs/Behavior/Slugged.md | 17 ++++- src/Model/Behavior/SluggedBehavior.php | 15 +++- src/View/Helper/FormatHelper.php | 72 +++++++++---------- .../Model/Behavior/SluggedBehaviorTest.php | 50 ++++++++++++- .../TestCase/View/Helper/FormatHelperTest.php | 48 ++++++++++++- 5 files changed, 155 insertions(+), 47 deletions(-) diff --git a/docs/Behavior/Slugged.md b/docs/Behavior/Slugged.md index c78e73d40..1765d4b01 100644 --- a/docs/Behavior/Slugged.md +++ b/docs/Behavior/Slugged.md @@ -44,6 +44,7 @@ A CakePHP behavior to automatically create and store slugs.
  • 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: returns a slug appropriate to use in a html id
  • +
  • {callable}: Use your custom callable to pass in your slugger method
  • @@ -96,17 +97,17 @@ A CakePHP behavior to automatically create and store slugs. replace - custom replacements as array. + Custom replacements as array. `Set to null` to disable. on - beforeSave or beforeValidate. + `beforeSave` or `beforeMarshal` or `beforeRules`. scope - certain conditions to use as scope. + Certain conditions to use as scope. tidy @@ -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;`). diff --git a/src/Model/Behavior/SluggedBehavior.php b/src/Model/Behavior/SluggedBehavior.php index 2a103179c..3418c37c7 100644 --- a/src/Model/Behavior/SluggedBehavior.php +++ b/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 @@ public function generateSlug($value, EntityInterface $entity = null) { 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']); diff --git a/src/View/Helper/FormatHelper.php b/src/View/Helper/FormatHelper.php index fa7133116..f818a1a1c 100644 --- a/src/View/Helper/FormatHelper.php +++ b/src/View/Helper/FormatHelper.php @@ -7,6 +7,7 @@ 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' => '', 'ok' => '{{content}}' - ] + ], + 'slugger' => null, ]; /** @@ -108,25 +110,6 @@ public function thumbs($value, array $options = [], array $attributes = []) { * @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 @@ public function neighbors(array $neighbors, $field, array $options = []) { $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 @@ public function neighbors(array $neighbors, $field, array $options = []) { $ret = '
    '; 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') . ' ' . __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)) . ' ' . __d('tools', 'prev' . $name $ret .= $this->icon('prev'); } + $ret .= '  '; 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') . ' ' . __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') . ' ' . __d('tools', 'next' . $name); } + $ret .= '
    '; + return $ret; } @@ -349,7 +324,7 @@ protected function _customIcon($icon, array $options = [], array $attributes = [ $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 @@ public function array2table(array $array, array $options = [], array $attributes 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); + } + } diff --git a/tests/TestCase/Model/Behavior/SluggedBehaviorTest.php b/tests/TestCase/Model/Behavior/SluggedBehaviorTest.php index c202d6ff2..db747d6e7 100644 --- a/tests/TestCase/Model/Behavior/SluggedBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/SluggedBehaviorTest.php @@ -6,6 +6,7 @@ use Cake\ORM\Entity; use Cake\ORM\TableRegistry; use Tools\TestSuite\TestCase; +use Tools\Utility\Text; /** * SluggedBehaviorTest @@ -612,7 +613,7 @@ public function testSlugGenerationWithScope() { * * @return void */ - public function testSlugGenerationWithVirualField() { + public function testSlugGenerationWithVirtualField() { $this->articles->removeBehavior('Slugged'); $this->articles->setEntityClass('\App\Model\Entity\SluggedArticle'); $this->articles->addBehavior('Tools.Slugged', [ @@ -630,6 +631,53 @@ public function testSlugGenerationWithVirualField() { $this->assertEquals('Some-Article-12345-dereuromark', $result['slug']); } + /** + * 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 * diff --git a/tests/TestCase/View/Helper/FormatHelperTest.php b/tests/TestCase/View/Helper/FormatHelperTest.php index 328abd21e..7287d1413 100644 --- a/tests/TestCase/View/Helper/FormatHelperTest.php +++ b/tests/TestCase/View/Helper/FormatHelperTest.php @@ -5,6 +5,7 @@ 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 @@ public function testSiteIcon() { } /** - * 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() { @@ -267,6 +296,23 @@ public function testNeighbors() { $this->assertEquals($expected, $result); } + /** + * 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 = '
     prevRecord   nextRecord
    '; + $this->assertEquals($expected, $result); + } + /** * FormatHelperTest::testTab2space() *