🎻

[Symfony] エンティティに対応するFormTypeの全項目が空だった場合はエンティティ自体を削除する

2020/05/30に公開

やりたいこと

例えば、以下のように PersonOneToOneProfile を持っているようなエンティティ構成を考えます。

/**
 * @ORM\Entity()
 */
class Person
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;
    
    /**
     * @ORM\Column(type="string", length=255)
     */
    private $screenName;
    
    /**
     * @ORM\OneToOne(targetEntity="Profile", cascade={"persist", "remove"})
     */
    private $profile;
    
    // ...
}
/**
 * @ORM\Entity()
 */
class Profile
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;
    
    /**
     * @ORM\Column(type="string", length=255)
     */
    private $fullName;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $email;
    
    // ...
}

そして、これらのエンティティを以下のように PersonType でまとめて編集できるようにします。

class PersonType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('screenName', TextType::class)
            ->add('profile', ProfileType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Person::class,
        ]);
    }
}
class ProfileType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('fullName', TextType::class, [
                'required' => false,
            ])
            ->add('email', TextType::class, [
                'required' => false,
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Profile::class,
        ]);
    }
}

これで、 PersonType をレンダリングすると

  • screenName (必須)
  • fullName (任意)
  • email (任意)

の3つのフォーム項目が出力されますね。

さて、このフォームにおいて、 fullNameemail の両方が空欄で送信されたときに、 Profile エンティティ自体を削除する、という振る舞いを実装したいとしましょう。

どうすればいいでしょうか?🤔

やり方

結論としては、

  • OneToOne リレーションの設定に orphanRemoval=true をつけておいて
  • FormTypeのEventListenerで、全項目が空欄だったら項目自体に null をセットし直す

という方法で実現可能です。

まず、 Person エンティティの $profile プロパティの OneToOneの 設定に以下のように orphanRemoval=true を追記します。

    /**
-    * @ORM\OneToOne(targetEntity="Profile", cascade={"persist", "remove"})
+    * @ORM\OneToOne(targetEntity="Profile", cascade={"persist", "remove"}, orphanRemoval=true)
     */
    private $profile;

これで、 Person エンティティの $profile プロパティに null がセットされたときに Profile エンティティが自動的に削除されるようになります。(参考

あとは、 PersonType にEventListenerをセットして、

  • profile.fullNameprofile.email が両方空だったら
  • profilenull をセットする

という処理をフォーム送信前に行うようにしてあげれば、目的を果たせます。

具体的には以下のような実装で実現できます。

class PersonType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('screenName', TextType::class)
            ->add('profile', ProfileType::class)

            // これを追加
            ->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
                if (!array_filter($event->getData()['profile'])) {
                    $event->getForm()->get('profile')->setData(null);
                }
            })
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Person::class,
        ]);
    }
}

ちなみに

逆に、全項目が空で送信された場合にも、「全項目が空なエンティティ」を作成したいという場合には、SUBMIT イベントフックを利用して以下のような感じで対応できます。

->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
    /** @var Person $person */
    $person = $event->getData();
    if (!$person->profile) {
        $person->profile = new Profile();
    }
}

まとめ

Symfonyで「エンティティに対応するFormTypeの全項目が空だった場合はエンティティ自体を削除する」という要件は、

  • リレーションの設定に orphanRemoval=true をつけておいて
  • FormTypeのEventListenerで、全項目が空欄だったら項目自体に null をセットし直す

という方法で実現できます、というお話でした。

GitHubで編集を提案

Discussion