29

How do I add a custom validation handler to an existing form (or form field) in Drupal 8?

I have a form that I haven't created. I want to add my own validation rules on some fields when the form is submitted.

For Drupal 7, Custom validation for a form? explains to implement hook_form_alter() and then add your validation handler][1] to the $form['#validate'] array, but in Drupal 8 forms are classes. Validation is done through the validateForm() method and I don't know how to plug my code into that.

3
  • 4
    Possible duplicate of Custom validation for a form?
    – bummi
    Jan 22 '16 at 12:13
  • 1
    It's not exactly a duplicate. My question is for D8, your link is for D7. Jan 22 '16 at 14:30
  • I came across this today and just wanted to note for others if you are not using POST (I wanted a URL submission to an existing view page) neither the validateForm nor the submitForm run. In hindsight this is obvious.... but I spent 30 minutes trying to figure it out before I realized.... :/ Jun 14 '18 at 17:10
22

The #validate property is still used in Drupal 8. (With Adi's solution you will override the existing validator)

If you want to add your custom validator in addition to the default, you will have to add something like this in hook_form_FORM_ID_alter (or similar):

$form['#validate'][] = 'my_test_validate';
3
  • Thanks, Shabir. So, adding a custom validator works just the same in D7 and D8. ;) Dec 11 '15 at 13:47
  • Exactly, consult code of node module. there are plenty of examples there
    – Shabir A.
    Dec 11 '15 at 13:49
  • 2
    I just tried it and it worked perfectly, thank you. Please note that contrary to what the D8 Form API Reference says for #validate (your link), you shouldn't use $form_state as an array (the D7 way), but as an object implementing FormStateInterface (the D8 way). In other words, the code in your custom validator should be analog to the code you'd find in an original validateForm() method. Dec 11 '15 at 14:02
35

Berdir gave the correct answer, that a constraint is the correct way to go about adding validation to a field in Drupal 8. Here is an example.

In the example below, I will be working with a node of type podcast, that has the single value field field_podcast_duration. The value for this field needs to be formatted as HH:MM:SS (hours, minutes and seconds).

To create a constraint, two classes need to be added. The first is the constraint definition, and the second is the constraint validator. Both of these are plugins, in the namespace of Drupal\[MODULENAME]\Plugin\Validation\Constraint.

First, the constraint definition. Note that the plugin ID is given as 'PodcastDuration', in the annotation (comment) of the class. This will be used further down.

namespace Drupal\[MODULENAME]\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;

/**
 * Checks that the submitted duration is of the format HH:MM:SS
 *
 * @Constraint(
 *   id = "PodcastDuration",
 *   label = @Translation("Podcast Duration", context = "Validation"),
 * )
 */
class PodcastDurationConstraint extends Constraint {

  // The message that will be shown if the format is incorrect.
  public $incorrectDurationFormat = 'The duration must be in the format HH:MM:SS or HHH:MM:SS. You provided %duration';
}

Next, we need to provide the constraint validator. This name of this class will be the class name from above, with Validator appended to it:

namespace Drupal\[MODULENAME]\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

/**
 * Validates the PodcastDuration constraint.
 */
class PodcastDurationConstraintValidator extends ConstraintValidator {

  /**
   * {@inheritdoc}
   */
  public function validate($items, Constraint $constraint) {
    // This is a single-item field so we only need to
    // validate the first item
    $item = $items->first();

    // Check that the value is in the format HH:MM:SS
    if ($item && !preg_match('/^[0-9]{1,2}:[0-5]{1}[0-9]{1}:[0-5]{1}[0-9]{1}$/', $item->value)) {
      // The value is an incorrect format, so we set a 'violation'
      // aka error. The key we use for the constraint is the key
      // we set in the constraint, in this case $incorrectDurationFormat.
      $this->context->addViolation($constraint->incorrectDurationFormat, ['%duration' => $item->value]);
    }
  }
}

Finally, we need to tell Drupal to use our constraint on field_podcast_duration on the podcast node type. We do this in hook_entity_bundle_field_info_alter():

use Drupal\Core\Entity\EntityTypeInterface;

function HOOK_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {
  if (!empty($fields['field_podcast_duration'])) {
    $fields['field_podcast_duration']->addConstraint('PodcastDuration');
  }
}
9
  • If you end up needing other field values to validate your field you can add a constraint to the content type. See this blog post: lakshminp.com/entity-validation-drupal-8-part-2
    – ummdorian
    Sep 13 '17 at 20:57
  • 1
    D8 Form Validation API has been explained in detail on Drupal.org here. Providing a custom validation constraint Feb 5 '18 at 8:03
  • 2
    Since this question is specifically about Form API, not Field API, how does one attach this constraint to a form element (not an entity field)? May 18 '18 at 16:00
  • 1
    This answer should be the accepted one. Thanks @Jaypan
    – aserww106
    Nov 25 '19 at 4:11
  • 1
    Thanks. I don't think it's possible for posters to change it after a period of time, and I answered this well after it was asked.
    – Jaypan
    Nov 25 '19 at 4:13
21

The correct way to do this for a content entity like node is to register it as a constraint.

See forum_entity_bundle_field_info_alter() and the corresponding ? ForumLeaf validation constraint (note that there are two classes needed).

That is a bit more complicated at first, but the advantage is that it is integrated into the validation API, so your validation isn't limited to the form system but can, for example, also work with nodes submitted through the REST API.

8
  • 1
    Good point: This is something new for who used to write code for Drupal 7. I am sure there are many users who will try adding validation handlers when a constraint is more appropriate.
    – apaderno
    Dec 11 '15 at 20:08
  • Berdir: I did explore this option by trying to implement hook_entity_bundle_field_info_alter() (as described here) but it never worked... There seems to be a documented problem with this hook: drupal.org/node/2346347. Dec 13 '15 at 15:07
  • There are some issues but I don't think they are related to your problem. forum.module shows that it works. Share your code, it's not possible to point out possible problems in your implementation otherwise.
    – Berdir
    Dec 13 '15 at 17:38
  • 1
    I'd like to go with this method, but until there's a good example of how to use it with data types (i.e. not field specific) to check if some external condition is met, I'm stuck with the form alters. The article didn't dig into this. Would someone kindly point me somewhere useful, or post it here? Thanks.
    – colan
    Mar 3 '17 at 20:28
  • 1
    @kiranking the user login form is not an entity form, anything you want to do there still needs to be done with plain old form validation and should work exactly like in 7.x
    – Berdir
    Mar 7 '18 at 22:06
12

I want to add some more light on this matter. The adding of the validation is exactly the same as before: in hook_form_alter:

$form['#validate'][] = '_form_validation_number_title_validate';

The use of the values object inside of the $form_state in the validate function is though a bit different. e.g.:

function _form_validation_number_title_validate(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {

  if ($form_state->hasValue('title')) {
     $title = $form_state->getValue('title');

     if (!is_numeric($title[0]['value'])) {
        $form_state->setErrorByName('title', t('Your title should be number'));
     }

  }
}

So not with a direct access to the private variables object, but rather with a getter function.

for more info you can see a full example in my github: https://github.com/flesheater/drupal8_modules_experiments/blob/master/webham_formvalidation/webham_formvalidation.module

cheers!

0
7

It is very much the same as in D7. A complete example:

mymodule.module:

use Drupal\Core\Form\FormStateInterface;

/**
 * Implements hook_form_FORM_ID_alter() for the FORM_ID() form.
 */
function mymodule_form_FORM_ID_alter(&$form, FormStateInterface $form_state, $form_id) {
  $form['#validate'][] = '_mymodule_form_FORM_ID_validate';
}

/**
 * Validates submission values in the FORM_ID() form.
 */
function _mymodule_form_FORM_ID_validate(array &$form, FormStateInterface $form_state) {
  // Validation code here
}
2
  • This is pretty close. Only the hook_form_FORM_ID_alter needs the form ID. You can have the custom validation function be anything you want. Also, follow the API guide here for the proper parameters.
    – mikeDOTexe
    Oct 19 '16 at 23:45
  • Before that , how to get the form id as well, where to check this code. Jul 28 '17 at 7:01
3

In complement of these good answers I would add :

$form['#validate'][] = 'Drupal\your_custom_module_name\CustomClass::customValidate';

It is how to call a distant class method for a form validation. I think it's better then calling an above function in the module file like in the example given.

1
  • There's no longer a need to jump into procedural code from OO.
    – colan
    Nov 1 '19 at 15:57
1

you can use the Clientside Validation module. Some more details about it (from its project page):

... adds clientside validation (aka "Ajax form validation") for all forms and webforms using jquery.validate. The included jquery.validate.js file is patched because we needed to be able to hide empty messages.

0

Surprisingly adding validation callbacks directly to $form['#validate'][] simply won't work in case of some forms. Some Drupal forms define their validation handler at actions > button form element level.

Example of such form is language_admin_edit_form where validation handler can be added this way:

$form['actions']['submit']['#validate'][] = 'callback'

It is worth to dump and inspect $form array inside HOOK_form_alter() and verify button element level also.

Not the answer you're looking for? Browse other questions tagged or ask your own question.