*/ protected array $_defaultConfig = [ 'idPrefix' => null, 'errorClass' => 'form-error', 'typeMap' => [ 'string' => 'text', 'text' => 'textarea', 'uuid' => 'string', 'datetime' => 'datetime', 'datetimefractional' => 'datetime', 'timestamp' => 'datetime', 'timestampfractional' => 'datetime', 'timestamptimezone' => 'datetime', 'date' => 'date', 'time' => 'time', 'year' => 'year', '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}}
', // 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}}', // selected class 'selectedClass' => 'selected', // required class 'requiredClass' => 'required', ], // set HTML5 validation message to custom required/empty messages 'autoSetCustomValidity' => true, ]; /** * Default widgets * * @var array> */ protected array $_defaultWidgets = [ 'button' => ['Button'], 'checkbox' => ['Checkbox'], 'file' => ['File'], 'label' => ['Label'], 'nestingLabel' => ['NestingLabel'], 'multicheckbox' => ['MultiCheckbox', 'nestingLabel'], 'radio' => ['Radio', 'nestingLabel'], 'select' => ['SelectBox'], 'textarea' => ['Textarea'], 'datetime' => ['DateTime', 'select'], 'year' => ['Year', 'select'], '_default' => ['Basic'], ]; /** * Constant used internally to skip the securing process, * and neither add the field to the hash or to the unlocked fields. * * @var string */ public const SECURE_SKIP = 'skip'; /** * Defines the type of form being created. Set by FormHelper::create(). * * @var string|null */ public ?string $requestType = null; /** * Locator for input widgets. * * @var \Cake\View\Widget\WidgetLocator */ protected WidgetLocator $_locator; /** * Context for the current form. * * @var \Cake\View\Form\ContextInterface|null */ protected ?ContextInterface $_context = null; /** * Context factory. * * @var \Cake\View\Form\ContextFactory|null */ protected ?ContextFactory $_contextFactory = null; /** * The action attribute value of the last created form. * Used to make form/request specific hashes for form tampering protection. * * @var string */ protected string $_lastAction = ''; /** * The supported sources that can be used to populate input values. * * `context` - Corresponds to `ContextInterface` instances. * `data` - Corresponds to request data (POST/PUT). * `query` - Corresponds to request's query string. * * @var array */ protected array $supportedValueSources = ['context', 'data', 'query']; /** * The default sources. * * @see FormHelper::$supportedValueSources for valid values. * @var array */ protected array $_valueSources = ['data', 'context']; /** * Grouped input types. * * @var array */ protected array $_groupedInputTypes = ['radio', 'multicheckbox']; /** * Form protector * * @var \Cake\Form\FormProtector|null */ protected ?FormProtector $formProtector = null; /** * 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['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'); } /** * Get the widget locator currently used by the helper. * * @return \Cake\View\Widget\WidgetLocator Current locator instance * @since 3.6.0 */ public function getWidgetLocator(): WidgetLocator { 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 = []): ContextFactory { if ($instance === null) { return $this->_contextFactory ??= ContextFactory::createWithDefaults($contexts); } $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. * - `url` The URL the form submits to. Can be a string or a URL array. * - `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 `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/5/en/views/helpers/form.html#Cake\View\Helper\FormHelper::create */ public function create(mixed $context = null, array $options = []): string { $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', 'url' => null, 'encoding' => strtolower(Configure::read('App.encoding')), 'templates' => null, 'idPrefix' => null, 'valueSources' => null, ]; 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['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['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 '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; if ($this->requestType !== 'get') { $formTokenData = $this->_View->getRequest()->getAttribute('formTokenData'); if ($formTokenData !== null) { $this->formProtector = $this->createFormProtector($formTokenData); } $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' => $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 array|string The action attribute for the form. */ protected function _formUrl(ContextInterface $context, array $options): array|string { $request = $this->_View->getRequest(); if ($options['url'] === null) { return $request->getRequestTarget(); } if ( is_string($options['url']) || (is_array($options['url']) && isset($options['url']['_name'])) ) { return $options['url']; } $actionDefaults = [ 'plugin' => $this->_View->getPlugin(), 'controller' => $request->getParam('controller'), 'action' => $request->getParam('action'), ]; return (array)$options['url'] + $actionDefaults; } /** * Correctly store the last created form action URL. * * @param array|string|null $url The URL of the last form. * @return void */ protected function _lastAction(array|string|null $url = null): void { $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 CsrfMiddleware. * * @return string */ protected function _csrfField(): string { $request = $this->_View->getRequest(); $csrfToken = $request->getAttribute('csrfToken'); if (!$csrfToken) { return ''; } return $this->hidden('_csrfToken', [ 'value' => $csrfToken, 'secure' => static::SECURE_SKIP, ]); } /** * 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/5/en/views/helpers/form.html#closing-the-form */ public function end(array $secureAttributes = []): string { $out = ''; if ($this->requestType !== 'get' && $this->_View->getRequest()->getAttribute('formTokenData') !== null) { $out .= $this->secure([], $secureAttributes); } $out .= $this->formatTemplate('formEnd', []); $this->templater()->pop(); $this->requestType = null; $this->_context = null; $this->_valueSources = ['data', 'context']; $this->_idPrefix = $this->getConfig('idPrefix'); $this->formProtector = null; 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 be added to * FormProtector for generating the hash. * @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 = []): string { if (!$this->formProtector) { return ''; } foreach ($fields as $field => $value) { if (is_int($field)) { $field = $value; $value = null; } $this->formProtector->addField($field, true, $value); } $debugSecurity = (bool)Configure::read('debug'); if (isset($secureAttributes['debugSecurity'])) { $debugSecurity = $debugSecurity && $secureAttributes['debugSecurity']; unset($secureAttributes['debugSecurity']); } $secureAttributes['secure'] = static::SECURE_SKIP; $tokenData = $this->formProtector->buildTokenData( $this->_lastAction, $this->_getFormProtectorSessionId() ); $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' => $tokenData['debug'], ]); $out .= $this->hidden('_Token.debug', $tokenDebug); } return $this->formatTemplate('hiddenBlock', ['content' => $out]); } /** * Get Session id for FormProtector * Must be the same as in FormProtectionComponent * * @return string */ protected function _getFormProtectorSessionId(): string { return $this->_View->getRequest()->getSession()->id(); } /** * Add to the list of fields that are currently unlocked. * * Unlocked fields are not included in the form protection field hash. * * @param string $name The dot separated name for the field. * @return $this */ public function unlockField(string $name) { $this->getFormProtector()->unlockField($name); return $this; } /** * Create FormProtector instance. * * @param array $formTokenData Token data. * @return \Cake\Form\FormProtector */ protected function createFormProtector(array $formTokenData): FormProtector { $session = $this->_View->getRequest()->getSession(); $session->start(); return new FormProtector( $formTokenData ); } /** * Get form protector instance. * * @return \Cake\Form\FormProtector * @throws \Cake\Core\Exception\CakeException */ public function getFormProtector(): FormProtector { if ($this->formProtector === null) { throw new CakeException( '`FormProtector` instance has not been created. Ensure you have loaded the `FormProtectionComponent`' . ' in your controller and called `FormHelper::create()` before calling `FormHelper::unlockField()`.' ); } return $this->formProtector; } /** * 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/5/en/views/helpers/form.html#displaying-and-checking-errors */ public function isFieldError(string $field): bool { 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 to html escape the contents of the error. * * @param string $field A field name, like "modelname.fieldname" * @param array|string|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/5/en/views/helpers/form.html#displaying-and-checking-errors */ public function error(string $field, array|string|null $text = null, array $options = []): string { if (str_ends_with($field, '._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, 'id' => $this->_domId($field) . '-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/5/en/views/helpers/form.html#creating-labels */ public function label(string $fieldName, ?string $text = null, array $options = []): string { if ($text === null) { $text = $fieldName; if (str_ends_with($text, '._ids')) { $text = substr($text, 0, -5); } if (str_contains($text, '.')) { $fieldElements = explode('.', $text); $text = array_pop($fieldElements); } if (str_ends_with($text, '_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/5/en/views/helpers/form.html#generating-entire-forms */ public function allControls(array $fields = [], array $options = []): string { $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` 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/5/en/views/helpers/form.html#generating-entire-forms */ public function controls(array $fields, array $options = []): string { $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); } /** * 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(string $fields = '', array $options = []): string { $legend = $options['legend'] ?? true; $fieldset = $options['fieldset'] ?? true; $context = $this->_getContext(); $out = $fields; 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/5/en/views/helpers/form.html#creating-form-controls */ public function control(string $fieldName, array $options = []): string { $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']); // Hidden inputs don't need aria. // Multiple checkboxes can't have aria generated for them at this layer. if ($options['type'] !== 'hidden' && ($options['type'] !== 'select' && !isset($options['multiple']))) { $isFieldError = $this->isFieldError($fieldName); $options += [ 'aria-required' => $options['required'] ? 'true' : null, 'aria-invalid' => $isFieldError ? 'true' : null, ]; // Don't include aria-describedby unless we have a good chance of // having error message show up. if ( str_contains($templater->get('error'), '{{id}}') && str_contains($templater->get('inputContainerError'), '{{error}}') ) { $options += [ 'aria-describedby' => $isFieldError ? $this->_domId($fieldName) . '-error' : null, ]; } if (isset($options['placeholder']) && $options['label'] === false) { $options += [ 'aria-label' => $options['placeholder'], ]; } } $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 = $options['nestedInput'] ?? $nestedInput; unset($options['nestedInput']); if ( $nestedInput === true && $options['type'] === 'checkbox' && !array_key_exists('hiddenField', $options) && $label !== false ) { $options['hiddenField'] = '_split'; } /** @var string $input */ $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, 'label' => $label, 'options' => $options, ]); if ($newTemplates) { $templater->pop(); } return $result; } /** * Generates an group template element * * @param array $options The options for group template * @return string The generated group template */ protected function _groupTemplate(array $options): string { $groupTemplate = $options['options']['type'] . 'FormGroup'; if (!$this->templater()->get($groupTemplate)) { $groupTemplate = 'formGroup'; } return $this->formatTemplate($groupTemplate, [ 'input' => $options['input'] ?? [], 'label' => $options['label'], 'error' => $options['error'], '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(array $options): string { $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'], 'label' => $options['label'] ?? '', 'required' => $options['options']['required'] ? ' ' . $this->templater()->get('requiredClass') : '', 'type' => $options['options']['type'], 'templateVars' => $options['options']['templateVars'] ?? [], ]); } /** * Generates an input element * * @param string $fieldName the field name * @param array $options The options for the input element * @return array|string The generated input element string * or array if checkbox() is called with option 'hiddenField' set to '_split'. */ protected function _getInput(string $fieldName, array $options): array|string { $label = $options['labelOptions']; unset($options['labelOptions']); switch (strtolower($options['type'])) { case 'select': case 'radio': case 'multicheckbox': $opts = $options['options']; if ($opts == null) { $opts = []; } unset($options['options']); return $this->{$options['type']}($fieldName, $opts, $options + ['label' => $label]); case 'input': throw new InvalidArgumentException(sprintf( 'Invalid type `input` used for field `%s`.', $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(string $fieldName, array $options): array { $needsMagicType = false; if (empty($options['type'])) { $needsMagicType = true; $options['type'] = $this->_inputType($fieldName, $options); } return $this->_magicOptions($fieldName, $options, $needsMagicType); } /** * 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(string $fieldName, array $options): string { $context = $this->_getContext(); if ($context->isPrimaryKey($fieldName)) { return 'hidden'; } if (str_ends_with($fieldName, '_id')) { return 'select'; } $type = 'text'; $internalType = $context->type($fieldName); $map = $this->_config['typeMap']; if ($internalType !== null && isset($map[$internalType])) { $type = $map[$internalType]; } $fieldName = array_slice(explode('.', $fieldName), -1)[0]; return match (true) { isset($options['checked']) => 'checkbox', isset($options['options']) => 'select', in_array($fieldName, ['passwd', 'password'], true) => 'password', in_array($fieldName, ['tel', 'telephone', 'phone'], true) => 'tel', $fieldName === 'email' => 'email', isset($options['rows']) || isset($options['cols']) => 'textarea', $fieldName === 'year' => 'year', default => $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(string $fieldName, array $options): array { if (isset($options['options'])) { return $options; } $internalType = $this->_getContext()->type($fieldName); if ($internalType && str_starts_with($internalType, 'enum-')) { $dbType = TypeFactory::build($internalType); if ($dbType instanceof EnumType) { if ($options['type'] !== 'radio') { $options['type'] = 'select'; } $options['options'] = $this->enumOptions($dbType->getEnumClassName()); return $options; } } $pluralize = true; if (str_ends_with($fieldName, '._ids')) { $fieldName = substr($fieldName, 0, -5); $pluralize = false; } elseif (str_ends_with($fieldName, '_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_iterable($varOptions)) { return $options; } if ($options['type'] !== 'radio') { $options['type'] = 'select'; } $options['options'] = $varOptions; return $options; } /** * Get map of enum value => label for select/radio options. * * @param class-string<\BackedEnum> $enumClass Enum class name. * @return array */ protected function enumOptions(string $enumClass): array { assert(is_subclass_of($enumClass, BackedEnum::class)); $values = []; /** @var \BackedEnum $case */ foreach ($enumClass::cases() as $case) { $hasLabel = $case instanceof EnumLabelInterface || method_exists($case, 'label'); $values[$case->value] = $hasLabel ? $case->label() : $case->name; } return $values; } /** * 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 it is allowed for this method to * overwrite the 'type' key in options. * @return array */ protected function _magicOptions(string $fieldName, array $options, bool $allowOverride): array { $options += [ 'templateVars' => [], ]; $options = $this->setRequiredAndCustomValidity($fieldName, $options); $typesWithOptions = ['text', 'number', 'radio', 'select']; $magicOptions = (in_array($options['type'], ['radio', 'select'], true) || $allowOverride); if ($magicOptions && in_array($options['type'], $typesWithOptions, true)) { $options = $this->_optionsOptions($fieldName, $options); } if ($allowOverride && str_ends_with($fieldName, '._ids')) { $options['type'] = 'select'; if (!isset($options['multiple']) || ($options['multiple'] && $options['multiple'] !== 'checkbox')) { $options['multiple'] = true; } } return $options; } /** * Set required attribute and custom validity JS. * * @param string $fieldName The name of the field to generate options for. * @param array $options Options list. * @return array Modified options list. */ protected function setRequiredAndCustomValidity(string $fieldName, array $options): array { $context = $this->_getContext(); if (!isset($options['required']) && $options['type'] !== 'hidden') { $options['required'] = $context->isRequired($fieldName); } $message = $context->getRequiredMessage($fieldName); $message = h($message); if ($options['required'] && $message) { $options['templateVars']['customValidityMessage'] = $message; if ($this->getConfig('autoSetCustomValidity')) { $options['data-validity-message'] = $message; $options['oninvalid'] = "this.setCustomValidity(''); " . 'if (!this.value) this.setCustomValidity(this.dataset.validityMessage)'; $options['oninput'] = "this.setCustomValidity('')"; } } return $options; } /** * Generate label for input * * @param string $fieldName The name of the field to generate label for. * @param array $options Options list. * @return string|false Generated label element or false. */ protected function _getLabel(string $fieldName, array $options): string|false { if ($options['type'] === 'hidden') { return false; } $label = $options['label'] ?? null; 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(string $name, array $options, mixed $default = null): mixed { 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 array|string|null $label Label text or array with label attributes. * @param array $options Options for the label element. * @return string Generated label element */ protected function _inputLabel(string $fieldName, array|string|null $label = null, array $options = []): string { $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|string. Set to false to disable a hidden input from * being generated. Passing a string will define the hidden input value. * - `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 array|string An HTML text input element. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-checkboxes */ public function checkbox(string $fieldName, array $options = []): array|string { $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'] !== false && is_scalar($options['hiddenField'])) { $hiddenOptions = [ 'name' => $options['name'], 'value' => $options['hiddenField'] !== true && $options['hiddenField'] !== '_split' ? (string)$options['hiddenField'] : '0', '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|string. Set to false to not include a hidden input with a value of ''. * Can also be a string to set the value of the hidden input. 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 iterable $options Radio button options array. * @param array $attributes Array of attributes. * @return string Completed radio widget set. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-radio-buttons */ public function radio(string $fieldName, iterable $options = [], array $attributes = []): string { $attributes['options'] = $options; $attributes['idPrefix'] = $this->_idPrefix; $generatedHiddenId = false; if (!isset($attributes['id'])) { $attributes['id'] = true; $generatedHiddenId = true; } $attributes = $this->_initInputField($fieldName, $attributes); $hiddenField = $attributes['hiddenField'] ?? true; unset($attributes['hiddenField']); $hidden = ''; if ($hiddenField !== false && is_scalar($hiddenField)) { $hidden = $this->hidden($fieldName, [ 'value' => $hiddenField === true ? '' : (string)$hiddenField, 'form' => $attributes['form'] ?? null, 'name' => $attributes['name'], 'id' => $attributes['id'], ]); } if ($generatedHiddenId) { unset($attributes['id']); } $radio = $this->widget('radio', $attributes); 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\CakeException When there are no params for the method call. */ public function __call(string $method, array $params): string { if (empty($params)) { throw new CakeException(sprintf('Missing field name for `FormHelper::%s`.', $method)); } $options = $params[1] ?? []; $options['type'] = $options['type'] ?? $method; $options = $this->_initInputField($params[0], $options); return $this->widget($options['type'], $options); } /** * Creates a textarea widget. * * ### Options: * * - `escape` - Whether 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/5/en/views/helpers/form.html#creating-textareas */ public function textarea(string $fieldName, array $options = []): string { $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/5/en/views/helpers/form.html#creating-hidden-inputs */ public function hidden(string $fieldName, array $options = []): string { $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->formProtector) { $this->formProtector->addField( $options['name'], true, $options['val'] === false ? '0' : (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/5/en/views/helpers/form.html#creating-file-inputs */ public function file(string $fieldName, array $options = []): string { $options += ['secure' => true]; $options = $this->_initInputField($fieldName, $options); unset($options['type']); return $this->widget('file', $options); } /** * Creates a `