简体   繁体   中英

Symfony 2.8: Form collection and one-to-many relationship not persisting correctly

I need to embed an entity form inside it's parent form. I followed the official documentation to embed a form inside another with one-to-many relationship.

The problem is that though I manage to display correctly the forms, when need to persist another child entity, it works properly with the first 2 object, when I try to create the third object, it deletes the second object in the array and create the third one.

Example : I have a CV entity that has one-to-many relationship to WorkExperience entity

Code:

Entities:

    class Curriculum
    {
        /**
        * @ORM\OneToMany(targetEntity="appBundle\Entity\WorkExperience", mappedBy="curriculum", cascade={"persist", "remove"}, orphanRemoval=true)
        */
        private $workExperience;

        public function __construct()
        {
            $this->workExperience = new \Doctrine\Common\Collections\ArrayCollection();
        }


     /**
     * Add workExperience
     *
     * @param appBundle\Entity\WorkExperience $workExperience
     * @return Curriculum
     */
    public function addWorkExperience(appBundle\Entity\WorkExperience $workExperience)
    {
        $this->workExperience->add($workExperience);
        $workExperience->setCurriculum($this);

        return $this;
    }

     /**
     * Remove workExperience
     *
     * @param appBundle\Entity\WorkExperience $workExperience
     */
    public function removeWorkExperience(appBundle\Entity\WorkExperience $workExperience)
    {
        $this->workExperience->removeElement($workExperience);
    }

     /**
     * Get workExperience
     *
     * @return \Doctrine\Common\Collections\Collection
     */
        public function getWorkExperience()
        {
            return $this->workExperience;
        }
}

WorkExperience:

class WorkExperience
{
    /**
     * @ORM\ManyToOne(targetEntity="appBundle\Entity\Curriculum", inversedBy="workExperience")
     * @ORM\JoinColumn(name="curriculum", referencedColumnName="id")
     */
    private $curriculum;

    /**
     * Set curriculum
     *
     * @param string $curriculum
     * @return WorkExperience
     */
    public function setCurriculum($curriculum)
    {
        $this->curriculum = $curriculum;

        return $this;
    }

    /**
     * Get curriculum
     *
     * @return string 
     */
    public function getCurriculum()
    {
        return $this->curriculum;
    }
}

Then, the formType (I only created the workExperience form since it's what I need in my collectionType field)

WorkExperienceType:

<?php

namespace appBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use appBundle\Entity\WorkExperience;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Intl\Intl;
use Symfony\Component\Form\Extension\Core\Type\DateType;

class WorkExperienceType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {

        $builder
            ->add('fromDate', DateType::class, array(
                'widget' => 'single_text',
                'html5' => false,
                'attr' => ['class' => 'form-control js-datepicker'],
                'format' => 'dd-MM-yyyy', 
                'label' => 'From')
            )
            ->add('toDate', DateType::class, array(
                'widget' => 'single_text',
                'html5' => false,
                'attr' => ['class' => 'form-control js-datepicker'],
                'format' => 'dd-MM-yyyy', 
                'label' => 'To',
                'required' => false,
                )
            )
            ->add('ongoing', 'checkbox', array('required' => false,))
            ->add('jobProfile', EntityType::class, array(
                'class' => 'appBundle:JobProfile',
                'query_builder' => function (EntityRepository $er) use ($options) {
                    return $er->createQueryBuilder('j')
                        ->where('j.company = :company')
                        ->setParameter('company', $options['company']);
                },
                'choice_label' => 'name',
                'required' => false,
                'placeholder' => 'Job Profile'
            ))
            ->add('employerName', 'text', array('label' => "Name"))
            ->add('employerCity', 'text', array('label' => "City"))
            ->add('activities', 'textarea', array('label' => "Activities and responsibilities"));
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefined('company');
        $resolver->setDefaults(array(
          'data_class' => WorkExperience::class,
        ));
    }
}

Curriculum Controller:

I only show you the form management since its where I have the problem (If you wondered I don't pass an id because the connected user can only have one Curriculum)

<?php

namespace appBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use appBundle\Entity\Curriculum;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use appBundle\Form\Type\WorkExperienceType;
use appBundle\Form\Type\EducationCvType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Intl\Intl;
use Doctrine\Common\Collections\ArrayCollection;
use appBundle\Entity\WorkExperience;

class CurriculumController extends Controller
{
public function curriculumEditAction(Request $request)
    {
        if (!$this->getUser() || !$this->get('session')) {
            $response = $this->generateUrl('homepage');
            return new RedirectResponse($response);
        }

        $em = $this->getDoctrine()->getManager();

        $curriculum = $this->getUser()->getEmployee()->getCurriculum();

        $originalWorkExperience = new ArrayCollection();

        foreach ($curriculum->getWorkExperience() as $workExperience) {
            $originalWorkExperience->add($workExperience);
        }

        $originalEducation = new ArrayCollection();

        foreach ($curriculum->getEducationTraining() as $educationTraining) {
            $originalEducation->add($educationTraining);
        }

        $countries = Intl::getRegionBundle()->getCountryNames();

        $formCurriculum = $this->createFormBuilder($curriculum)
                ->add('workExperience', CollectionType::class, array(
                    'entry_type'   => WorkExperienceType::class,
                    'allow_add' => true,
                    'allow_delete' => true,
                    'by_reference' => false,
                    'required' => false,
                    'type' => new WorkExperience(),
                    'entry_options'  => array(
                        'company' => $this->getUser()->getEmployee()->getCompany(),
                    ),
                ))
                ->add('save', 'submit', array('label' => 'Save changes', 'attr' => array('class' => 'btn btn-blue')))
                ->getForm();


        if ('POST' === $request->getMethod()) {

            $formCurriculum->bind($request);

            if ($formCurriculum->isValid()) {
                $curriculum = $formCurriculum->getData();

                try {

                    foreach ($curriculum->getWorkExperience() as $workExperience) {
                        $em->persist($workExperience);

                        $em->flush();
                    }

                    // remove the relationship between the tag and the Task
                    foreach ($originalWorkExperience as $workExperience) {
                        if (false === $curriculum->getWorkExperience()->contains($workExperience)) {
                            // remove the Task from the Tag
                            $workExperience->getCurriculum()->removeWorkExperience($workExperience);

                            // if you wanted to delete the Tag entirely, you can also do that
                            $em->remove($workExperience);
                        }
                    }

                    $em->persist($curriculum);
                    $em->flush();
                    $request->getSession()
                        ->getFlashBag()
                        ->add('success', 'edit_curriculum_ok')
                    ;
                } catch (Exception $e) {
                    $request->getSession()
                        ->getFlashBag()
                        ->add('error', 'edit_curriculum_ko')
                    ;
                }

                return $this->render('appBundle:Curriculum:curriculum_edit.html.twig', array('curriculum' => $curriculum, 'formCurriculum' => $formCurriculum->createView(),));
            }
        }

        return $this->render('appBundle:Curriculum:curriculum_edit.html.twig', array('curriculum' => $curriculum, 'formCurriculum' => $formCurriculum->createView(),));
    }
}

Template:

{% form_theme formCurriculum _self %}

{% block work_experience_widget %}
    <div class="form-group row">
        <div class="col-sm-6">
            <label for="">Nombre Empresa</label>
            {{ form_widget(form.employerName, {'attr': {'class': 'form-control'}}) }}
        </div>
        <div class="col-sm-5">
            <label for="">Categoría Profesional</label>

            {{ form_widget(form.jobProfile, {'attr': {'class': 'form-control'}}) }}
        </div>
        <div class="col-sm-1">
            <a href="#" class="btn btn-danger btn-xs text-center remove-tag"><i class="glyphicon glyphicon-remove"></i></a>
        </div>
    </div>
    <div class="form-group row">
        <div class="col-sm-3">
            <label for="">Fecha desde</label>
            <div class="input-group">
                {{ form_widget(form.fromDate) }}
                <span class="input-group-addon"><i class="glyphicon glyphicon-calendar"></i></span>
            </div>
        </div>
        <div class="col-sm-5">
            <label for="">Fecha hasta</label>
            <div class="input-group">
                <span class="input-group-addon">
                    {{ form_widget(form.ongoing) }}
                  </span>
                {{ form_widget(form.toDate) }}
                <span class="input-group-addon"><i class="glyphicon glyphicon-calendar"></i></span>
            </div>
        </div>
        <div class="col-sm-4">
            <label for="">Ciudad</label>
            {{ form_widget(form.employerCity, {'attr': {'class': 'form-control'}}) }}
        </div>
    </div>
    <div class="form-group">
        <label for="">Actividades y Responsabilidades</label>
        {{ form_widget(form.activities, {'attr': {'class': 'form-control'}}) }}
        {{ form_rest(form) }}
        {{ form_errors(form)}}
    </div>
    <hr />
{% endblock %}

{{ form_start(formCurriculum) }}

                        <div class="col-lg-8 col-xs-12">
                            <h3>Experiencia Laboral</h3>

                            <div class="workExperience" data-prototype="{{ form_widget(formCurriculum.workExperience.vars.prototype)|e('html_attr') }}">
                                {% for workExperience in formCurriculum.workExperience %}
                                    <div class="workExperienceUnit">
                                        {{ form(workExperience)}}
                                    </div>
                                {% endfor %}
                            </div>


                            <div class="form-group text-right">
                                {{ form_widget(formCurriculum.save, {'attr': {'class': 'btn btn-blue'}}) }}
                            </div>
                            <div class="clearfix"></div>
                        </div>
                    {{ form_rest(formCurriculum) }}
                    {{ form_end(formCurriculum) }}

Ajax and js for the form collection:

{% block javascripts %}
    {{ parent() }}    
    <script type="text/javascript">
        var $collectionHolder;

        // setup an "add a tag" link
        var $addWorkExperienceLink = $('<button type="button" class="btn btn-primary width100x100 add_workexperience_link"><span class="badge"><i class="glyphicon glyphicon-plus"></i></span> Agregar nueva experiencia laboral</button>');

        var $newLinkLi = $('<div class="form-group row"><div class="col-sm-12"></div></div>').append($addWorkExperienceLink);

        jQuery(document).ready(function() {

            $collectionHolder = $('div.workExperience');

            // add the "add a tag" anchor and li to the tags ul
            $collectionHolder.append($newLinkLi);

            // count the current form inputs we have (e.g. 2), use that as the new
            // index when inserting a new item (e.g. 2)


            $collectionHolder.data('index', $collectionHolder.find(':input').length);

            $addWorkExperienceLink.on('click', function(e) {
                // prevent the link from creating a "#" on the URL
                e.preventDefault();

                // add a new tag form (see next code block)
                addWorkExperienceForm($collectionHolder, $newLinkLi);
            });

        });

        function addWorkExperienceForm($collectionHolder, $newLinkLi) {
            // Get the data-prototype explained earlier
            var prototype = $collectionHolder.data('prototype');

            // get the new index
            var index = $collectionHolder.data('index');

            $collectionHolder.data('index', index + 1);

            // Replace '$$name$$' in the prototype's HTML to
            // instead be a number based on how many items we have
            var newForm = prototype.replace(/__name__/g, index);

            // Display the form in the page in an li, before the "Add a tag" link li
            var $newFormLi = $('<div class="workExperienceUnit"></div>').append(newForm);

            $newLinkLi.before($newFormLi);

            $('.remove-tag').click(function(e) {
                e.preventDefault();

                $(this).parent().parent().parent().remove();

                return false;
            });
        }

        // handle the removal, just for this example
        $('.remove-tag').click(function(e) {
            e.preventDefault();

            $(this).parent().parent().parent().remove();

            return false;
        });

    </script>
{% endblock %}

And here are the screenshots, the thing is that it deletes the object correctly, the onlye problem is when creating new objects

在此处输入图片说明

Severals things seem to be stranged.

  1. Use $form->handleRequest($request); instead of bind()
  2. Why did you use 'type' => new WorkExperience()? I think you don't have to use thois in CollectionType Field because the type of entity is defined in WorkexperienceTpe.
  3. When you post your form with handlerequest function, you don't need to retrieve all data from form because all workexperience have been added to $formCurriculum automatically

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM