Symfony2 forms collection does not call addxxx and removexxx, even if "by_reference" => false
I have a Customer object and two one-to-many CustomerPhone and CustomerAddress relationships.
The Customer object has addPhone / removePhone and addAddress / removeAddress "adders".
CustomerType collection options have 'by_reference' => false for both collections.
Entity functions addPhone / removePhone and addAddress / removeAddress are not called after the form is submitted, therefore CustomerPhone and CustomerAddress do not have a parent identifier after saving.
Why, when adding a submit form, the address / delete phone and addAddress / removeAddress cannot be added?
UPD 1.
After the @Baig sentence , I now have addPhone / removePhone "adders", but addAddress / removeAddress is not. It doesnโt work out why, because they are identical.
# TestCustomerBundle/Entity/Customer.php /** * @var string * * @ORM\OneToMany(targetEntity="CustomerPhone", mappedBy="customerId", cascade={"persist"}, orphanRemoval=true) */ private $phone; /** * @var string * * @ORM\OneToMany(targetEntity="CustomerAddress", mappedBy="customerId", cascade={"persist"}, orphanRemoval=true) */ private $address; The same file "adders"
# TestCustomerBundle/Entity/Customer.php /** * Add customer phone. * * @param Phone $phone */ public function addPhone(CustomerPhone $phone) { $phone->setCustomerId($this); $this->phone->add($phone); return $this; } /** * Remove customer phone. * * @param Phone $phone customer phone */ public function removePhone(CustomerPhone $phone) { $this->phone->remove($phone); } /** * Add customer address. * * @param Address $address */ public function addAddress(CustomerAddress $address) { $address->setCustomerId($this); $this->address->add($address); return $this; } /** * Remove customer address. * * @param Address $address customer address */ public function removeAddress(CustomerAddress $address) { $this->address->remove($address); } relations:
# TestCustomerBundle/Entity/CustomerPhone.php /** * @ORM\ManyToOne(targetEntity="Customer", inversedBy="phone") * @ORM\JoinColumn(name="customer_id", referencedColumnName="id") **/ private $customerId; #TestCustomerBundle/Entity/CustomerAddress.php /** * @ORM\ManyToOne(targetEntity="Customer", inversedBy="address") * @ORM\JoinColumn(name="customer_id", referencedColumnName="id") **/ private $customerId; CustomerType Form:
public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name') ->add('phone', 'collection', array( 'type' => new CustomerPhoneType(), 'allow_add' => true, 'allow_delete' => true, 'by_reference' => false, 'options' => array('label' => false) )) ->add('address', 'collection', array( 'type' => new CustomerAddressType(), 'allow_add' => true, 'allow_delete' => true, 'by_reference' => false, 'options' => array('label' => false) )) ->add('submit', 'submit') ; } controller.
# TestCustomerBundle/Controller/DefaultController.php public function newAction(Request $request) { $customer = new Customer(); // Create form. $form = $this->createForm(new CustomerType(), $customer); // Handle form to store customer obect with doctrine. if ($request->getMethod() == 'POST') { $form->bind($request); if ($form->isValid()) { /*$em = $this->get('doctrine')->getEntityManager(); $em->persist($customer); $em->flush();*/ $request->getSession()->getFlashBag()->add('success', 'New customer added'); } } // Display form. return $this->render('DeliveryCrmBundle:Default:customer_form.html.twig', array( 'form' => $form->createView() )); } UPD 2. Check if addAddress is called.
/** * Add customer address. * * @param Address $address */ public function addAddress(Address $address) { jkkh; // Test for error if method called. Nothing throws. $address->setCustomerId($this); $this->address->add($address); } UPD 3.
CustomerAddressType.php
<?php namespace Delivery\CrmBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class CustomerAddressType extends AbstractType { /** * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('street') ->add('house') ->add('building', 'text', ['required' => false]) ->add('flat', 'text', ['required' => false]) ; } /** * @param OptionsResolverInterface $resolver */ public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Delivery\CrmBundle\Entity\CustomerAddress' )); } /** * @return string */ public function getName() { return 'delivery_crmbundle_customeraddress'; } } CustomerPhoneType.php
<?php namespace Delivery\CrmBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class CustomerPhoneType extends AbstractType { /** * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('number') ; } /** * @param OptionsResolverInterface $resolver */ public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'Delivery\CrmBundle\Entity\CustomerPhone' )); } /** * @return string */ public function getName() { return 'phone'; } } This answer is consistent with Symfony 3, but I'm sure it applies to Symfony 2. Also, this answer is more a link than, in particular, an OP problem (which I don't understand)
In ..Symfony/Component/PropertyAccess/PropertyAccessor.php the writeProperty method writeProperty responsible for calling the setXXXXs or addXXX and removeXXXX .
So, here is the order in which he searches for a method:
If the object is an
arrayor an instance ofTraversable(which is anArrayCollection), then a pairaddEntityNameSingular()removeEntityNameSingular()Source for reference:
if (is_array($value) || $value instanceof \Traversable) { $methods = $this->findAdderAndRemover($reflClass, $singulars); if (null !== $methods) { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; $access[self::ACCESS_ADDER] = $methods[0]; $access[self::ACCESS_REMOVER] = $methods[1]; } }
If not, then:
setEntityName()entityName()__set()$entity_name(Must be publicly available)__call()Source for reference:
if (!isset($access[self::ACCESS_TYPE])) { $setter = 'set'.$camelized; $getsetter = lcfirst($camelized); // jQuery style, eg read: last(), write: last($item) if ($this->isMethodAccessible($reflClass, $setter, 1)) { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; $access[self::ACCESS_NAME] = $setter; } elseif ($this->isMethodAccessible($reflClass, $getsetter, 1)) { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_METHOD; $access[self::ACCESS_NAME] = $getsetter; } elseif ($this->isMethodAccessible($reflClass, '__set', 2)) { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; $access[self::ACCESS_NAME] = $property; } elseif ($access[self::ACCESS_HAS_PROPERTY] && $reflClass->getProperty($property)->isPublic()) { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_PROPERTY; $access[self::ACCESS_NAME] = $property; } elseif ($this->magicCall && $this->isMethodAccessible($reflClass, '__call', 2)) { // we call the getter and hope the __call do the job $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC; $access[self::ACCESS_NAME] = $setter; } else { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; $access[self::ACCESS_NAME] = sprintf( 'Neither the property "%s" nor one of the methods %s"%s()", "%s()", '. '"__set()" or "__call()" exist and have public access in class "%s".', $property, implode('', array_map(function ($singular) { return '"add'.$singular.'()"/"remove'.$singular.'()", '; }, $singulars)), $setter, $getsetter, $reflClass->name ); } }
To answer the OP problem, based on the above information, the symfony PropertyAccessor class will not be able to read your addXX and removeXX method removeXX . A potential reason may be that it is not identified as an array or ArrayCollection , which must be executed from the constructor of the object
public function __construct() { $this->address = new ArrayCollection(); // .... } For me, this was finally resolved by adding getXXX , which returns the collection in PropertyAccessor . Without this, you continue to wonder why addXXX or removeXXX not called.
Therefore, make sure that:
- The
by_referenceparameterby_referenceset tofalsein the field, - You have an
adderandremovermethod on the owner side of the relationship, getteris available forPropertyAccessorto check ifby_referencecan be used,- If you want to use
prototypeto handle add / remove using Javascript, make sureallow_addset totrue.
I had the same problem, but I'm not sure if this is the same reason.
I get my entity attribute, which has a one-way connection with the relationship "OneToMany", at the end should be "s". So in "handleRequest" (leave it a black box, I did not search inside), symfony will find your "addxxx" without "s".
In the Task - Tag example, he declared "tags", but getTag.
In your case, I would think that you are changing your phone to $ phones, and the method:
public function setPhones($phones){} public function addPhone(Phone $phone){} To the name of the method your form is looking for, simply remove the temporary setter in your entity and submit the form, symfony will tell you.
Hope this helps you :)