Symfony Forms, sub/embedded forms - php

I would like to update multiple entities and with the update I would like to add a field called "comment". This field is not related to the entity; I will save the comment in a separate table.
This is what I've tried (Controller code below) - this is just a sample of the code, but should give an idea of what I am trying to do:
$form = $this->createFormBuilder();
foreach (array(1,2,3) as $id) {
$subform = $this->createFormBuilder()
->add('story', 'entity', array(
'class' => 'AcmeDemoBundle:Story',
'query_builder' => function($em) use ($id) {
$qb = $em->createQueryBuilder('s')
$qb->select(array('j'))
->add('from', 'AcmeDemoBundle:Story')
->addWhere('j.id = :id')
->setParameter('id', $id);
return $qb;
},
'property' => 'id',
'required' => true, 'expanded' => true, 'multiple' => true
)
->add('comment', 'textarea');
}
$form->add($subform, '', array('label' => '');
...
// then I send the form to the template with $form->createView()
What I expected to see was a form with each entity (1,2,3) as a checkbox and next to that a comment block. Instead what happens is I only get one checkbox and one comment and always for the last entity in the array (in the above case, entity number 3). It seems that the form builder ignores all the subforms that I add and only takes the last one - this is also the case when looking at the SQL that goes to the DB, there is only a select for # 3 and no selects for 1 and 2.
Given that the html produced supports what I am trying to do, I expect the above to work:
<input type="checkbox" id="form_form_story_0" name="form[form][story][]" value="3">
<textarea id="form_form_comment" name="form[form][comment] />
I've also tried giving the fields unique names - for example ->add('story_' . $id) etc, but that didn't do anything. I also tried giving each querybuilder a unique name $em->createQueryBuilder('s'.$id) but that didn't work either.
Also, it doesn't work if I remove the query_builder (and just use the class)
Update: it doesn't seem to have anything to do with the entity, even if I try and create subforms with plain text fields it doesn't work...
Update 2 if the sub form has a different name using:
$subForm = $this->get('form.factory')->createNamedBuilder("form$id"), 'form', array())
then it works. Unfortunately this means that I can't loop through the subforms inside twig.

It can't work as stated/asked above.
->add
overwrites a previous copy of that child, as seen here:
Symfony\Component\Form
...
public function add(FormInterface $child)
...
$this->children[$child->getName()] = $child;
To "solve" the problem I discarded the idea of sub forms and named each item separately like this:
$form
...
->add("comment_$id", 'textarea')
made an array of the entities and in twig render the comment field for that entity like this:
{% for entity in entities %}
...
{% set child = "comment_" ~ entity.id %}{{ form_widget(form.children[child]) }}
...

Your example code contains error: you adding $subform after cycle, so you explicitly adding only last generated $subform object.
Noted line $this->children[$child->getName()] = $child; prevents from "simple" way to add subform builders, but there is a workaround:
Instead of calling $this->createFormBuilder() helper for subform (this will give it name "form"), you can create biulder with $this->container->get('form.factory')->createNamedBuilder('sub_form_1') with explicit name.

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

Entity form field with inactive records

I have a form definition like this (for demonstration purposes):
$builder->add('field', 'entity', [
'class' => EntityA::class,
'query_builder' => function($repo) {
return $repo->createQueryBuilder('e')
->andWhere('e.active = 1');
}
]);
This ensures that only active records can be selected in the dropdown field when using this form.
This leads to this case: When I edit an entity using the form definition from above, and this entity as an inactive EntityA assigned, it won't appear in the dropdown field. When I hit the save button, it will get the first active (if any) EntityA assigned. Also the form will suggest to the user that a different Entity is assigned than it actually is.
The correct way would be that the form displays all active records and the one inactive one that is currently assigned.
I looked into Form event listeners but this seems overly complicated. Also, extending the form just for editing could be a thing but it seems not "the right way" to me.
How can I solve this issue, preferably without using 3rd party bundles?
Get the object and load different data into dropdown depends of type of action: edit/create:
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
$form ->add('field', 'entity', [
'class' => EntityA::class,
'query_builder' => function($repo) use ($data) {
if ($data->getId()) {
// Edit mode: append the pre-selected record to dropdown
return $repo->createQueryBuilder('e')
->andWhere('e.active = 1')
->orWhere('e.id = :id')
->setParameter('id', $data->getId());
} else {
// Display only active records
return $repo->createQueryBuilder('e')->andWhere('e.active = 1');
}
}
]);
});

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.

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

Symfony2 Saving data of related Entity after submiting form

I'm starting developing with Symfony2 and looks like I need help. I have Product entity related with SynchronizationSetting entity. I can edit product data by form maped with his entity. But I also need to modify some data related to product in SynchronizationSetting. To do that I've modified the form so it look like that (Vendor\ProductBundle\Form\ProductType.php):
...
->add('synchronization_setting', 'choice', array(
'choices' => array('daily' => 'Daily', 'weekly' => 'Weekly', 'never' => 'Never'))
After form is submitted selected checkbox values are passed to setSynchronizationSetting method in Product Entity. Then I do that (Vendor\ProductBundle\Entity\SynchronizationSetting.php):
public function setSynchronizationSetting($data)
{
$synchronizationSetting = new SynchronizationSetting();
$synchronizationSetting->setDaily(in_array('daily', $data) ? '1' : '0');
...
}
And now I need to somehow save those SynchronizationSetting entity into database. I read that calling entity manager from here is very bad practice so... how should I save this?
One possible way (I'm not sure if it's good practice)
public function setSynchronizationSetting($data)
{
$synchronizationSetting = new SynchronizationSetting();
$synchronizationSetting->setDaily(in_array('daily', $data) ? '1' : '0');
}
public function retSynchronizationSetting()
{
return $this->synchronizationSetting;
}
Then in your controller in place where you handle form data you call retSynchronizationSetting() and save entity using EntityManager.

Categories