Contact Us

Migrating a Database Table for use with the CakePHP Sluggable Behavior

Posted on 8/6/09 by Tim Koschützki

Hey folks,

if you are not familiar with Mariano's Sluggable Behavior, you should definitely check it out. It's a nice tool to generate SEO-friendly urls in your application. If you are anything like with our long urls, you might want to look at it. ;)

If you don't know what a url slug is:

A slug is a few words that describe a post or a page. Slugs are usually a URL friendly version of the post title, but a slug can be anything you like. Slugs are meant to be used with your site's urls as they help describe what the content at the URL is. They might or might not help your SEO ranking as well for the keywords in the slug/content.

Example post url:

The slug for that game content is "tg-motocross-2".

This blogpost deals with migrating an existing production table into using Mariano's sluggable behavior and thereby increasing SEO friendliness of your site.

Step 1: Back up the database table

Back up the table for which you want to add slugs. We cannot be blamed for any database damage this might cause.

Step 2: Modify your database

Add a "slug" field to the table. The default length that the behavior uses is 100 chars which should be enough in most cases. So VARCHAR(100) is what you need or the equivalent for your db driver.

Step 3: Add your $actsAs declaration

Add the proper call to $actsAs to the model that you want to migrate. Put in any existing behaviors that you need as well:

var $actsAs = array(
  'Sluggable' => array('overwrite' => true)

Make sure to set the overwrite option, as otherwise the behavior will refuse to overwrite the slug field the way we will do it. Also make sure to set the 'label' option if the table field that your slugs will depend on is not called 'title'.

For the full host of options, check the sluggable behavior.

Important: The field your slugs depend on (title, name or combinations) MUST NOT be empty for any row in your table. The shell takes this into account and provides you with an error log. To save you some headaches, make sure the fields are properly filled before you run the sluggish shell.

Step 4: Modify The Sluggable Behavior Code

You need to change the Sluggable behavior code on two occurances to make it work with the recent Cake releases.

1. Change Line 121 to:

$conditions = array($Model->alias . '.' . $this->__settings[$Model->alias]['slug'] . ' LIKE' => $slug . '%');

2. Change Line 125 to:

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

This is simply changing the syntax for NOT and LIKE conditions.

Step 5: Download Debuggable's Sluggish Shell

Copy the following code to a 'sluggish.php' file in your vendors/shells folder. Just mouseover the code to see it in raw text.

 * Sluggish Shell
 * Set overwrite => true before running this in your $actsAs declaration!
 * This shell allows you to generate unique slugs for a database table ready for use
 * with the sluggable behavior by Mariano Iglesias
 * Sluggish Shell : Make your table sluggable
 * Copyright 2009, Debuggable, Ltd. (
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 * @filesource
 * @copyright     Copyright 2009, Debuggable, Ltd. (
 * @link CakePHP(tm) Project
 * @license The MIT License

class SluggishShell extends Shell {
 * undocumented function
 * @return void
 * @access public

  function main() {
    if (empty($this->args)) {
      return $this->out('You need to specify a modelname');

    $model = $this->args[0];
    $force = isset($this->args[1]) ? $this->args[1] : false;

    $Model = ClassRegistry::init($model);
    $behavior = 'Sluggable';

    if (!is_object($Model)) {
      return $this->out('This model does not exist.');

    if (!in_array($behavior, $Model->actsAs) && !array_key_exists($behavior, $Model->actsAs)) {
      return $this->out('The Sluggable Behavior is not yet linked to the model.');

    $label = 'title';
    if (isset($Model->actsAs['Sluggable']['label'])) {
      $label = $Model->actsAs['Sluggable']['label'];

    $Model->recursive = -1;
    $conditions = $force ? false : array('slug' => '');
    $rows = $Model->find('all', compact('conditions'));
    $count = count($rows);

    $i = 0;
    foreach ($rows as $row) {
        $Model->primaryKey => $row[$model][$Model->primaryKey],
        $label => $row[$model][$label]

      $row = $Model->find('first', array(
        'conditions' => array(
          $Model->primaryKey => $row[$model][$Model->primaryKey],
          'slug' => ''
        'contain' => false
      if (empty($row)) {
      } else {
        $this->out('Problem saving the slug for ' . $row[$model][$label]);

    $this->out('Added ' . $i . ' slugs for ' . $count . ' ' . Inflector::pluralize($model));
 * undocumented function
 * @return void
 * @access public

  function help() {
    $this->out('Debuggable Ltd. Sluggish Shell -');
    $this->out('Important: Configure your paths in the shell\'s initialize() function.');
    $this->out('This shell allows you to migrate a database table to use Mariano Iglesias\' Sluggable Behavior.');
    $this->out('Add a slug field to the table, download the sluggable behavior, add your $actsAs declaration and run this shell.');
    $this->out("Usage: cake sluggish ModelNameInCamelCase");

The code is very simple. The shell takes a modelname and checks if your $actsAs declaration is properly set up. If it is, it finds all rows from the table and saves them right away, with the label field in the Model::set() call.
Since we have specified overwrite => true in the $actsAs declaration, the sluggable behavior now overwrites all slug fields in the table, giving you nice slugs.

Step 6: Run The Sluggish Shell

Run "cake sluggish ModelNameInCamelCase" in your terminal.

Step 7: Change your urls

The Sluggable behavior creates unique slugs. If two of your blogposts for example have the same title, the first one will have 'my-blogpost-title' as its slug and the second one will have 'my-blogpost-title-1'. Mariano's behavior attaches an integer to the slug depending on how often the slug was used already.

Now that you have unique slugs, you can change your controller code and view code to take account of this. If you are heavily indexed in google already, you might want to provide 301 (permanent) redirects for the old urls, or just offer both urls to access the same blogpost, but only use slugs throughout the app.

Enjoy! Post any feedback or help requests in the comments below.

-- Tim Koschuetzki aka DarkAngelBGE


You can skip to the end and add a comment.

Dan  said on Jun 08, 2009:

You should really provide 301 (permanent) redirects so that search engines will use the new urls in future.

Also, in my experience, having the same content available through multiple urls had an adverse effect on my search rank.

This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.