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're going to tackle one of the most annoying things in Symfony's form system, and, I hope, make it as painless as possible... because the end result is pretty cool!
Log in as admin2@thespacebar.com
, password engage
and then go to /admin/article
. Click to create a new article. Here's the goal: on this form, I want to add two new drop-down select elements: a location drop-down - so you can choose where in the galaxy you are - and a second dropdown with more specific location options depending on what you chose for the location. For example, if you select "Near a star" for your location, the next drop-down would update to be a list of stars. Or, if you select "The Solar System", the next drop-down will be a list of planets.
This is called a "dependent form field", and, unfortunately, it's one of the trickier things to do with the form system - which is exactly why we're talking about it! Let's add the first new field. Find your terminal and run
php bin/console make:entity
Modify the Article
entity and create a new field called location
. Make it a string
field with "yes" to nullable
in the database: the location will be optional. Now run:
php bin/console make:migration
and open the Migrations/
directory to check out that new file.
... lines 1 - 12 | |
public function up(Schema $schema) : void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); | |
$this->addSql('ALTER TABLE article ADD location VARCHAR(255) DEFAULT NULL'); | |
} | |
... lines 20 - 29 |
No surprises here, so let's go back and run it:
php bin/console doctrine:migrations:migrate
Perfect!
Next, open the ArticleFormType
so we can add the new field. Add location
and set it to a ChoiceType
to make it a drop-down. Pass the choices
option set to just three choices. The Solar System
set to solar_system
, Near a star
set to star
and Interstellar Space
set to interstellar_space
.
... lines 1 - 15 | |
class ArticleFormType extends AbstractType | |
{ | |
... lines 18 - 24 | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
... lines 27 - 29 | |
$builder | |
... lines 31 - 39 | |
->add('location', ChoiceType::class, [ | |
'choices' => [ | |
'The Solar System' => 'solar_system', | |
'Near a star' => 'star', | |
'Interstellar Space' => 'interstellar_space' | |
], | |
... line 46 | |
]) | |
; | |
... lines 49 - 54 | |
} | |
... lines 56 - 65 |
The choices
on the ChoiceType
can look confusing at first: the key for each item will be what's actually displayed in the drop down. And the value will be what's set onto our entity if this option is selected. So, this is the string that will ultimately be saved to the database.
Let's also add one more option: required
set to false
.
... lines 1 - 29 | |
$builder | |
... lines 31 - 39 | |
->add('location', ChoiceType::class, [ | |
... lines 41 - 45 | |
'required' => false, | |
]) | |
; | |
... lines 49 - 65 |
Remember: as soon as we pass the field type as the second argument, the form field type guessing stops and does nothing. Lazy! It would normally guess that the required
option should be false - because this field is not required in the database, but that won't happen. So, we set it explicitly.
Cool! Let's try it - go refresh the form. Ha! It works... but in a surprising way: the location
field shows up... all the way at the bottom of the form.
The reason? We forgot to render it! Open templates/article_admin/_form.html.twig
. When you forget to render a field, {{ form_end() }}
renders it for you. It's kind of a nice reminder that I forgot it. Of course, we don't really want to render it all the way at the bottom like this. Instead, add {{ form_row(articleForm.location) }}
{{ form_start(articleForm) }} | |
... lines 2 - 5 | |
{{ form_row(articleForm.location) }} | |
... lines 7 - 12 | |
{{ form_end(articleForm) }} |
Oh, and I forgot: we'll want an "empty" choice at the top of the select. In the form, add one more option: placeholder
set to Choose a location
.
... lines 1 - 24 | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
... lines 27 - 39 | |
->add('location', ChoiceType::class, [ | |
'placeholder' => 'Choose a location', | |
... lines 42 - 47 | |
]) | |
; | |
... lines 50 - 55 | |
} | |
... lines 57 - 66 |
Refresh! So much nicer! And if we submitted the form, it would save.
So, let's add the second field! Go back to your terminal and run:
php bin/console make:entity
Update the Article
entity again and create a new field called specificLocationName
, which will store a string like "Earth" or "Mars". Make this "yes" to nullable
in the database - another optional field.
When you're done, make the migration:
php bin/console make:migration
And... I'm pretty confident that migration won't have any surprises, so let's just run it:
php bin/console doctrine:migrations:migrate
Sweet! Back in ArticleFormType
, copy the location
field, paste, and call it specificLocationName
. For the placeholder
, use Where exactly?
. And for the choices
... hmm - this is where things get interesting. I'll just add a dummy "TODO" option to start.
... lines 1 - 24 | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
... lines 27 - 48 | |
->add('specificLocationName', ChoiceType::class, [ | |
'placeholder' => 'Where exactly?', | |
'choices' => [ | |
'TODO' => 'TODO' | |
], | |
'required' => false, | |
]) | |
; | |
... lines 57 - 62 | |
} | |
... lines 64 - 73 |
Back in the form template, copy the location
render line, paste it right below, and change it to specificLocationName
.
{{ form_start(articleForm) }} | |
... lines 2 - 6 | |
{{ form_row(articleForm.specificLocationName) }} | |
... lines 8 - 13 | |
{{ form_end(articleForm) }} |
When we refresh now... no surprise: it works. Here's our location
and here's our specificLocationName
. But... this is not how we want this to work. When "solar system" is selected, I want this second drop-down to contain a list of planets. If "Near a star" is selected, this should be a list of stars. And if "Interstellar space" is selected, I don't want this field to be in the form at all. Woh.
The way to solve this is a combination of form events, JavaScript and luck! Ok, I hope we won't need too much of that. Let's start jumping into these topics next!
Haha, well, it was thanks to users like you who were asking for this that we finally covered it. This is a *tough* topic I admit!
My error is not particularly about this chapter, but I am getting this:
App\Entity\Meetstaat object not found by the @ParamConverter annotation.
I also googled and read some pages on the Symfony docs but I can't seem to understand the issue.
`
namespace App\Entity;
use App\Repository\MeetstaatRepository;
use Doctrine\ORM\Mapping as ORM;
/**
Hey Farry7,
It looks to me that the Meetstaat record you're trying to access doesn't exist on your database. Double-check that the ID you're passing to the route exists on your system. Cheers!
Hi there!
Any idea how I can add a "selected" option on a ChoiceType element???
Long time web developer, but I'm new to sf... and I'm implementing a basic CRUD and one of the fields needs to be a select. The value of the select field is stored in the db and when I read the value back... I need to pre-select that particular value on the select... I've been googling this for 2 days now and I would think this is something that should be trivial.
Any ideas?
Hey Jose M.
Have you tried out the "data" option?
https://symfony.com/doc/cur...
If you bind an object to your form, then its initial value will be taken from the object
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"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
"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/console": "^4.0", // v4.1.6
"symfony/flex": "^1.0", // v1.17.6
"symfony/form": "^4.0", // v4.1.6
"symfony/framework-bundle": "^4.0", // v4.1.6
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/security-bundle": "^4.0", // v4.1.6
"symfony/serializer-pack": "^1.0", // v1.0.1
"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/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.1.6
}
}
you have no idea how much i have waited for this part :D