Friendly URL Generation in CakePHP
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
class AppModel extends Model{public $niceNameField = 'nicename';public $buildUrlFrom = 'title';public $wordSeparator = '-';public function getNicename($string){$currentUrl = $this->_getStringAsNicename($string);$conditions = array($this->name . '.' . $this->niceNameField => $currentUrl);$results = $this->findAll($conditions, $this->name . '.*');if ( $results !== false && count($results) > 0 ){$sameUrls = array();foreach( $results as $record ){$sameUrls[] = $record[$this->name][$this->niceNameField];}}if ( isset($sameUrls) && count($sameUrls) > 0 ){$currentBeginningUrl = $currentUrl;$currentIndex = 1;while ( $currentIndex > 0 ){if ( !in_array($currentBeginningUrl . $this->wordSeparator . $currentIndex, $sameUrls) ){$currentUrl = $currentBeginningUrl . $this->wordSeparator . $currentIndex;$currentIndex = -1;}$currentIndex++;}}return $currentUrl;}private function _getStringAsNiceName($string){//strip tags, trim, and lowercase$string = strtolower(trim(strip_tags($string)));//replace single quotes and double quotes first$string = preg_replace('/[\']/i', '', $string);$string = preg_replace('/["]/i', '', $string);$string = preg_replace('/&/', 'and', $string);//remove non-valid characters$string = preg_replace('/[^-a-z0-9]/i', $this->wordSeparator, $string);$string = preg_replace('/-[-]*/i', $this->wordSeparator, $string);//remove from beginning and end$string = preg_replace('/' . $this->wordSeparator . '$/i', '', $string);$string = preg_replace('/^' . $this->wordSeparator . '/i', '', $string);return $string;}public function beforeSave(){if ( empty($this->id) && !empty($this->data[$this->name][$this->niceNameField]) ){$this->data[$this->name][$this->niceNameField] = $this->getNiceName($this->data[$this->name][$this->niceNameField]);}elseif ( !empty($this->id) && !empty($this->data[$this->name][$this->niceNameField]) ){$this->data[$this->name][$this->niceNameField] = $this->getNiceName($this->data[$this->name][$this->niceNameField]);}elseif ( isset($this->data[$this->name][$this->niceNameField]) && empty($this->data[$this->name][$this->niceNameField]) ){$this->data[$this->name][$this->niceNameField] = $this->getNiceName($this->data[$this->name][$this->buildUrlFrom]);}return true;}}
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
public function beforeSave(){if ( empty($this->id) && !empty($this->data[$this->name][$this->niceNameField]) ){$this->data[$this->name][$this->niceNameField] = $this->getNiceName($this->data[$this->name][$this->niceNameField]);}elseif ( !empty($this->id) && !empty($this->data[$this->name][$this->niceNameField]) ){$this->data[$this->name][$this->niceNameField] = $this->getNiceName($this->data[$this->name][$this->niceNameField]);}elseif ( empty($this->data[$this->name][$this->niceNameField]) ){$this->data[$this->name][$this->niceNameField] = $this->getNiceName($this->data[$this->name][$this->buildUrlFrom]);}return true;}
The beforeSave() method is called on each save, and the following checks are made:
-
If no ID has been set (
$this->id), then we are doing a new insert. If thenicenamefield is not empty — then we want to use thenicenamefield. It will still format it properly, just for extra caution and duplication check. -
If an ID has been set, then we are doing the update. If the
nicenamefield is not empty — then we want to use thenicenamefield. Again, format it accordingly and check for duplication. -
Finally, if the field is empty — then we need to generate the nice name from the
titlefield. This is useful on insert and on updates. If at any point you wanted to re-generate the URL with thetitle, then you would could simply leave thenicenamefield 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
public function beforeSave(){parent::beforeSave();//custom actions can be made here}
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
jojo June 7th, 2007
wwwwwhhhhhhaaaaaatttttttt!!!!!!
What is a URL? November 23rd, 2007
You go out of your way to make more work for yourself. Why not simply name your data with descriptive names in the database, and then the URL will be the same name as the name in the database. Have a unique ID and a unique name for each record.