想定する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の単一文字列とフォームで使うデータ配列を変換する。
まとめ
こんな面倒くさいことしないといかんのか。もっとスマートな方法がありそうな気がするけど教えて偉い人。
