Sluggable Behavior

By Mariano Iglesias (mariano)
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.

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

  1. Create a file named sluggable.php on your app/models/behaviors folder using the contents provided below.
  2. 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:

  1. 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.
  2. slug: name of the field in the mapped database table that will hold the generated slug. Defaults to 'slug'.
  3. separator: string to use to separate words in the generated slug. Defaults to '-'.
  4. length: maximum length (in characters) a slug can take. Defaults to 100.
  5. overwrite: tells if the slug should be generated only when creating new records (false) or also when editing (true). Defaults to false.
  6. 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']) >= && count($settings['translation']) % == 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($stringam($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($string0$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 295

CakePHP Team Comments Author Comments
 

Question

1 Multiple field label

Does anyone know how this could be modified to allow the label to be specified as a concatenation of two fields (say, firstName and lastName), not counting changing the database. That is, assume, that firstName and lastName are two seperate fields.

Posted Apr 11, 2007 by Joseph Lietz
 

Comment

2 Multiple field label

@Joseph: Use this version of the Behavior:

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.
Posted Apr 12, 2007 by Mariano Iglesias
 

Comment

3 Multiple field label

@Mariano

That seems to work perfectly. No bugs. Thanks.
Posted Apr 12, 2007 by Joseph Lietz
 

Comment

4 Unique Slugs

Should this Slug Behavior automatically accommodate for slug uniqueness? I could have sworn that at one point, it did. That is, when the behavior would check the database and add to the end of the slug until it was unique (as in, my-post-title-32).

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).
Posted Apr 20, 2007 by Joseph Lietz
 

Comment

5 Unique Slugs

@Joseph: it should work. On the bin you pasted check lines 72-77:

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.
Posted Apr 20, 2007 by Mariano Iglesias
 

Comment

6 Unique Slugs

@Joseph: I think I found an issue, while I'm building the test change line 81 from:

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']);
?>
Posted Apr 20, 2007 by Mariano Iglesias
 

Comment

7 Unique Slugs

@Joseph: I've updated the code on the article (use that one instead of the bin) and added the test case. Enjoy!
Posted Apr 20, 2007 by Mariano Iglesias
 

Comment

8 Unique Slugs

Hi Mariano,

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
Posted Apr 24, 2007 by Mandy Singh
 

Comment

9 Unique Slugs

@Mandy: Behaviors are only available for 1.2 branch of CakePHP. However, not all is lost, and you can use the tutorial Adding friendly URLS to The Cake Blog Tutorial to implement Slugs on CakePHP 1.1
Posted Apr 24, 2007 by Mariano Iglesias
 

Question

10 Translations

Hi Mariano,

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
Posted Jun 14, 2007 by Alex McFadyen
 

Comment

11 Editing..

This behaviour is really awesome thx for it.. but i was wondering about 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
Posted Jun 16, 2007 by chris
 

Bug

12 saveField

When saving a single field (e.g. incrementing a view counter), you get a notice that 'title' does not exist (line 62).

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.
Posted Jun 26, 2007 by Alex McFadyen
 

Comment

13 Unicode slugs

Currently the slug generation will strip out any non ascii chars w.g. accented chars, to allow them to be used, change line ~119 to

$string = preg_replace('/[^\p{Ll}0-9_]/u', $settings['separator'], $string);
Posted Jul 14, 2007 by Alex McFadyen
 

Bug

14 help..

Currently the slug generation will strip out any non ascii chars w.g. accented chars, to allow them to be used, change line ~119 to

$string = preg_replace('/[^\p{Ll}0-9_]/u', $settings['separator'], $string);

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
Posted Aug 22, 2007 by chris
 

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.
Posted Aug 22, 2007 by Alex McFadyen
 

Comment

16 thanks

Thanks but it doesn't work it removes all characters instead of converting them :( I don't know much about regex so am not sure if this is even possible though?

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 />';
?>

$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.

Posted Aug 31, 2007 by chris
 

Question

17 Non english characters converting

It is possible to add a hook or just translation table for non-english symbols? Or a user-defined function, that called before main regex?
Posted Nov 26, 2007 by Sergey Rodovnichenko
 

Comment

18 Non english characters converting

@Sergey: check out the new version which includes character translation table support, with which you can define your own or use the built in translations: utf-8 and iso-8859-1. I've also updated the tutorial to show how to define this setting.
Posted Nov 26, 2007 by Mariano Iglesias
 

Comment

19 Russian symbols UTF8 translation table

According to ISO 9-95 :-)


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
Posted Nov 26, 2007 by Sergey Rodovnichenko
 

Comment

20 Russian symbols UTF8 translation table

@Sergey: thanks! I've added it and committed it as revision 37 in SVN.

So use the latest SVN version, since I won't repackage until preparing a new release.
Posted Nov 26, 2007 by Mariano Iglesias
 

Comment

21 Add a custom prefix and suffix from controller

Adding a custom prefix (or suffix) to the slug might be useful. When I saves a dependent model, it contains only parent_id, so it is impossible to make a slug like 'parent-slug-my-slug'.

Something like this


Model->slug(array('prefix'=>$custom_string));
Model->save();

Posted Dec 4, 2007 by Sergey Rodovnichenko
 

Question

22 Does It work on pre alpha

Does this work with the pre-alpha version of 1.2?

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.
Posted Dec 6, 2007 by Baz L
 

Comment

23 Does It work on pre alpha

All Cake Syrup elements are developed against a relatively recent SVN checkout of CakePHP 1.2. However, as it is explained on the Bindable Behavior, you could use this behavior on early 1.2 releases if you edit the app/models/behaviors/sluggable.php file, look for the function function setup(&$Model, $settings = array()) and add this line:

PHP Snippet:

<?php 
$Model
->alias $Model->name;
?>

right before the following line:

PHP Snippet:

<?php 
if (!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.
Posted Dec 6, 2007 by Mariano Iglesias
 

Comment

24 excellent

Like a glove, recommended!
Posted Dec 17, 2007 by Hannibal Lecter
 

Question

25 upgrade problem

I upgraded from a really early version i think it was the first posted here.. but now it does not work anymore, it does not save the field.. i am using 1.2 latest svn.
Posted Jan 3, 2008 by chris
 

Comment

26 upgrade problem

@chris: did you run the test case against it? I'm seeing it working
Posted Jan 4, 2008 by Mariano Iglesias
 

Comment

27 okies

Ah sorry it works now :D It is something with my ajax function i think.. it did not send special characters ok.. so that why it was not saving.. it worked with older versions though :P
Posted Jan 4, 2008 by chris
 

Comment

28 Allow user submitted slugs when submitting a new record

Works great, but for my application the admin needs to be able to have the opportunity to craft a custom slug (for seo purposes), if they desire, or leave the field blank and have it autgenerated, so for this feature, change line 97 to read:


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']])))

?>
Posted Feb 5, 2008 by Raphael Spindell
 

Comment

29 Works great

Mariano, thanks for a very usefull behavior. Works great!!!

Posted Apr 4, 2008 by Leandro Lopez
 

Comment

30 Accents

Since I'm french, we use accents in titles and "new résumé" was slugged as "new_r_sum_", so I added the following code after line 335 :

$string = strtr($string,  "ÀÁÂÃÄÅàáâãäåÒÓÔÕÖØòóôõöøÈÉÊËèéêëÇçÌÍÎÏìíîïÙÚÛÜùúûüÿÑñ", "aaaaaaaaaaaaooooooooooooeeeeeeeecciiiiiiiiuuuuuuuuynn");
It remove accents and so "new résumé" become "new_resume".

Thanks for your behavior !
Posted May 21, 2008 by Remi
 

Question

31 Error on test

I seem to be having some errors when testing this :(

Individual test case: behaviors/sluggable.test.php

* Failed
Equal expectation fails with member [SlugArticle] with member [slug] at character 13 with [first-article] and [first-article-1] at [/Web/features2/app/tests/cases/behaviors/sluggable.test.php line 367] /Web/features2/app/tests/cases/behaviors/sluggable.test.php -> SluggableTestCase -> testBeforeSave

Any ideas why is this happening? I am using CakePHP 1.2.x RC1
Posted Jun 29, 2008 by Louie Miranda
 

Comment

32 Error on test

I still have to package a new release but for those of you having trouble with this behavior on 1.2 RC use the version straight from the SVN repository:

sluggable.php
and

sluggable.test.php
Posted Jun 30, 2008 by Mariano Iglesias
 

Question

33 Wont work no errors

I'm using the latest version (as far as I know), with cake 1.2.0.7296 RC2.
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?
Posted Jul 9, 2008 by Ben
 

Comment

34 Fixed

Fixed, silly mistake on my part, I declared $actAs in the controller instead of the model.
Posted Jul 9, 2008 by Ben
 

Bug

35 Nice

Nice behaviour, thanks for that!
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
                    
);
                }
?>
Posted Jul 11, 2008 by Florian Greinacher
 

Bug

36 Bug in current 1.2rc

Nice behaviour, thanks for that!
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
    
);
}
?>
Posted Jul 11, 2008 by Florian Greinacher
 

Comment

37 tar.gz

Do you really have to tar one file? I mean you tar something so it's faster to download, but one text file is not going to help. It would make it slightly easier for people just looking to browse the code to see what it's like if it was a text file download.

/my2cents
Posted Jul 29, 2008 by Rob Conner
 

Comment

38 duplicates in RC2

Thanks Florian, I was trying to find the problem with duplicate slugs in cake 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
    
);
}
?>
Posted Aug 14, 2008 by Jamie Mill
 

Question

39 Cross model slugnation

I have an Editor hasOne User, User belongsTo Editor relationship. My Editor.slug needs to come from User.name.

My /users/edit form updates the editors information with saveAll, and the slug is not regenerated. What can I do?

Thanks,
Aidan
Posted Dec 3, 2008 by Aidan Lister
 

Comment

40 Multi-language

Hi I have modified the Sluggable Behavior to use in multi-languages, I post it here

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']) >= && count($settings['translation']) % == 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($stringam($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($string0$settings['length']);
        }

        
$string preg_replace('/' preg_quote($settings['separator']) . '$/'''$string);
        
$string preg_replace('/^' preg_quote($settings['separator']) . '/'''$string);

        return 
$string;
    }
}
?>
?>
Posted Dec 23, 2008 by Igor Felluga
 

Comment

41 bug in unique slug

the query was broken, so behavior couldn't find unique results...

replace line 121 by
$conditions = array($Model->alias . '.' . 'slug LIKE' => $slug . '%');
Posted Jan 4, 2009 by seb
 

Comment

42 Create slug to existing data

How can i create slugs to the existing data, without having to edit each one?
Posted Feb 2, 2009 by Carlos
 

Comment

43 Loop Through And Update All

What I've done in the past, was to loop through ALL the records in the DB and do a save.

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.
Posted Feb 2, 2009 by Baz L
 

Comment

44 Unique slug

Cake has changed the way complex queries are written (the conditions comes with the field) so now it goes from line 121 :
$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/)
Posted Feb 6, 2009 by Raphaele Giordan
 

Question

45 Illegal Offset Type in /libs/set.php

CakePHP seems to suddenly be throwing a wobbly while using the Sluggable behavior. I'm get a warning...

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.
Posted Feb 20, 2009 by Mark
 

Bug

46 Unique slug

I've updated the lastest line 121. I've got the unique slug when I created a new record.
But when I edited a record, the slug might not be unique.
Is it a bug? Or I have missed something?
Posted Mar 24, 2009 by Tuan Tran
 

Comment

47 Unique slug

I've updated the lastest line 121. I've got the unique slug when I created a new record.
But when I edited a record, the slug might not be unique.
Is it a bug? Or I have missed something?

Oh god, I've found it a bug.
I've edited the line 125, from


$conditions[$Model->alias . '.' . $Model->primaryKey] = '!= '.$Model->id;
to

$conditions[$Model->alias . '.' . $Model->primaryKey. ' !='] = $Model->id;
Maybe the bug is due to the new CakePHP.
Posted Mar 24, 2009 by Tuan Tran
 

Question

48 Stop Words in Sluggable Behaviour

Hi!

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.
Posted Apr 7, 2009 by davidhc
 

Comment

49 multilanguage slugs with translate behavior

Just a quick note.
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.
Posted Apr 23, 2009 by Cosmin Cimpoi