Controller override

Because Yii doesn’t allow multiple locations for controllers (ie: it only looks in protected/controllers) i need a way to create controllers and also to extend those controllers so that i can override them.

The idea is that i would have an application and who ever uses it, doesn’t have to modify the base controllers, instead, they will extend the existing ones and override custom functionality.

I thought maybe doing something like the following structure:




--protected

---base-controllers

------BaseSiteController.php

---controllers

------SiteController.php

---models



And the controllers structure would be like:




// BaseSiteController.php

class BaseSiteController extends CController{


     public function actionIndex(){}

     public function actionView(){}

     public function actionSomethingElse(){}


}


// SiteController.php

Yii::import('application.base-controllers.*');

class SiteController extends BaseSiteController{


}



Now as you see, the SiteController is just an empty class, that extends the functionality of BaseSiteController and when the controller will be accesed it will call the parent class implementation.

Overriding an action of the base controller in would be as easy as:




Yii::import('application.base-controllers.*');

class SiteController extends BaseSiteController{




     public function actionIndex(){

         //run here the new index for the site controller.

     }


}



Now this all sounds very good till now, at least for me.

The issue here is that, i don’t want by default to create controllers in the controllers folder for each of the base controllers, basically i just want to have the base-controllers folder populated with the application controllers and in the controllers folder to add controllers only if i need to extend the functionality of one of the base controllers.

The issue is, that Yii will look in the controllers folder and of course that will be empty resulting in 404 errors for the requested actions, and i need a way to avoid that.

Any suggestion on how to do it ?

Any other ideas on how i could accomplish this task in any other way ?

I guess you have to override CWebApplication and change default controller instantiating functions:

public function runController($route)

public function createController($route,$owner=null)

those functions are responsible for instantiating controller class based on parsed route.

You can also experiment with CWebApplication::controllerMap attribute which is an associative array you can configure in main.php. This way you could have all base controllers named with current convention (XXXController) and if you have to extend them, then you provide ExtendedXXXController and map XXXController=>ExtendedXXXController in configuration… Doesn’t sound like automation, but should work out of the box :)

The generated standard Yii application actually put a Controller extends CController class in Controller.php in application/components from which all controllers derive.

@redguy - what you describe makes sense, sort of, but again, too much work and things to keep track of (not for me, but for the people extending those controllers)

@jacmoe - i don’t really understand your point here. Care to elaborate ?

I don’t really get what you are trying to do?

How is your situation different from deriving from a controller in components directory?

No matter how many times I read it, I don’t understand what makes it so different.

Could be because I am tired today, but I doubt that. :P

solution with controllerMap - yes,

but extending CWebApplication is what you are looking for. Override methods instantiating controllers to do what you want - check for extended controller and if not found - load base controller.

@redguy - hmm you are right, createController is the method i need to override in order to achieve this, basically all i have to do is to make that method to look in various places for a controller, not just in /controllers folder.

Maybe also altering the method getControllerPath() so that it can return an array of paths instead of just a single path.

Maybe the two methods can look like:




public function getControllerPath()

{

    if($this->_controllerPath!==null)

        return is_array($this->_controllerPath)?$this->_controllerPath:array($this->_controllerPath);

    else

        return array($this->_controllerPath=$this->getBasePath().DIRECTORY_SEPARATOR.'controllers');

}




public function createController($route,$owner=null)

{

    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 null;

        if(!$caseSensitive)

            $id=strtolower($id);

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

        if(!isset($basePath))  // first segment

        {

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

            {

                return array(

                    Yii::createComponent($owner->controllerMap[$id],$id,$owner===$this?null:$owner),

                    $this->parseActionParams($route),

                );

            }


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

                return $this->createController($route,$module);


            $basePath=$owner->getControllerPath();

            $controllerID='';

        }

        else

            $controllerID.='/';

		if(!is_array($basePath))

			$basePath=array($basePath);	

		foreach($basePath AS $basePathFile)

		{

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

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

			if(is_file($classFile))

			{

				if(!class_exists($className,false))

					require($classFile);

				if(class_exists($className,false) && is_subclass_of($className,'CController'))

				{

					$id[0]=strtolower($id[0]);

					return array(

						new $className($controllerID.$id,$owner===$this?null:$owner),

						$this->parseActionParams($route),

					);

				}

				return null;

			}

			$controllerID.=$id;

			$basePath.=DIRECTORY_SEPARATOR.$id;	

		}

		

    }

}



And in the application config one could just pass the controllerPath like:




'controllerPath'=>array(

   dirname(__FILE__).'../controllers',

   dirname(__FILE__).'../base-controllers',

),



And in theory it will first look in controllers and if won’t find it there it will fallback to the base one.

Makes sense ?

Looks like you are looking for mixins ?

I searched for ‘mixins in php’ and chose ‘last year’ as criteria in Google.

I think that’s better than me throwing the links at you. :)

I feel that’s what you want: extend existing classes without touching the base classes.

You can also call it ‘traits’. ;)

@jacmoe

I took a look at traits in php manual, and also i found a good explanation here:

http://simas.posterous.com/new-to-php-54-traits

I can see the advantages of using them, but still i can’t see how these should help me in this situation(moreover traits are php >= 5.4 thus not implemented in framework yet)

My problem is not multiple inheritance, but what i need(i think) is a controllers fallback system, like the framework providing me a way of looking into multiple locations to find a controller, not just in the controllers folder.

So blind sometimes…

I can solve this issue by using the controller action feature like stated here: http://www.yiiframework.com/doc/guide/1.1/en/basics.controller#action

Also, it can be solved by defining a bootstrap component that at init will populate the controllerMap property of the application to include the needed controllers(base or custom ones).