21

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.

  • 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. – AngularChef 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.... :/ – ben.hamelin Jun 14 '18 at 17:10
17

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';
  • Thanks, Shabir. So, adding a custom validator works just the same in D7 and D8. ;) – AngularChef 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. – AngularChef Dec 11 '15 at 14:02
22

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();

    // If there is no value we don't need to validate anything
    if(!isset($item))
    {
      return NULL;
    }

    // Check that the value is in the format HH:MM:SS
    if(!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');
  }
}
  • 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 – Sukhjinder Singh Feb 5 '18 at 8:03
  • 1
    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)? – aaronbauman May 18 '18 at 16:00
  • Form elements can't have constraints. You can add validation to a specific form element using #element_validate. See the top answer on this thread - it works the same in D8 as D7 drupal.stackexchange.com/questions/86990/… – Jaypan May 18 '18 at 17:01
  • Make sure you check $item = $items->first(); and return NULL if nothing is there or else you'll get a fatal error when editing the field: TypeError: Argument 2 passed to Drupal\Component\Utility\NestedArray::getValue() must be of the type array, null given, called in /data/app/core/lib/Drupal/Core/Field/WidgetBase.php on line 407 in Drupal\Component\Utility\NestedArray::getValue() (line 69 of core/lib/Drupal/Component/Utility/NestedArray.php). – Ivan Zugec Nov 14 '18 at 5:27
15

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.

  • 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. – kiamlaluno 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. – AngularChef 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
  • what if we need to add existing validation like \Drupal\user\Form\UserLoginForm::validateName(). In d7 it was simple as $form['#validate'] = array( 'user_login_name_validate', 'myother_validaion', ); But it seems no-contrib module implemented these changes drupal.org/node/2185941 – kiranking Mar 7 '18 at 18:41
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
}
  • 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. – logeshvaran Jul 28 '17 at 7:01
7

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!

  • Indeed it is. Just like I wrote in my comment above. ;) – AngularChef Apr 11 '16 at 19:40
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.

1

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.

protected by kiamlaluno Jun 8 at 19:43

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

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