Symfony: How to use Data Transformers with a FormFactory? - php

In my Symfony Application i have a From which contains a lot of checkboxes, radios, textfields and also DateTime Objects.
Everything but the DateTime Objects work fine, but with them i always get the error "Object of class DateTime could not be converted to string"
So i wanted to use this tutorial to change the datetime object to a string.
The thing is i am using a FormFactory and when i use the addModelTransformer()-Method like this...
1st Option:
$form->add($formFactory->createNamed('value', 'date', null, $fieldOptions));
$form->get('value')->addModelTransformer(new CallbackTransformer(
function($dateAsDate){
return $dateAsDate->format('Y-m-d');
},
function ($dateAsString){
return "test"; //TODO: to be changed to make string to date
}
));
...i get the Error "Call to undefined method Symfony\Component\Form\Form::addModelTransformer() "
When i use the builder function outside the Formfactory like this...
2nd Option:
//$builder->addEventListener Stuff is above
$builder->get('value')->addModelTransformer(new CallbackTransformer(
function($dateAsDate){ return $dateAsDate->format('Y-m-d'); },
function ($dateAsString){ return 'null'; } //TODO: to be changed to make string to date
));
...i get the Error "The child with the name "value" does not exist."
Does anybody have an idea how to make this work?

There is no need to use any custom Tranformer here.
The DateType field handles DateTime objects correctly by itself.
You may want to specify some options like 'format' and 'widget' depending on how you want to render your field, but the default 'input' should work fine with DateTime.
For example :
$builder->add('value', DateType::class, [
'format' => 'y-MM-dd',
'widget' => 'single_text', // To have a single input text box
'input' => 'datetime' // This is the default value
];

Related

How do I catch form data in my controller using a class object and not the traditional getData() Method in symfony

I have created a search form in which I use to search my database to produce some result.
using the below in a my forms directory as a form class I generate the form.
$builder->add(
'startDate',
DateType::class,
[
'label' => 'start date',
'format' => 'yyyy-MM-dd',
'required' => true,
'constraints' => [
new Constraints\NotBlank(),
new Constraints\DateTime(),
],
]
);
in my controller I already have retrieved the data using the getData() method
$form = $this->createForm(testForm::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$start = date_format($form->get('start')->getData(), 'Y-m-d');
At the moment I want to get this data using a class object
e.g like this
$form = $this->createForm(testForm::class, $classobject);
$form->handleRequest($request);
where I would use the class object to retrieve the posted data from the form class "testForm"
how have I tried to solve this on my own?
I tried reading tutorials on this concept e.g as below
https://blog.martinhujer.cz/symfony-forms-with-request-objects/
note : this is a learning curve for me, I do not really grasp this concept
Please, constructive responses would be well appreciated.
Thanks
$form->getData() will return the object populated with the submited data. By default the Form Component returns an array, but if you pass an object as the second arg to createForm or set data_class option then you will get the object back.

"Unable to reverse value for property path" for Symfony2 form ChoiceType field on Select2

Long story short, in Symfony 2.8 I've got Movie entity with actors field, which is ArrayCollection of entity Actor (ManyToMany) and I wanted the field to be ajax-loaded Select2.
When I don't use Ajax, the form is:
->add('actors', EntityType::class, array(
'class' => Actor::class,
'label' => "Actors of the work",
'multiple' => true,
'attr' => array(
'class' => "select2-select",
),
))
And it works.
I tried to put there an empty Select field:
->add('actors', ChoiceType::class, array(
'mapped' => false,
'multiple' => true,
'attr'=>array(
'class' => "select2-ajax",
'data-entity'=>"actor"
)
))
The Select2 Ajax works, everything in DOM looks the same as in previous example, but on form submit I get errors in the profiler: This value is not valid.:
Symfony\Component\Validator\ConstraintViolation
Object(Symfony\Component\Form\Form).children[actors] = [0 => 20, 1 => 21]
Caused by:
Symfony\Component\Form\Exception\TransformationFailedException
Unable to reverse value for property path "actors": Could not find all matching choices for the given values
Caused by:
Symfony\Component\Form\Exception\TransformationFailedException
Could not find all matching choices for the given values
The funny part is the data received is the same as they were when it was an EntityType: [0 => 20, 1 => 21]
I marked field as not mapped, I even changed field name to other than Movie entity's field name. I tried adding empty choices, I tried to leave it as EntityType but with custom query_builder, returning empty collection. Now I'm out of ideas.
How should I do it?
EDIT after Raymond's answer:
I added DataTransformer:
use Doctrine\Common\Persistence\ObjectManager;
use CompanyName\Common\CommonBundle\Entity\Actor;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class ActorToNumberTransformer implements DataTransformerInterface
{
private $manager;
public function __construct(ObjectManager $objectManager)
{
$this->manager = $objectManager;
}
public function transform($actors)
{
if(null === $actors)
return array();
$actorIds = array();
foreach($actors as $actor)
$actorIds[] = $actor->getId();
return $actorIds;
}
public function reverseTransform($actorIds)
{
if($actorIds === null)
return array();
foreach($actorIds as $actorId)
{
$actor = $this->manager->getRepository('CommonBundle:Actor')->find($actorId);
if(null === $actor)
throw new TransformationFailedException(sprintf('An actor with id "%s" does not exist!', $actorId));
$actors[] = $actor;
}
return $actors;
}
}
Added it at the end of the MovieType buildForm():
$builder->get('actors')
->addModelTransformer(new ActorToNumberTransformer($this->manager));
$builder->get('actors')
->addViewTransformer(new ActorToNumberTransformer($this->manager));
And added service:
common.form.type.work:
class: CompanyName\Common\CommonBundle\Form\Type\MovieType
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: form.type }
Nothing changed. On form submit, reverseTransform() gets the proper data, but profiler shows the same error. That's a big mistery for me now...
You'll need to add a DTO (Data Transformer ) to transform the value received from your form and return the appropriate object .
Since you're calling the value from Ajax it doesn't recognized it anymore as a an object but a text value.
Examples :
Symfony2 -Use of DTO
Form with jQuery autocomplete
The correct way isn't Data Transformer but Form Events, look here:
http://symfony.com/doc/current/form/dynamic_form_modification.html#form-events-submitted-data
In the example you have the field sport (an entity, like your Movie) and the field position (another entity, like actors).
The trick is to use ajax in order to reload entirely the form and use
PRE_SET_DATA and POST_SUBMIT.
I'm using Symfony 3.x but I think it's the same with 2.8.x
When you add data transformers and nothing seems to change, it sounds like the data never goes through your data transformers. The transformation probably fails before your new data transformers are called. Try to add a few lines to your code:
$builder->get('actors')->resetViewTransformers();
$builder->get('actors')->resetModelTransformers();
// and then add your own

DateTime not save properly in cakephp 3.x with bootstrap datetimepicker

i am using cakephp 3.x, i have one form in which one field is of date. in backend i am using mysql.
my field structure in mysql is dob of type date.
now in cakephp 3.x i had use below syntax to create input.
echo $this->Form->input('dob', array(
'label' => (__('Date of Birth')),
'type' => 'text',
'required' => false,
'class' => 'form-control date'
));
and i had used bootstrap datetimepicker like,
$('.date').datetimepicker({
format: 'YYYY-MM-DD'
});
now when i submit the form at and i print_r the request data at that time i got this field like this
[
....
'dob' => '2016-02-11',
....
]
but when i save the record and look in database then it show me random date like 2036-10-25
can anyone help me please?
and this is the Final General Solution,
//File : src/Model/Table/PatientsTable.php
namespace App\Model\Table;
use Cake\ORM\Table;
use Cake\Event\Event;
use ArrayObject;
use Cake\I18n\Time;
class PatientsTable extends Table
{
...
...
public function beforeMarshal(Event $event, ArrayObject $data, ArrayObject $options)
{
if (isset($data['dob'])) {
$data['dob'] = Time::parseDate($data['dob'], 'Y-M-d');
}
}
}
You declared dob type as date but tried to save date in string format instead of date time format. Try this
use Cake\I18n\Time;
$this->request->data['dob']= Time::parseDate($this->request->data['dob'],'Y-M-d');

How can i add the current time in datetime format to a textfield in yii?

Here's the code i want to work with
<?php echo $form->textField($model,'email_time_created',array('readonly'=>true)); ?>
i want to automatically add the current time to my database
I recommend it to add it at the database, asuming you're using MySQL you can create a trigger before saving and doing something like this:
SET NEW.email_time_created=NOW()
If not, you can do it at Yii/PHP level by adding the following function at the model class:
public function beforeSave(){
$this->email_time_created = CDbExpression('NOW()'); //Only in MYSQL
return parent::beforeSave();
}
It will set the column to the current value before saving the model. Notice that it won't be shown at the form, but you can add it by JS or using php's date() at the form's view.
also you can set the value in controller.
$model->email_time_created= now('Y-m-d H:i:s');
The simplest way is to add CTimestampBehavior to your model:
public function behaviors(){
return array(
'CTimestampBehavior' => array(
'class' => 'zii.behaviors.CTimestampBehavior',
'createAttribute' => 'create_time_attribute',
'updateAttribute' => 'update_time_attribute',
)
);
}
See API.

How-to: Optimize Symfony's forms' performance?

I have a form that is the bottleneck of my ajax-request.
$order = $this->getDoctrine()
->getRepository('AcmeMyBundle:Order')
->find($id);
$order = $order ? $order : new Order();
$form = $this->createForm(new OrderType(), $order);
$formView = $form->createView();
return $this->render(
'AcmeMyBundle:Ajax:order_edit.html.twig',
array(
'form' => $formView,
)
);
For more cleaner code I deleted stopwatch statements.
My OrderType has next fields:
$builder
->add('status') // enum (string)
->add('paid_status') // enum (string)
->add('purchases_price') // int
->add('discount_price') // int
->add('delivery_price') // int
->add('delivery_real_price', null, array('required' => false)) // int
->add('buyer_name') // string
->add('buyer_phone') // string
->add('buyer_email') // string
->add('buyer_address') // string
->add('comment') // string
->add('manager_comment') // string
->add('delivery_type') // enum (string)
->add('delivery_track_id') // string
->add('payment_method') // enum (string)
->add('payment_id') // string
->add('reward') // int
->add('reward_status') // enum (string)
->add('container') // string
->add('partner') // Entity: User
->add('website', 'website') // Entity: Website
->add('products', 'collection', array( // Entity: Purchase
'type' => 'purchase',
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'property_path' => 'purchases',
'error_bubbling' => false,
));
Purchase type:
$builder
->add('amount')
->add('price')
->add('code', 'variant', array(
'property_path' => 'variantEntity',
'data_class' => '\Acme\MyBundle\Entity\Simpla\Variant'
))
;
Also Purchase type has a listener that is not significant here. It is represented in Symfony profiler below as variant_retrieve, purchase_form_creating. You can see that it takes about 200ms.
Here I put the result of profilers:
As you can see: $this->createForm(...) takes 1011ms, $form->createView(); takes 2876ms and form rendering in twig is also very slow: 4335ms. As stated by blackfire profiler all the deal in ObjectHydrator::gatherRowData() and UnitOfWork::createEntity().
Method createEntity() called 2223 times because there is some field that mapped with Variant entity and has form type Entity. But as you can see from above code there is no entity types for variant. My VariantType is simple extended text form type that has modelTransformer. To not mess up everything you can see code for similar Type class at docs.
I found with XDebug that buildView for VariantType has been called in Purchase's buildView with text form type. But after that from somewhere buildView for VariantType was called again and in this case it has entity form type. How can it be possible? I tried to define empty array in choices and preferred_choices on every my form type but it didn't change anything. What I need to do to prevent EntityChoiceList to be loaded for my form?
The described behavior looks as the work of the guesser. I have the feeling that there is need to show an some additional code (listeners, VariantType, WebsiteType, PartnerType).
Let's assume a some class has association variant to Variant and FormType for this class has code ->add('variant') without explicit specifying type (as I see there is a lot of places where the type is not specified). Then DoctrineOrmTypeGuesser comes in the game.
https://github.com/symfony/symfony/blob/2.7/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php#L46
This code assign the entity type (!) to this child. The EntityRepository::findAll() is called and all variants from DB are hydrated.
As for another form optimization ways:
Try to specify type in all possible cases to prevent a type guessing;
Use SELECT with JOINs to get an order as new sub-requests to DB are sent to set an underlying data for an every form maps relation;
Preserve keys for collection elements on a submission as a removing of a single element without a keys preserving will trigger unnecessary updates.
I also had the same problem with the entity type, I needed to list cities, there were like mire then 4000, what I did basically is to inject the choices into the form. In your controller you ask the Variants from the database, in a repository call, hydrate them as array, and you select only the id and the name, or title, and then you pass into the form, as options value. With this the database part will be much quicker.

Categories