[Drupal]ノードフォームのバリデーションを一時的に無効にする

今回はノードのフォームにこんなボタンを追加してみます。

- クリックするとノードが保存される
- フォームの内容が本来バリデーションに引掛かるようなものでもそのまま保存できる

いわゆる「一時保存」ボタン（？）ですね。

ただし、タイトルは入力しないと`Drupal\Core\Entity\EntityStorageException: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'title' cannot be null: `というエラーで画面真っ白になるのでバリデーションを残す必要があります。

いろいろ試したのですが、自分の場合最終的に以下のようなコードになりました。

```php
/**
 * Implements hook_form_FORM_ID_alter().
 */
function my_module_form_node_form_alter(&$form, $form_state, $form_id) {
  // デフォルトの保存ボタンの内容をコピーする.
  $form['actions']['save_as_draft'] = $form['actions']['submit'];
  // ラベルを変更する.
  $form['actions']['save_as_draft']['#value'] = '一時保存';
  // HTML5のバリデーションをオフにする.
  $form['#attributes']['novalidate'] = 'novalidate';
  // タイトル以外のバリデーションを無効化するハンドラーを追加.
  $form['actions']['save_as_draft']['#validate'][] = '_my_module_clear_errors';
}

function _my_module_clear_errors(&$form, $form_state) {
  // タイトルのエラーを取得する.
  $title_error = $form_state->getError($form['title']['widget'][0]['value']);
  // エラーを削除する.
  $form_state->clearErrors();
  // タイトルのエラーがある場合は元に戻す.
  if ($title_error) {
    $form_state->setErrorByName('title][0][value', $title_error);
  }
  // タイトルのエラーが無い場合はバリデーションが完了したことにする.
  else {
    $form_state->setTemporaryValue('entity_validated', TRUE);
  }

}
```

タイトルのエラーを残す処理が入ってるのでややこしいんですが、要点をまとめると以下のようになります。まず、`hook_form_FORM_ID_alter`でボタンを追加します。今回はノードのフォームに追加したいので、`FORM_ID`は`node_form`になります。

```php
/**
 * Implements hook_form_FORM_ID_alter().
 */
function my_module_form_node_form_alter(&$form, $form_state, $form_id) {
  // デフォルトの保存ボタンの内容をコピーする.
  $form['actions']['save_as_draft'] = $form['actions']['submit'];
  // ラベルを変更する.
  $form['actions']['save_as_draft']['#value'] = '一時保存';
}
```
[hook_form_FORM_ID_alterのドキュメント](https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Form%21form.api.php/function/hook_form_FORM_ID_alter/8.2.x)
[form idの探し方](https://drupal.stackexchange.com/questions/5802/how-can-i-find-the-form-id-of-a-form#:~:text=Use%20the%20Get%20Form%20ID,will%20see%20the%20form%20ID.)

この時点で一時保存ボタンが追加されます。
![スクリーンショット 2021-04-15 21.55.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253501/aca40ca4-4ed3-6391-5b13-9a2bd73b5c0b.png)

この状態で一時保存ボタンを押すとHTML5の必須チェックが動いて邪魔なので、
![スクリーンショット 2021-04-15 21.55.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253501/f3d7d67c-cc09-2c79-45fb-26d5da3ddbc9.png)

こちらのコードでオフにしています。（フォーム全体のHTML5のバリデーションがオフになります。）

```php
// HTML5のバリデーションをオフにする.
  $form['#attributes']['novalidate'] = 'novalidate';
```
これでDrupalのバリデーションが反応している状態になります。この時点で一時保存ボタンはラベル以外保存ボタンと同一なので、画像のようにバリデーションエラーが出て保存出来ない状態です。
![スクリーンショット 2021-04-15 21.57.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253501/1ff0b000-6c59-8c56-21c3-cfc5ecfff130.png)

このバリデーションをオフにするには、

```php
// タイトル以外のバリデーションを無効化するハンドラーを追加.
  $form['actions']['save_as_draft']['#validate'][] = '_my_module_clear_errors';
```
でバリデーションハンドラーを追加します。これで一時保存ボタンクリック時に`_my_module_clear_errors`が呼び出されるようになります。`_my_module_clear_errors`の中では、

```php
  // エラーを削除する.
  $form_state->clearErrors();
```
でバリデーション時に出たエラーを全て無かったことにしています。（今回はカスタムモジュールが一個だけなので大丈夫ですが、いくつものモジュールがバリデーションハンドラーを追加してる場合は、この処理が一番最後に来るようにする必要があります。）

[hookの呼び出し順を変える](https://x-team.com/blog/changing-the-order-of-hooks-in-drupal/)

ただし、Drupalの仕様上、単純にバリデーションを消すだけだと`entity_validated`というフラグがFALSEのままなので、ノードのpresaveの段階で`Drupal\Core\Entity\EntityStorageException: Entity validation was skipped. `という例外がスローされてしまいます。そのため、最後に

```php
$form_state->setTemporaryValue('entity_validated', TRUE);
```
でバリデーションが完了したことにしています。

タイトルのエラーは

```php
// タイトルのエラーを取得する.
  $title_error = $form_state->getError($form['title']['widget'][0]['value']);
```
でいったん退避させて、

```php
 // タイトルのエラーがある場合は元に戻す.
  if ($title_error) {
    $form_state->setErrorByName('title][0][value', $title_error);
  }
```
で元に戻しています。
[public function FormState::getError](https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Form%21FormState.php/function/FormState%3A%3AgetError/8.2.x)
[public function FormState::setErrorByName](https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Form%21FormState.php/function/FormState%3A%3AsetErrorByName/8.2.x)

これで一時保存ボタンを押すと、タイトル以外のフィールドでエラーがあっても何も出ません。タイトルだけ入力すればバリデーションをスルーして保存することができます。

![スクリーンショット 2021-04-15 22.12.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253501/c0be3c7b-f2ce-e1f1-6ddd-644260d60d0d.png)

というわけで、ノードフォームのバリデーションを一時的に無効にする方法でした。