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

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.

Related

"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

Symfony2 form - Many to Many as text causing errors

I have tried looking around for a possible solution to this but with no luck.
What I have is a Many to many relationship between properties and postcodes, I can't display the postcodes in a select for example due to the amount of possible entries.
My solution was to have it as a text field in the form and then catch it on PrePersist to search for the matching record and then apply this to the entity before persisting to the db.
The problem is when the form is validating it is still trying to pass the string value to the setter which is expecting an entity object.
Is there anyway to prevent this from causing an error?
I have attached my form code for you.
Thanks,
Harry
$propertyData = new PropertyData();
$builder
->add('reference')
->add('listing_type', 'choice', array('choices' => $propertyData->getListingTypes()))
->add('listing_status', 'choice', array('choices' => $propertyData->getStatusList()))
->add('title')
->add('house_number')
->add('address_line_1')
->add('address_line_2')
->add('town', 'text', array('data_class'=> 'Minos\Bundle\PropertyBundle\Entity\UtilTown'))
->add('county')
->add('country')
->add('council')
->add('region')
->add('postcode', 'text', array('data_class'=> 'Minos\Bundle\PropertyBundle\Entity\UtilPostcode'))
->add('short_description')
->add('long_description')
->add('size_sq_ft')
->add('floor_level')
->add('property_age')
->add('tenure_type', 'choice', array('choices' => $propertyData->getTenureTypes()))
->add('garage')
->add('num_living_rooms')
->add('num_bathrooms')
->add('num_bedrooms')
->add('num_floors')
->add('num_receptions')
->add('property_type')
//->add('prices')
;
You need a data transformer to convert your string input to an entity before processing the form.
$builder
// ...
->add('postcode', 'text', array(
'data_class'=> 'Minos\Bundle\PropertyBundle\Entity\UtilPostcode'
))
// ...
;
$builder->get('postcode')->addModelTransformer(new CallbackTransformer(
//Render an entity to a string to display in the text input
function($originalInput){
$string = $originalInput->getPostcode();
return $string;
},
//Take the form submitted value and convert it before processing.
//$submittedValue will be the string because you defined
// it in the builder that way
function($submittedValue){
//Do whatever to fetch the postcodes entity:
$postcodeEntity = $entityManager->find('AppBundle\postcodes', $submittedValue);
return $postcodeEntity;
}
));
This is just an example (I haven't tested it), you will need to change some stuff to match how your entities look.

Symfony unmapped entity form field not having data

In Symfony 2.6, I am using an entity form type that is unmapped:
$form
->add(
'myEntity', // Form field name
'entity',
[
'mapped' => false, // Not mapped
'class' => 'MyVendor\MyBundle\Entity\MyEntity',
'choices' => $MyEntityCollection, // list of MyEntity
'property' => 'name',
'empty_value' => 'Please select MyEntity',
'empty_data' => null,
'attr' => [
'label' => 'My label'
]
]
);
This allows user to properly select an item of MyEntity or leave it blank. According to that, I am adding a EventSubscriber to modify the preSubmitted data if any value is selected, and leave it as it is if no choice has been made.
Here is the eventSubscriber:
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SUBMIT => 'preSubmitData'
];
}
/**
* #param FormEvent $event
*/
public function preSubmitData(FormEvent $event)
{
if( null === ($entity = $event->getForm()->get( 'myEntity' )->getData() ) ){
return;
}
// Set value if field has been defined
$event
->getForm()
->setData( $entity )
;
}
If user selects a choice other than blank, when I debug the preSubmitData function:
$event->getForm()->get('entity')->getData() gives null
$event->getData() gives an array having as 'entity' key the selected entity ID (just the scalar value)
My questions are:
Shouldn't $event->getForm()->get('entity')->getData() have the selected entity?
Why is $event->getForm()->get('entity')->getData() giving null if $event->getData() has at least the entity ID in it?
Is there any way to get the entity here (as it happens with the mapped entities) without having to call the entity manager and querying the entity via its ID?
Thanks in advance!
Edit
For the big picture, in my global form (other fields not described here) I have 2 depending fields:
A select A (not described here) with some options from a tree. This option does exist in the global form entity as a property.
A second B select named myEntity (described here). It doesn't exist as the global form entity as a property, thus the mapped = false. If any choice is made here, then the first select (A)'s option is overridden by this one. Else the first choice remains as the entity property value.
Hope is clearer now.
Ok, it was giving null because we are on the preSubmit event, and here data sent is not yet mapped within an entity.
Changing the event to submit gives the mapped entity as needed.

Symfony2 form is not valid but entity gets saved anyway?

I couldn't find a solution for my current problem in the web.
I got a formular and an entity.
If the formular is not valid ($form->isValid() returns false), the entity is saved with the invalid data anyway. I see the form errors and that's correct. But my entity should not be updated if the form is not valid. What is going wrong here?
My entity got a hand full fields and a many-to-many relation with extra fields too.
I can show you some code parts:
$entry = $em->getRepository('MyAppBundle:Entry')->find($id);
// ...
$form = $this->createForm(new EntryType($this->getUser(), $entry), $entry);
$request = $this->getRequest();
if ($request->getMethod() == 'POST') {
$form->bind($request);
// ...
// i set custom errors here by myself!
if ($entry->getDeadlineEnable() && NULL === $entry->getDeadlineAt()) {
$form->get('deadline_at')->addError(new FormError('Please enter a date.'));
}
// ...
if ($form->isValid()) {
$em->merge($entry);
$em->flush();
// ...
return $this->redirect($this->generateUrl('MyAppBundle_homepage'));
As you can see, I check the form with isValid, which works correct. It returns false if there is a form error, e.g.
$form->get('deadline_at')->addError(new FormError('Please enter a date.'));
But the entitiy gets updated exactly here in this line anyway:
if ($form->isValid()) {
Why is this?
This is a really bad problem here. I don't know why this happens.
Thanks for any advice.
EDIT:
More information:
I use validators too for all fields wich has simple conditions.
I do this in the EntryType.php # public function buildForm(FormBuilderInterface $builder, array $options), e.g.:
$builder->add('min_commitments', null, array(
'label' => 'Zusagen mindestens',
'attr' => array(
'class' => 'form-control',
'placeholder' => 'Ab wievielen findet\'s statt?',
'min' => 0,
'max' => 100,
),
'required' => false,
'invalid_message' => 'Das ist keine gütige Angabe.',
'constraints' => array(
new Range(array(
'minMessage' => 'Mindestens 1.',
'maxMessage' => 'Maximal 100.',
'min' => 1,
'max' => 100,
)),
),
));
This is a number field to type in a number between 1 and 100.
But for some fileds i use my own validation direclty in the controller action method because the fields (most of them are a combination of a checkbox field and an text field belonging to to this checkbox) because many fields are interdependent.
I still don't know whats wrong with isValid and why the entity behind this form gets saved even the form is not completely valid. isValid returns false there if an error exists (correct).
Could this maybe be caused by the many to many relation ship?
I implementet such a relationship to allow the user to select many checkboxes, where each checkbox represents an user. The relation rows are saved into the relation table EntryUser.
Here is the many to many field in the form definition (Entry.php):
/**
* #ORM\OneToMany(targetEntity="EntryUser", mappedBy="entry", cascade={"all"}, orphanRemoval=true)
*/
protected $entry_users; // One more thing: this is a one to many but it is a many to many relationship at all because i created a relationship table with extra fields so it bacame a own entity. on the other side, the file User.php got the oppsite part of the many-to-many relationship with:
User.php:
/**
* #ORM\OneToMany(targetEntity="EntryUser" , mappedBy="user" , cascade={"all"} , orphanRemoval=true)
*/
protected $entry_users;
The form->bind is what is actually updating the entity with posted
data regardless of the data's validity. You must have another flush
getting kicked off somewhere. Perhaps a listener? Normal workflow for
$form->isValid() === false is to redisplay the form with the invalid
data and error messages. Once again, it would appear that you have
something calling $em->flush.-
Credit to #Cerad
Have you ever tried validators? http://symfony.com/doc/current/book/validation.html
If your attached code is from your controller - the problem is that your condition is wrong.
if ($entry->getDeadlineEnable() && NULL === $entry->getDeadlineAt()) {
$form->get('deadline_at')->addError(new FormError('Please enter a date.'));
}
Controller isn't the best place to valide. How are you going to reuse the validator for the form?

Symfony2: How to use constraints on custom compound form type?

Here is a question I've been breaking my head over for a while now.
Please know that I'm not a Symfony2 expert (yet), so I might have made a rookie mistake somewhere.
Field1: Standard Symfony2 text field type
Field2: Custom field type compoundfield with text field + checkbox field)
My Goal: Getting constraints added to the autoValue field to work on the autoValue's text input child
The reason why the constraints don't work is probably because NotBlank is expecting a string value and the internal data of this form field is an array array('input'=>'value', 'checkbox' => true). This array value gets transformed back into a string with a custom DataTransformer. I suspect however that that happens AFTER validating the field against known constraints.
As you see below in commented code, I have been able to get constraints working on the text input, however only when hardcoded into the autoValue's form type, and I want to validate against the main field's constraints.
My (simplified) sample code for controller and field:
.
Controller code
Setting up a quick form for testing purposes.
<?php
//...
// $entityInstance holds an entity that has it's own constraints
// that have been added via annotations
$formBuilder = $this->createFormBuilder( $entityInstance, array(
'attr' => array(
// added to disable html5 validation
'novalidate' => 'novalidate'
)
));
$formBuilder->add('regular_text', 'text', array(
'constraints' => array(
new \Symfony\Component\Validator\Constraints\NotBlank()
)
));
$formBuilder->add('auto_text', 'textWithAutoValue', array(
'constraints' => array(
new \Symfony\Component\Validator\Constraints\NotBlank()
)
));
.
TextWithAutoValue source files
src/My/Component/Form/Type/TextWithAutoValueType.php
<?php
namespace My\Component\Form\Type;
use My\Component\Form\DataTransformer\TextWithAutoValueTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class TextWithAutoValueType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('value', 'text', array(
// when I uncomment this, the NotBlank constraint works. I just
// want to validate against whatever constraints are added to the
// main form field 'auto_text' instead of hardcoding them here
// 'constraints' => array(
// new \Symfony\Component\Validator\Constraints\NotBlank()
// )
));
$builder->add('checkbox', 'checkbox', array(
));
$builder->addModelTransformer(
new TextWithAutoValueTransformer()
);
}
public function getName()
{
return 'textWithAutoValue';
}
}
src/My/Component/Form/DataTransformer/TextWithAutoValueType.php
<?php
namespace My\Component\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
class TextWithAutoValueTransformer
implements DataTransformerInterface
{
/**
* #inheritdoc
*/
public function transform($value)
{
return array(
'value' => (string) $value,
'checkbox' => true
);
}
/**
* #inheritdoc
*/
public function reverseTransform($value)
{
return $value['value'];
}
}
src/My/ComponentBundle/Resources/config/services.yml
parameters:
services:
my_component.form.type.textWithAutoValue:
class: My\Component\Form\Type\TextWithAutoValueType
tags:
- { name: form.type, alias: textWithAutoValue }
src/My/ComponentBundle/Resources/views/Form/fields.html.twig
{% block textWithAutoValue_widget %}
{% spaceless %}
{{ form_widget(form.value) }}
{{ form_widget(form.checkbox) }}
<label for="{{ form.checkbox.vars.id}}">use default value</label>
{% endspaceless %}
{% endblock %}
.
Question
I have been reading docs and google for quite some hours now and can't figure out how to copy, bind, or reference the original constraints that have been added while building this form.
-> Does anyone know how to accomplish this?
-> For bonus points; how to enable the constraints that have been added to the main form's bound entity? (via annotations on the entity class)
PS
Sorry it became such a long question, I hope that I succeeded in making my issue clear. If not, please ask me for more details!
I suggest you read again the documentation about validation first.
What we can make out of this is that validation primarily occurs on classes rather than form types. That is what you overlooked. What you need to do is:
To create a data class for your TextWithAutoValueType, called src/My/Bundle/Form/Model/TextWithAutoValue for instance. It must contain properties called text and checkbox and their setters/getters;
To associate this data class to your form type. For this, you must create a TextWithAutoValueType::getDefaultOptions() method and populate the data_class option. Go here for more info about this method;
Create validation for your data class. You can either use annotations or a Resources/config/validation.yml file for this. Instead of associating your constraints to the fields of your form, you must associate them to the properties of your class:
validation.yml:
src/My/Bundle/Form/Model/TextWithAutoValue:
properties:
text:
- Type:
type: string
- NotBlank: ~
checkbox:
- Type:
type: boolean
Edit:
I assume that you already know how to use a form type in another. When defining your validation configuration, you can use a very useful something, called validation groups. Here a basic example (in a validation.yml file, since I'm not much proficient with validation annotations):
src/My/Bundle/Form/Model/TextWithAutoValue:
properties:
text:
- Type:
type: string
groups: [ Default, Create, Edit ]
- NotBlank:
groups: [ Edit ]
checkbox:
- Type:
type: boolean
There is a groups parameter that can be added to every constraint. It is an array containing validation group names. When requesting a validation on an object, you can specify with which set of groups you want to validate. The system will then look in the validation file what constraints should be applied.
By default, the "Default" group is set on all constraints. This also is the group that is used when performing a regular validation.
You can specify the default groups of a specific form type in MyFormType::getDefaultOptions(), by setting the validation_groups parameter (an array of strings - names of validation groups),
When appending a form type to another, in MyFormType::buildForm(), you can use specific validation groups.
This, of course, is the standard behaviour for all form type options. An example:
$formBuilder->add('auto_text', 'textWithAutoValue', array(
'label' => 'my_label',
'validation_groups' => array('Default', 'Edit'),
));
As for the use of different entities, you can pile up your data classes following the same architecture than your piled-up forms. In the example above, a form type using textWithAutoValueType will have to have a data_class that has a 'auto_text' property and the corresponding getter/setter.
In the validation file, the Valid constraints will be able to cascade validation. A property with Valid will detect the class of the property and will try to find a corresponding validation configuration for this class, and apply it with the same validation groups:
src/My/Bundle/Form/Model/ContainerDataClass:
properties:
auto_text:
Valid: ~ # Will call the validation conf just below with the same groups
src/My/Bundle/Form/Model/TextWithAutoValue:
properties:
... etc
As described here https://speakerdeck.com/bschussek/3-steps-to-symfony2-form-mastery#39 (slide 39) by Bernhard Schussek (the main contributor of symofny form extension), a transformer should never change the information, but only change its representation.
Adding the information (checkbox' => true), you are doing something wrong.
In
Edit:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('value', 'text', $options);
$builder->add('checkbox', 'checkbox', array('mapped'=>false));
$builder->addModelTransformer(
new TextWithAutoValueTransformer()
);
}

Categories