Buy

Form Options & Variables: Dream Team

We now know that these form variables kick butt, and we know how to override them from inside a template. But, could we also control these from inside of our form class?

Earlier, I mentioned that the options for a field are totally different than the variables for a field. Occasionally, a field has an option - like placeholder - and a variable with the same name, but that's not always true. But clearly, there must be some connection between options and variables. So what is it?!

Form Type Classes & Options

First, behind every field type is a class. Obviously, for the subFamily field, the class behind this is EntityType:

50 lines src/AppBundle/Form/GenusFormType.php
... lines 1 - 6
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
... lines 8 - 13
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('subFamily', EntityType::class, [
... lines 21 - 25
])
... lines 27 - 39
;
}
... lines 42 - 48
}

name is a text type, so the class behind it, is, well, TextType. I'll use the Shift+Shift shortcut in my editor to open the TextType file, from the Symfony Form component:

69 lines vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/TextType.php
... lines 1 - 11
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TextType extends AbstractType implements DataTransformerInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// When empty_data is explicitly set to an empty string,
// a string should always be returned when NULL is submitted
// This gives more control and thus helps preventing some issues
// with PHP 7 which allows type hinting strings in functions
// See https://github.com/symfony/symfony/issues/5906#issuecomment-203189375
if ('' === $options['empty_data']) {
$builder->addViewTransformer($this);
}
}
... lines 32 - 35
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'compound' => false,
));
}
... lines 42 - 45
public function getBlockPrefix()
{
return 'text';
}
... lines 50 - 53
public function transform($data)
{
// Model data should not be transformed
return $data;
}
... lines 59 - 63
public function reverseTransform($data)
{
return null === $data ? '' : $data;
}
}

Now, unlike variables, there is a specific set of valid options for a field. If you pass an option that doesn't exist, Symfony will scream at you. The valid options for a field are determined by this configureOptions() method:

69 lines vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/TextType.php
... lines 1 - 16
use Symfony\Component\OptionsResolver\OptionsResolver;
class TextType extends AbstractType implements DataTransformerInterface
{
... lines 21 - 35
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'compound' => false,
));
}
... lines 42 - 67
}

Apparently the TextType has a compound option, and it defaults to false.

Form Type Inheritance

Earlier, when we talked about form theme blocks, I mentioned that the field types have a built-in inheritance system. Well, technically, TextType extends AbstractType, but behind-the-scenes, the TextType's parent type is FormType. In fact, every field ultimately inherits options from FormType. Open that class:

195 lines vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/FormType.php
... lines 1 - 11
namespace Symfony\Component\Form\Extension\Core\Type;
... lines 13 - 21
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class FormType extends BaseType
{
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
public function __construct(PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
... lines 36 - 193
}

Tip

Wondering how you would know what the "parent" type of a field is? Each *Type class has a getParent() method that will tell you. If you don't see one, then it's defaulting to FormType.

This is cool because it also has a configureOptions() method that adds a bunch of options:

195 lines vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/FormType.php
... lines 1 - 24
class FormType extends BaseType
{
... lines 27 - 120
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
// Derive "data_class" option from passed "data" object
$dataClass = function (Options $options) {
return isset($options['data']) && is_object($options['data']) ? get_class($options['data']) : null;
};
// Derive "empty_data" closure from "data_class" option
$emptyData = function (Options $options) {
$class = $options['data_class'];
if (null !== $class) {
return function (FormInterface $form) use ($class) {
return $form->isEmpty() && !$form->isRequired() ? null : new $class();
};
}
return function (FormInterface $form) {
return $form->getConfig()->getCompound() ? array() : '';
};
};
// For any form that is not represented by a single HTML control,
// errors should bubble up by default
$errorBubbling = function (Options $options) {
return $options['compound'];
};
// If data is given, the form is locked to that data
// (independent of its value)
$resolver->setDefined(array(
'data',
));
$resolver->setDefaults(array(
'data_class' => $dataClass,
'empty_data' => $emptyData,
'trim' => true,
'required' => true,
'property_path' => null,
'mapped' => true,
'by_reference' => true,
'error_bubbling' => $errorBubbling,
'label_attr' => array(),
'inherit_data' => false,
'compound' => true,
'method' => 'POST',
// According to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt)
// section 4.2., empty URIs are considered same-document references
'action' => '',
'attr' => array(),
'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.',
));
$resolver->setAllowedTypes('label_attr', 'array');
}
... lines 179 - 193
}

These are the options that are available to every field type. And actually, the parent class - BaseType - has even more:

125 lines vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php
... lines 1 - 11
namespace Symfony\Component\Form\Extension\Core\Type;
... lines 13 - 27
abstract class BaseType extends AbstractType
{
... lines 30 - 109
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'block_name' => null,
'disabled' => false,
'label' => null,
'label_format' => null,
'attr' => array(),
'translation_domain' => null,
'auto_initialize' => true,
));
$resolver->setAllowedTypes('attr', 'array');
}
}

There are easier ways to find out the valid options for a field - like the documentation or the form web profiler tab. But sometimes, being able to see how an option is used in these classes, might help you find the right value.

The label Option versus Variable

Let's see an example. In the form, we add a subFamily field:

50 lines src/AppBundle/Form/GenusFormType.php
... lines 1 - 6
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
... lines 8 - 13
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... line 19
->add('subFamily', EntityType::class, [
'placeholder' => 'Choose a Sub Family',
'class' => SubFamily::class,
'query_builder' => function(SubFamilyRepository $repo) {
return $repo->createAlphabeticalQueryBuilder();
}
])
... lines 27 - 39
;
}
... lines 42 - 48
}

Then, in the template, we override the label variable:

24 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 5
{{ form_row(genusForm.subFamily, {
'label': 'Taxonomic Subfamily',
... lines 8 - 11
}) }}
... lines 13 - 22
{{ form_end(genusForm) }}

But, according to BaseType, this field, well any field, also has a label option:

125 lines vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php
... lines 1 - 27
abstract class BaseType extends AbstractType
{
... lines 30 - 109
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
... lines 113 - 114
'label' => null,
... lines 116 - 119
));
... lines 121 - 122
}
}

The Form to FormView Transition

That's interesting! Let's see if we can figure out how the option and variable work together. Scroll up in BaseType. These classes also have another function called buildView():

125 lines vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php
... lines 1 - 27
abstract class BaseType extends AbstractType
{
... lines 30 - 41
public function buildView(FormView $view, FormInterface $form, array $options)
{
$name = $form->getName();
$blockName = $options['block_name'] ?: $form->getName();
$translationDomain = $options['translation_domain'];
$labelFormat = $options['label_format'];
if ($view->parent) {
if ('' !== ($parentFullName = $view->parent->vars['full_name'])) {
$id = sprintf('%s_%s', $view->parent->vars['id'], $name);
$fullName = sprintf('%s[%s]', $parentFullName, $name);
$uniqueBlockPrefix = sprintf('%s_%s', $view->parent->vars['unique_block_prefix'], $blockName);
} else {
$id = $name;
$fullName = $name;
$uniqueBlockPrefix = '_'.$blockName;
}
if (null === $translationDomain) {
$translationDomain = $view->parent->vars['translation_domain'];
}
if (!$labelFormat) {
$labelFormat = $view->parent->vars['label_format'];
}
} else {
$id = $name;
$fullName = $name;
$uniqueBlockPrefix = '_'.$blockName;
// Strip leading underscores and digits. These are allowed in
// form names, but not in HTML4 ID attributes.
// http://www.w3.org/TR/html401/struct/global.html#adef-id
$id = ltrim($id, '_0123456789');
}
$blockPrefixes = array();
for ($type = $form->getConfig()->getType(); null !== $type; $type = $type->getParent()) {
array_unshift($blockPrefixes, $type->getBlockPrefix());
}
$blockPrefixes[] = $uniqueBlockPrefix;
$view->vars = array_replace($view->vars, array(
'form' => $view,
'id' => $id,
'name' => $name,
'full_name' => $fullName,
'disabled' => $form->isDisabled(),
'label' => $options['label'],
'label_format' => $labelFormat,
'multipart' => false,
'attr' => $options['attr'],
'block_prefixes' => $blockPrefixes,
'unique_block_prefix' => $uniqueBlockPrefix,
'translation_domain' => $translationDomain,
// Using the block name here speeds up performance in collection
// forms, where each entry has the same full block name.
// Including the type is important too, because if rows of a
// collection form have different types (dynamically), they should
// be rendered differently.
// https://github.com/symfony/symfony/issues/5038
'cache_key' => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getBlockPrefix(),
));
}
... lines 106 - 123
}

In a controller, when you pass your form into a template, you always call createView() on it first:

86 lines src/AppBundle/Controller/Admin/GenusAdminController.php
... lines 1 - 15
class GenusAdminController extends Controller
{
... lines 18 - 63
public function editAction(Request $request, Genus $genus)
{
... lines 66 - 81
return $this->render('admin/genus/edit.html.twig', [
'genusForm' => $form->createView()
]);
}
}

That line turns out to be very important: it transforms your Form object into a FormView object.

In fact, your form is a big tree, with a Form on top and fields below it, which themselves are also Form objects. When you call createView(), all of the Form objects are transformed into FormView objects.

To do that, the buildView() method is called on each individual field. And one of the arguments to buildView() is an array of the final options passed to this field:

125 lines vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php
... lines 1 - 15
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
... lines 18 - 27
abstract class BaseType extends AbstractType
{
... lines 30 - 41
public function buildView(FormView $view, FormInterface $form, array $options)
{
... lines 44 - 104
}
... lines 106 - 123
}

For example, for subFamily, we're passing three options:

50 lines src/AppBundle/Form/GenusFormType.php
... lines 1 - 13
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... line 19
->add('subFamily', EntityType::class, [
'placeholder' => 'Choose a Sub Family',
'class' => SubFamily::class,
'query_builder' => function(SubFamilyRepository $repo) {
return $repo->createAlphabeticalQueryBuilder();
}
])
... lines 27 - 39
;
}
... lines 42 - 48
}

We could also pass a label option here.

These values - merged with any other default values set in configureOptions() - are then passed to buildView() and... if you scroll down a little bit, many of them are used to populate the vars on the FormView object of this field. Yep, these are the same vars that become so important when rendering that field:

125 lines vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php
... lines 1 - 27
abstract class BaseType extends AbstractType
{
... lines 30 - 41
public function buildView(FormView $view, FormInterface $form, array $options)
{
... lines 44 - 83
$view->vars = array_replace($view->vars, array(
'form' => $view,
'id' => $id,
'name' => $name,
'full_name' => $fullName,
'disabled' => $form->isDisabled(),
'label' => $options['label'],
'label_format' => $labelFormat,
'multipart' => false,
'attr' => $options['attr'],
'block_prefixes' => $blockPrefixes,
'unique_block_prefix' => $uniqueBlockPrefix,
'translation_domain' => $translationDomain,
// Using the block name here speeds up performance in collection
// forms, where each entry has the same full block name.
// Including the type is important too, because if rows of a
// collection form have different types (dynamically), they should
// be rendered differently.
// https://github.com/symfony/symfony/issues/5038
'cache_key' => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getBlockPrefix(),
));
}
... lines 106 - 123
}

To put it simply: every field has options and sometimes these options are used to set the form variables that control how the field is rendered:

125 lines vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php
... lines 1 - 27
abstract class BaseType extends AbstractType
{
... lines 30 - 41
public function buildView(FormView $view, FormInterface $form, array $options)
{
... lines 44 - 83
$view->vars = array_replace($view->vars, array(
... lines 85 - 89
'label' => $options['label'],
... lines 91 - 92
'attr' => $options['attr'],
... lines 94 - 103
));
}
... lines 106 - 123
}

Symfony gives us a label option as a convenient way to ultimately set the label variable.

Close up all those classes. Here's a question: we know how to set the help variable from inside of a Twig template. But could we somehow set this variable from inside of the GenusFormType class? Yes, and there are actually two cool ways to do it. Let's check them out.

Leave a comment!

  • 2016-11-03 Daan Hage

    Awesome answers! I had to look up EntityType, but now I understand it!
    Thank you for this bundle of knowledge!

  • 2016-11-03 weaverryan

    Yo Daan!

    1) Performance can be an issue with forms... but only once you have *many* (e.g. 50, 100 or more) fields in your form. Just check out your web debug toolbar's "Performance" tab if you'd like: you can see if a page is slowing down that has a form. So, it's definitely possible... but probably not an issue.

    2) Yep, this is a really good question! So usually, the "thing" that you're selecting is a list that lives in a database somewhere. That means that - other than the fact that having 1000 options on your page is horrible UI (and slow to load) - the EntityType is the perfect solution. And the EntityType really gives you *two* things: (A) it renders as a select element with all of your entities as options and (B) [much more importantly] when the id of the selected entity is submitted (e.g. 5) the EntityType queries for this and converts it into that object (e.g. Genus) before setting it back on your object.

    What we want is that *second* functionality (B) but with a field that renders as a hidden field. The setup would look like this:

    i) Field renders as an <input type="hidden" value="5" />
    ii) You build some jQuery magic auto-complete magic, completely independent of the form system (except that it should read the "5" as the default value, and when you select the a new option, it should override the value with the id of the newly selected option).
    iii) When you submit the id, you would want the (B) functionality above to convert the id (e.g. 5) back into your entity.

    Unfortunately, this type of "HiddenEntityId" field doesn't exist in Symfony (though it's been proposed). But, you can add a custom field pretty easily - here's a bundle that has one (you can use this or use it as inspiration): https://github.com/Glifery/EntityHiddenTypeBundle and here's another example: https://gist.github.com/bjo3rnf/4061232. We *do* use this here on KnpU, it's really handy.

    Let me know if that makes sense!

    Cheers!

  • 2016-11-02 Daan Hage

    He Ryan,
    Great tutorials! Loving all the tech!
    However, I have 2 questions that I hope you can answer for me:
    1) Because there is so much being generated (especially when we sometimes have 4 forms on 1 page), won't this way impact the performance a lot?
    2) A select-box is nice, however, sometimes you can have more then 1000 options, the the select box won't be a qood option and we use something like jquery-autocomplete. Is there something for Symfony regarding that as well? Or is that something we need to make custom?
    Kind Regards and keep op the great work!

  • 2016-10-17 Greg

    Hey Ryan,

    You're always the best.
    Thanks and I wait the doctrine manytomany ;)

  • 2016-10-16 weaverryan

    Hey Greg!

    Thanks for the tip - we had a typo on our end. Chapter 8 had a typo - it should be better now: https://knpuniversity.com/s...

    Cheers!

  • 2016-10-16 Greg

    Hi
    The video in download is no good it is the same like chapter 8
    after watching all video, it is the 10 video is missing.
    Thanks ;)