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';
}
}
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:
by_reference
is set to false
at the field,adder
and remover
method on the owning side of the relationship,getter
is accessible for the PropertyAccessor
to check if by_reference
can be used,prototype
to handle adding/removing via Javascript, make sure allow_add
is set to true
.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:
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];
}
}
If not then:
setEntityName()
entityName()
__set()
$entity_name
(Should be public)__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();
// ....
}
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 :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With