Modifying Symfony Forms html attributes / overriding a single attribute - symfony

I am trying to figure out how to modify html attributes on the fly with Symfony2 forms.
The situation is a case where a default placeholder is used most of the time, but occasionally, the developer needs to write a custom message.
My Form type looks like this:
<?php
namespace My\AwesomeBundle\FormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use My\AwesomeBundle\Transformer\ProcedureCodeTransformer;
class ProcedureType extends AbstractType
{
private $em;
private $user;
public function __construct($em, $securityContext)
{
$this->em=$em;
$this->user=$securityContext->getToken()->getUser();
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new ProcedureTransformer( $this->em );
$builder->addModelTransformer( $transformer );
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$user = $this->user;
$resolver->setDefaults(
array(
'class' => 'My\AwesomeBundle\Entity\Procedure',
'label' => 'Procedure',
'label_attr' => array( 'class'=> 'control-label' ),
'required' => false,
'empty_value' => '',
'attr' => array(
'class' => 's2',
'data-select2-placeholder' => 'Select Procedure',
),
)
);
$resolver->setOptional( array( 'placeholder' ) );
}
public function getParent() {
return 'hidden';
}
public function getName() {
return 'procedure';
}
}
The default render then has "Select Procedure" for the data-select2-placeholder element and javascript is used to display it. This is then used in a more complex type:
<?php
namespace My\AwesomeBundle\FormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ProcedureType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options){
$builder->add('biller', 'billers', array( 'placeholder' => 'New Text'));
[...]
}
public function setDefaultOptions(OptionsResolverInterface $resolver){
$resolver->setDefaults( array(
'data_class' => 'My\AwesomeBundle\Entity\Procedure'
) );
}
public function getName(){
return 'my_awesomebundle_proceduretype';
}
}
I would like 'New Text' to be placed into the data-select2-placeholder html attribute. However, if I call the builder like this:
$builder->add('procedure',
new ProcedureCodeType()),
array( 'attr' => array('data-select2-placeholder' => 'New Text') )
);
The entire html attribute array is replaced. this is not surprising. Is there a function in the form builder that has eluded me to add or modify a single html attribute?

Unfortunately, there is no way to modify a single attr the way you would want it. This is the closest and most simple solution I could come with:
What you have to do in your situation is to use callback functions in your options.
For each default attribute in your form types, instead of a normal value, you can use a callback function that accepts a single input representing your options (Symfony class Symfony\Component\OptionsResolver\Options). The function is called when building the form and the return value is then used as the value of the option.
That way, you can build the resulting option depending on the other provided options. An example:
use Symfony\Component\OptionsResolver\Options;
class ProcedureType extends AbstractType
{
...
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$attr = function(Options $options) {
$default = array(
'class' => 's2',
'data-select2-placeholder' => 'Select Procedure',
);
return array_replace($default, $options['new_attr']);
};
$user = $this->user;
$resolver->setDefaults(
array(
'class' => 'My\AwesomeBundle\Entity\Procedure',
'label' => 'Procedure',
'label_attr' => array( 'class'=> 'control-label' ),
'required' => false,
'empty_value' => '',
'attr' => $attr,
'new_attr' => array(),
)
);
$resolver->setOptional( array( 'placeholder' ) );
}
That way, what you have to modify will be new_attr.

You might want to create a new class that resolves the nested options for you.
<?php
namespace My\AwesomeBundle\Form\Attr;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\OptionsResolver\Options;
class ProcedureCodeAttr
{
/**
* #var array
*/
protected $options;
/**
* #param array $options
*/
public function __construct(array $options = array())
{
$resolver = new OptionsResolver();
$this->setDefaultOptions($resolver);
$this->options = $resolver->resolve($options);
}
/**
* #param OptionsResolverInterface $resolver
*/
protected function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'class' => 's2',
'data-select2-placeholder' => 'Select Procedure',
));
}
/**
* #return array
*/
public function toArray()
{
return $this->options;
}
/**
* #param array $options
* #return array
*/
public static function resolve(array $options = array())
{
$attr = new static($options);
return $attr->toArray();
}
}
Then in your form type class you'd use it like so
$builder->add('procedure', new ProcedureCodeType()), array(
'attr' => ProcedureCodeAttr::resolve(array(
'data-select2-placeholder' => 'New Text'
))
));

Symfony 4.4 and above now support nested options https://symfony.com/doc/4.4/components/options_resolver.html#nested-options
Instead of defining a nested array, create a callback with OptionsResolver.
For example, a custom form type exists with the following defaults:
class CustomType extends AbstractType {
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('attr', function (OptionsResolver $attrResolver) {
$attrResolver->setDefaults([
'class' => 'form-control-lg',
'placeholder' => 'My Default Placeholder',
'autocomplete' => 'off',
]);
});
$resolver->setDefaults([
'label' => 'Name',
'help' => 'Enter a name',
]);
}
....
}
Attr is now a callback, not an array, now in any forms that use it just defined the attr properties like normal.
class MyFormType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', CustomType::class, [
'label' => 'My Form',
'help' => 'My Form Help',
'attr' => [
'placeholder' => 'My Form Placeholder',
],
])
....
}
The generated form will have the class: 'form-control-lg' and the label: 'My Form Placeholder'.

Related

Custom Variables in custom FormType

Got some question about passing custom variables to a custom FormType
I have a custom FormType named KontoType:
I pass some custom variables to it and this works like as expected, if i override the method buildForm and dump the passed $options array, the mandant exists and is a entity.
But how the heck can i now pass this custom variable to the function getChoices() which loads the choices based on the mandant in this custom FormType?
Even if i did reset the $options in the override buildForm function like $options['choices'] = $this->getChoices($options['mandant']) the select box is empty if i render this form.
<?php
namespace App\Form\Type;
use App\Entity\Core\Finanzen\Konto;
use App\Entity\Core\Organisation\Mandant;
use App\Services\LocaleService;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Translation\TranslatorInterface;
class KontoType extends AbstractType
{
/**
* #var ObjectManager
*/
private $manager;
/**
* #var TranslatorInterface
*/
private $translator;
/**
* #var LocaleService
*/
private $localeService;
public function __construct(ObjectManager $manager, TranslatorInterface $translator, LocaleService $localeService)
{
$this->manager = $manager;
$this->translator = $translator;
$this->localeService = $localeService;
}
private function getChoices(Mandant $mandant=null)
{
return $this->manager->getRepository(Konto::class)->findBuchbar(true, $mandant);
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'label' => 'konto.name',
'help' => 'konto.help',
'choices' => null,
'attr' => array(
'class' => 'bs-select',
'aria-hidden' => 'true',
'ref' => 'input',
'multiple' => false,
'tabindex' => 1,
'data-live-search' => true,
'data-size' => 6
),
'choice_label' => function ($choiceValue, $key, $value) {
return $choiceValue->getKonto()." ".$this->localeService->doTranslate($choiceValue);
},
'choice_value' => function(Konto $konto = null) {
return $konto ? $konto->getId() : '' ;
},
'required' => true,
'multiple' => false,
'empty_data' => null,
'label_attr' => array(
'class' => 'control-label'
),
'placeholder' => 'message.bitte wählen',
'mandant' => null
));
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$options['choices'] = $this->getChoices($options['mandant']);
parent::buildForm($builder, $options); // TODO: Change the autogenerated stub
}
public function getParent() {
return ChoiceType::class;
}
}

How to create a form to edit user roles with FriendsOfSymfony UserBundle

I'm trying to create a controller where I can edit the roles of a user (just that, nothing else) and I'm king of stuck.
I've created a form type:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'roles', 'choice', [
'choices' => ['ROLE_ADMIN', 'ROLE_USER', 'ROLE_CUSTOMER'],
'expanded' => true,
'multiple' => true,
]
)
->add('send', 'submit');
}
First, what would be the best way to retrieve the roles? Is there any way to associate a label to them?
In the controller I have this:
/**
* User role edition
*
* #Route(
* path="/edit-roles",
* name = "backoffice_user_edit_roles",
* requirements = {
* "id_user" = "\d*",
* },
* methods = {"GET"}
* )
*
* #Security("has_role('ROLE_ADMIN')")
*
* #Template
*/
public function editRolesAction($id_user)
{
$user = $this->user_repository->findOneById($id_user);
$form = $this->form_factory->create('dirital_user_roles_form_type', $user);
return [
'form' => $form->createView(),
'user' => $user
];
}
Problems that I have:
The form doesn't get populate with the current user roles, how should I do that?
When receiving the form, how can I update the user?
Thanks a lot
Actually it was easier than I thought – this is the form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'roles', 'choice', [
'choices' => ['ROLE_ADMIN' => 'ROLE_ADMIN', 'ROLE_USER' => 'ROLE_USER', 'ROLE_CUSTOMER' => 'ROLE_CUSTOMER'],
'expanded' => true,
'multiple' => true,
]
)
->add('save', 'submit', ['label' => 'ui.button.save']);
}
And the controller:
public function editRolesAction(Request $request, $id_user)
{
$user = $this->user_repository->findOneById($id_user);
$form = $this->form_factory->create('dirital_user_roles_form_type', $user);
$form->handleRequest($request);
if($form->isValid())
{
$this->addFlash('success', 'section.backoffice.users.edit_roles.confirmation');
$this->em->persist($user);
$this->em->flush();
$this->redirectToRoute('backoffice_user_edit_roles', ['id_user' => $user->getId()]);
}
return [
'form' => $form->createView(),
'user' => $user
];
}
The only part that remains to do is grabbing the form choices from the config instead of hardcoding them.
To rehuse easily in other controllers or actions, I like more this approach:
<?php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\CallbackTransformer;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add(
'roles', 'choice', [
'choices' => ['ROLE_ADMIN' => 'ROLE_ADMIN', 'ROLE_USER' => 'ROLE_USER', 'ROLE_CUSTOMER' => 'ROLE_CUSTOMER'],
'expanded' => true,
'multiple' => true,
]
)
->add('save', 'submit', ['label' => 'ui.button.save']);
;
// Data transformer
$builder->get('roles')
->addModelTransformer(new CallbackTransformer(
function ($rolesArray) {
// transform the array to a string
return count($rolesArray)? $rolesArray[0]: null;
},
function ($rolesString) {
// transform the string back to an array
return [$rolesString];
}
));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}
without touching the controller.
I added the whole file to see the clases you need to import

Add roles field to FOSUserBundle group form

When activating group( base on its documentation) in FOSUserBundle, the group roles are not embedded on edit and update form!I already override GroupFormType and GroupController but I can't pass roles from controller to form class.
my question is how can I add roles to form to let administrator change or assign role to groups?
Solving my problem by adding the role field to override GroupController
public function editAction(Request $request, $groupName)
{
...
/** #var $formFactory \FOS\UserBundle\Form\Factory\FactoryInterface */
$formFactory = $this->get('fos_user.group.form.factory');
$form = $formFactory->createForm();
$form->add('roles', 'choice', array(
'choices' => $this->getExistingRoles(),
'data' => $group->getRoles(),
'label' => 'Roles',
'expanded' => true,
'multiple' => true,
'mapped' => true,
));
...
}
public function getExistingRoles()
{
$roleHierarchy = $this->container->getParameter('security.role_hierarchy.roles');
$roles = array_keys($roleHierarchy);
foreach ($roles as $role) {
$theRoles[$role] = $role;
}
return $theRoles;
}
For my part I decided to inerite the GroupFormType class. Here is inherited class :
namespace UserBundle\Form;
use FOS\UserBundle\Form\Type\GroupFormType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
class GroupType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$permissions = array(
'Utilisateur' => 'ROLE_USER',
'Administrateur' => 'ROLE_ADMIN'
);
$builder
->add('name', null, array('label' => 'form.group_name', 'translation_domain' => 'FOSUserBundle'))
->add('role', ChoiceType::class, array(
'label' => 'Rôle',
'choices' => $permissions,
'multiple' => true,
'expanded' => true
))
;
}
public function getParent()
{
return GroupFormType::class;
}
}
Don't forget to precise your new class in the config.yml
fos_user:
group:
form:
type: UserBundle\Form\GroupType

FOS UserBundle - Override the FormFactory

i need some help overriding the FormFactory.
My Goal is to change the Profile. So, as i also use Facebook Login, i do not want them to change email, username and password.
So i use the ProfileController in my bundle to hand over the current user to the ProfileFormType class.
What i'm trying to do is to implement my own FormFactory, so i can set the user and put it into the options array inside the call
return $this->formFactory->createNamed($this->name, $this->type, null, array('validation_groups' => $this->validationGroups, 'user' => $this->user));
To achieve this, i need to define my FormFactory in sevices.yml.
Here is the original one from FOSUserBundle:
<service id="fos_user.profile.form.factory" class="FOS\UserBundle\Form\Factory\FormFactory">
<argument type="service" id="form.factory" />
<argument>%fos_user.profile.form.name%</argument>
<argument>%fos_user.profile.form.type%</argument>
<argument>%fos_user.profile.form.validation_groups%</argument>
</service>
I have difficulties to translate this into yml, as i do not understand the usages of aliases completely.
Could you help me to define it correct? Something like
skt_user.profile.form.factory:
class: SKT\UserBundle\Form\Factory\FormFactory
arguments: ???
Funny, after posting it, I found the solution. This is the correct configuration for my FormFactory:
skt_user.profile.form.factory:
class: SKT\UserBundle\Form\Factory\FormFactory
arguments: ["#form.factory", "%fos_user.profile.form.name%", "%fos_user.profile.form.type%", "%fos_user.profile.form.validation_groups%"]
In my controller, I simply used these 2 lines:
$formFactory = $this->container->get('skt_user.profile.form.factory');
$formFactory->setUser($user);
In the factory, I implemented this function
namespace SKT\UserBundle\Form\Factory;
use Symfony\Component\Form\FormFactoryInterface;
use FOS\UserBundle\Form\Factory\FactoryInterface;
class FormFactory implements FactoryInterface
{
private $formFactory;
private $name;
private $type;
private $validationGroups;
private $user;
public function __construct(FormFactoryInterface $formFactory, $name, $type, array $validationGroups = null)
{
$this->formFactory = $formFactory;
$this->name = $name;
$this->type = $type;
$this->validationGroups = $validationGroups;
}
public function createForm()
{
return $this->formFactory->createNamed($this->name, $this->type, null, array('validation_groups' => $this->validationGroups, 'user' => $this->user));
}
public function setUser($user)
{
$this->user = $user;
}
}
and this is how my Formtype looks
<?php
namespace SKT\UserBundle\Form\Type;
use SKT\CaromBundle\Repository\PlayerRepository;
use Symfony\Component\Form\FormBuilderInterface;
use FOS\UserBundle\Form\Type\RegistrationFormType as BaseType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ProfileFormType extends \FOS\UserBundle\Form\Type\ProfileFormType
{
private $class;
/**
* #param string $class The User class name
*/
public function __construct($class)
{
$this->class = $class;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Do not show email and username if login uses facebook
if (!$options['user']->getFacebookId()) {
$builder
->add('email', 'email', array('label' => 'form.email', 'translation_domain' => 'FOSUserBundle'))
->add('username', null, array('label' => 'form.username', 'translation_domain' => 'FOSUserBundle'));
}
$builder
->add('firstname', null, array('label' => 'Vorname'))
->add('lastname', null, array('label' => 'Nachname'))
->add('player', 'entity', array(
'label' => 'Spieler',
'class' => 'SKTCaromBundle:Player',
'property' => 'name',
'query_builder' => function (PlayerRepository $er) {
return $er->createQueryBuilder('p')
->orderBy('p.name', 'ASC');
},
'empty_value' => 'Verbinde Dich mit einem Spieler',
'required' => false,
));
}
public function getName()
{
return 'skt_user_profile';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => $this->class,
'intention' => 'profile',
'user' => null
));
}
}
works perfect!

Symfony2 collection form problems

Im using symfony 2.3.2, and I'm trying to add subelements to collection by this way on my form type
<?php
namespace candgo\EventoBundle\Form\Ajax;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use candgo\EventoBundle\Entity\Entrada;
use candgo\EventoBundle\Form\EntradaOrderType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class EntradaAjaxType extends AbstractType
{
protected $em,$id_evento;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('abc','collection');
$builder->get('abc')->add('poc');
}
public function __construct($em,$id_evento)
{
$this->em=$em;
$this->id_evento=$id_evento;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'show_legend' => false,
));
}
public function getName()
{
return 'purchase_tickets2';
}
}
I also tryied the code example provided by symfony doc
$builder->add('favorite_cities', 'collection', array(
'type' => 'choice',
'options' => array(
'choices' => array(
'nashville' => 'Nashville',
'paris' => 'Paris',
'berlin' => 'Berlin',
'london' => 'London',
),
),
));
At both chases the rendered form is empty, Anybody knows will be the problem?

Resources