Symfony2表单集合不调用addxxx和removexxx,即使'by_reference'=>假

时间:2015-10-27 02:29:20

标签: forms symfony collections doctrine

我有Customer实体和两个一对多关系CustomerPhone和CustomerAddress。

Customer实体具有addPhone / removePhone和addAddress / removeAddress“adders”。

CustomerType集合选项具有'by_reference'=>这两个集合都是假的。

实体函数addPhone / removePhone和在表单提交后未调用的addAddress / removeAddress,因此CustomerPhone和CustomerAddress在持久化后没有父ID。

为什么可以在表单提交时调用addPhone / removePhone和addAddress / removeAddress?

UPD 1。

@Baig suggestion之后我现在调用了addPhone / removePhone“adders”,但是没有添加地址/删除地址。无法理解,因为它们完全相同。

 # 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;

相同文件“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);
}

关系:

# 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表单:

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')
    ;
}

控制器。

# 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。 测试addAddress是否被调用。

/**
     * 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';
    }
}

3 个答案:

答案 0 :(得分:3)

这个答案对应于Symfony 3,但我相信这也适用于Symfony 2。此答案更多是作为参考,而不是特别解决OP的问题(我不清楚)

..Symfony/Component/PropertyAccess/PropertyAccessor.php上,方法writeProperty负责调用setXXXXsaddXXX&amp; removeXXXX方法。

所以这里是它寻找方法的顺序:

  1. 如果实体是arrayTraversable的实例(ArrayCollection是),那么

    • addEntityNameSingular()
    • removeEntityNameSingular()

      参考资料来源:

      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. 如果没有,那么:

    1. setEntityName()
    2. entityName()
    3. __set()
    4. $entity_name(应公开)
    5. __call()

      参考资料来源:

      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
              );
          }
      }
      
  3. 为了回答OP的问题,基于上述信息,symfony的PropertyAccessor类无法正确读取您的addXXremoveXX方法。可能的原因可能是未被识别为必须从实体的构造函数完成的arrayArrayCollection

    public function __construct() {
         $this->address = new ArrayCollection();
         // ....
    }
    

答案 1 :(得分:3)

对我来说,这最终通过添加getXXX来解决,PropertyAccessor将集合返回到addXXX。如果没有这个,你会继续想知道为什么removeXXXby_reference没有被调用。

所以请确保:

  • 选项false在字段
  • 设置为adder
  • 您在关系的拥有方拥有removergetter方法,
  • PropertyAccessor可以访问by_reference,以检查是否可以使用prototype
  • 如果您想使用allow_add来处理通过Javascript添加/删除的问题,请确保将true设置为case_insensitive_equals

答案 2 :(得分:1)

我有同样的问题,但我不确定这是同一个原因。

我的实体属性具有OneToMany关系,最后必须有's'。所以在“handleRequest”(留下一个黑盒子,我没有在里面查找),symfony会发现你的“addxxx”没有“s”。

在示例'Task - Tag'中,他声明了“tags”但是getTag。

在您的情况下,我认为您将$ phone更改为$ phone,方法如下:

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

对于您的表单搜索方法的名称,只需删除您实体中的临时设置者并提交您的表单,symfony会告诉您。

希望这会帮助你:)