Symfony 3 - 表单模型数据丢失 属性 字段未表示的值

Symfony 3 - Form model data loses property values which are not represented by fields

我有一个控制器操作方法,它应该处理两个拆分的表单。每个表单只处理我的实体 Workflow 的几个属性。提交第一个表单后,我可以毫无问题地创建和呈现第二个表单。现在的问题:

提交第二个表单后,第一个表单中设置的所有值的信息都没有了,这意味着调用submit(或handleRequest在这里没有任何区别)实体对象只保留第一种形式设置的属性数据,甚至不能正确解析某些值。

这里是控制器(有一些评论):

public function createWorkflowAction(Request $request, Project $project, Workflow $workflow = null) {   

    if(!$workflow) {
        $workflow = new Workflow($project);
    }

    $firstFormPart = $this->createForm(WorkflowStatesType::class, $workflow);

    // $firstFormPart->handleRequest($request);
    $firstFormPart->submit($request->get($firstFormPart->getName()), false);

    $secondFormPart = $this->createForm(WorkflowTransitionsType::class, $workflow);
    // secondFormPart is created correct with all values after submitting $firstFormPart and calling submit

    if($firstFormPart->isSubmitted() && $firstFormPart->isValid()) {
        return $this->render('@MyBundle/Workflow/workflow_edit_create_second_part.html.twig', array(
            'form'   => $secondFormPart->createView(),
        ));
        // This will render correctly with all values submitted in the $firstFormPart
    }

    $secondFormPart->submit($request->get($secondFormPart->getName()), false);
    // $secondFormPart->handleRequest($request);
    // HERE IS THE PROBLEM -> After submitting the $secondFormPart all property values set in the $firstFormPart are gone

    if($secondFormPart->isSubmitted() && $secondFormPart->isValid()) {
        dump($workflow);
        die();
    }

    return $this->render('@MyBundle/Workflow/workflow_edit_create_first_part.html.twig', array(
        'form' => $firstFormPart->createView(),
    ));
}

WorkflowStatesType:

class WorkflowStatesType extends AbstractType {

        /**
         * @var \Doctrine\ORM\Mapping\ClassMetadata
         */
        private $classMetadata;

        /**
         * WorkflowType constructor.
         * @param EntityManager $em
         */

        public function __construct(EntityManager $em) {
            $this->classMetadata = $em->getClassMetadata(Workflow::class);
        }

        public function buildForm(FormBuilderInterface $builder, array $options) {
            $builder
                ->setMethod('PATCH')
                ->add('name', TextType::class, array(
                    'label' => 'nameTrans',
                    'attr'  => array('maxLength' => $this->classMetadata->getFieldMapping('name')['length']),
                ))
                ->add('states',  CollectionType::class, array(
                    'entry_type'        => StateType::class,
                    'allow_add'         => true,
                    'error_bubbling'    => false,
                    'by_reference'      => false,
                    'label'             => 'workflowStatesTrans',
                ))
                ->add('next', SubmitType::class, array(
                    'label' => 'nextFormPartTrans',
                ));
        }

        public function configureOptions(OptionsResolver $resolver) {
            $resolver->setDefaults(array(
                'data_class'            => Workflow::class,
                'translation_domain'    => 'My_Bundle',
            ));
        }

    }

WorkflowTransitionsType:

class WorkflowTransitionsType extends AbstractType {

        /**
         * @var Workflow
         */
        private $workflow;

        /**
         * @var Session
         */
        private $session;

        /**
         * {@inheritdoc}
         */
        public function buildForm(FormBuilderInterface $builder, array $options) {

            /** @var Workflow $workflow */
            $this->workflow = $options['data'];

                $builder
                    ->setMethod('PATCH')
                    ->add('initialState', ChoiceType::class, array(
                        'choices'           => $this->workflow->getStates(),
                        'choice_label'      => function($state) {
                            return ($state && $state instanceof State) ? $state->getStatekey() : 'noVal';
                        },
                        'choice_value'      => function($state) {
                            return ($state && $state instanceof State) ? $state->getStatekey() : 'noVal';
                        },

                        // This combination of 'expanded' and 'multiple' implements a select box
                        'expanded'          => false,
                        'multiple'          => false,
                    ))
                    ->add('transitions', CollectionType::class, array(
                        'entry_type'        => TransitionType::class,
                        'allow_add'         => true,
                        'allow_delete'      => true,
                        'error_bubbling'    => false,
                        'by_reference'      => false,
                        'label'             => 'transitionsTrans',
                        'entry_options'     => array(
                            'states'    => $this->workflow->getStates(),
                        ),
                    ))
                    ->add('save', SubmitType::class, array(
                        'label'             => 'submitTrans',
                    ));
        }

        public function configureOptions(OptionsResolver $resolver) {
            $resolver->setDefaults(array(
                'data_class'            => Workflow::class,
                'translation_domain'    => 'My_Bundle',
            ));
            $resolver->setRequired(array(
                'session'
            ));
        }
    }

如何在提交 $secondFormPart 时保留 $firstFormPart 中提交的 $workflow 的 属性 值?

因为再次提交表单时只有 secondForm 数据,所以您丢失了 firstForm 数据。

您有 3 种方法来保留它们:

1) 将firstForm中的数据设置到查询中

// Insert that instead of the `return $this->render` for the second form
$url = $this->generateUrl(
    $request->attributes->get('_route'),
    array_merge(
        $request->query->all(),
        array('secondForm' => true, 'name' => $workflow->getName(), 'states' => $workflow->getStates()) // change the param
    )
);
return $this->redirect($url);

之前 $secondFormPart = $this->createForm(WorkflowTransitionsType::class, $workflow);

namestates 设置回 $workflow 实体,在本例中您可以检查查询变量 secondForm 以了解是否提交了第一个表单与否

2) 将来自 firstForm 的数据设置为带有一些隐藏字段的下一个 PATCH 请求

您必须修改 secondForm 以使用一些 hidden form type

来处理来自 firstForm 的数据

3)在返回第二个表单之前在session中设置数据

首先,您的实体必须实现接口 Serializable 并声明方法 serializeunserialize

那样

$this->get('session')->set('workflow', $workflow);

方法serialize将用于存储它。

你可以通过unserialize

的方法设置回退
$session = $this->get('session');
$workflow = new Workflow();
$workflow->unserialize($session->get('workflow'));

因为您将整个实体存储到会话中,所以此解决方案会大大降低应用程序的性能

您可以使用 属性 映射。参见 mapped property or allow_extra_fields

How can I hold the property values of the $workflow submitted in the $firstFormPart when submitting the $secondFormPart?

所以这是我的(不满意的)解决方案:

  1. 我将当前会话作为 class 表单的一个选项传递给 $secondFormPart,即 WorkflowTransitionsType class 并将其作为在 Controller 操作方法中调用 createForm 时的参数:

    $secondFormPart = $this->createForm(WorkflowTransitionsType::class, $workflow, array(
        'session'   => $this->get('session')
    ));
    
  2. 我在WorkflowTransitionsTypeclass中将会话保存为私有属性,将传递的工作流保存在当前会话中,并在[=16]时检索=] 被调用 2. 提交表单时的时间:

    class WorkflowTransitionsType extends AbstractType {
    
        /**
         * @var Workflow
         */
        private $workflow;
    
        /**
         * @var Session
         */
        private $session;
    
        /**
         * {@inheritdoc}
         */
        public function buildForm(FormBuilderInterface $builder, array $options) {
    
            /** @var Workflow $workflow */
            $this->workflow = $options['data'];
    
            /** @var Session $session */
            $this->session = $options['session'];
    
            // If the workflow is stored in the session we know that this method is called a 2. time!
            if($this->session->has($this->getBlockPrefix() . '_workflow')) $this->workflow = $this->session->get($this->getBlockPrefix() . '_workflow');
    
                $builder
                    ->setMethod('PATCH')
                    ->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) {
                        dump($event);
                        // This always gets called AFTER storing the workflow if it is present in the current session
                        $this->session->set($this->getBlockPrefix() . '_workflow', $this->workflow);
                    })
                    ->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
                        // Here we manipulating the passed workflow data by setting all previous values! 
                        $eventForm = $event->getForm();
    
                        /** @var Workflow $submitWorkflow */
                        $submitWorkflow = $eventForm->getData();
    
                        $submitWorkflow->setName($this->workflow->getName());
                        foreach($this->workflow->getStates() as $state) $submitWorkflow->addState($state);
    
                        $eventForm->setData($submitWorkflow);
                    })
                    ->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) {
                        // After submitting the workflow object is no longer required!
                        $this->session->remove($this->getBlockPrefix() . '_workflow');
                    })
                    ->add('initialState', ChoiceType::class, array(
                        ...
                        // Didn´t change (look at my question)
                    ))
                    ->add('transitions', CollectionType::class, array(
                        ...
                        // Didn´t change (look at my question)
                    ))
                    ->add('save', SubmitType::class, array(
                        ...
                        // Didn´t change (look at my question)
                    ));
        }
    
        /**
         * {@inheritdoc}
         */
        public function configureOptions(OptionsResolver $resolver) {
            $resolver->setDefaults(array(
                'data_class'            => Workflow::class,
                'translation_domain'    => 'MyBundle',
            ));
            $resolver->setRequired(array(
                // This is necessary to prevent an error about an unknown option!
                'session'
            ));
        }
    }