Problem/Motivation

The FormBuilder class is complex and long, but large portions of it are not actually for building and processing, but for validation.
This makes it harder to unit test, harder to understand the code flow, and harder to maintain.

Proposed resolution

Move form validation into a new class.
In order to minimize the changes for existing module code, have FormBuilder pass through those method calls to the new class.

Remaining tasks

N/A

User interface changes

N/A

API changes

flattenOptions() is now a static method on a new \Drupal\Core\Form\OptGroup class.
executeHandlers(), which took either 'submit' or 'validate' as the first parameter, is now split into executeSubmitHandlers() and executeValidateHandlers().
These are not called anywhere externally, since they are still wrapped by the deprecated form_execute_handlers(), which handles this change.

Original report by @shrikeh

Trying to give the rather bulky FormBuilder a bit more separation of concerns, I notice that currently the FormBuilder validateForm and doValidateForm could be moved into a separate form validation class. It also has the advantage that currently the only translation referenced in FormBuilder is during validation.

Comments

tim.plunkett’s picture

Assigned: shrikeh » tim.plunkett
Category: Feature request » Task
Status: Active » Needs review
StatusFileSize
new98.57 KB

Here's how I envisioned this.
I was also able to add more comprehensive unit tests, which is a major plus.

tim.plunkett’s picture

StatusFileSize
new97.86 KB

Removed left over code I meant to refactor out.

Status: Needs review » Needs work

The last submitted patch, 2: form-2209977-2.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new104.74 KB

More test coverage.

tim.plunkett’s picture

Title: FormBuilder should not handle form validation and should offload to separate class » Move form validation logic out of FormBuilder into a new class
Issue summary: View changes
Issue tags: +API change
+++ b/core/includes/form.inc
@@ -345,12 +346,18 @@ function drupal_redirect_form($form_state) {
-  \Drupal::formBuilder()->executeHandlers($type, $form, $form_state);
+  if ($type == 'submit') {
+    \Drupal::formBuilder()->executeSubmitHandlers($form, $form_state);
+  }
+  elseif ($type == 'validate') {
+    \Drupal::service('form_validator')->executeValidateHandlers($form, $form_state);
+  }

@@ -844,12 +851,12 @@ function form_set_value($element, $value, &$form_state) {
- *   Use \Drupal::formBuilder()->flattenOptions().
+ *   Use \Drupal\Core\Form\FormHelper::flattenOptions().

Splitting executeHandlers() into two methods, and moving flattenOptions into a static helper class are the only API changes here.

For BC purposes, FormBuilder still implements all of the original methods, and just passes the calls through to the new FormValidator class.

+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -32,8 +29,7 @@
-  use StringTranslationTrait;

@@ -1750,22 +1381,6 @@ protected function drupalInstallationAttempted() {
   /**
-   * Wraps watchdog().
-   */
-  protected function watchdog($type, $message, array $variables = NULL, $severity = WATCHDOG_NOTICE, $link = NULL) {
-    watchdog($type, $message, $variables, $severity, $link);
-  }
-
-  /**
-   * Wraps drupal_set_message().
-   *
-   * @return array|null
-   */
-  protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
-    return drupal_set_message($message, $type, $repeat);
-  }

A side benefit is that FormBuilder no longer cares about string translation, drupal_set_message, or watchdog.

+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -92,24 +88,15 @@ class FormBuilder implements FormBuilderInterface {
-  protected $forms;
...
-  protected $flattenedOptions = array();

$forms was leftover, and $flattenedOptions is now part of FormHelper, so now all of the properties of FormBuilder are other services, no more state!

dawehner’s picture

Nice refactoring!!

  1. +++ b/core/lib/Drupal/Core/Form/FormHelper.php
    @@ -0,0 +1,58 @@
    +/**
    + * Provides utility methods used by form processing.
    + */
    +class FormHelper {
    

    Could we just go with FormUtility or something better?

  2. +++ b/core/lib/Drupal/Core/Form/FormValidator.php
    @@ -0,0 +1,532 @@
    +
    +  /**
    +   * The current request.
    +   *
    +   * @var \Symfony\Component\HttpFoundation\Request
    +   */
    +  protected $request;
    ...
    +  public function __construct(UrlGeneratorInterface $url_generator, CsrfTokenGenerator $csrf_token = NULL) {
    

    The injection seems wrong. We should also use the stack

  3. +++ b/core/lib/Drupal/Core/Form/FormValidatorInterface.php
    @@ -0,0 +1,67 @@
    +
    +  /**
    +   * Sets the request object to use.
    +   *
    +   * @param \Symfony\Component\HttpFoundation\Request $request
    +   *   The request object.
    +   */
    +  public function setRequest(Request $request);
    +
    

    Let's really use the stack.

tim.plunkett’s picture

Issue summary: View changes
Related issues: +#2225605: Use request stack in form builder
StatusFileSize
new108.55 KB
new19.82 KB

Rerolled this on top of #2225605: Use request stack in form builder, it now includes that patch.

tim.plunkett’s picture

StatusFileSize
new109.02 KB
new8.65 KB

Per a discussion on IRC with sun, opened #2257835: Move form submission logic out of FormBuilder into a new class to do the same for submit.
Also when rewriting the services, I mistakenly did not inject string translation. Thanks @dawehner and @sun for catching that.
Fixed that and removed the optional-ness of CSRF generator.

tim.plunkett’s picture

StatusFileSize
new104.49 KB

Rerolled, that's in.

sun’s picture

The only remaining remark I had/have is whether we can rename the FormValidator::executeValidateHandlers() method into FormValidator::executeHandlers()?

The repetition of "validate" is unnecessarily verbose on a separate class/object. Especially given the follow-up issue, having a consistent executeHandlers() method on both the FormValidatorInterface and the FormSubmitterInterface would be logical.

This means that FormBuilder cannot implement FormValidatorInterface, but I'm interpreting that just as a temporary legacy/BC layer anyway, so we can easily resolve that by leaving the current methods on FormBuilderInterface + just replace their phpDoc to state that they are @deprecated proxies for the corresponding FormValidator methods.


Aside from that, I really like the idea of exposing the main 3 concepts of Form API as separate classes (build/validate/submit). That will certainly help many developers to understand the fundamental architectural design of our form processing.

tim.plunkett’s picture

I think I'd like to hold off on renaming that until #2257835: Move form submission logic out of FormBuilder into a new class, because I'm honestly not as confident about that issue as this one. And if that one doesn't pan out, changing executeValidateHandlers to executeHandlers will make much less sense.

sun’s picture

Status: Needs review » Reviewed & tested by the community

Hm. That would mean a second API change though...

I think it would be weird/confusing to only split out validation. I'm marking this RTBC on the assumption that we'll also split out submission.

Yes, that's less much less code and it might be harder (because some parts are intermixed into [re]building) — however, we wouldn't have to move the input processing, just the executeHandlers() + submitForm() logic that's contained in processForm()... at least that's what I thought of regarding parity for clarity.

alexpott’s picture

Issue tags: +Needs change record

Let's get a followup to move OptGroup tests out of FormBuilderTest.

Plus before this can be done we need a change notice or a list of change notices that need updating.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed f710a6c and pushed to 8.x. Thanks!

Let's update the change notices https://drupal.org/node/2121003 and https://drupal.org/node/2241767

  • Commit f710a6c on 8.x by alexpott:
    Issue #2209977 by tim.plunkett: Move form validation logic out of...
alexpott’s picture

tim.plunkett’s picture

Added https://drupal.org/node/2259297 as a change notice for OptGroup, and updated those two.
Thanks, see you in #2257835: Move form submission logic out of FormBuilder into a new class

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned
jibran’s picture

+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -801,97 +786,7 @@ public function prepareForm($form_id, &$form, &$form_state) {
    * {@inheritdoc}

@@ -973,209 +868,23 @@ public function redirectForm($form_state) {
+   * {@inheritdoc}

@@ -1202,93 +911,42 @@ public function executeHandlers($type, &$form, &$form_state) {
    * {@inheritdoc}
...
    * {@inheritdoc}
...
    * {@inheritdoc}
...
    * {@inheritdoc}
...
    * {@inheritdoc}
...
    * {@inheritdoc}

I think @deprecated can be added here.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.