I am looking for the best method for creating/adding dynamic options in a form. By options, I mean things like choice value pairs, or maybe even default values. I can see at least three options:
1) add the options to the $options array when adding the form type. For this, it appears that I must first declare a default value and then add them in the add method and in the controller:
controller:
$choices = [];
foreach ($pages as $page) {
$choices[$page->getId()] = $page->getTitle();
}
$options = ['pages' => $choices];
$form = $this->createForm('MyBundle\Form\Type\PageType', $data, $options);
FormType:
class PageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('pid', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', [
'choices' => $options['pages'],
'label' => __('Page')
]);
}
...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'pages' => []
]);
}
}
2) If the values are not dependent on controller values, it seems I could create them in the OptionsResolver (assuming access to the source data)
FormType:
class PageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('pid', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', [
'choices' => $options['pages'],
'label' => __('Page')
]);
}
...
public function configureOptions(OptionsResolver $resolver)
{
$choices = [];
$pages = $this->getPages();
foreach ($pages as $page) {
$choices[$page->getId()] = $page->getTitle();
}
$resolver->setDefaults([
'pages' => $choices
]);
}
3) Finally, I can also add in the buildForm method (again assuming access to source data):
FormType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [];
$pages = $this->getPages();
foreach ($pages as $page) {
$choices[$page->getId()] = $page->getTitle();
}
$builder
->add('pid', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', [
'choices' => $choices,
'label' => __('Page')
]);
}
Obviously, there is the most flexibility in the first option, but If I do not require that flexibility, or do not want to manage the options in the controller for some reason, does it make more sense to do the work in the buildForm or configureOptions methods?
If you require flexibility you can't use solution 3. But if you want to avoid flexibility, solution 3 is the best.
Solution 1 and 2 are OK, it really depend of what you need :
If you use your form in several actions with different choices: use solution 1, but add a requirement on this option to prevent the form to be called without choices
If your choices are often the same, but you want to override them only sometimes: chose solution 2
Personally I prefer the solution 1, because it's always better if your form relies on the less possible external objects ($this->pages in your example).
Regards
If you work with Doctrine Entities, you should use this:
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
// ...
$builder->add('pid', EntityType::class, array(
'class' => 'AppBundle:Page',
'choice_label' => 'title',
));
For working with another type of objects this one:
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use AppBundle\Entity\Page;
// ...
$builder->add('pid', ChoiceType::class, [
'choices' => [
new Page('Page 1'),
new Page('Page 2'),
new Page('Page 3'),
new Page('Page 4'),
],
'choices_as_values' => true,
'choice_label' => function($page, $key, $index) {
/** #var Page $page */
return $page->getTitle();
}
]);
More information you can read in blog post here.
Related
I try to create form with html select element using EntityType. I must get values by some condition and by this condition necessary default value not select from database. So i get all options values, without one, that must be a default value. So i try to find a way to put this value to select. What i tried...
Set value in form:
$growing = $em->getRepository('FarmBundle:Growing')->findGrowing($user_id);
$garden = $em->getRepository('FarmBundle:Garden')->find(7);
$tableForm = $this->createForm('FarmBundle\Form\GrowingType', $growing, ['user_id' => $user_id]);
$tableForm->get('garden')->setData($garden);
$form = $tableForm->createView()
Then i tried to set data in entity:
$growing = $em->getRepository('FarmBundle:Growing')->findGrowing($user_id);
$garden = $em->getRepository('FarmBundle:Garden')->find(7);
$growing->setGarden($garden);
$tableForm = $this->createForm('FarmBundle\Form\GrowingType', $growing, ['user_id' => $user_id]);
$form = $tableForm->createView()
Then i tried to set default select value in form_builder using 'data' attribute:
$growing = $em->getRepository('FarmBundle:Growing')->findGrowing($user_id);
$garden = $em->getRepository('FarmBundle:Garden')->find(7);
$tableForm = $this->createForm('FarmBundle\Form\GrowingType', $grow, [
'user_id' => $user_id,
'selected_choice' => $garden
]);
$form = $tableForm->createView();
Form_builder code:
class GrowingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id', HiddenType::class)
->add('garden', EntityType::class , [
'class' => 'FarmBundle\Entity\Garden',
'query_builder' => function (GardenRepository $gr) use ($options) {
return $gr->queryFreeGardens($options['user_id']);
},
'attr' => [
'data-type' => 'text',
'class' => 'table-select',
'disabled' => true
],
'required' => false,
'data' => $options['selected_choice']
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'FarmBundle\Entity\Growing',
'selected_choice' => null,
'user_id' => null
));
}
}
And code of query for query builder:
class GardenRepository extends \Doctrine\ORM\EntityRepository
{
public function queryFreeGardens($user_id)
{
$qb = $this->createQueryBuilder('g')
->leftJoin('g.growing', 'grow')
->where('grow.plantDate is NULL')
->orWhere('grow.endDate is not NULL')
->andWhere('g.user = :user_id')
->orderBy('g.name')
->setParameter('user_id', $user_id);
return $qb;
}
}
And all of this 3 methods not works. Result is one, if entity not get for query in query builder, i cant set this entity. If i will set entity as default value, that was in query builder all will works fine.
How can i solve this problem?
try this
in controller:
$growing = $em->getRepository('FarmBundle:Growing')->findGrowing($user_id);
$garden = $em->getRepository('FarmBundle:Garden')->find(7);
$tableForm = $this->createForm('FarmBundle\Form\GrowingType', $grow, [
'user_id' => $user_id,
'additional_id' => 7
]);
$form = $tableForm->createView();
in form:
class GrowingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id', HiddenType::class)
->add('garden', EntityType::class , [
'class' => 'FarmBundle\Entity\Garden',
'query_builder' => function (GardenRepository $gr) use ($options) {
return $gr->queryFreeGardens($options['user_id'], $options['additional_id');
},
'attr' => [
'data-type' => 'text',
'class' => 'table-select',
'disabled' => true
],
'required' => false
]);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'FarmBundle\Entity\Growing',
'additional_id' => null,
'user_id' => null
));
}
}
in repository:
class GardenRepository extends \Doctrine\ORM\EntityRepository
{
public function queryFreeGardens($user_id, $additional_id)
{
$qb = $this->createQueryBuilder('g')
->leftJoin('g.growing', 'grow')
->where('grow.plantDate is NULL')
->andWhere('g.id = :additional_id')
->orWhere('grow.endDate is not NULL')
->andWhere('g.user = :user_id')
->andWhere('g.id = :additional_id')
->orderBy('g.name')
->setParameter('user_id', $user_id)->setParameter('additional_id', $additional_id);
return $qb;
}
}
maybe you will need to adjust your repository method to retrieve values in right way. There are or clause, you should add this additional id to both branches of your or clause. The main idea is to retrieve you selected object too.
Trying to make a form that a user chooses an option and depending on their choice loads additional fields. So far I have a UserSignupType:
class UserSignupType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('userType', ChoiceType::class, array(
'choices' => array(
"Subscriber" => "Subscriber",
"Friend" => "Friend"
),
'expanded' => true,
'mapped' => false
));
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) {
$form = $event->getForm();
$usertype = $form->get('userType')->getData(); //updated per JBaffords answer
if($userType == "Subscriber")
{
$builder->add('agency', EntityType::class, array(
"class" => "\AppBundle\Entity\Agency",
"label" => "name"));
}
elseif($userType == "Friend")
{
$builder->add('phoneNumber', PhoneNumberType::class, array(
'default_region' => 'US',
'format' => PhoneNumberFormat::NATIONAL));
}
}
);
}
// ...
}
not sure if the getData method is the right method to use, and if it is, i need to somehow get the "userType" field out of it. I cant call getUserType because its not an actual mapped property and I don't want it to be. It simply decides the fields to show.
You can get the value for any form element (mapped or unmapped) by doing:
$form->get('fieldName')->getData();
get() returns a Form object, so if you have a nested form, you can continue to call ->get('nextFieldName') on each child until you get to the form element you need.
The value returned from getData for a form is going to depend on (amont other things) the mapping of its child elements. If the form has no children, then its value is its value; the mapping just determines whether that value is populated into its parent's data.
In your specific case, to get the data for the userType element, you would do:
$userType = $form->get('userType')->getData();
I have managed to override the template and the form for the registration page but one thing I have noticed is that the initial fields are being injected.
Here is my buildform Code:
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('firstname', 'text', array('label' => 'First Name'))
->add('lastname', 'text', array('label' => 'Last Name'))
->add('over_18', 'checkbox', array('label' => 'Yes I am over 18', 'mapped' => false))
->add('email', 'text', array('label' => 'Email'))
->add('phone', 'text', array('label' => 'What is your telephone number?'))
->add('password', 'password', array('label' => 'Choose a password'))
;
}
When I do a {{ dump(form) }} in the template I get this:
I understand I can do $builder->remove() on those fields, but if I am overwriting the form should I really need to do this?
Thanks
Yes. This is the only way. Because the buildForm() method is chained to parent buildForm(). That's how the form parent-child dependency works in symfony2:
/**
* #return string
*/
public function getParent()
{
return 'fos_user_registration';
}
Not sure if this is the info you are looking for, but when using FOSUser and Sonata User, you can simply extend parent::buildForm() and then add your custom fields like this:
// src/Application/Sonata/UserBundle/Form/Type/RegisterType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
// call to FOSUser buildForm, brings default fields
parent::buildForm($builder, $options);
$builder
->add('phone','text',array('label' => 'phone','required'=>true))
;
//do $builder->remove(..) if necessary
}
I'm using a jQuery plugin that takes the text from labels associated with form elements and puts them as default text for the fields themselves. (You can find the plugin here.)
Here's the catch: it can only do this if the label has the class "inline". Now, I know I can use the following code to do this:
$this->add(array (
'name' -> 'name',
....
'options' => array (
'label' => 'Name',
'label_attributes' => array (
'class' => 'inline'
)
)
));
This will work fine, and if it has to be done item by item, then so be it. But I was wondering if there's some way I can add the class to ALL labels associated with text and text area form elements without using JavaScript. I'm thinking this would either done by a plugin, or by looping through all the elements in the form, but I don't know how to do either.
You could extend the FormRow view helper.
Here is a little example:
use Zend\Form\View\Helper\AbstractHelper;
use Zend\Form\View\Helper\FormRow;
class CustomFormRow extends FormRow
{
public function render(ElementInterface $element) {
...
$label = $element->getLabel();
if (isset($label) && '' !== $label) {
// Translate the label
if (null !== ($translator = $this->getTranslator())) {
$label = $translator->translate(
$label, $this->getTranslatorTextDomain()
);
}
$label->setAttribute('class', 'inline');
}
...
if ($this->partial) {
$vars = array(
'element' => $element,
'label' => $label,
'labelAttributes' => $this->labelAttributes,
'labelPosition' => $this->labelPosition,
'renderErrors' => $this->renderErrors,
);
return $this->view->render($this->partial, $vars);
}
...
}
You could probably leave the rest as it is and you should be good to go once you add some configuration in your Module.php for your view helper.
public function getViewHelperConfig() {
return array(
'factories' => array(
'CustomFormRow' => function($sm) {
return new \Application\View\Helper\CustomFormRow;
},
)
);
}
In your template files you now have to use your viewHelper instead.
<?php echo $this->CustomFormRow($form->get('yourelement')); ?>
My form looks like this:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$factory = $builder->getFormFactory();
$builder->add('name');
$builder->add('description');
$builder->add('manufacturers', null, array(
'required' => false
));
$builder->add('departments', 'collection', array(
'type' => new Department
));
}
I have a class validator on the entity the form represents which calls:
if (!$valid) {
$this->context->addViolationAtSubPath('departments', $constraint->message);
}
Which will only add a 'global' error to the form, not an error at the sub path. I assume this is because departments is a collection embedding another FormType.
If I changed departments to one of the other fields it works fine.
How can I get this error to appear in the right place? I assume it would work fine if my error was on a single entity within the collection, and thus rendered in the child form, but my criteria is that the violation occur if none of the entities in the collection are marked as active, thus it needs to be at the parent level.
By default, forms have the option "error_bubbling" set to true, which causes the behavior you just described. You can turn off this option for individual forms if you want them to keep their errors.
$builder->add('departments', 'collection', array(
'type' => new Department,
'error_bubbling' => false,
));
I have been wrestling with this issue in Symfony 3.3, where I wished to validate an entire collection, but pass the error to the appropriate collection element/field. The collection is added to the form thus:
$form->add('grades', CollectionType::class,
[
'label' => 'student.grades.label',
'allow_add' => true,
'allow_delete' => true,
'entry_type' => StudentGradeType::class,
'attr' => [
'class' => 'gradeList',
'help' => 'student.grades.help',
],
'entry_options' => [
'systemYear' => $form->getConfig()->getOption('systemYear'),
],
'constraints' => [
new Grades(),
],
]
);
The StudentGradeType is:
<?php
namespace Busybee\Management\GradeBundle\Form;
use Busybee\Core\CalendarBundle\Entity\Grade;
use Busybee\Core\SecurityBundle\Form\DataTransformer\EntityToStringTransformer;
use Busybee\Core\TemplateBundle\Type\SettingChoiceType;
use Busybee\Management\GradeBundle\Entity\StudentGrade;
use Busybee\People\StudentBundle\Entity\Student;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class StudentGradeType extends AbstractType
{
/**
* #var ObjectManager
*/
private $om;
/**
* StaffType constructor.
*
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('status', SettingChoiceType::class,
[
'setting_name' => 'student.enrolment.status',
'label' => 'grades.label.status',
'placeholder' => 'grades.placeholder.status',
'attr' => [
'help' => 'grades.help.status',
],
]
)
->add('student', HiddenType::class)
->add('grade', EntityType::class,
[
'class' => Grade::class,
'choice_label' => 'gradeYear',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('g')
->orderBy('g.year', 'DESC')
->addOrderBy('g.sequence', 'ASC');
},
'placeholder' => 'grades.placeholder.grade',
'label' => 'grades.label.grade',
'attr' => [
'help' => 'grades.help.grade',
],
]
);
$builder->get('student')->addModelTransformer(new EntityToStringTransformer($this->om, Student::class));
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefaults(
[
'data_class' => StudentGrade::class,
'translation_domain' => 'BusybeeStudentBundle',
'systemYear' => null,
'error_bubbling' => true,
]
);
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'grade_by_student';
}
}
and the validator looks like:
namespace Busybee\Management\GradeBundle\Validator\Constraints;
use Busybee\Core\CalendarBundle\Entity\Year;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class GradesValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (empty($value))
return;
$current = 0;
$year = [];
foreach ($value->toArray() as $q=>$grade)
{
if (empty($grade->getStudent()) || empty($grade->getGrade()))
{
$this->context->buildViolation('student.grades.empty')
->addViolation();
return $value;
}
if ($grade->getStatus() === 'Current')
{
$current++;
if ($current > 1)
{
$this->context->buildViolation('student.grades.current')
->atPath('['.strval($q).']') // could do a single atPath with a value of "[".strval($q)."].status"
->atPath('status') // full path = children['grades'].data[1].status
->addViolation();
return $value;
}
}
$gy = $grade->getGradeYear();
if (! is_null($gy))
{
$year[$gy] = empty($year[$gy]) ? 1 : $year[$gy] + 1 ;
if ($year[$gy] > 1)
{
$this->context->buildViolation('student.grades.year')
->atPath('['.strval($q).']')
->atPath('grade')
->addViolation();
return $value;
}
}
}
}
}
This results in the error being added to the field in the element of the collection as per the attach image.
Craig
I have a case very similar. I have a CollectionType with a Custom Form (with DataTransformers inside, etc...), i need check one by one the elements and mark what of them is wrong and print it on the view.
I make that solution at the ConstraintValidator (my custom validator):
The validator must target to CLASS_CONSTRAINT to work or the propertyPath doesnt work.
public function validate($value, Constraint $constraint) {
/** #var Form $form */
$form = $this->context->getRoot();
$studentsForm = $form->get("students"); //CollectionType's name in the root Type
$rootPath = $studentsForm->getPropertyPath()->getElement(0);
/** #var Form $studentForm */
foreach($studentsForm as $studentForm){
//Iterate over the items in the collection type
$studentPath = $studentForm->getPropertyPath()->getElement(0);
//Get the data typed on the item (in my case, it use an DataTransformer and i can get an User object from the child TextType)
/** #var User $user */
$user = $studentForm->getData();
//Validate your data
$email = $user->getEmail();
$user = $userRepository->findByEmailAndCentro($email, $centro);
if(!$user){
//If your data is wrong build the violation from the propertyPath getted from the item Type
$this->context->buildViolation($constraint->message)
->atPath($rootPath)
->atPath(sprintf("[%s]", $studentPath))
->atPath("email") //That last is the name property on the item Type
->addViolation();
}
}
}
Just i validate agains the form elements in the collection and build the violation using the propertyPath from the item in the collection that is wrong.