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?
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.
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.
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.
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);
}
}
}
}
@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.
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',
)
);
}