Skip to content

Commit

Permalink
Merge pull request #215 from dereuromark/slugging
Browse files Browse the repository at this point in the history
Fix Slugged behavior to allow for non-deprecated and custom slugger.
  • Loading branch information
dereuromark authored Nov 26, 2018
2 parents b47190a + a93e3cf commit 0833d3c
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 47 deletions.
17 changes: 14 additions & 3 deletions docs/Behavior/Slugged.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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;`).
15 changes: 12 additions & 3 deletions src/Model/Behavior/SluggedBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,20 @@ 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.
* - overwrite: has 2 values
* 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
Expand Down Expand Up @@ -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']);
Expand Down
72 changes: 33 additions & 39 deletions src/View/Helper/FormatHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Cake\View\Helper;
use Cake\View\StringTemplate;
use Cake\View\View;
use RuntimeException;

/**
* Format helper with basic html snippets
Expand Down Expand Up @@ -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,
];

/**
Expand Down Expand Up @@ -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']);
Expand All @@ -135,62 +118,54 @@ 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);
}

$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;
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}

}
50 changes: 49 additions & 1 deletion tests/TestCase/Model/Behavior/SluggedBehaviorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Cake\ORM\Entity;
use Cake\ORM\TableRegistry;
use Tools\TestSuite\TestCase;
use Tools\Utility\Text;

/**
* SluggedBehaviorTest
Expand Down Expand Up @@ -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', [
Expand All @@ -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
*
Expand Down
48 changes: 47 additions & 1 deletion tests/TestCase/View/Helper/FormatHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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() {
Expand All @@ -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 = '<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()
*
Expand Down

0 comments on commit 0833d3c

Please sign in to comment.