null, 'errorClass' => 'form-error', 'typeMap' => [ 'string' => 'text', 'datetime' => 'datetime', 'boolean' => 'checkbox', 'timestamp' => 'datetime', 'text' => 'textarea', 'time' => 'time', 'date' => 'date', 'float' => 'number', 'integer' => 'number', 'decimal' => 'number', 'binary' => 'file', 'uuid' => 'string' ], 'templates' => [ 'button' => '{{text}}', 'checkbox' => '', 'checkboxFormGroup' => '{{label}}', 'checkboxWrapper' => '
{{label}}
', 'dateWidget' => '{{year}}{{month}}{{day}}{{hour}}{{minute}}{{second}}{{meridian}}', 'error' => '
{{content}}
', 'errorList' => '', 'errorItem' => '
  • {{text}}
  • ', 'file' => '', 'fieldset' => '{{content}}', 'formStart' => '', 'formEnd' => '', 'formGroup' => '{{label}}{{input}}', 'hiddenBlock' => '
    {{content}}
    ', 'input' => '', 'inputSubmit' => '', 'inputContainer' => '
    {{content}}
    ', 'inputContainerError' => '
    {{content}}{{error}}
    ', 'label' => '{{text}}', 'nestingLabel' => '{{hidden}}{{input}}{{text}}', 'legend' => '{{text}}', 'option' => '', 'optgroup' => '{{content}}', 'select' => '', 'selectMultiple' => '', 'radio' => '', 'radioWrapper' => '{{label}}', 'textarea' => '', 'submitContainer' => '
    {{content}}
    ', ] ]; /** * Default widgets * * @var array */ protected $_defaultWidgets = [ 'button' => ['Cake\View\Widget\ButtonWidget'], 'checkbox' => ['Cake\View\Widget\CheckboxWidget'], 'file' => ['Cake\View\Widget\FileWidget'], 'label' => ['Cake\View\Widget\LabelWidget'], 'nestingLabel' => ['Cake\View\Widget\NestingLabelWidget'], 'multicheckbox' => ['Cake\View\Widget\MultiCheckboxWidget', 'nestingLabel'], 'radio' => ['Cake\View\Widget\RadioWidget', 'nestingLabel'], 'select' => ['Cake\View\Widget\SelectBoxWidget'], 'textarea' => ['Cake\View\Widget\TextareaWidget'], 'datetime' => ['Cake\View\Widget\DateTimeWidget', 'select'], '_default' => ['Cake\View\Widget\BasicWidget'], ]; /** * List of fields created, used with secure forms. * * @var array */ 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 */ public $requestType = null; /** * An array of field names that have been excluded from * the Token hash used by SecurityComponent's validatePost method * * @see FormHelper::_secure() * @see SecurityComponent::validatePost() * @var array */ protected $_unlockedFields = []; /** * Registry for input widgets. * * @var \Cake\View\Widget\WidgetRegistry */ protected $_registry; /** * Context for the current form. * * @var \Cake\View\Form\ContextInterface */ protected $_context; /** * Context provider methods. * * @var array * @see addContextProvider */ protected $_contextProviders = []; /** * The action attribute value of the last created form. * Used to make form/request specific hashes for SecurityComponent. * * @var string */ protected $_lastAction = ''; /** * 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 = []) { $registry = null; $widgets = $this->_defaultWidgets; if (isset($config['registry'])) { $registry = $config['registry']; unset($config['registry']); } if (isset($config['widgets'])) { if (is_string($config['widgets'])) { $config['widgets'] = (array)$config['widgets']; } $widgets = $config['widgets'] + $widgets; unset($config['widgets']); } parent::__construct($View, $config); $this->widgetRegistry($registry, $widgets); $this->_addDefaultContextProviders(); $this->_idPrefix = $this->config('idPrefix'); } /** * Set the widget registry the helper will use. * * @param \Cake\View\Widget\WidgetRegistry $instance The registry instance to set. * @param array $widgets An array of widgets * @return \Cake\View\Widget\WidgetRegistry */ public function widgetRegistry(WidgetRegistry $instance = null, $widgets = []) { if ($instance === null) { if ($this->_registry === null) { $this->_registry = new WidgetRegistry($this->templater(), $this->_View, $widgets); } return $this->_registry; } $this->_registry = $instance; return $this->_registry; } /** * Add the default suite of context providers provided by CakePHP. * * @return void */ protected function _addDefaultContextProviders() { $this->addContextProvider('orm', function ($request, $data) { if (is_array($data['entity']) || $data['entity'] instanceof Traversable) { $pass = (new Collection($data['entity']))->first() !== null; if ($pass) { return new EntityContext($request, $data); } } if ($data['entity'] instanceof Entity) { return new EntityContext($request, $data); } if (is_array($data['entity']) && empty($data['entity']['schema'])) { return new EntityContext($request, $data); } }); $this->addContextProvider('form', function ($request, $data) { if ($data['entity'] instanceof Form) { return new FormContext($request, $data); } }); $this->addContextProvider('array', function ($request, $data) { if (is_array($data['entity']) && isset($data['entity']['schema'])) { return new ArrayContext($request, $data['entity']); } }); } /** * Returns if a field is required to be filled based on validation properties from the validating object. * * @param \Cake\Validation\ValidationSet $validationRules Validation rules set. * @return bool true if field is required to be filled, false otherwise */ protected function _isRequiredField($validationRules) { if (empty($validationRules) || count($validationRules) === 0) { return false; } $validationRules->isUpdate($this->requestType === 'put'); foreach ($validationRules as $rule) { if ($rule->skip()) { continue; } return !$validationRules->isEmptyAllowed(); } return false; } /** * 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. * - `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. * - `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')` * - `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. * * @param mixed $model The context for which the form is being defined. Can * be an ORM entity, ORM resultset, or an array of meta data. You can use false or null * to make a model-less form. * @param array $options An array of html attributes and options. * @return string An formatted opening FORM tag. * @link http://book.cakephp.org/3.0/en/views/helpers/form.html#Cake\View\Helper\FormHelper::create */ public function create($model = null, array $options = []) { $append = ''; if (empty($options['context'])) { $options['context'] = []; } $options['context']['entity'] = $model; $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, ]; 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->request->here(false); $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'; } $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 $templater->format('formStart', [ 'attrs' => $templater->formatAttributes($htmlAttributes) . $actionAttr ]) . $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 The action attribute for the form. */ protected function _formUrl($context, $options) { if ($options['action'] === null && $options['url'] === null) { return $this->request->here(false); } 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->plugin, 'controller' => $this->request->params['controller'], 'action' => $this->request->params['action'], ]; $action = (array)$options['url'] + $actionDefaults; $pk = $context->primaryKey(); if (count($pk)) { $id = $context->val($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 : ''; $this->_lastAction = parse_url($action, PHP_URL_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() { if (!empty($this->request['_Token']['unlockedFields'])) { foreach ((array)$this->request['_Token']['unlockedFields'] as $unlocked) { $this->_unlockedFields[] = $unlocked; } } if (empty($this->request->params['_csrfToken'])) { return ''; } return $this->hidden('_csrfToken', [ 'value' => $this->request->params['_csrfToken'], 'secure' => static::SECURE_SKIP ]); } /** * Closes an HTML form, cleans up values set by FormHelper::create(), and writes hidden * input fields where appropriate. * * @param array $secureAttributes Secure attibutes which will be passed as HTML attributes * into the hidden input elements generated for the Security Component. * @return string A closing FORM tag. * @link http://book.cakephp.org/3.0/en/views/helpers/form.html#closing-the-form */ public function end(array $secureAttributes = []) { $out = ''; if ($this->requestType !== 'get' && !empty($this->request['_Token']) ) { $out .= $this->secure($this->fields, $secureAttributes); $this->fields = []; } $templater = $this->templater(); $out .= $templater->format('formEnd', []); $templater->pop(); $this->requestType = null; $this->_context = null; $this->_idPrefix = $this->config('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 void|string A hidden input field with a security hash */ public function secure(array $fields = [], array $secureAttributes = []) { if (empty($this->request['_Token'])) { return; } $locked = []; $unlockedFields = $this->_unlockedFields; foreach ($fields as $key => $value) { if (!is_int($key)) { $locked[$key] = $value; unset($fields[$key]); } } sort($unlockedFields, SORT_STRING); sort($fields, SORT_STRING); ksort($locked, SORT_STRING); $fields += $locked; $locked = implode(array_keys($locked), '|'); $unlocked = implode($unlockedFields, '|'); $hashParts = [ $this->_lastAction, serialize($fields), $unlocked, Security::salt() ]; $fields = Security::hash(implode('', $hashParts), 'sha1'); $tokenFields = array_merge($secureAttributes, [ 'value' => urlencode($fields . ':' . $locked), ]); $out = $this->hidden('_Token.fields', $tokenFields); $tokenUnlocked = array_merge($secureAttributes, [ 'value' => urlencode($unlocked), ]); $out .= $this->hidden('_Token.unlocked', $tokenUnlocked); 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 mixed Either null, or the list of fields. * @link http://book.cakephp.org/3.0/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)) { $this->_unlockedFields[] = $name; } $index = array_search($name, $this->fields); 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)) { if ($value !== null) { return $this->fields[$field] = $value; } $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 http://book.cakephp.org/3.0/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 $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 http://book.cakephp.org/3.0/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 = (array)$context->error($field); if (is_array($text)) { $tmp = []; foreach ($error as $e) { if (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(). * * 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 $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 http://book.cakephp.org/3.0/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 inputs for `$fields`. If $fields is empty the fields of current model * will be used. * * You can customize individual inputs through `$fields`. * ``` * $this->Form->allInputs([ * 'name' => ['label' => 'custom label'] * ]); * ``` * * You can exclude fields by specifying them as false: * * ``` * $this->Form->allInputs(['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 input set. Or supply a string * to customize the legend text. * @return string Completed form inputs. * @link http://book.cakephp.org/3.0/en/views/helpers/form.html#generating-entire-forms */ public function allInputs(array $fields = [], array $options = []) { $context = $this->_getContext(); $modelFields = $context->fieldNames(); $fields = array_merge( Hash::normalize($modelFields), Hash::normalize($fields) ); return $this->inputs($fields, $options); } /** * Generate a set of inputs for `$fields` wrapped in a fieldset element. * * You can customize individual inputs through `$fields`. * ``` * $this->Form->inputs([ * '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 http://book.cakephp.org/3.0/en/views/helpers/form.html#generating-entire-forms */ public function inputs(array $fields, array $options = []) { $fields = Hash::normalize($fields); $out = ''; foreach ($fields as $name => $opts) { if ($opts === false) { continue; } $out .= $this->input($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($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) { $actionName = __d('cake', 'New %s'); $isCreate = $context->isCreate(); if (!$isCreate) { $actionName = __d('cake', 'Edit %s'); } $modelName = Inflector::humanize(Inflector::singularize($this->request->params['controller'])); $legend = sprintf($actionName, $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 input 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 input(). * 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. * * @param string $fieldName This should be "modelname.fieldname" * @param array $options Each type of input takes different options. * @return string Completed form widget. * @link http://book.cakephp.org/3.0/en/views/helpers/form.html#creating-form-inputs */ public function input($fieldName, array $options = []) { $options += [ 'type' => null, 'label' => null, 'error' => null, 'required' => null, 'options' => null, 'templates' => [], ]; $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) { $error = $this->error($fieldName, $options['error']); $errorSuffix = empty($error) ? '' : 'Error'; unset($options['error']); } $label = $options['label']; unset($options['label']); $nestedInput = false; if ($options['type'] === 'checkbox') { $nestedInput = true; } $nestedInput = isset($options['nestedInput']) ? $options['nestedInput'] : $nestedInput; if ($nestedInput === true && $options['type'] === 'checkbox' && !array_key_exists('hiddenField', $options) && $label !== false) { $options['hiddenField'] = '_split'; } $input = $this->_getInput($fieldName, $options); if ($options['type'] === 'hidden' || $options['type'] === 'submit') { if ($newTemplates) { $templater->pop(); } return $input; } $label = $this->_getLabel($fieldName, compact('input', 'label', 'error', 'nestedInput') + $options); $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 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->templater()->format($groupTemplate, [ 'input' => $options['input'], 'label' => $options['label'], 'error' => $options['error'] ]); } /** * 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->templater()->format($inputContainerTemplate, [ 'content' => $options['content'], 'error' => $options['error'], 'required' => $options['options']['required'] ? ' required' : '', 'type' => $options['options']['type'] ]); } /** * 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) { switch ($options['type']) { case 'select': $opts = $options['options']; unset($options['options']); return $this->select($fieldName, $opts, $options); case 'radio': $opts = $options['options']; unset($options['options']); return $this->radio($fieldName, $opts, $options); case 'multicheckbox': $opts = $options['options']; unset($options['options']); return $this->multicheckbox($fieldName, $opts, $options); case 'url': $options = $this->_initInputField($fieldName, $options); return $this->widget($options['type'], $options); 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(); if (!isset($options['required']) && $options['type'] !== 'hidden') { $options['required'] = $context->isRequired($fieldName); } $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 (empty($options['multiple'])) { $options['multiple'] = true; } } if ($options['type'] === 'select' && array_key_exists('step', $options)) { unset($options['step']); } $autoLength = !array_key_exists('maxlength', $options) && !empty($fieldDef['length']) && $options['type'] !== 'select'; $allowedTypes = ['text', 'textarea', 'email', 'tel', 'url', 'search']; if ($autoLength && in_array($options['type'], $allowedTypes)) { $options['maxlength'] = min($fieldDef['length'], 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) { $labelAttributes = []; if (is_array($label)) { $labelText = null; if (isset($label['text'])) { $labelText = $label['text']; unset($label['text']); } $labelAttributes = array_merge($labelAttributes, $label); } else { $labelText = $label; } $options += ['id' => null, 'input' => null, 'nestedInput' => false]; $labelAttributes['for'] = $options['id']; $groupTypes = ['radio', 'multicheckbox', 'date', 'time', 'datetime']; if (in_array($options['type'], $groupTypes, true)) { $labelAttributes['for'] = false; } if ($options['nestedInput']) { $labelAttributes['input'] = $options['input']; } 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 http://book.cakephp.org/3.0/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` - boolean to indicate whether or not labels for widgets should be displayed. * - `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. * - `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 http://book.cakephp.org/3.0/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); $value = $attributes['val']; $hiddenField = isset($attributes['hiddenField']) ? $attributes['hiddenField'] : true; unset($attributes['hiddenField']); $radio = $this->widget('radio', $attributes); $hidden = ''; if ($hiddenField && (!isset($value) || $value === '')) { $hidden = $this->hidden($fieldName, [ 'value' => '', '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 http://book.cakephp.org/3.0/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 http://book.cakephp.org/3.0/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 http://book.cakephp.org/3.0/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 `