Buy

Now that we've added the yearsStudied field to each GenusScientist, I'm not too sure that checkboxes make sense anymore. I mean, if I want to show that a User studies a Genus, I need to select a User, but I also need to tell the system how many years they have studied. How should this form look now?

Here's an idea, and one that works really well the form system: embed a collection of GenusScientist subforms at the bottom, one for each user that studies this Genus. Each subform will have a User drop-down and a "Years Studied" text box. We'll even add the ability to add or delete subforms via JavaScript, so that we can add or delete GenusScientist rows.

Creating the Embedded Sub-Form

Step one: we need to build a form class that represents just that little embedded GenusScientist form. Inside your Form directory, I'll press Command+N - but you can also right-click and go to "New" - and select "Form". Call it GenusScientistEmbeddedForm. Bah, remove that getName() method - that's not needed in modern versions of Symfony:

37 lines src/AppBundle/Form/GenusScientistEmbeddedForm.php
... lines 1 - 2
namespace AppBundle\Form;
... lines 4 - 8
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class GenusScientistEmbeddedForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 17 - 26
}
public function configureOptions(OptionsResolver $resolver)
{
... lines 31 - 33
}
}

Yay!

In configureOptions(), add $resolver->setDefaults() with the classic data_class set to GenusScientist::class:

37 lines src/AppBundle/Form/GenusScientistEmbeddedForm.php
... lines 1 - 4
use AppBundle\Entity\GenusScientist;
... lines 6 - 12
class GenusScientistEmbeddedForm extends AbstractType
{
... lines 15 - 28
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => GenusScientist::class
]);
}
... lines 35 - 36
}

We will ultimately embed this form into our main genus form... but at this point... you can't tell: this form looks exactly like any other. And it will ultimately give us a GenusScientist object.

For the fields, we need two: user and yearsStudied:

37 lines src/AppBundle/Form/GenusScientistEmbeddedForm.php
... lines 1 - 12
class GenusScientistEmbeddedForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user', EntityType::class, [
... lines 19 - 23
])
->add('yearsStudied')
;
}
... lines 28 - 36
}

We do not need a genus dropdown field: instead, we'll automatically set that property to whatever Genus we're editing right now.

The user field should be an EntityType dropdown. In fact, let's go to GenusFormType and steal the options from the genusScientists field - it'll be almost identical. Set this to EntityType::class and then paste the options:

37 lines src/AppBundle/Form/GenusScientistEmbeddedForm.php
... lines 1 - 5
use AppBundle\Entity\User;
use AppBundle\Repository\UserRepository;
... lines 8 - 12
class GenusScientistEmbeddedForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user', EntityType::class, [
'class' => User::class,
'choice_label' => 'email',
'query_builder' => function(UserRepository $repo) {
return $repo->createIsScientistQueryBuilder();
}
])
... line 25
;
}
... lines 28 - 36
}

And make sure you re-type the last r in User and auto-complete it to get the use statement on top. Do the same for UserRepository. The only thing that's different is that this will be a drop-down for just one User, so remove the multiple and expanded options.

Embedding Using CollectionType

This form is now perfect. Time to embed! Remember, our goal is still to modify the genusScientists property on Genus, so our form field will still be called genusScientists. But clear out all of the options and set the type to CollectionType::class. Set its entry_type option to GenusScientistEmbeddedForm::class:

60 lines src/AppBundle/Form/GenusFormType.php
... lines 1 - 11
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
... lines 13 - 18
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 24 - 46
->add('genusScientists', CollectionType::class, [
'entry_type' => GenusScientistEmbeddedForm::class
])
;
}
... lines 52 - 58
}

Before we talk about this, let's see what it looks like! Refresh!

Woh! This Genus is related to four GenusScientists... which you can see because it built an embedded form for each one! Awesome! Well, it's mostly ugly right now, but it works, and it's free!

Try updating one, like 26 to 27 and hit Save. It even saves!

Rendering the Collection... Better

But let's clean this up - because the form looks awful... even by my standards.

Open the template: app/Resources/views/admin/genus/_form.html.twig:

26 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 21
{{ form_row(genusForm.genusScientists) }}
... lines 23 - 24
{{ form_end(genusForm) }}

This genusScientists field is not and actual field anymore: it's an array of fields. In fact, each of those field is itself composed of more sub-fields. What we have is a fairly complex form tree, which is something we talked about in our Form Theming Tutorial.

To render this in a more controlled way, delete the form_row. Then, add an h3 called "Scientists", a Bootstrap row, and then loop over the fields with for genusScientistForm in genusForm.genusScientists:

34 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 22
<h3>Scientists</h3>
<div class="row">
{% for genusScientistForm in genusForm.genusScientists %}
... lines 26 - 28
{% endfor %}
</div>
... lines 31 - 32
{{ form_end(genusForm) }}

Yep, we're looping over each of those four embedded forms.

Add a column, and then call form_row(genusScientistForm) to print both the user and yearsStudied fields at once:

34 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 22
<h3>Scientists</h3>
<div class="row">
{% for genusScientistForm in genusForm.genusScientists %}
<div class="col-xs-4">
{{ form_row(genusScientistForm) }}
</div>
{% endfor %}
</div>
... lines 31 - 32
{{ form_end(genusForm) }}

So this should render the same thing as before, but with a bit more styling. Refresh! Ok, it's better... but what's up with those zero, one, two, three labels?

This genusScientistForm is actually an entire form full of several fields. So, it prints out a label for the entire form... which is zero, one, two, three, and four. That's not helpful!

Instead, print each field by hand. Start with form_errors(genusScientistForm), just in case there are any validation errors that are attached at this form level:

36 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 22
<h3>Scientists</h3>
<div class="row">
{% for genusScientistForm in genusForm.genusScientists %}
<div class="col-xs-4">
{{ form_errors(genusScientistForm) }}
... lines 28 - 29
</div>
{% endfor %}
</div>
... lines 33 - 34
{{ form_end(genusForm) }}

It's not common, but possible. Then, simply print form_row(genusScientistForm.user) and form_row(genusScientistForm.yearsStudied):

36 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 22
<h3>Scientists</h3>
<div class="row">
{% for genusScientistForm in genusForm.genusScientists %}
<div class="col-xs-4">
{{ form_errors(genusScientistForm) }}
{{ form_row(genusScientistForm.user) }}
{{ form_row(genusScientistForm.yearsStudied) }}
</div>
{% endfor %}
</div>
... lines 33 - 34
{{ form_end(genusForm) }}

Try it! Much better!

But you know what we can't do yet? We can't actually remove - or add - new scientists. all we can do is edit the existing ones. That's silly! So let's fix it!

Leave a comment!

  • 2018-04-25 Dan Costinel

    Unfortunately, it doesn't work.
    Here's the code for the editAction:


    /**
    * Displays a form to edit an existing question entity.
    *
    * @param Request $request
    * @param Question $question
    * @return Response
    * @Route("/{id}/edit", name="question_edit")
    * @Method({"GET", "POST"})
    */
    public function editAction(Request $request, Question $question)
    {
    $editForm = $this->createForm('AppBundle\Form\QuestionType', $question);
    $editForm->handleRequest($request);

    if ($editForm->isSubmitted() && $editForm->isValid()) {
    $this->getDoctrine()->getManager()->flush();
    }

    return $this->render('question/edit.html.twig', array(
    'question' => $question,
    'edit_form' => $editForm->createView(),
    ));
    }

    When I access the edit url, I get the form, but the answers are not being pre-populated with the data from Answers table (just the Question data is set in edit form fields): https://imgur.com/a/upJy4ut

    If I die $question, after ->handleRequest($request) line, here's what I get: https://imgur.com/a/4dGqqe9 . I'm not sure, but I guess those #answer1 through #answer6, should contain the data saved in Answers table when the Question was initially created.

    If I die $question after $editForm->isSubmitted() && $editForm->isValid(), and I pass some data into some of the Answers fields, I get: https://imgur.com/a/Ua93LDY

  • 2018-04-25 Victor Bocharsky

    Hey Dan,

    Great, then it should work I think. Or let me know if you have any issues with it ;)

    Cheers!

  • 2018-04-25 Dan Costinel

    Hi Victor,
    Thanks again for replying.
    Yes, I'm using one single Question form.
    I'll try your code as soon as I get to my PC, and I'll let you know if it worked or not.
    Cheers!

  • 2018-04-25 Victor Bocharsky

    Hey Dan,

    Glad you solved it by yourself! Do you solve it in a one complex form? Or do you create a several forms for it? Actually, if you need for editing the same form as you used for inserting - just reuse that form, but now you need to pass already existent data to the form, for example, in a controller:


    $question = $this->getDoctrine()
    ->getEntityManager()
    ->getRepository(Question::class)
    ->find($id);
    $form = $this->createForm(YourQuestionForm::class, $question);

    // Then just pass form view to the template and render it.

    As you can see, I pass data as a second argument to createForm(), and this way Form component will render the same form but with actual data, i.e. fill all the fields with current values in DB, so you will be able to edit them and send edited form. Then just handle submitted form as you already to with inserting data, i.e. if form is valid - call flush() and that's it.

    Cheers!

  • 2018-04-24 Dan Costinel

    Hi Victor, thanks for replying.
    Sorry that I've forgot to post an actual question. Anyways, I've solved that one. Indeed you were right, I was able to solve it using forms.

    Now, I would like to be able to edit those kind of form fields (Answer fields inside Question form).
    Here's an image of what I get when submitting the edit form (nothing special in that form, just an usual CRUD Controller-view generated code for editing). https://imgur.com/a/yoc8kE9
    And this would be the question: how to edit a embedded form-collection field?

    At first point, for editing, in the editAction(), I need to somehow get each Answer entity that was previously saved on the Question creation, and attach them into the Question object that is being edited. So if the question_id is 1, and the answers ids are: 1 , 2, 3, 4, then I need to somehow pass the data saved in the answers, into the first four Answer objects added into the Question Entity. I've added each of those 6 Answer objects into the Question Entity like this:


    /**
    * @Assert\Type(type="AppBundle\Entity\Answer")
    */
    private $answer1;

    public function getAnswer1()
    {
    return $this->answer1;
    }

    public function setAnswer1(Answer $answer = null)
    {
    $this->answer1 = $answer;

    return $this;
    }

    I've managed to do that, even though the code is really ugly.

    So, returning to the problem, if we look at the image, we have that QuestionController.php on line 235: there is the edited data for the Question fields + Answer fields,
    Down under, at QuestionController.php on line 237, is the original Answers that were inserted when the Question was created.
    I need to somehow, add the edited Answer fields to those original Answers.
    Any solution? :)
    Thanks, and sorry for this long post.

  • 2018-04-24 Victor Bocharsky

    Hey Dan,

    At the first sight, I think you can do it with Symfony Forms. But what is your question exactly? I don't see a question at all. Have you tried to implement it? Do you have any errors/problems?

    Cheers!

  • 2018-04-23 Dan Costinel

    Hi. I want to be able to display such a form: https://imgur.com/a/NsDQISz .

    In short, it's about a Quiz project, and at this point, I need to let the admin create a new Question, as well as the Answers for the question in the same template.
    For that I have two tables, Question 1-N Answers related:
    -- Question [id, question_text, explication, questionNo, answers]
    -- Answer [ answer_text, isCorrect, question ].
    One problem, is that the admin can define for a question, a variable number of answers. So it can define 'till six answers (in the image I've shown just 3 for simplicity).
    A second problem, is that the answers needs to be a CKEditor, so that the admin can easily apply styles to the text.

    I've followed the docs from http://symfony.com/doc/3.4/....

    I expect the html to be like: https://gist.github.com/dan...

    And then maybe to get beck, after the form is submitted, something like (and let's say the admin adds 3 answers for a question):


    QuestionNo: 1
    Quiz: object
    Question Text: whatever is entered in the editor
    Answer 1 [
    answer_text: whatever is entered in the editor
    isCorrect: 1 or 0, depending if the checkbox next to Answer 1 label is checked or not
    ]
    Answer 2 [
    answer_text: whatever is entered in the editor
    isCorrect: 1 or 0, depending if the checkbox next to Answer 2 label is checked or not
    ]
    Answer 3 [
    answer_text: whatever is entered in the editor
    isCorrect: 1 or 0, depending if the checkbox next to Answer 3 label is checked or not
    ]

    And then to be able to save:
    - in questions table, the QuestionNo, quiz_id, Question text, explication
    - in answers table, the answer_text, isCorrect, and question_id

    Thanks a lot for any suggestion!

  • 2017-11-07 Diego Aguiar

    Ohh, I got you

    There is a way to handle which records to show for a Entity/Collection Type field. Look at this example in the documentation https://symfony.com/doc/cur...
    I believe it's pretty straight forward, but, if you have doubts, let us know!

    Cheers!

  • 2017-11-07 Bettinz

    Hello Diego Aguiar , this is the situation: I've an Entity called Classroom and one called Student, and I've a OneToMany relationship between them.
    Now what I want to do is to provide to a teacher the ability to manage all male and female students of a classroom and order the form alphabetically.
    So I've created a ClassRoomType Form with one field called students (it's a CollectionType) and the entry_type is StudentType.
    The form StudentType include name, age, sex, etc.
    What I want now is to show only male (or female) students and order them by name.
    I haven't find anything like a query builder for collectionType, so I can't understand the way to go. Thanks

  • 2017-11-06 Diego Aguiar

    Hey Bettinz

    I'm not sure if I understood correctly to your question. What would be your inputs and how would like your end result?

    Cheers!

  • 2017-11-06 Bettinz

    Hello, is it possible to filer and order an Entity passed via Collection Type? :-) like a query builder inside the Collection Type.
    I need to order and filter student from one class but I also need to show the Collection Type because I've multiple students and a form to manage them :-)

  • 2017-01-29 weaverryan

    Yo ehymel!

    I think it should look something like this:


    {% for genusScientistForm in genusForm.genusScientists %}
    <div class="col-xs-4 js-genus-scientist-item">
    {# you don't have individual fields to render, you just have the ONE field to render #}
    {{ form_errors(genusScientistForm) }}
    {{ form_label(genusScientistForm, 'Genus Scientist') }}
    {{ form_widget(genusScientistForm) }}
    </div>
    {% endfor %}

    In the tutorial, genusScientistForm is really an embedded form, with sub-parts as you mentioned. But now, genusScientistForm is literally just a single field (EntityType)... without any sub-fields. So, you just need to render it like any other single field :). One simpler option might be something like this:


    {% for genusScientistForm in genusForm.genusScientists %}
    <div class="col-xs-4 js-genus-scientist-item">
    {{ form_row(genusScientistForm, {
    label: 'Genus Scientist')
    } }}
    </div>
    {% endfor %}

    Not tested - but give it a try :).

    Cheers!

  • 2017-01-29 ehymel

    I really really appreciate you helping work this out!!

    It looks like the only problem with your proposed solution is that I don't seem to have access to the sub-parts of the added EntityType when rendering the form. Specifically if I try to get rid of the automatically generated labels ("0", "1", "2", etc) in the way you did it in the lesson, it fails, I assume because twig doesn't now how to get to it:


    {% for genusScientistForm in genusForm.genusScientists %}
    <div class="col-xs-4 js-genus-scientist-item">
    {{ form_errors(genusScientistForm) }}
    {{ form_row(genusScientistForm.user) }} <------------------- this line fails
    </div>
    {% endfor %}

    Thoughts?

  • 2017-01-29 ehymel

    Genius! That works perfectly. Thanks very much.

    Glad to hear you had to think about it for a while :)

  • 2017-01-25 weaverryan

    Hi ehymel!

    Sorry for my slow reply - I needed to wait until I have a few minutes to really think about this :). So, the goal would be to have a collection of embedded forms (with the ability to add a new one, remove existing ones, etc), but each embedded form will have only the single, Scientist drop down. Is that correct?

    So, here's how to do it :). You will STILL have a CollectionType just like before. The only difference is that the "entry_type" (i.e. the form that's used in the collection) *won't* be an entirely different form class (e.g. GenusScientistEmbeddedForm), it will simply be the EntityType field. Something like this:


    $builder
    ->add('genusScientists', CollectionType::class, [
    'entry_type' => EntityType::class,
    'entry_options' => [
    'class' => User::class,
    'choice_label' => 'email'
    ],
    'allow_delete' => true,
    'allow_add' => true,
    ])

    ... and that's it! In your template, rendering is simpler, something more like this:


    {% for genusScientistForm in genusForm.genusScientists %}
    <div class="col-xs-4 js-genus-scientist-item">



    {# you don't have individual fields to render, you just have the ONE field to render #}
    {{ form_row(genusScientistForm) }}
    </div>
    {% endfor %}

    If you were doing this from the *inverse* side instead, then you would just need to make sure to follow the same steps we did here: https://knpuniversity.com/s...

    Let me know if that helps! And cheers!

  • 2017-01-22 ehymel

    In your lesson for using CollectionType, you have changed from ManyToMany to matching OneToMany and ManyToMany with a new entity to manage the relationship. All makes sense. Your embedded form for multiple drop-down boxes uses that new entity to persist changes to the relationship in the db.

    I would like to leave things as a ManyToMany relationship (like in your earlier lessons) but use drop-down boxes like you use in this lesson. However, it doesn't seem to work. I'm listing the entity of the inverse side of the relationship in the embedded form (so I can list choices in the drop-down select field, but when I save it modifies only this entity (inverse side of ManyToMany) rather than the relationship.

    In other words, using examples from your entities: Owning entity; Genus. Inverse side: Scientist. How to embed multiple drop-down list of scientists that in the same way you do when there is a named entity joining the two, but instead with the ManyToMany relationship itself defining things? I've tried the equivalent of adding an embedded form of Scientists drop-down list and embedding that into the Genus edit page. I get multiple drop-down lists, but the correct items are not pre-selected, and changing/saving updates the Scientist list itself rather than the relationship.