Howto create a secondary route format

Hi all,

I’m trying to set up a dblooked’up controller

currently Yii does the following :

  • analyse the request

  • get the value of the ‘r’ var

  • expand it

  • initialise correct controller with needed action

-> which gives in other terms

  • request : index.php?r=MyController/alert

  • initialize controller MyController with action actionAlert

Now, I would like to add a new routing method which does the following

  • analyze the request

  • get the value of the ‘c’ var (my new routing var)

  • expand it (using db lookup)

  • initialize correct controller with needed action

-> which gives in other terms

  • request : index.php?c=x23/alert

  • perform a db lookup to find correct controller. let’s say x23 is controller MySuperController

  • initialize controller MySuperController with actionAlert

At the end it would do something like this (simplistic but …) :


 // index.php?c=x23/alert

if(isset($_REQUEST['c'])) {

  $splitRoute = explode('/',$_REQUEST['c']);

  // $splitRoute = array('x23','alert');

  $splitRoute[0] = Url::dbLookUp($splitRoute[0]);

  // $splitRoute = array('MySuperController','alert');

  $_REQUEST['r'] = implode('/',$splitRoute);

  // $_REQUEST['r'] = 'MySuperController/alert';

}

// ...

// classic Yii routing



To make sure I’m clear about what I want :wink: here is how I implemented it in Prado some time ago :

I have a table in a db which link hashes with pages and vars




hash | page        | dbData

x26  | contact.php | manager@mail.com

x36  | contact.php | dev@mail.com

x28  | home.php    | someData



I created the following classes :

  • XDbUrlManager.php <-> TUrlManager.php (to handle constructUrl, …)

  • XDbUrlMapping.php <-> TUrlMapping.php

  • XDbService.php <-> TPageService.php (XDbService extends TPageService and perform the db lookup)

  • XDbPage.php <-> TPage.php (XDbPage extends TPage and handles properties retrieved through the db lookup)

  • XDbThemeManager.php <-> TThemeManager.php

In the end I use it like this :

index.php?dbpage=x26

-> db lookup which gives :


class contact extends XDbPage {

  public function onLoad($param) {

    parent::onLoad($param);

    // property dbData has been filled during request

    // so I can use it to pre-fill a data field

    $this->managerEmail->text = $this->dbData;

  }

}

If someone is able to give me an idea to create this kind of stuff under Yii, I would really appreciate it.

Thank you

So noone has an idea…

Too bad.

Thanks anyway

I think it’s easier than in Prado: Check out CUrlManager::parseUrl()'s source. It will extract the route from a given request. So you could extend your own manager class from it and override parseUrl() and constructUrl(). Inside that methods you can use the db component to access the DB and use your custom logic to parse or create a URL. Then configure that class as your urlManager component:


'components' => array(

 ...

    'urlManager' => array(

        'class'=>'MyUrlManager',

    ),

... 

Hi Mike,

thank you for your answer, first, I wanted to do something like that but I thought it wouldn’t not fit my needs.

In order to let you understand my problems, I’ll try to explain it in a better way.

I created a CMS for Prado. This CMS is base on :

  • XCmsService

  • XCmsPage

  • XCmsUrlManager

  • XCmsUrlMapping

Basicaly, a content is associated with a template in the DB. For example article 12 use the CMS template SimpleArticle.

here is how it is declared in the Prado application.xml :


<services>

    <service id="page" class="TPageService" DefaultPage="Home" BasePath="Application.Pages" />

    <service id="cms" class="XCmsService" DefaultPage="Article" BasePath="Application.Cms" />

</services>

finally, it’s working this way :

  1. call : index.php?cms=12

  2. XCmsService performs DB Lookup and return the template class : SimpleArticle

  3. SimpleArticle (which extends XCmsPage) has a property articleId which is filled with 12

  4. XCmsPage follows classic TPage lifecycle

Right now I’m trying to port this CMS to Yii but I have a simple problem :

I cannot only write a specific UrlManager because I want to be able to integrate the CMS

into a Yii project without having to rewrite anything.

For example, if my project has a specific SpecificUrlManager, I will have to rewrite the CmsUrlManager to extends this SpecificUrlManager and this can be pretty complex and this is exactly the opposite of what I want. In Prado, the integration is seamless and I want the same in Yii

The best would be to be able to configure the Yii app this way :


'components' => array(

...

    'routeService' => array(

        'class' => 'CRoutingManager',

        'routes' => array(

            array(

                'class' => 'CUrlManager', 

                'routeVar' => 'r', 

                'priority' => 1

            ),

            array(

                'class' => 'XCmsUrlManager', 

                'routeVar' => 'c', 

                'priority' => 2

            ),

        )

    ),

... 

If anyone has an idea, this would be great.

Thank you for your time.

You might want to take a look at the onBeginRequest event of an application.

I’ve never used this event, so I don’t know exactly what data/info is or is not available, but you might be able to set the route within that event.

Thank you for your answer but I’m not sure that hooking the method to choose the routing system in the onBeginRequest event is a good idea. I will check when this event is raised, but I’m not sure that

  • capturing the request

  • change the $_GET / $_POST values according to the DBLookup to update the route

  • resume initial behaviour

is really nice / Yii friendly.

Anyway, I had a deeper look in Yii, and I think I could :

  • extend CWebApplication to XWebApplication

  • rewrite CWebApplication::getUrlManager to use XRouter and allow multiples UrlManager (CUrlManager / XCmsUrlManager / …)

  • create the XCmsUrlManager with my db lookups

  • check in the framework code where the UrlManager is involved to analyse side effects

to sum up, I think I would create :

XService to manage multiple UrlManager


class XRouter extends CApplicationComponent {

    ....

    public function discoverUrlManager($request) {

        if($this->currentUrlManager === null) {

            foreach ($this->_urlManagers as $urlManager) {

                // keep the order defined in the config file

                if($request->getParam($urlManager->routeValue) !== null) {

                    $this->currentUrlManager = $urlManager;

                    break;

                }

            }

        }

        return $this->currentUrlManager;

    }

    ....

}

XWebApplication


class XWebApplication extends CWebApplication {

    ....

    public function getUrlManager()

    {

        return $this->getRouters()->discoverUrlManager($this->getRequest());

    }


    ....

}

This would allow me to handle multiple routing systems without breaking the whole Yii system. Of course these snippets are draft but I think it should help you understand what I’m targeting.

If anyone has an idea for something less intrusive, this would be really great.

Perhaps Qiang has an idea and can give is own feedback ?

Hmm. I still don’t see the requirement for multiple UrlManagers. Everything that has to do with URL resolution/construction is inside that class (that’s the beauty of OOP). CWebApplication doesn’t resolve anything.

Simply extend CUrlManager, check for your custom route from DB and if nothing found, return parent::parseUrl() instead. Due to inheritance your custom class already has all capabilities of CUrlManager - don’t need a second one.

Events also won’t help here, because you don’t want to do something at an “interesting moment” but change the behavior of a specific component: UrlManager.

You can also add ‘priority’ property to your UrlManager if that’s really a concern and check it’s value in your parseUrl() method to change your logic accordingly. Rule of thumb: Keep it simple! :)

I’ve read your post once again. I’m still not sure exactly what you want to achieve.

Now I feel like you’re wanting to redirect just because you want to use a CMS specific webpage layout. Is that correct?

Maybe all you need is a CmsController class and override the init() method to initialise things (like retrieving some data like the layout). Or maybe override the beforeAction() method to initialise things.

Or another option could be to develop your CMS as a module (I’m currently trying to do a similar project to learn the ins and outs of Yii).

So:




class CmsModule extends CModule

{

 .....

}



and then the default controller specific to this module:




class MyCmsController extends CController

{

  .....

}



By using the module approach your CMS routes are always distinct from the existing webapplication.

I agree with you Mike about everything except that :

  • how my routing system will integrate if I already have a Yii application with sepcific UrlManager ?

Anyway, you are right, for my initial backport I’ll keep it simple and extend the CUrlManager but I’m pretty sure that won’t be a good solution in the long run because two distinct behaviours in the same component seems not good to me.

let me show you an example :


$this->createUrl('registration/step1');

// this produce index.php?r=registration/step1

my problem is how can I generate my url for an article produced by the Cms ?

in the Prado version of the CMS I’m doing it this way :


/*

 * XCmsUrlManager::buildServiceParameter(array $parameters) return a string with all parameters needed encoded

 */

// if I am in a page (outside the cms service)

$this->service->constructUrl('cms',XCmsUrlManager::buildServiceParameter(array('article'=>12,'rubric'=>5)));

// which gives index.php?cms=<hashcode>

// if I am in a cms page (inside the cms service)

$this->constructUrl(XCmsUrlManager::buildServiceParameter(array('article'=>12,'rubric'=>5)));

// which gives index.php?cms=<hashcode>

with your explanation, I will create the XCmsUrlManager but I would have to create a new method : createCmsUrl(…) which means patch CHtml class, and all class relying on the createUrl method to take care of the new createCmsUrl … This way I would probably have to upgrade a lot of code in the framework and I cannot afford it.

Perhaps another way is to create a specific XCmsUrlRule to handle the whole dblookup system.

Hmm, we posted our last responses at the same time.

Based on your last response I think you should go by the module approach. Your routes would then be something like /index.php?r=/cms/x23/article

This is not fully correct.

I want to route request not by the name of the controler but by an hashcode which (after dblookup) gives me :

  • the controler to use

  • the IDs needed to expand CMS content

but, and this is the important part, I must keep original Yii behaviours.

In fact, in the CWebApplication::processRequest(), I want all the Yii framework think we are in a standard model.

My main problem is not really about layouts, but about

  • defining the controller in the database

  • feeding the controller with usefull data

  • keep the Yii functionalities intact (actions, filters, …)

This is a small part of the problem, but I already extended CController to create the XCmsController to handle all the data needed to manage the CMS elements

Well, the module do not solve my routing problem. The problem is not really about distinctivness but in mixing classic pages (which pull content) and cms pages (where content is pushed).

This is quite difficult for me to explain it ;-). To let you understand this difficulty, it took me more than a month to explain it to my team (before creating the Prado CMS) to let them understand the “why” (but they where not really convinced). Now, as the CMS is working and as they are using it on a daily basis for more than a year, they perfectly understand it and they don’t want to set up an alternative :-D.

So I have to find an idea to backport my CMS from Prado to Yii without breaking it too much.

My best explanation would be that :

  • using the r routing system (classic)
  • using the c routing system (cmsfriendly)

Thank you for your ideas

:wink: yes response at the same time. please check my longer post. I think I’m more clear in it.

Thank you for the time you are spending on my problem.

I think I’m getting what you want (but like your team used to say: WHY??) :slight_smile:

What you want to do is like running both Windows and Linux on 1 PC. At the same time! That’s not possible.

It is, however, possible to run a windows emulator on Linux.

You’ll have to create this emulator. You’d have to develop a controller (say ‘cms’) and an action (say ‘router’).

When you want your CMS router to kick in, your url would be like: /index?r=/cms/router&c=xxx/yyy

This controller action is where you need to process your cms-specific routing. The ‘router’ action will redirect to the appropriate router based on the ‘c=xxx/yyy’ parameter.

Now if you want to omit the ‘r=/cms/router’ part you have 2 options:

  1. Make the CmsController the default application controller and the ‘router’ action the default CmsController action.

  2. Rewrite the url such that if no explicit route is mentioned in the url (i.e. the ‘r=…’ parameter is omitted) and the ‘c’ parameter is in the url then rewrite to ‘/index.php?r=/cms/router&c=…’.

I don’t know if this can be done using UrlManager or .htaccess, but if you refrase your question like this you might get more response.

Well, about the “why” I don’t know how to explain it (I’ll ask my team to find a better answer ;-)).

I think this is a good solution because it perfectly fits our needs and because the "philosophy" is easily understandable.

Anyway, about your “running windows and linux at the same time”, I think it’s not a really good image.

In Prado, you have several services which can run at the same time :

  • TPageService (classic page) : index.php?page=Register

  • XCmsPageService (content driven pages) : index.php?cms=A1N4

  • TJsonService (json output) : index.php?json=getUserInfo

  • TSoapService (soap output) : index.php?soap=getUserRight

In Prado, it seems obvious to have specialized services because their lifecycle are different, and this is a good way for "optimization".

In Yii, as there is no "complex" or "specific" page management (everything is handled by the developer in the controller) such service separation becomes irrelevant.

In my case, I think that keeping different “services” is still interesting (for example : the url rewriting of our CMS is really complex and we don’t want side effects with classic pages, …).

this is why I want to be able to call :

  • index.php?r=MyController (the controller fetch content according to its design)

  • index.php?c=A1N4 (the content fetch and then execute the controller according to db data)

this way, I should be able to add actions like for classic controllers :

  • index.php?r=MyController/MyAction

  • index.php?c=A1N4/MyAction

Doing it with a central Cms controller : index.php?r=Cms&cmsData=A1N4 is not an option because with such representation, the controller Cms needs external data to function properly and this is strictly different from index.php?c=A1N4.

In the latter, everything is self contained in the route while in the former, the route only is insufficient.

This is why I want to handle two routing systems :

  • index.php?r=MyController(/MyAction)

  • index.php?c=A1N4(/MyAction)

Expecting to be a little bit more clear :wink:

Thank you.

This sounds somewhat similar to a problem I’m facing. Hoping somewhere here can give me some direction. I’ve taken the sample blog application and expanded on it some. What I’d like to be able to do is have muliple blogs/bloggers but I’m not sure how the controller/routes would really work. Currently, if you want to see the listing of posts you would request:

index.php?r=post/list

I’ve turned this into a module so now my route looks like

index.php?r=blog/post/list

All this works fine, but say now I created a top level above posts (table relations) so that I can have multiple blogs. So, if now I create a blog for Sally and one for Jim, ideally I’d like to see a url that resembles:

index.php?r=blog/sally/post/list

index.php?r=blog/jim/post/list

I’m not really sure if this is possible since essentially my parameter (sally or jim) comes after the module but before the view and the action. Also, as I move throught the additional pages in the module (update, create, manage, etc) it looks like I would have to carry this blogger parameter to every page.

Has anybody else run into something similar? I’d be interested in hearing how this could be accomplished. Ultimately, I’m not that concerned about the exact format of the urls but I do wonder what would be the best method to “carry” this parameter throughout the application. Any help would be appreciated.

Regarding the previous post I made, I was able to come up with a solution. I was hoping someone had run into this before and could point me in the right direction, but I haven’t gotten much response. So, I did it anyway.

How?

By making a parameterized Module. This involved three things: extending CController, CWebModule, and adding a function to CWebApplication. This last part bothers me since I am changing a piece of Yii code but I couldn’t figure out how to extend CWebApplication and get my application to use it.

Basically, this is what I did.

  1. I set how many parameters in the URL my module expects



abstract class BaseWebModule extends CWebModule {

  public $numParameters = 0;

  public $routeOverride;

  public $routeParams;

}



  1. Created a function in CWebApplication that strips off the number of parameters,sets the module parameters to these values, and returns the proper route. I used the code from createController since it’s logic did most of the work.



public function getModifiedRoute($route,$owner=null){


  $routeHolder = $route;

  $paramList;


  if($owner===null)

    $owner=$this;  

  if(($route=trim($route,'/'))==='')

    $route=$owner->defaultController;

    	

  $caseSensitive=$this->getUrlManager()->caseSensitive;


  $route.='/';

  while(($pos=strpos($route,'/'))!==false) {

		

    $id=substr($route,0,$pos);

    if(!preg_match('/^\w+$/',$id)){

      return $route;

    }

    if(!$caseSensitive){

      $id=strtolower($id);

    }

				

    $route=(string)substr($route,$pos+1);

			

    if(!isset($basePath)) {

      // first segment

      if(isset($owner->controllerMap[$id])) {

        return $route;

      }


      if(($module=$owner->getModule($id))!==null){

					

        $myobj = $this->createController($route,$module);

					

        if (($pos=strpos($route,'/'))!==false){

          if(isset($module->numParameters)){

            if($module->numParameters>0){

              $tmpRoute = $route;

              $newParam = '';

              for($i=0;$i<$module->numParameters;++$i){

                $newpos=strpos($tmpRoute,'/');

                $newParam = (string)substr($tmpRoute,0,$newpos);


                if (isset($paramList)) {

                  $paramList = $paramList . '/' . $newParam;

                }else{

                  $paramList = $newParam;

                }


                $tmpRoute=(string)substr($tmpRoute,$newpos+1);

             }

             $tmpRoute=substr($tmpRoute,0,strlen($tmpRoute)-1);


             $route=$id . '/' . $tmpRoute;

             $module->routeOverride = $route;

             $module->routeParams = $paramList;

             return $route;

           }

         }

       }

       return $routeHolder;

     }

     $basePath=$owner->getControllerPath();

     $controllerID='';

   }

   else

     $controllerID.='/';

				

	 $className=ucfirst($id).'Controller';

	 $classFile=$basePath.DIRECTORY_SEPARATOR.$className.'.php';

	 if(is_file($classFile)) {

	   return $routeHolder;

	 }

	 $controllerID.=$id;

	 $basePath.=DIRECTORY_SEPARATOR.$id;

   } //--end while	


} 




  1. Slightly modify the runController method in CWebApplication to use my new function to manipulate the route.



public function runController($route) {

  $route = $this->getModifiedRoute($route);

  if(($ca=$this->createController($route))!==null) {

			

  list($controller,$actionID)=$ca;

    $oldController=$this->_controller;

    $this->_controller=$controller;

    $controller->init();

    $controller->run($actionID);

    $this->_controller=$oldController;

  }

  else

    throw new CHttpException(404,Yii::t('yii','Unable to resolve the request "{route}".',

    array('{route}'=>$route===''?$this->defaultController:$route)));

  }



  1. Overrided the createUrl function in my Controller to check to see if these module parameters are set. If so, then modify the route



	public function createUrl($route,$params=array(),$ampersand='&') {


		if($route==='')

			$route=$this->getId().'/'.$this->getAction()->getId();

		else if(strpos($route,'/')===false)

			$route=$this->getId().'/'.$route;

		if($route[0]!=='/' && ($module=$this->getModule())!==null){

    	if (isset($module->routeParams)){

					$route=$module->getId().'/'.$module->routeParams.'/'.$route;

    	} else {

					$route=$module->getId().'/'.$route;

    	}

		}


		return Yii::app()->createUrl(trim($route,'/'),$params,$ampersand);

	}



It actually works well so far. So, now I can have the urls:

index.php?r=blog/sally/post/list

index.php?r=blog/jim/post/list

I know the UrlManager is probably best used for something like this but I had problems with it working in this manner. It also seems that the UrlManager works on all routes, not just a specific module. What I’ve done feels like more of a hack, but it works well. Only, a lot of testing will determine if it works in all situations. Overall there wasn’t much to do as I was only concerned about routes for modules. I’m sure someone really familiar with Yii could implement this better, or do it another way altogether.