0

Here is the situation, I have a form in which I need an entity field type. Inside the BenefitGroup entity I have a BenefitGroupCategory selection.

My buildform is:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->add('BenefitGroupCategories', 'entity', array(
                'class' => 'AppBundle:BenefitGroupCategory',
                'property' => 'name',
                'label' => false,
                'query_builder' => function(EntityRepository $er) {
                    return $er->createQueryBuilder('c')
                    ->orderBy('c.name', 'ASC');
                },))
            ->add('benefitsubitems', 'collection', array('type' => new BenefitSubItemFormType(), 'allow_add'    => true, 'label' => false,));

}

It's almost a typical product-category relationship. A BenefitGroup can have only one category and a category can belong to many BenefitGroups (the only complication, not implemented yet, but that's the reason I need the query builder, is that all will depend on another parameter (project) so that some categories will be the default ones (always available), others will be available only for specific projects (see below the reference to project in the BenefitGroupCategory entity)).

You'll notice another field, benefitsubitems, which is not relevant for the question at hand.

As far I understand it, from the Doctrine perspective, I have to set up a One-To-Many, Unidirectional with Join Table.

The two entities are:

<?php
// src/AppBundle/Entity/BenefitGroup.php

namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="AppBundle\Entity\BenefitGroupRepository")
 * @ORM\Table(name="benefit_groups")
 */
class BenefitGroup
{
/**
 * @ORM\Column(type="integer")
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="AUTO")
 */
protected $id;

/**
 * @ORM\ManyToOne(targetEntity="BenefitItem", cascade={"persist"}, inversedBy="BenefitGroups")
 */
protected $benefitItem;

/**
 * @ORM\oneToMany(targetEntity="BenefitSubItem", mappedBy="benefitGroup")
 */    
protected $BenefitSubItems;


/**
 * @ORM\ManyToMany(targetEntity="BenefitGroupCategory")
 * @ORM\JoinTable(name="BenefitGroup_BenefitGroupCategory", joinColumns={@ORM\JoinColumn(name="BenefitGroup_id", referencedColumnName="id")}, inverseJoinColumns={@ORM\JoinColumn(name="BenefitGroupCategory_id", referencedColumnName="id", unique=true)})
 */    
protected $BenefitGroupCategories;

// HERE I HAVE SOME IRRELEVANT GETTERS AND SETTERS    

/**
 * Constructor
 */
public function __construct()
{
    $this->BenefitSubItems = new ArrayCollection();
    $this->BenefitGroupCategories = new ArrayCollection();
}

/**
 * Add BenefitGroupCategories
 *
 * @param \AppBundle\Entity\BenefitGroupCategory $benefitGroupCategories
 * @return BenefitGroup
 */
public function addBenefitGroupCategory(\AppBundle\Entity\BenefitGroupCategory $benefitGroupCategories)
{
    $this->BenefitGroupCategories[] = $benefitGroupCategories;

    return $this;
}

/**
 * Remove BenefitGroupCategories
 *
 * @param \AppBundle\Entity\BenefitGroupCategory $benefitGroupCategories
 */
public function removeBenefitGroupCategory(\AppBundle\Entity\BenefitGroupCategory $benefitGroupCategories)
{
    $this->BenefitGroupCategories->removeElement($benefitGroupCategories);
}

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

You'll also notice another entity, BenefitItem, which is the "father" of BenefitGroup.

And

<?php
// src/AppBundle/Entity/BenefitGroupCategory.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * @ORM\Entity()
 * @ORM\Table(name="benefit_group_category")
 * @UniqueEntity(fields={"name", "project"}, ignoreNull=false, message="Duplicated group category for this project")
 */
class BenefitGroupCategory
{
/**
 * @ORM\Column(type="integer")
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="AUTO")
 */
protected $id;

/**
 * @ORM\Column(type="string", length=50)
 */
protected $name;
/**
 * @ORM\ManyToOne(targetEntity="Project")
 */
protected $project;

// HERE I HAVE SOME IRRELEVANT GETTERS AND SETTERS    

}

In the controller (you'll see several embedded collections, which work ok) I have:

/**
 * @Route("/benefit/show/{projectID}", name="benefit_show")
 */
public function showAction(Request $request, $projectID)
{
    $id=4; //the Id of the CVC to look for
    $storedCVC = $this->getDoctrine()
                      ->getRepository('AppBundle:CVC')
                      ->find($id);
    $form = $this->createForm(new CVCFormType(), clone $storedCVC);

    $form->handleRequest($request);

    if ($form->isValid())
        {
        $em = $this->getDoctrine()->getManager();
        //$benefitGroupCategoryRepository = $this->getDoctrine()->getRepository('AppBundle:BenefitGroupCategory');
        $formCVC = $form->getData();
        $em->persist($formCVC);
        foreach ($formCVC->getBenefitItems() as $formBI)
            {
            $newBI = new BenefitItem();
            $newBI->setCVC($formCVC);
            $newBI->setComment($formBI->getComment());
            $em->persist($newBI);

            foreach ($formBI->getBenefitGroups() as $formBG)
                {
                $newBG = new BenefitGroup();
                $newBG->setBenefitItem($newBI);
                $newBG->setBenefitGroupCategories($formBG->getBenefitGroupCategories());
                $em->persist($newBG);

                foreach ($formBG->getBenefitSubItems() as $formSI)
                    {
                    $newSI = new BenefitSubItem();
                    $newSI->setBenefitGroup($newBG);
                    $newSI->setComment($formSI->getComment());
                    $em->persist($newSI);
                    }
                }
            }
            $em->flush();            
        }

    return $this->render('benefit/show.html.twig', array(
        'form' => $form->createView(),
    ));
}

The problem is: in visualization it visualizes correctly the form (even though it does not retrieve correctly the category. I have a choice of categories, which is ok, but it does not retrieve the right one. Do I have to set the default value in the form?

The problem gets way worse when I sumbit the form it's supposed to create a new entity (notice the clone) with all the nested ones. The problem is that it crashes saying:

Neither the property "BenefitGroupCategories" nor one of the methods
"addBenefitGroupCategory()"/"removeBenefitGroupCategory()", 
"setBenefitGroupCategories()", "benefitGroupCategories()", "__set()" or 
"__call()" exist and have public access in class 
"AppBundle\Entity\BenefitGroup".

The "beauty" is that even if I comment completeley the (nasty) part inside the "isValid" it behaves exactly the same.

I'm lost :(

Sergio Negri
  • 2,023
  • 2
  • 16
  • 38

1 Answers1

0

About the cloning you have to unset the id of the cloned entity, look here: https://stackoverflow.com/a/14158815/4723525

EDIT:

Yes, but PHP just do shallow copy, you have to clone other objects. Look at Example #1 Cloning an object in http://php.net/manual/en/language.oop5.cloning.php. You have to clone your objects by defining __clone method (for Doctrine lower than 2.0.2 you have to do this by calling own method after cloning because proxy defines it's own __clone method). So for example:

function __clone() {
   $oldCollection = $this->collection;
   $this->collection = new ArrayCollection();
   foreach ($oldCollection as $oldElement) {
       $newElement = clone $oldElement;
       // additional actions for example setting this object to owning side
       $newElement->setParent($this);
       $this->collection->add($newElement);
   }
}
Community
  • 1
  • 1
Greg
  • 783
  • 4
  • 11
  • I'm pretty sure you don't have to. I was suggested the method here https://stackoverflow.com/questions/29264467/cloning-object-after-getting-data-from-a-form-in-symfony2 and it actually works perfectly. If I eliminate the category thing but keep all the rest (the 3 level embeddedness etc...) it all works, it creates an updated version of all the objects correctly linked to each other. – Sergio Negri Mar 28 '15 at 15:02
  • I'm running Doctrine 2.4.7. And I do create all the other objects assigning to the right entity. It works. The problem is not there. Actually I stripped down the code to a few lines and detected where it fails. It's in the create form (I also eliminated the clone just in case and the problem persisted). Digging a bit more Symfony's documentation I found that I have to implement the getters properly. "One easy way to do this is to add some extra logic to addTag(), which is called by the form type since by_reference is set to false:". Will have to figure out what to do in my case. – Sergio Negri Mar 29 '15 at 13:59
  • I got this all wrong (I was treating the entity type as a collection). You were right about the deep cloning: for some obscure reason (namely the cascade persist) all entities get properly persisted. The only one that gets referenced to the original entity is the entity type, so when I update the category in the form it creates all the entities but changes the original category as well :( . Will have to try implementing what you suggest. – Sergio Negri Mar 30 '15 at 12:02