Buy

Symfony 4 Forms: Build, Render & Conquer!

0%
Buy

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We know what it looks like to create a new Article form: create the form, process the form request, and save the article to the database. But what does it look like to make an "edit" form?

The answer is - delightfully - almost identical! In fact, let's copy all of our code from the new() action and go down to edit(), where the only thing we're doing so far is allowing Symfony to query for our article. Paste! Excellent.

.

... lines 1 - 14
class ArticleAdminController extends AbstractController
{
... lines 17 - 46
public function edit(Article $article)
{
$form = $this->createForm(ArticleFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var Article $article */
$article = $form->getData();
$em->persist($article);
$em->flush();
$this->addFlash('success', 'Article Created! Knowledge is power!');
return $this->redirectToRoute('admin_article_list');
}
return $this->render('article_admin/new.html.twig', [
'articleForm' => $form->createView()
]);
}
... lines 68 - 79
}

Oh, but we need a few arguments: the Request and EntityManagerInterface $em. This is now exactly the same code from the new form. So... how can we make this an edit form? You're going to love it! Pass $article as the second argument to ->createForm().

... lines 1 - 46
public function edit(Article $article, Request $request, EntityManagerInterface $em)
{
$form = $this->createForm(ArticleFormType::class, $article);
... lines 50 - 65
}
... lines 67 - 80

We're done! Seriously! When you pass $article, this object - which we just got from the database becomes the data attached to the form. This causes two things to happen. First, when Symfony renders the form, it calls the getter methods on that Article object and uses those values to fill in the values for the fields.

Heck, we can see this immediately! This is using the new template, but that's fine temporarily. Go to /article/1/edit. Dang - I don't have an article with id

  1. Let's go find a real id. In your terminal, run:
php bin/console doctrine:query:sql 'SELECT * FROM article'

Perfect! Let's us id 26. Hello, completely pre-filled form!

The second thing that happens is that, when we submit, the form system calls the setter methods on that same Article object. So, we can still say $article = $form->getData()... But these two Article objects will be the exact same object. So, we don't need this.

So.. ah... yea! Like I said, we're done! By passing an existing object to createForm() our "new" form becomes a perfectly-functional "edit" form. Even Doctrine is smart enough to know that it needs to update this Article in the database instead of creating a new one. Booya!

Tweaks for the Edit Form

The real differences between the two forms are all the small details. Update the flash message:

Article updated! Inaccuracies squashed!

... lines 1 - 51
if ($form->isSubmitted() && $form->isValid()) {
... lines 53 - 55
$this->addFlash('success', 'Article Updated! Inaccuracies squashed!');
... lines 57 - 60
}
... lines 62 - 80

And then, instead of redirecting to the list page, give this route a name="admin_article_edit". Then, redirect right back here! Don't forget to pass a value for the id route wildcard: $article->getId().

... lines 1 - 42
/**
* @Route("/admin/article/{id}/edit", name="admin_article_edit")
... line 45
*/
public function edit(Article $article, Request $request, EntityManagerInterface $em)
{
... lines 49 - 51
if ($form->isSubmitted() && $form->isValid()) {
... lines 53 - 57
return $this->redirectToRoute('admin_article_edit', [
'id' => $article->getId(),
]);
}
... lines 62 - 65
}
... lines 67 - 80

Controller, done!

Next, even though it worked, we don't really want to re-use the same Twig template, because it has text like "Launch a new article" and "Create". Change the template name to edit.html.twig. Then, down in the templates/article_admin directory, copy the new.html.twig and name it edit.html.twig, because, there's not much that needs to be different.

Update the h1 to Edit the Article and, for the button, Update!.

{% extends 'content_base.html.twig' %}
{% block content_body %}
<h1>Edit the Article! ?</h1>
{{ form_start(articleForm) }}
{{ form_row(articleForm.title, {
label: 'Article title'
}) }}
{{ form_row(articleForm.author) }}
{{ form_row(articleForm.content) }}
{{ form_row(articleForm.publishedAt) }}
<button type="submit" class="btn btn-primary">Update!</button>
{{ form_end(articleForm) }}
{% endblock %}

Cool! Let's try this - refresh! Looks perfect! Let's change some content, hit Update and... we're back!

Reusing the Form Rendering Template

Cool except... I don't love having all this duplicated form rendering logic - especially if we start customizing more stuff. To avoid this, create a new template file: _form.html.twig. I'm prefixing this by _ just to help me remember that this template will render a little bit of content - not an entire page.

Next, copy the entire form code and paste! Oh, but the button needs to be different for each page! No problem: render a new variable: {{ button_text }}.

{{ form_start(articleForm) }}
{{ form_row(articleForm.title, {
label: 'Article title'
}) }}
{{ form_row(articleForm.author) }}
{{ form_row(articleForm.content) }}
{{ form_row(articleForm.publishedAt) }}
<button type="submit" class="btn btn-primary">{{ button_text }}</button>
{{ form_end(articleForm) }}

Then, from the edit template, use the include() function to include article_admin/_form.html.twig and pass one extra variable as a second argument: button_text set to Update!.

... lines 1 - 2
{% block content_body %}
... lines 4 - 5
{{ include('article_admin/_form.html.twig', {
button_text: 'Update!'
}) }}
{% endblock %}

Copy this and repeat it in new: remove the duplicated stuff and say Create!.

... lines 1 - 2
{% block content_body %}
... lines 4 - 5
{{ include('article_admin/_form.html.twig', {
button_text: 'Create!'
}) }}
{% endblock %}

I love it! Let's double-check that it works. No problems on edit! And, if we go to /admin/article/new... nice!

And just to make our admin section even more awesome, back on the list page, let's add a link to edit each article. Open list.html.twig, add a new empty table header, then, in the loop, create the link with href="path('admin_article_edit')" passing an id wildcard set to article.id. For the text, print an icon using the classes fa fa-pencil.

... lines 1 - 9
<thead>
<tr>
... lines 12 - 14
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for article in articles %}
<tr>
... lines 21 - 25
<td>
<a href="{{ path('admin_article_edit', {
id: article.id
}) }}">
<span class="fa fa-pencil"></span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
... lines 36 - 38

Cool! Try that out - refresh the list page. Hello pencil icon! Click any of these to hop right into that form.

We just saw one of the most pleasant things about the form component: edit and new pages are almost identical. Heck, the Form component can't even tell the difference! All it knows is that, if we don't pass an Article object, it needs to create one. And if we do pass an Article object, it says, okay, I'll just update that object instead of making a new one. In both cases, Doctrine is smart enough to INSERT or UPDATE correctly.

Next: let's turn to a super interesting form use-case: our highly-styled registration form.

Leave a comment!

  • 2019-03-19 Diego Aguiar

    Wow, I didn't know you can't add an anchor tag inside a button, that's new to me :)
    Cheers man!

  • 2019-03-15 Arjen Schrijvers

    Thanks. Made a little change, above was not working.

    Just toke the a-tag (removed the button).
    Now it is working. Seems a-tag within button not working. Again thanks for you help and pointing me to the right direction

  • 2019-03-15 Diego Aguiar

    You only have to render a route using twig. Probably you may enjoy watching our Twig tutorial: https://symfonycasts.com/sc...

    Anyway here is how:


    <button id="form_close" name="form[close]" type="button" class="btn btn-sm">
    <a' href="{{ path('admin_company_list') }}">Close</a'>
    </button>


    I had to add a tick to the anchor tag because it was been rendered as HTML

  • 2019-03-15 Arjen Schrijvers

    I understand what you meaning. My code is as follow:

    <button id="form_save" name="form[save]" type="submit" class="btn btn-success btn-sm">Save</button>
    <button id="form_close" name="form[close]" type="button" class="btn btn-sm" onclick="document.location.href='/admin/klanten/lijst';">Close</button>

    It need going to be back to mij main list (admin/klanten/lijst the route in the controller is 'admin_company_list' How dow I jump to that part in twig/symfony?

    btw, I like to use the naming from the routing. Not a hard coded url in href

  • 2019-03-15 Diego Aguiar

    Hey Arjen Schrijvers

    That's super simply, specially if you follow Symfony Form Best Practices. The "BP" is to render your buttons manually, so you can reuse your FormTypes easily. So, what you have to do is just to add some extra HTML for adding a cancel/back button

    Cheers!

  • 2019-03-15 Arjen Schrijvers

    All edit and new forms I see one submit button. How to implement next to the Submitbutton a Cancel button? Is that easy to do? Especially for editing forms it's great.

  • 2019-03-10 weaverryan

    Hey dave!

    Yep, that works fine! In the file uploads tutorial, we use an "unmapped" field, which is kinda nice, because the file upload doesn't override a property on your entity. But, both totally work!

    Cheers!

  • 2019-03-08 dave

    the solution I found was to check if the user has uploaded any photo in the FileType if yes it updates it if not it sets the actual photo


    $editUser = $this->getDoctrine()->getRepository(User::class)->find($id);

    $user = $this->getUser();
    if ($id != $user->getId())
    $this->denyAccessUnlessGranted('ROLE_ADMIN');

    $actualPhoto = $editUser->getPhoto();

    $editForm = $this->createFormBuilder($editUser)
    ->add('firstname', TextType::class, [
    'attr' => [
    'class' => 'input-field'
    ],
    'label' => 'First Name'
    ])
    ->add('lastname', TextType::class, [
    'attr' => [
    'class' => 'input-field'
    ],
    'label' => 'Last Name'
    ])
    ->add('username', TextType::class, [
    'attr' => [
    'class' => 'input-field'
    ]
    ])
    ->add('mobile', TextType::class, [
    'attr' => [
    'class' => 'input-field'
    ]
    ])
    ->add('zone', EntityType::class, array(
    'class' => Zone::class,
    'choice_label' => 'name',
    'attr' => [
    'class' => 'input-field'
    ]
    ))
    ->add('isArchived', ChoiceType::class, [
    'choices' => [
    'Yes' => '1',
    'No' => '0'
    ],
    'attr' => [
    'class' => 'input-field'
    ]
    ])
    ->add('address', TextareaType::class, [
    'attr' => [
    'class' => 'input-field'
    ]
    ])
    ->add('photo', FileType::class, [
    'data_class' => null,
    'attr' => [
    'class' => 'input-field'
    ],
    'required' => false
    ])
    ->add('update', SubmitType::class, [
    'attr' => [
    'class' => 'btn btn-warning'
    ]
    ])
    ->getForm();

    $editForm->handleRequest($request);

    if ($editForm->isSubmitted() && $editForm->isValid()) {
    $editUser = $editForm->getData();
    $em = $this->getDoctrine()->getManager();
    if ($editUser->getPhoto() != null){
    $files = $editForm->get('photo')->getData();
    $filename = md5(uniqid()) . '.' . $files->guessExtension();
    $files->move($this->getParameter('uploads'), $filename);
    $editUser->setPhoto($filename);
    $this->addFlash('edit', 'Photo successfully edited');
    } else {
    $editUser->setPhoto($actualPhoto);
    $this->addFlash('edit', 'User successfully edited');
    }
    $em->persist($editUser);
    $em->flush();
    return $this->redirectToRoute('editUser', array('id' => $id));
    }

    return $this->render('Admin/editUser.html.twig', [
    'edit' => $editForm->createView(),
    'photo' => $editUser
    ]);
  • 2019-03-07 weaverryan

    Hey Dave!

    We're currently talking about file uploads in our uploads tutorial - and I think what you're talking about is covered here: https://symfonycasts.com/sc...

    However, the error you're seeing is a bit odd - it comes from the File validation constraint. Normally, if you choose not to upload a file and you submit, the File constraint is smart enough to see that you didn't upload anything, and it does nothing. However, in your case, it *does* seem to be validating still - except that the uploaded file is missing. My guess is that there's something "quirky" in your setup. Can you post your form and controller code? And when exactly dose this error occur?

    Cheers!

  • 2019-03-07 dave

    what if I ve got a photo attribute in the database which i dont want to update in the same function it shows me an error "The file could not be found"