问题
so I have a Mission entity connected to an Option entity through a ManyToMany relationship. At the time of creating a new mission it is possible to add several options (this point is ok). Now when updating a mission, when I add a new option it overwrites the last option if it already exists otherwise it fits well. I want to be able to add as many options when modifying a mission without overwriting the last existing option. I post my code:
Mission entity :
/**
* @var ArrayCollection $options
*
* @ORM\ManyToMany(targetEntity="App\Entity\Option", inversedBy="missionOptions", cascade={"persist","remove"})
* @ORM\JoinColumn(nullable=true)
*/
protected $options;
/**
* Mission constructor.
*/
public function __construct()
{
$this->options = new ArrayCollection();
}
public function addOption(Option $option)
{
$tag->addMission($this);
$this->options->add($option);
return $this;
}
public function addOptions($options)
{
foreach($options as $option){
$this->addOption($option);
}
return $this;
}
public function removeOption(Option $option)
{
$this->options->removeElement($option);
return $this;
}
public function removeOptions($options)
{
foreach($options as $option){
$this->removeOption($option);
}
return $this;
}
public function setOptions(Option $option)
{
$this->options[] = $option;
return $this;
}
/**
* Get options
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getOptions()
{
return $this->options;
}
Option entity:
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Mission", mappedBy="options", cascade={"persist","remove"})
*/
private $missionOptions;
/**
* Option constructor.
*/
public function __construct()
{
$this->missionOptions = new ArrayCollection();
}
public function setMission($missions)
{
$this->missionOptions = $missions;
}
public function getMission()
{
return $this->missionOptions;
}
public function addMission(Mission $mission)
{
if (!$this->missionOptions->contains($mission)) {
$this->missionOptions->add($mission);
}
return $this;
}
OptionType:
$builder->add('tagname', TextType::class, array(
'required' => true,
'translation_domain' => 'FOSUserBundle',
'label' => 'form.option_name',
'attr' => array(
'class' => 'with-border',
'placeholder' => 'Nouvelle option'
)
))
->add('prix', NumberType::class, array(
'required' => true,
'translation_domain' => 'FOSUserBundle',
'label' => 'form.option_price',
'attr' => array(
'min' => '2000',
'class' => 'with-border',
'placeholder' => 'prix'
)
))
;
MissionType:
$builder->add('options', CollectionType::class, [
'entry_type' => OptionType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required' => false
])
In MissionController:
public function updateAction(Request $request, $id)
{
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('App:Mission')->find($id);
$originalOptions = new ArrayCollection();
foreach ($entity->getOptions() as $option) {
$originalOptions->add($option);
}
$editForm = $this->createEditForm($entity);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
foreach ($originalOptions as $option) {
if ($option->getTagname() == null || $option->getPrix() == null) {
$option->getMission()->removeElement($entity);
$em->persist($option);
$em->remove($option);
}
}
$em->flush();
return $this->redirectToRoute('dashboard_missions');
}
}
In my twig:
<ul class="options" data-prototype="{{ form_widget(edit_form.options.vars.prototype)|e('html_attr') }}"></ul>
<script>
var $collectionHolder;
var $addTagButton = $('<button type="button" class="button add_option_link"> Ajouter une option</button>');
var $newLinkLi = $('<li></li>').append($addTagButton);
jQuery(document).ready(function() {
$collectionHolder = $('ul.options');
$collectionHolder.append($newLinkLi);
$collectionHolder.data('index', $collectionHolder.find(':input').length);
$addTagButton.on('click', function(e) {
addTagForm($collectionHolder, $newLinkLi);
});
});
function addTagForm($collectionHolder, $newLinkLi) {
var prototype = $collectionHolder.data('prototype');
var index = $collectionHolder.data('index');
var newForm = prototype;
newForm = newForm.replace(/__name__/g, index);
$collectionHolder.data('index', index + 1);
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
addTagFormDeleteLink($newFormLi);
}
function addTagFormDeleteLink($tagFormLi) {
var $removeFormButton = $('<button style="margin-left: 10px" type="button" class="button"></button>');
$tagFormLi.append($removeFormButton);
$removeFormButton.on('click', function(e) {
$tagFormLi.remove();
});
}
</script>
Screening: Show edit :
When add new:
After update:
Thank you in advance for all help
回答1:
The observations you describe are consistent with the following ...
When your form is created, it adds fields with the names
mission_edit[options][0][tagname]
mission_edit[options][0][prix]
mission_edit[options][1][tagname]
mission_edit[options][1][prix]
mission_edit[options][2][tagname]
mission_edit[options][2][prix]
and when you add two other option, it adds:
mission_edit[options][0][tagname]
mission_edit[options][0][prix]
mission_edit[options][1][tagname]
mission_edit[options][1][prix]
and then you scrap the first option leaving you with (including the original options!)
mission_edit[options][0][tagname]
mission_edit[options][0][prix]
mission_edit[options][1][tagname]
mission_edit[options][1][prix]
mission_edit[options][2][tagname]
mission_edit[options][2][prix]
mission_edit[options][1][tagname]
mission_edit[options][1][prix]
and since php will always overwrite duplicate var names ... you overwrite your original second option (with the index 1
) with the new option.
You should verify by actually looking at the DOM. If my assumption is correct, than you "cleverly" omitted the specific part of your form in your twig templates, where the original elements are created, which is probably outside the <ul>
which leads to the index
counter on the ul
to be 0
, and overwriting the elements accordingly.
To be honest, I dislike using a specific line of code from the symfony example - which you're also using - for exactly that reason, the line of code is:
$collectionHolder.data('index', $collectionHolder.find(':input').length);
since it uses a non-obvious set of assumptions, particularly: your sub-form has at least one input type of field, which might not be the case! (e.g. editable + lots of js), and relevant for your example, that all existing children are put inside the collection holder, which you don't do. To be fair the symfony example where this is shown absolutely works, since the assumptions hold true there.
Instead* I would use (* meaning, removing the other line of code!)
<ul class="options"
data-prototype="{{ form_widget(edit_form.options.vars.prototype)|e('html_attr') }}"
data-index="{{ edit_form.options.children|length }}"> {# <- this is the important part!! #}
{# I would also add the existing children here, but there may be reasons not to! #}
</ul>
because it correctly sets the index to the number of children, which it is supposed to be.
来源:https://stackoverflow.com/questions/58556002/new-collection-element-overwrites-the-last-element-if-it-already-exists-when-upd