Buy

Sharing Form Templates with include()

Adding edit was quick! But the entire template is now duplicated. This includes the code to render the form and the other blocks that include the needed CSS and JS files.

First, copy the form rendering code and move that into a new file: _form.html.twig:

13 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
{{ form_row(genusForm.name) }}
{{ form_row(genusForm.subFamily) }}
{{ form_row(genusForm.speciesCount, {
'label': 'Number of Species'
}) }}
{{ form_row(genusForm.funFact) }}
{{ form_row(genusForm.isPublished) }}
{{ form_row(genusForm.firstDiscoveredAt) }}
<button type="submit" class="btn btn-primary" formnovalidate>Save</button>
{{ form_end(genusForm) }}

Paste it here.

In edit, just include that template: include('admin/genus/_form.html.twig'):

34 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>
{{ include('admin/genus/_form.html.twig') }}
</div>
</div>
</div>
{% endblock %}

Copy that, open new.html.twig, and paste it there:

34 lines app/Resources/views/admin/genus/new.html.twig
... lines 1 - 22
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>New Genus</h1>
{{ include('admin/genus/_form.html.twig') }}
</div>
</div>
</div>
{% endblock %}

Ok, I'm feeling better. Refresh now: everything still looks good.

And by the way, if there were any customizations you needed to make between new and edit, I would pass a variable in through the second argument of the include function and use that to control the differences.

Using a Form Layout

So let's fix the last problem: the duplicated block overrides.

To solve this, we'll need a shared layout between these two templates. Create a new file called formLayout.html.twig. This will just be used by these two templates.

Copy the extends code all the way through the javascripts block and delete it from edit.html.twig:

14 lines app/Resources/views/admin/genus/edit.html.twig
... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Edit Genus</h1>
{{ include('admin/genus/_form.html.twig') }}
</div>
</div>
</div>
{% endblock %}

Paste it in formLayout.html.twig:

22 lines app/Resources/views/admin/genus/formLayout.html.twig
{% extends 'base.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.6.0/css/bootstrap-datepicker.css">
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.6.0/js/bootstrap-datepicker.min.js"></script>
<script>
jQuery(document).ready(function() {
$('.js-datepicker').datepicker({
format: 'yyyy-mm-dd'
});
});
</script>
{% endblock %}

So this template itself will extend base.html.twig, but not before adding some stylesheets and some JavaScripts. In edit, re-add the extends to use this template: admin/genus/formLayout.html.twig:

14 lines app/Resources/views/admin/genus/edit.html.twig
{% extends 'admin/genus/formLayout.html.twig' %}
... lines 2 - 14

Copy that, open new.html.twig and repeat: delete the javascripts and stylesheets and paste in the new extends:

14 lines app/Resources/views/admin/genus/new.html.twig
{% extends 'admin/genus/formLayout.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>New Genus</h1>
{{ include('admin/genus/_form.html.twig') }}
</div>
</div>
</div>
{% endblock %}

Try it! Cool! We're using our Twig tools to get rid of duplication!

A Word of Caution

Congrats team - that's it for our first form episode. You should feel dangerous. Most of the time, forms are easy, and amazing! They do a lot of work for you.

Let me give you one last word of warning: because this is how I see people get into trouble.

Right now, our form is bound to our entity and that makes this form super easy to use. But eventually, you'll need to build a form that does not look exactly like your entity: perhaps it has a few extra fields or is a combination of fields from several entities.

When you run into this: here's what I want you to do. Don't bind your form to your entity class. Instead, create a brand new class: I usually put these classes inside my Form directory. For example, GenusModel. This class will have the exact properties that your form needs.

Bind this class to your form and add all your validation rules like normal. After you submit your form, $form->getData() will return this other object. Then, it'll be your job to write a little bit of extra code that reads this data, updates your entities - or whatever else you need that data for - and saves things.

If you have questions, let me know in the comments.

There's certainly more to learn, but don't wait! Get out there and build something crazy cool!

Seeya guys next time!

Leave a comment!

  • 2016-09-12 Victor Bocharsky

    Ah, OK. So the correct way is pass the string value instead of array to the setter.

    The best practices is to use a "$plainPassword" property on User entity and add an event listener to encode the plain password to the password hash before update its value in DB. In the event listener you should encode plain password if only the $plainPassword property is NOT null. The only notice, that you should also change any mapped field of User entity, otherwise you will not get in the listener (it's because the $plainPassword is not mapped to the DB usually). So when you handle your form and set the new password to the $planPassword, you also should manually change $updatedAt field too (or any other mapped field, but usually, it's a $updatedAt field). It will be enough to trigger your event listener which in turn encode plain password and set its hash to the mapped $password property. But, first of all ensure you get the password inputed by user when handle the form.

    BTW, do you have any listeners which could update user entity after you? Check them all and ensure they do not update password field.

    P.S. You could see how to handle (encode) plain password with an entity listener in the next course Symfony Security: Beautiful Authentication, Powerful Authorization. Please, take a look at it.

    Cheers!

  • 2016-09-12 james

    I already tried it also, same result:

    string(8) "password" NULL

    Which I really can't get it...

    $userPassword = $formForgotPassword->getData();

    var_dump($userPassword["plainPassword"]);

    $user->setPassword($userPassword["plainPassword"]);
    $em = $this->getDoctrine()->getManager();
    $em->persist($user);
    $em->flush();

    var_dump($user->getPassword());

    die();

  • 2016-09-12 Victor Bocharsky

    Hey James,

    I think I see the problem ;). You dump $userPassword["plainPassword"] string value *but* then set $userPassword array to the user object. So try to change 3rd line to:


    $user->setPassword($userPassword["plainPassword"]);

    Cheers!

  • 2016-09-10 james

    Hi,

    Another question on another subject, I have my login form and register form, all good until I try to create the forgot password form. When the user click on forgot password, an email is sent to his email address with a link, the link has got his email address and a token created just before the email was sent. When the user clicks the link he is redirected to a page with a form to renew his password, same password part of the form than in the register form. But when the user enters the password and the repeat password (I changed the encoder to plaintext):

    Code:

    $userPassword = $formForgotPassword->getData();

    var_dump($userPassword["plainPassword"]);

    $user->setPassword($userPassword);

    $em = $this->getDoctrine()->getManager();

    $em->persist($user);

    $em->flush();

    var_dump($user->getPassword());

    die();

    Result:

    "password" NULL

    What is the problem?

  • 2016-08-30 Victor Bocharsky

    Hey James,

    The second your example is correct. What does the quantities is? The second argument for path() Twig function should be an associative array, like {param1: 'value1', param2: 'value2'} if you declare your parameters in Twig. Try to set dummy data first and look if it works:


    {{ path('update_cart', {param1: 'value1', param2: 'value2'}) }}

    You should get some URL like this: "/your-update-cart-url?param1=value1&param2=value2", and then in action you can get access to these params via request:

    dump($request->query->get('param1')); // will dump "value1" string

    Cheers!

  • 2016-08-30 james

    Thanks for the quick answer! I tried something like this:

    {{ path('update_cart', {quantities}) }}
    and
    {{ path('update_cart', quantities) }}

    But it does not work, how do you insert the array as a parameter?

    Also when trying to get the value of the quantity input ( {{ dump(form.quantity.vars.value) }} ) it does seem to work...

  • 2016-08-30 Victor Bocharsky

    Hey James,

    Thanks to the Symfony Router component, it takes an array of parameters as a second argument, so you don't need to merge and encode your values, just pass your array as a second parameter to the "path()" or "url()" Twig functions and router do the job for you. If your route has some required parameters, then merge your parameters with it.

    Cheers!

  • 2016-08-30 james

    Hi,

    Just a question concerning request of values form the twig template to a controller, is there any other way to get the values of twig template directly to our controller? I have for example a cart and a button to update the cart but form would not work because the quantity field will be included in each loop to get each product and so will appear as many time I have products. Any ideas?

    At the moment as a solution I am creating an array in twig, then merging elements to it, then encode it and add it to the url (/{{ items }}) and in my controller I can then decode the json and get my values. Complicated? maybe...

  • 2016-08-09 Victor Bocharsky

    Hey,

    Yes, Symfony plugin adds this method for backward compatibility, but you should avoid using it and since Symfony 2.8, the name defaults to the fully-qualified class name, i.e. Symfony generates this name for you behind the scene based on your FQCN. This class name is internal and used for generate unique form element names and CSS IDs. You could find it if inspect your form HTML code.

    Cheers!

  • 2016-08-09 3amprogrammer

    Hey weaverryan!
    After watching this series I am a little confused about getName method. I was trying to find the moment when you have added this, but there is no one (or I've missed it). Can you please explain it to me briefly what is the purpose of:


    public function getName()
    {
    return 'app_bundle_genus_form_type';
    }

    @Edit
    I have found out that this method was added by Symfony Plugin. Anyway is there any reason for it?