CForm.php 22 KB

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