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


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

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

いわゆる「一時保存」ボタン(?)ですね。

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

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

/**
 * 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_IDnode_formになります。

/**
 * 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のドキュメント
form idの探し方

この時点で一時保存ボタンが追加されます。

この状態で一時保存ボタンを押すとHTML5の必須チェックが動いて邪魔なので、

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

// HTML5のバリデーションをオフにする.
  $form['#attributes']['novalidate'] = 'novalidate';

これでDrupalのバリデーションが反応している状態になります。この時点で一時保存ボタンはラベル以外保存ボタンと同一なので、画像のようにバリデーションエラーが出て保存出来ない状態です。

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

// タイトル以外のバリデーションを無効化するハンドラーを追加.
  $form['actions']['save_as_draft']['#validate'][] = '_my_module_clear_errors';

でバリデーションハンドラーを追加します。これで一時保存ボタンクリック時に_my_module_clear_errorsが呼び出されるようになります。_my_module_clear_errorsの中では、

  // エラーを削除する.
  $form_state->clearErrors();

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

hookの呼び出し順を変える

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

$form_state->setTemporaryValue('entity_validated', TRUE);

でバリデーションが完了したことにしています。

タイトルのエラーは

// タイトルのエラーを取得する.
  $title_error = $form_state->getError($form['title']['widget'][0]['value']);

でいったん退避させて、

 // タイトルのエラーがある場合は元に戻す.
  if ($title_error) {
    $form_state->setErrorByName('title][0][value', $title_error);
  }

で元に戻しています。
public function FormState::getError
public function FormState::setErrorByName

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

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