The Edit Form

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

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!

  • 2020-02-12 Victor Bocharsky

    Hey Gianluca,

    Thanks for sharing your solution with others! Yeah, you can always query for the object manually, especially in some complex cases - that's not a big deal. Glad you solved it yourself!

    Cheers!

  • 2020-02-11 Gianluca Mazzeo

    Thanks Victor,
    I'm resoved like so:


    public function edit(EntityManagerInterface $em, Request $request,$invoiceNumber)
    {
    $invoice = $em->getRepository(Invoice::class)
    ->findOneBy(['InvoiceNumber' => $invoiceNumber]);

    $form = $this->createForm(InvoiceFormType::class,$invoice);
  • 2020-02-11 Victor Bocharsky

    Hey Gianluca,

    This sounds like a problem that does not relate to the form. Could you double check that $invoice is not null and an instance of \App\Entity\Invoice in your edit() controller? You can use "dd($invoice)" or "dump($invoice); die;" in the begin of the edit(). Do you see the same error? Btw, does your Invoice entity has "invoiceNumber" property?

    Cheers!

  • 2020-02-10 Gianluca Mazzeo

    Hello,

    I'm trying to create an edit form starting from the edit form tutorial, I added the edit function on the controller, but when I try to print the form it gives me this error,

    Unable to guess how to get a Doctrine instance from the request information for parameter "invoice"

    below my configuration:

    - InvoiceRepository


    namespace App\Repository;

    use Doctrine\ORM\EntityRepository;

    class InvoiceRepository extends EntityRepository
    {
    public function findAllOrderByDate()
    {
    // $dql = 'SELECT inv FROM AppBundle\Entity\Invoice inv ORDER BY inv.InvoiceDate DESC';
    // $query = $this->getEntityManager()->createQuery($dql);
    // var_dump($query->getSQL());die;
    $qb = $this->createQueryBuilder('invoice')
    ->leftJoin('invoice.detail', 'detail')
    ->addOrderBy('invoice.InvoiceDate', 'DESC');
    $query = $qb->getQuery();
    //var_dump($query->getDQL());die;
    return $query->execute();
    }
    }

    - Entity/Invoice


    /**
    * @ORM\Entity(repositoryClass="App\Repository\InvoiceRepository")
    * @ORM\Table(name="invoice")
    * @UniqueEntity(fields={"InvoiceNumber"}, message="Invoice number exists")
    */
    class Invoice
    {
    /**
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    * @ORM\Column(type="integer")

    - InvoiceController


    /**
    * @Route("/invoice/{invoiceNumber}/edit", name="invoice_edit")
    */
    public function edit(Invoice $invoice, Request $request, EntityManagerInterface $em)
    {

    $form = $this->createForm(InvoiceFormType::class,$invoice,[

    ]);


    $form->handleRequest($request);

    Please can you help me?
    Thanks

  • 2020-01-10 Diego Aguiar

    Hey Joel Bushiri

    I believe the id of the article you are trying to access does not exist anymore. Could you double check that such id exists on your database?

    Cheers!

  • 2020-01-10 Joel Bushiri

    Hello, I'm getting a App\Entity\Article object not found by the @ParamConverter annotation. I followed the video without much changed to the code. How should I solve this?

  • 2019-11-23 Victor Bocharsky

    Hey Carlo,

    Even if you required a value for this field in the DB i.e. the field could not be NULL, you still need to allow null in your model (entity) in case you want to use Symfony Forms, that's kind of limitation. So, allow null in your strict types for this title field, I suppose you need to do it in setter/getter:



    public function getTitle(): ?string
    {}


    public function setTitle(?string $title)
    {}

    And then it should work. As an alternative solution - you can use a separate model (like DTO) for your form where you will allow null for this field and then map data to your entity in case you don't want to allow null in your entity.

    I hope this helps!

    Cheers!

  • 2019-11-21 Carlo Mario Chierotti

    Hello Ryan,

    sorry to disturb but I see something strange with validation in my edit form.

    In my entity I have a field "title" with @Assert\NotBlank() and validation works pretty well when I create a new record.

    When I edit an existing record, delete the content of "title" field and send the form, instead of a validation warning I get a 500 error with message "Expected argument of type "string", "NULL" given at property path "title".

    Thank you for your attention.

  • 2019-10-12 Dung Le

    Cancelled my question :)

  • 2019-08-27 Zach Bimson

    Thanks for posting and thanks Diego Aguiar for the reply! after reading the docs and installing the SensioFrameworkExtraBundle the magic kicked in!
    I worked around it with the EntityManager but i wasn't satisfied without getting the DI working!

  • 2019-08-19 Diego Aguiar

    Hey yvon Huynh

    Good questions :)
    Entities should be excluded from being auto-wired because they are just a data model, so you should not inject any services into their constructor

    > How does the id in the url translates into the Article object?

    There is a bit of magic that Symfony performs when you type-hint a controller's action with an entity class, it's called "Param converter".
    If you want to know more, you can watch this episode where Ryan explains it: https://symfonycasts.com/sc...
    Or, you can read the docs: https://symfony.com/doc/cur...

    Cheers!

  • 2019-08-19 yvon Huynh

    Hello,
    I make a similar project based on it, it says it cannot autowire Entities, in the service.yaml, indeed they are excluded, googling I see some parts saying it's normal, and they advise to create a manager class (I don't see what it is, they mean repository?), is it good practice to exclude Entities?
    One more question : How does the id in the url translates into the Article object?

  • 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"