I am still in the progress of overhauling my website, and will launch a bare-bones version here soon. The new site will be moved off of Wordpress and will be using the CakePHP Framework for the back-end. This meant a little more work to get all of the data moved over and organized to my liking. With the new system, there will be more data in place.

With the new data in place, I needed to keep things neat and tidy. One specific area is that of the URL. I am not a big fan of using numerical ID’s in the address bar. So, I needed a quick way to generate SEF URL’s (or manually over-ride if necessary). My search initially led me to Adding friendly URLs to The Cake Blog Tutorial by Mariano Iglesias. I liked his approach, but needed something more.

I am a firm believer of the DRY principle and wanted to use his approach, but didn’t want to continually add a beforeSave() method in all of my models. I decided to stick with a convention and extend it a little further. The code looks like this:

url-app-model.php

  1. class AppModel extends Model
  2. {
  3. public $niceNameField = 'nicename';
  4. public $buildUrlFrom = 'title';
  5. public $wordSeparator = '-';
  6.  
  7. public function getNicename($string)
  8. {
  9. $currentUrl = $this->_getStringAsNicename($string);
  10. $conditions = array($this->name . '.' . $this->niceNameField => $currentUrl);
  11. $results = $this->findAll($conditions, $this->name . '.*');
  12. if ( $results !== false && count($results) > 0 )
  13. {
  14. $sameUrls = array();
  15. foreach( $results as $record )
  16. {
  17. $sameUrls[] = $record[$this->name][$this->niceNameField];
  18. }
  19. }
  20. if ( isset($sameUrls) && count($sameUrls) > 0 )
  21. {
  22. $currentBeginningUrl = $currentUrl;
  23. $currentIndex = 1;
  24. while ( $currentIndex > 0 )
  25. {
  26. if ( !in_array($currentBeginningUrl . $this->wordSeparator . $currentIndex, $sameUrls) )
  27. {
  28. $currentUrl = $currentBeginningUrl . $this->wordSeparator . $currentIndex;
  29. $currentIndex = -1;
  30. }
  31. $currentIndex++;
  32. }
  33. }
  34. return $currentUrl;
  35. }
  36.  
  37. private function _getStringAsNiceName($string)
  38. {
  39. //strip tags, trim, and lowercase
  40. $string = strtolower(trim(strip_tags($string)));
  41. //replace single quotes and double quotes first
  42. $string = preg_replace('/[\']/i', '', $string);
  43. $string = preg_replace('/["]/i', '', $string);
  44.  
  45. $string = preg_replace('/&/', 'and', $string);
  46.  
  47. //remove non-valid characters
  48. $string = preg_replace('/[^-a-z0-9]/i', $this->wordSeparator, $string);
  49. $string = preg_replace('/-[-]*/i', $this->wordSeparator, $string);
  50.  
  51. //remove from beginning and end
  52. $string = preg_replace('/' . $this->wordSeparator . '$/i', '', $string);
  53. $string = preg_replace('/^' . $this->wordSeparator . '/i', '', $string);
  54.  
  55. return $string;
  56. }
  57.  
  58. public function beforeSave()
  59. {
  60. if ( empty($this->id) && !empty($this->data[$this->name][$this->niceNameField]) )
  61. {
  62. $this->data[$this->name][$this->niceNameField] = $this->getNiceName($this->data[$this->name][$this->niceNameField]);
  63. }
  64. elseif ( !empty($this->id) && !empty($this->data[$this->name][$this->niceNameField]) )
  65. {
  66. $this->data[$this->name][$this->niceNameField] = $this->getNiceName($this->data[$this->name][$this->niceNameField]);
  67. }
  68. elseif ( isset($this->data[$this->name][$this->niceNameField]) && empty($this->data[$this->name][$this->niceNameField]) )
  69. {
  70. $this->data[$this->name][$this->niceNameField] = $this->getNiceName($this->data[$this->name][$this->buildUrlFrom]);
  71. }
  72.  
  73. return true;
  74. }
  75. }

This is placed in your app folder as app_model.php. Now, instead of having to call beforeSave() in all of my models, it will automatically be called. At the top you will see the convention that I used. The URL friendly name is stored as nicename, and is generated from title in the same table. Also, I chose to separate my URL’s with ‘-’. (This could be changed to be an underscore if you liked).

url-app-model.php

  1. public function beforeSave()
  2. {
  3. if ( empty($this->id) && !empty($this->data[$this->name][$this->niceNameField]) )
  4. {
  5. $this->data[$this->name][$this->niceNameField] = $this->getNiceName($this->data[$this->name][$this->niceNameField]);
  6. }
  7. elseif ( !empty($this->id) && !empty($this->data[$this->name][$this->niceNameField]) )
  8. {
  9. $this->data[$this->name][$this->niceNameField] = $this->getNiceName($this->data[$this->name][$this->niceNameField]);
  10. }
  11. elseif ( empty($this->data[$this->name][$this->niceNameField]) )
  12. {
  13. $this->data[$this->name][$this->niceNameField] = $this->getNiceName($this->data[$this->name][$this->buildUrlFrom]);
  14. }
  15.  
  16. return true;
  17. }

The beforeSave() method is called on each save, and the following checks are made:

  1. If no ID has been set ($this->id), then we are doing a new insert. If the nicename field is not empty — then we want to use the nicename field. It will still format it properly, just for extra caution and duplication check.

  2. If an ID has been set, then we are doing the update. If the nicename field is not empty — then we want to use the nicename field. Again, format it accordingly and check for duplication.

  3. Finally, if the field is empty — then we need to generate the nice name from the title field. This is useful on insert and on updates. If at any point you wanted to re-generate the URL with the title, then you would could simply leave the nicename field blank and a new one would be generated.

Now, if you wanted to over-ride this in a sub-class, then you could simply call the beforeSave() method with nothing in it — the URL generator would never be executed. However, if you needed to do some other custom save methods in your model, but still needed the URL generator you could use:

url-model.php

  1. public function beforeSave()
  2. {
  3. parent::beforeSave();
  4.  
  5. //custom actions can be made here
  6. }

Overall, this eliminated the need to clutter my models and avoid repetition, but still gave me the flexibility to generate the friendly URL’s. Thoughts?

2 Comments Add your comment

  1. Matt February 17th, 2009

    Really good idea, I was looking for something similar myself.

    My pages table has ID and friendly columns so I want to make sure that the friendly URL doesn't already exist before saving. I just needed a function that replaces special characters before saving and was wondering if Cake already had something and stumbled on yor site whilst googling.

    Great idea!

    Cheers

  2. Amirul October 22nd, 2009

    Hi,
    I've tried this code but didn't work. Put your code app_model.php then add another field 'nicename' to the model and when I add post, it didn't save any string to the 'nicename' field. Do you know where I'm doing wrong?

Comments are closed.