PhalconのUniquenessバリデーションで登録時と更新時でバリデーション変えるには?

phalcon5にはデータが一意か(重複しないか)バリデーションするクラスが用意されています。

そのクラスがUniquenessです。

マニュアルはこちら。
https://docs.phalcon.io/5.0/ja-jp/filter-validation#uniqueness

このUniquenessバリデーションを使うパターンとしては、ログイン情報を登録するテーブルのメールアドレスなどがあります。

この時、ハマったポイントがあったのでメモ。

Uniquenessバリデーションでハマった点

例えば以下のようなフォーム作成したいとします。

  • ログインのアカウント情報を登録するフォームを作成
  • アカウント情報テーブルに登録するメールアドレスは一意とする

この時にメールアドレスが重複していないかUniquenessバリデーションを使って書くと以下のようになります。
今回はバリデーションクラスを作成せず、form生成時にバリデーションを記載とします。

$mail = new Text('mail');
$mail->setLabel('メールアドレス');
$mail->setFilters('email');
$mail->addValidators([
    new PresenceOf([
        'message' => 'メールアドレスを入力してください。',
        'cancelOnFail' => true,
    ]),
    new Email([
        'message' => 'メールアドレスの形式で入力してください。'
    ]),
    new StringLength([
        'max' => 300,
        'messageMaximum' => 'メールアドレスは300文字以内で入力してください。'
    ]),
    // ここが重複チェック(一意か)のバリデーション
    new Uniqueness([
        'model'   => new Account(),
        'convert' => function (array $values) {
            // メールアドレスを暗号化してテーブルに保存している場合は、メアドを暗号化して検索
            // $valuesにはformで送信されてきたメールアドレスが格納されています
            $values['mail'] = crypt($values['mail']);
            return $values;
        },
        'message' => 'すでに登録済みのメールアドレスです。'
    ])
]);
$this->add($mail);

これで、formから送られてきた値をisValid()でバリデーションした際にUniquenessのバリデーションを実行できます。

今回は'model'で設定したAccountクラス(Accountテーブル)のmailカラムに、すでに同じメールアドレスが登録されていないかチェックしています。

で、新規登録時も更新時も同じFormを使用することが多いと思いますが、この時に新規登録の場合はいいのですが、更新時に問題が発生します。

更新時にメールアドレスを変更せずに更新しようとすると、Uniquenessのバリデーションにひっかかって更新できないのです。

例えば、以下のようなデータがあった場合に、

id mail
1 aaaa@bbb.com
2 hoge@hoge.com

id=1のデータを更新する場合に、メールアドレスを変更せずに更新とすると、id=1の「aaaa@bbb.com」と重複しているのでバリデーションエラーになります。

この場合は更新できてほしいです。

ハマりポイントの解決方法

実現したいバリデーションは以下です。

  • 新規登録時はすべてのデータと比較し重複していないかチェック
  • 更新時は自身のデータ以外と比較して重複していないかチェック

上記の方法では、新規登録時は条件を満たしていますが、更新時の条件は満たしていません。

この問題を解決するために'except'オプションを使用します。

'except'オプションは、指定した値を除外して重複チェックしてくれます。

マニュアルはこちらです。
https://docs.phalcon.io/5.0/ja-jp/filter-validation#using-except-for-fields-sql-operation-value-not-in-except

$mail = new Text('mail');
$mail->setLabel('メールアドレス');
$mail->setFilters('email');
$mail->addValidators([
    new PresenceOf([
        'message' => 'メールアドレスを入力してください。',
        'cancelOnFail' => true,
    ]),
    new Email([
        'message' => 'メールアドレスの形式で入力してください。'
    ]),
    new StringLength([
        'max' => 300,
        'messageMaximum' => 'メールアドレスは300文字以内で入力してください。'
    ]),
    // ここが重複チェック(一意か)のバリデーション
    new Uniqueness([
        'model'   => new Account(),
        'convert' => function (array $values) {
            // メールアドレスを暗号化してテーブルに保存している場合は、メアドを暗号化して検索
            // $valuesにはformで送信されてきたメールアドレスが格納されています
            $values['mail'] = crypt($values['mail']);
            return $values;
        },
        'except' => $entity->mail, // ここを追加
        'message' => 'すでに登録済みのメールアドレスです。'
    ])
]);
$this->add($mail);

exceptには更新しようとするデータのメールアドレスを指定します。

$entityはformクラスのconstructで受け渡ししています。

if ($id) {
    // 更新時はテーブルからデータを取得
    $account = Account::findFirst($id);
} else {
    // 新規登録時はデータなし
    $account = new Account();
}
$form = new AccountForm($account);

こうすることで、新規登録時は$entity->mailはnullなので、mailがnull以外のデータと比較してくれ、

更新時は、$entity->mailが更新しようとするデータのmailなので、自身のmail以外のデータと比較してくれます。

id mail
1 aaaa@bbb.com
2 hoge@hoge.com

こちらのデータの例でいうと

  • 新規登録時はすべてのmailと比較
  • id=1のデータ更新時はid=1のmail以外のmailと比較

となり、実現したかったバリデーションとなります。