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-11-29 weaverryan

    Ha, nice job thinking correctly! Being able to think clearly about what objects you need and how they are related are the most important thing you can do to keep things clearly. So, keeping going! My advice to keep things simple would be:

    A) Build your model first (as you were already doing/describing) - make it as flexible as you need, but not more flexible :).

    B) If your Symfony form gets too complex, stop using it. Symfony forms are great, but eventually, if you need a lot of flexibility and user interaction, then you really need to build a normal HTML+JS frontend that adds/updates/deletes data via AJAX by hitting API endpoints in your app. Sometimes I see people build HUGE forms, with many embedded layers, which is both hard, and ultimately not a great user experience (i.e. because it means that your user does a lot of work, and none of it saves until they hit the save button at the bottom).

    Good luck Benjamin!

  • 2016-11-28 Benjamin Quarta

    Hey weaverryan,

    that are great news! I will go for the screencasts and let you if know how it worked out for me :)

    For the following part: Apologies if my english may be strange now and then, but since I'm no native speaker, I'll try to explain the best I can :D...

    To answer your question: The problem is much more complicated as it sounds, because in the finished version of the application the user should have *both* possibilities. Just imagine the use-case:
    A user is adding a new Publication, let's say it is an album holding 10 Songs (the table is called "tracks"). Now these song's exist in the table. Half a year later the third song of the album (let's call it "Ducktrinity" :P) is re-released on a compilation, Now the user that adds the compilation should have the possibility to either add a new song, which is then directly added to the song-table or select one existing song from the songs that already exist.
    But that is a problem which is "step 2", and I first want to get the simpler case of just "multiplying" forms going.

    Later on it is getting even more complicated:
    The application builds the groundwork of a netlabel-application, which means that we have to add lot's of meta-data, which an enduser does not really need, such as "ISRC" (International Standard Recording Code), which is kind of an EAN/UPC on the album-side, so that the track can uniquely be recognized as *this* song on *this* publication in the whole wide world.

    This said, you can imagine, that it's getting more complicated, when it comes to joining tables. It is technically the same song, but the ISRC will differ. If I just build a joining table and select the same song, i won't cover the issue in a clean way. If I - on the other hand - make a new entry for the same song twice, then i will have some redundancy in my database, which i am aiming to avoid.

    So my approach will be the following: Extending the "join tables". As I don't see by now that doctrine gives me the possibility to add additional fields to a join table (please tell me if I'm wrong) I need to make the join table a real table with an entity itself, so that we've got the following table-setup:
    - publication (which holds all the data for the publication itself)
    - join_publication_track (which holds the publication_id, the track_id PLUS ISRC and whatever else can differ)
    - track (which holds all the data for the tracks)

    -------------------
    EDIT: I just catched a glimpse at the Doctrine course (Chapter 16) and i see my approach seems pretty much like that, what you cover here :D... Guess I'm intuitively doing the thing right... good sign :P
    -------------------

    Before I'm running off the track (pun intended :P) even more, I'll stop at this point (since there is a lot more to do as relating tracks to each other... think of live-versions, radio-edits, instrumental-versions, etc. of the same track), but you get an idea where this leads ;)

    And thanks to Leanna, Victor and you it seems to be more fun than ever :) Keep up your great work!

    best regards :)

  • 2016-11-28 weaverryan

    Hey Benjamin Quarta!

    Nice to meet you - and I'm happy you're getting more and more dangerous :p.

    You've described your problem really well and you've setup your existing ManyToOne form setup perfectly. Well done :). And I have good news about your problem! We're covering it right now here: http://knpuniversity.com/scree.... Specifically, you will be interested in the chapters 19-23. Now, the situation is *slightly* different, but all the information is there. What I mean is:

    A) In the tutorial, we first have a ManyToMany relationship. Here, we setup a form where the user checks checkboxes. In your app, this would be as if the user was simply *choosing* from existing "songs", not embedding a bunch of song forms.

    B) But later, we change to a "join table" setup (you'll see what I mean, it's chapter 16). Then, with this, we actually embed using the CollectionType.

    So, it sounds like you want to do what we do in part (B) from a form perspective, but with a ManyToMany relationship. I think if you go through those chapters, you'll have all the pieces you'll need (but you can ask here later if not).

    But, I have do have one question :). When the user is creating a new publication, are they choosing existing Songs from the database or creating new ones right then? If they're creating new ones, then perfect: you'll want the exact setup we're talking about here. But if you want the user to choose *existing* songs, then you'll actually want to follow the "checkboxes" setup instead... well... mostly. If you actually want the user to choose existing songs (instead of creating new ones), let me know and I'll give you more details.

    Cheers!

  • 2016-11-27 Benjamin Quarta

    Hello there Ryan,

    thank you for the awesome courses, which make me feel "seriously dangerous" again :D...
    Okay... not as dangerous as I'd like to be by now, so I've got a question ;)

    To give it a context:
    Since we're talking of Symfony and this sound's a lot like music (and we all like listening to music, right?), I'd like to build an application, where you can store albums and artists

    I found out that, if I have another table "bound" to the entity-class (this is the job of doctrine), I simply can pass the other form to the first one like in my ArtistFormType... In the builder I simply can do the following:

    ->add('address', AddressFormType::class)

    and get the required fields in the Twig-template like that: {{ form_row(artistForm.address.street) }}

    This works just fine, but now I've got another challenge, which makes me a bit dizzy...

    Since the upper example is a ManyToOne-Relationship, I only have to pass the nested form once, and this is clearly addressable.
    But now, if I'm not storing Addresses but Album-Releases (which I'll call publications, because "release" is a reserved word), i have a ManyToMany Relationship, since many Releases can have many Songs (not OneToMany, since the song can appear on another publication, let's say a compilation too)...

    If I fill out a "New-Publication-Form" I should have the possibility to add a bunch of songs all at once, so the SongFormType must appear "multiple times" or - let's think of the User-Experience - there should be an "Add Song-Button" which dynamically repeats the Song-Form within the Release-Form...

    I really have no idea by now, how to approach that issue :D... Ryan and his team always have great ideas how to solve issues best practice... maybe you've got one for me ;)...

    Thanks in advance :)

  • 2016-10-18 weaverryan

    Sweet! I'm happy it's clear :). Your plan makes sense to me!

    Also, once User A edits the record, if you want to *force* them to fill in the missing fields, you can conditionally make those required by using NotBlank and validation groups: https://knpuniversity.com/scre.... We do that in another tutorial to make some fields required on registration, but not required on a form that edits the user.

    Cheers and good luck!

  • 2016-10-18 Matt-You

    Hi there,

    Thank you for your answer! You are totally right, the problem comes from the database and it's that precise eror that I get. I knew that I could set it to "nullable=true" and allow another user (the admin for instance) to update the field (and set "@Assert\NotBlank" if I wanted the field to be updated anyway (the default value CANNOT be left like that). So, you made me think and came with this idea: I can set a default value that would be set automatically when the form creates a new entity. In my case, it's even better because it's more user-friendly. I explain myself: let's assume that user A is the admin and user B is a simple user of my platform. User B fills in the form (but he is allowed to fill only SOME fields, which are displayed). User A, then, edit the entity by completing the fields that user B were not allowed to do. Meanwhile, on a user B's page, I display the information that user B submitted and he/she waits for user A to "answer". So, it's better if by default, the field that user B couldn't fill in displays a value like "Waiting for user A to reply". And if I do that by default when the form is submitted, it's better both for my database and for my user B, C etc.

    All in all, just thank you very much for your answer! Everything is clearer now!

  • 2016-10-17 weaverryan

    Hey Matt!

    Thanks for the nice words man! And cool question - I have at least one answer for you, depending on your exact situation :).

    First, what you're doing is totally normal. In fact, it's really quite normal: it's really common to have a form that has some, but not all of your entity's fields mapped to it. When this happens, the *other* fields are just left completely untouched (i.e. if you're editing an entity, those fields will have the same value as when you fetched them from the database. If this is a new entity, they will remain null).

    In your situation, you say that you "get an error when submitting the form saying that I can't leave the field 'null'". From what you've told me, I assume that error is coming from the database - something like "Integrity constraint violation - Column cannot be null"? Is that accurate? If so, then I'm also guessing that this is a "new" form (not an edit form), otherwise this field would already be set in the database. And if *that's* true (I could be off-base by now), then the question is: how *should* this field be set? What I mean is, if the nullable=false property is not meant to be set in this form, but this form is creating a new entity that is meant to be inserted into the database, then we have a problem ... because nothing is ever setting this nullable=false field :).

    If I was right along my long line of assumptions, then you need to re-think this form: either that form should be nullable=true (because you *do* want it to be possible to create an entity from this form, but this user should not be able to set that value) *or* you'll need to set this value outside of the form (e.g. perhaps this user cannot edit this value, but that property should be given a default value, which you can do in the controller right before saving it).

    Or maybe I'm way off :). If so, let me know!

    Cheers!

  • 2016-10-17 Matt-You

    Hi everyone,

    First of all, thanks for the tutorials which are remarkable!

    My question is about the end of the course, when you say that we should not bind our forms to our entities. Well, I think I have a problem related to that. I'll explain: I created a form that hydrates an entity, but only partially (I only use "->add()" on some fields). One of the attributes of my entity is set to "nullable=false" by default; this attribute is not part of my form because only one type of users can actually hydrate it (with a dfferent form). Nevertheless, I get an error when submitting the form, saying that I can't leave the field "null" whereas this very field is not in my form (I didn't use "->add()" on it).

    Basically, my question is: can we hydrate an entity partially through a form, even though one of the attributes is set to "nullable = false"?

    I hope I'm clear...
    Thanks in advance!

  • 2016-10-13 Johan

    Yea, they solved a lot of problems I had with Laravel. For example, one of them is having duplicated code in edit/create views. In a project these two views are often very similar, but not quite the same. One needs pre-filled fields, the other does not and validation requirements are slightly different. I was never able to find a really elegant solution for it in Laravel, but I think Symfony forms solved all those problems, it's beautiful :)

    Time to rewrite all my Laravel projects in Symfony! Haha!

  • 2016-10-13 weaverryan

    And forms can be tough in some cases... but they do SO much work for you. Anytime I try to use something like Silex without Symfony forms... I regret it quickly.

    Good luck and cheers!

  • 2016-10-11 Victor Bocharsky

    Hey Johan,

    Haha, well, Laravel uses a lot of Symfony components inside itself, so I think Symfony will be very familiar to you ;)
    Easy and interesting learning!

    Cheers!

  • 2016-10-11 Johan

    I'm used to Laravel, but I think I just fell in love with Symfony <3

  • 2016-10-10 Victor Bocharsky

    OK, so speaking in terms, you have many-to-many relationship between Slide and Bank entities. Here's a link about this implementation using Doctrine ORM: https://knpuniversity.com/scre... . Is it appropriate for you?

    But with this approach, the SlideBank entity will be able to store only bank_id and slide_id. If you want to store a bit more information, like also a bank's name - you probably interested in a bit more complex relationships: Slide entity relates one-to-many to the SlideBank entity, which in turn relates many-to-one to the Bank entity, i.e. as a result you'll have the same many-to-many relationship between Bank and Slide, but you also will have ability to add more fields to the SlideBank entity except required bank_id and slide_id. This way you need 3 entities: Slide, Bank and SlideBank. I hope it makes sense for you.

    Cheers!

  • 2016-10-10 diarselimi92

    Hey Victor Bocharsky thanks for your response, but i think i can't do that !

    i have a table where i have some banks in it but the problem is that i don't have to relate anything to that table i only have to get the banks and save them in another table that is related to the Slide Table

    the stucture : | BANK | i will get the data from this bank table, then i have | SLIDE | table where i will save the some slides here , then i have a table | SLIDE_BANK | where i will save every bank that i selected for a | SLIDE | here , so one SLIDE have multiple BANKS. note: I will not save the bank's id but the name in it.

    I hope it is understandable.
    I know how to do it in the PHP Way but i wan't to go by the symfony's rules.

  • 2016-10-10 Victor Bocharsky

    Hey diarselimi92 ,

    You can make relations first and then pass the new object to the createForm() as a second argument, i.e. build form on a object which already has relations with other objects. Look at the next example:


    // Use entity manager and repository to fetch any stored data from any tables
    $category = $em->getCategoryRepository()->find($categoryId);

    $post = new Post();
    // Make relations fetched data
    $post->setCategory($category);

    $form = $this->createForm(PostType::class, $post)
    // handle form...

    In the example above, the new Post object will be attached to the existent category.

    I hope I understand you right and this example helps you. Let me know if you still have any questions.

    Cheers!

  • 2016-10-10 diarselimi92

    Hi
    I was working on a form with symfony and i now wan't to save data to a table but i wan't to get the data from another table,
    ex: Table1 is where i wan't to save main data and i have Table2 related with Table1 but i wan't to get The data from Table3 and save those to Table2, does anyone had this issue and how could this be solved ?
    Thanks :)

  • 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?