Score:1

Validation on custom entity edit form

lc flag

I have a custom entity where, instead of using the usual way of specifying edit form options with setDisplayOptions('form' and relying on ContentEntityForm to create my edit form for me, I have to make my own form in buildForm() (I have Ajax interactions between the fields that the stock from can't provide). It works all right, just that I can't trigger validation as with the stock form. The constraints are there and if I override:

public function validateForm(array &$form, FormStateInterface $form_state) {
  $entity = parent::validateForm($form, $form_state);
  $violations = $entity->validate();
  foreach ($violations as $v) {
    dpm($v->getMessage());
  }
  return $entity;
}

the validation errors are in fact found and listed, just the form won't display the usual red warnings, keeping the user from going on. Can I reconcile the custom form building with the automatic validation?

Score:0
us flag

The User entity is one of the Drupal core entities that programmatically adds form elements in their entity form class and still uses validation constraints.
AccountForm::form() adds the form elements and User::baseFieldDefinitions() adds the validation constraints to the entity fields.
(AccountForm is the class extended by the ProfileForm and the RegistrationForm classes, which are two of the three entity form classes used for the User entity.)

  // Account information.
  $form['account'] = [
    '#type' => 'container',
    '#weight' => -10,
  ];

  // The mail field is NOT required if account originally had no mail set
  // and the user performing the edit has 'administer users' permission.
  // This allows users without email address to be edited and deleted.
  // Also see \Drupal\user\Plugin\Validation\Constraint\UserMailRequired.
  $form['account']['mail'] = [
    '#type' => 'email',
    '#title' => $this->t('Email address'),
    '#description' => $this->t('A valid email address. All emails from the system will be sent to this address. The email address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by email.'),
    '#required' => !(!$account->getEmail() && $user->hasPermission('administer users')),
    '#default_value' => !$register ? $account->getEmail() : '',
  ];

  // Only show name field on registration form or user can change own username.
  $form['account']['name'] = [
    '#type' => 'textfield',
    '#title' => $this->t('Username'),
    '#maxlength' => UserInterface::USERNAME_MAX_LENGTH,
    '#description' => $this->t("Several special characters are allowed, including space, period (.), hyphen (-), apostrophe ('), underscore (_), and the @ sign."),
    '#required' => TRUE,
    '#attributes' => [
      'class' => [
        'username',
      ],
      'autocorrect' => 'off',
      'autocapitalize' => 'off',
      'spellcheck' => 'false',
    ],
    '#default_value' => !$register ? $account->getAccountName() : '',
    '#access' => $account->name->access('edit'),
  ];
  $fields['name'] = BaseFieldDefinition::create('string')
    ->setLabel(t('Name'))
    ->setDescription(t('The name of this user.'))
    ->setRequired(TRUE)
    ->setConstraints([
    // No Length constraint here because the UserName constraint also covers
    // that.
    'UserName' => [],
    'UserNameUnique' => [],
  ]);
  $fields['name']
    ->getItemDefinition()
    ->setClass('\\Drupal\\user\\UserNameItem');
  $fields['pass'] = BaseFieldDefinition::create('password')
    ->setLabel(t('Password'))
    ->setDescription(t('The password of this user (hashed).'))
    ->addConstraint('ProtectedUserField');

The AccountForm, ProfileForm, and RegisterForm classes, which extend the [ContentEntityForm][6] class, doen't extend [ContentEntityForm::validateForm()][7], though. They implement methods that is necessary to Drupal to understand which entity fields are edited and which violations should be shown: [AccountForm::getEditedFieldNames()][8] and [AccountForm::flagViolations()`]6.

protected function getEditedFieldNames(FormStateInterface $form_state) {
  return array_merge([
    'name',
    'pass',
    'mail',
    'timezone',
    'langcode',
    'preferred_langcode',
    'preferred_admin_langcode',
  ], parent::getEditedFieldNames($form_state));
}
protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
  // Manually flag violations of fields not handled by the form display. This
  // is necessary as entity form displays only flag violations for fields
  // contained in the display.
  $field_names = [
    'name',
    'pass',
    'mail',
    'timezone',
    'langcode',
    'preferred_langcode',
    'preferred_admin_langcode',
  ];
  foreach ($violations->getByFields($field_names) as $violation) {
    list($field_name) = explode('.', $violation->getPropertyPath(), 2);
    $form_state->setErrorByName($field_name, $violation->getMessage());
  }
  parent::flagViolations($violations, $form, $form_state);
}

ContentEntityForm::validateForm() shows only the validation errors for those form elements that are reported to be edited. This means that if a form element name isn't returned from AccountForm::getEditedFieldNames() or ContentEntityForm::getEditedFieldNames(), that form element and the corresponding entity field aren't considered edited.

To answer the question: Yes, it's possible to add form elements in the form() method of the entity form class and use validation constraints (and their "automatic" validation), as long as the entity form class implements the getEditedFieldNames() and flagViolations() methods.

lc flag
Very nice, thank you, I'll go this route.
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.