🎻

symfony/formで「どちらか片方の入力は必須」をシュッと作る

2020/04/13に公開

例えば、問い合わせフォームなんかで 「電話番号かメールアドレスのどちらかは必須」 というような要件は割とよくありますよね。

こんなふうに「どちらか片方の入力は必須」という要件をsymfony/formで実現する方法はいくつか考えられますが、その中でも簡単にシュッと作れる僕がいつもやっている方法をご紹介します。

結論

結論としては、Callback constraint を使います。

カスタムバリデーションを作る とかが真っ先に思い浮かびますが、 Callback を使うほうが簡単にシュッと出来ます。

具体的には以下のようなコードで実現できます👍

<?php
namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

class ContactType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder

            // ...

            ->add('tel', Type::class, [
                'required' => false,
                'label' => '電話番号',
                'constraints' => [
                    new Assert\Callback([$this, 'validateTelOrEmail']),
                ],
            ])
            ->add('email', EmailType::class, [
                'required' => false,
                'label' => 'メールアドレス',
                'constraints' => [
                    new Assert\Callback([$this, 'validateTelOrEmail']),
                ],
            ])

            // ...
        ;
    }

    public function validateTelOrEmail($value, ExecutionContextInterface $context)
    {
        $form = $context->getObject()->getParent();

        if (!$form->get('tel')->getData() && !$form->get('email')->getData()) {
            $context
                ->buildViolation('電話番号かメールアドレスのどちらかは必ず入力してください')
                ->addViolation()
            ;
        }
    }
}

両方空欄の状態で送信してみると、このようにエラーが表示されます。

コードの解説

まず、電話番号とメールアドレスの両方のフィールドに対して

'constraints' => [
    new Assert\Callback([$this, 'validateTelOrEmail']),
],

をセットして、自クラスの validateTelOrEmail() メソッドを Callback constraintに登録しています。

コールバックを配列型式で渡した場合、実際のバリデーション処理としては この辺のコード が走るので、コールバック関数が受け取る引数は ($object, $this->context, $constraint->payload) だということが分かります。

ちなみにこの $object にはバリデーション対象の値そのものが入っています。(参考

で、コールバック関数である validateTelOrEmail() メソッドは、今回は以下のように実装しました。

public function validateTelOrEmail($value, ExecutionContextInterface $context)
{
    $form = $context->getObject()->getParent();

    if (!$form->get('tel')->getData() && !$form->get('email')->getData()) {
        $context
            ->buildViolation('電話番号かメールアドレスのどちらかは必ず入力してください')
            ->addViolation()
        ;
    }
}

コンテキストから親であるフォームのインスタンスを取得して、 tel フィールドと email フィールドの値を調べ、どちらも空ならエラーを追加、というシンプルな処理です。

この処理が telemail の両方に対して実行されるので、エラーメッセージは両方のフィールドに表示されます。

ちなみに、多くの場合

$form = $context->getObject()->getParent();

$form = $context->getRoot();

と書いても動くんですが、これだと FormType をネストしているときに一番ルートのフォームが取得されてしまって $form->get('tel')$form->get('email') が「そんなフィールドありません」とエラーになってしまうので、 $context->getObject()->getParent(); と明示的に書いておくほうが無難でしょう。

参考リンク

まとめ

  • symfony/formで「どちらか片方の入力は必須」を実装するには、Callback constraintFormType に直接セットするのが簡単
GitHubで編集を提案

Discussion