Friendly URLs without IDs

Hello Guys,

This is my case:

I have the following URL:

www.example.com/games/view/344/god_of_war_4

How can I make this URL www.example.com/god_of_war_4 routes to the above URL?

Can I do it by using CurlManager rules? or do I need to play in .htaccess file?

Thanks in advance for your help :)

Topic moved (Tips, Snippets and Tutorials > General Discussion for Yii 1.1.x).

I feel the need to share today, so here it is:

In the table where you store the games, add a new column, call it nice_url(the idea is to store the nice url of the game here) and unique INDEX IT.

How you will generate the nice url is up to you, also, i suggest you use dashes(-) instead of underscore in the url because google likes that more.

So let’s say you save a game and the nice_url column of the game is: my-super-duper-game.

Now, having this set, it’s time to work with the url manager, to make it work with our nice url format.

First thing we need to do is to create a url rule class:




// protected/components/MyUrlRule.php


class MyUrlRule extends CBaseUrlRule

{

    public $connectionID = 'db';

 

    public function createUrl($manager,$route,$params,$ampersand)

    {

        if($route==='game/index')

        {

            if (isset($params['nice_url']))

                return $params['nice_url'];

        }

        return false;  // this rule does not apply

    }

 

    public function parseUrl($manager,$request,$pathInfo,$rawPathInfo)

    {

        if(preg_match('%^[a-z0-9\-]+$%', $pathInfo, $matches))

        {

            $nice_url=$matches[0];

            $command=Yii::app()->{$this->connectionID}->createCommand();

            $result=$command->select('nice_url')->from('{{games}}');

            $result->where('nice_url=:nice_url', array(':nice_url'=>$nice_url));

            $result = $result->queryRow();

            if(!empty($result)&&$result['nice_url']===$nice_url)

            {

                $_GET['nice_url']=$result['nice_url'];

                return 'game/index';

            }

        }

        return false;  // this rule does not apply

    }

}




Now that we have the url rule class, let’s use it in the url manager component




// protected/config/main.php

'urlManager'=>array(

	'rules' => array(

                // all you existing rules here, the MyUrlRule class will be the last one.

 

		array(

			'class' => 'application.components.MyUrlRule',

			'connectionID' => 'db',

		),

	),

),



NOTE:

Now, when somebody access http://www.your-domain.com/contact for example, if you have a controller or module named contact, it will match that one so you don’t have to worry about breaking existing rules.

If you don’t have a controller/module named contact, then it will try your “MyUrlRule” class and look into the games table to see if for some reason you have a game called contact. if you don’t have, then it will throw a 404 error.

Back to our example, accessing http://www.your-domain.com/my-super-duper-game will reach your new url rule class in the url manager and the slug "my-super-duper-game" will be matched against the nice_url column from the database. If it will find it, it will forward the request to your game controller, which will process it.

The game controller needs to look like:




class GameController extends CController{


     public function actionIndex($nice_url)

     {

        $model=Game::model()->findByAttributes(array('nice_url'=>$nice_url));

        echo $nice_url;//will show "my-super-duper-game";

     }


}



As you see, in your controller, based on the $nice_url var you can load your game model and use it further :)

Well, that’s it :)

@andy_s - You did it again :D

Thanks a lot dude, will give it a try :)

Thanks a lot working like charm :)

I have not tried it yet, but I plan to look into it sometime soon.

I found this solution here: erickennedy.org/blog/Drupal-to-Yii-Migration-Tips (sorry I cant post links yet)

Here Eric shows how he extended CUrlManager to do lookups on a uri_alias table as well as a widget.

I don’t know what is better but I think it is relevant to the discussion.

I think twisted1919 suggestion is the more official way of doing it using a rule, but I like the way Eric is matching controller actions to aliases. This method is a little more generic and can be used for any path. I think the best solution would be to take the best from both methods, but it all depends on what you are trying to achieve.

For easy of reference here is his code. For a detailed explanation I suggest you have a look at his blog post.

Table:




CREATE TABLE `uri_alias` (

   `id` int(10) unsigned NOT NULL AUTO_INCREMENT,

   `uri` varchar(128) NOT NULL DEFAULT '',

   `controller_action` varchar(32) NOT NULL DEFAULT '',

   `content_id` int(10) unsigned NOT NULL DEFAULT '',

   `redirect` varchar(128) NOT NULL DEFAULT '',

   PRIMARY KEY (`id`),

   UNIQUE KEY `uri` (`uri`),

   KEY `controller_id` (`content_id`,`controller_action`(<img src='http://www.yiiframework.com/forum/public/style_emoticons/default/cool.gif' class='bbc_emoticon' alt='8)' />)

 ) ENGINE=MyISAM DEFAULT CHARSET=utf8



Example insert:




INSERT INTO yii.uri_alias (uri, controller_action, content_id) values ('blog/drupal-launch', 'blog/view', 3);



Class:




/**

  * Override CUrlManager to allow paths to append more arguments and still resolve the path

  */

 class UrlManager extends CUrlManager {

  

   /**

      * Parses the user request.

      * @param CHttpRequest the request application component

      * @return string the route (controllerID/actionID) and $_GET[] parameters set by this function

      */

     public function parseUrl($request) {

     $uri = $request->pathInfo;

        

     // 99% of requests hit a rewritten url, so we should query DB first

     $rs = db()->createCommand("SELECT controller_action,content_id,redirect,uri FROM uri_alias WHERE uri = :uri")->queryRow(TRUE, array('uri' => $uri));    

        

     if ($rs['content_id'] > 0) {

           

       if (strcmp($uri, $rs['uri']) == 0) {   // case matches exactly

          $_GET['id'] = $rs['content_id'];     // set GET parameter for controller

          return $rs['controller_action'];     

       }

       // case must not match, so redirect

       $request->redirect('/' . $rs['uri'], true, 301);

                   

     } elseif (!empty($rs['redirect'])) {

       $request->redirect($rs['redirect'], true, 301);

     }

     

     /**

      * if no DB match, try pattern matching

      * standard paths are

      * blog/$id/view = blog/$id

      * blog/$id/update or blog/$id/delete

      */

     if (is_numeric(arg(1))) {

             $controller = str_replace('-', '', arg(0));

             $action = arg(2);

             $_GET['id'] = arg(1);

             return $this->removeDashes($controller) . '/' . ($action ? $this->removeDashes($action) : 'view');

     }

     

     switch (arg(0)) {

         case 'minify':

             $_GET['group'] = arg(1);

             return 'minify/index';

             break;

         case 'xmlsitemap0.xml':

             return 'XMLSitemap/view';

             break;

     }

        

     // Direct

     return $this->removeDashes($uri); // Remove dashes to support dash-path actions (linux case sensitive too!)

     }

     

     /**

      * Replaces dashes, and also ensures the next letter will be uppercase, since Linux is case-sensitive

      */

     private function removeDashes($str) {

         return str_replace(' ', '', ucwords(str_replace('-', ' ', $str)));

     }

 }



FYI: Credit for this code should go to Eric Kennedy @erickennedy.org

twisted1919 thx for greate solution !