Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony2 form collection not calling addxxx and removexxx even if 'by_reference' => false

I have the Customer entity and two one-to-many relations CustomerPhone and CustomerAddress.

The Customer entity has addPhone/removePhone and addAddress/removeAddress "adders".

CustomerType collections options has 'by_reference' => false for both collections.

Entity functions addPhone/removePhone and addAddress/removeAddress not called after form submitted, so CustomerPhone and CustomerAddress have no parent id after persist.

Why could addPhone/removePhone and addAddress/removeAddress not called on form submit?

UPD 1.

After @Baig suggestion now I have addPhone/removePhone "adders" called, but addAddress/removeAddress not. Can't get 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;

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. Test if addAddress 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';
    }
}
like image 763
Nick Denry Avatar asked Oct 27 '15 02:10

Nick Denry


3 Answers

For me this was eventually solved by adding getXXX, which returns the collection to the PropertyAccessor. Without that you keep on wondering why addXXX or removeXXX aren't called.

So make sure that:

  • The option by_reference is set to false at the field,
  • You have both the adder and remover method on the owning side of the relationship,
  • The getter is accessible for the PropertyAccessor to check if by_reference can be used,
  • If you want to use the prototype to handle adding/removing via Javascript, make sure allow_add is set to true.
like image 171
Rvanlaak Avatar answered Nov 18 '22 12:11

Rvanlaak


This answer corresponds to Symfony 3, but I am sure this applies to Symfony 2 as well. Also this answer is more as a reference than addressing OP's issue in particular (which I am not to clear)

On ..Symfony/Component/PropertyAccess/PropertyAccessor.php the method writeProperty is responsible for calling either setXXXXs or addXXX & removeXXXX methods.

So here is order on which it looks for the method:

  1. If the entity is array or instance of Traversable (which ArrayCollection is) then pair of

    • addEntityNameSingular()
    • 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];
          }
      }
      
  2. If not then:

    1. setEntityName()
    2. entityName()
    3. __set()
    4. $entity_name (Should be public)
    5. __call()

      Source for reference:

      if (!isset($access[self::ACCESS_TYPE])) {
          $setter = 'set'.$camelized;
          $getsetter = lcfirst($camelized); // jQuery style, e.g. 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 OP's issue, based on the above mentioned information, the PropertyAccessor class of symfony is not able to read your addXX and removeXX method properly. The potential reason might be that is not identified as array or ArrayCollection which has to be done from the constructor of the entity

public function __construct() {
     $this->address = new ArrayCollection();
     // ....
}
like image 36
Starx Avatar answered Nov 18 '22 10:11

Starx


I had the same problem, but I'm not sure that it's the same cause.

I your entity's attribute that has OneToMany relation ship must have an 's' at the end. So that in "handleRequest" (leave it a black box, I didn't look up inside), symfony would find your "addxxx" without "s".

In the exemple 'Task - Tag', he declared "tags" but getTag.

In your case I would think you change your $phone to $phones and the method follow:

public function setPhones($phones){}
public function addPhone(Phone $phone){}

To the name of method your form searching for, just delete temporarily setter in your entity and submit your form, symfony will told you.

Just hope this would help you out :)

like image 1
Kihansi Avatar answered Nov 18 '22 11:11

Kihansi