PageRenderTime 60ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/template/helper/Form.php

http://github.com/UnionOfRAD/lithium
PHP | 965 lines | 867 code | 9 blank | 89 comment | 12 complexity | 887db2c954ba0903358efc0b6b6d8df1 MD5 | raw file
  1. <?php
  2. /**
  3. * li₃: the most RAD framework for PHP (http://li3.me)
  4. *
  5. * Copyright 2009, Union of RAD. All rights reserved. This source
  6. * code is distributed under the terms of the BSD 3-Clause License.
  7. * The full license text can be found in the LICENSE.txt file.
  8. */
  9. namespace lithium\template\helper;
  10. use lithium\aop\Filters;
  11. use lithium\util\Set;
  12. use lithium\util\Inflector;
  13. /**
  14. * A helper class to facilitate generating, processing and securing HTML forms. By default, `Form`
  15. * will simply generate HTML forms and widgets, but by creating a form with a _binding object_,
  16. * the helper can pre-fill form input values, render error messages, and introspect column types.
  17. *
  18. * For example, assuming you have created a `Posts` model in your application:
  19. *
  20. * In controller code:
  21. * ```
  22. * use app\models\Posts;
  23. * $post = Posts::find(1);
  24. * return compact('post');
  25. * ```
  26. *
  27. * In view code:
  28. * ```
  29. * <?= $this->form->create($post) ?> // Echoes a <form> tag and binds the helper to $post
  30. * <?= $this->form->text('title') ?> // Echoes an <input /> element, pre-filled with $post's title
  31. * <?= $this->form->submit('Update') ?> // Echoes a submit button with the title 'Update'
  32. * <?= $this->form->end() ?> // Echoes a </form> tag & unbinds the form
  33. * ```
  34. */
  35. class Form extends \lithium\template\Helper {
  36. /**
  37. * String templates used by this helper.
  38. *
  39. * @var array
  40. */
  41. protected $_strings = [
  42. 'button' => '<button{:options}>{:title}</button>',
  43. 'checkbox' => '<input type="checkbox" name="{:name}"{:options} />',
  44. 'checkbox-multi' => '<input type="checkbox" name="{:name}[]"{:options} />',
  45. 'checkbox-multi-group' => '{:raw}',
  46. 'error' => '<div{:options}>{:content}</div>',
  47. 'errors' => '{:raw}',
  48. 'input' => '<input type="{:type}" name="{:name}"{:options} />',
  49. 'file' => '<input type="file" name="{:name}"{:options} />',
  50. 'form' => '<form action="{:url}"{:options}>{:append}',
  51. 'form-end' => '</form>',
  52. 'hidden' => '<input type="hidden" name="{:name}"{:options} />',
  53. 'field' => '<div{:wrap}>{:label}{:input}{:datalist}{:error}</div>',
  54. 'field-checkbox' => '<div{:wrap}>{:input}{:label}{:error}</div>',
  55. 'field-radio' => '<div{:wrap}>{:input}{:label}{:error}</div>',
  56. 'label' => '<label for="{:id}"{:options}>{:title}</label>',
  57. 'legend' => '<legend>{:content}</legend>',
  58. 'option-group' => '<optgroup label="{:label}"{:options}>{:raw}</optgroup>',
  59. 'password' => '<input type="password" name="{:name}"{:options} />',
  60. 'radio' => '<input type="radio" name="{:name}"{:options} />',
  61. 'select' => '<select name="{:name}"{:options}>{:raw}</select>',
  62. 'select-empty' => '<option value=""{:options}>&nbsp;</option>',
  63. 'select-multi' => '<select name="{:name}[]"{:options}>{:raw}</select>',
  64. 'select-option' => '<option value="{:value}"{:options}>{:title}</option>',
  65. 'submit' => '<input type="submit" value="{:title}"{:options} />',
  66. 'submit-image' => '<input type="image" src="{:url}"{:options} />',
  67. 'text' => '<input type="text" name="{:name}"{:options} />',
  68. 'textarea' => '<textarea name="{:name}"{:options}>{:value}</textarea>',
  69. 'fieldset' => '<fieldset{:options}><legend>{:content}</legend>{:raw}</fieldset>',
  70. 'datalist' => '<datalist{:options}>{:raw}</datalist>',
  71. 'datalist-option' => '<option value="{:value}"></option>'
  72. ];
  73. /**
  74. * Maps method names to template string names, allowing the default template strings to be set
  75. * permanently on a per-method basis.
  76. *
  77. * For example, if all text input fields should be wrapped in `<span />` tags, you can configure
  78. * the template string mappings per the following:
  79. *
  80. * ```
  81. * $this->form->config(['templates' => [
  82. * 'text' => '<span><input type="text" name="{:name}"{:options} /></span>'
  83. * ]]);
  84. * ```
  85. *
  86. * Alternatively, you can re-map one type as another. This is useful if, for example, you
  87. * include your own helper with custom form template strings which do not match the default
  88. * template string names.
  89. *
  90. * ```
  91. * // Renders all password fields as text fields
  92. * $this->form->config(['templates' => ['password' => 'text']]);
  93. * ```
  94. *
  95. * @var array
  96. * @see lithium\template\helper\Form::config()
  97. */
  98. protected $_templateMap = [
  99. 'create' => 'form',
  100. 'end' => 'form-end'
  101. ];
  102. /**
  103. * The data object or list of data objects to which the current form is bound. In order to
  104. * be a custom data object, a class must implement the following methods:
  105. *
  106. * - schema(): Returns an array defining the objects fields and their data types.
  107. * - data(): Returns an associative array of the data that this object represents.
  108. * - errors(): Returns an associate array of validation errors for the current data set, where
  109. * the keys match keys from `schema()`, and the values are either strings (in cases
  110. * where a field only has one error) or an array (in case of multiple errors),
  111. *
  112. * For an example of how to implement these methods, see the `lithium\data\Entity` object.
  113. *
  114. * @see lithium\data\Entity
  115. * @see lithium\data\Collection
  116. * @see lithium\template\helper\Form::create()
  117. * @var mixed A single data object, a `Collection` of multiple data objects, or an array of data
  118. * objects/`Collection`s.
  119. */
  120. protected $_binding = null;
  121. /**
  122. * Array of options used to create the form to which `$_binding` is currently bound.
  123. * Overwritten when `end()` is called.
  124. *
  125. * @var array
  126. */
  127. protected $_bindingOptions = [];
  128. /**
  129. * Constructor.
  130. *
  131. * @param array $config Configuration options.
  132. * @return void
  133. */
  134. public function __construct(array $config = []) {
  135. $defaults = [
  136. 'base' => [],
  137. 'text' => [],
  138. 'textarea' => [],
  139. 'select' => ['multiple' => false],
  140. 'attributes' => [
  141. 'id' => function($method, $name, $options) {
  142. if (in_array($method, ['create', 'end', 'label', 'error'])) {
  143. return;
  144. }
  145. if (!$name || ($method === 'hidden' && $name === '_method')) {
  146. return;
  147. }
  148. $info = $this->binding($name);
  149. $model = $info->class;
  150. $id = Inflector::camelize(Inflector::slug($info->name));
  151. return $model ? basename(str_replace('\\', '/', $model)) . $id : $id;
  152. },
  153. 'name' => function($method, $name, $options) {
  154. if (!strpos($name, '.')) {
  155. return $name;
  156. }
  157. $name = explode('.', $name);
  158. $first = array_shift($name);
  159. return $first . '[' . join('][', $name) . ']';
  160. }
  161. ],
  162. 'binding' => function($object, $name = null) {
  163. $result = compact('name') + [
  164. 'data' => null, 'errors' => null, 'class' => null
  165. ];
  166. if (is_object($object)) {
  167. $result = compact('name') + [
  168. 'data' => $object->data($name),
  169. 'errors' => $object->errors($name),
  170. 'class' => $object->model()
  171. ];
  172. }
  173. return (object) $result;
  174. }
  175. ];
  176. parent::__construct(Set::merge($defaults, $config));
  177. }
  178. /**
  179. * Object initializer. Adds a content handler for the `wrap` key in the `field()` method, which
  180. * converts an array of properties to an attribute string.
  181. *
  182. * @return void
  183. */
  184. protected function _init() {
  185. parent::_init();
  186. if ($this->_context) {
  187. $this->_context->handlers(['wrap' => 'attributes']);
  188. }
  189. }
  190. /**
  191. * Allows you to configure a default set of options which are included on a per-method basis,
  192. * and configure method template overrides.
  193. *
  194. * To force all `<label />` elements to have a default `class` attribute value of `"foo"`,
  195. * simply do the following:
  196. *
  197. * ```
  198. * $this->form->config(['label' => ['class' => 'foo']]);
  199. * ```
  200. *
  201. * Note that this can be overridden on a case-by-case basis, and when overriding, values are
  202. * not merged or combined. Therefore, if you wanted a particular `<label />` to have both `foo`
  203. * and `bar` as classes, you would have to specify `'class' => 'foo bar'`.
  204. *
  205. * You can also use this method to change the string template that a method uses to render its
  206. * content. For example, the default template for rendering a checkbox is
  207. * `'<input type="checkbox" name="{:name}"{:options} />'`. However, suppose you implemented your
  208. * own custom UI elements, and you wanted to change the markup used, you could do the following:
  209. *
  210. * ```
  211. * $this->form->config(['templates' => [
  212. * 'checkbox' => '<div id="{:name}" class="ui-checkbox-element"{:options}></div>'
  213. * ]]);
  214. * ```
  215. *
  216. * Now, for any calls to `$this->form->checkbox()`, your custom markup template will be applied.
  217. * This works for any `Form` method that renders HTML elements.
  218. *
  219. * @see lithium\template\helper\Form::$_templateMap
  220. * @param array $config An associative array where the keys are `Form` method names (or
  221. * `'templates'`, to include a template-overriding sub-array), and the
  222. * values are arrays of configuration options to be included in the `$options`
  223. * parameter of each method specified.
  224. * @return array Returns an array containing the currently set per-method configurations, and
  225. * an array of the currently set template overrides (in the `'templates'` array key).
  226. */
  227. public function config(array $config = []) {
  228. if (!$config) {
  229. $keys = ['base' => '', 'text' => '', 'textarea' => '', 'attributes' => ''];
  230. return ['templates' => $this->_templateMap] + array_intersect_key(
  231. $this->_config, $keys
  232. );
  233. }
  234. if (isset($config['templates'])) {
  235. $this->_templateMap = $config['templates'] + $this->_templateMap;
  236. unset($config['templates']);
  237. }
  238. return ($this->_config = Set::merge($this->_config, $config)) + [
  239. 'templates' => $this->_templateMap
  240. ];
  241. }
  242. /**
  243. * Creates an HTML form, and optionally binds it to a data object which contains information on
  244. * how to render form fields, any data to pre-populate the form with, and any validation errors.
  245. * Typically, a data object will be a `Record` object returned from a `Model`, but you can
  246. * define your own custom objects as well. For more information on custom data objects, see
  247. * `lithium\template\helper\Form::$_binding`.
  248. *
  249. * @see lithium\template\helper\Form::$_binding
  250. * @see lithium\data\Entity
  251. * @param mixed $bindings List of objects, or the object to bind the form to. This is usually
  252. * an instance of `Record` or `Document`, or some other class that extends
  253. * `lithium\data\Entity`.
  254. * @param array $options Other parameters for creating the form. Available options are:
  255. * - `'url'` _mixed_: A string URL or URL array parameters defining where in the
  256. * application the form should be submitted to.
  257. * - `'action'` _string_: This is a shortcut to be used if you wish to only
  258. * specify the name of the action to submit to, and use the default URL
  259. * parameters (i.e. the current controller, etc.) for generating the remainder
  260. * of the URL. Ignored if the `'url'` key is set.
  261. * - `'type'` _string_: Currently the only valid option is `'file'`. Set this if
  262. * the form will be used for file uploads.
  263. * - `'method'` _string_: Represents the HTTP method with which the form will be
  264. * submitted (`'get'`, `'post'`, `'put'` or `'delete'`). If `'put'` or
  265. * `'delete'`, the request method is simulated using a hidden input field.
  266. * @return string Returns a `<form />` open tag with the `action` attribute defined by either
  267. * the `'action'` or `'url'` options (defaulting to the current page if none is
  268. * specified), the HTTP method is defined by the `'method'` option, and any HTML
  269. * attributes passed in `$options`.
  270. * @filter
  271. */
  272. public function create($bindings = null, array $options = []) {
  273. $request = $this->_context ? $this->_context->request() : null;
  274. $binding = is_array($bindings) ? reset($bindings) : $bindings;
  275. $defaults = [
  276. 'url' => $request ? $request->params : [],
  277. 'type' => null,
  278. 'action' => null,
  279. 'method' => $binding ? ($binding->exists() ? 'put' : 'post') : 'post'
  280. ];
  281. list(, $options, $tpl) = $this->_defaults(__FUNCTION__, null, $options);
  282. list($scope, $options) = $this->_options($defaults, $options);
  283. $params = compact('scope', 'options', 'bindings');
  284. $extra = ['method' => __METHOD__] + compact('tpl', 'defaults');
  285. return Filters::run($this, __FUNCTION__, $params, function($params) use ($extra) {
  286. $scope = $params['scope'];
  287. $options = $params['options'];
  288. $this->_binding = $params['bindings'];
  289. $append = null;
  290. $scope['method'] = strtolower($scope['method']);
  291. if ($scope['type'] === 'file') {
  292. if ($scope['method'] === 'get') {
  293. $scope['method'] = 'post';
  294. }
  295. $options['enctype'] = 'multipart/form-data';
  296. }
  297. if (!($scope['method'] === 'get' || $scope['method'] === 'post')) {
  298. $append = $this->hidden('_method', ['value' => strtoupper($scope['method'])]);
  299. $scope['method'] = 'post';
  300. }
  301. $url = $scope['action'] ? ['action' => $scope['action']] : $scope['url'];
  302. $options['method'] = strtolower($scope['method']);
  303. $this->_bindingOptions = $scope + $options;
  304. return $this->_render($extra['method'], $extra['tpl'],
  305. compact('url', 'options', 'append')
  306. );
  307. });
  308. }
  309. /**
  310. * Echoes a closing `</form>` tag and unbinds the `Form` helper from any `Record` or `Document`
  311. * object used to generate the corresponding form.
  312. *
  313. * @return string Returns a closing `</form>` tag.
  314. * @filter
  315. */
  316. public function end() {
  317. list(, $options, $template) = $this->_defaults(__FUNCTION__, null, []);
  318. $params = compact('options', 'template');
  319. $result = Filters::run($this, __FUNCTION__, $params, function($params) {
  320. $this->_bindingOptions = [];
  321. return $this->_render('end', $params['template'], []);
  322. });
  323. unset($this->_binding);
  324. $this->_binding = null;
  325. return $result;
  326. }
  327. /**
  328. * Returns the entity that the `Form` helper is currently bound to.
  329. *
  330. * @see lithium\template\helper\Form::$_binding
  331. * @param string $name If specified, match this field name against the list of bindings
  332. * @param string $key If $name specified, where to store relevant $_binding key
  333. * @return object Returns an object, usually an instance of `lithium\data\Entity`.
  334. */
  335. public function binding($name = null) {
  336. if (!$this->_binding) {
  337. return $this->_config['binding'](null, $name);
  338. }
  339. $binding = $this->_binding;
  340. $model = null;
  341. $key = $name;
  342. if (is_array($binding)) {
  343. switch (true) {
  344. case strpos($name, '.'):
  345. list($model, $key) = explode('.', $name, 2);
  346. $binding = isset($binding[$model]) ? $binding[$model] : reset($binding);
  347. break;
  348. case isset($binding[$name]):
  349. $binding = $binding[$name];
  350. $key = null;
  351. break;
  352. default:
  353. $binding = reset($binding);
  354. break;
  355. }
  356. }
  357. return $key ? $this->_config['binding']($binding, $key) : $binding;
  358. }
  359. /**
  360. * Implements alternative input types as method calls against `Form` helper. Enables the
  361. * generation of HTML5 input types and other custom input types:
  362. *
  363. * ``` embed:lithium\tests\cases\template\helper\FormTest::testCustomInputTypes(1-2) ```
  364. *
  365. * @param string $type The method called, which represents the `type` attribute of the
  366. * `<input />` tag.
  367. * @param array $params An array of method parameters passed to the method call. The first
  368. * element should be the name of the input field, and the second should be an array
  369. * of element attributes.
  370. * @return string Returns an `<input />` tag of the type specified in `$type`.
  371. */
  372. public function __call($type, array $params = []) {
  373. $params += [null, []];
  374. list($name, $options) = $params;
  375. list($name, $options, $template) = $this->_defaults($type, $name, $options);
  376. $template = $this->_context->strings($template) ? $template : 'input';
  377. return $this->_render($type, $template, compact('type', 'name', 'options'));
  378. }
  379. /**
  380. * Determines if a given method can be called.
  381. *
  382. * @deprecated
  383. * @param string $method Name of the method.
  384. * @param boolean $internal Provide `true` to perform check from inside the
  385. * class/object. When `false` checks also for public visibility;
  386. * defaults to `false`.
  387. * @return boolean Returns `true` if the method can be called, `false` otherwise.
  388. */
  389. public function respondsTo($method, $internal = false) {
  390. $message = '`' . __METHOD__ . '()` has been deprecated. ';
  391. $message .= "Use `is_callable([<class>, '<method>'])` instead.";
  392. trigger_error($message, E_USER_DEPRECATED);
  393. return is_callable([$this, $method], true);
  394. }
  395. /**
  396. * Generates a form field with a label, input, and error message (if applicable), all contained
  397. * within a wrapping element.
  398. *
  399. * ```
  400. * echo $this->form->field('name');
  401. * echo $this->form->field('present', ['type' => 'checkbox']);
  402. * echo $this->form->field(['email' => 'Enter a valid email']);
  403. * echo $this->form->field(['name','email','phone'], ['div' => false]);
  404. * ```
  405. *
  406. * @param mixed $name The name of the field to render. If the form was bound to an object
  407. * passed in `create()`, `$name` should be the name of a field in that object.
  408. * Otherwise, can be any arbitrary field name, as it will appear in POST data.
  409. * Alternatively supply an array of fields that will use the same options
  410. * [$field1 => $label1, $field2, $field3 => $label3]
  411. * @param array $options Rendering options for the form field. The available options are as
  412. * follows:
  413. * - `'label'` _mixed_: A string or array defining the label text and / or
  414. * parameters. By default, the label text is a human-friendly version of `$name`.
  415. * However, you can specify the label manually as a string, or both the label
  416. * text and options as an array, i.e.:
  417. * `['Your Label Title' => ['class' => 'foo', 'other' => 'options']]`.
  418. * - `'type'` _string_: The type of form field to render. Available default options
  419. * are: `'text'`, `'textarea'`, `'select'`, `'checkbox'`, `'password'` or
  420. * `'hidden'`, as well as any arbitrary type (i.e. HTML5 form fields).
  421. * - `'template'` _string_: Defaults to `'template'`, but can be set to any named
  422. * template string, or an arbitrary HTML fragment. For example, to change the
  423. * default wrapper tag from `<div />` to `<li />`, you can pass the following:
  424. * `'<li{:wrap}>{:label}{:input}{:error}</li>'`.
  425. * - `'wrap'` _array_: An array of HTML attributes which will be embedded in the
  426. * wrapper tag.
  427. * - `list` _array|string_: If `'type'` is set to `'select'`, `'list'` is an array of
  428. * key/value pairs representing the `$list` parameter of the `select()` method.
  429. * If `'type'` is set to `'text'`, `'list'` is used to render/reference a corresponding
  430. * `<datalist>` element. It then can either be an array of option values or
  431. * a string to reference an existing `<datalist>`.
  432. * - `error` _array|string|boolean_: Allows to control rendering of error messages.
  433. * By setting this option to `false` any messages are disabled. When an array
  434. * mapping failed rule names to messages, will use these alternative message
  435. * instead of the ones provided when the validation was originally defined (i.e
  436. * inside the model). The array may contain a `'default'` key which value will
  437. * be used as a fallback message. In absence of a default message, the original
  438. * messages get rendered instead. When providing a string it will be used for
  439. * any error messages.
  440. * @return string Returns a form input (the input type is based on the `'type'` option), with
  441. * label and error message, wrapped in a `<div />` element.
  442. */
  443. public function field($name, array $options = []) {
  444. if (is_array($name)) {
  445. return $this->_fields($name, $options);
  446. }
  447. $method = __FUNCTION__;
  448. if (isset($options['type']) && !empty($this->_config['field-' . $options['type']])) {
  449. $method = 'field-' . $options['type'];
  450. }
  451. list(, $options, $template) = $this->_defaults($method, $name, $options);
  452. $defaults = [
  453. 'label' => null,
  454. 'type' => isset($options['list']) ? 'select' : 'text',
  455. 'template' => $template,
  456. 'wrap' => [],
  457. 'list' => null,
  458. 'error' => []
  459. ];
  460. list($options, $field) = $this->_options($defaults, $options);
  461. $label = $input = null;
  462. $wrap = $options['wrap'];
  463. $type = $options['type'];
  464. $list = $options['list'];
  465. $error = $options['error'];
  466. $template = $options['template'];
  467. $notText = $template === 'field' && $type !== 'text';
  468. if ($notText && $this->_context->strings('field-' . $type)) {
  469. $template = 'field-' . $type;
  470. }
  471. if (($options['label'] === null || $options['label']) && $options['type'] !== 'hidden') {
  472. if (!$options['label']) {
  473. $options['label'] = Inflector::humanize(preg_replace('/[\[\]\.]/', '_', $name));
  474. }
  475. $label = $this->label(isset($options['id']) ? $options['id'] : '', $options['label']);
  476. }
  477. $datalist = null;
  478. if ($type === 'text' && $list) {
  479. if (is_array($list)) {
  480. list($list, $datalist) = $this->_datalist($list, $options);
  481. }
  482. $field['list'] = $list;
  483. }
  484. $call = ($type === 'select') ? [$name, $list, $field] : [$name, $field];
  485. $input = call_user_func_array([$this, $type], $call);
  486. if ($error !== false && $this->_binding) {
  487. $error = $this->error($name, null, ['messages' => $error]);
  488. } else {
  489. $error = null;
  490. }
  491. return $this->_render(__METHOD__, $template, compact(
  492. 'wrap', 'label', 'input', 'datalist', 'error'
  493. ));
  494. }
  495. /**
  496. * Helper method used by `Form::field()` for iterating over an array of multiple fields.
  497. *
  498. * @see lithium\template\helper\Form::field()
  499. * @param array $fields An array of fields to render.
  500. * @param array $options The array of options to apply to all fields in the `$fields` array. See
  501. * the `$options` parameter of the `field` method for more information.
  502. * @return string Returns the fields rendered by `field()`, each separated by a newline.
  503. */
  504. protected function _fields(array $fields, array $options = []) {
  505. $result = [];
  506. foreach ($fields as $field => $label) {
  507. if (is_numeric($field)) {
  508. $result[] = $this->field($label, $options);
  509. } else {
  510. $result[] = $this->field($field, compact('label') + $options);
  511. }
  512. }
  513. return join("\n", $result);
  514. }
  515. /**
  516. * Generates an HTML button `<button></button>`.
  517. *
  518. * @param string $title The title of the button.
  519. * @param array $options Any options passed are converted to HTML attributes within the
  520. * `<button></button>` tag.
  521. * @return string Returns a `<button></button>` tag with the given title and HTML attributes.
  522. */
  523. public function button($title = null, array $options = []) {
  524. $defaults = ['escape' => true];
  525. list($scope, $options) = $this->_options($defaults, $options);
  526. list($title, $options, $template) = $this->_defaults(__METHOD__, $title, $options);
  527. $arguments = compact('title', 'options');
  528. return $this->_render(__METHOD__, 'button', $arguments, $scope);
  529. }
  530. /**
  531. * Generates an HTML `<input type="submit" />` object.
  532. *
  533. * @param string $title The title of the submit button.
  534. * @param array $options Any options passed are converted to HTML attributes within the
  535. * `<input />` tag.
  536. * @return string Returns a submit `<input />` tag with the given title and HTML attributes.
  537. */
  538. public function submit($title = null, array $options = []) {
  539. list($name, $options, $template) = $this->_defaults(__FUNCTION__, null, $options);
  540. return $this->_render(__METHOD__, $template, compact('title', 'options'));
  541. }
  542. /**
  543. * Generates an HTML `<textarea>...</textarea>` object.
  544. *
  545. * @param string $name The name of the field.
  546. * @param array $options The options to be used when generating the `<textarea />` tag pair,
  547. * which are as follows:
  548. * - `'value'` _string_: The content value of the field.
  549. * - Any other options specified are rendered as HTML attributes of the element.
  550. * @return string Returns a `<textarea>` tag with the given name and HTML attributes.
  551. */
  552. public function textarea($name, array $options = []) {
  553. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  554. list($scope, $options) = $this->_options(['value' => null], $options);
  555. $value = isset($scope['value']) ? $scope['value'] : '';
  556. return $this->_render(__METHOD__, $template, compact('name', 'options', 'value'));
  557. }
  558. /**
  559. * Generates an HTML `<input type="text" />` object.
  560. *
  561. * @param string $name The name of the field.
  562. * @param array $options All options passed are rendered as HTML attributes.
  563. * @return string Returns a `<input />` tag with the given name and HTML attributes.
  564. */
  565. public function text($name, array $options = []) {
  566. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  567. return $this->_render(__METHOD__, $template, compact('name', 'options'));
  568. }
  569. /**
  570. * Generates an HTML `<datalist></datalist>` object with `<option>` elements.
  571. *
  572. * @link https://www.w3.org/wiki/HTML/Elements/datalist
  573. * @param array $list Valuues, which will be used to render the options of the datalist.
  574. * @param array $scope An array of options passed to the parent scope.
  575. * @return string Returns a `<datalist>` tag with `<option>` elements.
  576. */
  577. protected function _datalist(array $list, array $scope) {
  578. $options = [];
  579. if (isset($scope['id'])) {
  580. $id = $options['id'] = $scope['id'] . 'List';
  581. }
  582. $raw = array_reduce($list, function($carry, $value) {
  583. return $carry .= $this->_render(__METHOD__, 'datalist-option', compact('value'));
  584. }, '');
  585. return [
  586. isset($options['id']) ? $options['id'] : null,
  587. $this->_render(__METHOD__, 'datalist', compact('options', 'raw'))
  588. ];
  589. }
  590. /**
  591. * Generates a `<select />` list using the `$list` parameter for the `<option />` tags. The
  592. * default selection will be set to the value of `$options['value']`, if specified.
  593. *
  594. * For example:
  595. * ```
  596. * $this->form->select('colors', [1 => 'red', 2 => 'green', 3 => 'blue'], [
  597. * 'id' => 'Colors', 'value' => 2
  598. * ]);
  599. * // Renders a '<select />' list with options 'red', 'green' and 'blue', with the 'green'
  600. * // option as the selection
  601. * ```
  602. *
  603. * @param string $name The `name` attribute of the `<select />` element.
  604. * @param array $list An associative array of key/value pairs, which will be used to render the
  605. * list of options.
  606. * @param array $options Any HTML attributes that should be associated with the `<select />`
  607. * element. If the `'value'` key is set, this will be the value of the option
  608. * that is selected by default.
  609. * @return string Returns an HTML `<select />` element.
  610. */
  611. public function select($name, $list = [], array $options = []) {
  612. $defaults = ['empty' => false, 'value' => null];
  613. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  614. list($scope, $options) = $this->_options($defaults, $options);
  615. if ($scope['empty']) {
  616. $list = ['' => ($scope['empty'] === true) ? '' : $scope['empty']] + $list;
  617. }
  618. if ($template === __FUNCTION__ && $scope['multiple']) {
  619. $template = 'select-multi';
  620. }
  621. $raw = $this->_selectOptions($list, $scope);
  622. return $this->_render(__METHOD__, $template, compact('name', 'options', 'raw'));
  623. }
  624. /**
  625. * Generator method used by `select()` to produce `<option />` and `<optgroup />` elements.
  626. * Generally, this method should not need to be called directly, but through `select()`.
  627. *
  628. * @param array $list Either a flat key/value array of select menu options, or an array which
  629. * contains key/value elements and/or elements where the keys are `<optgroup />`
  630. * titles and the values are sub-arrays of key/value pairs representing nested
  631. * `<option />` elements.
  632. * @param array $scope An array of options passed to the parent scope, including the currently
  633. * selected value of the associated form element.
  634. * @return string Returns a string of `<option />` and (optionally) `<optgroup />` tags to be
  635. * embedded in a select element.
  636. */
  637. protected function _selectOptions(array $list, array $scope) {
  638. $result = "";
  639. foreach ($list as $value => $title) {
  640. if (is_array($title)) {
  641. $label = $value;
  642. $options = [];
  643. $raw = $this->_selectOptions($title, $scope);
  644. $params = compact('label', 'options', 'raw');
  645. $result .= $this->_render('select', 'option-group', $params);
  646. continue;
  647. }
  648. $selected = (
  649. (is_array($scope['value']) && in_array($value, $scope['value'])) ||
  650. ($scope['empty'] && empty($scope['value']) && $value === '') ||
  651. (is_scalar($scope['value']) && ((string) $scope['value'] === (string) $value))
  652. );
  653. $options = $selected ? ['selected' => true] : [];
  654. $params = compact('value', 'title', 'options');
  655. $result .= $this->_render('select', 'select-option', $params);
  656. }
  657. return $result;
  658. }
  659. /**
  660. * Generates an HTML `<input type="checkbox" />` object.
  661. *
  662. * @param string $name The name of the field.
  663. * @param array $options Options to be used when generating the checkbox `<input />` element:
  664. * - `'checked'` _boolean_: Whether or not the field should be checked by default.
  665. * - `'value'` _mixed_: if specified, it will be used as the 'value' html
  666. * attribute and no hidden input field will be added.
  667. * - Any other options specified are rendered as HTML attributes of the element.
  668. * @return string Returns a `<input />` tag with the given name and HTML attributes.
  669. */
  670. public function checkbox($name, array $options = []) {
  671. $defaults = ['value' => '1', 'hidden' => true];
  672. $options += $defaults;
  673. $default = $options['value'];
  674. $key = $name;
  675. $out = '';
  676. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  677. list($scope, $options) = $this->_options($defaults, $options);
  678. if (!isset($options['checked'])) {
  679. $options['checked'] = ($this->binding($key)->data == $default);
  680. }
  681. if ($scope['hidden']) {
  682. $out = $this->hidden($name, ['value' => '', 'id' => false]);
  683. }
  684. $options['value'] = $scope['value'];
  685. return $out . $this->_render(__METHOD__, $template, compact('name', 'options'));
  686. }
  687. /**
  688. * Generates an HTML `<input type="radio" />` object.
  689. *
  690. * @param string $name The name of the field
  691. * @param array $options All options to be used when generating the radio `<input />` element:
  692. * - `'checked'` _boolean_: Whether or not the field should be selected by default.
  693. * - `'value'` _mixed_: if specified, it will be used as the 'value' html
  694. * attribute. Defaults to `1`
  695. * - Any other options specified are rendered as HTML attributes of the element.
  696. * @return string Returns a `<input />` tag with the given name and attributes
  697. */
  698. public function radio($name, array $options = []) {
  699. $defaults = ['value' => '1'];
  700. $options += $defaults;
  701. $default = $options['value'];
  702. $key = $name;
  703. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  704. list($scope, $options) = $this->_options($defaults, $options);
  705. if (!isset($options['checked'])) {
  706. $options['checked'] = ($this->binding($key)->data == $default);
  707. }
  708. $options['value'] = $scope['value'];
  709. return $this->_render(__METHOD__, $template, compact('name', 'options'));
  710. }
  711. /**
  712. * Generates an HTML `<input type="password" />` object.
  713. *
  714. * @param string $name The name of the field.
  715. * @param array $options An array of HTML attributes with which the field should be rendered.
  716. * @return string Returns a `<input />` tag with the given name and HTML attributes.
  717. */
  718. public function password($name, array $options = []) {
  719. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  720. unset($options['value']);
  721. return $this->_render(__METHOD__, $template, compact('name', 'options'));
  722. }
  723. /**
  724. * Generates an HTML `<input type="hidden" />` object.
  725. *
  726. * @param string $name The name of the field.
  727. * @param array $options An array of HTML attributes with which the field should be rendered.
  728. * @return string Returns a `<input />` tag with the given name and HTML attributes.
  729. */
  730. public function hidden($name, array $options = []) {
  731. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  732. return $this->_render(__METHOD__, $template, compact('name', 'options'));
  733. }
  734. /**
  735. * Generates an HTML `<label></label>` object.
  736. *
  737. * @param string $id The DOM ID of the field that the label is for.
  738. * @param string $title The content inside the `<label></label>` object.
  739. * @param array $options Besides HTML attributes, this parameter allows one additional flag:
  740. * - `'escape'` _boolean_: Defaults to `true`. Indicates whether the title of the
  741. * label should be escaped. If `false`, it will be treated as raw HTML.
  742. * @return string Returns a `<label>` tag for the name and with HTML attributes.
  743. */
  744. public function label($id, $title = null, array $options = []) {
  745. $defaults = ['escape' => true];
  746. if (is_array($title)) {
  747. $options = current($title);
  748. $title = key($title);
  749. }
  750. $title = $title ?: Inflector::humanize(str_replace('.', '_', $id));
  751. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $id, $options);
  752. list($scope, $options) = $this->_options($defaults, $options);
  753. if (strpos($id, '.')) {
  754. $generator = $this->_config['attributes']['id'];
  755. $id = $generator(__METHOD__, $id, $options);
  756. }
  757. return $this->_render(__METHOD__, $template, compact('id', 'title', 'options'), $scope);
  758. }
  759. /**
  760. * Generates an error message for a field which is part of an object bound to a form in
  761. * `create()`.
  762. *
  763. * @param string $name The name of the field for which to render an error.
  764. * @param mixed $key If more than one error is present for `$name`, a key may be specified.
  765. * If `$key` is not set in the array of errors, or if `$key` is `true`, the first
  766. * available error is used.
  767. * @param array $options Any rendering options or HTML attributes to be used when rendering
  768. * the error.
  769. * @return string Returns a rendered error message based on the `'error'` string template.
  770. */
  771. public function error($name, $key = null, array $options = []) {
  772. $defaults = ['class' => 'error', 'messages' => []];
  773. list(, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  774. $options += $defaults;
  775. if (is_array($options['messages'])) {
  776. $messages = $options['messages'] + ['default' => null];
  777. } else {
  778. $messages = ['default' => $options['messages']];
  779. }
  780. unset($options['messages']);
  781. $params = compact('name', 'key', 'messages', 'options', 'template');
  782. return Filters::run($this, __FUNCTION__, $params, function($params) {
  783. $options = $params['options'];
  784. $template = $params['template'];
  785. $messages = $params['messages'];
  786. if (isset($options['value'])) {
  787. unset($options['value']);
  788. }
  789. if (!$content = $this->binding($params['name'])->errors) {
  790. return null;
  791. }
  792. $result = '';
  793. if (!is_array($content)) {
  794. return $this->_render(__METHOD__, $template, compact('content', 'options'));
  795. }
  796. $errors = $content;
  797. if ($params['key'] === null) {
  798. foreach ($errors as $rule => $content) {
  799. if (isset($messages[$rule])) {
  800. $content = $messages[$rule];
  801. } elseif ($messages['default']) {
  802. $content = $messages['default'];
  803. }
  804. $result .= $this->_render(__METHOD__, $template, compact('content', 'options'));
  805. }
  806. return $result;
  807. }
  808. $key = $params['key'];
  809. $content = !isset($errors[$key]) || $key === true ? reset($errors) : $errors[$key];
  810. return $this->_render(__METHOD__, $template, compact('content', 'options'));
  811. });
  812. }
  813. /**
  814. * Builds the defaults array for a method by name, according to the config.
  815. *
  816. * @param string $method The name of the method to create defaults for.
  817. * @param string $name The `$name` supplied to the original method.
  818. * @param string $options `$options` from the original method.
  819. * @return array Defaults array contents.
  820. */
  821. protected function _defaults($method, $name, $options) {
  822. $params = compact('method', 'name', 'options');
  823. return Filters::run($this, __FUNCTION__, $params, function($params) {
  824. $method = $params['method'];
  825. $name = $params['name'];
  826. $options = $params['options'];
  827. $methodConfig = isset($this->_config[$method]) ? $this->_config[$method] : [];
  828. $options += $methodConfig + $this->_config['base'];
  829. $options = $this->_generators($method, $name, $options);
  830. $hasValue = (
  831. (!isset($options['value']) || $options['value'] === null) &&
  832. $name && $value = $this->binding($name)->data
  833. );
  834. $isZero = (isset($value) && ($value === 0 || $value === "0"));
  835. if ($hasValue || $isZero) {
  836. $options['value'] = $value;
  837. }
  838. if (isset($options['value']) && !$isZero) {
  839. $isZero = ($options['value'] === 0 || $options['value'] === "0");
  840. }
  841. if (isset($options['default']) && empty($options['value']) && !$isZero) {
  842. $options['value'] = $options['default'];
  843. }
  844. unset($options['default']);
  845. $generator = $this->_config['attributes']['name'];
  846. $name = $generator($method, $name, $options);
  847. $tplKey = isset($options['template']) ? $options['template'] : $method;
  848. $template = isset($this->_templateMap[$tplKey]) ? $this->_templateMap[$tplKey] : $tplKey;
  849. return [$name, $options, $template];
  850. });
  851. }
  852. /**
  853. * Iterates over the configured attribute generators, and modifies the settings for a tag.
  854. *
  855. * @param string $method The name of the helper method which was called, i.e. `'text'`,
  856. * `'select'`, etc.
  857. * @param string $name The name of the field whose attributes are being generated. Some helper
  858. * methods, such as `create()` and `end()`, are not field-based, and therefore
  859. * will have no name.
  860. * @param array $options The options and HTML attributes that will be used to generate the
  861. * helper output.
  862. * @return array Returns the value of the `$options` array, modified by the attribute generators
  863. * added in the `'attributes'` key of the helper's configuration. Note that if a
  864. * generator is present for a field whose value is `false`, that field will be removed
  865. * from the array.
  866. */
  867. protected function _generators($method, $name, $options) {
  868. foreach ($this->_config['attributes'] as $key => $generator) {
  869. if ($key === 'name') {
  870. continue;
  871. }
  872. if ($generator && !isset($options[$key])) {
  873. if (($attr = $generator($method, $name, $options)) !== null) {
  874. $options[$key] = $attr;
  875. }
  876. continue;
  877. }
  878. if ($generator && $options[$key] === false) {
  879. unset($options[$key]);
  880. }
  881. }
  882. return $options;
  883. }
  884. }
  885. ?>