Best way to handle large Form Collection in Symfony2 - forms

Edited my post to be similar to Symfony Cookbook and added some code.
http://symfony.com/doc/current/cookbook/form/form_collections.html
Note that the Entity/Form code posted in the part is the same as the one in the doc linked above.
I have a "Task" entity, which is linked to a "Tag" entity.To keep it simple, "Task" has a single field "description", and a "tag" has a single field "name".A "Tag" is linked to one "Task", and a "Task" is linked to many "Tags".
Entity:
class Task
{
protected $description;
protected $tags;
public function __construct()
{ $this->tags = new ArrayCollection(); }
public function getDescription()
{ return $this->description;}
public function setDescription($description)
{ $this->description = $description; }
public function getTags()
{ return $this->tags; }
public function setTags(ArrayCollection $tags)
{ $this->tags = $tags; }
}
class Tag
{
public $name;
}
At the moment, I use a Collection of "Tags" in the "Task" form to edit all them at once, as described in Symfony CookBook:
Form:
class TagType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\TaskBundle\Entity\Tag',
));
}
public function getName()
{
return 'tag';
}
}
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('description');
$builder->add('tags', 'collection', array('type' => new TagType()));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\TaskBundle\Entity\Task',
));
}
public function getName()
{
return 'task';
}
}
But a problem appears when I create a collection of "Tags" larger than a thousand elements. At this time, the form take seconds and seconds to load, and sometimes it crashes due to memory.
$task = new Task();
$tag1 = new Tag();
$tag1->name = 'tag1';
$task->getTags()->add($tag1);
$tag2 = new Tag();
$tag2->name = 'tag2';
$task->getTags()->add($tag2);
//Create a couple thousand more item here ...
//The script crashes here, when the form is being created
$form = $this->createForm(new TaskType(), $task);
The error does not come from Doctrine, which handles very well the whole thing, but from Symfony Form.
Is it a good idea to handle my form using Symfony2 built-in form system (with Collections) in this case, or should I handle it like in the old days, with raw html inputs and server-side validation/saves?

I'm wondering if your issue has nothing to do with the form part of this, but the hydration of the tag objects. If you're asking doctrine to hydrate a ton of objects, it's going to use a big chunk of memory. You may want to look into another method of hydrating the tags, perhaps HYDRATE_ARRAY instead of HYDRATE_OBJECT.

Related

How to generate a 'collection' form in Symfony, generated by each object within an entity?

Having read through the Symfony forms collection/entity types I'm trying to generate a form that only involves these two attributes of an entity (Person) but uses all instances of Person in the database. The purpose of the form is to provide a activation tickbox for every person on a single page so that multiple status' can be flushed to the database on form submission. Eg, the form should look something like:
☑ John Smith
☐ Jane Doe
☑ ...etc
☑ ...etc
My attempts below are not working as they just return an empty page, although I can see that $allpersons is populated
My Person entity:
class Person
{
/**
* #ORM\COLUMN(type="boolean")
*/
protected $active = true;
/**
* #ORM\COLUMN(type="string", length=100)
*/
protected $fullname ;
/* ...many other attributes... */
}
My Controller:
class DefaultController extends BaseController {
public function ​activePersonsAction(Request $request) {
$em = $this->getDoctrine()->getManager();
$persons = $em->getRepository('AppBundle:Person')->findAll();
$form = $this->createForm(AllPersonsType::class, $persons);
$form->handleRequest($request);
if ($form->isSubmitted() && ($form->isValid())) {
$em = $this->getDoctrine()->getManager();
$persons = $form->getData();
foreach ($persons as $person) {
$em->persist($person);
}
$em->flush();
return $this->redirectToRoute('home_user');
}
return $this->render('activatePersons.html.twig', array(
'page_title' => 'Active Persons',
'form' => $form->createView(),
));
}
}
My FormTypes:
class AllPersonsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('persons', CollectionType::class, array(
'entry_type' => ActivatePersonType::class
));
}
public function getName()
{
return 'person';
}
}
class ActivatePersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('active',CheckboxType::class)
->add('fullname', TextType::class);
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Person',
));
}
public function getName()
{
return 'person';
}
}
You pass $persons collection as form data to AllPersonsType, while the type expects that you pass an array with persons key and persons collection as value.
$form = $this->createForm(AllPersonsType::class, ['persons' => $persons]);

Possible for a child form to return a value to parent form and not an object?

I see that Symfony2 always ties form types to objects, but is it possible to reuse a form type to where it can only be used as a child?
This is just an example of what I'm trying to accomplish, but I think it could apply to many scenarios.
First lets say I have the following entity:
class Person
{
public function getState();
public function getHomeState();
}
These two fields are choice fields of states.
I want that when a user selects a choice getState() and getHomeState() should only return a string.
Something like:
<?php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class PersonFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('State', 'statelist'),//statelist is a form service.
->add('HomeState', 'statelist')//use form service here too
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Entity\Person'
));
}
public function getName()
{
return 'person';
}
}
The issue I'm having is being able to reuse the form below.
<?php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class StateListType extends AbstractType
{
protected $ChoiceList;
public function __construct(ChoiceListInterface $Choices)
{
$this->ChoiceList = $Choices;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
//Unsure what to do here to accomplish what I want.
$builder->add(?, 'choice', array('choice_list' => $this->ChoiceList);
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'statelist';
}
}
The problem is that I don't want the form above to be tied to any data class and just let me reuse the choices that would provide a string to the parent form.
So that I may easily do $Person->getState(), have the string and be done with it.
Right now if I were to use another data_class for the child form above lets say Address. Which contained a method of the same name getState(). The child form would return the Address object to the getState() method of Person and I'd have to access the data via $Person->getState()->getState() and $Person->getHomeState()->getState() respectively.
It just doesn't make sense (from a readability point of view) to do this and this applies to other similar use cases.
Is it possible to have a child form just return a string to it's parent form and not an object?
P.S.
I know I can just use the choice list alone but choice lists can't be made into a service. I have to 'wrap' the choice list class into a another form so that I can use the form as a service which allows greater flexibility.
yes you can redefine a basic "string" field as "choice" without adding inner fields but simply overriding setDefaultOptions, a nice explanation about how OptionResolverInterface works is here
<?php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class StateListType extends AbstractType
{
protected $ChoiceList;
public function __construct(ChoiceListInterface $Choices)
{
$this->ChoiceList = $Choices;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver
->setDefaults(
array(
'choices' => $this->ChoiceList,
'empty_value' => 'select a state'
)
);
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'statelist';
}
}
another instrument used to transform object to string(but usually is used to transform string to objects) is add a datatransformer to the field datatransformer
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer($this->createTransformer());
}
/**
* #return DataTransformerInterface
*/
protected function createTransformer()
{
return new FieldTransformer($this->getRepository(), static::FIELD_NAME);
}
but i think this is another story. The important thing to understand is that if you add fields under a formtype your field will return an object (or an array i don't remember) and not a string

Reusable form choices in symfony2 forms

I have the following code that's working... but I think it can be done better.
(Description below).
Class Address
{
protected province;
public function getProvince()...
}
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
$build->add(Province, new ProvinceType());
...
}
}
class ProvinceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$provinceList = array(... //very long list
...
$build->add(Province, 'choice', array(
'choices' => $provinceList;
));
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\Bundle\Entity\Address'
));
}
}
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$Province = array(... //very long list
...
$build->add('Address', new AddressType());
$build->add('FromProvince', new AddressType());
}
}
I have two problems with the code above:
Using this in twig PersonType form I have to do form_widget(person.Address.ProvinceType.ProvinceType) to use it. This just looks so wrong.
To validate the province I have to go one method deeper than I should have to.
In the end I just want to be able to validate fields that make sense such as:
Acme\Bundle\Entity\Person:
property:
provinceBorn:
- NotBlank: ~ //Wish to reuse province list here for straight-forward validation.
Address:
Valid: ~
Acme\Bundle\Entity\Address:
property:
province:
- NotBlank: ~ //As well as here.
To shorten the path to your ProvinceType, you should maybe define it as a base widget that would extend Symfony's choice type (see the doc on this). The best you'd get here would be something like {{ form_widget(person.address.province) }}.
To make choices reusable, it would be smart to extract your ProvinceType into a service (see Symfony's doc on how to do this) and pass the list of provinces as a parameter into the ProvinceType's __construct method (that would be defined in your bundle's services.yml). That way you would be able to extract your provinces into an external storage.
On validation, keep in mind that the YAML you've supplied here has mostly nothing to do with Form component, it's about your entity. So, duplicating NotBlank constraints actually makes sense, because you're not linking Person's provinceBorn property to an Address entity, you're saving a separate field.
Though, if you define a custom field type, you can make it required by default by moving the constraint into the Type you extracted to a service. Such constraint can be defined like this:
<?php
class ProvinceChoiceType extends AbstractType
{
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'constraints' => [
new NotBlank(['message' => 'Title is required']),
],
]);
}
...
I have done the something similar in this way (I'll use your example):
Class Address
{
protected province;
public function getProvince()...
}
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$provinceList = array(... //very long list
...
$build->add('province', 'choice', array(
'choices' => $provinceList, 'empty_value' => null, 'required' => true,
));
...
}
}
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$Province = array(... //very long list
...
$build->add('address', new AddressType());
$build->add('fromProvince', new AddressType());
}
}
And you send the data:
...
$form = $this->createForm(new PersonType(), $entity);
return array(
'form' => $editForm->createView(),
);
And use this in Twig as below:
{{ form_widget(form.address.province) }}
Finally, I think your validation are correct, but if you need something more specific, you could use the getters method as in the Symfony documentation is specified, in the validation section http://symfony.com/doc/current/book/validation.html#getters
Extended answer from before with explanations.
Inheriting form
http://symfony.com/doc/current/cookbook/form/inherit_data_option.html
Code
With your code it should look something like this
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
$build->add(Province, new ProvinceType(), [
'inherit_data' => true
// this is an alterrnative for having it bellow inside configure options
// use one or another, what suit you better
]);
...
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'inherit_data' => true
]);
}
// in sf > 2.7 use public function configureOptions(OptionsResolver $resolver)
}
class ProvinceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$provinceList = array(... //very long list
...
$build->add(Province, 'choice', array(
'choices' => $provinceList;
));
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\Bundle\Entity\Address',
'inherit_data' => true
));
}
}
class PersonType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$Province = array(... //very long list
...
$build->add('Address', new AddressType());
$build->add('FromProvince', new AddressType());
}
}
This you addressType inherits fields from provinceType, and PersonType inherits from both addressType (including its inherited fields from provinceType).
Template
So it should be possible to do this inside template
{{ form_row(form.Province)}}
Validation
The best way would be to so validation constrains on your relations with the Valid constrain.
This will force the validations on the children also
http://symfony.com/doc/current/reference/constraints/Valid.html
The other option is setting cascade_validation on the form, but this wont forward your validation groups if any.
Either way, you would then need define validation only on each entity once.

How render a form many times

im developing the create student requiriment for my application.
I have my Student entity, the entity contains two properties:
User (instance of User entity)
Course (instance of Course entity)
I build the form, but i wish render the form same times via a click button. On this way the administrator could be add any students without refresh the page.
Its is possible ? How manage the submit on the controller ?
Any ideas ? Thanks
NOTE: Im search a similiar Phpmyadmin behavior when add a new record.
What you should do is create a new object and form (e.g. StudentCollection) that allows for adding the student form using the collection type. This will allow you to manage the dynamically adding/removing student forms a lot better.
More on form collections here http://symfony.com/doc/current/cookbook/form/form_collections.html
e.g. Assuming you have a student form called StudentFormType, something like this should work. There's a good example on the link above that you should use if you want to know how to dynamically add/remove student forms as well as handle the submission.
// src/PathTo/YourBundle/Form/Type/StudentCollectionFormType.php
// Form object
class StudentCollectionFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('students', 'collection', array(
'type' => new StudentFormType(),
'allow_add' => true,
'allow_delete' => true,
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'PathTo\YourBundle\Model\StudentCollection',
));
}
// ...
}
// src/PathTo/YourBundle/Model/StudentCollection.php
namespace PathTo\YourBundle\Model;
// ...
class StudentCollection
{
// ...
protected $students;
public function __construct()
{
$this->students = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getStudents()
{
return $this->students;
}
public function addStudent(Student $student)
{
$this->students->add($student);
}
public function removeStudent(Student $student)
{
$this->students->removeElement($student);
}
}
Then in your controller
// src/PathTo/YourBundle/Controller/StudentController.php
public function editAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$collection = new StudentCollection();
// Prepopulate the collection with students
// ...
$form = $this->createForm(new StudentCollectionFormType(), $collection);
$form->handleRequest($request);
if ($form->isValid()) {
foreach ($collection->getStudents() as $student) {
$em->persist($student);
}
$em->flush();
// redirect back to some edit page
// ...
}
// render some form template
// ...
}

How to clear field value with Symfony2 forms

I'm writing my own CAPTCHA class and when the form doesn't validate, I don't want to pre-populate the captcha input with the previous answer, for obvious reasons. I just want to the clear the input before it's rendered.
I've discovered the data option is only for the default value, which is overwritten by what the user enters. I tried the following code:
$form->get('captcha')->setData(null);
.. After the request is bound with the form, but an AlreadyBoundException is thrown. I have actually managed to get it working with:
if (isset($formView->children['captcha'])) {
$formView->children['captcha']->vars['value'] = null;
}
But that just looks wrong, and definitely not up to Symfony standards. I've looked through the other options you can provide when building the form, but I can't see anything of note.
Does anyone have any idea?
By the way, I half expect Symfony2 comes packaged with a CAPTCHA solution, this is mainly a learning exercise while I get used to the framework.
I think you want to handle this form field like Symfony handles a password field: it won't get populated. Let's take a look at the PasswordType:
namespace Symfony\Component\Form\Extension\Core\Type;
class PasswordType extends AbstractType
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
if ($options['always_empty'] || !$form->isSubmitted()) {
$view->vars['value'] = '';
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'always_empty' => true,
'trim' => false,
));
}
//...
}
So, it's pretty simple: just add $view->vars['value'] = '' in the buildView method of your FormType (i.e. CaptchaType). That means the data of the field is not being cleared, but it won't be passed to the Twig template. Different approach, but the result is the same: the password field stays empty after validation failed.
If you are really lazy, you can use the PasswordType, but since the input of that field will be masked (*****), will that make an annoying captcha field even worse.
Your Form Type maybe look like this:
class CaptchaType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['value'] = '';
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return __NAMESPACE__.'\TextType';
}
/**
* {#inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'captcha';
}
}
Edit:
Just found that CaptchaBundle took the same approach.
There is a funny way to modify the Request before handling it. However I'd look into Stephan's answer as it seems more clean.
Something like so:
public function indexAction(Request $request)
{
$form = $this->createForm(Form::class);
$subData=$request->request->get('form');
$subData['task']=null;
$request->request->set('form',$subData);
$form->handleRequest($request);
if ($form->isValid()) {
//do stuff
}
return $this->render('default/index.html.twig', array(
'form' => $form->createView()
));
}
Get submitted data with the name 'form' as an array of values, change the said value to null, then set the request's value with the new one and have the form handle it.
And a simple form
class Form extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('task')
->add('save', SubmitType::class);
}
}
No matter what you type, the data will always be null after submitting the form. Of course, you need to verify the captcha before setting the value to null.
You can pass an incomplete entity to the action called when your control finds form invalid.
public function updateAction(Request $request, $id)
{
$entity = $this->EM()->getRepository('Bundle:Entity')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Entity entity.');
}
$form = $this->createForm(new RecommendationType()
,$entity
,array(
'attr' => array(
,'entity' => $entity
)
)
);
$form->bind($request);
if ($form->isValid()) {
$this->EM()->persist($entity);
$this->EM()->flush();
return $this->redirect($this->generateUrl('entity_show'
,array('id' => $id)));
} else {
$entity->setCapthca(Null);
}
return $this->render('Bundle:Entity:edit.html.twig'
,array(
'entity' => $entity
,'form' => $form->createView()
)
);
}
The create action would have similar modification.