Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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!

30
Login or Register to join the conversation
davidmintz Avatar
davidmintz Avatar davidmintz | posted 1 year ago | edited

This may be a little pedantic, but: I don't think your controller's edit method needs to say $em->persist($article), does it? Doctrine already knows we want to persist, yes?

Reply

Hey @David!

You’re 100% correct. I used to do this… but these days I typically just use flush() (but persist was never needed, except for new objects).

Cheers!

Reply

Is it also possible to have the edit-form appear as a popup over the article listing?

Reply

Hey RobinBastiaan

Yes, it's possible. You only need to make an AJAX request to your backend to fetch the form's data (as HTML or JSON if you want to style up the form on your frontend). Then, just create such endpoint for fetching the form's data.

Cheers!

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 2 years ago

Why would you include the _form.html.twig but on some other parts you create a html.twig files with 'extends base.html.twig' and parent(). Why wouldnt you just include everything like _form.html.twig?

Reply

Hey @Farry7

It depends on purpose. If there is a possibility that you will re-use your form in other place, than using include _form.html.twig you can include it everywhere, however if you have full template which extends your base, than you can use it only as main template of action. BTW simple templates without base can be easily updated via ajax actions!

Cheers!

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 2 years ago

Too many arguments to "doctrine:query:sql" command, expected arguments "sql".

Reply

Hey @Farry7!

Hmmm. Make sure you have the query surrounded by quotes - otherwise it will look like individual arguments to the command. If single quotes are working for some reason, try double-quotes. That "*" character in the query can sometimes confuse terminal systems (as it can be a special character).

Let me know if that helps!

Cheers!

Reply
Gianluca M. Avatar
Gianluca M. Avatar Gianluca M. | posted 3 years ago | edited

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,

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

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
Reply

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!

1 Reply
Gianluca M. Avatar
Gianluca M. Avatar Gianluca M. | Gianluca M. | posted 3 years ago | edited

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);
Reply

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!

Reply
Default user avatar
Default user avatar Joel Bushiri | posted 3 years ago

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?

Reply

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!

Reply

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.

Reply

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!

Reply
Dung L. Avatar
Dung L. Avatar Dung L. | posted 3 years ago

Cancelled my question :)

Reply
Benoit L. Avatar
Benoit L. Avatar Benoit L. | posted 4 years ago

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?

Reply
Zach B. Avatar
Zach B. Avatar Zach B. | Benoit L. | posted 3 years ago | edited

Thanks for posting and thanks Benoit L. 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!

1 Reply

Hey Benoit L.

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!

Reply

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.

Reply

Hey Ginius

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!

Reply

I understand what you meaning. My code is as follow:
<br /><button id="form_save" name="form[save]" type="submit" class="btn btn-success btn-sm">Save</button><br /><button id="form_close" name="form[close]" type="button" class="btn btn-sm" onclick="document.location.href='/admin/klanten/lijst';">Close</button><br />

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

Reply

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

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

Reply

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

Reply

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

Reply
Default user avatar

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"

Reply

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!

Reply
Default user avatar
Default user avatar dave | weaverryan | posted 4 years ago | edited

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
        ]);
Reply

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!

Reply
Cat in space

"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
        "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
    }
}
userVoice