PageRenderTime 38ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/lithium/template/helper/Form.php

https://github.com/prestes/chegamos
PHP | 569 lines | 252 code | 43 blank | 274 comment | 28 complexity | 634213d9b315cd38afba8ad7c8e98e06 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\template\helper;
  9. use \lithium\util\Set;
  10. use \lithium\util\Inflector;
  11. use \UnexpectedValueException;
  12. /**
  13. * A helper class to facilitate generating, processing and securing HTML forms. By default, `Form`
  14. * will simply generate HTML forms and widgets, but by creating a form with a _binding object_,
  15. * the helper can pre-fill form input values, render error messages, and introspect column types.
  16. *
  17. * For example, assuming you have created a `Post` model in your application:
  18. * {{{// In controller code:
  19. * use \app\models\Post;
  20. * $post = Post::find(1);
  21. * return compact('post');
  22. *
  23. * // In view code:
  24. * <?=$this->form->create($post); // Echoes a <form> tag and binds the helper to $post ?>
  25. * <?=$this->form->text('title'); // Echoes an <input /> element, pre-filled with $post's title ?>
  26. * <?=$this->form->submit('Update'); // Echoes a submit button with the title 'Update' ?>
  27. * <?=$this->form->end(); // Echoes a </form> tag & unbinds the form ?>
  28. * }}}
  29. */
  30. class Form extends \lithium\template\Helper {
  31. /**
  32. * String templates used by this helper.
  33. *
  34. * @var array
  35. */
  36. protected $_strings = array(
  37. 'button' => '<input type="{:type}"{:options} />',
  38. 'checkbox' => '<input type="checkbox" name="{:name}"{:options} />',
  39. 'checkbox-multi' => '<input type="checkbox" name="{:name}[]"{:options} />',
  40. 'checkbox-multi-end' => '',
  41. 'checkbox-multi-start' => '',
  42. 'error' => '<div{:options}>{:content}</div>',
  43. 'errors' => '{:content}',
  44. 'element' => '<input type="{:type}" name="{:name}"{:options} />',
  45. 'file' => '<input type="file" name="{:name}"{:options} />',
  46. 'form' => '<form action="{:url}"{:options}>{:append}',
  47. 'form-end' => '</form>',
  48. 'hidden' => '<input type="hidden" name="{:name}"{:options} />',
  49. 'field' => '<div{:wrap}>{:label}{:input}{:error}</div>',
  50. 'field-checkbox' => '<div{:wrap}>{:input}{:label}{:error}</div>',
  51. 'label' => '<label for="{:name}"{:options}>{:title}</label>',
  52. 'legend' => '<legend>{:content}</legend>',
  53. 'option-group' => '<optgroup label="{:label}"{:options}>',
  54. 'option-group-end' => '</optgroup>',
  55. 'password' => '<input type="password" name="{:name}"{:options} />',
  56. 'radio' => '<input type="radio" name="{:name}" id="{:id}"{:options} />{:label}',
  57. 'select-start' => '<select name="{:name}"{:options}>',
  58. 'select-multi-start' => '<select name="{:name}[]"{:options}>',
  59. 'select-empty' => '<option value=""{:options}>&nbsp;</option>',
  60. 'select-option' => '<option value="{:value}"{:options}>{:title}</option>',
  61. 'select-end' => '</select>',
  62. 'submit' => '<input type="submit" value="{:title}"{:options} />',
  63. 'submit-image' => '<input type="image" src="{:url}"{:options} />',
  64. 'text' => '<input type="text" name="{:name}"{:options} />',
  65. 'textarea' => '<textarea name="{:name}"{:options}>{:value}</textarea>',
  66. 'fieldset' => '<fieldset{:options}>{:content}</fieldset>',
  67. 'fieldset-start' => '<fieldset><legend>{:content}</legend>',
  68. 'fieldset-end' => '</fieldset>'
  69. );
  70. /**
  71. * Maps method names to template string names, allowing the default template strings to be set
  72. * permanently on a per-method basis.
  73. *
  74. * For example, if all text input fields should be wrapped in `<span />` tags, you can configure
  75. * the template string mappings per the following:
  76. *
  77. * {{{
  78. * $this->form->config(array('templates' => array(
  79. * 'text' => '<span><input type="text" name="{:name}"{:options} /></span>'
  80. * )));
  81. * }}}
  82. *
  83. * Alternatively, you can re-map one type as another. This is useful if, for example, you
  84. * include your own helper with custom form template strings which do not match the default
  85. * template string names.
  86. *
  87. * {{{
  88. * // Renders all password fields as text fields
  89. * $this->form->config(array('templates' => array('password' => 'text')));
  90. * }}}
  91. *
  92. * @var array
  93. * @see lithium\template\helper\Form::config()
  94. */
  95. protected $_templateMap = array(
  96. 'create' => 'form',
  97. 'end' => 'form-end'
  98. );
  99. /**
  100. * The data object or list of data objects to which the current form is bound. In order to
  101. * be a custom data object, a class must implement the following methods:
  102. *
  103. * - schema(): Returns an array defining the objects fields and their data types.
  104. * - data(): Returns an associative array of the data that this object represents.
  105. * - errors(): Returns an associatie array of validation errors for the current data set, where
  106. * the keys match keys from `schema()`, and the values are either strings (in cases
  107. * where a field only has one error) or an array (in case of multiple errors),
  108. *
  109. * For an example of how to implement these methods, see the `lithium\data\Entity` object.
  110. *
  111. * @var mixed A single data object, a `Collection` of multiple data ovjects, or an array of data
  112. * objects/`Collection`s.
  113. * @see lithium\data\Entity
  114. * @see lithium\template\helper\Form::create()
  115. */
  116. protected $_binding = null;
  117. public function __construct(array $config = array()) {
  118. $defaults = array(
  119. 'base' => array(), 'text' => array(), 'textarea' => array(),
  120. 'select' => array('multiple' => false)
  121. );
  122. parent::__construct($config + $defaults);
  123. }
  124. /**
  125. * Object initializer. Adds a content handler for the `wrap` key in the `field()` method, which
  126. * converts an array of properties to an attribute string.
  127. *
  128. * @return void
  129. */
  130. protected function _init() {
  131. parent::_init();
  132. $this->_context->handlers(array('wrap' => '_attributes'));
  133. }
  134. /**
  135. * Allows you to configure a default set of options which are included on a per-method basis,
  136. * and configure method template overrides.
  137. *
  138. * To force all `<label />` elements to have a default `class` attribute value of `"foo"`,
  139. * simply do the following:
  140. *
  141. * {{{
  142. * $this->form->config(array('label' => array('class' => 'foo')));
  143. * }}}
  144. *
  145. * Note that this can be overridden on a case-by-case basis, and when overridding, values are
  146. * not merged or combined. Therefore, if you wanted a particular `<label />` to have both `foo`
  147. * and `bar` as classes, you would have to specify `'class' => 'foo bar'`.
  148. *
  149. * You can also use this method to change the string template that a method uses to render its
  150. * content. For example, the default template for rendering a checkbox is
  151. * `'<input type="checkbox" name="{:name}"{:options} />'`. However, suppose you implemented your
  152. * own custom UI elements, and you wanted to change the markup used, you could do the following:
  153. *
  154. * {{{
  155. * $this->form->config(array('templates' => array(
  156. * 'checkbox' => '<div id="{:name}" class="ui-checkbox-element"{:options}></div>'
  157. * )));
  158. * }}}
  159. *
  160. * Now, for any calls to `$this->form->checkbox()`, your custom markup template will be applied.
  161. * This works for any `Form` method that renders HTML elements.
  162. *
  163. * @see lithium\template\helper\Form::$_templateMap
  164. * @param array $config An associative array where the keys are `Form` method names (or
  165. * `'templates'`, to include a template-overriding sub-array), and the
  166. * values are arrays of configuration options to be included in the `$options`
  167. * parameter of each method specified.
  168. * @return array Returns an array containing the currently set per-method configurations, and
  169. * an array of the currently set template overrides (in the `'templates'` array key).
  170. */
  171. public function config(array $config = array()) {
  172. if (!$config) {
  173. return array('templates' => $this->_templateMap) + array_intersect_key(
  174. $this->_config, array('base' => '', 'text' => '', 'textarea' => '')
  175. );
  176. }
  177. if (isset($config['templates'])) {
  178. $this->_templateMap = $config['templates'] + $this->_templateMap;
  179. unset($config['templates']);
  180. }
  181. return ($this->_config = Set::merge($this->_config, $config)) + array(
  182. 'templates' => $this->_templateMap
  183. );
  184. }
  185. /**
  186. * Creates an HTML form, and optionally binds it to a data object which contains information on
  187. * how to render form fields, any data to pre-populate the form with, and any validation errors.
  188. * Typically, a data object will be a `Record` object returned from a `Model`, but you can
  189. * define your own custom objects as well. For more information on custom data objects, see
  190. * `lithium\template\helper\Form::$_binding`.
  191. *
  192. * @see lithium\template\helper\Form::$_binding
  193. * @see lithium\data\Entity
  194. * @param object $binding
  195. * @param array $options
  196. * @return string Returns a `<form />` open tag with the `action` attribute defined by either
  197. * the `'action'` or `'url'` options (defaulting to the current page if none is
  198. * specified), the HTTP method is defined by the `'method'` option, and any HTML
  199. * attributes passed in `$options`.
  200. */
  201. public function create(\lithium\data\Entity $binding = null, array $options = array()) {
  202. $defaults = array(
  203. 'url' => $this->_context->request()->params,
  204. 'type' => null,
  205. 'action' => null,
  206. 'method' => $binding ? ($binding->exists() ? 'put' : 'post') : 'post'
  207. );
  208. list(, $options, $template) = $this->_defaults(__FUNCTION__, null, $options);
  209. list($scope, $options) = $this->_options($defaults, $options);
  210. $_binding =& $this->_binding;
  211. $method = __METHOD__;
  212. $params = compact('scope', 'options', 'binding');
  213. $filter = function($self, $params, $chain) use ($method, $template, $defaults, &$_binding) {
  214. $scope = $params['scope'];
  215. $options = $params['options'];
  216. $_binding = $params['binding'];
  217. $append = null;
  218. if ($scope['type'] == 'file') {
  219. if (strtolower($scope['method']) == 'get') {
  220. $scope['method'] = 'post';
  221. }
  222. $options['enctype'] = 'multipart/form-data';
  223. }
  224. if (!in_array(strtolower($scope['method']), array('get', 'post'))) {
  225. $append = $self->hidden('_method', array(
  226. 'value' => strtoupper($scope['method'])
  227. ));
  228. $scope['method'] = 'post';
  229. }
  230. $url = $scope['action'] ? array('action' => $scope['action']) : $scope['url'];
  231. $options['method'] = strtolower($scope['method']);
  232. return $self->invokeMethod('_render', array(
  233. $method, $template, compact('url', 'options', 'append')
  234. ));
  235. };
  236. return $this->_filter($method, $params, $filter);
  237. }
  238. /**
  239. * Echoes a closing `</form>` tag and unbinds the `Form` helper from any `Record` or `Document`
  240. * object used to generate the corresponding form.
  241. *
  242. * @return string Returns a closing `</form>` tag.
  243. */
  244. public function end() {
  245. list(, $options, $template) = $this->_defaults(__FUNCTION__, null, array());
  246. $params = compact('options', 'template');
  247. $_binding =& $this->_binding;
  248. $_context =& $this->_context;
  249. $filter = function($self, $params, $chain) use (&$_binding, &$_context) {
  250. unset($_binding);
  251. return $_context->strings('form-end');
  252. };
  253. $result = $this->_filter(__METHOD__, $params, $filter);
  254. unset($this->_binding);
  255. $this->_binding = null;
  256. return $result;
  257. }
  258. /**
  259. * Implements alternative input types as method calls against `Form` helper. Enables the
  260. * generation of HTML5 input types and other custom input types:
  261. *
  262. * {{{ embed:lithium\tests\cases\template\helper\FormTest::testCustomInputTypes(1-2) }}}
  263. *
  264. * @param string $type The method called, which represents the `type` attribute of the
  265. * `<input />` tag.
  266. * @param array $params An array of method parameters passed to the method call. The first
  267. * element should be the name of the input field, and the second should be an array
  268. * of element attributes.
  269. * @return string Returns an `<input />` tag of the type specified in `$type`.
  270. */
  271. public function __call($type, array $params = array()) {
  272. $params += array(null, array());
  273. list($name, $options) = $params;
  274. list($name, $options, $template) = $this->_defaults($type, $name, $options);
  275. $template = $this->_context->strings($template) ? $template : 'element';
  276. return $this->_render($type, $template, compact('type', 'name', 'options', 'value'));
  277. }
  278. /**
  279. * Generates a form field with a label, input, and error message (if applicable), all contained
  280. * within a wrapping element.
  281. *
  282. * @param string $name The name of the field to render. If the form was bound to an object
  283. * passed in `create()`, `$name` should be the name of a field in that object.
  284. * Otherwise, can be any arbitrary field name, as it will appear in POST data.
  285. * @param array $options Rendering options for the form field. The available options are as
  286. * follows:
  287. * - `'label'` _mixed_: A string or array defining the label text and / or
  288. * parameters. By default, the label text is a human-friendly version of `$name`.
  289. * However, you can specify the label manually as a string, or both the label
  290. * text and options as an array, i.e.:
  291. * `array('label text' => array('class' => 'foo', 'any' => 'other options'))`.
  292. * - `'type'` _string_: The type of form field to render. Available default options
  293. * are: `'text'`, `'textarea'`, `'select'`, `'checkbox'`, `'password'` or
  294. * `'hidden'`, as well as any arbitrary type (i.e. HTML5 form fields).
  295. * - `'template'` _string_: Defaults to `'template'`, but can be set to any named
  296. * template string, or an arbitrary HTML fragment. For example, to change the
  297. * default wrapper tag from `<div />` to `<li />`, you can pass the following:
  298. * `'<li{:wrap}>{:label}{:input}{:error}</li>'`.
  299. * - `'wrap'` _array_: An array of HTML attributes which will be embedded in the
  300. * wrapper tag.
  301. * - `list` _array_: If `'type'` is set to `'select'`, `'list'` is an array of
  302. * key/value pairs representing the `$list` parameter of the `select()` method.
  303. * @return string Returns a form input (the input type is based on the `'type'` option), with
  304. * label and error message, wrapped in a `<div />` element.
  305. */
  306. public function field($name, array $options = array()) {
  307. $defaults = array(
  308. 'label' => null,
  309. 'type' => 'text',
  310. 'template' => 'field',
  311. 'wrap' => array(),
  312. 'list' => null,
  313. );
  314. list($options, $fieldOptions) = $this->_options($defaults, $options);
  315. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  316. if ($options['template'] != $defaults['template']) {
  317. $template = $options['template'];
  318. }
  319. $wrap = $options['wrap'];
  320. $type = $options['type'];
  321. $label = $input = null;
  322. if ($options['label'] === null || $options['label']) {
  323. $label = $this->label($name, $options['label']);
  324. }
  325. switch (true) {
  326. case ($type == 'select'):
  327. $input = $this->select($name, $options['list'], $fieldOptions);
  328. break;
  329. default:
  330. $input = $this->{$type}($name, $fieldOptions);
  331. break;
  332. }
  333. $error = ($this->_binding) ? $this->error($name) : null;
  334. return $this->_render(__METHOD__, $template, compact('wrap', 'label', 'input', 'error'));
  335. }
  336. /**
  337. * Generates an HTML `<input type="submit" />` object.
  338. *
  339. * @param string $title The title of the submit button.
  340. * @param array $options Any options passed are converted to HTML attributes within the
  341. * `<input />` tag.
  342. * @return string Returns a submit `<input />` tag with the given title and HTML attributes.
  343. */
  344. public function submit($title = null, array $options = array()) {
  345. list($name, $options, $template) = $this->_defaults(__FUNCTION__, null, $options);
  346. return $this->_render(__METHOD__, $template, compact('title', 'options'));
  347. }
  348. /**
  349. * Generates an HTML `<textarea>...</textarea>` object.
  350. *
  351. * @param string $name The name of the field.
  352. * @param array $options The options to be used when generating the `<textarea />` tag pair,
  353. * which are as follows:
  354. * - `'value'` _string_: The content value of the field.
  355. * - Any other options specified are rendered as HTML attributes of the element.
  356. * @return string Returns a `<textarea>` tag with the given name and HTML attributes.
  357. */
  358. public function textarea($name, array $options = array()) {
  359. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  360. list($scope, $options) = $this->_options(array('value' => null), $options);
  361. $value = isset($scope['value']) ? $scope['value'] : '';
  362. return $this->_render(__METHOD__, $template, compact('name', 'options', 'value'));
  363. }
  364. /**
  365. * Generates an HTML `<input type="text" />` object.
  366. *
  367. * @param string $name The name of the field.
  368. * @param array $options All options passed are rendered as HTML attributes.
  369. * @return string Returns a `<input />` tag with the given name and HTML attributes.
  370. */
  371. public function text($name, array $options = array()) {
  372. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  373. return $this->_render(__METHOD__, $template, compact('name', 'options'));
  374. }
  375. /**
  376. * Generates a `<select />` list using the `$list` parameter for the `<option />` tags. The
  377. * default selection will be set to the value of `$options['value']`, if specified.
  378. *
  379. * For example: {{{
  380. * $this->form->select('colors', array(1 => 'red', 2 => 'green', 3 => 'blue'), array(
  381. * 'id' => 'Colors', 'value' => 2
  382. * ));
  383. * // Renders a '<select />' list with options 'red', 'green' and 'blue', with the 'green'
  384. * // option as the selection
  385. * }}}
  386. *
  387. * @param string $name The `name` attribute of the `<select />` element.
  388. * @param array $list An associative array of key/value pairs, which will be used to render the
  389. * list of options.
  390. * @param array $options Any HTML attributes that should be associated with the `<select />`
  391. * element. If the `'value'` key is set, this will be the value of the option
  392. * that is selected by default.
  393. * @return string Returns an HTML `<select />` element.
  394. */
  395. public function select($name, $list = array(), array $options = array()) {
  396. $defaults = array('empty' => false, 'value' => null);
  397. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  398. list($scope, $options) = $this->_options($defaults, $options);
  399. if ($scope['empty']) {
  400. $list = array('' => ($scope['empty'] === true) ? '' : $scope['empty']) + $list;
  401. }
  402. $startTemplate = ($scope['multiple']) ? 'select-multi-start' : 'select-start';
  403. $output = $this->_render(__METHOD__, $startTemplate, compact('name', 'options'));
  404. foreach ($list as $value => $title) {
  405. $selected = false;
  406. if (is_array($scope['value']) && in_array($value, $scope['value'])) {
  407. $selected = true;
  408. } elseif ($scope['value'] == $value) {
  409. $selected = true;
  410. }
  411. $options = $selected ? array('selected' => true) : array();
  412. $output .= $this->_render(__METHOD__, 'select-option', compact(
  413. 'value', 'title', 'options'
  414. ));
  415. }
  416. return $output . $this->_context->strings('select-end');
  417. }
  418. /**
  419. * Generates an HTML `<input type="checkbox" />` object.
  420. *
  421. * @param string $name The name of the field.
  422. * @param array $options Options to be used when generating the checkbox `<input />` element:
  423. * - `'checked'` _boolean_: Whether or not the field should be checked by default.
  424. * - Any other options specified are rendered as HTML attributes of the element.
  425. * @return string Returns a `<input />` tag with the given name and HTML attributes.
  426. */
  427. public function checkbox($name, array $options = array()) {
  428. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  429. list($scope, $options) = $this->_options(array('value' => null), $options);
  430. if (!isset($scope['checked'])) {
  431. $options['checked'] = isset($scope['value']) ? $scope['value'] : false;
  432. }
  433. return $this->_render(__METHOD__, $template, compact('name', 'options'));
  434. }
  435. /**
  436. * Generates an HTML `<input type="password" />` object.
  437. *
  438. * @param string $name The name of the field.
  439. * @param array $options An array of HTML attributes with which the field should be rendered.
  440. * @return string Returns a `<input />` tag with the given name and HTML attributes.
  441. */
  442. public function password($name, array $options = array()) {
  443. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  444. return $this->_render(__METHOD__, $template, compact('name', 'options'));
  445. }
  446. /**
  447. * Generates an HTML `<input type="hidden" />` object.
  448. *
  449. * @param string $name The name of the field.
  450. * @param array $options An array of HTML attributes with which the field should be rendered.
  451. * @return string Returns a `<input />` tag with the given name and HTML attributes.
  452. */
  453. public function hidden($name, array $options = array()) {
  454. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  455. return $this->_render(__METHOD__, $template, compact('name', 'options'));
  456. }
  457. /**
  458. * Generates an HTML `<label></label>` object.
  459. *
  460. * @param string $name The name of the field that the label is for.
  461. * @param string $title The content inside the `<label></label>` object.
  462. * @param array $options Besides HTML attributes, this parameter allows one additional flag:
  463. * - `'escape'` _boolean_: Defaults to `true`. Indicates whether the title of the
  464. * label should be escaped. If `false`, it will be treated as raw HTML.
  465. * @return string Returns a `<label>` tag for the name and with HTML attributes.
  466. */
  467. public function label($name, $title = null, array $options = array()) {
  468. $defaults = array('escape' => true);
  469. if (is_array($title)) {
  470. list($title, $options) = each($title);
  471. }
  472. $title = $title ?: Inflector::humanize($name);
  473. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  474. list($scope, $options) = $this->_options($defaults, $options);
  475. return $this->_render(__METHOD__, $template, compact('name', 'title', 'options'), $scope);
  476. }
  477. /**
  478. * Generates an error message for a field which is part of an object bound to a form in
  479. * `create()`.
  480. *
  481. * @param string $name The name of the field for which to render an error.
  482. * @param mixed $key If more than one error is present for `$name`, a key may be specified.
  483. * By default, the first available error is used.
  484. * @param array $options Any rendering options or HTML attributes to be used when rendering
  485. * the error.
  486. * @return string Returns a rendered error message based on the `'error'` string template.
  487. */
  488. public function error($name, $key = null, array $options = array()) {
  489. $defaults = array('class' => 'error');
  490. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  491. $options += $defaults;
  492. if (!$this->_binding || !$content = $this->_binding->errors($name)) {
  493. return null;
  494. }
  495. if (is_array($content)) {
  496. $content = !isset($content[$key]) ? reset($content) : $content[$key];
  497. }
  498. return $this->_render(__METHOD__, $template, compact('content', 'options'));
  499. }
  500. /**
  501. * Builds the defaults array for a method by name, according to the config.
  502. *
  503. * @param string $method The name of the method to create defaults for.
  504. * @param string $name The `$name` supplied to the original method.
  505. * @param string $options `$options` from the original method.
  506. * @return array Defaults array contents.
  507. */
  508. protected function _defaults($method, $name, $options) {
  509. $methodConfig = isset($this->_config[$method]) ? $this->_config[$method] : array();
  510. $options += $methodConfig + $this->_config['base'];
  511. $hasValue = (
  512. (!isset($options['value']) || empty($options['value'])) &&
  513. $name && $this->_binding && $value = $this->_binding->data($name)
  514. );
  515. if ($hasValue) {
  516. $options['value'] = $value;
  517. }
  518. if (isset($options['default']) && empty($options['value'])) {
  519. $options['value'] = $options['default'];
  520. }
  521. unset($options['default']);
  522. $template = isset($this->_templateMap[$method]) ? $this->_templateMap[$method] : $method;
  523. return array($name, $options, $template);
  524. }
  525. }
  526. ?>