Sluggable Behavior
This behavior lets your models act as slug-based models, useful for generating Search Engine friendly URLs. Easy to install, and easy to configure.
This behavior will let you create unique slugs for every record added (or updated) to any of your models you attach it to. This is particularly useful to create SEO links out of, for example, a table of articles. Instead of seeing those ugly /articles/view/4 URLs, use the Sluggable Behavior and easily accept URLs such as /articles/view/my-seo-friendly-article. It handles the slug creation, slug collision, and allows you to specify different settings such as the separator to use, maximum width of a slug, among other useful parameters.
Get Sluggable Behavior 1.1.36 (Release Notes & Changelog)
All reports, enhancements and feature feedback should be provided through the project page, and not in comments for this article, so I can keep a closer track. Please do report any issues you find with Sluggable Behavior using its tracker:
Cake Syrup Tracker (Bugs / Features)
If you want to view the source code of the latest version of the Sluggable Behavior you can do so using the SVN browser: sluggable.php
That's it! Whenever a new model is created it will generate the slug on the 'slug' field using the string provided on the 'title' field.
To change those and other settings, you can specify an array of settings right on your $actsAs property. Let's look at all the available settings:
On our previous example, let us say that we want to use the character '_' as the separator and also wish to regenerate slugs when an article is being edited. We do so by specifying the appropiate settings:
You can also select to use a built in character translation table. Using this certain specific characters can be kept from being replaced with the replacement characters, so instead of removing accents, they would get converted to their non-accent equivalent. There are two built in translation tables available: utf-8 and iso-8859-1, and you specify them using the 'translation' setting:
You can instead define your own character translation tables. Refer to the test case included at the end of this article to see example of custom made translation tables.
Once you have your test environment setup and you have installed the Slug behavior as was instructed on previous section, create a file named slug_article_fixture.php in your app/tests/fixtures folder with the contents shown on the following link:
slug_article_fixture.php
Now create a file named sluggable.test.php and place it on your app/tests/cases/behaviors folder with the contents shown on the following link:
sluggable.test.php
Run your test by accessing the URL (replace example.com with your own server address): http://www.example.com/test.php. Once there, click on App Test Cases, and then look for the option behaviors/sluggable.test.php and click it. You will see the results of the test on your browser.
Download, Source Code and Bug Tracking
The latest Sluggable Behavior release is 1.1.36. For those of you who wish to keep up with the latest (not necessarily stable) Sluggable Behavior resides in the SVN repository of a project that includes other CakePHP goodies: Cake Syrup. All future official releases will be posted on this article.Get Sluggable Behavior 1.1.36 (Release Notes & Changelog)
All reports, enhancements and feature feedback should be provided through the project page, and not in comments for this article, so I can keep a closer track. Please do report any issues you find with Sluggable Behavior using its tracker:
Cake Syrup Tracker (Bugs / Features)
If you want to view the source code of the latest version of the Sluggable Behavior you can do so using the SVN browser: sluggable.php
Installation
- Create a file named sluggable.php on your app/models/behaviors folder using the contents provided below.
- For those models that will use this behavior, you need to identify which field holds the string (eg: title) that will be used to generate the slug, and which field will hold the slug (eg: slug)
Usage
The simplest way you can use this behavior is by adding its name to the $actsAs array for your model. For example, let's assume you have a model named Article (which maps to a database table named articles, that has among other fields 'title' and 'slug'). Then edit your app/models/article.php file and add $actsAs as follows:Model Class:
Download code
<?php
class Article extends AppModel {
var $name = 'Article';
var $actsAs = array('Sluggable');
}
?>
That's it! Whenever a new model is created it will generate the slug on the 'slug' field using the string provided on the 'title' field.
To change those and other settings, you can specify an array of settings right on your $actsAs property. Let's look at all the available settings:
- label: name of the field (or array of field names) in the mapped database table that holds the string that will be used to generate the slug. Defaults to 'title'. If you specify more than one field the string will be generated by concatenating the value of the specified fields, separating each value by a space.
- slug: name of the field in the mapped database table that will hold the generated slug. Defaults to 'slug'.
- separator: string to use to separate words in the generated slug. Defaults to '-'.
- length: maximum length (in characters) a slug can take. Defaults to 100.
- overwrite: tells if the slug should be generated only when creating new records (false) or also when editing (true). Defaults to false.
- translation: allows you to specify two methods of built-in character translation (utf-8 and iso-8859-1) to keep specific characters from being considered as invalid, or declare your own translation tables.
On our previous example, let us say that we want to use the character '_' as the separator and also wish to regenerate slugs when an article is being edited. We do so by specifying the appropiate settings:
Model Class:
Download code
<?php
class Article extends AppModel {
var $name = 'Article';
var $actsAs = array('Sluggable' => array('separator' => '_', 'overwrite' => true));
}
?>
You can also select to use a built in character translation table. Using this certain specific characters can be kept from being replaced with the replacement characters, so instead of removing accents, they would get converted to their non-accent equivalent. There are two built in translation tables available: utf-8 and iso-8859-1, and you specify them using the 'translation' setting:
Model Class:
Download code
<?php
class Article extends AppModel {
var $name = 'Article';
var $actsAs = array('Sluggable' => array('translation' => 'utf-8'));
}
?>
You can instead define your own character translation tables. Refer to the test case included at the end of this article to see example of custom made translation tables.
Fetching your records
Since your selected slug field is just another regular field for the mapped table, you can use CakePHP findBy magic methods to fetch a specific record using a slug. On the above example (where Article model holds a field named 'slug') we would do:Controller Class:
Download code
<?php
class ArticlesController extends AppController {
var $name = 'Articles';
function view($slug) {
$article = $this->Article->findBySlug($slug);
$this->set('article', $article);
}
}
?>
Important
If you are saving a model that acts as a Sluggable and use the $fieldList argument to specify which fields should be saved, then you'll need to add the name of your slug field to the list (otherwise the slug will never be saved on this cases.)Behavior
Behavior Class:
Download code
<?php
/* SVN FILE: $Id: sluggable.php 36 2007-11-26 15:10:14Z mgiglesias $ */
/**
* Sluggable Behavior class file.
*
* @filesource
* @author Mariano Iglesias
* @link http://cake-syrup.sourceforge.net/ingredients/sluggable-behavior/
* @version $Revision: 36 $
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @package app
* @subpackage app.models.behaviors
*/
/**
* Model behavior to support generation of slugs for models.
*
* @package app
* @subpackage app.models.behaviors
*/
class SluggableBehavior extends ModelBehavior
{
/**
* Contain settings indexed by model name.
*
* @var array
* @access private
*/
var $__settings = array();
/**
* Initiate behavior for the model using specified settings. Available settings:
*
* - label: (array | string, optional) set to the field name that contains the
* string from where to generate the slug, or a set of field names to
* concatenate for generating the slug. DEFAULTS TO: title
*
* - slug: (string, optional) name of the field name that holds generated slugs.
* DEFAULTS TO: slug
*
* - separator: (string, optional) separator character / string to use for replacing
* non alphabetic characters in generated slug. DEFAULTS TO: -
*
* - length: (integer, optional) maximum length the generated slug can have.
* DEFAULTS TO: 100
*
* - overwrite: (boolean, optional) set to true if slugs should be re-generated when
* updating an existing record. DEFAULTS TO: false
*
* @param object $Model Model using the behaviour
* @param array $settings Settings to override for model.
* @access public
*/
function setup(&$Model, $settings = array())
{
$default = array('label' => array('title'), 'slug' => 'slug', 'separator' => '-', 'length' => 100, 'overwrite' => false, 'translation' => null);
if (!isset($this->__settings[$Model->alias]))
{
$this->__settings[$Model->alias] = $default;
}
$this->__settings[$Model->alias] = am($this->__settings[$Model->alias], ife(is_array($settings), $settings, array()));
}
/**
* Run before a model is saved, used to set up slug for model.
*
* @param object $Model Model about to be saved.
* @return boolean true if save should proceed, false otherwise
* @access public
*/
function beforeSave(&$Model)
{
$return = parent::beforeSave($Model);
// Make label fields an array
if (!is_array($this->__settings[$Model->alias]['label']))
{
$this->__settings[$Model->alias]['label'] = array($this->__settings[$Model->alias]['label']);
}
// Make sure all label fields are available
foreach($this->__settings[$Model->alias]['label'] as $field)
{
if (!$Model->hasField($field))
{
return $return;
}
}
// See if we should be generating a slug
if ($Model->hasField($this->__settings[$Model->alias]['slug']) && ($this->__settings[$Model->alias]['overwrite'] || empty($Model->id)))
{
// Build label out of data in label fields, if available, or using a default slug otherwise
$label = '';
foreach($this->__settings[$Model->alias]['label'] as $field)
{
if (!empty($Model->data[$Model->alias][$field]))
{
$label .= ife(!empty($label), ' ', '') . $Model->data[$Model->alias][$field];
}
}
// Keep on going only if we've got something to slug
if (!empty($label))
{
// Get the slug
$slug = $this->__slug($label, $this->__settings[$Model->alias]);
// Look for slugs that start with the same slug we've just generated
$conditions = array($Model->alias . '.' . $this->__settings[$Model->alias]['slug'] => 'LIKE ' . $slug . '%');
if (!empty($Model->id))
{
$conditions[$Model->alias . '.' . $Model->primaryKey] = '!= ' . $Model->id;
}
$result = $Model->find('all', array('conditions' => $conditions, 'fields' => array($Model->primaryKey, $this->__settings[$Model->alias]['slug']), 'recursive' => -1));
$sameUrls = null;
if (!empty($result))
{
$sameUrls = Set::extract($result, '{n}.' . $Model->alias . '.' . $this->__settings[$Model->alias]['slug']);
}
// If we have collissions
if (!empty($sameUrls))
{
$begginingSlug = $slug;
$index = 1;
// Attach an ending incremental number until we find a free slug
while($index > 0)
{
if (!in_array($begginingSlug . $this->__settings[$Model->alias]['separator'] . $index, $sameUrls))
{
$slug = $begginingSlug . $this->__settings[$Model->alias]['separator'] . $index;
$index = -1;
}
$index++;
}
}
// Now set the slug as part of the model data to be saved, making sure that
// we are on the white list of fields to be saved
if (!empty($Model->whitelist) && !in_array($this->__settings[$Model->alias]['slug'], $Model->whitelist))
{
$Model->whitelist[] = $this->__settings[$Model->alias]['slug'];
}
$Model->data[$Model->alias][$this->__settings[$Model->alias]['slug']] = $slug;
}
}
return $return;
}
/**
* Generate a slug for the given string using specified settings.
*
* @param string $string String from where to generate slug
* @param array $settings Settings to use (looks for 'separator' and 'length')
* @return string Slug for given string
* @access private
*/
function __slug($string, $settings)
{
if (!empty($settings['translation']) && is_array($settings['translation']))
{
// Run user-defined translation tables
if (count($settings['translation']) >= 2 && count($settings['translation']) % 2 == 0)
{
for($i=0, $limiti=count($settings['translation']); $i < $limiti; $i+=2)
{
$from = $settings['translation'][$i];
$to = $settings['translation'][$i + 1];
if (is_string($from) && is_string($to))
{
$string = strtr($string, $from, $to);
}
else
{
$string = r($from, $to, $string);
}
}
}
else if (count($settings['translation']) == 1)
{
$string = strtr($string, $settings['translation'][0]);
}
$string = low($string);
}
else if (!empty($settings['translation']) && is_string($settings['translation']) && in_array(low($settings['translation']), array('utf-8', 'iso-8859-1')))
{
// Run pre-defined translation tables
$translations = array(
'iso-8859-1' => array(
chr(128).chr(131).chr(138).chr(142).chr(154).chr(158)
.chr(159).chr(162).chr(165).chr(181).chr(192).chr(193).chr(194)
.chr(195).chr(196).chr(197).chr(199).chr(200).chr(201).chr(202)
.chr(203).chr(204).chr(205).chr(206).chr(207).chr(209).chr(210)
.chr(211).chr(212).chr(213).chr(214).chr(216).chr(217).chr(218)
.chr(219).chr(220).chr(221).chr(224).chr(225).chr(226).chr(227)
.chr(228).chr(229).chr(231).chr(232).chr(233).chr(234).chr(235)
.chr(236).chr(237).chr(238).chr(239).chr(241).chr(242).chr(243)
.chr(244).chr(245).chr(246).chr(248).chr(249).chr(250).chr(251)
.chr(252).chr(253).chr(255),
'EfSZsz' . 'YcYuAAA' . 'AAACEEE' . 'EIIIINO' . 'OOOOOUU' . 'UUYaaaa' . 'aaceeee' . 'iiiinoo' . 'oooouuu' . 'uyy',
array(chr(140), chr(156), chr(198), chr(208), chr(222), chr(223), chr(230), chr(240), chr(254)),
array('OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th')
),
'utf-8' => array(
array(
// Decompositions for Latin-1 Supplement
chr(195).chr(128) => 'A', chr(195).chr(129) => 'A',
chr(195).chr(130) => 'A', chr(195).chr(131) => 'A',
chr(195).chr(132) => 'A', chr(195).chr(133) => 'A',
chr(195).chr(135) => 'C', chr(195).chr(136) => 'E',
chr(195).chr(137) => 'E', chr(195).chr(138) => 'E',
chr(195).chr(139) => 'E', chr(195).chr(140) => 'I',
chr(195).chr(141) => 'I', chr(195).chr(142) => 'I',
chr(195).chr(143) => 'I', chr(195).chr(145) => 'N',
chr(195).chr(146) => 'O', chr(195).chr(147) => 'O',
chr(195).chr(148) => 'O', chr(195).chr(149) => 'O',
chr(195).chr(150) => 'O', chr(195).chr(153) => 'U',
chr(195).chr(154) => 'U', chr(195).chr(155) => 'U',
chr(195).chr(156) => 'U', chr(195).chr(157) => 'Y',
chr(195).chr(159) => 's', chr(195).chr(160) => 'a',
chr(195).chr(161) => 'a', chr(195).chr(162) => 'a',
chr(195).chr(163) => 'a', chr(195).chr(164) => 'a',
chr(195).chr(165) => 'a', chr(195).chr(167) => 'c',
chr(195).chr(168) => 'e', chr(195).chr(169) => 'e',
chr(195).chr(170) => 'e', chr(195).chr(171) => 'e',
chr(195).chr(172) => 'i', chr(195).chr(173) => 'i',
chr(195).chr(174) => 'i', chr(195).chr(175) => 'i',
chr(195).chr(177) => 'n', chr(195).chr(178) => 'o',
chr(195).chr(179) => 'o', chr(195).chr(180) => 'o',
chr(195).chr(181) => 'o', chr(195).chr(182) => 'o',
chr(195).chr(182) => 'o', chr(195).chr(185) => 'u',
chr(195).chr(186) => 'u', chr(195).chr(187) => 'u',
chr(195).chr(188) => 'u', chr(195).chr(189) => 'y',
chr(195).chr(191) => 'y',
// Decompositions for Latin Extended-A
chr(196).chr(128) => 'A', chr(196).chr(129) => 'a',
chr(196).chr(130) => 'A', chr(196).chr(131) => 'a',
chr(196).chr(132) => 'A', chr(196).chr(133) => 'a',
chr(196).chr(134) => 'C', chr(196).chr(135) => 'c',
chr(196).chr(136) => 'C', chr(196).chr(137) => 'c',
chr(196).chr(138) => 'C', chr(196).chr(139) => 'c',
chr(196).chr(140) => 'C', chr(196).chr(141) => 'c',
chr(196).chr(142) => 'D', chr(196).chr(143) => 'd',
chr(196).chr(144) => 'D', chr(196).chr(145) => 'd',
chr(196).chr(146) => 'E', chr(196).chr(147) => 'e',
chr(196).chr(148) => 'E', chr(196).chr(149) => 'e',
chr(196).chr(150) => 'E', chr(196).chr(151) => 'e',
chr(196).chr(152) => 'E', chr(196).chr(153) => 'e',
chr(196).chr(154) => 'E', chr(196).chr(155) => 'e',
chr(196).chr(156) => 'G', chr(196).chr(157) => 'g',
chr(196).chr(158) => 'G', chr(196).chr(159) => 'g',
chr(196).chr(160) => 'G', chr(196).chr(161) => 'g',
chr(196).chr(162) => 'G', chr(196).chr(163) => 'g',
chr(196).chr(164) => 'H', chr(196).chr(165) => 'h',
chr(196).chr(166) => 'H', chr(196).chr(167) => 'h',
chr(196).chr(168) => 'I', chr(196).chr(169) => 'i',
chr(196).chr(170) => 'I', chr(196).chr(171) => 'i',
chr(196).chr(172) => 'I', chr(196).chr(173) => 'i',
chr(196).chr(174) => 'I', chr(196).chr(175) => 'i',
chr(196).chr(176) => 'I', chr(196).chr(177) => 'i',
chr(196).chr(178) => 'IJ',chr(196).chr(179) => 'ij',
chr(196).chr(180) => 'J', chr(196).chr(181) => 'j',
chr(196).chr(182) => 'K', chr(196).chr(183) => 'k',
chr(196).chr(184) => 'k', chr(196).chr(185) => 'L',
chr(196).chr(186) => 'l', chr(196).chr(187) => 'L',
chr(196).chr(188) => 'l', chr(196).chr(189) => 'L',
chr(196).chr(190) => 'l', chr(196).chr(191) => 'L',
chr(197).chr(128) => 'l', chr(197).chr(129) => 'L',
chr(197).chr(130) => 'l', chr(197).chr(131) => 'N',
chr(197).chr(132) => 'n', chr(197).chr(133) => 'N',
chr(197).chr(134) => 'n', chr(197).chr(135) => 'N',
chr(197).chr(136) => 'n', chr(197).chr(137) => 'N',
chr(197).chr(138) => 'n', chr(197).chr(139) => 'N',
chr(197).chr(140) => 'O', chr(197).chr(141) => 'o',
chr(197).chr(142) => 'O', chr(197).chr(143) => 'o',
chr(197).chr(144) => 'O', chr(197).chr(145) => 'o',
chr(197).chr(146) => 'OE',chr(197).chr(147) => 'oe',
chr(197).chr(148) => 'R',chr(197).chr(149) => 'r',
chr(197).chr(150) => 'R',chr(197).chr(151) => 'r',
chr(197).chr(152) => 'R',chr(197).chr(153) => 'r',
chr(197).chr(154) => 'S',chr(197).chr(155) => 's',
chr(197).chr(156) => 'S',chr(197).chr(157) => 's',
chr(197).chr(158) => 'S',chr(197).chr(159) => 's',
chr(197).chr(160) => 'S', chr(197).chr(161) => 's',
chr(197).chr(162) => 'T', chr(197).chr(163) => 't',
chr(197).chr(164) => 'T', chr(197).chr(165) => 't',
chr(197).chr(166) => 'T', chr(197).chr(167) => 't',
chr(197).chr(168) => 'U', chr(197).chr(169) => 'u',
chr(197).chr(170) => 'U', chr(197).chr(171) => 'u',
chr(197).chr(172) => 'U', chr(197).chr(173) => 'u',
chr(197).chr(174) => 'U', chr(197).chr(175) => 'u',
chr(197).chr(176) => 'U', chr(197).chr(177) => 'u',
chr(197).chr(178) => 'U', chr(197).chr(179) => 'u',
chr(197).chr(180) => 'W', chr(197).chr(181) => 'w',
chr(197).chr(182) => 'Y', chr(197).chr(183) => 'y',
chr(197).chr(184) => 'Y', chr(197).chr(185) => 'Z',
chr(197).chr(186) => 'z', chr(197).chr(187) => 'Z',
chr(197).chr(188) => 'z', chr(197).chr(189) => 'Z',
chr(197).chr(190) => 'z', chr(197).chr(191) => 's',
// Euro Sign
chr(226).chr(130).chr(172) => 'E'
)
)
);
return $this->__slug($string, am($settings, array('translation' => $translations[$settings['translation']])));
}
$string = low($string);
$string = preg_replace('/[^a-z0-9_]/i', $settings['separator'], $string);
$string = preg_replace('/' . preg_quote($settings['separator']) . '[' . preg_quote($settings['separator']) . ']*/', $settings['separator'], $string);
if (strlen($string) > $settings['length'])
{
$string = substr($string, 0, $settings['length']);
}
$string = preg_replace('/' . preg_quote($settings['separator']) . '$/', '', $string);
$string = preg_replace('/^' . preg_quote($settings['separator']) . '/', '', $string);
return $string;
}
}
?>
Test Case
First of all, follow instructions on how to set up your CakePHP test suite by reading the section Installation on the article Testing Models with CakePHP 1.2 test suite.Once you have your test environment setup and you have installed the Slug behavior as was instructed on previous section, create a file named slug_article_fixture.php in your app/tests/fixtures folder with the contents shown on the following link:
slug_article_fixture.php
Now create a file named sluggable.test.php and place it on your app/tests/cases/behaviors folder with the contents shown on the following link:
sluggable.test.php
Run your test by accessing the URL (replace example.com with your own server address): http://www.example.com/test.php. Once there, click on App Test Cases, and then look for the option behaviors/sluggable.test.php and click it. You will see the results of the test on your browser.
Comments
Question
1 Multiple field label
Comment
2 Multiple field label
http://bin.cakephp.org/view/351367207
With that version label can either be a string (which then would make the Behavior act as it does now) or an array of field names, which makes the slug be generated from the concatenation of all specified fields. At least one field must be completed before the save.
I haven't tested it, though.
Comment
3 Multiple field label
That seems to work perfectly. No bugs. Thanks.
Comment
4 Unique Slugs
Regardless, of whether it ever did that, now it isn't for me. I'm using the updated version of the code linked to by Mariano (http://bin.cakephp.org/view/351367207).
Comment
5 Unique Slugs
PHP Snippet:
<?php
$conditions = array($model->name . '.' . $this->settings[$model->name]['slug'] => 'LIKE ' . $slug . '%');
if (!empty($model->{$model->primaryKey})) {
$conditions[$model->name . '.' . $model->primaryKey] = '!= ' . $model->{$model->primaryKey};
}
?>
That's where I'm building the conditions to find any existing slugs. I'm building a test case for this behavior though.
Comment
6 Unique Slugs
PHP Snippet:
<?php$slugs = Set::extract($result, '{n}.' . $this->settings[$model->name]['slug']);
?>
to:
PHP Snippet:
<?php$slugs = Set::extract($result, '{n}.' $model->name . '.' . $this->settings[$model->name]['slug']);
?>
Comment
7 Unique Slugs
Comment
8 Unique Slugs
Thank you for this lovely post.
Is their a way it can work with cake's latest 1.1.x build?
I think behaviors are available in that as well?
Can you please let me know if this will work safely in the 1.1.x build as well?
Thanks,
Mandy.
http://mandysingh.blogspot.com
Comment
9 Unique Slugs
Question
10 Translations
Just what i've been looking for, thanks so much for sharing! I was doing it myself in the controller :( this is MUCH nicer, but i was thinking, would it be possible to get this to work with the translation behavior?
So you can get multi language SEO urls?
Alex
Comment
11 Editing..
I understand that when editing the url stays the same for google / bookmarks etc. But what if you made a spelling error for example, it would be there forever, what would be the best way to edit it?
Maybe it would be best if it has the slug and in the end the id? I think wordpress uses this also like: this-is-the-slug-id
Then as long as the id is changing the slug would not matter anymore
Bug
12 saveField
so change line 57 to :
if ($model->hasField($this->settings[$model->name]['slug']) && count($model->data[$model->name]) > 1 && ($this->settings[$model->name]['overwrite'] || empty($model->{$model->primaryKey}))) {
and it sorts it out.
Comment
13 Unicode slugs
$string = preg_replace('/[^\p{Ll}0-9_]/u', $settings['separator'], $string);
Bug
14 help..
It does not work :(
Één êên äèààëùúû produces only ----- characters. it would probably be better if they get converted to the characters without special signs like: é -> e
Comment
15 try this
$string = preg_replace('/[^p{L}0-9_]/u', $settings['separator'], $string);
taken from : http://www.regular-expressions.info/unicode.html
I'm no regex guru so YUMV, if the above didn't work but you get it working, please post here.
Comment
16 thanks
I see that at this article: http://www.thinkingphp.org/2006/10/19/title-to-url-slug-conversion/ They are converting the characters "manual"
<?php
$string = 'Één êên äèààëùúû';
echo $string . '<br />';
$string = preg_replace('/[^p{L}0-9_]/u', $settings['separator'], $string);
echo $string . '<br />';
?>
Question
17 Non english characters converting
Comment
18 Non english characters converting
Comment
19 Russian symbols UTF8 translation table
array(
chr(208).chr(129) => 'YO',
chr(208).chr(132) => 'E',
chr(208).chr(134) => 'I',
chr(208).chr(135) => 'YI',
chr(208).chr(144) => 'A',
chr(208).chr(145) => 'B',
chr(208).chr(146) => 'V',
chr(208).chr(147) => 'G',
chr(208).chr(148) => 'D',
chr(208).chr(149) => 'E',
chr(208).chr(150) => 'ZH',
chr(208).chr(151) => 'Z',
chr(208).chr(152) => 'I',
chr(208).chr(153) => 'Y',
chr(208).chr(154) => 'K',
chr(208).chr(155) => 'L',
chr(208).chr(156) => 'M',
chr(208).chr(157) => 'N',
chr(208).chr(158) => 'O',
chr(208).chr(159) => 'P',
chr(208).chr(160) => 'R',
chr(208).chr(161) => 'S',
chr(208).chr(162) => 'T',
chr(208).chr(163) => 'U',
chr(208).chr(164) => 'F',
chr(208).chr(165) => 'H',
chr(208).chr(166) => 'TS',
chr(208).chr(167) => 'CH',
chr(208).chr(168) => 'SH',
chr(208).chr(169) => 'SCH',
chr(208).chr(170) => '', // this letter usually translates to apostrophe sign
chr(208).chr(171) => 'YI',
chr(208).chr(172) => '',
chr(208).chr(173) => 'E',
chr(208).chr(174) => 'YU',
chr(208).chr(175) => 'YA',
chr(208).chr(176) => 'a',
chr(208).chr(177) => 'b',
chr(208).chr(178) => 'v',
chr(208).chr(179) => 'g',
chr(208).chr(180) => 'd',
chr(208).chr(181) => 'e',
chr(208).chr(182) => 'zh',
chr(208).chr(183) => 'z',
chr(208).chr(184) => 'i',
chr(208).chr(185) => 'y',
chr(208).chr(186) => 'k',
chr(208).chr(187) => 'l',
chr(208).chr(188) => 'm',
chr(208).chr(189) => 'n',
chr(208).chr(190) => 'o',
chr(208).chr(191) => 'p',
chr(209).chr(128) => 'r',
chr(209).chr(129) => 's',
chr(209).chr(130) => 't',
chr(209).chr(131) => 'u',
chr(209).chr(132) => 'f',
chr(209).chr(133) => 'h',
chr(209).chr(134) => 'ts',
chr(209).chr(135) => 'ch',
chr(209).chr(136) => 'sh',
chr(209).chr(137) => 'sch',
chr(209).chr(138) => '', // this letter usually translates to apostrophe sign
chr(209).chr(139) => 'yi',
chr(209).chr(140) => '',
chr(209).chr(141) => 'e',
chr(209).chr(142) => 'yu',
chr(209).chr(143) => 'ya',
chr(209).chr(145) => 'yo',
chr(209).chr(148) => 'e',
chr(209).chr(150) => 'i',
chr(209).chr(151) => 'yi',
chr(210).chr(144) => 'G',
chr(210).chr(145) => 'g',
chr(226).chr(132).chr(150) => '#', //russian NUMBER (No.) sign
Comment
20 Russian symbols UTF8 translation table
So use the latest SVN version, since I won't repackage until preparing a new release.
Comment
21 Add a custom prefix and suffix from controller
Something like this
Model->slug(array('prefix'=>$custom_string));
Model->save();
Question
22 Does It work on pre alpha
As soon as I put it in the behaviors folder I get this:
Illegal offset type in isset or empty [CORE/app/models/behaviors/sluggable.php, line 62]
I may of course be doing something stupid.
Comment
23 Does It work on pre alpha
PHP Snippet:
<?php$Model->alias = $Model->name;
?>
right before the following line:
PHP Snippet:
<?phpif (!isset($this->__settings[$Model->alias]))
?>
However I strongly recommend that you update your CakePHP 1.2 core revision, taking care of the changes that have been implemented though.
Comment
24 excellent
Question
25 upgrade problem
Comment
26 upgrade problem
Comment
27 okies
Comment
28 Allow user submitted slugs when submitting a new record
Component Class:
<?php
if ($Model->hasField($this->__settings[$Model->alias]['slug']) && ($this->__settings[$Model->alias]['overwrite'] || empty($Model->data[$Model->alias][$this->__settings[$Model->alias]['slug']])))
?>
Comment
29 Works great
Comment
30 Accents
$string = strtr($string, "ÀÁÂÃÄÅàáâãäåÒÓÔÕÖØòóôõöøÈÉÊËèéêëÇçÌÍÎÏìíîïÙÚÛÜùúûüÿÑñ", "aaaaaaaaaaaaooooooooooooeeeeeeeecciiiiiiiiuuuuuuuuynn");It remove accents and so "new résumé" become "new_resume".
Thanks for your behavior !
Question
31 Error on test
Any ideas why is this happening? I am using CakePHP 1.2.x RC1
Comment
32 Error on test
sluggable.php
and
sluggable.test.php
Question
33 Wont work no errors
The tests show no errors, but when I try it out in my application the slugs aren't saved/created. Seeing as there are no errors returned, I'm a bit clueless as to where I should look next...
I'm not doing anything fancy, it's a simple baked model/view/controller combination for newsposts.
Any ideas?
Comment
34 Fixed
Bug
35 Nice
But with latest Cake release, the duplicate value check isn't working, because of the new conditions-syntax.
Source and possible fixes:
Model Class:
<?php
$slug = $this->__slug($label, $this->__settings[$Model->alias]);
// Look for slugs that start with the same slug we've just generated
// Bug 1
// The following line is not working any more:
// $conditions = array($Model->alias . '.' . $this->__settings[$Model->alias]['slug'] => 'LIKE ' . $slug . '%');
// Fix for Bug1:
$conditions = array($Model->alias . '.' . $this->__settings[$Model->alias]['slug'] => $slug);
if (!empty($Model->id))
{
// Bug 2
// The following line is not working any more:
// $conditions[$Model->alias . '.' . $Model->primaryKey] = '!= ' . $Model->id;
// Fix for Bug 2:
$conditions['not'] = array(
$Model->alias . '.' . $Model->primaryKey =>
$Model->id
);
}
?>
Bug
36 Bug in current 1.2rc
But with latest Cake release, the duplicate value check isn't working, because of the new conditions-syntax.
Source and possible fixes: (starting around line 117)
Model Class:
<?php
$slug = $this->__slug($label, $this->__settings[$Model->alias]);
// Look for slugs that start with the same slug we've just generated
// Bug 1
// The following line is not working any more:
// $conditions = array($Model->alias . '.' . $this->__settings[$Model->alias]['slug'] => 'LIKE ' . $slug . '%');
// Fix for Bug1:
$conditions = array($Model->alias . '.' . $this->__settings[$Model->alias]['slug'] => $slug);
if (!empty($Model->id))
{
// Bug 2
// The following line is not working any more:
// $conditions[$Model->alias . '.' . $Model->primaryKey] = '!= ' . $Model->id;
// Fix for Bug 2:
$conditions['not'] = array(
$Model->alias . '.' . $Model->primaryKey =>
$Model->id
);
}
?>
Comment
37 tar.gz
/my2cents
Comment
38 duplicates in RC2
However, I think your fix for bug 1 should be this, otherwise you still end up with duplicates, but with -1 on the end of them all:
Model Class:
<?php$conditions = array($Model->alias . '.' . $this->__settings[$Model->alias]['slug'].' LIKE' => $slug.'%'); ?>
so the complete changes would be
Model Class:
<?php
$slug = $this->__slug($label, $this->__settings[$Model->alias]);
// Look for slugs that start with the same slug we've just generated
// Bug 1
// The following line is not working any more:
// $conditions = array($Model->alias . '.' . $this->__settings[$Model->alias]['slug'] => 'LIKE ' . $slug . '%');
// Fix for Bug1:
$conditions = array($Model->alias . '.' . $this->__settings[$Model->alias]['slug'].' LIKE' => $slug.'%');
if (!empty($Model->id))
{
// Bug 2
// The following line is not working any more:
// $conditions[$Model->alias . '.' . $Model->primaryKey] = '!= ' . $Model->id;
// Fix for Bug 2:
$conditions['not'] = array(
$Model->alias . '.' . $Model->primaryKey =>
$Model->id
);
}
?>
Question
39 Cross model slugnation
My /users/edit form updates the editors information with saveAll, and the slug is not regenerated. What can I do?
Thanks,
Aidan
Comment
40 Multi-language
Model Class:
<?php
<?php
/* SVN FILE: $Id: sluggable.php 36 2007-11-26 15:10:14Z mgiglesias $ */
/**
* Sluggable Behavior class file.
*
* @filesource
* @author Mariano Iglesias
* @link http://cake-syrup.sourceforge.net/ingredients/sluggable-behavior/
* @version $Revision: 36 $
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @package app
* @subpackage app.models.behaviors
*/
/**
* Model behavior to support generation of slugs for models.
*
* @package app
* @subpackage app.models.behaviors
*/
class SluggableBehavior extends ModelBehavior
{
/**
* Contain settings indexed by model name.
*
* @var array
* @access private
*/
var $__settings = array();
/**
* Initiate behavior for the model using specified settings. Available settings:
*
* - label: (array | string, optional) set to the field name that contains the
* string from where to generate the slug, or a set of field names to
* concatenate for generating the slug. DEFAULTS TO: title
*
* - slug: (string, optional) name of the field name that holds generated slugs.
* DEFAULTS TO: slug
*
* - separator: (string, optional) separator character / string to use for replacing
* non alphabetic characters in generated slug. DEFAULTS TO: -
*
* - length: (integer, optional) maximum length the generated slug can have.
* DEFAULTS TO: 100
*
* - overwrite: (boolean, optional) set to true if slugs should be re-generated when
* updating an existing record. DEFAULTS TO: false
*
* @param object $Model Model using the behaviour
* @param array $settings Settings to override for model.
* @access public
*/
function setup(&$Model, $settings = array())
{
$default = array('label' => array('slug'=>array('title')),'slug' => 'slug', 'separator' => '-', 'length' => 100, 'overwrite' => false, 'translation' => null);
if (!isset($this->__settings[$Model->alias]))
{
$this->__settings[$Model->alias] = $default;
}
$this->__settings[$Model->alias] = am($this->__settings[$Model->alias], ife(is_array($settings), $settings, array()));
}
/**
* Run before a model is saved, used to set up slug for model.
*
* @param object $Model Model about to be saved.
* @return boolean true if save should proceed, false otherwise
* @access public
*/
function beforeSave(&$Model)
{
$return = parent::beforeSave($Model);
// Make label fields an array
if (!is_array($this->__settings[$Model->alias]['label']))
{
$this->__settings[$Model->alias]['label'] = array(
$this->__settings[$Model->alias]['slug']=>array($this->__settings[$Model->alias]['label'])
);
}
// Make sure all label fields are available
foreach($this->__settings[$Model->alias]['label'] as $slugfield => $fields)
{
if(!is_array($fields)) {
$fields = array($fields);
$this->__settings[$Model->alias]['label'][$slugfield] = $fields;
}
foreach($fields as $field) if (!$Model->hasField($field)) return $return;
if (!$Model->hasField($slugfield)) return $return;
}
// See if we should be generating a slug
if ($this->__settings[$Model->alias]['overwrite'] || empty($Model->id))
{
foreach($this->__settings[$Model->alias]['label'] as $slugfield => $fields)
{
$this->__settings[$Model->alias]['slug'] = $slugfield;
// Build label out of data in label fields, if available, or using a default slug otherwise
$label = '';
foreach($fields as $field)
{
if (!empty($Model->data[$Model->alias][$field]))
{
$label .= ife(!empty($label), ' ', '') . $Model->data[$Model->alias][$field];
}
}
// Keep on going only if we've got something to slug
if (!empty($label))
{
// Get the slug
$slug = $this->__slug($label, $this->__settings[$Model->alias]);
// Look for slugs that start with the same slug we've just generated
$conditions = array($Model->alias . '.' . $this->__settings[$Model->alias]['slug'] => 'LIKE ' . $slug . '%');
if (!empty($Model->id))
{
$conditions[$Model->alias . '.' . $Model->primaryKey] = '!= ' . $Model->id;
}
$result = $Model->find('all', array('conditions' => $conditions, 'fields' => array($Model->primaryKey, $this->__settings[$Model->alias]['slug']), 'recursive' => -1));
$sameUrls = null;
if (!empty($result))
{
$sameUrls = Set::extract($result, '{n}.' . $Model->alias . '.' . $this->__settings[$Model->alias]['slug']);
}
// If we have collissions
if (!empty($sameUrls))
{
$begginingSlug = $slug;
$index = 1;
// Attach an ending incremental number until we find a free slug
while($index > 0)
{
if (!in_array($begginingSlug . $this->__settings[$Model->alias]['separator'] . $index, $sameUrls))
{
$slug = $begginingSlug . $this->__settings[$Model->alias]['separator'] . $index;
$index = -1;
}
$index++;
}
}
// Now set the slug as part of the model data to be saved, making sure that
// we are on the white list of fields to be saved
if (!empty($Model->whitelist) && !in_array($this->__settings[$Model->alias]['slug'], $Model->whitelist))
{
$Model->whitelist[] = $this->__settings[$Model->alias]['slug'];
}
$Model->data[$Model->alias][$this->__settings[$Model->alias]['slug']] = $slug;
}
}
//debug($Model->data);
}
return $return;
}
/**
* Generate a slug for the given string using specified settings.
*
* @param string $string String from where to generate slug
* @param array $settings Settings to use (looks for 'separator' and 'length')
* @return string Slug for given string
* @access private
*/
function __slug($string, $settings)
{
if (!empty($settings['translation']) && is_array($settings['translation']))
{
// Run user-defined translation tables
if (count($settings['translation']) >= 2 && count($settings['translation']) % 2 == 0)
{
for($i=0, $limiti=count($settings['translation']); $i < $limiti; $i+=2)
{
$from = $settings['translation'][$i];
$to = $settings['translation'][$i + 1];
if (is_string($from) && is_string($to))
{
$string = strtr($string, $from, $to);
}
else
{
$string = r($from, $to, $string);
}
}
}
else if (count($settings['translation']) == 1)
{
$string = strtr($string, $settings['translation'][0]);
}
$string = low($string);
}
else if (!empty($settings['translation']) && is_string($settings['translation']) && in_array(low($settings['translation']), array('utf-8', 'iso-8859-1')))
{
// Run pre-defined translation tables
$translations = array(
'iso-8859-1' => array(
chr(128).chr(131).chr(138).chr(142).chr(154).chr(158)
.chr(159).chr(162).chr(165).chr(181).chr(192).chr(193).chr(194)
.chr(195).chr(196).chr(197).chr(199).chr(200).chr(201).chr(202)
.chr(203).chr(204).chr(205).chr(206).chr(207).chr(209).chr(210)
.chr(211).chr(212).chr(213).chr(214).chr(216).chr(217).chr(218)
.chr(219).chr(220).chr(221).chr(224).chr(225).chr(226).chr(227)
.chr(228).chr(229).chr(231).chr(232).chr(233).chr(234).chr(235)
.chr(236).chr(237).chr(238).chr(239).chr(241).chr(242).chr(243)
.chr(244).chr(245).chr(246).chr(248).chr(249).chr(250).chr(251)
.chr(252).chr(253).chr(255),
'EfSZsz' . 'YcYuAAA' . 'AAACEEE' . 'EIIIINO' . 'OOOOOUU' . 'UUYaaaa' . 'aaceeee' . 'iiiinoo' . 'oooouuu' . 'uyy',
array(chr(140), chr(156), chr(198), chr(208), chr(222), chr(223), chr(230), chr(240), chr(254)),
array('OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th')
),
'utf-8' => array(
array(
// Decompositions for Latin-1 Supplement
chr(195).chr(128) => 'A', chr(195).chr(129) => 'A',
chr(195).chr(130) => 'A', chr(195).chr(131) => 'A',
chr(195).chr(132) => 'A', chr(195).chr(133) => 'A',
chr(195).chr(135) => 'C', chr(195).chr(136) => 'E',
chr(195).chr(137) => 'E', chr(195).chr(138) => 'E',
chr(195).chr(139) => 'E', chr(195).chr(140) => 'I',
chr(195).chr(141) => 'I', chr(195).chr(142) => 'I',
chr(195).chr(143) => 'I', chr(195).chr(145) => 'N',
chr(195).chr(146) => 'O', chr(195).chr(147) => 'O',
chr(195).chr(148) => 'O', chr(195).chr(149) => 'O',
chr(195).chr(150) => 'O', chr(195).chr(153) => 'U',
chr(195).chr(154) => 'U', chr(195).chr(155) => 'U',
chr(195).chr(156) => 'U', chr(195).chr(157) => 'Y',
chr(195).chr(159) => 's', chr(195).chr(160) => 'a',
chr(195).chr(161) => 'a', chr(195).chr(162) => 'a',
chr(195).chr(163) => 'a', chr(195).chr(164) => 'a',
chr(195).chr(165) => 'a', chr(195).chr(167) => 'c',
chr(195).chr(168) => 'e', chr(195).chr(169) => 'e',
chr(195).chr(170) => 'e', chr(195).chr(171) => 'e',
chr(195).chr(172) => 'i', chr(195).chr(173) => 'i',
chr(195).chr(174) => 'i', chr(195).chr(175) => 'i',
chr(195).chr(177) => 'n', chr(195).chr(178) => 'o',
chr(195).chr(179) => 'o', chr(195).chr(180) => 'o',
chr(195).chr(181) => 'o', chr(195).chr(182) => 'o',
chr(195).chr(182) => 'o', chr(195).chr(185) => 'u',
chr(195).chr(186) => 'u', chr(195).chr(187) => 'u',
chr(195).chr(188) => 'u', chr(195).chr(189) => 'y',
chr(195).chr(191) => 'y',
// Decompositions for Latin Extended-A
chr(196).chr(128) => 'A', chr(196).chr(129) => 'a',
chr(196).chr(130) => 'A', chr(196).chr(131) => 'a',
chr(196).chr(132) => 'A', chr(196).chr(133) => 'a',
chr(196).chr(134) => 'C', chr(196).chr(135) => 'c',
chr(196).chr(136) => 'C', chr(196).chr(137) => 'c',
chr(196).chr(138) => 'C', chr(196).chr(139) => 'c',
chr(196).chr(140) => 'C', chr(196).chr(141) => 'c',
chr(196).chr(142) => 'D', chr(196).chr(143) => 'd',
chr(196).chr(144) => 'D', chr(196).chr(145) => 'd',
chr(196).chr(146) => 'E', chr(196).chr(147) => 'e',
chr(196).chr(148) => 'E', chr(196).chr(149) => 'e',
chr(196).chr(150) => 'E', chr(196).chr(151) => 'e',
chr(196).chr(152) => 'E', chr(196).chr(153) => 'e',
chr(196).chr(154) => 'E', chr(196).chr(155) => 'e',
chr(196).chr(156) => 'G', chr(196).chr(157) => 'g',
chr(196).chr(158) => 'G', chr(196).chr(159) => 'g',
chr(196).chr(160) => 'G', chr(196).chr(161) => 'g',
chr(196).chr(162) => 'G', chr(196).chr(163) => 'g',
chr(196).chr(164) => 'H', chr(196).chr(165) => 'h',
chr(196).chr(166) => 'H', chr(196).chr(167) => 'h',
chr(196).chr(168) => 'I', chr(196).chr(169) => 'i',
chr(196).chr(170) => 'I', chr(196).chr(171) => 'i',
chr(196).chr(172) => 'I', chr(196).chr(173) => 'i',
chr(196).chr(174) => 'I', chr(196).chr(175) => 'i',
chr(196).chr(176) => 'I', chr(196).chr(177) => 'i',
chr(196).chr(178) => 'IJ',chr(196).chr(179) => 'ij',
chr(196).chr(180) => 'J', chr(196).chr(181) => 'j',
chr(196).chr(182) => 'K', chr(196).chr(183) => 'k',
chr(196).chr(184) => 'k', chr(196).chr(185) => 'L',
chr(196).chr(186) => 'l', chr(196).chr(187) => 'L',
chr(196).chr(188) => 'l', chr(196).chr(189) => 'L',
chr(196).chr(190) => 'l', chr(196).chr(191) => 'L',
chr(197).chr(128) => 'l', chr(197).chr(129) => 'L',
chr(197).chr(130) => 'l', chr(197).chr(131) => 'N',
chr(197).chr(132) => 'n', chr(197).chr(133) => 'N',
chr(197).chr(134) => 'n', chr(197).chr(135) => 'N',
chr(197).chr(136) => 'n', chr(197).chr(137) => 'N',
chr(197).chr(138) => 'n', chr(197).chr(139) => 'N',
chr(197).chr(140) => 'O', chr(197).chr(141) => 'o',
chr(197).chr(142) => 'O', chr(197).chr(143) => 'o',
chr(197).chr(144) => 'O', chr(197).chr(145) => 'o',
chr(197).chr(146) => 'OE',chr(197).chr(147) => 'oe',
chr(197).chr(148) => 'R',chr(197).chr(149) => 'r',
chr(197).chr(150) => 'R',chr(197).chr(151) => 'r',
chr(197).chr(152) => 'R',chr(197).chr(153) => 'r',
chr(197).chr(154) => 'S',chr(197).chr(155) => 's',
chr(197).chr(156) => 'S',chr(197).chr(157) => 's',
chr(197).chr(158) => 'S',chr(197).chr(159) => 's',
chr(197).chr(160) => 'S', chr(197).chr(161) => 's',
chr(197).chr(162) => 'T', chr(197).chr(163) => 't',
chr(197).chr(164) => 'T', chr(197).chr(165) => 't',
chr(197).chr(166) => 'T', chr(197).chr(167) => 't',
chr(197).chr(168) => 'U', chr(197).chr(169) => 'u',
chr(197).chr(170) => 'U', chr(197).chr(171) => 'u',
chr(197).chr(172) => 'U', chr(197).chr(173) => 'u',
chr(197).chr(174) => 'U', chr(197).chr(175) => 'u',
chr(197).chr(176) => 'U', chr(197).chr(177) => 'u',
chr(197).chr(178) => 'U', chr(197).chr(179) => 'u',
chr(197).chr(180) => 'W', chr(197).chr(181) => 'w',
chr(197).chr(182) => 'Y', chr(197).chr(183) => 'y',
chr(197).chr(184) => 'Y', chr(197).chr(185) => 'Z',
chr(197).chr(186) => 'z', chr(197).chr(187) => 'Z',
chr(197).chr(188) => 'z', chr(197).chr(189) => 'Z',
chr(197).chr(190) => 'z', chr(197).chr(191) => 's',
// Euro Sign
chr(226).chr(130).chr(172) => 'E'
)
)
);
return $this->__slug($string, am($settings, array('translation' => $translations[$settings['translation']])));
}
$string = low($string);
$string = preg_replace('/[^a-z0-9_]/i', $settings['separator'], $string);
$string = preg_replace('/' . preg_quote($settings['separator']) . '[' . preg_quote($settings['separator']) . ']*/', $settings['separator'], $string);
if (strlen($string) > $settings['length'])
{
$string = substr($string, 0, $settings['length']);
}
$string = preg_replace('/' . preg_quote($settings['separator']) . '$/', '', $string);
$string = preg_replace('/^' . preg_quote($settings['separator']) . '/', '', $string);
return $string;
}
}
?>
?>
Comment
41 bug in unique slug
replace line 121 by
$conditions = array($Model->alias . '.' . 'slug LIKE' => $slug . '%');
Comment
42 Create slug to existing data
Comment
43 Loop Through And Update All
Once, on a particularly huge database, I had to use a temporary field (I did have access. I guess you could use cache if you don't) to flag which I've already updated, and I just did those is batches using the LIMIT option.
Works like a charm on MySQL. Also used this process with the Auth component to convert plaintext passwords in the database.
Good luck.
Now the Million Dollar question, does this work with Cake 1.2 Final? I'll be trying soon.
Comment
44 Unique slug
$conditions = array($Model->alias . ‘.’ . ’slug LIKE’ => $slug . ‘%’);
if (!empty($Model->id))
{
$conditions[$Model->alias . '.' . $Model->primaryKey. ' <>'] = $Model->id;
}
if you want to maintain unique slugs (thanks to red frog who started me on founding the solution : http://blog.awpny.com/2008/07/sluggable-behavior-rc2/)
Question
45 Illegal Offset Type in /libs/set.php
Illegal offset type [CORE/cake/libs/set.php, line 901]The code listed for the warning...
for ($i = 0; $i < $count; $i++) {if (is_int($keys[$i])) {
$newList[$list[$keys[$i]]] = null;
And finally the context is listing an array from the Sluggable behavior...
$list = array(
"Sluggable",
array(
"overwrite" => "true"
)
)
$assoc = true
$sep = ","
$trim = true
$keys = array(
0,
1
)
$count = 2
$numeric = true
$newList = array(
"Sluggable" => null
)
$i = 1
Any ideas on how to fix this warning? Thanks in advance.
Bug
46 Unique slug
But when I edited a record, the slug might not be unique.
Is it a bug? Or I have missed something?
Comment
47 Unique slug
Oh god, I've found it a bug.
I've edited the line 125, from
to$conditions[$Model->alias . '.' . $Model->primaryKey] = '!= '.$Model->id;
Maybe the bug is due to the new CakePHP.$conditions[$Model->alias . '.' . $Model->primaryKey. ' !='] = $Model->id;
Question
48 Stop Words in Sluggable Behaviour
This behaviour is great, thnx! But I miss a stopwords option. Can I setup this?
Something like this:
http://wordpress.org/extend/plugins/remove-stopwords-from-slug/
Thank you so much!
David.
Comment
49 multilanguage slugs with translate behavior
If you want to use multilanguage slugs with the translate behavior then in the sluggable behavior you need to use beforeValidate instead of beforeSave to generate your slugs because the translate behavior expects the fields to be translated in beforeValidate.