1: <?php
2: /**
3: * CForm class file.
4: *
5: * @author Qiang Xue <qiang.xue@gmail.com>
6: * @link http://www.yiiframework.com/
7: * @copyright 2008-2013 Yii Software LLC
8: * @license http://www.yiiframework.com/license/
9: */
10:
11: /**
12: * CForm represents a form object that contains form input specifications.
13: *
14: * The main purpose of introducing the abstraction of form objects is to enhance the
15: * reusability of forms. In particular, we can divide a form in two parts: those
16: * that specify each individual form inputs, and those that decorate the form inputs.
17: * A CForm object represents the former part. It relies on the rendering process to
18: * accomplish form input decoration. Reusability is mainly achieved in the rendering process.
19: * That is, a rendering process can be reused to render different CForm objects.
20: *
21: * A form can be rendered in different ways. One can call the {@link render} method
22: * to get a quick form rendering without writing any HTML code; one can also override
23: * {@link render} to render the form in a different layout; and one can use an external
24: * view template to render each form element explicitly. In these ways, the {@link render}
25: * method can be applied to all kinds of forms and thus achieves maximum reusability;
26: * while the external view template keeps maximum flexibility in rendering complex forms.
27: *
28: * Form input specifications are organized in terms of a form element hierarchy.
29: * At the root of the hierarchy, it is the root CForm object. The root form object maintains
30: * its children in two collections: {@link elements} and {@link buttons}.
31: * The former contains non-button form elements ({@link CFormStringElement},
32: * {@link CFormInputElement} and CForm); while the latter mainly contains
33: * button elements ({@link CFormButtonElement}). When a CForm object is embedded in the
34: * {@link elements} collection, it is called a sub-form which can have its own {@link elements}
35: * and {@link buttons} collections and thus form the whole form hierarchy.
36: *
37: * Sub-forms are mainly used to handle multiple models. For example, in a user
38: * registration form, we can have the root form to collect input for the user
39: * table while a sub-form to collect input for the profile table. Sub-form is also
40: * a good way to partition a lengthy form into shorter ones, even though all inputs
41: * may belong to the same model.
42: *
43: * Form input specifications are given in terms of a configuration array which is
44: * used to initialize the property values of a CForm object. The {@link elements} and
45: * {@link buttons} properties need special attention as they are the main properties
46: * to be configured. To configure {@link elements}, we should give it an array like
47: * the following:
48: * <pre>
49: * 'elements'=>array(
50: * 'username'=>array('type'=>'text', 'maxlength'=>80),
51: * 'password'=>array('type'=>'password', 'maxlength'=>80),
52: * )
53: * </pre>
54: * The above code specifies two input elements: 'username' and 'password'. Note the model
55: * object must have exactly the same attributes 'username' and 'password'. Each element
56: * has a type which specifies what kind of input should be used. The rest of the array elements
57: * (e.g. 'maxlength') in an input specification are rendered as HTML element attributes
58: * when the input field is rendered. The {@link buttons} property is configured similarly.
59: *
60: * If you're going to use AJAX and/or client form validation with the enabled error summary
61: * you have to set {@link $showErrors} property to true. Please refer to it's documentation
62: * for more details.
63: *
64: * For more details about configuring form elements, please refer to {@link CFormInputElement}
65: * and {@link CFormButtonElement}.
66: *
67: * @property CForm $root The top-level form object.
68: * @property CActiveForm $activeFormWidget The active form widget associated with this form.
69: * This method will return the active form widget as specified by {@link activeForm}.
70: * @property CBaseController $owner The owner of this form. This refers to either a controller or a widget
71: * by which the form is created and rendered.
72: * @property CModel $model The model associated with this form. If this form does not have a model,
73: * it will look for a model in its ancestors.
74: * @property array $models The models that are associated with this form or its sub-forms.
75: * @property CFormElementCollection $elements The form elements.
76: * @property CFormElementCollection $buttons The form elements.
77: *
78: * @author Qiang Xue <qiang.xue@gmail.com>
79: * @package system.web.form
80: * @since 1.1
81: */
82: class CForm extends CFormElement implements ArrayAccess
83: {
84: /**
85: * @var string the title for this form. By default, if this is set, a fieldset may be rendered
86: * around the form body using the title as its legend. Defaults to null.
87: */
88: public $title;
89: /**
90: * @var string the description of this form.
91: */
92: public $description;
93: /**
94: * @var string the submission method of this form. Defaults to 'post'.
95: * This property is ignored when this form is a sub-form.
96: */
97: public $method='post';
98: /**
99: * @var mixed the form action URL (see {@link CHtml::normalizeUrl} for details about this parameter.)
100: * Defaults to an empty string, meaning the current request URL.
101: * This property is ignored when this form is a sub-form.
102: */
103: public $action='';
104: /**
105: * @var string the name of the class for representing a form input element. Defaults to 'CFormInputElement'.
106: */
107: public $inputElementClass='CFormInputElement';
108: /**
109: * @var string the name of the class for representing a form button element. Defaults to 'CFormButtonElement'.
110: */
111: public $buttonElementClass='CFormButtonElement';
112: /**
113: * @var array HTML attribute values for the form tag. When the form is embedded within another form,
114: * this property will be used to render the HTML attribute values for the fieldset enclosing the child form.
115: */
116: public $attributes=array();
117: /**
118: * @var boolean whether to show error summary. Defaults to false.
119: */
120: public $showErrorSummary=false;
121: /**
122: * @var boolean|null whether error elements of the form attributes should be rendered. There are three possible
123: * valid values: null, true and false.
124: *
125: * Defaults to null meaning that {@link $showErrorSummary} will be used as value. This is done mainly to keep
126: * backward compatibility with existing applications. If you want to use error summary with AJAX and/or client
127: * validation you have to set this property to true (recall that {@link CActiveForm::error()} should be called
128: * for each attribute that is going to be AJAX and/or client validated).
129: *
130: * False value means that the error elements of the form attributes shall not be displayed. True value means that
131: * the error elements of the form attributes will be rendered.
132: *
133: * @since 1.1.14
134: */
135: public $showErrors;
136: /**
137: * @var string|null HTML code to prepend to the list of errors in the error summary. See {@link CActiveForm::errorSummary()}.
138: */
139: public $errorSummaryHeader;
140: /**
141: * @var string|null HTML code to append to the list of errors in the error summary. See {@link CActiveForm::errorSummary()}.
142: */
143: public $errorSummaryFooter;
144: /**
145: * @var array the configuration used to create the active form widget.
146: * The widget will be used to render the form tag and the error messages.
147: * The 'class' option is required, which specifies the class of the widget.
148: * The rest of the options will be passed to {@link CBaseController::beginWidget()} call.
149: * Defaults to array('class'=>'CActiveForm').
150: * @since 1.1.1
151: */
152: public $activeForm=array('class'=>'CActiveForm');
153:
154: private $_model;
155: private $_elements;
156: private $_buttons;
157: private $_activeForm;
158:
159: /**
160: * Constructor.
161: * If you override this method, make sure you do not modify the method
162: * signature, and also make sure you call the parent implementation.
163: * @param mixed $config the configuration for this form. It can be a configuration array
164: * or the path alias of a PHP script file that returns a configuration array.
165: * The configuration array consists of name-value pairs that are used to initialize
166: * the properties of this form.
167: * @param CModel $model the model object associated with this form. If it is null,
168: * the parent's model will be used instead.
169: * @param mixed $parent the direct parent of this form. This could be either a {@link CBaseController}
170: * object (a controller or a widget), or a {@link CForm} object.
171: * If the former, it means the form is a top-level form; if the latter, it means this form is a sub-form.
172: */
173: public function __construct($config,$model=null,$parent=null)
174: {
175: $this->setModel($model);
176: if($parent===null)
177: $parent=Yii::app()->getController();
178: parent::__construct($config,$parent);
179: if($this->showErrors===null)
180: $this->showErrors=!$this->showErrorSummary;
181: $this->init();
182: }
183:
184: /**
185: * Initializes this form.
186: * This method is invoked at the end of the constructor.
187: * You may override this method to provide customized initialization (such as
188: * configuring the form object).
189: */
190: protected function init()
191: {
192: }
193:
194: /**
195: * Returns a value indicating whether this form is submitted.
196: * @param string $buttonName the name of the submit button
197: * @param boolean $loadData whether to call {@link loadData} if the form is submitted so that
198: * the submitted data can be populated to the associated models.
199: * @return boolean whether this form is submitted.
200: * @see loadData
201: */
202: public function submitted($buttonName='submit',$loadData=true)
203: {
204: $ret=$this->clicked($this->getUniqueId()) && $this->clicked($buttonName);
205: if($ret && $loadData)
206: $this->loadData();
207: return $ret;
208: }
209:
210: /**
211: * Returns a value indicating whether the specified button is clicked.
212: * @param string $name the button name
213: * @return boolean whether the button is clicked.
214: */
215: public function clicked($name)
216: {
217: if(strcasecmp($this->getRoot()->method,'get'))
218: return isset($_POST[$name]);
219: else
220: return isset($_GET[$name]);
221: }
222:
223: /**
224: * Validates the models associated with this form.
225: * All models, including those associated with sub-forms, will perform
226: * the validation. You may use {@link CModel::getErrors()} to retrieve the validation
227: * error messages.
228: * @return boolean whether all models are valid
229: */
230: public function validate()
231: {
232: $ret=true;
233: foreach($this->getModels() as $model)
234: $ret=$model->validate() && $ret;
235: return $ret;
236: }
237:
238: /**
239: * Loads the submitted data into the associated model(s) to the form.
240: * This method will go through all models associated with this form and its sub-forms
241: * and massively assign the submitted data to the models.
242: * @see submitted
243: */
244: public function loadData()
245: {
246: if($this->_model!==null)
247: {
248: $class=CHtml::modelName($this->_model);
249: if(strcasecmp($this->getRoot()->method,'get'))
250: {
251: if(isset($_POST[$class]))
252: $this->_model->setAttributes($_POST[$class]);
253: }
254: elseif(isset($_GET[$class]))
255: $this->_model->setAttributes($_GET[$class]);
256: }
257: foreach($this->getElements() as $element)
258: {
259: if($element instanceof self)
260: $element->loadData();
261: }
262: }
263:
264: /**
265: * @return CForm the top-level form object
266: */
267: public function getRoot()
268: {
269: $root=$this;
270: while($root->getParent() instanceof self)
271: $root=$root->getParent();
272: return $root;
273: }
274:
275: /**
276: * @return CActiveForm the active form widget associated with this form.
277: * This method will return the active form widget as specified by {@link activeForm}.
278: * @since 1.1.1
279: */
280: public function getActiveFormWidget()
281: {
282: if($this->_activeForm!==null)
283: return $this->_activeForm;
284: else
285: return $this->getRoot()->_activeForm;
286: }
287:
288: /**
289: * @return CBaseController the owner of this form. This refers to either a controller or a widget
290: * by which the form is created and rendered.
291: */
292: public function getOwner()
293: {
294: $owner=$this->getParent();
295: while($owner instanceof self)
296: $owner=$owner->getParent();
297: return $owner;
298: }
299:
300: /**
301: * Returns the model that this form is associated with.
302: * @param boolean $checkParent whether to return parent's model if this form doesn't have model by itself.
303: * @return CModel the model associated with this form. If this form does not have a model,
304: * it will look for a model in its ancestors.
305: */
306: public function getModel($checkParent=true)
307: {
308: if(!$checkParent)
309: return $this->_model;
310: $form=$this;
311: while($form->_model===null && $form->getParent() instanceof self)
312: $form=$form->getParent();
313: return $form->_model;
314: }
315:
316: /**
317: * @param CModel $model the model to be associated with this form
318: */
319: public function setModel($model)
320: {
321: $this->_model=$model;
322: }
323:
324: /**
325: * Returns all models that are associated with this form or its sub-forms.
326: * @return array the models that are associated with this form or its sub-forms.
327: */
328: public function getModels()
329: {
330: $models=array();
331: if($this->_model!==null)
332: $models[]=$this->_model;
333: foreach($this->getElements() as $element)
334: {
335: if($element instanceof self)
336: $models=array_merge($models,$element->getModels());
337: }
338: return $models;
339: }
340:
341: /**
342: * Returns the input elements of this form.
343: * This includes text strings, input elements and sub-forms.
344: * Note that the returned result is a {@link CFormElementCollection} object, which
345: * means you can use it like an array. For more details, see {@link CMap}.
346: * @return CFormElementCollection the form elements.
347: */
348: public function getElements()
349: {
350: if($this->_elements===null)
351: $this->_elements=new CFormElementCollection($this,false);
352: return $this->_elements;
353: }
354:
355: /**
356: * Configures the input elements of this form.
357: * The configuration must be an array of input configuration array indexed by input name.
358: * Each input configuration array consists of name-value pairs that are used to initialize
359: * a {@link CFormStringElement} object (when 'type' is 'string'), a {@link CFormElement} object
360: * (when 'type' is a string ending with 'Form'), or a {@link CFormInputElement} object in
361: * all other cases.
362: * @param array $elements the elements configurations
363: */
364: public function setElements($elements)
365: {
366: $collection=$this->getElements();
367: foreach($elements as $name=>$config)
368: $collection->add($name,$config);
369: }
370:
371: /**
372: * Returns the button elements of this form.
373: * Note that the returned result is a {@link CFormElementCollection} object, which
374: * means you can use it like an array. For more details, see {@link CMap}.
375: * @return CFormElementCollection the form elements.
376: */
377: public function getButtons()
378: {
379: if($this->_buttons===null)
380: $this->_buttons=new CFormElementCollection($this,true);
381: return $this->_buttons;
382: }
383:
384: /**
385: * Configures the buttons of this form.
386: * The configuration must be an array of button configuration array indexed by button name.
387: * Each button configuration array consists of name-value pairs that are used to initialize
388: * a {@link CFormButtonElement} object.
389: * @param array $buttons the button configurations
390: */
391: public function setButtons($buttons)
392: {
393: $collection=$this->getButtons();
394: foreach($buttons as $name=>$config)
395: $collection->add($name,$config);
396: }
397:
398: /**
399: * Renders the form.
400: * The default implementation simply calls {@link renderBegin}, {@link renderBody} and {@link renderEnd}.
401: * @return string the rendering result
402: */
403: public function render()
404: {
405: return $this->renderBegin() . $this->renderBody() . $this->renderEnd();
406: }
407:
408: /**
409: * Renders the open tag of the form.
410: * The default implementation will render the open form tag.
411: * @return string the rendering result
412: */
413: public function renderBegin()
414: {
415: if($this->getParent() instanceof self)
416: return '';
417: else
418: {
419: $options=$this->activeForm;
420: if(isset($options['class']))
421: {
422: $class=$options['class'];
423: unset($options['class']);
424: }
425: else
426: $class='CActiveForm';
427: $options['action']=$this->action;
428: $options['method']=$this->method;
429: if(isset($options['htmlOptions']))
430: {
431: foreach($this->attributes as $name=>$value)
432: $options['htmlOptions'][$name]=$value;
433: }
434: else
435: $options['htmlOptions']=$this->attributes;
436: ob_start();
437: $this->_activeForm=$this->getOwner()->beginWidget($class, $options);
438: return ob_get_clean() . "<div style=\"visibility:hidden\">".CHtml::hiddenField($this->getUniqueID(),1)."</div>\n";
439: }
440: }
441:
442: /**
443: * Renders the close tag of the form.
444: * @return string the rendering result
445: */
446: public function renderEnd()
447: {
448: if($this->getParent() instanceof self)
449: return '';
450: else
451: {
452: ob_start();
453: $this->getOwner()->endWidget();
454: return ob_get_clean();
455: }
456: }
457:
458: /**
459: * Renders the body content of this form.
460: * This method mainly renders {@link elements} and {@link buttons}.
461: * If {@link title} or {@link description} is specified, they will be rendered as well.
462: * And if the associated model contains error, the error summary may also be displayed.
463: * The form tag will not be rendered. Please call {@link renderBegin} and {@link renderEnd}
464: * to render the open and close tags of the form.
465: * You may override this method to customize the rendering of the form.
466: * @return string the rendering result
467: */
468: public function renderBody()
469: {
470: $output='';
471: if($this->title!==null)
472: {
473: if($this->getParent() instanceof self)
474: {
475: $attributes=$this->attributes;
476: unset($attributes['name'],$attributes['type']);
477: $output=CHtml::openTag('fieldset', $attributes)."<legend>".$this->title."</legend>\n";
478: }
479: else
480: $output="<fieldset>\n<legend>".$this->title."</legend>\n";
481: }
482:
483: if($this->description!==null)
484: $output.="<div class=\"description\">\n".$this->description."</div>\n";
485:
486: if($this->showErrorSummary && ($model=$this->getModel(false))!==null)
487: $output.=$this->getActiveFormWidget()->errorSummary($model,$this->errorSummaryHeader,$this->errorSummaryFooter)."\n";
488:
489: $output.=$this->renderElements()."\n".$this->renderButtons()."\n";
490:
491: if($this->title!==null)
492: $output.="</fieldset>\n";
493:
494: return $output;
495: }
496:
497: /**
498: * Renders the {@link elements} in this form.
499: * @return string the rendering result
500: */
501: public function renderElements()
502: {
503: $output='';
504: foreach($this->getElements() as $element)
505: $output.=$this->renderElement($element);
506: return $output;
507: }
508:
509: /**
510: * Renders the {@link buttons} in this form.
511: * @return string the rendering result
512: */
513: public function renderButtons()
514: {
515: $output='';
516: foreach($this->getButtons() as $button)
517: $output.=$this->renderElement($button);
518: return $output!=='' ? "<div class=\"row buttons\">".$output."</div>\n" : '';
519: }
520:
521: /**
522: * Renders a single element which could be an input element, a sub-form, a string, or a button.
523: * @param mixed $element the form element to be rendered. This can be either a {@link CFormElement} instance
524: * or a string representing the name of the form element.
525: * @return string the rendering result
526: */
527: public function renderElement($element)
528: {
529: if(is_string($element))
530: {
531: if(($e=$this[$element])===null && ($e=$this->getButtons()->itemAt($element))===null)
532: return $element;
533: else
534: $element=$e;
535: }
536: if($element->getVisible())
537: {
538: if($element instanceof CFormInputElement)
539: {
540: if($element->type==='hidden')
541: return "<div style=\"visibility:hidden\">\n".$element->render()."</div>\n";
542: else
543: return "<div class=\"row field_{$element->name}\">\n".$element->render()."</div>\n";
544: }
545: elseif($element instanceof CFormButtonElement)
546: return $element->render()."\n";
547: else
548: return $element->render();
549: }
550: return '';
551: }
552:
553: /**
554: * This method is called after an element is added to the element collection.
555: * @param string $name the name of the element
556: * @param CFormElement $element the element that is added
557: * @param boolean $forButtons whether the element is added to the {@link buttons} collection.
558: * If false, it means the element is added to the {@link elements} collection.
559: */
560: public function addedElement($name,$element,$forButtons)
561: {
562: }
563:
564: /**
565: * This method is called after an element is removed from the element collection.
566: * @param string $name the name of the element
567: * @param CFormElement $element the element that is removed
568: * @param boolean $forButtons whether the element is removed from the {@link buttons} collection
569: * If false, it means the element is removed from the {@link elements} collection.
570: */
571: public function removedElement($name,$element,$forButtons)
572: {
573: }
574:
575: /**
576: * Evaluates the visibility of this form.
577: * This method will check the visibility of the {@link elements}.
578: * If any one of them is visible, the form is considered as visible. Otherwise, it is invisible.
579: * @return boolean whether this form is visible.
580: */
581: protected function evaluateVisible()
582: {
583: foreach($this->getElements() as $element)
584: if($element->getVisible())
585: return true;
586: return false;
587: }
588:
589: /**
590: * Returns a unique ID that identifies this form in the current page.
591: * @return string the unique ID identifying this form
592: */
593: protected function getUniqueId()
594: {
595: if(isset($this->attributes['id']))
596: return 'yform_'.$this->attributes['id'];
597: else
598: return 'yform_'.sprintf('%x',crc32(serialize(array_keys($this->getElements()->toArray()))));
599: }
600:
601: /**
602: * Returns whether there is an element at the specified offset.
603: * This method is required by the interface ArrayAccess.
604: * @param mixed $offset the offset to check on
605: * @return boolean
606: */
607: public function offsetExists($offset)
608: {
609: return $this->getElements()->contains($offset);
610: }
611:
612: /**
613: * Returns the element at the specified offset.
614: * This method is required by the interface ArrayAccess.
615: * @param integer $offset the offset to retrieve element.
616: * @return mixed the element at the offset, null if no element is found at the offset
617: */
618: public function offsetGet($offset)
619: {
620: return $this->getElements()->itemAt($offset);
621: }
622:
623: /**
624: * Sets the element at the specified offset.
625: * This method is required by the interface ArrayAccess.
626: * @param integer $offset the offset to set element
627: * @param mixed $item the element value
628: */
629: public function offsetSet($offset,$item)
630: {
631: $this->getElements()->add($offset,$item);
632: }
633:
634: /**
635: * Unsets the element at the specified offset.
636: * This method is required by the interface ArrayAccess.
637: * @param mixed $offset the offset to unset element
638: */
639: public function offsetUnset($offset)
640: {
641: $this->getElements()->remove($offset);
642: }
643: }
644: