Symfony 4, Object and SubObjects, missing foreign

2019-08-19 01:34发布

I am not able to find the trick to get the following.

Say I have two Entity: Main and Minor, Main one-to-many Minor, mainId being the foreign key field.

I wish to have both a (Minor) form to create a Minor object, such that users may select its Main object from a list of already available Main objects, and a (Main) form to create a Main object and possibly many different Minor (sub)objects at once.

The issue is that in the latter case, I am not able to save the foreign key.

For the Minor form, I define:

$builder ->add('minorTitle')
         ->add('Main', EntityType::class, array(
               'class' => Main::class,
               'choice_label' => 'mainTtile',
               'label' => 'main'))

have 'data_class' => Minor::class, and it works fine.

For the Main form, I tried:

$builder
        ->add('mainTitle')
        ->add('Minors', CollectionType::class, array(
              'entry_type' => MinorType::class,
              'allow_add' => true,
              'label' => 'Minor'
              ))   

              'data_class' => Main::class`

So the Minor form is indeed embedded as a subform within the Main one. To add more subforms, I have some JS as suggested in CollectionType. To avoid to display the Main field in the Minor subforms, I have hacked a little the prototype, by something like:

newWidget = newWidget.replace(newWidget.match(/\B<div class="form-group"><label class="required" for="main_Minors___name_Main">Main<\/label><select id="main_Minors___name_Main" name="main\[Minors\]\[__name__\]\[Main\]" class="form-control">.*<\/select>\B/g),""); 

A user is able to create a Main object, and many Minor ones too, but the id of the former is not saved as the foreign keys of the latter ones. I have tried to fix things within the Main Controller by something like (or variants):

public function new(Request $request): Response {
    $em = $this->getDoctrine()->getManager();
    $main = new Main();
    $form = $this->createForm(MainType::class, $main);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $postData = $request->request->get('main');

        $minors = array();
        foreach($postData['Minors'] as $key => $obj){
            $minors[$key]= new Minor();
            $minors[$key]->setMain($main);
            $minors[$key]->setMinorTitle($obj['minorTitle']);
            $em->persist($minors[$key]);
        }

        $em->persist($main);
        $em->flush();
   }

but either it does not work, or it saves twice the same subobject (only once with the correct foreign key).

(Maybe, I could fix by two different MinorType classes, but I would like to avoid that)

Thanks

标签: forms symfony
1条回答
叛逆
2楼-- · 2019-08-19 02:12

Just a number of hints.

  1. your form types should have the data_class option set to the appropriate class.
  2. your form field names should match the property name on the entity. (and by default, all property names are in camelCase, lowercase first char ... in symfony)
  3. after 1. and 2. you get proper entities by just calling $form->getData() (or, as you might have noticed, when you give the createForm call an entity, it will be modified by the form component - this might not always be intended. consider Data Transfer Objects (DTO) for when it's not intended.)
  4. your CollectionType field should have option byReference set to false, such that the setters get used on the collection field (Main::setMinors, in this case).
  5. usually the one-to-many side (i.e. Main class) can get away with:

    public function setMinors(array $minors) {
        foreach($minors as $minor) {
            $minor->setMain($this); // set the main, just to be safe
        }
        $this->minors = $minors;    // set the property Main.minors
    }
    

    but you should not do this in setMain in reverse too (it's also not so trivial. alternative to setMinors are addMinor and removeMinor, there are benefits and costs for either solution, but when it comes to forms, they are quite equivalent, I would say)

  6. on Main if you set the cascade={"PERSIST"} option on the OneToMany (i.e. @ORM\OneToMany(targetEntity="App\Entity\Minor", cascade={"PERSIST"})), you don't have to explicitly call persist on all minors, they will get persisted as soon as you persist (and flush) the Main object/instance.

  7. Finally, either add an option to your minor type, to omit the main form field, or add a new form type MainMinorType (or whatever) that doesn't have the main form field (extend MinorType and remove the main field). This removes the necessity for dirty hacks ;o)

However, overall, if you don't set the minors on the main in a bi-directional relationship, the results are not clearly defined. (just assume for a moment, A has a link to B, but B doesn't have a link to A, but should have, because it's a bi-directional relationship. It could mean, that the link has to be established. It could also mean, that the link should be removed. So, to be safe and clearly communicate what is intended, set both sides!) And ultimately, this might be the reason it doesn't work as intended.

update

To elaborate on point 7. Your MinorType could be amended like this:

class MinorType extends AbstractType {
    public function buildForm(FormBuilderInterface $builder, array $options) {
        // ... other fields before
        if(empty($options['remove_main_field'])) {
            // field is the same, but isn't added always, due to 'if'
            $builder->add('main', EntityType::class, [
               'class' => Main::class,
               'choice_label' => 'mainTtile',
               'label' => 'main'
            ]);
        }
        // ... rest of form
    }

    public function configureOptions(OptionsResolver $resolver) {
        // maybe parent call ...
        $resolver->setDefaults([
            // your other defaults
            'remove_main_field' => false, // add new option, self-explanatory
        ]);
    }
}

in your MainType you had, the following, to which I added the new option

       ->add('Minor', EntityType::class, array(
               'class' => Minor::class,
               'remove_main_field' => true, // <-- this is new
       ))

now, this will remove the main field from your minors forms, when it's embedded in your main form. the default is however, to not remove the main field, so when you edit a minor by itself, the main field will be rendered, as it was before ... unless I made a mistake in my code ;o)

查看更多
登录 后发表回答