null, 'errorClass' => 'form-error', 'typeMap' => [ 'string' => 'text', 'text' => 'textarea', 'uuid' => 'string', 'datetime' => 'datetime', 'timestamp' => 'datetime', 'date' => 'date', 'time' => 'time', 'boolean' => 'checkbox', 'float' => 'number', 'integer' => 'number', 'tinyinteger' => 'number', 'smallinteger' => 'number', 'decimal' => 'number', 'binary' => 'file', ], 'templates' => [ // Used for button elements in button(). 'button' => '{{text}}', // Used for checkboxes in checkbox() and multiCheckbox(). 'checkbox' => '', // Input group wrapper for checkboxes created via control(). 'checkboxFormGroup' => '{{label}}', // Wrapper container for checkboxes. 'checkboxWrapper' => '
{{label}}
', // Widget ordering for date/time/datetime pickers. 'dateWidget' => '{{year}}{{month}}{{day}}{{hour}}{{minute}}{{second}}{{meridian}}', // Error message wrapper elements. 'error' => '
{{content}}
', // Container for error items. 'errorList' => '', // Error item wrapper. 'errorItem' => '
  • {{text}}
  • ', // File input used by file(). 'file' => '', // Fieldset element used by allControls(). 'fieldset' => '{{content}}', // Open tag used by create(). 'formStart' => '', // Close tag used by end(). 'formEnd' => '', // General grouping container for control(). Defines input/label ordering. 'formGroup' => '{{label}}{{input}}', // Wrapper content used to hide other content. 'hiddenBlock' => '
    {{content}}
    ', // Generic input element. 'input' => '', // Submit input element. 'inputSubmit' => '', // Container element used by control(). 'inputContainer' => '
    {{content}}
    ', // Container element used by control() when a field has an error. 'inputContainerError' => '
    {{content}}{{error}}
    ', // Label element when inputs are not nested inside the label. 'label' => '{{text}}', // Label element used for radio and multi-checkbox inputs. 'nestingLabel' => '{{hidden}}{{input}}{{text}}', // Legends created by allControls() 'legend' => '{{text}}', // Multi-Checkbox input set title element. 'multicheckboxTitle' => '{{text}}', // Multi-Checkbox wrapping container. 'multicheckboxWrapper' => '{{content}}', // Option element used in select pickers. 'option' => '', // Option group element used in select pickers. 'optgroup' => '{{content}}', // Select element, 'select' => '', // Multi-select element, 'selectMultiple' => '', // Radio input element, 'radio' => '', // Wrapping container for radio input/label, 'radioWrapper' => '{{label}}', // Textarea input element, 'textarea' => '', // Container for submit buttons. 'submitContainer' => '
    {{content}}
    ', //Confirm javascript template for postLink() 'confirmJs' => '{{confirm}}', ], // set HTML5 validation message to custom required/empty messages 'autoSetCustomValidity' => false, ]; /** * Default widgets * * @var array */ protected $_defaultWidgets = [ 'button' => ['Button'], 'checkbox' => ['Checkbox'], 'file' => ['File'], 'label' => ['Label'], 'nestingLabel' => ['NestingLabel'], 'multicheckbox' => ['MultiCheckbox', 'nestingLabel'], 'radio' => ['Radio', 'nestingLabel'], 'select' => ['SelectBox'], 'textarea' => ['Textarea'], 'datetime' => ['DateTime', 'select'], '_default' => ['Basic'], ]; /** * List of fields created, used with secure forms. * * @var string[] */ public $fields = []; /** * Constant used internally to skip the securing process, * and neither add the field to the hash or to the unlocked fields. * * @var string */ const SECURE_SKIP = 'skip'; /** * Defines the type of form being created. Set by FormHelper::create(). * * @var string|null */ public $requestType; /** * An array of field names that have been excluded from * the Token hash used by SecurityComponent's validatePost method * * @see \Cake\View\Helper\FormHelper::_secure() * @see \Cake\Controller\Component\SecurityComponent::validatePost() * @var string[] */ protected $_unlockedFields = []; /** * Locator for input widgets. * * @var \Cake\View\Widget\WidgetLocator */ protected $_locator; /** * Context for the current form. * * @var \Cake\View\Form\ContextInterface|null */ protected $_context; /** * Context factory. * * @var \Cake\View\Form\ContextFactory */ protected $_contextFactory; /** * The action attribute value of the last created form. * Used to make form/request specific hashes for SecurityComponent. * * @var string */ protected $_lastAction = ''; /** * The sources to be used when retrieving prefilled input values. * * @var string[] */ protected $_valueSources = ['context']; /** * Grouped input types. * * @var string[] */ protected $_groupedInputTypes = ['radio', 'multicheckbox', 'date', 'time', 'datetime']; /** * Construct the widgets and binds the default context providers * * @param \Cake\View\View $View The View this helper is being attached to. * @param array $config Configuration settings for the helper. */ public function __construct(View $View, array $config = []) { $locator = null; $widgets = $this->_defaultWidgets; if (isset($config['registry'])) { deprecationWarning('`registry` config key is deprecated in FormHelper, use `locator` instead.'); $config['locator'] = $config['registry']; unset($config['registry']); } if (isset($config['locator'])) { $locator = $config['locator']; unset($config['locator']); } if (isset($config['widgets'])) { if (is_string($config['widgets'])) { $config['widgets'] = (array)$config['widgets']; } $widgets = $config['widgets'] + $widgets; unset($config['widgets']); } if (isset($config['groupedInputTypes'])) { $this->_groupedInputTypes = $config['groupedInputTypes']; unset($config['groupedInputTypes']); } parent::__construct($View, $config); if (!$locator) { $locator = new WidgetLocator($this->templater(), $this->_View, $widgets); } $this->setWidgetLocator($locator); $this->_idPrefix = $this->getConfig('idPrefix'); } /** * Set the widget registry the helper will use. * * @param \Cake\View\Widget\WidgetLocator|null $instance The registry instance to set. * @param array $widgets An array of widgets * @return \Cake\View\Widget\WidgetLocator * @deprecated 3.6.0 Use FormHelper::widgetLocator() instead. */ public function widgetRegistry(WidgetRegistry $instance = null, $widgets = []) { deprecationWarning('widgetRegistry is deprecated, use widgetLocator instead.'); if ($instance) { $instance->add($widgets); $this->setWidgetLocator($instance); } return $this->getWidgetLocator(); } /** * Get the widget locator currently used by the helper. * * @return \Cake\View\Widget\WidgetLocator Current locator instance * @since 3.6.0 */ public function getWidgetLocator() { return $this->_locator; } /** * Set the widget locator the helper will use. * * @param \Cake\View\Widget\WidgetLocator $instance The locator instance to set. * @return $this * @since 3.6.0 */ public function setWidgetLocator(WidgetLocator $instance) { $this->_locator = $instance; return $this; } /** * Set the context factory the helper will use. * * @param \Cake\View\Form\ContextFactory|null $instance The context factory instance to set. * @param array $contexts An array of context providers. * @return \Cake\View\Form\ContextFactory */ public function contextFactory(ContextFactory $instance = null, array $contexts = []) { if ($instance === null) { if ($this->_contextFactory === null) { $this->_contextFactory = ContextFactory::createWithDefaults($contexts); } return $this->_contextFactory; } $this->_contextFactory = $instance; return $this->_contextFactory; } /** * Returns an HTML form element. * * ### Options: * * - `type` Form method defaults to autodetecting based on the form context. If * the form context's isCreate() method returns false, a PUT request will be done. * - `method` Set the form's method attribute explicitly. * - `action` The controller action the form submits to, (optional). Use this option if you * don't need to change the controller from the current request's controller. Deprecated since 3.2, use `url`. * - `url` The URL the form submits to. Can be a string or a URL array. If you use 'url' * you should leave 'action' undefined. * - `encoding` Set the accept-charset encoding for the form. Defaults to `Configure::read('App.encoding')` * - `enctype` Set the form encoding explicitly. By default `type => file` will set `enctype` * to `multipart/form-data`. * - `templates` The templates you want to use for this form. Any templates will be merged on top of * the already loaded templates. This option can either be a filename in /config that contains * the templates you want to load, or an array of templates to use. * - `context` Additional options for the context class. For example the EntityContext accepts a 'table' * option that allows you to set the specific Table class the form should be based on. * - `idPrefix` Prefix for generated ID attributes. * - `valueSources` The sources that values should be read from. See FormHelper::setValueSources() * - `templateVars` Provide template variables for the formStart template. * * @param mixed $context The context for which the form is being defined. * Can be a ContextInterface instance, ORM entity, ORM resultset, or an * array of meta data. You can use false or null to make a context-less form. * @param array $options An array of html attributes and options. * @return string An formatted opening FORM tag. * @link https://book.cakephp.org/3/en/views/helpers/form.html#Cake\View\Helper\FormHelper::create */ public function create($context = null, array $options = []) { $append = ''; if ($context instanceof ContextInterface) { $this->context($context); } else { if (empty($options['context'])) { $options['context'] = []; } $options['context']['entity'] = $context; $context = $this->_getContext($options['context']); unset($options['context']); } $isCreate = $context->isCreate(); $options += [ 'type' => $isCreate ? 'post' : 'put', 'action' => null, 'url' => null, 'encoding' => strtolower(Configure::read('App.encoding')), 'templates' => null, 'idPrefix' => null, 'valueSources' => null, ]; if (isset($options['action'])) { trigger_error('Using key `action` is deprecated, use `url` directly instead.', E_USER_DEPRECATED); } if (isset($options['valueSources'])) { $this->setValueSources($options['valueSources']); unset($options['valueSources']); } if ($options['idPrefix'] !== null) { $this->_idPrefix = $options['idPrefix']; } $templater = $this->templater(); if (!empty($options['templates'])) { $templater->push(); $method = is_string($options['templates']) ? 'load' : 'add'; $templater->{$method}($options['templates']); } unset($options['templates']); if ($options['action'] === false || $options['url'] === false) { $url = $this->_View->getRequest()->getRequestTarget(); $action = null; } else { $url = $this->_formUrl($context, $options); $action = $this->Url->build($url); } $this->_lastAction($url); unset($options['url'], $options['action'], $options['idPrefix']); $htmlAttributes = []; switch (strtolower($options['type'])) { case 'get': $htmlAttributes['method'] = 'get'; break; // Set enctype for form case 'file': $htmlAttributes['enctype'] = 'multipart/form-data'; $options['type'] = $isCreate ? 'post' : 'put'; // Move on case 'post': // Move on case 'put': // Move on case 'delete': // Set patch method case 'patch': $append .= $this->hidden('_method', [ 'name' => '_method', 'value' => strtoupper($options['type']), 'secure' => static::SECURE_SKIP, ]); // Default to post method default: $htmlAttributes['method'] = 'post'; } if (isset($options['method'])) { $htmlAttributes['method'] = strtolower($options['method']); } if (isset($options['enctype'])) { $htmlAttributes['enctype'] = strtolower($options['enctype']); } $this->requestType = strtolower($options['type']); if (!empty($options['encoding'])) { $htmlAttributes['accept-charset'] = $options['encoding']; } unset($options['type'], $options['encoding']); $htmlAttributes += $options; $this->fields = []; if ($this->requestType !== 'get') { $append .= $this->_csrfField(); } if (!empty($append)) { $append = $templater->format('hiddenBlock', ['content' => $append]); } $actionAttr = $templater->formatAttributes(['action' => $action, 'escape' => false]); return $this->formatTemplate('formStart', [ 'attrs' => $templater->formatAttributes($htmlAttributes) . $actionAttr, 'templateVars' => isset($options['templateVars']) ? $options['templateVars'] : [], ]) . $append; } /** * Create the URL for a form based on the options. * * @param \Cake\View\Form\ContextInterface $context The context object to use. * @param array $options An array of options from create() * @return string|array The action attribute for the form. */ protected function _formUrl($context, $options) { $request = $this->_View->getRequest(); if ($options['action'] === null && $options['url'] === null) { return $request->getRequestTarget(); } if ( is_string($options['url']) || (is_array($options['url']) && isset($options['url']['_name'])) ) { return $options['url']; } if (isset($options['action']) && empty($options['url']['action'])) { $options['url']['action'] = $options['action']; } $actionDefaults = [ 'plugin' => $this->_View->getPlugin(), 'controller' => $request->getParam('controller'), 'action' => $request->getParam('action'), ]; $action = (array)$options['url'] + $actionDefaults; $pk = $context->primaryKey(); if (count($pk)) { $id = $this->getSourceValue($pk[0]); } if (empty($action[0]) && isset($id)) { $action[0] = $id; } return $action; } /** * Correctly store the last created form action URL. * * @param string|array $url The URL of the last form. * @return void */ protected function _lastAction($url) { $action = Router::url($url, true); $query = parse_url($action, PHP_URL_QUERY); $query = $query ? '?' . $query : ''; $path = parse_url($action, PHP_URL_PATH) ?: ''; $this->_lastAction = $path . $query; } /** * Return a CSRF input if the request data is present. * Used to secure forms in conjunction with CsrfComponent & * SecurityComponent * * @return string */ protected function _csrfField() { $request = $this->_View->getRequest(); if ($request->getParam('_Token.unlockedFields')) { foreach ((array)$request->getParam('_Token.unlockedFields') as $unlocked) { $this->_unlockedFields[] = $unlocked; } } if (!$request->getParam('_csrfToken')) { return ''; } return $this->hidden('_csrfToken', [ 'value' => $request->getParam('_csrfToken'), 'secure' => static::SECURE_SKIP, 'autocomplete' => 'off', ]); } /** * Closes an HTML form, cleans up values set by FormHelper::create(), and writes hidden * input fields where appropriate. * * Resets some parts of the state, shared among multiple FormHelper::create() calls, to defaults. * * @param array $secureAttributes Secure attributes which will be passed as HTML attributes * into the hidden input elements generated for the Security Component. * @return string A closing FORM tag. * @link https://book.cakephp.org/3/en/views/helpers/form.html#closing-the-form */ public function end(array $secureAttributes = []) { $out = ''; if ($this->requestType !== 'get' && $this->_View->getRequest()->getParam('_Token')) { $out .= $this->secure($this->fields, $secureAttributes); $this->fields = []; $this->_unlockedFields = []; } $out .= $this->formatTemplate('formEnd', []); $this->templater()->pop(); $this->requestType = null; $this->_context = null; $this->_valueSources = ['context']; $this->_idPrefix = $this->getConfig('idPrefix'); return $out; } /** * Generates a hidden field with a security hash based on the fields used in * the form. * * If $secureAttributes is set, these HTML attributes will be merged into * the hidden input tags generated for the Security Component. This is * especially useful to set HTML5 attributes like 'form'. * * @param array $fields If set specifies the list of fields to use when * generating the hash, else $this->fields is being used. * @param array $secureAttributes will be passed as HTML attributes into the hidden * input elements generated for the Security Component. * @return string A hidden input field with a security hash, or empty string when * secured forms are not in use. */ public function secure(array $fields = [], array $secureAttributes = []) { if (!$this->_View->getRequest()->getParam('_Token')) { return ''; } $debugSecurity = Configure::read('debug'); if (isset($secureAttributes['debugSecurity'])) { $debugSecurity = $debugSecurity && $secureAttributes['debugSecurity']; unset($secureAttributes['debugSecurity']); } $secureAttributes['secure'] = static::SECURE_SKIP; $secureAttributes['autocomplete'] = 'off'; $tokenData = $this->_buildFieldToken( $this->_lastAction, $fields, $this->_unlockedFields ); $tokenFields = array_merge($secureAttributes, [ 'value' => $tokenData['fields'], ]); $out = $this->hidden('_Token.fields', $tokenFields); $tokenUnlocked = array_merge($secureAttributes, [ 'value' => $tokenData['unlocked'], ]); $out .= $this->hidden('_Token.unlocked', $tokenUnlocked); if ($debugSecurity) { $tokenDebug = array_merge($secureAttributes, [ 'value' => urlencode(json_encode([ $this->_lastAction, $fields, $this->_unlockedFields, ])), ]); $out .= $this->hidden('_Token.debug', $tokenDebug); } return $this->formatTemplate('hiddenBlock', ['content' => $out]); } /** * Add to or get the list of fields that are currently unlocked. * Unlocked fields are not included in the field hash used by SecurityComponent * unlocking a field once its been added to the list of secured fields will remove * it from the list of fields. * * @param string|null $name The dot separated name for the field. * @return array|null Either null, or the list of fields. * @link https://book.cakephp.org/3/en/views/helpers/form.html#working-with-securitycomponent */ public function unlockField($name = null) { if ($name === null) { return $this->_unlockedFields; } if (!in_array($name, $this->_unlockedFields, true)) { $this->_unlockedFields[] = $name; } $index = array_search($name, $this->fields, true); if ($index !== false) { unset($this->fields[$index]); } unset($this->fields[$name]); } /** * Determine which fields of a form should be used for hash. * Populates $this->fields * * @param bool $lock Whether this field should be part of the validation * or excluded as part of the unlockedFields. * @param string|array $field Reference to field to be secured. Can be dot * separated string to indicate nesting or array of fieldname parts. * @param mixed $value Field value, if value should not be tampered with. * @return void */ protected function _secure($lock, $field, $value = null) { if (empty($field) && $field !== '0') { return; } if (is_string($field)) { $field = Hash::filter(explode('.', $field)); } foreach ($this->_unlockedFields as $unlockField) { $unlockParts = explode('.', $unlockField); if (array_values(array_intersect($field, $unlockParts)) === $unlockParts) { return; } } $field = implode('.', $field); $field = preg_replace('/(\.\d+)+$/', '', $field); if ($lock) { if (!in_array($field, $this->fields, true)) { if ($value !== null) { $this->fields[$field] = $value; return; } if (isset($this->fields[$field]) && $value === null) { unset($this->fields[$field]); } $this->fields[] = $field; } } else { $this->unlockField($field); } } /** * Returns true if there is an error for the given field, otherwise false * * @param string $field This should be "modelname.fieldname" * @return bool If there are errors this method returns true, else false. * @link https://book.cakephp.org/3/en/views/helpers/form.html#displaying-and-checking-errors */ public function isFieldError($field) { return $this->_getContext()->hasError($field); } /** * Returns a formatted error message for given form field, '' if no errors. * * Uses the `error`, `errorList` and `errorItem` templates. The `errorList` and * `errorItem` templates are used to format multiple error messages per field. * * ### Options: * * - `escape` boolean - Whether or not to html escape the contents of the error. * * @param string $field A field name, like "modelname.fieldname" * @param string|array|null $text Error message as string or array of messages. If an array, * it should be a hash of key names => messages. * @param array $options See above. * @return string Formatted errors or ''. * @link https://book.cakephp.org/3/en/views/helpers/form.html#displaying-and-checking-errors */ public function error($field, $text = null, array $options = []) { if (substr($field, -5) === '._ids') { $field = substr($field, 0, -5); } $options += ['escape' => true]; $context = $this->_getContext(); if (!$context->hasError($field)) { return ''; } $error = $context->error($field); if (is_array($text)) { $tmp = []; foreach ($error as $k => $e) { if (isset($text[$k])) { $tmp[] = $text[$k]; } elseif (isset($text[$e])) { $tmp[] = $text[$e]; } else { $tmp[] = $e; } } $text = $tmp; } if ($text !== null) { $error = $text; } if ($options['escape']) { $error = h($error); unset($options['escape']); } if (is_array($error)) { if (count($error) > 1) { $errorText = []; foreach ($error as $err) { $errorText[] = $this->formatTemplate('errorItem', ['text' => $err]); } $error = $this->formatTemplate('errorList', [ 'content' => implode('', $errorText), ]); } else { $error = array_pop($error); } } return $this->formatTemplate('error', ['content' => $error]); } /** * Returns a formatted LABEL element for HTML forms. * * Will automatically generate a `for` attribute if one is not provided. * * ### Options * * - `for` - Set the for attribute, if its not defined the for attribute * will be generated from the $fieldName parameter using * FormHelper::_domId(). * - `escape` - Set to `false` to turn off escaping of label text. * Defaults to `true`. * * Examples: * * The text and for attribute are generated off of the fieldname * * ``` * echo $this->Form->label('published'); * * ``` * * Custom text: * * ``` * echo $this->Form->label('published', 'Publish'); * * ``` * * Custom attributes: * * ``` * echo $this->Form->label('published', 'Publish', [ * 'for' => 'post-publish' * ]); * * ``` * * Nesting an input tag: * * ``` * echo $this->Form->label('published', 'Publish', [ * 'for' => 'published', * 'input' => $this->text('published'), * ]); * * ``` * * If you want to nest inputs in the labels, you will need to modify the default templates. * * @param string $fieldName This should be "modelname.fieldname" * @param string|null $text Text that will appear in the label field. If * $text is left undefined the text will be inflected from the * fieldName. * @param array $options An array of HTML attributes. * @return string The formatted LABEL element * @link https://book.cakephp.org/3/en/views/helpers/form.html#creating-labels */ public function label($fieldName, $text = null, array $options = []) { if ($text === null) { $text = $fieldName; if (substr($text, -5) === '._ids') { $text = substr($text, 0, -5); } if (strpos($text, '.') !== false) { $fieldElements = explode('.', $text); $text = array_pop($fieldElements); } if (substr($text, -3) === '_id') { $text = substr($text, 0, -3); } $text = __(Inflector::humanize(Inflector::underscore($text))); } if (isset($options['for'])) { $labelFor = $options['for']; unset($options['for']); } else { $labelFor = $this->_domId($fieldName); } $attrs = $options + [ 'for' => $labelFor, 'text' => $text, ]; if (isset($options['input'])) { if (is_array($options['input'])) { $attrs = $options['input'] + $attrs; } return $this->widget('nestingLabel', $attrs); } return $this->widget('label', $attrs); } /** * Generate a set of controls for `$fields`. If $fields is empty the fields * of current model will be used. * * You can customize individual controls through `$fields`. * ``` * $this->Form->allControls([ * 'name' => ['label' => 'custom label'] * ]); * ``` * * You can exclude fields by specifying them as `false`: * * ``` * $this->Form->allControls(['title' => false]); * ``` * * In the above example, no field would be generated for the title field. * * @param array $fields An array of customizations for the fields that will be * generated. This array allows you to set custom types, labels, or other options. * @param array $options Options array. Valid keys are: * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will * be enabled * - `legend` Set to false to disable the legend for the generated control set. Or supply a string * to customize the legend text. * @return string Completed form controls. * @link https://book.cakephp.org/3/en/views/helpers/form.html#generating-entire-forms */ public function allControls(array $fields = [], array $options = []) { $context = $this->_getContext(); $modelFields = $context->fieldNames(); $fields = array_merge( Hash::normalize($modelFields), Hash::normalize($fields) ); return $this->controls($fields, $options); } /** * Generate a set of controls for `$fields`. If $fields is empty the fields * of current model will be used. * * @param array $fields An array of customizations for the fields that will be * generated. This array allows you to set custom types, labels, or other options. * @param array $options Options array. Valid keys are: * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will * be enabled * - `legend` Set to false to disable the legend for the generated control set. Or supply a string * to customize the legend text. * @return string Completed form controls. * @link https://book.cakephp.org/3/en/views/helpers/form.html#generating-entire-forms * @deprecated 3.4.0 Use FormHelper::allControls() instead. */ public function allInputs(array $fields = [], array $options = []) { deprecationWarning( 'FormHelper::allInputs() is deprecated. ' . 'Use FormHelper::allControls() instead.' ); return $this->allControls($fields, $options); } /** * Generate a set of controls for `$fields` wrapped in a fieldset element. * * You can customize individual controls through `$fields`. * ``` * $this->Form->controls([ * 'name' => ['label' => 'custom label'], * 'email' * ]); * ``` * * @param array $fields An array of the fields to generate. This array allows * you to set custom types, labels, or other options. * @param array $options Options array. Valid keys are: * - `fieldset` Set to false to disable the fieldset. You can also pass an * array of params to be applied as HTML attributes to the fieldset tag. * If you pass an empty array, the fieldset will be enabled. * - `legend` Set to false to disable the legend for the generated input set. * Or supply a string to customize the legend text. * @return string Completed form inputs. * @link https://book.cakephp.org/3/en/views/helpers/form.html#generating-entire-forms */ public function controls(array $fields, array $options = []) { $fields = Hash::normalize($fields); $out = ''; foreach ($fields as $name => $opts) { if ($opts === false) { continue; } $out .= $this->control($name, (array)$opts); } return $this->fieldset($out, $options); } /** * Generate a set of controls for `$fields` wrapped in a fieldset element. * * @param array $fields An array of the fields to generate. This array allows * you to set custom types, labels, or other options. * @param array $options Options array. Valid keys are: * - `fieldset` Set to false to disable the fieldset. You can also pass an * array of params to be applied as HTML attributes to the fieldset tag. * If you pass an empty array, the fieldset will be enabled. * - `legend` Set to false to disable the legend for the generated input set. * Or supply a string to customize the legend text. * @return string Completed form inputs. * @link https://book.cakephp.org/3/en/views/helpers/form.html#generating-entire-forms * @deprecated 3.4.0 Use FormHelper::controls() instead. */ public function inputs(array $fields, array $options = []) { deprecationWarning( 'FormHelper::inputs() is deprecated. ' . 'Use FormHelper::controls() instead.' ); return $this->controls($fields, $options); } /** * Wrap a set of inputs in a fieldset * * @param string $fields the form inputs to wrap in a fieldset * @param array $options Options array. Valid keys are: * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will * be enabled * - `legend` Set to false to disable the legend for the generated input set. Or supply a string * to customize the legend text. * @return string Completed form inputs. */ public function fieldset($fields = '', array $options = []) { $fieldset = $legend = true; $context = $this->_getContext(); $out = $fields; if (isset($options['legend'])) { $legend = $options['legend']; } if (isset($options['fieldset'])) { $fieldset = $options['fieldset']; } if ($legend === true) { $isCreate = $context->isCreate(); $modelName = Inflector::humanize(Inflector::singularize($this->_View->getRequest()->getParam('controller'))); if (!$isCreate) { $legend = __d('cake', 'Edit {0}', $modelName); } else { $legend = __d('cake', 'New {0}', $modelName); } } if ($fieldset !== false) { if ($legend) { $out = $this->formatTemplate('legend', ['text' => $legend]) . $out; } $fieldsetParams = ['content' => $out, 'attrs' => '']; if (is_array($fieldset) && !empty($fieldset)) { $fieldsetParams['attrs'] = $this->templater()->formatAttributes($fieldset); } $out = $this->formatTemplate('fieldset', $fieldsetParams); } return $out; } /** * Generates a form control element complete with label and wrapper div. * * ### Options * * See each field type method for more information. Any options that are part of * $attributes or $options for the different **type** methods can be included in `$options` for control(). * Additionally, any unknown keys that are not in the list below, or part of the selected type's options * will be treated as a regular HTML attribute for the generated input. * * - `type` - Force the type of widget you want. e.g. `type => 'select'` * - `label` - Either a string label, or an array of options for the label. See FormHelper::label(). * - `options` - For widgets that take options e.g. radio, select. * - `error` - Control the error message that is produced. Set to `false` to disable any kind of error reporting (field * error and error messages). * - `empty` - String or boolean to enable empty select box options. * - `nestedInput` - Used with checkbox and radio inputs. Set to false to render inputs outside of label * elements. Can be set to true on any input to force the input inside the label. If you * enable this option for radio buttons you will also need to modify the default `radioWrapper` template. * - `templates` - The templates you want to use for this input. Any templates will be merged on top of * the already loaded templates. This option can either be a filename in /config that contains * the templates you want to load, or an array of templates to use. * - `labelOptions` - Either `false` to disable label around nestedWidgets e.g. radio, multicheckbox or an array * of attributes for the label tag. `selected` will be added to any classes e.g. `class => 'myclass'` where * widget is checked * * @param string $fieldName This should be "modelname.fieldname" * @param array $options Each type of input takes different options. * @return string Completed form widget. * @link https://book.cakephp.org/3/en/views/helpers/form.html#creating-form-inputs */ public function control($fieldName, array $options = []) { $options += [ 'type' => null, 'label' => null, 'error' => null, 'required' => null, 'options' => null, 'templates' => [], 'templateVars' => [], 'labelOptions' => true, ]; $options = $this->_parseOptions($fieldName, $options); $options += ['id' => $this->_domId($fieldName)]; $templater = $this->templater(); $newTemplates = $options['templates']; if ($newTemplates) { $templater->push(); $templateMethod = is_string($options['templates']) ? 'load' : 'add'; $templater->{$templateMethod}($options['templates']); } unset($options['templates']); $error = null; $errorSuffix = ''; if ($options['type'] !== 'hidden' && $options['error'] !== false) { if (is_array($options['error'])) { $error = $this->error($fieldName, $options['error'], $options['error']); } else { $error = $this->error($fieldName, $options['error']); } $errorSuffix = empty($error) ? '' : 'Error'; unset($options['error']); } $label = $options['label']; unset($options['label']); $labelOptions = $options['labelOptions']; unset($options['labelOptions']); $nestedInput = false; if ($options['type'] === 'checkbox') { $nestedInput = true; } $nestedInput = isset($options['nestedInput']) ? $options['nestedInput'] : $nestedInput; unset($options['nestedInput']); if ($nestedInput === true && $options['type'] === 'checkbox' && !array_key_exists('hiddenField', $options) && $label !== false) { $options['hiddenField'] = '_split'; } $input = $this->_getInput($fieldName, $options + ['labelOptions' => $labelOptions]); if ($options['type'] === 'hidden' || $options['type'] === 'submit') { if ($newTemplates) { $templater->pop(); } return $input; } $label = $this->_getLabel($fieldName, compact('input', 'label', 'error', 'nestedInput') + $options); if ($nestedInput) { $result = $this->_groupTemplate(compact('label', 'error', 'options')); } else { $result = $this->_groupTemplate(compact('input', 'label', 'error', 'options')); } $result = $this->_inputContainerTemplate([ 'content' => $result, 'error' => $error, 'errorSuffix' => $errorSuffix, 'options' => $options, ]); if ($newTemplates) { $templater->pop(); } return $result; } /** * Generates a form control element complete with label and wrapper div. * * @param string $fieldName This should be "modelname.fieldname" * @param array $options Each type of input takes different options. * @return string Completed form widget. * @link https://book.cakephp.org/3/en/views/helpers/form.html#creating-form-inputs * @deprecated 3.4.0 Use FormHelper::control() instead. */ public function input($fieldName, array $options = []) { deprecationWarning( 'FormHelper::input() is deprecated. ' . 'Use FormHelper::control() instead.' ); return $this->control($fieldName, $options); } /** * Generates an group template element * * @param array $options The options for group template * @return string The generated group template */ protected function _groupTemplate($options) { $groupTemplate = $options['options']['type'] . 'FormGroup'; if (!$this->templater()->get($groupTemplate)) { $groupTemplate = 'formGroup'; } return $this->formatTemplate($groupTemplate, [ 'input' => isset($options['input']) ? $options['input'] : [], 'label' => $options['label'], 'error' => $options['error'], 'templateVars' => isset($options['options']['templateVars']) ? $options['options']['templateVars'] : [], ]); } /** * Generates an input container template * * @param array $options The options for input container template * @return string The generated input container template */ protected function _inputContainerTemplate($options) { $inputContainerTemplate = $options['options']['type'] . 'Container' . $options['errorSuffix']; if (!$this->templater()->get($inputContainerTemplate)) { $inputContainerTemplate = 'inputContainer' . $options['errorSuffix']; } return $this->formatTemplate($inputContainerTemplate, [ 'content' => $options['content'], 'error' => $options['error'], 'required' => $options['options']['required'] ? ' required' : '', 'type' => $options['options']['type'], 'templateVars' => isset($options['options']['templateVars']) ? $options['options']['templateVars'] : [], ]); } /** * Generates an input element * * @param string $fieldName the field name * @param array $options The options for the input element * @return string The generated input element */ protected function _getInput($fieldName, $options) { $label = $options['labelOptions']; unset($options['labelOptions']); switch (strtolower($options['type'])) { case 'select': $opts = $options['options']; unset($options['options']); return $this->select($fieldName, $opts, $options + ['label' => $label]); case 'radio': $opts = $options['options']; unset($options['options']); return $this->radio($fieldName, $opts, $options + ['label' => $label]); case 'multicheckbox': $opts = $options['options']; unset($options['options']); return $this->multiCheckbox($fieldName, $opts, $options + ['label' => $label]); case 'input': throw new RuntimeException("Invalid type 'input' used for field '$fieldName'"); default: return $this->{$options['type']}($fieldName, $options); } } /** * Generates input options array * * @param string $fieldName The name of the field to parse options for. * @param array $options Options list. * @return array Options */ protected function _parseOptions($fieldName, $options) { $needsMagicType = false; if (empty($options['type'])) { $needsMagicType = true; $options['type'] = $this->_inputType($fieldName, $options); } $options = $this->_magicOptions($fieldName, $options, $needsMagicType); return $options; } /** * Returns the input type that was guessed for the provided fieldName, * based on the internal type it is associated too, its name and the * variables that can be found in the view template * * @param string $fieldName the name of the field to guess a type for * @param array $options the options passed to the input method * @return string */ protected function _inputType($fieldName, $options) { $context = $this->_getContext(); if ($context->isPrimaryKey($fieldName)) { return 'hidden'; } if (substr($fieldName, -3) === '_id') { return 'select'; } $internalType = $context->type($fieldName); $map = $this->_config['typeMap']; $type = isset($map[$internalType]) ? $map[$internalType] : 'text'; $fieldName = array_slice(explode('.', $fieldName), -1)[0]; switch (true) { case isset($options['checked']): return 'checkbox'; case isset($options['options']): return 'select'; case in_array($fieldName, ['passwd', 'password']): return 'password'; case in_array($fieldName, ['tel', 'telephone', 'phone']): return 'tel'; case $fieldName === 'email': return 'email'; case isset($options['rows']) || isset($options['cols']): return 'textarea'; } return $type; } /** * Selects the variable containing the options for a select field if present, * and sets the value to the 'options' key in the options array. * * @param string $fieldName The name of the field to find options for. * @param array $options Options list. * @return array */ protected function _optionsOptions($fieldName, $options) { if (isset($options['options'])) { return $options; } $pluralize = true; if (substr($fieldName, -5) === '._ids') { $fieldName = substr($fieldName, 0, -5); $pluralize = false; } elseif (substr($fieldName, -3) === '_id') { $fieldName = substr($fieldName, 0, -3); } $fieldName = array_slice(explode('.', $fieldName), -1)[0]; $varName = Inflector::variable( $pluralize ? Inflector::pluralize($fieldName) : $fieldName ); $varOptions = $this->_View->get($varName); if (!is_array($varOptions) && !($varOptions instanceof Traversable)) { return $options; } if ($options['type'] !== 'radio') { $options['type'] = 'select'; } $options['options'] = $varOptions; return $options; } /** * Magically set option type and corresponding options * * @param string $fieldName The name of the field to generate options for. * @param array $options Options list. * @param bool $allowOverride Whether or not it is allowed for this method to * overwrite the 'type' key in options. * @return array */ protected function _magicOptions($fieldName, $options, $allowOverride) { $context = $this->_getContext(); $options += [ 'templateVars' => [], ]; if (!isset($options['required']) && $options['type'] !== 'hidden') { $options['required'] = $context->isRequired($fieldName); } if (method_exists($context, 'getRequiredMessage')) { $message = $context->getRequiredMessage($fieldName); $message = h($message); if ($options['required'] && $message) { $options['templateVars']['customValidityMessage'] = $message; if ($this->getConfig('autoSetCustomValidity')) { $options['oninvalid'] = "this.setCustomValidity(''); if (!this.validity.valid) this.setCustomValidity('$message')"; $options['oninput'] = "this.setCustomValidity('')"; } } } $type = $context->type($fieldName); $fieldDef = $context->attributes($fieldName); if ($options['type'] === 'number' && !isset($options['step'])) { if ($type === 'decimal' && isset($fieldDef['precision'])) { $decimalPlaces = $fieldDef['precision']; $options['step'] = sprintf('%.' . $decimalPlaces . 'F', pow(10, -1 * $decimalPlaces)); } elseif ($type === 'float') { $options['step'] = 'any'; } } $typesWithOptions = ['text', 'number', 'radio', 'select']; $magicOptions = (in_array($options['type'], ['radio', 'select']) || $allowOverride); if ($magicOptions && in_array($options['type'], $typesWithOptions)) { $options = $this->_optionsOptions($fieldName, $options); } if ($allowOverride && substr($fieldName, -5) === '._ids') { $options['type'] = 'select'; if (!isset($options['multiple']) || ($options['multiple'] && $options['multiple'] != 'checkbox')) { $options['multiple'] = true; } } if ($options['type'] === 'select' && array_key_exists('step', $options)) { unset($options['step']); } $typesWithMaxLength = ['text', 'textarea', 'email', 'tel', 'url', 'search']; if ( !array_key_exists('maxlength', $options) && in_array($options['type'], $typesWithMaxLength) ) { $maxLength = null; if (method_exists($context, 'getMaxLength')) { $maxLength = $context->getMaxLength($fieldName); } if ($maxLength === null && !empty($fieldDef['length'])) { $maxLength = $fieldDef['length']; } if ($maxLength !== null) { $options['maxlength'] = min($maxLength, 100000); } } if (in_array($options['type'], ['datetime', 'date', 'time', 'select'])) { $options += ['empty' => false]; } return $options; } /** * Generate label for input * * @param string $fieldName The name of the field to generate label for. * @param array $options Options list. * @return bool|string false or Generated label element */ protected function _getLabel($fieldName, $options) { if ($options['type'] === 'hidden') { return false; } $label = null; if (isset($options['label'])) { $label = $options['label']; } if ($label === false && $options['type'] === 'checkbox') { return $options['input']; } if ($label === false) { return false; } return $this->_inputLabel($fieldName, $label, $options); } /** * Extracts a single option from an options array. * * @param string $name The name of the option to pull out. * @param array $options The array of options you want to extract. * @param mixed $default The default option value * @return mixed the contents of the option or default */ protected function _extractOption($name, $options, $default = null) { if (array_key_exists($name, $options)) { return $options[$name]; } return $default; } /** * Generate a label for an input() call. * * $options can contain a hash of id overrides. These overrides will be * used instead of the generated values if present. * * @param string $fieldName The name of the field to generate label for. * @param string $label Label text. * @param array $options Options for the label element. * @return string Generated label element */ protected function _inputLabel($fieldName, $label, $options) { $options += ['id' => null, 'input' => null, 'nestedInput' => false, 'templateVars' => []]; $labelAttributes = ['templateVars' => $options['templateVars']]; if (is_array($label)) { $labelText = null; if (isset($label['text'])) { $labelText = $label['text']; unset($label['text']); } $labelAttributes = array_merge($labelAttributes, $label); } else { $labelText = $label; } $labelAttributes['for'] = $options['id']; if (in_array($options['type'], $this->_groupedInputTypes, true)) { $labelAttributes['for'] = false; } if ($options['nestedInput']) { $labelAttributes['input'] = $options['input']; } if (isset($options['escape'])) { $labelAttributes['escape'] = $options['escape']; } return $this->label($fieldName, $labelText, $labelAttributes); } /** * Creates a checkbox input widget. * * ### Options: * * - `value` - the value of the checkbox * - `checked` - boolean indicate that this checkbox is checked. * - `hiddenField` - boolean to indicate if you want the results of checkbox() to include * a hidden input with a value of ''. * - `disabled` - create a disabled input. * - `default` - Set the default value for the checkbox. This allows you to start checkboxes * as checked, without having to check the POST data. A matching POST data value, will overwrite * the default value. * * @param string $fieldName Name of a field, like this "modelname.fieldname" * @param array $options Array of HTML attributes. * @return string|array An HTML text input element. * @link https://book.cakephp.org/3/en/views/helpers/form.html#creating-checkboxes */ public function checkbox($fieldName, array $options = []) { $options += ['hiddenField' => true, 'value' => 1]; // Work around value=>val translations. $value = $options['value']; unset($options['value']); $options = $this->_initInputField($fieldName, $options); $options['value'] = $value; $output = ''; if ($options['hiddenField']) { $hiddenOptions = [ 'name' => $options['name'], 'value' => $options['hiddenField'] !== true && $options['hiddenField'] !== '_split' ? $options['hiddenField'] : '0', 'form' => isset($options['form']) ? $options['form'] : null, 'secure' => false, ]; if (isset($options['disabled']) && $options['disabled']) { $hiddenOptions['disabled'] = 'disabled'; } $output = $this->hidden($fieldName, $hiddenOptions); } if ($options['hiddenField'] === '_split') { unset($options['hiddenField'], $options['type']); return ['hidden' => $output, 'input' => $this->widget('checkbox', $options)]; } unset($options['hiddenField'], $options['type']); return $output . $this->widget('checkbox', $options); } /** * Creates a set of radio widgets. * * ### Attributes: * * - `value` - Indicates the value when this radio button is checked. * - `label` - Either `false` to disable label around the widget or an array of attributes for * the label tag. `selected` will be added to any classes e.g. `'class' => 'myclass'` where widget * is checked * - `hiddenField` - boolean to indicate if you want the results of radio() to include * a hidden input with a value of ''. This is useful for creating radio sets that are non-continuous. * - `disabled` - Set to `true` or `disabled` to disable all the radio buttons. Use an array of * values to disable specific radio buttons. * - `empty` - Set to `true` to create an input with the value '' as the first option. When `true` * the radio label will be 'empty'. Set this option to a string to control the label value. * * @param string $fieldName Name of a field, like this "modelname.fieldname" * @param array|\Traversable $options Radio button options array. * @param array $attributes Array of attributes. * @return string Completed radio widget set. * @link https://book.cakephp.org/3/en/views/helpers/form.html#creating-radio-buttons */ public function radio($fieldName, $options = [], array $attributes = []) { $attributes['options'] = $options; $attributes['idPrefix'] = $this->_idPrefix; $attributes = $this->_initInputField($fieldName, $attributes); $hiddenField = isset($attributes['hiddenField']) ? $attributes['hiddenField'] : true; unset($attributes['hiddenField']); $radio = $this->widget('radio', $attributes); $hidden = ''; if ($hiddenField) { $hidden = $this->hidden($fieldName, [ 'value' => $hiddenField === true ? '' : $hiddenField, 'form' => isset($attributes['form']) ? $attributes['form'] : null, 'name' => $attributes['name'], ]); } return $hidden . $radio; } /** * Missing method handler - implements various simple input types. Is used to create inputs * of various types. e.g. `$this->Form->text();` will create `` while * `$this->Form->range();` will create `` * * ### Usage * * ``` * $this->Form->search('User.query', ['value' => 'test']); * ``` * * Will make an input like: * * `` * * The first argument to an input type should always be the fieldname, in `Model.field` format. * The second argument should always be an array of attributes for the input. * * @param string $method Method name / input type to make. * @param array $params Parameters for the method call * @return string Formatted input method. * @throws \Cake\Core\Exception\Exception When there are no params for the method call. */ public function __call($method, $params) { $options = []; if (empty($params)) { throw new Exception(sprintf('Missing field name for FormHelper::%s', $method)); } if (isset($params[1])) { $options = $params[1]; } if (!isset($options['type'])) { $options['type'] = $method; } $options = $this->_initInputField($params[0], $options); return $this->widget($options['type'], $options); } /** * Creates a textarea widget. * * ### Options: * * - `escape` - Whether or not the contents of the textarea should be escaped. Defaults to true. * * @param string $fieldName Name of a field, in the form "modelname.fieldname" * @param array $options Array of HTML attributes, and special options above. * @return string A generated HTML text input element * @link https://book.cakephp.org/3/en/views/helpers/form.html#creating-textareas */ public function textarea($fieldName, array $options = []) { $options = $this->_initInputField($fieldName, $options); unset($options['type']); return $this->widget('textarea', $options); } /** * Creates a hidden input field. * * @param string $fieldName Name of a field, in the form of "modelname.fieldname" * @param array $options Array of HTML attributes. * @return string A generated hidden input * @link https://book.cakephp.org/3/en/views/helpers/form.html#creating-hidden-inputs */ public function hidden($fieldName, array $options = []) { $options += ['required' => false, 'secure' => true]; $secure = $options['secure']; unset($options['secure']); $options = $this->_initInputField($fieldName, array_merge( $options, ['secure' => static::SECURE_SKIP] )); if ($secure === true) { $this->_secure(true, $this->_secureFieldName($options['name']), (string)$options['val']); } $options['type'] = 'hidden'; return $this->widget('hidden', $options); } /** * Creates file input widget. * * @param string $fieldName Name of a field, in the form "modelname.fieldname" * @param array $options Array of HTML attributes. * @return string A generated file input. * @link https://book.cakephp.org/3/en/views/helpers/form.html#creating-file-inputs */ public function file($fieldName, array $options = []) { $options += ['secure' => true]; $options = $this->_initInputField($fieldName, $options); unset($options['type']); return $this->widget('file', $options); } /** * Creates a `