想定するEntityは以下の様
/**
* @ORM\Column(type="string", length=8, nullable=true)
*/
private $postal;
つまりEntityでは$postal
の文字列のみで、入力フォームは分割したい。
意外と探しても情報がなかったのでできた方法をメモする
郵便番号用のFormTypeを作成
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
class PostalType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('postal1', TextType::class, [
"label" => false,
"mapped" => false,
"attr" => [
"class" => "p-postal-code",
"maxlength" => 3,
],
"constraints" => [
new NotBlank([
"message" => "郵便番号が未入力です"
]),
new Length([
"max" => 3,
"maxMessage" => "郵便番号が文字数オーバーです"
])
]
])
->add('postal2', TextType::class, [
"label" => false,
"mapped" => false,
"attr" => [
"class" => "p-postal-code",
"maxlength" => 4,
],
"constraints" => [
new NotBlank([
"message" => "郵便番号が未入力です"
]),
new Length([
"max" => 4,
"maxMessage" => "郵便番号が文字数オーバーです"
])
]
])
;
$builder
->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) use ($builder) {
if(!is_array($event->getData())) return null;
$parent = $event->getForm()->getParent();
$parent->get($builder->getName())->setData(
implode("-", $event->getData())
);
})
->addModelTransformer(new CallbackTransformer(
function ($var) {
if(null === $var) return [];
$exploded = explode("-", $var);
if(!isset($exploded[1])) {
return [];
}
return [
"postal1" => $exploded[0],
"postal2" => $exploded[1]
];
},
function ($var) {
return implode("-", $var);
}
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
]);
}
}
利用側FormType
<?php
namespace App\Form;
use App\Entity\Inquiry;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class InquiryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('postal', PostalType::class, [
"parent_name" => "postal",
"label" => "郵便番号",
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Inquiry::class,
]);
}
}
ControllerとTwig
Controllerは普通に
public function form() {
return $this->render('inquiry/form.html.twig, [
"form" => $this->createForm(InquiryType::class, new Inquiry())->createView()
]);
}
Twigではこんな感じ
<div>
{{ form_label(form.postal) }}
{{ form_widget(form.postal.postal1) }}-{{ form_widget(form.postal.postal2) }}
</div>
PostalTypeのイベントとDataTransformer
これらを使ってなんとかできた
->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) use ($builder) {
if(!is_array($event->getData())) return null; // 最初のアクセス時など初期データが無い場合の対応
$parent = $event->getForm()->getParent(); // 利用側にデータを渡す
$parent->get($builder->getName())->setData(
implode("-", $event->getData())
);
})
イベントリスナーでPRE_SUBMIT
のイベントでPostalType
のデータを親Typeに結合して渡している
->addModelTransformer(new CallbackTransformer(
// Entityのデータが文字列で渡される
function ($var) {
if(null === $var) return []; // データない時とかは空配列を返しておく
$exploded = explode("-", $var); // ハイフンで分割
if(!isset($exploded[1])) {
return []; // データがおかしい時とかは空配列を返しておく
}
// PostalTypeのフィールド名に差し替えて返す
return [
"postal1" => $exploded[0],
"postal2" => $exploded[1]
];
},
// フォームデータが配列で渡されるので結合して返す
function ($var) {
return implode("-", $var);
}
))
;
このデータトランスフォーマーでEntityの単一文字列とフォームで使うデータ配列を変換する。
まとめ
こんな面倒くさいことしないといかんのか。もっとスマートな方法がありそうな気がするけど教えて偉い人。