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
}

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:

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!

  • 2017-10-13 weaverryan

    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!

  • 2017-10-12 Mohammad Althayabeh

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

  • 2017-10-12 weaverryan

    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!

  • 2017-10-12 Mohammad Althayabeh

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

  • 2017-10-12 weaverryan

    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!

  • 2017-10-12 Mohammad Althayabeh

    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"

  • 2017-10-11 Diego Aguiar

    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);
  • 2017-10-11 Mohammad Althayabeh

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

  • 2017-10-11 weaverryan

    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!

  • 2017-10-11 Mohammad Althayabeh

    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

  • 2017-09-12 Diego Aguiar

    Hey Mesut Hazen

    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!

  • 2017-09-12 Mesut Hazen

    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

  • 2017-08-21 Diego Aguiar

    Hey Mike

    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!

  • 2017-08-20 Mike

    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)

  • 2017-08-16 weaverryan

    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!

  • 2017-08-15 Luka Sikic

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

  • 2017-08-15 weaverryan

    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!

  • 2017-08-14 Luka Sikic

    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.

  • 2017-08-14 Luka Sikic

    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?

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