English 中文(简体)
Symfony2 的窗体后处理
原标题:Form post-processing in Symfony2
I am new of Symfony, and I am trying to create a form bound to an Entity User. One field of this entity is of type ArrayCollection. It is actually a OneToMany relationship with objects of another class. So, a little bit of code just to be clearer. class User { \... /** * @ORMOneToMany(targetEntity="UserGoods", mappedBy="users") * @ORMJoinColumn(name="goods", referencedColumnName="id") */ private $goods; public function __construct() { $this->goods = new ArrayCollection(); } \... } And the associated class class UserGoods { /** * @var integer * * @ORMColumn(name="id", type="integer") * @ORMId * @ORMGeneratedValue(strategy="AUTO") */ private $id; /** * @var DateTime * * @ORMColumn(name="inserted_at", type="datetime") */ private $insertedAt; /** * @var float * * @ORMColumn(name="value", type="float") */ private $value; /** * @ORMManyToOne(targetEntity="User", inversedBy="goods") */ protected $users; } Now, I want to create a FormBuilder that does something extremely simple, yet I couldn t figure it out how to do it by myself. I want just a field of type number, and if an object of type Goods with the current date exists, modify it, otherwise add a new object to the collection. This could be easily done inside the controller, but I have a lot of instances of this form, and this would make my program impossible to maintain. Is there a way to add some post-processing of submitted data inside the form builder? I already tried with DataTransformers but these won t suffice, as at most they would transform a number to a UserGoods object, and the original ArrayCollection would not be preserved (and what about doctrine associations?). In addition, if I declare the field type as collection of number types, all the items inside the ArrayCollection would be displayed when rendering the form, not just the last one. Any idea on how to get out of this? Thank you in advance for your help.
问题回答
As suggested, use Form Events. Inside the event you will check if the Goods with the submitted date already exist (load them from database) and your will modify them with the post data. If they dont exist, you will be creating new ones. You can also make another method in your entity, getLastItemsInCollection(), where you can use Criteria, to only load the last one from the database (recommended), or get the last item from original ArrayCollection. You can make a field unmapped, and map the Goods manually in the FormEvent, as described above. I hope that helps and I hope I understood correctly.
I followed Cerad and tomazahlin suggestions and I came up with a solution. I am sure that every year at least 2 people over the world share my same problem, so I ll take some time to post my outcome. Feel free to correct, criticize or add me, in the end I am a newbie of Symfony! First, how I defined my two classes in the end. class User { //... /** * @ORMManyToMany(targetEntity="UserGoods", inversedBy="users", cascade={"persist", "remove"}) * @ORMJoinColumn(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(); } //... } And the connected class /** * @ORMHasLifecycleCallbacks() **/ class UserGoods { /** * @var integer * * @ORMColumn(name="id", type="integer") * @ORMId * @ORMGeneratedValue(strategy="AUTO") */ private $id; /** * @var DateTime * * @ORMColumn(name="inserted_at", type="datetime") */ private $insertedAt; /** * @var float * * @ORMColumn(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; /** * @ORMManyToMany(targetEntity="User", inversedBy="goods") */ protected $users; /** * @ORMPrePersist() * @ORMPreUpdate() */ // This automatically sets InsertedAt value when inserting or // updating an element. public function setInsertedAtValue() { $date = new DateTime(); $this->setInsertedAt( $date ); } } As I said, I wanted a FormBuilder to handle my array collection. The best form type for this purpose is... collection type. This require a subform to be defined as its type. add( goods , collection , array( type => new GoodsdataWithDateType(), required => false, ) ); \ ... And the subform. Since I need only the today s value to be displayed, and not all of them, I also need to add a FormEvent clause to check which items to insert. namespace MyBundleFormType; use SymfonyComponentFormAbstractType; use SymfonyComponentFormFormBuilderInterface; use SymfonyComponentOptionsResolverOptionsResolverInterface; use DoctrineORMEntityManager; use SymfonyComponentFormFormEvent; use SymfonyComponentFormFormEvents; 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 => MyBundleEntityUserGoods , )); } public function getName() { return goodsdatawithdate ; } } This works fine, but is displayed very badly when rendered with something like {{ form(form) }} in twig files. To make it more user-friendly, I customized how the form was presented, in order to remove some garbage and include only the labels that were necessary. So in my twig: {{ form_start(form) }} {{ form_errors(form) }}
{{ form_label(form.goods) }} {{ form_errors(form.goods) }}
{% for field in form.goods %} {{ form_widget(field) }} {% endfor %}
{{ form_end(form) }} This is nice so far, but I also want to include new elements in my collection, in particular if today s good has not been inserted yet. I can do this inside my FormBuilder, by manually add a new item in the array before calling the $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 ) ); But I also want that if a user removes an inserted value (i.e., inserts nothing in the form), the associated array element should be removed. Allowing the user to remove elements is a little bit trickyer. I cannot rely on allow_delete property, since by working only with the last item in the collection, all the previous ones would be removed when the form is submitted. I cannot rely on LifecycleCallbacks neither, because the changes made to relationships are not persisted in the database. Thankfully to open source, I found a post here that helped me. What I needed was an EventListener on Doctrine Flush operations. namespace MyBundleEventListener; use DoctrineORMEventOnFlushEventArgs; use MyBundleEntityUserGoods; 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( MyBundleEntityUserGoods ); $uow->computeChangeSet($md, $entity); $em->persist($curruser); $md = $em->getClassMetadata( MyBundleEntityUser ); $uow->computeChangeSet($md, $curruser); } } } } } } } and registered it in my config.yml as mybundle.emptyvalues_listener: class: MyBundleEventListenerEmptyValueListener tags: - { name: doctrine.event_listener, event: onFlush }




相关问题
Brute-force/DoS prevention in PHP [closed]

I am trying to write a script to prevent brute-force login attempts in a website I m building. The logic goes something like this: User sends login information. Check if username and password is ...

please can anyone check this while loop and if condition

<?php $con=mysql_connect("localhost","mts","mts"); if(!con) { die( unable to connect . mysql_error()); } mysql_select_db("mts",$con); /* date_default_timezone_set ("Asia/Calcutta"); $date = ...

定值美元

如何确认来自正确来源的数字。

Generating a drop down list of timezones with PHP

Most sites need some way to show the dates on the site in the users preferred timezone. Below are two lists that I found and then one method using the built in PHP DateTime class in PHP 5. I need ...

Text as watermarking in PHP

I want to create text as a watermark for an image. the water mark should have the following properties front: Impact color: white opacity: 31% Font style: regular, bold Bevel and Emboss size: 30 ...

How does php cast boolean variables?

How does php cast boolean variables? I was trying to save a boolean value to an array: $result["Users"]["is_login"] = true; but when I use debug the is_login value is blank. and when I do ...

热门标签