Actions by Behavioring

Is it possible to create a Behavior with some actions, and implement that Behavior in a Controller, using those actions transparently?

Example:


<?php

class ExampleBehavior extends CBehavior {


public function actionTesting() {


}


}


class CodeController extends CController {


[...]


public function behaviors() {

return array('test' => array('class'=>'ext.behaviors.ExampleBehavior'));

}


}

?>

Calling mysite.com/code/testing would say "The system is unable to find the requested action "testing".

I know the behavioring is successful because I can create an action inside the Controller and call actionTesting() inside it.

But is it possible to call the behavior’s actions transparently? Or it’s just an unexpected behavior from yii code? A bug?

Is there a "recommended" workaround?

I would like to know this as well. Haven’t been able to get a behavior to serve actions in a controller transparently.

If it’s not possible, is there a technical reason for that, or has it just not been done yet?

Thank you!

No this is not possible yet because CController::createAction() checks if "actionExample" is a valid method for the given controller. Behavior methods are no real methods within a controller since they get called via magic __get().

For now, as workaround I guess you can override CController::createAction() or CController::missingAction() in some way to get it work.

But I agree, would be nice enhancement.

I have the feeling I’m missing something obvious here since I haven’t found much questioning about this feature. But anyway I filed a ticket #1465. Vote for it if you’d like to see it happen :)

Did you try to override actions() in the behavior? You would have to create action classes with your behavior for this to work. But at least you can inject actions.

You should also merge $this->owner->actions() with the behaviors action to make behavoir actions() and controller actions() coexist nicely.

Not tested.

I don’t think this will work. Behavior methods won’t override any methods from the controller.

You’re right - partially. ;)

I forgot that CComponent::__call() is only used, if the called method isn’t found. So this method will only work, if the controller doesn’t already have a action() method.

Maybe using a behavior for adding actions to a controller is a bad idea. We already have the actions() method to import actions from outside. So better use this mechanism if you want to create reusable actions.

For now it won’t work at all since CController checks for valid method in controller instance (no magic involved there). Or do you mean the actions() method? This won’t work as well since CController returns empty array by default. Means actions() in a behavior won’t have any effect.

Not sure yet if/how I would use it in a project, but I’m pretty sure there is some use for it. At least it makes sense to implement it I think.

How right you are :). I forgot the default implementation of actions() in CController.

I don’t think how I could help to improve this discussion, I’m just thinking it’s being very productive (:

BTW, I can give you an example on how I would use this (I’m using the missingAction() tip rawtaz gave me at IRC and Y!! here)

I created a controller behavior to make it easier to upload videos to YouTube.

In fact, it have some internal methods that calls some Zend Classes and then an actionFormData() to retrieve POST URL and Token from YouTube.

This action is used in an AJAX request before the form submission, and would simply print a JSON.

Why not develop an action class instead of a behavior?

“Because I didn’t think about that” would be a nice answer? haha

I’ll consider your somewhat OBVIOUS idea and try to implement it this night (: (brazilian guy here, 11AM now)

This is a modification of createAction to provide possibility of attaching several actions using behaviors. But there is one inconvenience - you should use $this->owner to operate with controller object.




public function createAction($actionID)

{

   if($actionID==='') $actionID=$this->defaultAction;

   if(method_exists($this,'action'.$actionID) && strcasecmp($actionID,'s')) // we have actions method

   {

       return new CInlineAction($this,$actionID);

   }

   elseif ($a = $this->createActionFromMap($this->actions(),$actionID,$actionID))

   {

        return $a;

   }

   elseif (($behaviorList = $this->behaviors()) && is_array($behaviorList))

   {	

        foreach ($behaviorList as $behaviorId=>$data)

        {

             if(is_object($behaviorObj = $this->asa($behaviorId)) &&  

method_exists($behaviorObj,'action'.$actionID) && strcasecmp($actionID,'s')) // we have actions method

             {

                 return new CInlineAction($behaviorObj, $actionID);

             }

         }

    }

}	




i found that the solution i provided is useless :) you guys, should use modules instead of actions by behavioring

@Mimin your solution worked perfectly. What was the problem? Also, I don’t understand how modules does something comparable to having a behavior with multiple actions?

so unless your method breaks other stuff, there’s nothing wrong with it. It makes perfect sense for a scenario where you have a bunch of related actions, perhaps for ajax purposes all for one page. You can easily port the behavior to a new project and have all the ajax stuff running instantaneously just by adding this behavior. that’s what im doing, and it’s awesome! thanks bro.

@FaceySpacey you’re welcome! Actually, i can’t remember now what made myself confused about this solution. lol :) But it doesn’t break other stuff definitely.

Using action classes is fine (and I’ve done so a number of times) when it’s about one/the occasional action, but if you are making for example an extension that provides a number of actions actions (possibly in combination with other stuff like a widget needing to call home to the actions you want to provide here), then it’s way cleaner for the user of the extension to add one little behavior instead of a list of action classes, to the controller. That is the main reason I want actions in behaviors, for extensions (base controller classes are not a good option here, for obvious reasons).

When a request comes in, the controller checks for an action method on itself. If none is found it uses the action class map to find an action. Adding to that a third check for action methods in the controller’s behaviors, that runs after the two aforementioned checks, shouldn’t add a notable overhead except for when the behavior should actually be used to handle the request (in which case it should be fine). Right, or am I missing something obvious in that reasoning?

Apart from that I guess there’s a slight overhead when the behavior is added to the controller, but seriously that cannot be much, can it? In worst case, if someone has such extreme performance requirements that they cannot afford adding a behavior to a controller with the overhead of having it initialized, then they can simply use action classes instead.

Thanks for listening!

I think it was done in one of extensions. If I remember correctly it was http://www.yiiframework.com/extension/ejnestedtreeactions

Why not develop a widget class and use it as an action provider? see this wiki for more detail.

I found what Mimin posted to be pretty useful. Here’s an easy version to plug into your code.

Just have your controller extend BehaviorProxyController, and then either override behaviors() or use attachBehavior() to add actions to the controller.




<?php

class BehaviorProxyController extends Controller

{

	private $_behaviorIDs = array();


	public function createAction($actionID)

	{

		$action = parent::createAction($actionID);

		if($action !== null)

			return $action;

		foreach($this->_behaviorIDs as $behaviorID)

		{

			$object = $this->asa($behaviorID);

			if($object->getEnabled() && method_exists($object,'action'.$actionID))

				return new CInlineAction($object,$actionID);

		}

	}


	public function attachBehavior($name, $behavior)

	{

		$this->_behaviorIDs[] = $name;

		parent::attachBehavior($name, $behavior);

	}

}






public function init()

{

	$this->attachBehavior('SomeControllerBehavior', array(

		'class'=>'SomeControllerBehavior',

	));

	return parent::init();

}


OR


public function behaviors()

{

	return array(

		'SomeControllerBehavior'=>array(

			'class'=>'SomeControllerBehavior',

		)

	);

}