loadInclude(). The files * listed here are automatically loaded by * \Drupal::formBuilder()->getCache(). By default the current menu router * item's 'file' definition is added, if any. Use self::loadInclude() to * add include files from a form constructor. * - form_id: Identification of the primary form being constructed and * processed. * - base_form_id: Identification for a base form, as declared in the form * class's \Drupal\Core\Form\BaseFormIdInterface::getBaseFormId() method. * - immutable: If this flag is set to TRUE, a new form build id is * generated when the form is loaded from the cache. If it is subsequently * saved to the cache again, it will have another cache id and therefore * the original form and form-state will remain unaltered. This is * important when page caching is enabled in order to prevent form state * from leaking between anonymous users. * * @var array */ protected $build_info = [ 'args' => [], 'files' => [], ]; /** * Similar to self::$build_info. * * But pertaining to \Drupal\Core\Form\FormBuilderInterface::rebuildForm(). * * This property is uncacheable. * * @var array */ protected $rebuild_info = []; /** * Determines whether the form is rebuilt. * * Normally, after the entire form processing is completed and submit handlers * have run, a form is considered to be done and * \Drupal\Core\Form\FormSubmitterInterface::redirectForm() will redirect the * user to a new page using a GET request (so a browser refresh does not * re-submit the form). However, if 'rebuild' has been set to TRUE, then a new * copy of the form is immediately built and sent to the browser, instead of a * redirect. This is used for multi-step forms, such as wizards and * confirmation forms. Normally, self::$rebuild is set by a submit handler, * since it is usually logic within a submit handler that determines whether a * form is done or requires another step. However, a validation handler may * already set self::$rebuild to cause the form processing to bypass submit * handlers and rebuild the form instead, even if there are no validation * errors. * * This property is uncacheable. * * @see self::setRebuild() * * @var bool */ protected $rebuild = FALSE; /** * Determines if only safe element value callbacks are called. * * If set to TRUE the form will skip calling form element value callbacks, * except for a select list of callbacks provided by Drupal core that are * known to be safe. * * This property is uncacheable. * * @see self::setInvalidToken() * * @var bool */ protected $invalidToken = FALSE; /** * The response object. * * Used when a form needs to return some kind of a * \Symfony\Component\HttpFoundation\Response object, e.g., a * \Symfony\Component\HttpFoundation\BinaryFileResponse when triggering a * file download. If you use self::setRedirect() or self::setRedirectUrl(), * it will be used to build a * \Symfony\Component\HttpFoundation\RedirectResponse and will populate this * key. * * @var \Symfony\Component\HttpFoundation\Response|null */ protected $response; /** * Used to ignore destination when redirecting. * * @var bool */ protected bool $ignoreDestination = FALSE; /** * Used to redirect the form on submission. * * @see self::getRedirect() * * This property is uncacheable. * * @var \Drupal\Core\Url|\Symfony\Component\HttpFoundation\RedirectResponse|null */ protected $redirect; /** * If set to TRUE the form will NOT perform a redirect. * * Redirect will not be performed, even if self::$redirect is set. * * This property is uncacheable. * * @var bool */ protected $no_redirect; /** * The HTTP form method to use for finding the input for this form. * * May be 'POST' or 'GET'. Defaults to 'POST'. Note that 'GET' method forms do * not use form ids so are always considered to be submitted, which can have * unexpected effects. The 'GET' method should only be used on forms that do * not change data, as that is exclusively the domain of 'POST.' * * This property is uncacheable. * * @var string */ protected $method = 'POST'; /** * The HTTP method used by the request building or processing this form. * * May be any valid HTTP method. Defaults to 'GET', because even though * $method is 'POST' for most forms, the form's initial build is usually * performed as part of a GET request. * * This property is uncacheable. * * @var string */ protected $requestMethod = 'GET'; /** * Determines if the unprocessed form structure is cached. * * If set to TRUE the original, unprocessed form structure will be cached, * which allows the entire form to be rebuilt from cache. A typical form * workflow involves two page requests; first, a form is built and rendered * for the user to fill in. Then, the user fills the form in and submits it, * triggering a second page request in which the form must be built and * processed. By default, $form and $form_state are built from scratch during * each of these page requests. Often, it is necessary or desired to persist * the $form and $form_state variables from the initial page request to the * one that processes the submission. 'cache' can be set to TRUE to do this. * A prominent example is an Ajax-enabled form, in which * \Drupal\Core\Render\Element\RenderElementBase::processAjaxForm() * enables form caching for all forms that include an element with the #ajax * property. (The Ajax handler has no way to build the form itself, so must * rely on the cached version.) Note that the persistence of $form and * $form_state happens automatically for (multi-step) forms having the * self::$rebuild flag set, regardless of the value for self::$cache. * * @var bool */ protected $cache = FALSE; /** * If set to TRUE the form will NOT be cached, even if 'cache' is set. * * @var bool */ protected $no_cache; /** * An associative array of values submitted to the form. * * The validation functions and submit functions use this array for nearly all * their decision making. (Note that #tree determines whether the values are a * flat array or an array whose structure parallels the $form array. See * \Drupal\Core\Render\Element\FormElementBase for more information.) * * This property is uncacheable. * * @var array */ protected $values = []; /** * An associative array of form value keys to be removed by cleanValues(). * * Any values that are temporary but must still be displayed as values in * the rendered form should be added to this array using addCleanValueKey(). * Initialized with internal Form API values. * * This property is uncacheable. * * @var array */ protected $cleanValueKeys = [ 'form_id', 'form_token', 'form_build_id', 'op', ]; /** * The array of values as they were submitted by the user. * * These are raw and non validated, so should not be used without a thorough * understanding of security implications. In almost all cases, code should * use the data in the 'values' array exclusively. The most common use of this * key is for multi-step forms that need to clear some of the user input when * setting 'rebuild'. The values correspond to \Drupal::request()->request or * \Drupal::request()->query, depending on the 'method' chosen. * * This property is uncacheable. * * @var array|null * The submitted user input array, or NULL if no input was submitted yet. */ protected $input; /** * If TRUE and the method is GET, a form_id is not necessary. * * This property is uncacheable. * * @var bool */ protected $always_process; /** * Indicates if a validation will be forced. * * Ordinarily, a form is only validated once, but there are times when a form * is resubmitted internally and should be validated again. Setting this to * TRUE will force that to happen. This is most likely to occur during Ajax * operations. * * This property is uncacheable. * * @var bool */ protected $must_validate; /** * Indicates if the form was submitted programmatically. * * If TRUE, the form was submitted programmatically, usually invoked via * \Drupal\Core\Form\FormBuilderInterface::submitForm(). Defaults to FALSE. * * @var bool */ protected $programmed = FALSE; /** * Indicates if programmatic form submissions bypasses #access check. * * If TRUE, programmatic form submissions are processed without taking #access * into account. Set this to FALSE when submitting a form programmatically * with values that may have been input by the user executing the current * request; this will cause #access to be respected as it would on a normal * form submission. Defaults to TRUE. * * @var bool */ protected $programmed_bypass_access_check = TRUE; /** * Indicates correct form submission. * * TRUE signifies correct form submission. This is always TRUE for programmed * forms coming from \Drupal\Core\Form\FormBuilderInterface::submitForm() (see * 'programmed' key), or if the form_id coming from the * \Drupal::request()->request data is set and matches the current form_id. * * @var bool */ protected $process_input; /** * If TRUE, the form has been submitted. Defaults to FALSE. * * This property is uncacheable. * * @var bool */ protected $submitted = FALSE; /** * If TRUE, the form was submitted and has been processed and executed. * * This property is uncacheable. * * @var bool */ protected $executed = FALSE; /** * The form element that triggered submission. * * This may or may not be a button (in the case of Ajax forms). This key is * often used to distinguish between various buttons in a submit handler, and * is also used in Ajax handlers. * * This property is uncacheable. * * @var array|null */ protected $triggering_element; /** * Indicates a file element is present. * * If TRUE, there is a file element and Form API will set the appropriate * 'enctype' HTML attribute on the form. * * @var bool */ protected $has_file_element; /** * Contains references to details elements to render them within vertical tabs. * * This property is uncacheable. * * @var array */ protected $groups = []; /** * The storage. * * This is not a special key, and no specific support is provided for it in * the Form API. By tradition it was the location where application-specific * data was stored for communication between the submit, validation, and form * builder functions, especially in a multi-step-style form. Form * implementations may use any key(s) within $form_state (other than the keys * listed here and other reserved ones used by Form API internals) for this * kind of storage. The recommended way to ensure that the chosen key doesn't * conflict with ones used by the Form API or other modules is to use the * module name as the key name or a prefix for the key name. For example, the * entity form classes use $this->entity in entity forms, or * $form_state->getFormObject()->getEntity() outside the controller, to store * information about the entity being edited, and this information stays * available across successive clicks of the "Preview" button (if available) * as well as when the "Save" button is finally clicked. * * @var array */ protected $storage = []; /** * A list containing copies of all submit and button elements in the form. * * This property is uncacheable. * * @var array */ protected $buttons = []; /** * Holds temporary data accessible during the current page request only. * * All $form_state properties that are not reserved keys (see * other properties marked as uncacheable) persist throughout a multistep form * sequence. Form API provides this key for modules to communicate information * across form-related functions during a single page request. It may be used * to temporarily save data that does not need to or should not be cached * during the whole form workflow; e.g., data that needs to be accessed during * the current form build process only. There is no use-case for this * functionality in Drupal core. * * This property is uncacheable. * * @var array */ protected $temporary = []; /** * Tracks if the form has finished validation. * * This property is uncacheable. * * @var bool */ protected $validation_complete = FALSE; /** * Contains errors for this form. * * This property is uncacheable. * * @var array */ protected $errors = []; /** * Stores which errors should be limited during validation. * * An array of "sections" within which user input must be valid. If the * element is within one of these sections, the error must be recorded. * Otherwise, it can be suppressed. self::$limit_validation_errors can be an * empty array, in which case all errors are suppressed. For example, a * "Previous" button might want its submit action to be triggered even if none * of the submitted values are valid. * * This property is uncacheable. * * @var array|null */ protected $limit_validation_errors; /** * Stores the gathered validation handlers. * * This property is uncacheable. * * @var array */ protected $validate_handlers = []; /** * Stores the gathered submission handlers. * * This property is uncacheable. * * @var array */ protected $submit_handlers = []; /** * {@inheritdoc} */ public function setFormState(array $form_state_additions) { foreach ($form_state_additions as $key => $value) { if (property_exists($this, $key)) { $this->{$key} = $value; } else { $this->set($key, $value); } } return $this; } /** * {@inheritdoc} */ public function setAlwaysProcess($always_process = TRUE) { $this->always_process = (bool) $always_process; return $this; } /** * {@inheritdoc} */ public function getAlwaysProcess() { return $this->always_process; } /** * {@inheritdoc} */ public function setButtons(array $buttons) { $this->buttons = $buttons; return $this; } /** * {@inheritdoc} */ public function getButtons() { return $this->buttons; } /** * {@inheritdoc} */ public function setCached($cache = TRUE) { // Persisting $form_state is a side-effect disallowed during a "safe" HTTP // method. if ($cache && $this->isRequestMethodSafe()) { throw new \LogicException(sprintf('Form state caching on %s requests is not allowed.', $this->requestMethod)); } $this->cache = (bool) $cache; return $this; } /** * {@inheritdoc} */ public function isCached() { return empty($this->no_cache) && $this->cache; } /** * {@inheritdoc} */ public function disableCache() { $this->no_cache = TRUE; return $this; } /** * {@inheritdoc} */ public function setExecuted() { $this->executed = TRUE; return $this; } /** * {@inheritdoc} */ public function isExecuted() { return $this->executed; } /** * {@inheritdoc} */ public function setGroups(array $groups) { $this->groups = $groups; return $this; } /** * {@inheritdoc} */ public function &getGroups() { return $this->groups; } /** * {@inheritdoc} */ public function setHasFileElement($has_file_element = TRUE) { $this->has_file_element = (bool) $has_file_element; return $this; } /** * {@inheritdoc} */ public function hasFileElement() { return $this->has_file_element; } /** * {@inheritdoc} */ public function setLimitValidationErrors($limit_validation_errors) { $this->limit_validation_errors = $limit_validation_errors; return $this; } /** * {@inheritdoc} */ public function getLimitValidationErrors() { return $this->limit_validation_errors; } /** * {@inheritdoc} */ public function setMethod($method) { $this->method = strtoupper($method); return $this; } /** * {@inheritdoc} */ public function isMethodType($method_type) { return $this->method === strtoupper($method_type); } /** * {@inheritdoc} */ public function setRequestMethod($method) { $this->requestMethod = strtoupper($method); return $this; } /** * Checks whether the request method is a "safe" HTTP method. * * Link below defines GET and HEAD as "safe" methods, meaning they SHOULD NOT * have side-effects, such as persisting $form_state changes. * * @return bool * * @see \Symfony\Component\HttpFoundation\Request::isMethodSafe() * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 */ protected function isRequestMethodSafe() { return in_array($this->requestMethod, ['GET', 'HEAD']); } /** * {@inheritdoc} */ public function setValidationEnforced($must_validate = TRUE) { $this->must_validate = (bool) $must_validate; return $this; } /** * {@inheritdoc} */ public function isValidationEnforced() { return $this->must_validate; } /** * {@inheritdoc} */ public function disableRedirect($no_redirect = TRUE) { $this->no_redirect = (bool) $no_redirect; return $this; } /** * {@inheritdoc} */ public function isRedirectDisabled() { return $this->no_redirect; } /** * {@inheritdoc} */ public function setProcessInput($process_input = TRUE) { $this->process_input = (bool) $process_input; return $this; } /** * {@inheritdoc} */ public function isProcessingInput() { return $this->process_input; } /** * {@inheritdoc} */ public function setProgrammed($programmed = TRUE) { $this->programmed = (bool) $programmed; return $this; } /** * {@inheritdoc} */ public function isProgrammed() { return $this->programmed; } /** * {@inheritdoc} */ public function setProgrammedBypassAccessCheck($programmed_bypass_access_check = TRUE) { $this->programmed_bypass_access_check = (bool) $programmed_bypass_access_check; return $this; } /** * {@inheritdoc} */ public function isBypassingProgrammedAccessChecks() { return $this->programmed_bypass_access_check; } /** * {@inheritdoc} */ public function setRebuildInfo(array $rebuild_info) { $this->rebuild_info = $rebuild_info; return $this; } /** * {@inheritdoc} */ public function getRebuildInfo() { return $this->rebuild_info; } /** * {@inheritdoc} */ public function addRebuildInfo($property, $value) { $rebuild_info = $this->getRebuildInfo(); $rebuild_info[$property] = $value; $this->setRebuildInfo($rebuild_info); return $this; } /** * {@inheritdoc} */ public function setStorage(array $storage) { $this->storage = $storage; return $this; } /** * {@inheritdoc} */ public function &getStorage() { return $this->storage; } /** * {@inheritdoc} */ public function setSubmitHandlers(array $submit_handlers) { $this->submit_handlers = $submit_handlers; return $this; } /** * {@inheritdoc} */ public function getSubmitHandlers() { return $this->submit_handlers; } /** * {@inheritdoc} */ public function setSubmitted() { $this->submitted = TRUE; return $this; } /** * {@inheritdoc} */ public function isSubmitted() { return $this->submitted; } /** * {@inheritdoc} */ public function setTemporary(array $temporary) { $this->temporary = $temporary; return $this; } /** * {@inheritdoc} */ public function getTemporary() { return $this->temporary; } /** * {@inheritdoc} */ public function &getTemporaryValue($key) { $value = &NestedArray::getValue($this->temporary, (array) $key); return $value; } /** * {@inheritdoc} */ public function setTemporaryValue($key, $value) { NestedArray::setValue($this->temporary, (array) $key, $value, TRUE); return $this; } /** * {@inheritdoc} */ public function hasTemporaryValue($key) { $exists = NULL; NestedArray::getValue($this->temporary, (array) $key, $exists); return $exists; } /** * {@inheritdoc} */ public function setTriggeringElement($triggering_element) { $this->triggering_element = $triggering_element; return $this; } /** * {@inheritdoc} */ public function &getTriggeringElement() { return $this->triggering_element; } /** * {@inheritdoc} */ public function setValidateHandlers(array $validate_handlers) { $this->validate_handlers = $validate_handlers; return $this; } /** * {@inheritdoc} */ public function getValidateHandlers() { return $this->validate_handlers; } /** * {@inheritdoc} */ public function setValidationComplete($validation_complete = TRUE) { $this->validation_complete = (bool) $validation_complete; return $this; } /** * {@inheritdoc} */ public function isValidationComplete() { return $this->validation_complete; } /** * {@inheritdoc} */ public function loadInclude($module, $type, $name = NULL) { if (!isset($name)) { $name = $module; } $build_info = $this->getBuildInfo(); if (!isset($build_info['files']["$module:$name.$type"])) { // Only add successfully included files to the form state. if ($result = $this->moduleLoadInclude($module, $type, $name)) { $build_info['files']["$module:$name.$type"] = [ 'type' => $type, 'module' => $module, 'name' => $name, ]; $this->setBuildInfo($build_info); return $result; } } return FALSE; } /** * {@inheritdoc} */ public function getCacheableArray() { return [ 'build_info' => $this->getBuildInfo(), 'response' => $this->getResponse(), 'programmed' => $this->isProgrammed(), 'programmed_bypass_access_check' => $this->isBypassingProgrammedAccessChecks(), 'process_input' => $this->isProcessingInput(), 'has_file_element' => $this->hasFileElement(), 'storage' => $this->getStorage(), // Use the properties directly, since self::isCached() combines them and // cannot be relied upon. 'cache' => $this->cache, 'no_cache' => $this->no_cache, ]; } /** * {@inheritdoc} */ public function setCompleteForm(array &$complete_form) { $this->complete_form = &$complete_form; return $this; } /** * {@inheritdoc} */ public function &getCompleteForm() { return $this->complete_form; } /** * {@inheritdoc} */ public function &get($property) { $value = &NestedArray::getValue($this->storage, (array) $property); return $value; } /** * {@inheritdoc} */ public function set($property, $value) { NestedArray::setValue($this->storage, (array) $property, $value, TRUE); return $this; } /** * {@inheritdoc} */ public function has($property) { $exists = NULL; NestedArray::getValue($this->storage, (array) $property, $exists); return $exists; } /** * {@inheritdoc} */ public function setBuildInfo(array $build_info) { $this->build_info = $build_info; return $this; } /** * {@inheritdoc} */ public function getBuildInfo() { return $this->build_info; } /** * {@inheritdoc} */ public function addBuildInfo($property, $value) { $build_info = $this->getBuildInfo(); $build_info[$property] = $value; $this->setBuildInfo($build_info); return $this; } /** * {@inheritdoc} */ public function &getUserInput() { return $this->input; } /** * {@inheritdoc} */ public function setUserInput(array $user_input) { $this->input = $user_input; return $this; } /** * {@inheritdoc} */ public function &getValues() { return $this->values; } /** * {@inheritdoc} */ public function setResponse(Response $response) { $this->response = $response; return $this; } /** * {@inheritdoc} */ public function getResponse() { return $this->response; } /** * {@inheritdoc} */ public function setRedirect($route_name, array $route_parameters = [], array $options = []) { $url = new Url($route_name, $route_parameters, $options); return $this->setRedirectUrl($url); } /** * {@inheritdoc} */ public function setRedirectUrl(Url $url) { $this->redirect = $url; return $this; } /** * {@inheritdoc} */ public function getRedirect() { // Skip redirection for form submissions invoked via // \Drupal\Core\Form\FormBuilderInterface::submitForm(). if ($this->isProgrammed()) { return FALSE; } // Skip redirection if rebuild is activated. if ($this->isRebuilding()) { return FALSE; } // Skip redirection if it was explicitly disallowed. if ($this->isRedirectDisabled()) { return FALSE; } return $this->redirect; } /** * {@inheritdoc} */ public function setIgnoreDestination(bool $status = TRUE) { $this->ignoreDestination = $status; return $this; } /** * {@inheritdoc} */ public function getIgnoreDestination(): bool { return $this->ignoreDestination; } /** * Sets the global status of errors. * * @param bool $errors * TRUE if any form has any errors, FALSE otherwise. */ protected static function setAnyErrors($errors = TRUE) { static::$anyErrors = $errors; } /** * {@inheritdoc} */ public static function hasAnyErrors() { return static::$anyErrors; } /** * {@inheritdoc} */ public function setErrorByName($name, $message = '') { if ($this->isValidationComplete()) { throw new \LogicException('Form errors cannot be set after form validation has finished.'); } $errors = $this->getErrors(); if (!isset($errors[$name])) { $record = TRUE; $limit_validation_errors = $this->getLimitValidationErrors(); if ($limit_validation_errors !== NULL) { $record = FALSE; foreach ($limit_validation_errors as $section) { // Exploding by '][' reconstructs the element's #parents. If the // reconstructed #parents begin with the same keys as the specified // section, then the element's values are within the part of // $form_state->getValues() that the clicked button requires to be // valid, so errors for this element must be recorded. As the exploded // array will all be strings, we need to cast every value of the // section array to string. if (array_slice(explode('][', $name), 0, count($section)) === array_map('strval', $section)) { $record = TRUE; break; } } } if ($record) { $errors[$name] = $message; $this->errors = $errors; static::setAnyErrors(); } } return $this; } /** * {@inheritdoc} */ public function setError(array &$element, $message = '') { $this->setErrorByName(implode('][', $element['#parents']), $message); return $this; } /** * {@inheritdoc} */ public function clearErrors() { $this->errors = []; static::setAnyErrors(FALSE); } /** * {@inheritdoc} */ public function getError(array $element) { if ($errors = $this->getErrors()) { $parents = []; foreach ($element['#parents'] as $parent) { $parents[] = $parent; $key = implode('][', $parents); if (isset($errors[$key])) { return $errors[$key]; } } } } /** * {@inheritdoc} */ public function getErrors() { return $this->errors; } /** * {@inheritdoc} */ public function setRebuild($rebuild = TRUE) { $this->rebuild = $rebuild; return $this; } /** * {@inheritdoc} */ public function isRebuilding() { return $this->rebuild; } /** * {@inheritdoc} */ public function prepareCallback($callback) { if (is_string($callback) && str_starts_with($callback, '::')) { $callback = [$this->getFormObject(), substr($callback, 2)]; } return $callback; } /** * {@inheritdoc} */ public function setFormObject(FormInterface $form_object) { $this->addBuildInfo('callback_object', $form_object); return $this; } /** * {@inheritdoc} */ public function getFormObject() { return $this->getBuildInfo()['callback_object']; } /** * {@inheritdoc} */ public function getCleanValueKeys() { return $this->cleanValueKeys; } /** * {@inheritdoc} */ public function setCleanValueKeys(array $cleanValueKeys) { $this->cleanValueKeys = $cleanValueKeys; return $this; } /** * {@inheritdoc} */ public function addCleanValueKey($cleanValueKey) { $keys = $this->getCleanValueKeys(); $this->setCleanValueKeys(array_merge((array) $keys, [$cleanValueKey])); return $this; } /** * {@inheritdoc} */ public function cleanValues() { foreach ($this->getCleanValueKeys() as $value) { $this->unsetValue($value); } // Remove button values. // \Drupal::formBuilder()->doBuildForm() collects all button elements in a // form. We remove the button value separately for each button element. foreach ($this->getButtons() as $button) { // Remove this button's value from the submitted form values by finding // the value corresponding to this button. // We iterate over the #parents of this button and move a reference to // each parent in self::getValues(). For example, if #parents is: // @code // array('foo', 'bar', 'baz') // @endcode // then the corresponding self::getValues() part will look like this: // @code // array( // 'foo' => array( // 'bar' => array( // 'baz' => 'button_value', // ), // ), // ) // @endcode // We start by (re)moving 'baz' to $last_parent, so we are able unset it // at the end of the iteration. Initially, $values will contain a // reference to self::getValues(), but in the iteration we move the // reference to self::getValue('foo'), and finally to // self::getValue(array('foo', 'bar')), which is the level where we // can unset 'baz' (that is stored in $last_parent). $parents = $button['#parents']; $last_parent = array_pop($parents); $key_exists = NULL; $values = &NestedArray::getValue($this->getValues(), $parents, $key_exists); if ($key_exists && is_array($values)) { unset($values[$last_parent]); } } return $this; } /** * {@inheritdoc} */ public function setInvalidToken($invalid_token) { $this->invalidToken = (bool) $invalid_token; return $this; } /** * {@inheritdoc} */ public function hasInvalidToken() { return $this->invalidToken; } /** * Wraps ModuleHandler::loadInclude(). */ protected function moduleLoadInclude($module, $type, $name = NULL) { return \Drupal::moduleHandler()->loadInclude($module, $type, $name); } }