Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: ^7.1.3
Subscribe to download the code!Compatible PHP versions: ^7.1.3
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Form Rendering Functions: form_*
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeTo render the form, we're using a few form functions: one that makes the form start tag, one that makes the end tag and one that renders all the fields, labels and errors inside.
This was easy to set up. The problem is that we have almost no control over the HTML markup that's used! Sure, we were able to activate a "form theme" that told it to use Bootstrap-friendly markup. But, what if you need more control?
This is probably the hardest part of Symfony's form system. But don't worry: we're going to learn several different strategies to help you get the markup you need... without going crazy... probably.
The Form Rendering Functions
Go to your other tab and Google for "Symfony form rendering functions" to find a page that talks all about the functions we're using and a few others.
First, form_start()
, yes, this does just render the form start tag, which might seem kind of silly, but it can come in handy when you add a file upload field to your form: it automatically add the enctype
attribute.
Oh, but notice: form_start()
has a second argument: an array of variables that can be passed to customize it. Apparently you can pass method
to change the method
attribute or attr
to add any other attributes to the form tag - like a class.
Next: find form_end()
. This one seems even sillier because it literally prints... yep! The form closing tag! But, it has a hidden superpower: it also renders any fields that we forgot to render. Now, that might not make sense yet because this magic form_widget()
function seems to be rendering everything automatically. But, in a moment, we'll render the fields one-by-one. When we do that, if we've forgotten to render any of the fields, form_end()
will render them for us... and then the closing tag.
That still may not seem like a good feature... and, in many ways, it's not! In reality, the purpose of this is not so that we can be lazy and form_end()
will save us. Nope - the true purpose is that form_end()
will render any hidden fields automatically, without us needing to even think about them. Most importantly, it will render your form's CSRF token
CSRF Token
Inspect element near the bottom of the form. Woh! Without us doing anything, we have a hidden input
tag called _token
. This is a CSRF token and it was automatically added by Symfony. And, even cooler, when we submit, Symfony automatically validates it.
Without even knowing it, all of our forms are protected from CSRF attacks.
form_widget and form_row()
Back to the form rendering goodness! To print the form fields themselves, the easiest way is to call form_widget()
and pass the entire form. But, if you need a little bit more control, instead of form_widget()
, you can call form_row()
and render each field individually. For example, articleForm.title
. Copy that and paste it three more times. Render articleForm.content
, articleForm.publishedAt
and articleForm.author
.
Show Lines
|
// ... lines 1 - 5 |
{{ form_start(articleForm) }} | |
{{ form_row(articleForm.title) }} | |
{{ form_row(articleForm.author) }} | |
{{ form_row(articleForm.content) }} | |
{{ form_row(articleForm.publishedAt) }} | |
Show Lines
|
// ... line 12 |
{{ form_end(articleForm) }} | |
Show Lines
|
// ... lines 14 - 15 |
Before we talk about this function, move over, refresh and... ok! It looks exactly the same. That's no accident! Calling form_widget()
and passing it the entire form is just a shortcut for calling form_row()
on each field individually.
This introduces an important concept in Symfony's form rendering system: the "row". The form_row()
function basically adds a "wrapper" around the field - like a div - then renders the 4 components of each field: the label, the "widget" - that's the form field itself, the help text and, if needed, the validation errors.
At first, it looks like using form_row()
isn't much more flexible than what we had before, except that we can reorder the fields. But, in reality, we've just unlocked quite a lot of control via a system called "form variables". Let's check those out next!
8 Comments
Hey Sebastian-K,
Yes, you need to override the form type's block prefix, i.e. getBlockPrefix()
parent method in your form type. I'm not sure how to do this on the fly with your code, seems you're trying to do this with that "block_prefix" option, but IIRC that option does not available for fields, it should be applied to the whole form, not to a separate field. Try to apply that option in $defaultOptions maybe, but Probably better to create a separate form type for this and override it there like we did in this screencast: https://symfonycasts.com/screencast/javascript/post-proper-api-endpoint
UPD: Actually, that "block_prefix" does exist for fields, but from the docs I see it's always null anyway, so you're not changing anything with this code, see https://symfony.com/doc/current/reference/forms/types/form.html#block-prefix - it seems like it works the opposite, it's always empty but you can add some prefix for the field. I believe you need to apply that "block_prefix" to the whole form instead, not to the specific field as you did.
I hope this helps :)
Cheers!
Hi Ryan and team,
my comment is more related to this course than video but I am looking for help or suggestions.
We have clients who call the same thing with different names. It is just lets say 5 fields out of 100 currently but as application grow there will be more and more of such fields. For simplicity I will just throw here similar example. Imagine you have an entity called Dog and you are allowing your users to enter their name and age and you will also generate onscreen report and pdf copy.
1st client wants to see on the input screen Enter dog's name & age and 2nd one wants to see Enter puppy's name & age.
So currently I have in code something like If the client is 1 then label = dog else puppy.
I am wondering if I could somehow use translation for this otherwise above code is randomly placed into controllers, form types and templates.
I would like to achieve something like I have list of all those 100 fields in text file and once the client and label is detected I have prepared a translation for it but not per language but per client if that make sense. Issue is that it needs to be not just labels in forms but also readonly version or in script that is generating pdfs of your entities so as you can imagine currently it is still manageable since it is just 5 fields but I am afraid it will be more an issue once we will get more clients and they call internally same thing differently then other clients.
How would you handle this?
Hey Peter K.!
That's a fascinating challenge :). So... somehow using the translator also came to my mind... though I don't think we want to mess with the locale and start faking that client1 speaks "client1" language and client2 speak "client2" language and so has translations inside messages_client2.yml. That would be the quickest and dirtiest way to get this done, but I think it may have side effects (e.g. it may confuse a 3rd party bundle that does have translations... but maybe not... it just seems odd).
What I would probably do is add my own, tiny "client translation" service that would have, for example a public function translate(string $message): string
. Internally, you would look up the current client and find their translation. The trickiest detail will be that you'll need to call this service each time you need to translate something. I would, at least initially, "be ok" with doing this - e.g. inject this service into your form classes, call this method, and pass the final string to the label. It's... kind of beautifully boring and simple :p.
For the translation files themselves, you could use any format - but if you use something like YAML, then you may just want to inject a cache service so you only parse those files once per deploy (use the cache.system cache service, which automatically clears on deploy).
Let me know what you think :).
Cheers!
Hi Ryan and team,
my comment is more related to this course than video but I am looking for help or suggestions.
We have clients who call the same thing with different names. It is just lets say 5 fields out of 100 currently but as application grow there will be more and more of such fields. For simplicity I will just throw here similar example. Imagine you have an entity called Dog and you are allowing your users to enter their name and age and you will also generate onscreen report and pdf copy.
1st client wants to see on the input screen Enter dog's name & age and 2nd one wants to see Enter puppy's name & age.
So currently I have in code something like If the client is 1 then label = dog else puppy.
I am wondering if I could somehow use translation for this otherwise above code is randomly placed into controllers, form types and templates.
I would like to achieve something like I have list of all those 100 fields in text file and once the client and label is detected I have prepared a translation for it but not per language but per client if that make sense. Issue is that it needs to be not just labels in forms but also readonly version or in script that is generating pdfs of your entities so as you can imagine currently it is still manageable since it is just 5 fields but I am afraid it will be more an issue once we will get more clients and they call internally same thing differently then other clients.
How would you handle this?
Hi,
I have weird behaviour.
I am using 2 forms on 1 twig template.
I will create 2 forms in controller and pass them to template.
<br />'form1' => form1->createView(),<br />'form2' => form2->createView()<br />
then in controller when the forms are submitted i check (there are being check in one route)
if form 1 is valid then do this and that
if form 2 is valid then do this and that
everything works so far ok.
Now if I submit only form1 with some blank fields so it will return validation errors, it will also throw errors on form2
so in my twig if I check<br />form1.vars.valid will return true -> as expected<br />form2.vars.valid will return true -> never been submitted<br />
so I decided to check if in twig if form has been submitted and I added<br />form1.vars.valid and form1.vars.sibmitted -> returns true as expected<br />form2.vars.valid and form2.vars.submitted ->returns true it hasnt been submitted.<br />
so the question is why are both forms being submitted if I submit only 1
to be added in twig I am properly starting and ending each form
`
{{ form_start(form1) }}
{{form_widget(form1)}}
<button type="submit">.....</button>
{{form_end(form1)}}
{{ form_start(form2) }}
{{form_widget(form2)}}
<button type="submit">.....</button>
{{form_end(form2)}}
`
Possibly Ive solved it so this is just a future reference for someone with this issue.
The issue is that the forms had the same name when creating them in controller.
so you either need to create form class for each form and give it different name or when creting form in controller to use
$form1 = $this->get('form.factory')->createNamedBuilder('form1')->add(......
Probably another issue is that both forms have the same action URL. It may cause problems
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/annotations": "^1.0", // 1.10.2
"doctrine/doctrine-bundle": "^1.6.10", // 1.10.2
"doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // v2.0.0
"doctrine/orm": "^2.5.11", // v2.7.2
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
"knplabs/knp-time-bundle": "^1.8", // 1.8.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"phpdocumentor/reflection-docblock": "^3.0|^4.0", // 4.3.0
"sensio/framework-extra-bundle": "^5.1", // v5.2.1
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.1.6
"symfony/cache": "^3.3|^4.0", // v4.1.6
"symfony/console": "^4.0", // v4.1.6
"symfony/flex": "^1.0", // v1.21.6
"symfony/form": "^4.0", // v4.1.6
"symfony/framework-bundle": "^4.0", // v4.1.6
"symfony/property-access": "^3.3|^4.0", // v4.1.6
"symfony/property-info": "^3.3|^4.0", // v4.1.6
"symfony/security-bundle": "^4.0", // v4.1.6
"symfony/serializer": "^3.3|^4.0", // v4.1.6
"symfony/twig-bundle": "^4.0", // v4.1.6
"symfony/validator": "^4.0", // v4.1.6
"symfony/web-server-bundle": "^4.0", // v4.1.6
"symfony/yaml": "^4.0", // v4.1.6
"twig/extensions": "^1.5" // v1.5.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.7
"fzaninotto/faker": "^1.7", // v1.8.0
"symfony/debug-bundle": "^3.3|^4.0", // v4.1.6
"symfony/dotenv": "^4.0", // v4.1.6
"symfony/maker-bundle": "^1.0", // v1.8.0
"symfony/monolog-bundle": "^3.0", // v3.3.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
"symfony/stopwatch": "^3.3|^4.0", // v4.1.6
"symfony/var-dumper": "^3.3|^4.0", // v4.1.6
"symfony/web-profiler-bundle": "^3.3|^4.0" // v4.1.6
}
}
Not sure if that's the correct part of the series, but I've got a problem with my Form.
This will render to
<input type="text" id="form_HOOK_URL" name="form[HOOK_URL]"
but I need it to be<input type="text" id="form_HOOK_URL" name="HOOK_URL"
(without theform[]
part in the name)The form submit IS NOT handled by the controller that created the form, it's a complete different URL/endpoint that doesn't "understand"
from[HOOK_URL]
, onlyHOOK_URL
.