Smart actions

Hi

I like to break down my page into modular components, that way I can reuse a component in several spots and when I make a change it is globally applied. This works great as long as I do not change an action signature. But as soon as I need to add another action then I need to go back through the controllers and add in the appropriate action handlers. So my thought to resolve this was to call a static method on each "component" to build my action array for the controller like



public function actions() {


 return array_merge(Control1::actions(),


                   Control2::actions("foo"), // prefix all returned action names with "foo" 


                                      // handles case for 2 callbacks with same action


                   Control3::actions(),


                   array(


            'edit'=>'application.controllers.post.UpdateAction',


                        );


}


I realize it will be performance hit to make these static calls but at least if I change them I wont have to update the controller(s).

What is your thoughts on this ? Should all components support this "simplified" approach

nz

Are Control1, Control2 and Control3 modules that consist of actions?

What is the base class for Control1, 2, 3?

The purpose of using a method like actions(), rules(), attributeLabels() is to allow customization like you did here. For the framework code, we should keep it as simple as possible, but still allow arbitrary customization.

Control1,2,3 would be an extension of CWidget. The same file could contain the "CAction" classes for handling the responses, or they could refer to a CAction class elsewhere. Once the "Control1" is stable I could remove the static call and replace the call with the standard binding.

As with everything it is all about trade-offs. Invoking a static method on a class is way more time costly then building an array arguement - but on the other hand if I know the control supports the “::actions()” then I can add it very quickly to a controller and a view without even having to check the documentation. So my development time is decreased. Because this is a framework I think developers will appreciate the ability to quickly add or remove complex controls in a fast manner - as long as they know they can optimize it later. I agree the methods you list above should not be changed but I am just requesting that controls or extensions that require “actions” simply have this developer-friendly static actions() function call built in. Also these controls should have a standard actionIdPrefix attribute so the callbacks action names can be prefixed in a common manner. What do you think ?

I love the customization the action,rules,attributeLabels methods provide. , I am just trying to figure what would best way to optimize my development process and perhaps this might help some people with their own development process.

nz

I think I don't fully understand you. Could you show a detailed example?

Ill try …



class BGoogleApp extends CWidget {


public static function actions($prefix='') {


  return array(


             ($prefix."pointDrop")=>"BGooglePointDrop",


             ($prefix."center")=>"BGoogleMapCenter",


             ($prefix."zoom")=>"BGoogleMapZoom",


             ($prefix."drawObject")=>"BGoogleMapDrawObject",


 }


}


class BGooglePointDrop extends CAction {}


class BGoogleMapCenter extends CAction {}


class BGoogleMapZoom extends CAction {}


class BGoogleMapDrawObject extends CAction {}


<view>


.....


</view>




public class MyMapper extends CWidget {


public static function actions($prefix='') {


 return array_merge(


                   BGoogleApp::actions("mymapper"), 


                   array(


            $prefix.'edit'=>'EditMapAction',


                        );


}


}


class EditMapAction extends CAction {}


<view>


<com:BGoogleApp actionIdPrefix="mymapper"/>


</view>


 


class CityController extends CController {


public function actions() {


   return array_merge(MyMapper::actions("city"))


}


OR


public function actions() {


'cityedit'=>'EditMapAction',


'mymapperpointDrop'=>'BGooglePointDrop',


'mymappercenter'=>'BGoogleMapCenter',


'mymapperzoom'=>'BGoogleMapZoom',


'mymapperdrawObject'=>'BGoogleMapDrawObject'


}


<view>


<com:MyMapper actionIdPrefix="city"/>


</view>


So lets say BGoogleApp is an extension and then they release a new and improved version with another 4 callbacks, now I need to go through every controller to check to see if the widget is used (or if another widget uses that widget)

Does this help ? Is this what you wanted to see ?

nz

Thanks for explanation. Please confirm my following understanding:

You want to create a self-contained widget that relies on some actions to handle user interactions with the widget. Currently, this would require developers to change controllers' actions() method to manually add these action references. You want this to be done automatically by the framework.

How would you propose to solve this problem?

Yes that is exactly my thoughts.

I dont think the framework should change to solve this issue - The framework is very streamlined and changes of this nature generally tend to be "slow" (the class to handle the action needs to be found etc etc…). I would like to see self contained widgets support a mechanism which allows them to be "wired in" quickly. But at the same time still allow the "extreme speed" approach of the actions function. Thats why I suggested something like a static call that simply merges  the actions together. But maybe there is a better way ?

nz

I did think of one thing but I am not sure if the cost would be worth it - the controller could cache the results of the actions() call and based on the lastupdatedate of the file it would either use or rebuild those values…

Here are some of my thoughts:

  1. A widget usually should not rely on too many actions. If it needs many different kinds of callbacks, you can define a single action and use some GET variable to dispatch sub-action requests.

  2. Using the static method as you proposed still requires developers to configure the actions() method in an explicit way, which does not bring much convenience.

  3. Some widgets need to be "preloaded" like some application components. We probably can think of a way to support this. So if a widget can be preloaded before any action occurs, it can register some actions, which may ultimately solve your problem. However, this is at the cost of bringing extra cost of preloading a widget for every action, even if the widget is not used.

I agree that when you design a widget you want to keep the action count to 1, but if you have multiple widgets (each with there own action) in your widget this is not an option. How about a wildcard match on the actions ? So anything that matches "foo.*" (preg_match) gets dispatched to action "foo" then foo would re-dispatch the event (or handle it) internally. That should allow a level of encapsulation without an enormous amount of effort or speed cost. The only thing that would be required in the design of widgets that do a "callback" is that the action id can be specified (and maybe that attribute name could be more standardized)

nz

I have been thinking about this for some time and still couldn't come up with a perfect solution. I think we will need to keep on exploring all possible solutions and get this done for 1.1 release.

I still don't quite like the static method way for the 3rd reason I mentioned. Another reason is it needs something like a prefix, which doesn't look very nice. But anyway, this is possibly the ultimate solution. Let's keep thinking.

Thoughts this week -

An action list item that points to a "common" action list ? So when identifying the "actions" for the controllers we reference a "common" action array. Each custom widget can be manually added to the common actions file (under config/actions.php). Then the controller identifies that it should use an COMMON_ACTION_SET from the common actions

file: config/actions.php



$common_action_set = 


array("BGoogle"=>array(


             "pointDrop"=>"BGooglePointDrop",


             "center"=>"BGoogleMapCenter",


             "zoom"=>"BGoogleMapZoom",


             "drawObject"=>"BGoogleMapDrawObject"),


       "MyMapper"=>array("editMap"=>"EditMapAction", 


                                  "BGoogle"=>COMMON_ACTION_SET))


Note MyMapper can identify that it recursively uses the action map identified by "BGoogle"

Then the controller can reference any one of the COMMON_ACTION_SET actions by using the identifying constant COMMON_ACTION_SET



class CityController extends CController {


public function actions() {


   return array("MyMapper"=>COMMON_ACTION_SET)


}


}


Then changes only need to be done to the COMMON_ACTION_SET

nz

Slight modification, Reverse the COMMON_ACTION_SET to point to an array (as opposed to having the action set point to the constant). I have also included the modifications to CController to perform this function



$common_action_set=


array("BGoogle"=>array(


             "pointDrop"=>"BGooglePointDrop",


             "center"=>"BGoogleMapCenter",


             "zoom"=>"BGoogleMapZoom",


             "drawObject"=>"BGoogleMapDrawObject"),


       "MyMapper"=>array("editMap"=>"EditMapAction",


                         COMMON_ACTION_SET=>array("BGoogle")));





  public function actions() {


    return array(self::COMMON_ACTION_SET=>array("MyMapper"));


  }


In CController



  const COMMON_ACTION_SET="COMMON_ACTION_SET";


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


		$actionMap=$this->actions();


		


    if (isset($actionMap[self::COMMON_ACTION_SET])) {


      include Yii::getPathOfAlias("application.config.actions").".php";


      $this->processMap($actionMap, $actionMap[self::COMMON_ACTION_SET],$common_action_set);


      unset($actionMap[self::COMMON_ACTION_SET]);


    }


//print_r($actionMap);die("done");


		if(isset($actionMap[$actionID]))


		{


			$c=$actionMap[$actionID];


			if(is_string($c))


			{


				$className=Yii::import($c,true);


				return new $className($this,$actionID);


			}


			else


				return CConfiguration::createObject($c,$this,$actionID);


		}


		return null;


	}


	private function processMap(&$actionMap, $actionsetList,$common_actions) {


    // Check for common actions


    foreach($actionsetList as $actionset) {


      $ca = $common_actions[$actionset];


      $actionMap = array_merge($actionMap,$ca);


      if (isset($ca[self::COMMON_ACTION_SET])) {


        $this->processMap($actionMap,$ca[self::COMMON_ACTION_SET],$common_actions);


      }


    }


	}


Now that the release is out, any thoughts on the technique outlined above qiang ?

Sorry for late reply. I have been debating about this back and forth…

I think your second approach is a bit twisted and unintuitive. Below is an approach I thought that is based on your first approach:



class MyController extends CController


{


	public function actions()


	{


		return array(


			'edit'=>'path.to.EditAction',


			// this will use default action map declared in Widget1


			'path.to.Widget1',  


			// this allows customization of actions in Widget2


			array(


				'path.to.Widget2',


				// the action prefix


				'prefix',


				// customization of 'edit' and 'delete' actions in Widget2


				'edit'=>...


				'delete'=>...


			),


		);


	}


}





class Widget2 extends CWidget


{


	public static function actions()


	{


		// similar as in MyController::actions


	}


}


Using this approach, widgets' static actions methods do not need to be called if an action can be found in the top level of the action map.

What do you think?

Hmm, there’s a problem with the above approach: how does the widget know the action ID prefix?

Not a problem. It can be solved using your approach by setting a property of the widget when it is rendered in the view.

Twisted is my middle name !

I thought one of the nice "Pros" to my approach was that at the worst case it would only have to load one more file. The cons are that building this file could be a bit twisted

Looking at your approach, the actions array can return three types of objects

  1. String (key) , String (value) - Normal action name / action class

  2. Number(key), String (value) - The "Path to Widget" to fetch actions

  3. Number(key), Array(…) (value) - The "Path to widget" with customization

And the last array can be

3a) Number(key), String(value) - The "Path to widget" to fetch actions

3b) Number(key), String(value) - The prefix for the widget actions

3c) String (key) , String (value) - Normal action name / action class

The nice thing about this approach is the action / response is self contained.

The bad thing is that (like you said) there is no way to tie in the prefix to the widget and that (in the worst case scenario) every widget class would have to be loaded in order to determine the proper response for an action.

The prefix thing is not a big issue, the programmer programming the view should simply know that they need to define the widget with the same prefix as defined in the actions.

Personally I am still liking the common action set idea, but I have yet to develop anything significant with it, once I finish with my forum I will see if I can brainstorm any more ideas…

Happy holidays…

z

Using the prefix trick, we can avoid loading all widget classes. Given an action ID, we can check its prefix first before loading a widget class. If the prefix mismatches, the widget class will not be skipped. And that's nearly the best we can do: only when an action ID falls into the action map of a widget, will we load that widget class.

I am not sure if I fully understand your thoughts on the "prefix" mechanism. Can you provide an example or some pseudo code on how this would be done ?

Thanks

nz

The following is an example:



<?php


	return array(


		'action1'=>'Action1',


		'action2'=>'Action2',


		'action3'=>array(


			'class'=>'Action3',


			'name1'=>'value1',


			'name2'=>'value2',


		),


		'Widget1 + w1.',


		'Widget2 + w2.',


		array(


			'Widget2 + w3.',


			'action11'=>array(


				'name'=>'value',


			),


		),


	);


The string 'Widget1 + w1.' means 'Widget1' is the action provider (the widget class), and 'w1.' is the action ID prefix. We have 3 action providers in this example.

Assume action 'w2.action11' is requested. We first check if the ID appears as a normal action ID in actions(). If not, we check every action provider in actions(). For each provider, before loading its class, we check the action ID prefix first. If matching, we load the provider class and so on.