Forms Without a Data Class
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 SubscribeWe're almost at the finish line of our journey with Symfony Forms. But before we wrap it up, let's dive into something both fun and practical. If you navigate to the /parts page, you'll see a simple search input. It's a basic HTML form, and that's all good. But here's a thought: can we recreate this using Symfony Forms? You bet!
Creating a Form Without an Entity
When we discuss Symfony Forms, we usually link them to an entity or a data class. However, this isn't set in stone. Symfony Forms can stand alone, without any object mapping happening behind the scenes. All the form data is neatly stored in plain PHP arrays. Our search form is a great example of this.
Sure, we could build this form right in the controller using the form builder. But to keep things neat and tidy, let's stick with the form type again. Plus, we'll pick up a few handy tricks along the way.
Head to your terminal and run the following command:
symfony console make:form
Name the form PartSearchType. This time around, when it asks for an entity or data class, leave it blank. Next up, locate the generated PartSearchType in the src/Form directory and open it.
You'll see a placeholder field named field_name:
| // ... lines 1 - 8 | |
| class PartSearchType extends AbstractType | |
| { | |
| public function buildForm(FormBuilderInterface $builder, array $options): void | |
| { | |
| $builder | |
| ->add('field_name') | |
| ; | |
| } | |
| public function configureOptions(OptionsResolver $resolver): void | |
| { | |
| $resolver->setDefaults([ | |
| // Configure your form options here | |
| ]); | |
| } | |
| } |
Swap that out with query to match the name of the legacy search input:
| // ... lines 1 - 8 | |
| class PartSearchType extends AbstractType | |
| { | |
| public function buildForm(FormBuilderInterface $builder, array $options): void | |
| { | |
| $builder | |
| ->add('query') | |
| ; | |
| } | |
| // ... lines 17 - 23 | |
| } |
Using the Form in the Controller
Now, head over to the index() action in src/Controller/PartController.php. At the start, create a form with $this->createForm() passing PartSearchType::class and store it in a variable named $searchForm. When rendering the template below, pass it as an additional parameter with 'searchForm' => $searchForm:
| // ... lines 1 - 4 | |
| use App\Form\PartSearchType; | |
| // ... lines 6 - 11 | |
| final class PartController extends AbstractController | |
| { | |
| // ... line 14 | |
| public function index(StarshipPartRepository $repository, Request $request,): Response | |
| { | |
| $searchForm = $this->createForm(PartSearchType::class); | |
| // ... lines 18 - 21 | |
| return $this->render('part/index.html.twig', [ | |
| // ... line 23 | |
| 'searchForm' => $searchForm, | |
| ]); | |
| } | |
| } |
In templates/part/index.html.twig, render the whole form with {{ form(searchForm) }}:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="flex justify-end mt-6 mb-6"> | |
| <div class="relative w-full max-w-md"> | |
| <form method="get" action="{{ path('app_part_index') }}"> | |
| // ... lines 10 - 18 | |
| </form> | |
| {{ form(searchForm) }} | |
| </div> | |
| </div> | |
| // ... lines 24 - 48 | |
| {% endblock %} |
For now, let's keep the original so that we can compare them. We'll tidy this up later.
After refreshing your browser, you'll notice two search inputs. The new kid on the block has a label, while the old one is label-free. Let's sort that out first.
Hiding the Form Field Label
Back in PartSearchType, for the query field, pass null for the type, and an array for options. Within it, add the label option. You can set this to any string you want the label to be. Or, just set it to false. This instructs Symfony not to render the label at all:
| // ... lines 1 - 8 | |
| class PartSearchType extends AbstractType | |
| { | |
| public function buildForm(FormBuilderInterface $builder, array $options): void | |
| { | |
| $builder | |
| ->add('query', null, [ | |
| 'label' => false, | |
| ]) | |
| ; | |
| } | |
| // ... lines 19 - 25 | |
| } |
While we're here, let's polish things up a bit. Add the attr option for attributes, and inside that, add placeholder set to Search... to match the legacy form. On the next line, class. Grab the CSS classes from the original form and paste them here:
| // ... lines 1 - 8 | |
| class PartSearchType extends AbstractType | |
| { | |
| public function buildForm(FormBuilderInterface $builder, array $options): void | |
| { | |
| $builder | |
| ->add('query', null, [ | |
| 'label' => false, | |
| 'attr' => [ | |
| 'placeholder' => 'Search...', | |
| 'class' => 'w-full p-3 pl-10 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500', | |
| ], | |
| ]) | |
| ; | |
| } | |
| // ... lines 23 - 29 | |
| } |
Jump back to your browser and refresh the page. It should now be a spitting image of the legacy search field, minus the search icon, which we'll take care of later.
If you try to submit the form now, you'll notice it uses the POST method, which is the default behavior. However, our search form uses GET, which is more fitting for a search feature.
What's Next?
Next, we'll switch the form method from POST to GET and learn how to manage it properly in the controller.
2 Comments
I believe setting the label to
falseremoves it completely. Labels associated with form controls are a crucial accessibility requirement, so you should never set the label tofalse. Theplaceholderattribute is very problematic when it comes to accessibility, and is not a replacement for the form control labels.If you definitely need to hide a form control label, please consider using CSS to hide the label properly so that it remains associated with the control, but is not displayed to the user.
Hey @Inan!
Awesome catch and explanation, I didn't know this! Thanks for the details.
--Kevin
"Houston: no signs of life"
Start the conversation!