Buy

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():

81 lines src/AppBundle/Controller/Admin/GenusAdminController.php
... 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:

81 lines src/AppBundle/Controller/Admin/GenusAdminController.php
... 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:

81 lines src/AppBundle/Controller/Admin/GenusAdminController.php
... 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:

50 lines src/AppBundle/Form/GenusFormType.php
... 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:

81 lines src/AppBundle/Controller/Admin/GenusAdminController.php
... 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:

81 lines src/AppBundle/Controller/Admin/GenusAdminController.php
... 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":

45 lines app/Resources/views/admin/genus/edit.html.twig
... 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:

33 lines app/Resources/views/admin/genus/list.html.twig
... 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:

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

Alright. 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:

150 lines src/AppBundle/Entity/Genus.php
... 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!

  • 2016-11-30 Victor Bocharsky

    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!

  • 2016-11-29 Boutboul Michael

    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.

  • 2016-11-29 Victor Bocharsky

    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!

  • 2016-11-29 Boutboul Michael

    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

  • 2016-11-14 weaverryan

    Hey Terry!

    You're totally right to question this, and it just comes down to preference. In the Symfony world, we tend to do it with 2 different controllers. This has more duplication, but it's mostly "boilerplate" duplication - i.e. stuff that is always the same and doesn't really change often (so, the risk of needing to update something in one spot, but forgetting to update in the other is quite low). I then typically create private method and call those from both places if there is some extra logic that is shared. If you combine into one controller, then you just tend to have a few extra if statements here and there (e.g. an if statement to change the flash message to "Post Created" versus "Post Updated", or probably an if statement to change the template that's rendered). But, it's totally valid :).

    About the route handling, you have 2 options I believe!

    1) You could make the {id} optional as you said. I believe as long as you have "Genus $genus = null", then it won't cause a problem with the magic param converter (but I'm not 100% sure on this).

    2) A better option (but similar) would be just to add *two* @Route's above the controller, one with /genus/new, and one with /genus/edit/{id}. Then, making argument Genus $genus = null will definitely do the trick.

    Cheers!

  • 2016-11-12 Terry Caliendo

    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?

  • 2016-11-04 weaverryan

    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!

  • 2016-11-03 Johan

    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.

  • 2016-07-04 weaverryan

    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!

  • 2016-07-01 HelgeN

    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

  • 2016-06-24 Dominik

    Thank you so much for all the answers!

    Can't wait for another courses!

    Cheers!

  • 2016-06-24 Victor Bocharsky

    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!

  • 2016-06-24 Dominik

    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.

  • 2016-06-24 Victor Bocharsky

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

    And by the way, that was a good point!

    Cheers!

  • 2016-06-24 Dominik

    Hello!

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

  • 2016-06-24 weaverryan

    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!

  • 2016-06-23 Victor Bocharsky

    Hi, Dominik!

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

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

    Cheers!

  • 2016-06-23 Dominik

    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.

  • 2016-06-23 Dominik

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