PHP silex Formbuilder and object with array of object - php

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!

Related

Selected null-value for 1:N Doctrine relation in EntityType-field

I got incidents which have a reference to a risk.
I am using Symfony 2.6 and Doctrine ORM entities with the following rather symbolic entities:
class Incident
{
private $id;
private $name;
private $date;
/**
* #ORM\ManyToOne(targetEntity="Risk")
* #ORM\JoinColumn(name="risk_id")
*/
private $risk;
}
class Risk
{
private $id;
private $level;
private $name;
}
The risks-Table has data like:
id level name
1 0 none
2 2 low
3 1 very low
4 5 moderate
5 10 high
For the incidents I got a form-type like this:
class IncidentType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
[...]
->add('risk', 'entity', [
'class' => 'Model:Risk',
'property' => 'name',
'empty_data' => null,
'empty_value' => 'not assessed',
'expanded' => true,
'required' => [true|false](see below),
])
[...]
}
So my incidents risk can be 'not assessed'. That is when there is no risk set (=null). To keep my model clean I would not want to have a row with a level of -1 and a name of 'not assessed'.
My form works as expected, missing one tiny detail: when the risk is null, my list of radiobuttons has no value selected. This happens regardless of the value of the 'required'-option.
For a null-value I get:
( ) not assessed
( ) none
( ) very low
[...]
I would want:
(x) not assessed
( ) none
( ) very low
[...]
I guess that behaviour is just fine for a (not expanded) drop-down field.
Is there a way to have the 'placeholder'/'empty_value'-option of my radiobuttons selected when the data-value is null?
For your risk field you should do:
class IncidentType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
[...]
->add('risk', 'entity', [
'class' => 'Model:Risk',
'property' => 'name',
'empty_data' => null,
'empty_value' => 'not assessed',
'expanded' => true,
'required' => true
])
[...]
}
}
I just checked that and it works, the first value is selected when the risk is empty and when using the above code. Note I added 'empty_data' => null, 'empty_value' => 'not assessed', and removed placeholder.
that works but is not a good solution... what would happens if your first value is dynamic?

Implementation of a search form with Symfony2 Form Component

Maybe i have a very dumb question, but i'm new in Symfony2 and i was wondering if i can build a Search Form with Symfony Form Component, the same way I do with a Registration Form for example.
My Search form will have a Country select field, a Club select field, a Gender radio buttons field a Level select field and the submit button.
Is it possible to do that with the Form Component or to do something like this is better to just build the search form directly in the view?
I've been searching for information about this, but I didn't find anything.
Here is how my SearchPlayerType.php looks like.
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvents;
use Doctrine\ORM\EntityRepository;
class SearchPlayersType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('country', 'entity', array(
'placeholder' => 'Choose a country',
'class' => 'AppBundle:Country',
'property' => 'name',
'query_builder' => function(EntityRepository $er){
return $er->createQueryBuilder('c')->orderBy('c.name', 'ASC');
},
))
->add('club', 'entity', array(
'placeholder' => 'Choose a club',
'class' => 'AppBundle:Club',
'property' => 'name',
'query_builder' => function(EntityRepository $er){
return $er->createQueryBuilder('c')->orderBy('c.name', 'ASC');
},
))
->add('gender', 'entity', array(
'class' => 'AppBundle:Gender',
'property' => 'name',
'expanded' => true
))
->add('level', 'rating', array(
'label' => 'Playing Level',
'stars' => 5))
;
}
public function getName()
{
return 'SearchPlayer';
}
If it is possible to do it this way I don´t know what Entity my data_class needs to be
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Usuario'
));
}
}
?>
You can go two ways with this: you can either create a form specific model for your search form which will either be a really thin object with public properties; or you can remove the entry for data_class in your form options which will switch your form into returning an array rather than an object (documentation).
The former is the more OO way of doing things and allows you to add validation annotations without embedding those within the form, this way also means you can add getters and setters that transform your search data very easily and not cluttering up your controllers. So your model would look something like:
namespace MyBundle\Form\Model;
class SearchModel
{
public $country;
public $club;
// ...
}
The alternate is just:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
// don't set 'data_class' in here
));
}
Then when you do $form->getData() you'll just get an array back rather than an object.

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;

Can you 'extend' form classes?

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
}
}

Categories