Reusable form choices in symfony2 forms - 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.

Related

Symfony3: How to translate FormErrors added in eventListeners?

Let say I have this form:
//...
class BananaType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('flavor');
$builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
$form = $event->getForm();
if ($form->get('flavor')->getData() === null) {
$form->addError(new FormError('form.error.flavor_missing'));
}
}
}
}
Even though the message form.error.flavor_missing is defined both in messages.yml and validators.yml, it's not displayed.
Any idea how to translate it? I hope I won't have to inject the translator service in every form using this kind of code in order to solve this.
why not use Validation Constraints
use Symfony\Component\Validator\Constraints as Assert;
// ...
class BananaType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('flavor', YourType::class, array(
'constraints' => array(
new Assert\NotNull(), // or any other
),
));
}
}
You can use a custom message
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('flavor', YourType::class, array(
'constraints' => array(
new Assert\NotNull(array(
'message' => 'form.error.flavor_missing'
)),
),
));
}
If you open your dev environment, you should see the missing string. Taking a look at them, you should see the domain and the expected messages file.

ChoiceList as service possible?

I have the following code:
namespace Acme\Demo\Form;
use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
class MyChoiceList extends LazyChoiceList
{
protected function loadChoiceList()
{
return SimpleChoiceList(array('t'=>'test'));
}
}
services.yml:
SMyChoiceList:
class: Acme\Demo\Form\MyChoiceList;
Then when I try to do:
$builder
->add('mychoice', 'choice', array('choice_list' => 'SMyChoiceList'
));
I get:
The option "choice_list" with value "SMyChoiceList" is expected to be of type "null", "Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface"
LazyChoiceList already implements ChoiceListInterface... so I'm guessing Symfony2 choice_list parameter doesn't support services or am I missing something?
I assumed it worked similar to http://symfony.com/doc/current/cookbook/form/create_custom_field_type.html#form-cookbook-form-field-service
But I guess not.
With ..
'choice_list' => 'SMyChoiceList'
.. you are just using a string.
You can call it directly using ..
'choice_list' => new MyChoiceList()
Or if it has dependents you can inject it into your form constructor like..
your.form:
class: %your.form.class%
arguments:
- #SMyChoiceList
tags:
- { name: form.type, alias: your_form_alias }
.. and then use it in your form like..
protected $choiceList;
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('mychoice', 'choice', array(
'choice_list' => $this->choiceList,
))
;
}
UPDATE
To use your choice list in a custom form type you would do the following (using the same services set up as above)...
protected $choiceList;
public function __construct(ChoiceListInterface $choiceList)
{
$this->choiceList = $choiceList;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'choice_list' => $this->choiceList,
));
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'your_form_alias';
}
Which you could then use instead of choice, like...
$builder
->add('something', 'your_form_alias', array(
// Your choice options (expanded, label, attr, etc)
))
;
It's all in the page that you added before, more specifically http://symfony.com/doc/current/cookbook/form/create_custom_field_type.html#defining-the-field-type

How do I pass a value to Form Builder in Symfony2?

I'm using a Form Builder to create a form for use in an application. In the form, the user needs to be able to select a list of events that they are associated with. However, I'm unable to figure out how exactly I can pass the user's ID to the form builder?
My code is like this at the moment:
EvType.php
<?php
// src/Acme/MembersBundle/Form/Type/EvType.php
// This is to handle forms for the Members Form
namespace Acme\MembersBundle\Form;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class EvType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$centre = $this->centre;
$builder->add('id', 'integer', array('required'=>false));
$builder->add('list','entity', array('class'=>'Acme\InstructorBundle\Entity\MapLists', 'property'=>'name',
'query_builder' => function(EntityRepository $br) {
return $br->createQueryBuilder('ml')
->where('ml.user = :user')
->setParameter('user','1' );
}));
$builder->add('eventHorse', 'text', array('required'=>false));
}
public function getName()
{
return 'ev';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\InstructorBundle\Entity\MapListCentreMembers',
'csrf_protection' => false,
'csrf_field_name' => '_token',
// a unique key to help generate the secret token
'intention' => 'task_item',
));
}
}
->setParameter('user','1' ); is where I want to be able to pass the User's ID from the form. For now, I've statically assigned the user ID.
DefaultController.php
// User ID
$userid = $mem['userID'];
// Get Tests from Entity for Form use
$memberEV = $dm->getRepository('InstructorBundle:MapListMembers')->find($memberint);
// Generate Form to edit Tests & Achievements
$ev = $this->createForm( new EvType(), $memberEV);
you can simply pass a value in the __construct.
See below:
EvType.php
class EvType extends AbstractType
{
private $user;
public function __construct($user)
{
$this->user = $user;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$user = $this->user;
....
}
DefaultController.php
$ev = $this->createForm( new EvType($user), $memberEV);

Pass custom options to a symfony2 form

in symfony 1.4 it was possible to parameterize a form class definition, via the options of the form. Is there any way to pass custom options to my custom form type??? i've tried to use the options parameter of the buildForm method, but i'm not very sure what this array is, and apparently it is not for what i want... Thanks!
The solution is simple, if you want your custom option to be available also in Twig template, you must use
$builder->setAttribute() in buildForm method
and
$view->set() method in buildView() method, too.
<?php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType as FormAbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
// For Symfony 2.1 and higher:
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* ImagePreviewType
*
*/
class ImagePreviewType extends FormAbstractType
{
/**
* {#inheritDoc}
* For Symfony 2.0
*/
//public function getDefaultOptions(array $options)
//{
// $options = parent::getDefaultOptions($options);
// $options['base_path'] = 'path/to/default/dir/';
//
// return $options;
//}
/**
* {#inheritDoc}
* For Symfony 2.1 and higher
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'base_path' => '',
));
}
/**
* {#inheritDoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
// For Symfony 2.0:
// $view->set('base_path', $form->getAttribute('base_path'));
// For Symfony 2.1 and higher:
$view->vars['base_path'] = $options['base_path'];
}
/**
* {#inheritDoc}
*/
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->setAttribute('base_path', $options['base_path'])
;
}
/**
* {#inheritDoc}
*/
public function getName()
{
return 'image_preview';
}
public function getParent(array $options)
{
// for Symfony 2.0:
// return 'field';
// for Symfony 2.1 and higher:
return 'form';
}
}
Template for custom form type (file ...Acme/DemoBundle/Resources/views/Form/fields.html.twig):
{% block image_preview_widget %}
{% spaceless %}
<img src="{{ base_path ~ value }}" alt="" {{ block('widget_container_attributes') }} />
{% endspaceless %}
{% endblock %}
Register your template for custom form types in app/config/config.yml
twig:
debug: %kernel.debug%
strict_variables: %kernel.debug%
form:
resources:
- 'AcmeDemoAdminBundle:Form:fields.html.twig'
Usage: Display preview of user's image while editing his profile:
// src/Acme/DemoBundle/Form/Type/UserType.php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class UserType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('user_profile_image_file_name', new ImagePreviewType(), array(
'base_path' => 'some/other/dir',
));
}
}
2014-08-18: Updated for Symfony 2.1 or higher
UPDATE: Please note that this solution only works in Symfony 2.0.x, which is obsolete, use setDefaultOptions instead of getDefaultOptions.
Justly, Symfony 2 form types accept options that you can use for anything you want inside the form type. You need to override getDefaultOptions method to specify your type options.
For example, I have a type MyCustomType that accept my_option, this option has a default value of false, the implementation of MyCustomType can be something like this.
class MyCustomType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
if($options['my_option']){
//do something
} else {
//do another thing
}
...
}
public function getDefaultOptions(array $options)
{
return array(
'my_option' => false
);
}
public function getName()
{
return 'mycustomtype';
}
}
Later, you will need to specify the option when you create the form in the controller, using the third parameter of buildForm:
$form = $this->buildForm(new MyCustomType(), null, array(
'my_option' => true
));
If you not specify the my_option option, it takes the default value (false).
Using symfony 2.8, I succeeded by using the proposed solution extending the configureOptions() method.
class ElementType extends AbstractType
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'my_custom_option_parameter' => null,
));
}
}
I needed to use the ElementType, as a collection and embedded form. I recognized that it was not possible to pass the my_custom_option_parameter to the CollectionType, because I did not customize configureOptions() of CollectionType, but of my ElementType. If you need to pass the my_custom_option_parameter through a CollectionType, you may succeed by defining my_custom_option_parameter in entry_options (see Documentation CollectionType Field) array of CollectionType.
Example passing my_custom_option_parameter through a CollectionType:
class OuterFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
//...
$builder->add('elements', CollectionType::class, array(
'entry_type' => ElementType::class,
// ...
'entry_options' => array(
'my_custom_option_parameter' => 'value is now set!'
)
));
//...
}
}
basing on #pulzarraider answer I created code with changes for Symfony 3.
You need to change
OptionsResolverInterface for OptionsResolver
FormBuilder for FormBuilderInterface
In my case:
namespace MediaBundle\Form;
use Symfony\Component\Form\AbstractType as FormAbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
class ImageType extends FormAbstractType {
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'max_images' => ''
));
}
public function buildView(FormView $view, FormInterface $form, array $options) {
$view->vars['max_images'] = $options['max_images'];
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->setAttribute('max_images', $options['max_images'])
;
}
public function getName() {
return 'image_preview';
}
public function getParent() {
return TextareaType::class;
}
}
Using Symfony 3, I was able to pass custom options to the form by setting a default option in the OptionsResolver injected into configureOptions method of my form type class:
In the Controller:
//Compile whatever your options are here. Assume an array is returned
$customOptions = $this->getMyCustomOptions($args);
//Build the form:
$form = $this->createForm(MyCustomFormType::class, array('my_custom_options' => $customOptions));
MyCustomFormType.php:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => DataModel::class,
'my_custom_options' => []
]);
}
//custom options is now set in "$options" array:
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('my_custom_fields', Type\ChoiceType::class, [
'choices' => $options['my_custom_options'],
'mapped' => false //if not part of the data model.
. . .
So, it looks like you can define a contract with your form and the data provider to set arbitrary data on the form.
I've just successfully implemented this procedure. Note that on the return trip, since you've set 'mapped' => false, in the formBuilder, $form->getData() will not return the selection. To get the selected value:
$mySelectedValue = $form->get('my_custom_options')->getViewData();
in your controller. Why this is is beyond me . . .
I have tried using that options array with no success as it seemed that it could carry only small, predefined subset of keys. This was, by all means, unacceptable for me...
However, you can pass all the options via forms __construct method and store it in class properties for later use. Then, from buildForm you can access it using $this->"propertyName"...
It's up to you to decide whether you want to pass single array or just few variables to __construct...
This is just a rough example:
class Foobar{
private $update = false;
public function __construct($update = false){
$this->update = $update;
}
public function buildForm(FormBuilder builder, array options){
if ( $update ){
// something
}else{
// well, this is not an update - do something else
}
}
}

How access my Service (DependencyInjection) inside my Type classes?

I have a Service (DependencyInjection) that i create, and i use that on my controllers as:
$this->get("service_name")->someMethod()
I want to know how to use that on my Form classes.
Here my example form of class:
namespace Company\SampleBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class AnswerType extends AbstractType {
public function buildForm(FormBuilder $builder, array $options) {
// I want use: $this->get("service") here, how can i use that?
$builder->add('answer', 'textarea');
}
public function getName() {
return 'answer';
}
public function getDefaultOptions(array $options) {
return array(
'data_class' => 'Company\SampleBundle\Entity\Answer',
);
}
}
Thanks
You can use the $options to achieve this. This implies you modify the getDefaultOptions accordingly.
public function getDefaultOptions(array $options) {
return array(
'service' => null,
'data_class' => 'Company\SampleBundle\Entity\Answer',
);
}
In your controller, when you call createForm()
use the $options argument, like this:
$this->createForm('Answer', null, array('service' => $service))