On Yii and Forms

Hello!

I’ve been using yii for a couple of months (110 days to be exact) and I admire the flexibility the framework provides. I would choose yii any day of the week for it’s flexibility.

One of the aspects where I feel yii deviates from it’s flexible nature is form generation (or creation; where generation refers to automatic generation and creation implies manual creation of forms). As we all know, form generation, validation and handling is a redundant and time-consuming task and yii provides very convenient helpers and tools for this:

a) create them manually : This approach requires you to write the core markup of the form and use CActiveForm or it's derivatives to generate the widgets (which in turn calls CHtml), error messages, etc. Or you can use CHtml directly if no model interaction is required (or perhaps you decide to do that manually). This is by far the most flexible method but promotes redundancy.





b) use CForm, the yii form builder : CForm attempts to reduce redundancy by eliminating the markup from the development process. Thus, the developer simply defines the elements of a form as a multi-dimensional array and the class takes care of the rest (i.e. presentation, data loading and validation, post data presistence and submission handling). This method can be made just as flexible as CActiveForm since the individual components (as defined in the array) can be "broken apart" and displayed individually. This method reduces redudancy in exchange for loss of control over the markup (for e.g. if we want to style ALL the inputs in a certain way, we have two choices: 1) either extend CForm class and override the rendering methods (personally I think this is mind-bending and time-consuming process, I tried it three times before deciding to write my own form builder) 2) Use the form elements individually and change their markup in every view file - again promoting redundancy). 

Let me focus on point a) first, the manual method. Using this method, a form would be created along the lines of:




	// In Controller:

	

	...

	

	public function actionIndex(){

		$model = new MyFormModel; // assuming we have a model called MyFormModel with a few fictional properties, among whom "myfield" is one

		

		if(isset($_POST['MyFormModel'])){

			$model->setAttributes($_POST['MyFormModel']);

			if($model->validate()){

				// do your thing

			}

		}

		

		$this->render('index', array(

			'model' => $model

		));

	}

	

	...

	

	// In View (only barebones shown for brevity):

	

	echo 'Your title';

	$form = $this->beginWidget('CActiveForm' /*, options here as an associative array */);

	echo $form->label($model, 'myfield');

	echo $form->textField($model, 'myfield');

	echo $form->error($model, 'myfield');

	// and so on and so forth

	$this->endWidget();

	

	// Which creates a form like:

	

	<h1>Your title</h1>

	<form id="yw0" action="/debate/proposal/create" method="post">

		<label for="MyFormModel_myfield">Myfield</label>

		<input name="MyFormModel[myfield]" id="MyModel_myfield" type="text" />

	</form>



(For full example, refer to documentation: http://www.yiiframework.com/doc/guide/1.1/en/form.view)

In the above example, we can see that there is a lot of unnecessary redundancy, the $model passed to each of the methods of the $form object could be specified as an option (of course, I believe the reason behind not doing so is to allow for sub-forms to be created).

Furthermore, it should be noted that when the form was created, the form elements are using the name of the model (MyFormModel). Perhaps I’m being too picky but I believe the developer should be able to specify that. Say for example, in the future I want to change the name of MyFormModel to FormModel: I now have to change isset($_POST[‘MyFormModel’]) to isset($_POST[‘FormModel’]) and $model->setAttributes($_POST[‘MyFormModel’]) to $model->setAttributes($_POST[‘FormModel’]); in every single controller which uses this form! Instead if we let the developer choose a generic name independent of the Model we can eliminate this redundancy.

Of course, the Yii developers were aware of this (I assume) and that’s why they created the almighty CForm builder (ok, I exaggerated a little, it won’t make you coffee; you’d have to extend CFormBuilder with CCoffeeBuilder for that)! The CForm builder addresses a few problems mentioned above, namely the redudancy part. Practice predates theory:




	// In Controller

	

	public function actionIndex(){

		$model = new MyModelForm;

		$form = new CForm(array(

			'title' => 'Your title'

			'elements' => array(

				'myfield' => array(

					'type' => 'text'

				)

			),

			'buttons' => array(

				// any buttons you want

			)

		), $model);

		

		if($form->submitted('submit') && $form->validate()){ // of course, I haven't configured a button yet; but let's assume *you* have

			// do your things

		}

		

		$this->render('index', array(

			'form' => $form

		));

	}

	

	// In View

	echo $form; // or echo $form->render() which I prefer, see below

	

	// Which will hopefully produce something along the lines of (depending on your rendering logic):

	<h1>Your title</h1>

	<form id="yw0" action="/debate/proposal/create" method="post">

		<div style="visibility:hidden"><input type="hidden" value="1" name="yform_59c5720d" id="yform_59c5720d" /></div>

		<label for="MyFormModel_myfield">Myfield</label>

		<input name="MyFormModel[myfield]" id="MyModel_myfield" type="text" />

	</form>

	

	// But it can also produce:

	Fatal error: Method CForm::__toString() must not throw an exception in /bla/bla/bla/controllers/MyController.php on line 46i



So, the good things first:

  1. We have effectively decoupled the model from the form submission, we can change the name of the model (in this simple example at least) without affecting the form handling block!

  2. We have separated the concerns of the model with the view. The developer can now focus on the core of the form rather than the crust. Thus, saving time.

  3. The view is a lot cleaner!

  4. If needed, you can access individual elements and customise them as you want!

The bad:

  1. Typecasting the $form object into a string via. the __toString method fails if any of the elements throw an exception. And it seems absurd to block the whole page with a fatal error just for a form! What if it’s a contact form? The solution? As-of-now use $form->render instead of $form (unless you like inflicting pain on yourself - in which case, you should seek help)

  2. If you use a widget (not illustrated here), the widget is not instantiated until render-time. If the widget throws an error at the model (via. $model->addError); the validation function will simply bypass it since validation is done before rendering and by the time the widget sets the error, it’s already too late (it will set the error, BUT the form submission will not be inhibited). You might say: “Oh, but you are doing it wrong; you should not have validation logic in a widget”. But it should be realised that widgets are highly self-contained components and if I have a widget that expresses data in a certain way (say an array); I expect the widget to validate it as well (instead of manually installing validators in each model).

The ugly:

  1. The CForm et al. family of classes are too coupled to the view. Instead of these classes managing the view, they’re generating them! (Although, my views (no pun intended) might be biased since the mode of operation of yii forms assumes each element to be a separate object thus, it seems natural to have them generate their own views. My implication was that in order to, say, change how a checkbox label is displayed, it seems unnecessarily complicated to create a complete class for it.)

And finally, the absurd:

  1. What is the point of the “invisible” div around the hidden input? Aren’t hidden inputs … hidden?

It would be simply ridiculous to point out flaws without a possible solution. As previously mentioned, the frustation caused me to write my own form builder, which in no way is perfect and has serious flaws of it’s own: however, I hope it is a step in the right direction and will benefit everyone. Although I cannot release the code, I can outline the strategies I use.

My implementation of form builder is heavily inspired by CForm (primarily due to compatibility) with a few exceptions:

  1. The constructor can take multiple models at once (The signature is __construct($formStruct, array $models) similar to CForm)

  2. Unlike CForm each element is coupled with a View rather than a class (it could be argued that it this is what CForm does but with classes but this approach makes customising forms much easier).

  3. The attributes of an element is goverened by the view itself, thus the interpretation of an attribute ‘title’ is context dependent (whatever the view interprets is as)

  4. The names of each element can be customised

  5. The views (let’s call it themes) for each element can be customised and mixed-and-matched

  6. Each element can override it’s parents model and it’s attribute (so you can make an attribute called ‘abc’ reference ‘username’; the practical use is unidentified but the feature exists anyways)

  7. The elements are rendered deepest-first

I will now explain how it addresses the problems aforementioned:

The formbuilder takes a similar structure to CForm:




__construct(array( // the outermost array denotes a form

	'name' => 'someForm', // this is the base name of the form; elements will be named someForm[element_name]

	'additional' => 'data', // additional data sent to the form view

	'elements' => array(

		'user' => array( // you can create a group

			'type' => 'group',

			'label' => 'Basic details', // this is interpreted by the view

			'model' => 'user', // refers to the key in the second argument

			'elements' => array(

				'username' => array( // the name of this input will be someForm[username]

					'type' => 'input/text',

					'attributes' => array() // attributes

					// loads into the User model

				),

				'pwd' => array( // name of this widget will be someForm[pwd] but will be loaded into the User::password attribute

					'type' => 'widget',

					'class' => 'PasswordCheckWidget',

					'properties' => array(

						// properties passed to the widget

					)

					// loads into the User model

					'attribute' => 'password'

				),

				'email' => array(

					'type' => 'input/text',

					'name' => 'email', // The name of this widget will be email but will act as someForm[email] loading into the UserInfo::email property

					'model' => 'info' // loads into the UserInfo model

				)

				'submit' => array(

					'type' => 'button/submit',

					'template' => 'weird' // we can specify which template an individual element uses

				)

			)

		)

	)

), array(

	'user' => new User,

	'info' => new UserInfo

));



The models can be accessed in a similar manner to CForm and submission ($form->submitted(‘buttonname’)) and validation ($form->validate()) inherit the same methods.

During construction, a recursive function goes through each of the elements mapping the structure along the way:

model map : maps each of the model and attribute to it’s displayed name

button map : identifes all the buttons and their names

widget map : identifies all the widgets and stores instantiated widgets

The recursive function stores a trail of "ancestors" and a "parent". When it encouters an element without a model it traverses up the ancestors and attempts to identify the closest model. On failure, it uses the first model declared in the models argument of the constructor. Once found it appends the model to the element array for further processing.

In the rendering phase, it recurses through the array again and calls an internal renderer (similar to the one used in yii) which loads the corresponding file from the corresponding "theme" and executes that. Since the scope is preserved, $this inside the template refers to the current form and it has access to all the methods and properties. Along with $this, the ancestors, parent and the element structure itself is passed:




set depth to 0

set ancestors to empty list

set parents to none

function recurse(elements)

	set output to empty string

	foreach elements as element

		if element is array

			set parent to element

			increment depth

			push element into ancestors

			append recurse(elements in element) to element

			decrement depth

			pop element from ancestors

			set output to render element [this is what calls the internal renderer; you may want to process the element further]

			append output to element

		else

			set output to render element

			append output to element

	return output



When a view is called, it is passed all the necessary information and the element structure contains a ‘content’ key which contains the generated content of it’s children. The view can then “echo $element[‘content’]” to show the rendered content of it’s children.

During validation, we make extensive use of the model maps. First we traverse all the models specified in the constructor, then we traverse all the attributes of the model and find their corresponding key in the posted data. When found we load it into the model. Before the model is validated, we emit an event for all the widgets attached to this attribute to validate the value. The model is then validated.

Since the individual views of elements have complete control over their environment, they are in control of what and whether post data is persisted or not.

And that is how it works. The downside of this are the file seeks required for each of the inputs (although I haven’t seen much of a performance hit, the form is generated in fractions of a second) although with proper caching mechanism (currently unimplemented) the performance could be improved.

I rest my case with the hope that such a system is implemented in yii and form generation will be easy and flexible. I would be happy to answer any questions regarding this issue. Any comments or criticisms are welcome as well. I reiterate that I am not implying my approach is the best or the only one but simply laying down my thoughts on how things could be improved and may or may not effect how they are. Finally, I apologise if any errors have been made or I have overlooked some points.

EDIT: Also, to eliminate the page-blocking exception thrown at the __toString method, I catch the exception and display it gracefully. Basically:

__toString(){

try {

$this->render();

} catch(Exception $e){

return YII_DEBUG ? $e->getMessage() . ’ ’ . $e->getLine : ‘Error’;

}

}

I agree in that CForm builder, though being a great tool, lacks some flexibility when it comes to visual customization of the form. I use it every time I can (alias, when I need simple forms). would be nice to see an implementation of your proposal.