Scenario:
step 1: fetch information from some archaic web service
step 2: adding x radio buttons to my form, while x depends on the information from the web service
I understand that I should add an Event Subscriber Class as shown in the documentation
$event has a setData method. Probably I have to use it. How can I access this method from the controller?
Additional information:
I am using Doctrine. At the moment I am creating a Doctrine entity and pass this to the form like this:
$product = new Product(); $form = $this->createForm(new ProductType(), $product);
This is my solution I ended up with. Not sure if this is the best way, so this answer is not marked as correct to encourage other, more experience Symfony user to come up with a better solution.
What I don't like about this solution, is that I cannot pass my whole product entity to the form any more, but have to add each attribute individually to the options array:
Anyway, that's how it is implemented now:
The dynamic data is passed using the options array as second parameter to createForm():
// ExampleController.php
use Acme\ExampleBundle\Entity\Product;
public function addAction()
{
// choices contains the dynamic data I am fetching from the webservice in my actual code
$choices = array("key1" => "value1", "key2" => "value2");
// now create a new Entity
$product = new Product();
// and some attributes already have values for some reason
$product->setPrice(42);
$form = $this->createForm(new AddproductType(),
array(
"choices" => $choices,
"price" => $product->getPrice()
)
);
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
// ... standard symfony stuff here
}
}
return array(
'form' => $form->createView()
);
}
And now for the form class. Note the event listener part and the setDefaultOptions method.
// ProductType.php
namespace Acme\ExampleBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$formFactory = $builder->getFormFactory();
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (\Symfony\Component\Form\FormEvent $event) use ($formFactory) {
// get the option array passed from the controller
$options = $event->getData();
$choices = $options["choices"];
$event->getForm()->add(
// add a new element with radio buttons to the form
$formFactory->createNamed("my_dynamic_field_name", "choice", null, array(
"empty_value" => "Choose an option",
"label" => "My dynamic field: ",
"choices" => $choices
)
)
);
}
);
// ... add other form elements
$builder->add("price", "text");
// ..
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
// register choices as a potential option
$resolver->setDefaults(array(
'choices' => null
));
}
}
You don't have to setData() on the $event object. You've to implement an on POST_SET_DATA logic that builds your form the way you want according to you data.
You've then to initialize your form (inside your controller) using your webservice's reply.
(Note: SET_DATA form event is deprecated since version 2.1. It'll be definitely removed in version 2.3)
Update:
You can set an array as form data and use a DataTransformer to structure your form's data the way you want. Take a look at the Data Transformation part of Symfony 2 Form Tricks during the last San Francisco Symfony Live.
Related
I have Entity like this
class User implements UserInterface
{
// ...
/**
* Many Departments have One Central Department (User)
* #ORM\ManyToOne(targetEntity="User")
*/
private $centralDepartment;
// ...
}
with self-referencing association. In related buildForm I use
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('centralDepartment');
// ...
}
}
and it creates in my view select list with list of Users. It's correct.
But the goal is to show on the list only Users with specific Role. If there is possibility I want also to validate before saving in database if selected User has specific Role.
Should I use option choice_loader from https://symfony.com/doc/3.4/reference/forms/types/choice.html or is there some better option in Symfony? I tried to change at first the label using
->add('centralDepartment', ChoiceType::class, array('label' => 'Choose Central Department'));
But my select list is empty now.
First try using EntityType instead of ChoiceType which is a more specialized ChoiceType for entity relations.
With EntityType you have a query_builder option to select the wanted choices.
See: https://symfony.com/doc/current/reference/forms/types/entity.html#query-builder
This could also be achieved through the choice_loader option with a ChoiceType but requires more work.
Going by #Joe suggestion I used EntityType
->add('centralDepartment', EntityType::class, array(
'class' => 'AppBundle:User',
'query_builder' => function(UserRepository $er) {
return $er->findAllBC(true);
}
));
where findAllBC(true) is my method in UserRepository to return Query Builder object which included specific Users for my use:
public function findAllBC($returnQB = false)
{
$qb = $this->createQueryBuilder('u')
->andWhere('u.roles LIKE :role')
->setParameter('role', '%BC%');
if(!$returnQB) return $qb->getQuery()->execute();
else return $qb;
}
I'm trying by overriding the controller and the formtype, to show the roles from the selected group in my view, but I don't find the right way. I've followed the steps to override everything, that works, but problem comes when I try to say to the service that I'm passing to the constructor an entity object.
As the formtype has to be overridden, how to pass through the service that you need to implement, my Group entity?
Does anyone have an idea of how to achieve that?
Here's what I've done:
Override the controller, and when creating the Form, pass the $group entity
$formFactory = $this->container->get('fos_user.group.form.factory');
$form = $formFactory->createForm($group); //Here
Override the form, and use a custom __construct method where I can pass my entity (maybe here is my error and that should be done in a better or other way)
public function __construct(Container $container, Groups $group)
{
$this->container = $container;
$this->roles = array_keys($this->container->getParameter('security.role_hierarchy.roles'));
$this->group = $group; #How?
}
The container to get the keys for the roles is passed without errors, that works.
Create the service as the documentation says (here comes the real problem and the exceptions)
x_s_cosmos.group.form.type:
class: X\S\CosmosBundle\Form\Type\GroupFormType
arguments: [#service_container, here_should_be_my_entity?]
tags:
- { name: form.type, alias: kosmos_group_form }
I'm really stacked with that and don't have any idea of how to go on.
Finally, after overriding the GroupController.php and adding a choice field type to my form, I could achieve my goal.
$form->add('roles', 'choice', array(
'choices' => $this->getExistingRoles(),
'data' => $group->getRoles(),
'label' => 'Roles',
'expanded' => true,
'multiple' => true,
'mapped' => true,
));
Where getExistingRoles() is:
$roleHierarchy = $this->container->getParameter('security.role_hierarchy.roles');
$roles = array_keys($roleHierarchy);
foreach ($roles as $role) {
$theRoles[$role] = $role;
}
return $theRoles;
I was just going in the wrong way, it was not so difficult to get the roles of a group and show them in an admin interface so that you can choose one of your system roles and give it to the group. No need to override the FormType, just the controller to add your own fields to the form.
Hope it helps, as it has given lot of headache to me.
You should not pass an entity to the constructor. If you need to access entity inside the form, you have to add an event listener to the form builder, like this:
public function buildForm(FormBuilder $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$entity = $event->getData();
// This is your Group entity, now do something with it...
if ($entity instanceof Group) {
// $form->add ...
}
});
}
I have to change a web that is using different types to generate forms. There is in Bundle/Form/ folder 2 files:
ProductType.php
ProductEditType.php
It's working fine, the first one is used to generate the new product form and the second one the form to edit it.
Almost 95% of both files is the same, so I guess it has to exist any way to use one type to generate more than one form.
I have been reading about how to modify forms using form events, but I have not found clearly what is the general good practice about it.
Thanks a lot.
Update
I wrote an Event Subscriber as follows.
<?php
namespace Project\MyBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Description of ProductTypeOptionsSubscriber
*
* #author Javi
*/
class ProductTypeOptionsSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents() {
return array(FormEvents::PRE_SET_DATA => 'preSetData');
}
public function preSetData(FormEvent $event){
$data = $event->getData();
$form = $event->getForm();
if( !$data || !$data->getId() ){
// No ID, it's a new product
//.... some code for other options .....
$form->add('submit','submit',
array(
'label' => 'New Produtc',
'attr' => array('class' => 'btn btn-primary')
));
}else{
// ID exists, generating edit options .....
$form->add('submit','submit',
array(
'label' => 'Update Product',
'attr' => array('class' => 'btn btn-primary')
));
}
}
}
In ProductType, inside buildForm:
$builder->addEventSubscriber(new ProductTypeOptionsSubscriber());
So that's all, it was very easy to write and it works fine.
You can read this cookbook event subscriber, the first scenario can do for you.
Returning to the example of the documentation..
Add the fields that you want them to being modified in this way:
$builder->addEventSubscriber(new AddNameFieldSubscriber());
Then create the event event subscriber by entering your logic:
// src/Acme/DemoBundle/Form/EventListener/AddNameFieldSubscriber.php
namespace Acme\DemoBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class AddNameFieldSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
// Tells the dispatcher that you want to listen on the form.pre_set_data
// event and that the preSetData method should be called.
return array(FormEvents::PRE_SET_DATA => 'preSetData');
}
public function preSetData(FormEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
// check if the product object is "new"
// If you didn't pass any data to the form, the data is "null".
// This should be considered a new "Product"
if (!$data || !$data->getId()) {
$form->add('name', 'text');
....
..... // other fields
}
}
}
I am learning to use Symfony2 and in the documentation I have read, all entities being used with Symfony forms have empty constructors, or none at all. (examples)
http://symfony.com/doc/current/book/index.html Chapter 12
http://symfony.com/doc/current/cookbook/doctrine/registration_form.html
I have parametrized constructors in order to require certain information at time of creation. It seems that Symfony's approach is to leave that enforcement to the validation process, essentially relying on metadata assertions and database constraints to ensure that the object is properly initialized, forgoing constructor constraints to ensure state.
Consider:
Class Employee {
private $id;
private $first;
private $last;
public function __construct($first, $last)
{ .... }
}
...
class DefaultController extends Controller
{
public function newAction(Request $request)
{
$employee = new Employee(); // Obviously not going to work, KABOOM!
$form = $this->createFormBuilder($employee)
->add('last', 'text')
->add('first', 'text')
->add('save', 'submit')
->getForm();
return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
'form' => $form->createView(),
));
}
}
Should I not be using constructor arguments to do this?
Thanks
EDIT : Answered Below
Found a solution:
Looking into the API for the Controllers "createForm()" method I found something that is not obvious from the examples. It seems that the second argument is not necessarily an object:
**Parameters**
string|FormTypeInterface $type The built type of the form
mixed $data The initial data for the form
array $options Options for the form
So rather than pass in an instance of the Entity, you can simply pass in an Array with the appropriate field values:
$data = array(
'first' => 'John',
'last' => 'Doe',
);
$form = $this->createFormBuilder($data)
->add('first','text')
->add('last', 'text')
->getForm();
Another option (which may be better), is to create an empty data set as a default option in your Form Class.
Explanations here and here
class EmployeeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('first');
$builder->add('last');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'empty_data' => new Employee('John', 'Doe'),
));
}
//......
}
class EmployeeFormController extends Controller
{
public function newAction(Request $request)
{
$form = $this->createForm(new EmployeeType());
}
//.........
}
Hope this saves others the head scratching.
I have a CRUD for one of my entities in Symfony2. In order to create a new entry, I have two controller functions:
public function newAction($id) {
$entity = new Clientes();
// Get the reference to the Login entity using its ID
$em = $this->getDoctrine()->getManager();
$ref_login = $em->getReference('LoginBundle:Login', $id);
// Put the retrieved reference to the entity
$entity->setLogin($ref_login);
$form = $this->createForm(new ClientesType(), $entity);
return $this
->render('MovinivelBundle:Persona/Clientes:new.html.twig',
array('entity' => $entity,
'form' => $form->createView(),));
}
public function createAction(Request $request) {
$entity = new Clientes();
$form = $this->createForm(new ClientesType(), $entity);
$form->bind($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('clientes'));
}
return $this
->render('MovinivelBundle:Persona/Clientes:new.html.twig',
array('entity' => $entity,
'form' => $form->createView(),));
}
In the previous code I added the $id input parameter to the newAction() function because I want it to be established from outside, because each of this Clientes is additional info of Login and has to be linked.
In the ClientesType form I have the following:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('login')
->add('direccion')
->add('localidad')
->add('provincia')
->add('telefono')
;
}
So far it works. In my form, the login parameter is chosen depending on the $id value. But the thing is that I want the login parameter to be fixed once the form is created, so the user cannot modify it from the form, but only calling the newAction($id) function with the appropiate value.
The thing is that if I delete the ->add('login') line in the FormType, it doesn't work anymore. It comes to my mind two options:
Hide somehow the 'login' in the form, but keeping it working, although I don't know how, or
pass to the createAction the $id parameter along with the $request one as input parameters, but I cannot figure out how do it either.
Any thoughts on this?
I think you are looking for a hidden field type:
public function buildForm(...)
{
$builder
->add('login', 'hidden')
// ...
;
}
Ok, I came out with the solution. All I was looking for is actually the following:
<div style="display:none">
{{ form_rest(form) }}
</div>
Typing this at the end of the template after having shown explicitly any other form field avoids any user to modify the fields I don't want to, while it still sends the info using the $POST method.