🎻

[Symfony] 1つのフォームで複数のエンティティを作成するには

2020/04/18に公開

例えば以下のような仕様を考えます。

  • User エンティティと Profile エンティティがOneToOneで紐付いている
  • ユーザー登録フォームに User エンティティの情報と Profile エンティティの情報を両方入力してもらう
  • フォームが送信されたら UserProfile の両方のエンティティを同時に作成する

Symfonyでこういう仕様を実装するときのセオリーを説明します。

比較的Symfony初心者さん向けの内容になります。

1つのフォームで1つのエンティティを作る場合なら

まず、普通に1つのフォームで1つのエンティティを作る場合についておさらいしましょう。

例えば以下のような User エンティティがあるとします。

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 */
class User
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255, unique=true)
     */
    private $username;

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

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

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

    // getters and setters
}

この場合は、普通に User エンティティに対応する UserType などのフォームタイプを作成しますよね。

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('username')
            ->add('fullName')
            ->add('tel')
            ->add('address')
            ->add('submit', SubmitType::class)
        ;
    }

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

そしてコントローラでこんなふうにエンティティの作成をするでしょう。

class UserController extends AbstractController
{
    /**
     * @Route("/user/new", name="user_new")
     */
    public function new(Request $request)
    {
        $form = $this->createForm(UserType::class, $user = new User());
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->flush();

            $this->addFlash('success', 'ユーザーの作成が完了しました');

            $this->redirectToRoute('user_index');
        }

        return $this->render('user/new.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

ここまではSymfonyのごく基本的な使い方ですね。

複数のエンティティを一度に作りたい場合はどうすればいいのか

それでは本題です。

ユーザーの情報が User エンティティと Profile エンティティの2つに分かれていて、ユーザーを作成するためのフォームは1つ、という場合について考えてみましょう。

エンティティのコードは以下のようなイメージになります。

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 */
class User
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255, unique=true)
     */
    private $username;

    /**
     * @ORM\OneToOne(targetEntity="App\Entity\Profile", inversedBy="user", cascade={"persist", "remove"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $profile;

    // getters and setters
}
/**
 * @ORM\Entity(repositoryClass="App\Repository\ProfileRepository")
 */
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 $tel;
    
    /**
     * @ORM\Column(type="string", length=255)
     */
    private $address;
    
    /**
     * @ORM\OneToOne(targetEntity="App\Entity\User", mappedBy="profile", cascade={"persist", "remove"})
     */
    private $user;

    // getters and setters
}

これらのエンティティを1つのフォームで作りたいわけですが、フォームとエンティティが1対1対応だったときは

'data_class' => User::class,

で簡単にエンティティに対応したフォームが作れましたが、今回は1対1ではないので、「しょうがないから全部の情報を入力できるフォームをエンティティと紐付けずに作って、コントローラ側でエンティティを組み立てるようにするか」と思ってしまいがちですが、ちょっと待ってください。

Symfonyでは、こういう場合でもちゃんとエンティティに対応したフォームを作って、コントローラ側は handleRequest() するだけでオーケーというものが作れるんです。

それは、 FormTypeをネストする という方法です。

実際に具体的なコードを見てみましょう。

まずは UserType ProfileType の2つを普通に作ります。

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('username')
        ;
    }

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

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

そして、これら2つのFormTypeをフィールドに持つ、ユーザー登録用フォームを作ります。

class UserRegisterType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('user', UserType::class)
            ->add('profile', ProfileType::class)
            ->add('submit', SubmitType::class)
        ;
    }
}

このように、 $builder->add() の第2引数に自分で作ったFormTypeを指定することで、ネストしたFormTypeを作ることができるのです。

このフォームをレンダリングすると、inputの部分は以下のような内容になります。

<input type="text" id="user_register_user_username" name="user_register[user][username]" required="required" maxlength="255">

<input type="text" id="user_register_profile_fullName" name="user_register[profile][fullName]" required="required" maxlength="255">

<input type="text" id="user_register_profile_tel" name="user_register[profile][tel]" required="required" maxlength="255">

<input type="text" id="user_register_profile_address" name="user_register[profile][address]" required="required" maxlength="255">

name 属性に注目すると、 [user][usename] [profile][fullName] といった形で連想配列形式になっていますね。

ということは、コントローラ側でも同じように user profile というキーでネストした連想配列として値を取り回してあげればよさそうです。

つまり、この場合コントローラの具体的な実装は以下のようなコードになります。

class UserController extends AbstractController
{
    /**
     * @Route("/user/new", name="user_new")
     */
    public function new(Request $request)
    {
        $form = $this->createForm(UserRegisterType::class, [
            'user' => $user = new User(),
            'profile' => $profile = new Profile(),
        ]);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $user->setProfile($profile);
            $em->persist($user);
            $em->flush();

            $this->addFlash('success', 'ユーザーの作成が完了しました');

            $this->redirectToRoute('user_index');
        }

        return $this->render('user/new.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}

どうでしょう。何をやっているか分かるでしょうか。

ポイントはここです。

$form = $this->createForm(UserRegisterType::class, [
    'user' => $user = new User(),
    'profile' => $profile = new Profile(),
]);

UserRegisterType のフィールドは、

$builder
    ->add('user', UserType::class)
    ->add('profile', ProfileType::class)
    ->add('submit', SubmitType::class)
;

このように定義したので、 user profile というキーによってネストされています。inputタグの name 属性も [user] [profile] というキーになっていましたよね。

なので、フォームに渡す値は ['user' => $user, 'profile' => $profile] という連想配列にする必要があるのです。

あとは handleRequest() すれば $user $profile それぞれのプロパティがフォームのデータによって適切に変更されるので、 $user->setProfile($profile) だけ追加でしてあげてからflushすればOKというわけです。( $profile はcascadeによって自動的にpersistされます)

まとめ

  • Symfonyでは、FormTypeをネストさせれば1つのフォームで複数のエンティティをまとめて作ることも簡単にできる
GitHubで編集を提案

Discussion