[Symfony] 1つのフォームで複数のエンティティを作成するには
例えば以下のような仕様を考えます。
-
User
エンティティとProfile
エンティティがOneToOneで紐付いている - ユーザー登録フォームに
User
エンティティの情報とProfile
エンティティの情報を両方入力してもらう - フォームが送信されたら
User
とProfile
の両方のエンティティを同時に作成する
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つのフォームで複数のエンティティをまとめて作ることも簡単にできる
Discussion