Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Easy 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

When you get to the form, it's completely blank. How could we add default data to the form?

Well, it turns out answering that question is exactly the same as answering the question

How do we create an edit form?

Let's tackle that.

In GenusAdminController, I'm going to be lazy: copy the entire newAction() and update the URL to /genus/{id}/edit. Give it a different route name: admin_genus_edit and call it editAction():

... lines 1 - 13
class GenusAdminController extends Controller
{
... lines 16 - 55
/**
* @Route("/genus/{id}/edit", name="admin_genus_edit")
*/
public function editAction(Request $request, Genus $genus)
{
... lines 61 - 79
}
}

Our first job should be to query for a Genus object. I'll be lazy again and just type-hint an argument with Genus:

... lines 1 - 4
use AppBundle\Entity\Genus;
... lines 6 - 8
use Symfony\Component\HttpFoundation\Request;
... lines 10 - 13
class GenusAdminController extends Controller
{
... lines 16 - 58
public function editAction(Request $request, Genus $genus)
{
... lines 61 - 79
}
}

Thanks to the param converter from SensioFrameworkExtraBundle, this will automatically query for Genus by using the {id} value.

Passing in Default Data

This form needs to be pre-filled with all of the data from the database. So again, how can I pass default data to a form? It's as simple as this: the second argument to createForm is the default data. Pass it the entire $genus object:

... lines 1 - 13
class GenusAdminController extends Controller
{
... lines 16 - 58
public function editAction(Request $request, Genus $genus)
{
$form = $this->createForm(GenusFormType::class, $genus);
... lines 62 - 79
}
}

Why the entire object? Because remember: our form is bound to the Genus class:

... lines 1 - 13
class GenusFormType extends AbstractType
{
... lines 16 - 42
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Genus'
]);
}
}

That means that its output will be a Genus object, but its input should also be a Genus object.

Behind the scenes, it will use the getter functions on Genus to pre-fill the form: like getName(). And everything else is exactly the same. Well, I'll tweak the flash message but you get the idea:

... lines 1 - 13
class GenusAdminController extends Controller
{
... lines 16 - 58
public function editAction(Request $request, Genus $genus)
{
... lines 61 - 64
if ($form->isSubmitted() && $form->isValid()) {
... lines 66 - 71
$this->addFlash('success', 'Genus updated!');
... lines 73 - 74
}
... lines 76 - 79
}
}

Rendering the Edit Form

Update the template to edit.html.twig:

... lines 1 - 13
class GenusAdminController extends Controller
{
... lines 16 - 58
public function editAction(Request $request, Genus $genus)
{
... lines 61 - 76
return $this->render('admin/genus/edit.html.twig', [
'genusForm' => $form->createView()
]);
}
}

I'm still feeling lazy, so I'll completely duplicate the new template and update the h1 to say "Edit Genus":

... lines 1 - 22
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Edit Genus</h1>
... lines 28 - 40
</div>
</div>
</div>
{% endblock %}

Don't worry, this duplication is temporary.

Finally, in the admin list template, I already have a spot ready for the edit link. Fill that in with path('admin_genus_edit') and pass it the single wildcard value: id: genus.id:

... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
... lines 8 - 13
<table class="table table-striped">
... lines 15 - 19
{% for genus in genuses %}
<tr>
... lines 22 - 23
<td>
<a href="{{ path('admin_genus_edit', {'id': genus.id}) }}" class="btn btn-xs btn-success"><span class="fa fa-pencil"></span></a>
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
{% endblock %}

LOVE it. Open up /admin/genus in your browser.

Ah good, an explosion - I felt like things were going too well today:

Method id for object Genus does not exist in list.html.twig at line 25.

So apparently I do not have a getId() function on Genus. Let's check it out. And indeed, when I created this class, I did not add a getter for ID. I'll use command+N, or the "Code"->"Generate" menu to add it:

... lines 1 - 12
class Genus
{
... lines 15 - 68
public function getId()
{
return $this->id;
}
... lines 73 - 148
}

All right. Let's try it again. Refresh. No errors!

Edit the first genus. Check that out: it completely pre-filled the form for us. In fact, check out this weird "TEST" text inside of funFact. That is left over from an earlier tutorial. I hacked in this TEST string temporarily in getFunFact() so we could play with markdown. This proves that the form is using the getter functions to pre-fill things.

So, that's really interesting but let's take it out:

... lines 1 - 12
class Genus
{
... lines 15 - 106
public function getFunFact()
{
return $this->funFact;
}
... lines 111 - 148
}

Refresh. Change the "Fun fact" to be even more exciting, hit enter, and there it is:

Genus updated - you are amazing!

Edit that Genus again: there's the new fun fact. This is a really cool thing about the form framework: the new and edit endpoints are identical. The only difference is that one is passed default data.

So this is great! Except for the template duplication. That's not great still.

Leave a comment!

51
Login or Register to join the conversation
Jim-S Avatar

Hi! Very helpful tutorial. Had a question on form edit. I know we pass entity to the form class to pre-populate all field for us. I am in a situation, one of the field is a json string stored in database and it needs to be extracted in array before populating for Choice field(multi select)
(data_class with entity is optional as mentioned in one of the tutorial)
Wanted to see an example how we can manually pre-populate the edit form fields without passing entity class . Thanks

Reply

Hey Jim-S,

This is a legacy tutorial, we already have a fresher one on Symfony 4, you can find it here: https://symfonycasts.com/screencast/symfony-forms . But if your project is written in Symfony 3 - then it should be good for you I think.

What about your question, you can set choices with the "choices" key, see https://symfony.com/doc/current/reference/forms/types/choice.html#choices

If you store possible choices in the entity, you can pass the entity into the form type and convert JSON string of possible choices into a PHP simple array that you will pass to that "choices" key as its value.

I hope this helps!

Cheers!

Reply
Jennifer K. Avatar
Jennifer K. Avatar Jennifer K. | posted 4 years ago

I'm using this tutorial to create something in an older version of Symfony (2.8). Creating the editAction() with Request and and Entity parameters doesn't work with the route (path with the id parameter), however. The error I get is "Controller ...requires that you provide a value for the "$entity" argument (because there is no default value or because there is a non optional argument after this one).

I'm guessing that the resolver magic in Symony 3 is missing in this version and it doesn't know how to convert the id to the Entity automatically. How can I do this in Symfony 2.8? Do I need to make the Entity parameter of the editAction() method into an int parameter - id?

Update: I updated the params and the routes to all have the same parameter name, and then I get a more informative error: Argument 2 passed to ...editAction() must be an instance of Entity, string given

Reply

Hey Jennifer K.

The param converter feature exists since Symfony2, probably you are doing something wrong, check this guide: https://symfony.com/doc/mas...

Tip: The wildcard of your route must match an unique field on your entity

Cheers!

Reply
Jennifer K. Avatar

Thanks for the tip!

1 Reply
Default user avatar
Default user avatar Dominik | posted 5 years ago

Hello!
"LOVE it. Open up /admin/genus in your browser."
U mean /genus probably, coz in GenusAdminController there is

* @Route("/genus", name="admin_genus_list")

Reply
Default user avatar

And i got another question:
we got duplicated code in controller. Can we do sth, about it? It's totally awful.
In next chapter we learn about do sth about this in .twig, not in php files.

Reply

I'm glad you dislike the duplication actually :). There are 2 basic approaches to this:

A) Create your own BaseController class (that itself extends Symfony's Controller) and add any helper methods that you want inside of there. You can add whatever methods you want to cut-down on the boilerplate of the forms code.

B) Do the same basic thing, but put the code in some outside service. These are often called "Form handlers" - I don't do this approach, but a lot of people do.

Cheers!

Reply
Default user avatar

Soo....
I have to add BaseController which extends Controller (symfony one) and then GenusAdminController extends the BaseController, right?

But how about this solution:
create private method inside a GenusAdminController:

public function generateAdminForm($action = null) {
//code here
//$action will take care for addFlash (we got 2 different massage).
if ($action){
flash for new

} else {
flahs for edit

}

}

Your second solution - it's too high lvl for me right now.

Reply

Hey, Dominik!

Yes, you caught it right!

Well, you can create a private method in the GenusAdminController to reuse it in other methods of this contoller. But! What if you need the same method in another controller? You got the idea? In this case better to move this reusable method to a base controller, and then extend it by whatever controller where you need this. Of course, if you have only one contoller in whole project - it looks pointless, but on practice, you probable have many of them, where you need to reuse some common logic between them.

Cheers!

Reply
Default user avatar

Thank you so much for all the answers!

Can't wait for another courses!

Cheers!

Reply

Hi, Dominik!

That's because we have a "/admin" prefix for whole GenusAdminController:

/**
 * @Route("/admin")
 */
class GenusAdminController extends Controller

Cheers!

Reply
Default user avatar

Hello!

Thank you for explain this. I think its necessary because in next course where're going to do some authorization, right?

Reply

Yes, exactly! We'll close this security hole in the next episode! Keep learning! ;)

And by the way, that was a good point!

Cheers!

Reply
Dirk Avatar

What if you would like to create a static controller to edit a specific genus with a known id? So if you want to edit 'genus1' the route is something like "/genus1/edit" and in the controller you would like to only be able to edit this one genus. How would you do this? My best, Dirk

Reply
Dirk Avatar

I think I managed to do so with:

$id = 1;
$em = $this->getDoctrine()->getManager();
$genus = $em->getRepository(Genus::class)->find($id);

And I had to remove the 'genus $genus' from the method. Not sure why, but it works :)

Reply

Hey Dirk

A bit odd use case :) Can I know why you need to do something like that?
You had to remove it because when Symfony try to invoke your controller method, it will see that that method requires an argument coming from your route, but your route does not contain any parameter, so it will get confused and throw an exception

Cheers!

Reply
Dirk Avatar

Hi Diego,

Sorry, only just now that I log in again I see your reply! I have forgotten why I wanted to achieve this, sorry. Thank you for your time.

Reply

Haha, no worries, maybe some day you will remember it ;)

Reply
Default user avatar

Hi, i love these tutorials, but i was really hoping to see one about how to embed a collection of forms and dynamically adding and removing fields.

any chance of getting a tutorial on this topic?

cheers

Reply

Thanks for the nice words! And yes, we'll definitely do that - hopefully in the next couple of months - along with a more advanced form theming tutorial :)

Cheers!

1 Reply
Default user avatar

I'm continually amazed by all the 'magic' Symfony does behind the scene for common web tasks + CRUD operations, it's sweet. But then I kinda want to know how it works :)

For instance, how does editAction() in GenusAdminController.php know that the {id} token should be used to a) get a Genus object (and not, say, a Request object, silly as that sounds) and b) use the ID field to do so?

Does the token name 'id' matter then? What if we used {genusID} in there?

I presume it tries to use the getVARNAME() function on each in turn, until it finds one - then somehow realizes this Entity can be retrieved by the id?

What if you had other params in the editAction() method, wouldn't it get confused?

Reply
MolloKhan Avatar MolloKhan | SFCASTS | Goz | posted 5 years ago | edited

Hey Goz

Those are great questions, let me see if I can help you:

>> how does editAction() in GenusAdminController.php know that...

When you type hint an argument of any controller's action with an entity, Symfony's Controller Resolver will expect an ID or any other unique field being defined in your route, then, that parameter must match to the property name, e.g. If you create a route like this "/users/{id}" for showing a user details, then, in your User class, you must have a property named "id", and it must be unique (In this case, an id is expected to be unique, but you could change it to use a slug for example). This magic is called "ParamConverter", you can read more info about it here: http://symfony.com/doc/curr...

>> What if you had other params in the editAction() method, wouldn't it get confused?

You can inject all the paramters you want, but it may require more configuration, as you can read in the link I posted above

I hope it helps you clarifying things a bit

Cheers!

Reply
Default user avatar
Default user avatar Goz | MolloKhan | posted 5 years ago | edited

This is REALLY great info - thanks so much MolloKhan!

Reply
Default user avatar
Default user avatar Luka Sikic | posted 5 years ago

Hello there,

What if we have a file upload in the form? Here's the error that I get:
"The form's view data is expected to be an instance of class Symfony\Component\HttpFoundation\File\File, but is a(n) string. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms a(n) string to an instance of Symfony\Component\HttpFoundation\File\File." What can I do about this?

Reply
Default user avatar

Ok, so basically I went to the FormType file and added array with "data" argument to null:

->add('thumbnail', FileType::class, ['data' => null]);

I hope this is a good approach.

Reply

Yo Luka Sikic!

Interesting. I've not seen this error before! I'm not sure about your fix - if it works, awesome. I might know the real issue .Is your thumbnail field a string field that's saved to the database? If so, typically, in Symfony, we add a second, non-persisted field (e.g. thumbnailFile) and use that as the field on the form. The underlying issue is that "thumbnail" is a string (e.g. foo.jpg), but when you submit the form, the form gives you an UploadedFile object... which you then need to do some work with to move the file and get the final filename (which you then set on the thumbnail property). By setting data to null, I think you're not harming anything: you're just telling the form to ignore the fact that this property starts as a string (and I'm guessing you still have logic somewhere to move the uploaded file and re-set the thumbnail property to a string).

Let me know if any of this makes sense ;)

Cheers!

Reply
Default user avatar

Yeah, It makes sense, thumbail is string but form type is requiring file. I get it. What type should be thumbnailFile column?

Reply

Awesome :). The thumnbnailFile property in your entity will NOT be persisted to Doctrine. So, it will just be a normal property with NO @ORM\Column annotation. It exists *only* to help you work with your form. It's kind of an annoying detail, but that's how it should work.

Cheers!

Reply

Hello i download course script and copied properly into my directory. i migrated database. and loaded fixtures but when i try to visit /admin/genus i got an error it says

Impossible to access an attribute ("name") on a string variable ("Schmeler") in admin/genus/list.html.twig at line 23

on admin list html file 23 genus.subFamily.name is passed. but on db genus table subfamily field there are names, ithink it should be id of sub families but fixture load names. i couldnot fix the error . plz help. thank you

Reply

Hey msthzn

This is kind of weird, how did you executed the migrations?
Could you recreate your database / load fixtures and try again? Something went wrong while setting up the project

Cheers!

Reply
Default user avatar
Default user avatar Mohammad Althayabeh | posted 5 years ago

Hi, subFamily does not show up on the Form on edit, I dumped $genus and the subFamily value is there. All fields are filled except Subfamily is not selected. Any idea why this is happening. Thank you

Reply

Yo Mohammad Althayabeh!

Hmmm. Is the subFamily field completely *absent* on the page (i.e. not in the form at all), or is it simply that the *value* is missing from the box? If the field is there, is it a drop-down field? And do you see the sub families in that drop down? And finally, what value do you see when you dump $genus? By the way, that was good detective work to dump $genus :). EasyAdminBundle is simply using the form system behind the scenes, so if you have a form field called "subFamily" and a property called "subFamily" with data, then the form system should take care of the rest. So, something is definitely strange :).

Cheers!

Reply
Default user avatar
Default user avatar Mohammad Althayabeh | weaverryan | posted 5 years ago

well, the field is there like all the fields the only difference is that it is not retrieving the value and it's just showing the default "Choose an option". it is dropdown and the value is in the list it's just not selected. I am not using EasyAdminBundle yet, i am just following the tutorial. So again, everything seems to be right except the display.

Thanks for your help :)

Reply

Hey Mohammad Althayabeh

When you dumped Genus, did you see it's SubFamily field is not null? If that not the case, I believe you may have forgotten to pass the Genus object while creating the form

$form = $this->createForm(GenusFormType::class, $genus);```

Reply
Default user avatar
Default user avatar Mohammad Althayabeh | MolloKhan | posted 5 years ago

Yes I see the value when i dump the Genus Object, and yes I passed $genus. If I don't pass $genus then all field will be empty. The issue here is all fields filled with their data except SubFamily is not selected the right choice and selected the placeholder instead "Choose an option"

Reply

Yo Mohammad Althayabeh!

Hmm, ok! I think we can figure this out :). When you dump the subFamily field, does this return a string or an object? Because the subFamily field is the EntityType in the form, in order for the correct options to be chosen, the *value* of that property should be a SubFamily *object*. I believe that during the first few Symfony tutorials (e.g. https://knpuniversity.com/s..., the subFamily property was just a string. Then, before one of our tutorials (maybe this one), I converted that to a true relation to make the tutorial more interesting. So, if the value for this field is a *string*, that's the problem! Try downloading the fresh start code and using that.

Let me know if that's the issue or not! And yea, sorry about the EasyAdminBundle comment - I got lost on what tutorial we were chatting about!

Cheers!

Reply
Default user avatar
Default user avatar Mohammad Althayabeh | weaverryan | posted 5 years ago | edited

That's correct, I see it is a string not an object. However, I checked the GenusEntity and it has SubFamily with relationship :
`
/**

 * @Assert\NotBlank()
 * @ORM\ManyToOne(targetEntity="AppBundle\Entity\SubFamily")
 * @ORM\Column(nullable=false)
 */
private $subFamily;

`

What should I do to return an object? thank you for the suggestion about downloading the files but I am trying to track all changes and do them along with the tutorial.

Thanks again :)

Reply

Hey Mohammad Althayabeh!

Ah, ok! I see it! Removing the @ORM\Column. That is confusing Doctrine: it's mapping it both as a normal column... and a relationship. You'll also need to reset your database after doing this, as it will likely need to add some foreign-keys etc.

To make sure you've got everything the same as when we start the tutorial, you could at least download the "start" code and compare your Genus.php with the Genus.php from that download. You will see a few changes, including the subFamily field changes and also a new firstDiscoveredAt column :).

Cheers!

Reply
Default user avatar
Default user avatar Mohammad Althayabeh | weaverryan | posted 5 years ago

Thanks man, it is working perfectly. Is there a reason that @ORM\Column causing an issue? Sorry I know I asked too much questions :)

Reply

Hey Mohammad Althayabeh!

Woohoo! Great news! Basically, the problem is that you can't have both @ORM\Column and also @ORM\ManyToOne - they conflict. Let me give you a slightly different example to explain. Imagine you accidentally had this:


/**
 * @ORM\Column(name="sub_family_col1", type="string")
 * @ORM\Column(name="sub_family_col2", type="string")
 */
private $subFamily

This is technically legal, but you've now mapped this one property to 2 columns! If you saved a new entity, I think it might work - it would just take the subFamily value and put it on both sub_family_col1 and sub_family_col2. But, what happens when you query for this entity? Which value should go onto the subFamily property - the value in sub_family_col1 or the value in sub_family_col2?

That's more or less the problem you had before. With the Column and ManyToOne, we were simultaneously telling Doctrine "subFamily is a string column" and also "subFamily is an integer column, and it's a foreign key, so the integer value should be converted into a SubFamily object". This really should be an error, but instead, I think the second annotation simply "won".

Cheers!

Reply
Default user avatar
Default user avatar Boutboul Michael | posted 5 years ago

Hello !
How could I make the same thing on post method ?
For Genus it's ok but for users... A bit lack of security ;)
An other problem : how can we update in an edit form the plainPassword ? It does not work like the other fields !
See you
Michael

Reply

Hi Michael,

What do you mean? Actually, Symfony forms use POST method by default. And what lack of security exactly are talking about?

It's simple: what you really want to update is the password field which stores in DB, not plainPassword. you need to add a plainPassword property for you edit form, which is not mapped, i.e. "mapped" => false, check mapped option. Also you need an event listener which will encode plainPassword value if it's not set to null and store result hash to the password property, which is stored in DB. Please, check a few chapters on our "Symfony Security: Beautiful Authentication, Powerful Authorization" tutorial starting from Users Need Passwords (plainPassword) at least. Btw, you probably would like to use RepeatedType field instead of single HiddenType to be sure that users type correct password.

Cheers!

Reply
Default user avatar
Default user avatar Boutboul Michael | Victor | posted 5 years ago

Sorry, I try to explain myself better :
For my first problem, I am talking about the "Genus id" we pass through the get method with Admin/{id}/edit. Can we pass this id with form method ?
For my second problem : I've made the whole user tuto (Symfony Security: Beautiful Authentication, Powerful Authorization) and my code work for create users but for my update form it does not work when I do this tutorial with the User form.
I can add code do a response if necessary.
I tried to :
->add('plainPassword', RepeatedType::class,
[ 'type' => PasswordType::class,
'mapped' => true])
but it does not work better.

Reply

Ah, now I see. Actually, it's a bit unusual, because because ID is something which generates in DB (auto-incremented) and then doesn't change during the whole data life. But forms (especially if you send these forms by POST method) mean that you can change data inside a form. So it's not a good idea I think, but it's possible and probably you have some good use case for it. Just double think about it, do you really need it?

Yes, I see an error in your code. As I said in my previous response, you should use "mapped" option and set it to "false", because the plainPassword field does not store in a DB, it's just an auxiliary field which is not mapped.

Cheers!

Reply
Default user avatar

I see you always put the POST and GET part in one controller method. I'm used to having one method for the GET request and one for example for the POST request.

On one hand I think this separates the code a little better and keeps it manageable but on the other hand I often notice that it's easy to get duplicated code.

Reply

Hey Johan!

This is a question that people ask from time-to-time, because it's different than you see in other frameworks. Long story short, you can (of course) do it either way :). It's done this way because (A) [as you correctly mentioned] it helps reduce boilerplate duplications and (B) [more importantly] when I originally helped write the official Symfony documentation, I chose to do it this way... and so everyone has been doing it this way ever since :). I still think it's the best way... but there's no huge reason why you should choose one vs the other.

Cheers!

1 Reply
Mike P. Avatar
Mike P. Avatar Mike P. | posted 5 years ago

Ive added a new "publicUsername" field which is in itself unique. Does it make sense to set it unique?

ORM\Column(type="string", unique=true

Or should only be the ID set to unique because of performance / db storage consumption?
(Or course the set publicUsername method does have a check that only unique values gets inserted)

Reply

Hey Mike P.

As I know if you declare your field unique, you are setting a constraint at the database level, so it will throw a DB exception whenever you try to insert a duplicated value, so would be wise to add an unique validation too.
More info here: https://symfony.com/doc/cur...

Cheers!

1 Reply
Default user avatar
Default user avatar Terry Caliendo | posted 5 years ago

You talk about your template having duplicate code, which you later go back and reduce. But aren't you doing a lot of duplication in your controller between the "new" and "edit" that could be reduced?

Couldn't they be handled by the same route/function? Just make the "id" parameter optional. If its populated, its an "edit", if its null, then its a "new".

That then leaves the auto populate of the Genus Entity on the "edit" side. Can you make that parameter optional? Or you could just do the manual query if the "id" field isn't null.

Or do things tend to get too complicated in this direction?

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice