Symfony Dynamic Modification of Form without Entity - symfony

I am following the Symfony (v2.7) Cookbook recipe for dynamic form modification. What I am aiming for is displaying certain fields based on a user's radio button selection. For example, if a user wishes to filter a search based on records from the last fiscal year, he selects the "Fiscal Year" radio button from the criteriaFilter choice field type (example below), and the appropriate fields are generated. If he changes his mind and selects "Semester" instead, the fiscal year fields are replaced with the semester fields, and so on.
Example code:
$builder
->add('librarian', 'entity', array(
'class' => 'AppBundle:Staff',
'query_builder' => function(EntityRepository $er){
$qb = $er->createQueryBuilder('st');
$qb
->where('st.employmentStatus = :employmentStatus')
->setParameter('employmentStatus', 'faclib')
->orderBy('st.lastName', 'DESC')
->getQuery();
return $qb;
},
'placeholder' => 'All Librarians',
'required' => false
))
->add('program', 'entity', array(
'class' => 'AppBundle:LiaisonSubject',
'query_builder'=>function(EntityRepository $er){
$qb = $er->createQueryBuilder('ls');
$qb
->orderBy('ls.root, ls.lvl, ls.name', 'ASC')
->getQuery();
return $qb;
},
'property' => 'indentedTitle',
'placeholder' => 'All Programs',
'required' => false,
'label' => 'Program'
))
->add('criteriaFilter', 'choice', array(
'expanded' => true,
'multiple' => false,
'choices' => array(
'academic' => 'Academic Year',
'fiscal' => 'Fiscal Year',
'semester' => 'Semester',
'custom' => 'Custom Range'
),
))
;
This seems pretty straighforward based on the cookbook entry. However, the form I am creating is not bound to an entity. Therefore, fetching data via the method
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){
$form = $event->getForm();
//normally the entity, but NULL in this case
$data = $event->getData();
...
which would normally allow for calling of getter methods on entity properties returns null. So obviously this can't work in this case.
So the question is, is there another way to dynamically generate fields inside of a form that is not tied to an entity?

You can pass options to the form, including data. So something like (from memory but it's untested):
// controller
$this->createForm(SomeForm::class, null, ['fiscalYears' => [2001, 2002]);
// type
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['fiscalyears' => []);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$fiscalYears = $options['fiscalYears'];
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($fiscalYears) {
$form = $event->getForm();
$form->add('fiscalYear', ChoiceType::class, [
'choices' => $fiscalYears
]);
}
}

Related

Symfony 3: How to use two choices/dropdowns from two tables in one form

Symfony version 3.1.3
I have created a dropdown using the entity called Classes and you can see the Controller below,
public function studentAddClassAction( $id, Request $request )
{
// get the student from the student table
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('PIE10Bundle:Users')->find($id);
// new class object and create the form
$classes= $em->getRepository('PIE10Bundle:Classes')->findAll();
$form = $this->createForm(ClassType::class, $classes);
$form->handleRequest($request);
if( $form->isSubmitted() && $form->isValid() )
{
// form submit operations
}
return $this->render(
'PIE10Bundle:student:layout_student_addclass.html.twig',
array(
'user' => $user,
'title' => 'Add Class',
'tables'=> 1,
'form' => $form->createView()
)
);
}
and the ClassType is below
class ClassType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('classes',
EntityType::class,
array('class' => 'PIE10Bundle:Classes',
'expanded' => false,
'multiple' => false,));
$builder->add('Add Class',
SubmitType::class,
array('attr' => array('class' => 'btn btn-primary',
'style' => 'margin:15px 0;')) );
}
}
And this works fine and it gives all the classes from the database. Also I have another entity called Users and it has a column called roles (DC2Type:array) and it has a role called ROLE_PARENT and I can retrieve all the parents using the following query
$query = $this->getDoctrine()->getEntityManager()
->createQuery('SELECT u FROM PIE10Bundle:Users u WHERE u.roles LIKE :role')
->setParameter('role', '%"ROLE_PARENT"%' );
$users = $query->getResult();
My Question is how to add these parents list as a choice list into same above form in the studentAddClassAction Controller.
Please let me know in any other information is needed for this.
To have a custom set of entities as a choice list you need to use a query_builder option
So it will look like
$builder->add('parent',
EntityType::class,
array('class' => 'PIE10Bundle:Users',
'expanded' => false,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('u')
->where('u.roles LIKE :role')
->setParameter('role', '%"ROLE_PARENT"%');
},
'multiple' => false
));

Dynamic Generation for Submitted Forms with Form events

I've a little problem with FormEvents, I want do 3 fields populated dynamically.
I explain, I've 3 fields: Project > Box > Cell, the user choose a Project, the Box list is updated, he choose a Box, the cell list is updated.
To do it, I use FormEvent like the documentation say (http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html#cookbook-form-events-submitted-data)
But I've a problem, for just one field dynamically updated, it's work, but no for 2 fields... Actually a user can choose a project, and when he does it, the box field is updated. But, when he choose a box, the cell field wasn't updated...
But, I've find something, who permit it to work, just change something in a ->add() and inversed to ->add(). But I don't want it.
My code is:
$builder
->add('project', EntityType::class, array(
'class' => 'AppBundle\Entity\Project',
'choice_label' => 'name',
'placeholder' => '-- select a project --',
'mapped' => false,
))
->add('box', EntityType::class, array(
'class' => 'AppBundle\Entity\Box',
'choice_label' => 'name',
'placeholder' => '-- select a box --',
'choices' => [],
))
->add('cell', ChoiceType::class, array(
'placeholder' => '-- select a cell --',
))
;
And when I change it to:
builder
->add('box', EntityType::class, array(
'class' => 'AppBundle\Entity\Box',
'choice_label' => 'name',
'placeholder' => '-- select a box --',
// 'choices' => [],
))
->add('project', EntityType::class, array(
'class' => 'AppBundle\Entity\Project',
'choice_label' => 'name',
'placeholder' => '-- select a project --',
'mapped' => false,
))
->add('cell', ChoiceType::class, array(
'placeholder' => '-- select a cell --',
))
;
It's work... But I want an empty list for box at the start, and I want project before box...
A little precision, this form is embded in an other form as a CollectionType.
All the code of this Type:
<?php
namespace AppBundle\Form;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TubeType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('project', EntityType::class, array(
'class' => 'AppBundle\Entity\Project',
'choice_label' => 'name',
'placeholder' => '-- select a project --',
'mapped' => false,
))
->add('box', EntityType::class, array(
'class' => 'AppBundle\Entity\Box',
'choice_label' => 'name',
'placeholder' => '-- select a box --',
'choices' => [],
))
->add('cell', ChoiceType::class, array(
'placeholder' => '-- select a cell --',
))
;
// MODIFIER
$boxModifier = function (FormInterface $form, $project) {
$boxes = (null === $project) ? [] : $project->getBoxes();
$form->add('box', EntityType::class, array(
'class' => 'AppBundle\Entity\Box',
'choice_label' => 'name',
'placeholder' => '-- select a box --',
'choices' => $boxes,
));
};
$cellModifier = function(FormInterface $form, $box) {
$cells = (null === $box) ? [] : $box->getEmptyCells();
$form->add('cell', ChoiceType::class, array(
'placeholder' => '-- select a cell --',
'choices' => $cells,
));
};
// FORM EVENT LISTENER
$builder->get('project')->addEventListener(
FormEvents::POST_SUBMIT,
function(FormEvent $event) use ($boxModifier) {
$project = $event->getForm()->getData();
$boxModifier($event->getForm()->getParent(), $project);
}
);
$builder->get('box')->addEventListener(
FormEvents::POST_SUBMIT,
function(FormEvent $event) use ($cellModifier) {
$box = $event->getForm()->getData();
$cellModifier($event->getForm()->getParent(), $box);
}
);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Tube'
));
}
}
Thanks a lot to your help :)
You should use $builder->addEventListener. For multiple fields all you need to do is to have dynamic fields inside FormEvents::PRE_SET_DATA event handler. Also, use parent field data, as explained in the doc to fetch child field choices.
I have used this approach, for generating Country, State and City Entities in hierarchical fields. Let me know if it helps or you need more information.
EDIT : For bigger logic, you can use eventSubscriber which will keep your code clean and you also get to re-use dynamic part of the form for somewhere else.
For multiple dependent hierarchical fields, just add them through conditions in the eventSubscriber class.
Update with code snippet :
Here is a walk through on code snippet that worked for me in Symfony 2.7
Note : I don't replace the dynamic html field as described in the document, instead via jQuery I simply collect child options as per selected parent option and fill in those. When submitted, The form recognises correct child options as per eventSubscriber context. So here is how you might do it :
In your parent Form type (where you have all 3 fields) call a eventSubscriber instead of defining those 3 fields :
$builder->add(); // all other fields..
$builder->addEventSubscriber(new DynamicFieldsSubscriber());
Create an eventSubscriber as defined in the doc, here the file name is DynamicFieldsSubscriber
<?php
namespace YourBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\FormInterface;
class DynamicFieldsSubscriber implements EventSubscriberInterface
{
/**
* Define the events we need to subscribe
* #return type
*/
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => 'preSetData', // check preSetData method below
FormEvents::PRE_SUBMIT => 'preSubmitData', // check preSubmitData method below
);
}
/**
* Handling form fields before form renders.
* #param FormEvent $event
*/
public function preSetData(FormEvent $event)
{
$location = $event->getData();
// Location is the main entity which is obviously form's (data_class)
$form = $event->getForm();
$country = "";
$state = "";
$district = "";
if ($location) {
// collect preliminary values for 3 fields.
$country = $location->getCountry();
$state = $location->getState();
$district = $location->getDistrict();
}
// Add country field as its static.
$form->add('country', 'entity', array(
'class' => 'YourBundle:Country',
'label' => 'Select Country',
'empty_value' => ' -- Select Country -- ',
'required' => true,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('c')
->where('c.status = ?1')
->setParameter(1, 1);
}
));
// Now add all child fields.
$this->addStateField($form, $country);
$this->addDistrictField($form, $state);
}
/**
* Handling Form fields before form submits.
* #param FormEvent $event
*/
public function preSubmitData(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
// Here $data will be in array format.
// Add property field if parent entity data is available.
$country = isset($data['country']) ? $data['country'] : $data['country'];
$state = isset($data['state']) ? $data['state'] : $data['state'];
$district = isset($data['district']) ? $data['district'] : $data['district'];
// Call methods to add child fields.
$this->addStateField($form, $country);
$this->addDistrictField($form, $state);
}
/**
* Method to Add State Field. (first dynamic field.)
* #param FormInterface $form
* #param type $country
*/
private function addStateField(FormInterface $form, $country = null)
{
$countryCode = (is_object($country)) ? $country->getCountryCode() : $country;
// $countryCode is dynamic here, collected from the event based data flow.
$form->add('state', 'entity', array(
'class' => 'YourBundle:State',
'label' => 'Select State',
'empty_value' => ' -- Select State -- ',
'required' => true,
'attr' => array('class' => 'state'),
'query_builder' => function (EntityRepository $er) use($countryCode) {
return $er->createQueryBuilder('u')
->where('u.countryCode = :countrycode')
->setParameter('countrycode', $countryCode);
}
));
}
/**
* Method to Add District Field, (second dynamic field)
* #param FormInterface $form
* #param type $state
*/
private function addDistrictField(FormInterface $form, $state = null)
{
$stateCode = (is_object($state)) ? $state->getStatecode() : $state;
// $stateCode is dynamic in here collected from event based data flow.
$form->add('district', 'entity', array(
'class' => 'YourBundle:District',
'label' => 'Select District',
'empty_value' => ' -- Select District -- ',
'required' => true,
'attr' => array('class' => 'district'),
'query_builder' => function (EntityRepository $er) use($stateCode) {
return $er->createQueryBuilder('s')
->where('s.stateCode = :statecode')
->setParameter('statecode', $stateCode);
}
));
}
}
After this, you need to write jQuery events which should update child options on change of parent option explicitly, You shouldn't face any error on submission of the form.
Note : The code above is extracted and changed for publishing here. Take care of namespace and variables where ever required.

Symfony2 FormBuilder with Entity class

I have a form that works well, there is just one issue with it and I'm hoping that I'll get an answer on how to do what I need to do.
<?php
namespace ADS\UserBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Security\Core\SecurityContext;
class UserType extends AbstractType {
private $type;
public function __construct($type) {
$this->type = $type;
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('firstName', 'text', array('required' => true))
->add('lastName', 'text', array('required' => true));
$builder->add('email', 'email', array('required' => true));
$builder->add('parentCompany', 'entity', array(
'class' => 'ADSUserBundle:Company',
'expanded' => false,
'empty_value' => 'CHOOSE ONE',
'required' => false,
'property' => 'companyName'
))
->add('enabled', 'choice', array('choices' => array('1' => 'Enabled', '0' => 'Disabled')))
->add('roles', 'entity', array(
'class' => 'ADSUserBundle:Roles',
'required' => true,
'property' => 'displayName',
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\UserBundle\Entity\User'));
}
public function getName() { return 'ads_userbundle_user'; }
}
I have this form, the portion I am looking at is the 'roles' portion... Right now it created a multiple select box ( as I expect it to ), though the value is sequentially ie: 0,1,2,3,4...
What I really need is to figure out how to take this entity, and make the property to be the displayName ( as it is now ) and get the value to be the corresponding internalName This way it'll give me an array like:
array('ROLE_EXAMPLE' => 'EXAMPLE', 'ROLE_EXAMPLE1' => 'EXAMPLE1')
Any ideas how to accomplish this is greatly appreciated.
Kamil Adryjanek is correct, it is going to be much easier if you change it from an entity to a choice field. I've done some testing, both with FOSUserBundle and without the bundle - in both cases I hit some interesting road blocks.
First, I tried to run it through QueryBuilder in a repository, that didn't work out as it should have. The reason being, the fact that you wanted to be returning an array instead of a ORM object causes an error.
So next, I started looking at creating the choice field. All the guides, say to use the fieldname role instead of roles so I tried that, but I then had to duplicate the UserInterface from FOSUserBundle - I didn't want to do that -- so here I am stressed, and trying to figure it out.
Here is what I ended up doing, which works well.
private $normalRoles = array();
then in the __construct I add: $this->normalRoles = $roles;
Here is the builder:
$builder
->add('roles', 'choice', array(
'multiple' => true,
'choices' => $this->normalRoles
))
;
Originally, I left the multiple part out, figuring that it'd at least let me see an option box. I ended up getting an Array to String conversion error. So, adding the 'multiple' => true in, fixes that error.
Then, in my repository I created a function called normalizeRoles
public function normalizeRoles() {
$data = array();
$qb = $this->getEntityManager();
$query = $qb->createQuery(
"SELECT r.internalName, r.displayName FROM AcmeUserBundle:Roles r"
)->getArrayResult();
foreach ($query as $k => $v) {
$data[$v['internalName']] = $v['displayName'];
}
return $data;
}
From here, we just have to make some small edits in the DefaultController of the UserBundle in the newAction and editAction ( both are the same changes )
So, first off is to put into your Controller use Acme/UserBundle/Entity/Roles in order to avoid any errors and be able to get that repository.
Next, right before you create the form you run the normalizeRoles() function
$roles = $em->getRepository('AcmeUserBundle:Roles')->normalizeRoles()
Then, you pass it through the construct via: new UserType($roles)
full line for that would look like this:
$form = $this->createForm(new UserType($roles), $entity, array(
'action' => $this->generateUrl('acmd.user.edit', array(
'id' => $id)
)
));
or for new:
$form = $this->createForm(new UserType($roles), $entity, array(
'action' => $this->generateUrl('acmd.user.new')
)
));
At this point -- You'll have a working system that will allow you to dynamically add roles into a database table, and then associate those with a new or current user.
You can try do it via query_builder attribute:
$builder->add('roles', 'entity', array(
'class' => 'ADSUserBundle:Roles',
'required' => true,
'property' => 'displayName',
'query_builder' => function (RolesRepository $queryBuilder) {
return $queryBuilder->someMethod() // some method in this repository that return correct query to db.
},
));
In this case it would be better to use choice field Type (http://symfony.com/doc/current/reference/forms/types/choice.html) instead of entity and pass some role choices as option to form because entity field Type get entity id as key for choices:
public function buildForm(FormBuilderInterface $builder, array $options) {
...
$builder->add('roles', 'choice', array(
'choices' => $options['role_choices']
));
...
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'data_class' => 'ADS\UserBundle\Entity\User',
'role_choices' => array()
));
}
Notice: it's recommended to pass variables to form through options parameter, not in constructor.
if I understand your question correctly, you need a data transformers. They help you to show data in form as you want.
Documentation: http://symfony.com/doc/current/cookbook/form/data_transformers.html

symfony2 how to save specific data with entity in formbuilder

I need to store userinfo for active user represented by $id, which I parsed from controller. In this case (in code) I obtain in form list of all users, but I need to obtain only actual user which I work with.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$id = $this->id;
$builder
->add('companyname')
->add('address')
->add('city')
->add('zip')
->add('userinfo', 'entity', array('property' => 'id', 'class' => 'Admin\SecureBundle\Entity\Userinfo', 'attr' => array('value' => $id), 'empty_value' => $id));
idea: I have a user and an invoice address for that user. In database onetoone rel.
I need to create invoiceaddress of the active user - id of this user should be stored in userinfo.
Any ideas?
This is from the Symfony2-Doc about the entity form:
$builder->add('users', 'entity', array(
'class' => 'AcmeHelloBundle:User',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('u')
->where('u.isActive = 1')
->orderBy('u.username', 'ASC');
},
));
You need to make your own query here.

How validate a selection of at least one element (or N elements) in Symfony 2?

My form as field users (entity type). How can i add validation in order to specificity that at least one user should be selected? Actually i'm adding an event listener but i don't know if this is a legit solution or not:
public function buildForm(\Symfony\Component\Form\FormBuilder $builder,
array $options)
{
$builder
->add('title', 'text', array(
'label' => 'Titolo'
))
->add('content', 'textarea', array(
'label' => 'Contenuto'
))
->add('sender_text', 'text', array(
'label' => 'Mittente testuale',
))
->add('users', 'entity', array(
'label' => 'Destinatari',
'class' => 'DL\FidelityBundle\Entity\User',
'property' => 'select_label',
'multiple' => true
));
;
// Valida il numero di utenti selezionati
$builder->addEventListener(\Symfony\Component\Form\FormEvents::POST_BIND,
function($event) {
$form = $event->getForm();
$data = $event->getData();
if(!$data->users->isEmpty()) return;
$msg = 'Occorre specificare almeno un utente destinatario';
$form->get('users')->addError(new FormError($msg));
});
}
As of Symfony 2.1, you can use the Count constraint. If you are on 2.0, you can simply copy the constraint to your project and adapt its namespaces and its API (which was slightly changed between 2.0 and 2.1).
/**
* #Assert\Count(min = 1, minMessage = "Occorre specificare almeno un utente destinatario")
*/
private $users = new ArrayCollection();
Have you tried using Count constraint validator?
I suppose your code will look like this:
->add('users', 'entity', array(
'label' => 'Destinatari',
'class' => 'DL\FidelityBundle\Entity\User',
'property' => 'select_label',
'multiple' => true,
'constraints' => new Count(
array('min' => 1, 'minMessage' => "Please select at least one user')
),
));
Have a look at the validation component: http://symfony.com/doc/current/book/validation.html
You can write a callback constraint within the object you want to validate:
use Symfony\Component\Validator\ExecutionContext;
public function isUsersValid(ExecutionContext $context)
{
if ($this->users->isEmpty()) {
$propertyPath = $context->getPropertyPath() . '.users';
$context->setPropertyPath($propertyPath);
$context->addViolation('Occorre specificare almeno un utente destinatario', array(), null);
}
}
See the callback constraint page how to add this constraint to your entity (this depens if you are using annotations or yaml/xml).

Resources