Symfony 2: pass custom variable to twig form template - php

In just started at symfony and Im trying to do something very simple (seems simple to me) using the form templating system, but I cant find a way to do it.
My goal is to render a "back" button besides the submit button. I know how to set the form template and how to override the submit_widget, but the problem is: the back button URL must be defined at the template which is calling the form, so I need to pass this as a variable to the submit_widget somehow, and I cant find a way to do it.
Ideally, it would work like this:
Template:
{% form_theme form 'TutsAdminBundle:Form:bootstrap-horizontal.html.twig' %}
{% block content %}
<h1>User creation</h1>
{{ form(form, { 'attr': {'role': 'form', 'class': 'form-horizontal'}, 'back': path('list_user') }) }}
{% endblock %}
And then, at the form template
{% block submit_widget %}
<div class="col-sm-6 col-sm-offset-1">
<div class="pull-right">
<a class="btn btn-warning" href="{{ back }}"> Back</a>
<button type="submit" class="btn btn-success">Save</button>
</div>
</div>
{% endblock submit_widget %}
But I just cant find a way to access my "back" variable inside the submit_widget block.
How can I achieve that?
UPDATE:
I managed to do what I wanted by:
1- creating a custom field following Chausser advice bellow:
class SaveButtonType extends SubmitType
{
public function __construct($router)
{
$this->_router = $router;
}//constructor
protected $_router;
/* (non-PHPdoc)
* #see \Symfony\Component\Form\AbstractType::buildForm()
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
}//buildForm
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$href = function(Options $options, $value){
return array('href'=>$this->_router->generate($options['route'], $options['params']), 'class'=>'btm btn-warning');
};
$resolver->setDefaults(array('mapped'=>false))->setRequired(array('route', 'params'));
$resolver->setNormalizers(array('attr'=>$href));
}//setDefaultOptions
public function getName()
{
return 'save_button';
}//getName
}//SaveButtonType
Ive made it extends the SubmitType otherwise it would be rendered inside a form_row.
services.yml
tuts_admin.form.field.type.save_button:
class: Tuts\AdminBundle\Form\Field\SaveButtonType
arguments: ["#router"]
tags:
- {name: "form.type", alias: "save_button"}
2- then I defined its template (on that same file Im using to override the form theme
{% block save_button_widget %}
<div class="col-sm-6 col-sm-offset-1">
<div class="pull-right marginL25">
<button type="submit" class="btn btn-success">Save</button>
</div>
<div class="pull-right">
Back
</div>
</div>
{% endblock %}
This type render the submit and the back button at once.
3- finally, since I could not find a way to prevent the rendering of another submit button, I overrided the submit_widget:
{% block submit_widget %}
{% endblock submit_widget %}
A lot more complicated than I was expecting, but it does works.
Thank you Chausser for all your help.

Personally I would write a Custom Field Type and make if for back buttons. Something that will accept a route name and route params.
//Acme/DemoBundle/Form/Type/BackButtonType
<?php
namespace Acme\DemoBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\OptionsResolver\Options;
class BackButtonType extends AbstractType
{
protected $router;
public function __construct($router)
{
$this->router = $router;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$attr = function(Options $options, $value){
return array(
'href'=>$this->_router->generate($options['route'], $options['params']),
'class'=>'btn btn-warning'
);
};
$resolver
->setDefaults(array(
'attr' => $attr,
'mapped' => false
'params' => array(),
))
->setRequired(array(
'route',
))
->setNormalizers(array(
'attr'=>$attr
));
}
public function getName()
{
return 'back_button';
}
}
Then you need to register this as a new form type:
parameters:
acme_demo.form.type.back_button.class: Acme\DemoBundle\Form\Type\BackButtonType
services:
acme_demo.form.type.back_button:
class: %acme_demo.form.type.back_button.class%
arguments: ["#router"]
tags:
- { name: "form.type", alias: "back_button" }
Now you can use this in your normal forms. Last step is to create the twig block to render this form.
//Acme/DemoBundle/Resources/Twig/fields.html.twig
{% block back_button_widget %}
<a {{block('widget_attributes')}}>{{block('form_label')}}</a>
{% endblock %}
Then you need to add this file to the form resources for twig:
//app/config/config.yml
twig:
debug: %kernel.debug%
strict_variables: %kernel.debug%
form:
resources:
- 'AcmeDemoBunlde:Twig:fields.html.twig'
After you have done that you need to clear your caches:
php app/console cache:clear --env=dev
php app/console cache:clear --env=prod
Now how to use this:
//In your Form
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// Add other fields
->add('back', 'back_button',array('route'=>'my_custom_route_name','params'=>array('user_id'=>$this->getUser()->getUserId())))
->add('submit','submit',array('attr'=>array('class'=>'btn btn-primary')));
}
This should work. Hasnt been fully tested but if you have any issues with it let me know.

I believe that you don't need to pass variable, but only make it available (known) to Twig, since form rendering (as opposed to to extending and including other templates) is just block rendering. That is, those variables are in the same scope.
So:
Parent template
{% set back = "MyFooValue" %}
Form template:
<div class="{{ back }}">Some content</div>
Also, I like defining default as fallback in case I forget to (or do not at all) define variable:
<div class="{{ back|default("") }}">Some content</div>
Hope this helps...

Related

Add generate password button in symfony add user admin panel form

First problem: i want to add Generate password button under password text input.
code:
class UserAdmin extends AbstractAdmin
{
...
protected function configureFormFields(FormMapper $formMapper) :void
{
if ($this->getFormAction() == 'create')
$formMapper
->add('email')
->add('plainPassword', TextType::class);
$formMapper->add('enabled');
...
I tried to include ButtonType and add ->add('button', ButtonType::class);after plainPassword input but i got error:
The options "label_render", "property_path",
"sonata_field_description" do not exist. Defined options are: "attr",
"auto_initialize", "block_name", "disabled", "label", "label_format",
"translation_domain".
Other try by add button in twig: in custom_edit.html.twig
{% extends 'SonataAdminBundle:CRUD:edit.html.twig' %}
{% block content %}
<button class="btn btn-primary" id="generate_password">Generate password</button>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="{{ asset('build/admin.js') }}"></script>
{% endblock %}
but button show above the form and won't be easy to handle the action since i use vue:
Second problem: i use this password-generator lib i want when i click Generate button ^ the value of plainPassword input changes to new password every each click. here is code:
class UserAdminCRUDController extends Controller
{
....
public function generatePassword()
{
$generator = new ComputerPasswordGenerator();
$generator
->setUppercase()
->setLowercase()
->setNumbers()
->setSymbols(true)
->setLength(9);
return $generator->generatePasswords(1);
}
....

Symfony/TWIG: Unable to set custom theme for form with dynamic ID (createNamedBuilder)

I wanted to replace Symfony's default radio button widget with Bootstrap's Radio Button Group. I was trying to achieve that by setting a custom theme for an individual field in Symfony's form created with createNamedBuilder function. I failed, because custom field themes require the ID of the field which is dynamic in my case. I know that having variables in theme names is impossible in TWIG, but maybe there is an alternative approach that I could use to resolve my issue.
CODE
I have a Controller which creates multiple instances of one form type in a loop. The name of the form is dynamically created by concatenating requestform_ and the ID of the form:
public function listAction(Request $request)
{
$entityManager = $this->getDoctrine()->getManager();
$requestForms = $entityManager->getRepository('AppBundle:RequestForm')->findBy(array(), array('id' => 'ASC'));
$forms = array();
foreach ($requestForms as $requestForm) {
$formBuilder = $this->get('form.factory')->createNamedBuilder('requestform_'.$requestForm->getId(), RequestFormType::class, $requestForm);
$form = $formBuilder->getForm()->handleRequest($request);
$forms[] = $form->createView();
}
return $this->render('form/index.html.twig', array('forms' => $forms));
}
buildForm function of the RequestFormType prepares radio button widget and looks like this:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('status', EntityType::class, array(
'class' => 'AppBundle:Status',
'choice_label' => 'name',
'expanded' => true,
'multiple' => false
));
}
The Controller renders the following template:
{% block content %}
{% for form in forms %}
{{ form_start(form) }}
{{ form_widget(form.status) }}
{{ form_end(form) }}
{% endfor %}
{% endblock %}
Which is themed to show Bootstrap's button group selector instead of radio buttons:
{% block _requestform_1_status_widget %}
<div id={{ form.vars.id }} class="btn-group" data-toggle="buttons">
{% for status in form.children %}
<label for={{ status.vars.id }} class="btn btn-primary required custom-button-radio">
<input type="radio" id={{ status.vars.id }} name={{ status.vars.full_name }} required="required" value={{ status.vars.value }}>
{{ status.vars.label }}
</label>
{% endfor %}
</div>
{% endblock _requestform_1_status_widget %}
As you may have noticed this will work only for requestForm ID 1 and will not work for other ID values due to TWIG not allowing variables in block names, i.e. {% block _requestform_VARIABLE_status_widget %}.
Any help much appreciated! Thanks
My gut feeling is that you might want to have only one form that includes a CollectionType field with entry_type = RequestFormType::class, and then in the twig, apply a form to the "outer" form as a whole. You should still be able to manage things the way you wanted.

Symfony form validation errors are displayed twice

I need a simple form for the registration of a user. The form validation works as expected, except for the fact that the validation errors coming from a callback-constraint are shown twice for some reason.
I inserted a random number in an error message to see if the callback was called once or twice. The 2 error messages show the same random number, so the calback is (probably) only called once.
Any help is greatly appreciated!
Extra question: "All the errors from the callback function are shown on the same place in the form (as expected) . Is it ( easily ) possible to specify the field at which a validation error should be dislayed? "
The form type class looks as follows :
class RegisterType extends AbstractType{
static private $em;
static private $translator;
public function __construct( EntityManager $em , TranslatorInterface $translator){
RegisterType::$em = $em;
RegisterType::$translator = $translator;
}
public function buildForm(FormBuilderInterface $builder , array $options){
$builder
->add('email','email',[
'attr'=>['placeholder'=>RegisterType::$translator->trans('your.email')],
'label'=>false ,
'constraints'=>[new NotBlank() , new Assert\Email()]])
->add('password','password',[
'attr'=>['placeholder'=>RegisterType::$translator->trans('your.password')],
'label'=>false ,
'constraints'=>[new NotBlank()]])
->add('confirmPassword','password',[
'attr'=>['placeholder'=>RegisterType::$translator->trans('confirm.password')],
'label'=>false ,
'constraints'=>[new NotBlank()]]);
}
public function getName(){
return 'register';
}
public function configureOptions( OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'constraints' =>array( new Assert\Callback( array('AppBundle\Forms\Type\RegisterType','validateForm' ) )),
));
}
static public function validateForm(array $data, ExecutionContextInterface $context){
if($data['email']){
$user = RegisterType::$em->getRepository('AppBundle:User')->findByEmail($data['email']);
if($user){
$context->buildViolation('email.is.used'.rand())->addViolation();
}
}
if($data['password'] && $data['confirmPassword']){
if($data['password'] != $data['confirmPassword']){
$context->buildViolation('password.confirm.failed')->addViolation();
}
}
}
The form is called in a controller as follows:
public function registerFormAction(Request $request){
$form = $this->createForm('register');
$form->handleRequest($request);
if($form->isValid()){
//TODO
}
return new JsonResponse(['success'=>false,
'message'=>$this->renderView('AppBundle:Security:register.html.twig' , ['form'=>$form->createView()])
]);
}
The twig template used to render the form is :
{% form_theme form _self %}
{%- block form_row -%}
<div class = "form-group">
{{- form_errors(form) -}}
{{- form_widget(form) -}}
</div>
{%- endblock form_row -%}
{{ form(form) }}
You are displaying the form errors and form widgets twice!
You have
<div class = "form-group">
{{- form_errors(form) -}} //displays all form errors
{{- form_widget(form) -}} //displays all form widgets (not the labels!)
</div>
and
{{ form(form) }} //displays the whole form
in your code.
Why don't you just render the form by yourself, then you can also set where the form errors should be shown.
{{ form_start(form) }}
<div class="form-group">
{{ form_errors(form.username) }} //errors for the "username" field of your form
{{ form_label(form.username) }} //labels for the "username" field of your form
{{ form_widget(form.username) }} //formfields for the "username" field of your form
</div>
{{ form_end(form) }}
Also, there's a twig function called form_rest(form) which will display all form fields that aren't manuelly rendered yet!
It turned out that the {% form_theme form _self %} functionality can only be used if your template extends another template.
So i put the form_row block in another file and i used
{% form_theme form "otherfile.htmltwig"%}
Which worked fine.

Extend the ChoiceType to get a new option in a form

In a form I need to associate each radio button of an input to a different image.
I would call the construction of this form this way:
$builder = $this->formFactory->createBuilder();
$builder->add('input_name', 'my_choice', array(
'data' => 'n',
'choices' => array('c1' => 'choice1', 'c2' => 'choice2'),
'required' => true,
'expanded' => true,
'multiple' => false,
'images' => array('choice1.jpg', 'choice2.jpg')));
$form = $builder->getForm();
Meaning the radio_button choice1 will be associated to choice1.jpg and the radio_button choice2 will be associated to choice2.jpg.
In plaint HTML using Bootstrap I want this result
<form method="post" action="{{ path('my_path')}}" name="myform">
<div class="btn-group btn-group-justified" data-toggle="buttons">
<label id="btn-choice1" class="btn active">
<input type="radio" name="is_choice" id="c1" value="Choice1" checked>
<img id="choice1_img" src="{{ asset('bundles/myBundle/icons/choice1.png') }}" alt="Choice 1">
</label>
<label id="btn-choice2" class="btn">
<input type="radio" name="is_choice" id="c2" value="Choice2"><img id="choice2_img" src="{{ asset('bundles/myBundle/icons/choice2.png') }}" alt="Choice 2">
</label>
</div>
<button class="btn" type="submit" name="submit">Submit</button>
</form>
I read the official Symfony doc related to this: http://symfony.com/fr/doc/2.3/cookbook/form/create_custom_field_type.html
but the example is very poor.
I need to describe to Symfony where to look for this new option but cannot figure out how to achieve this. I read hundreds of examples on the net but nothing explaining the logic behind how to do this.
So far My new Type is like this below:
<?php
namespace myProject\Bundle\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
class MyChoiceType extends AbstractType
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace($view->vars, array(
'images' => $options['images']
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'images' => array()
));
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'my_choice';
}
}
Would you be able to explain me how to describe this new Type to Symfony (what methods to call with what parameters).
I breaked the code for hours but Symfony is making so muchfor a simple form creation that I lost the path. FormRegistry, FormType, OptionsResolverInterface, BaseType, FormFactory and so many others classes are involved that it makes it difficult to get the global picture.
Otherwise if one can point me to an architecture document explaining the logical behind this it could also be fine.
Found the solution.
I needed also to access the images new option through a Form theme redefinition. That was the difficult part since I didn't know how and what I could access from the form theme template.
Here is how I succeeded in doing it:
{% block my_choice_widget %}
{% spaceless %}
{% if expanded %}
<ul {{ block('widget_container_attributes') }}>
{% for child in form %}
<li>
{{ form_widget(child) }}
{{ form_label(child) }}
{% if images is defined %} {{images[loop.index-1]}} {% endif %}
</li>
{% endfor %}
</ul>
{% else %}
{# just let the choice widget render the select tag #}
{{ block('choice_widget') }}
{% endif %}
{% endspaceless %}
{% endblock %}
The part I added to the Symfony form them template is:
{% if images is defined %} {{images[loop.index-1]}} {% endif %}
That displays the image name in front of the proper radio button.
That should be refined to make sure there are enough images to populate all the loops.
A simple twig test on images count compared to loop.last element will make it.

Symfony2 disable HTML5 form validation

I want to validate my form using server side validation only. However, if the browser supports HTML5 it validates using the HTML5 attributes added to the form by symfony2 so I need to prevent HTML5 validation.
Just add novalidate to your <form> tag:
<form novalidate>
If you are rendering the form in TWIG, you can use following.
{{ form(form, {'attr': {'novalidate': 'novalidate'}}) }}
I know its old question but with SF2.6 in FormType, you can do:
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'attr'=>array('novalidate'=>'novalidate')
));
}
While googling for a solution to this I found one, that seems the most elegant if you want to disable html5 validation in your whole app, so I thought i'd share it here. Credits go to the author of this blog article.
The idea is to create an extension for the "form" form type like this:
<?php
// src/AppBundle/Form/Extension/NoValidateExtension.php
namespace AppBundle\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
class NoValidateExtension extends AbstractTypeExtension
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['attr'] = array_merge($view->vars['attr'], [
'novalidate' => 'novalidate',
]);
}
public function getExtendedType()
{
return 'form';
}
}
?>
Then you just register it in your services.yml like this:
app.no_validation_form_extension:
class: AppBundle\Form\Extension\NoValidateExtension
tags:
- {name: form.type_extension, alias: form}
and you're done. All your forms automatically have a novalidate attribute now.
Symfony 3.3
As of Symfony 3.3 the configuration is slightly different, but still possible.
Slight update to the getExtendedType method to return the FormType class.
// src/AppBundle/Form/Extension/NoValidateExtension.php
namespace AppBundle\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\Type\FormType;
class NoValidateExtension extends AbstractTypeExtension
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['attr'] = array_merge($view->vars['attr'], [
'novalidate' => 'novalidate',
]);
}
public function getExtendedType()
{
return FormType::class;
}
}
Plus some a minor addition of the extended_type tag, which is now required in your service declaration:
app.no_validation_form_extension:
class: AppBundle\Form\Extension\NoValidateExtension
tags:
- {name: form.type_extension, alias: form, extended_type: Symfony\Component\Form\Extension\Core\Type\FormType}
Alternatively if for some reason you don't want to do it in twig as in the answer above...
{{ form(form, {'attr': {'novalidate': 'novalidate'}}) }}
or you create your from manually with createFormBuilder then you could simply use createFormBuilder as a second parameter to define form attribute:
//someAction
$form = $this->createFormBuilder(null, ['attr'=>['novalidate'=>'novalidate']])
->add(...)
->add(...)
->add(...)
->getFrom();
return $this->render("-----:----:----.html.twig", [
'form'=>$form->createView()
]);
If you are using Symfony 3 (or 2) and want to turn off validation for a specific field only you can do this.
$form = $this->createFormBuilder($task)
->add('task', TextType::class, array('required' => false))
->add('dueDate', DateType::class)
->add('save', SubmitType::class, array('label' => 'Create Task'))
->add('saveAndAdd', SubmitType::class, array('label' => 'Save and Add'))
->getForm();
In this sample form notice the array('required' => false), you can add this to any element that you want to disable validation for without disabling validation for the others. Very useful if you want to temporarily disable only one element instead of the entire form.
Note this ONLY disables the HTML5 validation! This does not disable server side validation.
Reference: http://symfony.com/doc/current/book/forms.html#field-type-options
If you actually need to remove the validation attributes (if you are using a validation library want to keep all of your validation constraints in one place for example), you can overwrite the widget_attributes block in twig.
If you are already using custom form templates in app/Resources/views/form.html.twig for example (and have enabled it in your config.yml) you can just add a block for
{% block widget_attributes %}
{% spaceless %}
id="{{ id }}" name="{{ full_name }}"{% if read_only %} readonly="readonly"{% endif %}{% if disabled %} disabled="disabled"{% endif %}
{% for attrname, attrvalue in attr %}{% if attrname in ['placeholder', 'title'] %}{{ attrname }}="{{ attrvalue|trans({}, translation_domain) }}" {% else %}{{ attrname }}="{{ attrvalue }}" {% endif %}{% endfor %}
{% endspaceless %}
{% endblock widget_attributes %}
All I have done here is remove the attributes related to validation:
{% if required %} required="required"{% endif %}{% if max_length %} maxlength="{{ max_length }}"{% endif %}{% if pattern %} pattern="{{ pattern }}"{% endif %}
To disable Regex validation for specific field using formType class:
->add('foo',null,array=>('attr'=>('pattern'=>'/[^~,]/'))
Use form theming:
First create form theme template, e.g app/Resources/views/form/fields.html.twig:
{% extends 'form_div_layout.html.twig' %}{# or some other base layout #}
{% block form_start %}
{% if attr.novalidate is not defined %}
{% set attr = attr|merge({'novalidate':'novalidate'}) %}
{% endif %}
{{ parent() }}
{% endblock %}
Then use that form theme in your template:
{% form_theme form with 'form/fields.html.twig' %}
{{ form_start(form) }} <-- now renders with novalidate attribute
...
{{ form_end(form) }}
Or, apply theme globally (app/config/config.yml):
twig:
form_themes:
- ':form/fields.html.twig'

Categories