Can you 'extend' form classes? - php

I am creating form classes for my forms, but cannot figure out how to 'extend' them.
For example, I have a CustomerType form class, and an EmailType form class. I could add the EmailType directly into my CustomerType
$builder->add('emails', 'collection', array(
'type' => new EmailType(),
'allow_add' => true,
'by_reference' => false
));
but I'd prefer to do this in the controller, so that my CustomerType form class contains only customer information. I feel this is more modular and reusable, since sometimes I want my user to be able to edit only Customer details, and others both Customer details as well as Email objects associated with that customer. (For example, in the first case when viewing a customer's work order, and in the second when creating a new customer).
Is this possible? I'm thinking something along the lines of
$form = $this->createForm(new CustomerType(), $customer);
$form->add('emails', 'collection', ...)
in my controller.

You could pass an option (say "with_email_edition") to your form when it's created that would tell if the form should embed the collection or not.
In the Controller:
$form = $this->createForm( new CustomerType(), $customerEntity, array('with_email_edition' => true) );
In the form:
Just add the option in the setDefaultOptions:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'with_email_edition' => null,
))
->setAllowedValues(array(
'with_email_edition' => array(true, false),
));
}
and then check in the "buildForm" the value of this option,and add a field based on it:
public function buildForm(FormBuilderInterface $builder, array $options)
{
if( array_key_exists("with_email_edition", $options) && $options['with_email_edition'] === true )
{
//Add a specific field with $builder->add for example
}
}

Related

PHP silex Formbuilder and object with array of object

I am using PHP with the Silex framework and after some hours trying to find a satisfying solution I am still blocked with the following problem regarding forms and objects containing array of Objects. The hours spent permitted me to find a working solution but I hope there is a better way to do that with Silex.
In my application I have a User class that is defined like :
class User implements UserInterface
{
private $id;
private $username;
private $displayname;
private $capacities;
//...
}
$capacities variable contains an array of objects from another class (Capacity). A Capacity is a specific role in my app with various information (label, place of the capacity ...) and I have added, in that Capacity class a boolean telling if the Capacity is active for a specific user, when attached to a user via the $capacities array.
At the moment I am able to create the form that looks as I want with the following code :
use Planning\Domain\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username', TextType::class, array(
'required' => true,
'label' => "Login (pour Jean Durant → jdurant)"
))
->add('displayname', TextType::class, array(
'label' => "Nom à afficher"
));
$choices = array();
$choicesActive = array();
foreach ($builder->getData()->getCapacities() as $id => $capacity) {
$choices[$capacity->getLabel()] = $capacity->getId();
if ($capacity->getActive()) {
$choicesActive[] = $capacity->getId();
}
}
$builder->add('capacities', ChoiceType::class, array(
'choices' => $choices,
'data' => $choicesActive,
'label' => "Groupes",
'multiple' => True,
'expanded' => True
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => User::class,
));
}
public function getName()
{
return 'capacity';
}
}
but the User object I am getting after the form is validated contains for capacities :
[capacities:Planning\Domain\User:private] => Array
(
[0] => 2
[1] => 4
[2] => 1
)
capacities variable contains the list of values for checkboxes that have been checked in the form. The issue is that my User object is not consistant with its definition which says that the capacities property should be an array of Capacity objects. What I am doing at the moment is that I have added the following code to my controller when $userForm->isSubmitted() and $userForm->isValid() :
// Getting the array from the returned user, should contain Capacity object but just contains the IDs.
$capacitiesChecked = $userForm->getData()->getCapacities();
// We regenerate the full array of capacities for this user
$user->setCapacities($app["dao.capacity"]->findAll());
// Then we will activate capacities that have been checked, one by one
foreach ($capacitiesChecked as $capacityChecked) {
$user->getCapacityById($capacityChecked)->setActive(True);
}
This is working and I am happy with it but being new to Silex and the framework world, I am surprized that I have not been able to find an easier way to answer my problem which I believe should be quite common.
I might be missing something from the Silex/Symfony philosophy and I hope someone there will be able to point me to the correct place to get more information or find a solution!
Edit following #dbrumann answer
As it might not be clear how my data is organized, here are the tables in my database :
user
id
username
displayname
capacity
id
label
place
user_capacity
id_user
id_capacity
There might be an issue with the modeling of my project but I have a Capacity class and a User class and User has an attribute with an array containing all the Capacity available in the database and each one of this Capacity object has an active attribute that is set to True if there is an entry in the table user_capacity that links user and capacity. What I would like is ONE form that allows me to properly update data into tables user and user_capacity.
If I understand correctly Capacity is another entity that you want to link. In that case you could use the more specific EntityType and then filter the number of values by using the query_builder attribute as described in the documentation:
$builder->add('users', EntityType::class, array(
'class' => Capacity::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('capacity')
->where('capacity.active = 1')
->orderBy('capacity.id', 'ASC');
},
'choice_label' => 'id',
));
I have found a solution that I think is acceptable and might be useful for someone landing on this page. So what I have done is that I have changed my User class so that the $capacities attribute now contains only Capacity objects that are related to the user. But to get all the capacities available on the form, I am passing them as an option (allCapacities) and iterating over them to find which one are present in User->capacities to check them in the form.
The updated class used to build the form is as following:
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username', TextType::class, array(
'required' => true,
'label' => "Login (pour Jean Durant → jdurant)"
))
->add('displayname', TextType::class, array(
'label' => "Nom à afficher"
));
$choices = array();
$choicesActive = array();
foreach ($options["allCapacities"] as $id => $capacity) {
$choices[] = $capacity;
if ($builder->getData()->hasCapacity($capacity)) {
$choicesActive[] = $capacity;
}
}
$builder->add('capacities', ChoiceType::class, array(
'choices' => $choices,
'data' => $choicesActive,
'choice_label' => function($category, $key, $index) {
return $category->getLabel();
},
'label' => "Groupes",
'multiple' => True,
'expanded' => True,
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => User::class,
));
$resolver->setRequired(array(
'allCapacities',
));
}
public function getName()
{
return 'capacity';
}
}
This is working as expected and does seem to be overcomplicated but there might be easier solution by changing the design, any comment on this would then be welcome!

Form - Accessing submitted/bound data

Imagine this form
class CarType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('brand', EntityType::class, array(
'class' => 'AppBundle:CarBrand',
'label' => 'Brand',
))
->add('model', EntityType::class, array(
'class' => 'AppBundle:CarModel',
'label' => 'Car model',
'choices' => array(), // <- Depends on brand
));
// ...
}
}
I used some ajax to make the model list depending on brand selection.
I didn't use FormEvents because it was a real pain in the ass for my simple need (I have actually 3 dependencies based on multiple parameters and I already spent an entire day trying to figure out how to make it work with FormEvents)
Now, once the form is submitted, I want to get the bound values from my controller.
/**
* #Route("/context", name="context_selection")
*/
public function carSelectionAction(Request $request)
{
$form = $this->createForm(CarType::class, new Car());
$form->handleRequest($request);
if ($form->isSubmitted()) {
$car = $form->getData();
dump($car); // <- Getting only brand here
}
return $this->render('AppBundle::car-selection.html.twig', array(
'form' => $form->createView()));
}
The dumped data is showing only the CarBrand object bound, I imagine because the selected CarModel is not a valid choice.
How can I get the bound CarModel ?
I'd rather not having to read the request params and load the objects myself

Modifying field options using FormTypeExtensions

I know there's no clean way to do this after the form has been built using FormEvents however is there a way to mainpulate the options passed to a form using FormTypeExtensionInterface::buildForm before it has been completely built?
e.g: I will use this to set multiple options to specific values when another option is set in the form e.g: when the option "helper" is set true set the "label" option to "helper" and set "disabled" option to true
So what you can do is pass the option to your form when you create it. For example, in your controller:
$form = $this->createForm(new YourFormType(), null, array('helper' => true));
Then in your buildForm function:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('myfield', null, array(
'label' => ($options['helper']) ? 'helper' : 'mylabel',
'disabled' => ($options['helper']) ? true : false,
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'helper' => false,
));
}
The only thing is, this is a global option for the entire form. Did you mean that you want this option for every individual field?

Issues with form inheritance

I have a PersonType form and then I have LegalPersonType and NaturalPersonType forms and both extends from PersonType since they have a common field on that form (mapped at Entity level). For example, this is the code for NaturalPersonType.php
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Tanane\FrontendBundle\DBAL\Types\CIType;
use Tanane\FrontendBundle\Form\Type\PersonType;
class NaturalPersonType extends PersonType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('identification_type', 'choice', array(
'label' => 'Número de Cédula',
'choices' => CIType::getChoices()
))
->add('ci', 'number', array(
'required' => true,
'label' => false,
'attr' => array(
'maxlength' => 8,
))
)
->add('lives_in_ccs', 'checkbox', array(
'label' => false,
'required' => false,
'value' => 1,
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\NaturalPerson'
));
}
public function getName()
{
return 'natural_person';
}
}
Then at SaveFormController/orderAction() I'm doing this:
$order = new Orders();
$orderForm = $this->createForm(new OrdersType(array($type)), $order, array('action' => $this->generateUrl('save_order')));
But any time I try to render the form I get this error:
Neither the property "nat" nor one of the methods "getNat()", "nat()",
"isNat()", "hasNat()", "__get()" exist and have public access in class
"Tanane\FrontendBundle\Entity\Orders".
Relationship are at Entity level, how I fix that error?
Thanks in advance
1st possible solution
Following suggestions from user here I change, in OrderType.php Form my code to this:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Tanane\FrontendBundle\DBAL\Types\SFType;
class OrdersType extends AbstractType {
/**
* #var string
*/
protected $register_type;
public function __construct($register_type)
{
$this->register_type = $register_type;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// here goes $builder with default options remove for see less code
if ($this->register_type[0] == "natural")
{
$builder->add('nat', new NaturalPersonType(), array(
'data_class' => 'Tanane\FrontendBundle\Entity\NaturalPerson'
));
}
elseif ($this->register_type[0] == "legal")
{
$builder->add('leg', new LegalPersonType(), array(
'data_class' => 'Tanane\FrontendBundle\Entity\LegalPerson'
));
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\Orders',
'render_fieldset' => FALSE,
'show_legend' => FALSE
));
}
public function getName()
{
return 'orders';
}
}
I've fixed by adding 'mapped' => FALSE on each new FormType I add in OrdersType but I don't know if this is the right. Also, if I'm defining the data_class here, and NaturalType will never be access directly just trough OrdersType should I remove the default options from that form or should I leave them there? How can I fix the problem now? What I'm missing?
This is not a direct answer to your question but maybe could solve some problem before that happens...
I don't remember to have seen it's possible to extend a form like this instead of extend AbstractType, but as explained in the docs, if you have common fields to share between different types of forms you should use the native framework modularity offered by inherit_data.
If you need something more specific (some special methods to execute on some field) you can create a new field type or extend an existing one using AbstractTypeExtension.
EDIT:
I don't know exactly why you are using this approach (that I never used in my projects) but IMO PersonType, NaturalPersonType and LegalPersonType should be only "FormType/FieldType" initialized with inherit_data (and not entities like in your code) that contains the fields related to their use, while OrdersType should be composed with the block of forms needed to the type of person who fills it and with data_class setted on the UNIQUE entity that store the data outputted by the form.

Set Default value of choice field Symfony FormType

I want from the user to select a type of questionnaire, so I set a select that contains questionnaires types.
Types are loaded from a an entity QuestionType .
$builder
->add('questionType', 'entity', array(
'class' => 'QuizmooQuestionnaireBundle:QuestionType',
'property' => 'questionTypeName',
'multiple' => false,
'label' => 'Question Type'))
->add('type', 'hidden')
;
What am not able to achieve is to set a default value to the resulted select.
I have googled a lot but I got only preferred_choice solution which works only with arrays
I made it by setting a type in the newAction of my Controller I will get the seted type as default value.
public function newAction($id)
{
$entity = new RankingQuestion();
//getting values form database
$em = $this->getDoctrine()->getManager();
$type = $em->getRepository('QuizmooQuestionnaireBundle:QuestionType')->findBy(array('name'=>'Ranking Question'));
$entity->setQuestionType($type); // <- default value is set here
// Now in this form the default value for the select input will be 'Ranking Question'
$form = $this->createForm(new RankingQuestionType(), $entity);
return $this->render('QuizmooQuestionnaireBundle:RankingQuestion:new.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
'id_questionnaire' =>$id
));
}
You can use data attribute if you have a constant default value (http://symfony.com/doc/current/reference/forms/types/form.html)
but it wont be helpful if you are using the form to edit the entity ( not to create a new one )
If you are using the entity results to create a select menu then you can use preferred_choices.
The preferred choice(s) will be rendered at the top of the list as it says on the docs and so the first will technically be the default providing you don't add an empty value.
class MyFormType extends AbstractType{
public function __construct($foo){
$this->foo = $foo;
}
$builder
->add('questionType', 'entity', array(
'class' => 'QuizmooQuestionnaireBundle:QuestionType',
'property' => 'questionTypeName',
'multiple' => false,
'label' => 'Question Type'
'data' => $this->foo))
->add('type', 'hidden')
;
}
In controller
$this->createForm(new MyFormType($foo));
The accepted answer of setting in the model beforehand is a good one. However, I had a situation where I needed a default value for a certain field of each object in a collection type. The collection has the allow_add and allow_remove options enabled, so I can't pre-instantiate the values in the collection because I don't know how many objects the client will request. So I used the empty_data option with the primary key of the desired default object, like so:
class MyChildType
extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('optionalField', 'entity', array(
'class' => 'MyBundle:MyEntity',
// Symfony appears to convert this ID into the entity correctly!
'empty_data' => MyEntity::DEFAULT_ID,
'required' => false,
));
}
}
class MyParentType
extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('children', 'collection', array(
'type' => new MyChildType(),
'allow_add' => true
'allow_delete' => true,
'prototype' => true, // client can add as many as it wants
));
}
}
Set a default value on the member variable inside your entity (QuestionType), e.g.
/**
* default the numOfCourses to 10
*
* #var integer
*/
private $numCourses = 10;

Categories