能否将readonly类用作Symfony表单的DTO?如何实现?
解决Symfony表单与只读DTO的构造参数不足问题
要保留DTO的readonly特性和必填约束,完全可行,核心是通过自定义数据映射器(DataMapper) 接管Symfony表单的实例化逻辑,避免默认的无参构造尝试。以下是具体实现步骤:
1. 定义只读DTO(保留必填约束)
创建带必填构造参数的readonly DTO,确保属性不可修改且必须初始化:
// src/DataTransfer/CreateThing.php namespace App\DataTransfer; use Symfony\Component\Validator\Constraints as Assert; readonly class CreateThing { public function __construct( #[Assert\NotBlank(message: '名称不能为空')] public string $name, #[Assert\NotBlank(message: '描述不能为空')] public string $description ) {} }
2. 自定义表单类型并实现数据映射器
在表单类型中实现DataMapperInterface,接管表单数据与DTO之间的映射逻辑:
// src/Form/Type/CreateThingType.php namespace App\Form\Type; use App\DataTransfer\CreateThing; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\DataMapperInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; class CreateThingType extends AbstractType implements DataMapperInterface { public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, array $options) { $builder ->add('name', TextType::class) ->add('description', TextareaType::class) ->setDataMapper($this); // 将当前类设置为数据映射器 } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'data_class' => CreateThing::class, 'empty_data' => null, // 禁用默认的空实例创建逻辑 ]); } // 从表单数据生成DTO实例 public function mapFormsToData($forms, &$data) { $formFields = iterator_to_array($forms); // 收集所有表单提交的有效数据 $dtoParams = [ 'name' => $formFields['name']->getData(), 'description' => $formFields['description']->getData(), ]; // 通过构造函数实例化只读DTO $data = new CreateThing(...$dtoParams); } // 从DTO同步数据到表单(用于编辑场景,创建场景可保留基础实现) public function mapDataToForms($data, $forms) { if (null === $data) { return; } $formFields = iterator_to_array($forms); $formFields['name']->setData($data->name); $formFields['description']->setData($data->description); } }
3. 控制器中正常处理表单
控制器逻辑无需特殊修改,按常规方式处理表单提交即可:
// src/Controller/ThingController.php namespace App\Controller; use App\DataTransfer\CreateThing; use App\Form\Type\CreateThingType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class ThingController extends AbstractController { public function create(Request $request): Response { $form = $this->createForm(CreateThingType::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { /** @var CreateThing $createThingDto */ $createThingDto = $form->getData(); // 用DTO数据创建并持久化ThingEntity // $thing = new ThingEntity(); // $thing->setName($createThingDto->name); // $thing->setDescription($createThingDto->description); // $entityManager->persist($thing); // $entityManager->flush(); return $this->redirectToRoute('thing_list'); } return $this->render('thing/create.html.twig', [ 'form' => $form->createView(), ]); } }
原理说明
Symfony表单默认会尝试通过无参构造实例化data_class对象,再通过反射赋值属性——但readonly属性只能通过构造函数初始化,且你的DTO没有无参构造,因此抛出参数不足的错误。
通过自定义DataMapper,我们跳过了默认的实例化流程,改为在表单数据收集完成后,用提交的参数调用DTO的构造函数创建实例,既保留了readonly的不可变性,又保证了构造参数的必填约束。
内容的提问来源于stack exchange,提问作者Angulator1st
相关产品推荐
相关产品推荐

