Reputation: 624
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
Upvotes: 0
Views: 454
Reputation: 8374
Just a number of hints.
data_class
option set to the appropriate class.$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.)CollectionType
field should have option byReference
set to false
, such that the setters get used on the collection field (Main::setMinors
, in this case).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)
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.
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)
Upvotes: 2