在Symfony2中进行表单后处理

时间:2014-10-11 15:38:40

标签: forms symfony doctrine-orm formbuilder

我是Symfony的新手,我正在尝试创建一个绑定到实体用户的表单。

此实体的一个字段是ArrayCollection类型。它实际上是与另一个类的对象的OneToMany关系。 所以,一些代码只是为了更清楚。

class User 
{
    \\...

    /**
    * @ORM\OneToMany(targetEntity="UserGoods", mappedBy="users")
    * @ORM\JoinColumn(name="goods", referencedColumnName="id")
    */
    private $goods;


    public function __construct()
    {
        $this->goods = new ArrayCollection();
    }

    \\...

}

以及相关的课程

class UserGoods
{
    /**
    * @var integer
    *
    * @ORM\Column(name="id", type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    private $id;

    /**
    * @var \DateTime
    *
    * @ORM\Column(name="inserted_at", type="datetime")
    */
    private $insertedAt;

    /**
    * @var float
    *
    * @ORM\Column(name="value", type="float")
    */
    private $value;

    /**
    * @ORM\ManyToOne(targetEntity="User", inversedBy="goods")
    */
    protected $users;

}

现在,我想创建一个非常简单的 FormBuilder ,但我自己也无法弄清楚如何做到这一点。 我只想要一个 number 类型的字段,如果存在具有当前日期的 Goods 类型的对象,请修改它,否则将新对象添加到集合中。

这可以很容易地在控制器内完成,但我有很多这种形式的实例,这将使我的程序无法维护。

有没有办法在表单构建器中添加一些提交数据的后处理? 我已经尝试过使用DataTransformers,但这些都不够,因为它们最多只能将一个数字转换为UserGoods对象,并且不会保留原始的ArrayCollection(以及教条关联呢?)。 另外,如果我将字段类型声明为数字类型的集合, all ,则在呈现表单时将显示ArrayCollection中的项目,而不仅仅是最后一个。

有关如何摆脱这种情况的任何想法? 提前感谢您的帮助。

2 个答案:

答案 0 :(得分:0)

根据建议,使用表单事件。在活动中,您将检查已提交日期的商品是否已存在(从数据库加载),您将使用发布数据修改它们。如果他们不存在,你将创造新的。您还可以在实体中创建另一个方法getLastItemsInCollection(),其中您可以使用Criteria,仅从数据库加载最后一个(推荐),或从原始ArrayCollection获取最后一项。您可以使字段取消映射,并在FormEvent中手动映射货物,如上所述。我希望有所帮助,我希望我能正确理解。

答案 1 :(得分:0)

我遵循了Cerad和tomazahlin的建议,我想出了一个解决方案。

我相信每年至少有2个人分享同样的问题,所以我会花一些时间来发布我的结果。
随意纠正,批评或加我,最后我是Symfony的新手!

首先,我最终如何定义了我的两个类。

class User 
{
    //...

    /**
    * @ORM\ManyToMany(targetEntity="UserGoods", inversedBy="users", cascade={"persist", "remove"})
    * @ORM\JoinColumn(name="goods", referencedColumnName="id")
    */
  // Should have been a OneToMany relationship, but Doctrine requires the
  //  owner side to be on the Many side, and I need it on the One side. 
  //  A ManyToMany relationship compensate this.
    private $goods;

    public function __construct()
    {
        $this->goods = new ArrayCollection();
    }

    //...

}

连接类

/**
 * @ORM\HasLifecycleCallbacks()
**/
class UserGoods
{
    /**
    * @var integer
    *
    * @ORM\Column(name="id", type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    private $id;

    /**
    * @var \DateTime
    *
    * @ORM\Column(name="inserted_at", type="datetime")
    */
    private $insertedAt;

    /**
    * @var float
    *
    * @ORM\Column(name="value", type="float", nullable=true) 
    */
    // I do not want this field to be null, but in this way when 
    // persisting I can look for null elements and remove them
    private $value;

   /**
    * @ORM\ManyToMany(targetEntity="User", inversedBy="goods")
    */
    protected $users;

    /**
    * @ORM\PrePersist()
    * @ORM\PreUpdate()
    */
    // This automatically sets InsertedAt value when inserting or 
    // updating an element.
    public function setInsertedAtValue()
    {
        $date = new \DateTime();
        $this->setInsertedAt( $date );

    }

}

正如我所说,我想要一个FormBuilder来处理我的数组集合。用于此目的的最佳表单类型是...集合类型。
这需要将子表单定义为其类型。

<?php

namespace MyBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

use MyBundle\Entity\UserGoods;

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {

      $builder->add('goods', 'collection', array(
          'type' => new GoodsdataWithDateType(),
          'required' => false,
          )
      );
\\ ...

和子表单。 由于我只需要显示今天的值,而不是全部,我还需要添加一个FormEvent子句来检查要插入的项目。

namespace MyBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

use Doctrine\ORM\EntityManager;

use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

class GoodsdataWithDateType extends AbstractType
{

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // Here I add the event listener:
        // Since I want only today's value to be displayed, I implement 
        // a check on this field of each element

        $builder->addEventListener(
          FormEvents::PRE_SET_DATA, function (FormEvent $event) {
            $goods = $event->getData();
            $form = $event->getForm();

              $datetime1 = $goods->getInsertedAt();
              $datetime2 = new \DateTime();
              $datetime2->setTime(0, 0, 0);

              if ($datetime1 > $datetime2)
              {
                $form->add('value', 'number', array(
                    'required' => false,
                    ));
        // I am setting this value with LifecycleCallbacks, and I do not 
        // want the user to change it, I am adding it commented just for
        // completeness
        //         $form->add('insertedAt', 'date', array(
        //             'widget' => 'single_text',
        //             'format' => 'yyyy,MM,dd',
        //         ));
              }
        });

    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'MyBundle\Entity\UserGoods',
        ));
    }

    public function getName()
    {
        return 'goodsdatawithdate';
    }

}


这样可以正常工作,但在使用twig文件中的{{form(form)}}之类的内容时显示得非常糟糕。
为了使其更加用户友好,我定制了表单的呈现方式,以便删除一些垃圾并仅包含必要的标签。
所以在我的树枝上:

{{ form_start(form) }}

  {{ form_errors(form) }}
<div>
  {{ form_label(form.goods) }}
  {{ form_errors(form.goods) }} 
  <br>
  {% for field in form.goods %}
      {{ form_widget(field) }}
  {% endfor %}
</div>

{{ form_end(form) }}


到目前为止,这很好,但我也希望在我的收藏中包含新元素,特别是如果尚未插入今天的好。
我可以在我的FormBuilder中执行此操作,方法是在调用$ builder之前在数组中手动​​添加一个新项。

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {

      $thisuser = $builder->getData();

      // I added the following function inside the User class.
      // I use a for loop to scroll all the associated Goods to get the
      // latest one.
      $mygoods = $thisuser->getLatestGoods(); 

      if ( $mygoods && null !== $mygoods->getId() ) {

 //          The Array contains already some elements
          $datetime1 = $mygoods->getInsertedAt();
          $datetime2 = new \DateTime();
          $datetime2->setTime(0, 0, 0);

 //          Check when was the last one inserted
          if ($datetime1 < $datetime2)      // Nice way to compare dates
          {
 //          If it is older than today, add a new element to the array
              $newgoods = new UserGoods();
              $thisuser->addGoods($newgoods);
          }
      } else {

 //          The array is empty and I need to create the firs element
              $newgoods = new UserGoods();
              $thisuser->addGoods($newgoods);
      }

      $builder->add('goods', 'collection', array(
          'type' => new GoodsdataWithDateType(),
          'required' => false,
          'allow_add' => true,      // this enables the array to be
                                    // populated with new elements
          )
      );

但我也希望如果用户删除插入的值(即,在表单中不插入任何内容),则应删除关联的数组元素。
允许用户删除元素有点棘手。我不能依赖'allow_delete'属性,因为只处理集合中的最后一项,所有以前的项都将在提交表单时被删除。
我也不能依赖LifecycleCallbacks,因为对关系所做的更改不会保留在数据库中。
谢天谢地,我找到了一篇帮助我的帖子here。 我需要的是Doctrine Flush操作的EventListener。

namespace MyBundle\EventListener;

use Doctrine\ORM\Event\OnFlushEventArgs;
use MyBundle\Entity\UserGoods;

class EmptyValueListener
{
  public function onFlush(OnFlushEventArgs $args)
  {

    $em = $args->getEntityManager();
    $uow = $em->getUnitOfWork();

    $entities = array_merge(
        $uow->getScheduledEntityInsertions(),
        $uow->getScheduledEntityUpdates()
    );

    foreach ($entities as $entity) {
      if ($entity instanceof UserGoods) {
        if ($entity && null !== $entity )
        {
          if ( empty($entity->getValue()) ) 
          {
            $users = $entity->getUsers();
            foreach ($users as $curruser)
            {
              $curruser->removeGoods($entity);

              $em->remove($entity);
              $md = $em->getClassMetadata('MyBundle\Entity\UserGoods');
              $uow->computeChangeSet($md, $entity);

              $em->persist($curruser);
              $md = $em->getClassMetadata('MyBundle\Entity\User');
              $uow->computeChangeSet($md, $curruser);
            }
          }
        }
      }
    }
  }
}

并在我的config.yml中注册为

mybundle.emptyvalues_listener:
    class: MyBundle\EventListener\EmptyValueListener
    tags:
        - { name: doctrine.event_listener, event: onFlush }