Form Options & Variables: Dream Team

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

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:

... 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:

... 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:

... 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:

... 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:

... 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:

... 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:

... 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:

{{ 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:

... 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():

... 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:

... 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:

... 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:

... 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:

... 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:

... 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!

This tutorial is built on Symfony 3 but form theming hasn't changed much in Symfony 4 and Symfony 5. Other than some path differences - this tutorial should work fine.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
        "stof/doctrine-extensions-bundle": "^1.2" // v1.2.2
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}