If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe 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?!
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
.
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.
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 | |
} | |
} |
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.
Hey Selman555!
You're totally right about that. But, can you tell me what you're trying to accomplish? We show getBlockPrefix() being used in this chapter. Are you trying to customize the actual name="" attribute on your form fields?
Cheers!
Hi,
No the name attribute can stay the same as far as I'm concerned.
In my case I have simple array of Setting entities fetched from database. It feels natural to just iterate through them and create a form for each of the entities and pass all the forms to the view. But the id="" and for="" attributes are prefixed with the FQDN of Setting::class. This results in duplicate ID's.
I use CSRF-tokens as well in every form so I can validate the source of the post. The tokens are invalid from the second form down, probably because the second form is submitting data for the first form (I'm not sure).
The sad part is that in a FormType, the `getBlockPrefixes()` method is called first, so I can't change the prefix in the configuration methods.
Does this make sense?
I have posted a question on stackoverflow: https://stackoverflow.com/q...
I'm still trying to figure out how I can implement the answer I got.
Greetings from Belgium
Hey Selman555!
Ah, yes, now I understand much better :). One important question is: do you need each as an actual individual form, or one big form? I think you want individual forms, so actually, that answer on Stackoverflow won't help (and it looks complex).
And you're right - the CSRF token is based on the form name, so it's being re-used and (I'm pretty sure) cleared/reset in the session after the first form submits. Fortunately, we can use a lower-level method to create the form, which gives you more control. In a controller:
use Symfony\Component\Form\FormFactoryInterface;
public function editSettings(FormFactoryInterface $formFactory)
{
// $setting here represents one Setting object - you'll probably have some sort of loop, but you get the idea
$form = $formFactory->createNamedBuilder('settings_'.$setting->getId(), SettingFormType::class, $setting)->getForm();
}
This createNamedBuilder()
is actually what the normal $this->createForm() code eventually calls... but when IT calls it, the first argument to createNamedBuilder() - which is the form NAME - is set to the block prefix. By using this lower-level method, you can create a unique name for each form.
I haven't tried this code, but I think it should solve everything. Or... get us closer ;).
Cheers!
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!
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!
Awesome answers! I had to look up EntityType, but now I understand it!
Thank you for this bundle of knowledge!
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 ;)
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!
// composer.json
{
"require": {
"php": "^7.1.3",
"symfony/symfony": "3.4.*", // v3.4.49
"doctrine/orm": "^2.5", // 2.7.5
"doctrine/doctrine-bundle": "^1.6", // 1.12.13
"doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
"symfony/swiftmailer-bundle": "^2.3", // v2.6.7
"symfony/monolog-bundle": "^2.8", // v2.12.1
"symfony/polyfill-apcu": "^1.0", // v1.23.0
"sensio/distribution-bundle": "^5.0", // v5.0.25
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
"incenteev/composer-parameter-handler": "^2.0", // v2.1.4
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
"doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
"stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.1.7
"symfony/phpunit-bridge": "^3.0", // v3.4.47
"nelmio/alice": "^2.1", // v2.3.6
"doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
}
}
I've noticed something weird. Since recent releases (not sure since when) you can't override `getName` anymore in your form type. You have to use `getBlockPrefix` in order to change the block prefixes. That's alright but in the `buildView` function it still uses `$form->getName()`. Some of my forms aren't processed because they have not been submitted (i.e `$form->isSubmitted() === false`) because I have multiple forms with the same FQDN