Buy

Field Types and Options

So far, we set the property name of the form fields and Symfony tries to guess the best field type to render. Since isPublished is a boolean in the database, it guessed a checkbox field:

139 lines src/AppBundle/Entity/Genus.php
... lines 1 - 11
class Genus
{
... lines 14 - 41
/**
* @ORM\Column(type="boolean")
*/
private $isPublished = true;
... lines 46 - 137
}

But obviously, that's not always going to work.

Google for "Symfony form field types", and find a document called Form Types Reference.

These are all the different field types we can choose from. It's got stuff you'd expect - like text and textarea, some HTML5 fields - like email and integer and some more unusual types, like date, which helps render date fields, either as three drop-downs or a single text field.

Right now, isPublished is a checkbox. But instead, I'd rather have a select drop-down with "yes" and "no" options. But... you won't see a "select" type in this list. Instead, it's called ChoiceType.

Using ChoiceType

ChoiceType is a little special: it can render a drop-down, radio buttons or checkboxes based on what options you pass to it. One of the options is choices, which, if you look down at the example, you can see controls the actual items in the drop-down.

Let's use this! In GenusFormType, the optional second argument to the add function is the field type you want to use. Set it to ChoiceType::class:

36 lines src/AppBundle/Form/GenusFormType.php
... lines 1 - 5
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
... lines 7 - 9
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 15 - 18
->add('isPublished', ChoiceType::class, [
... lines 20 - 23
])
... line 25
;
}
... lines 28 - 34
}

The third argument is an array of options to configure that field. What can you pass here? Well, each field type has different options... though there are a lot of options shared by all types, like the ability to customize the label.

Either way, the reference section will tell you what you can use. Pass in the choices option and set that to an array. Add Yes mapped to true and No mapped to false. The keys - Yes and No will be the text in the drop down:

36 lines src/AppBundle/Form/GenusFormType.php
... lines 1 - 9
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 15 - 18
->add('isPublished', ChoiceType::class, [
'choices' => [
'Yes' => true,
'No' => false,
]
])
... line 25
;
}
... lines 28 - 34
}

The values - true and false will be the value that's passed to setIsPublished() if that option is chosen.

Try it out! Head back to the form and refresh! Perfect.

The EntityType

Let's keep going with this. This subFamily field is also a drop-down. So that's probably being guessed as a ChoiceType, right? Almost. Check out the EntityType. This is just like the ChoiceType, except it's really good at fetching the options by querying an entity.

In this case, it's automatically querying for the SubFamily entity: it guessed which type to use and auto-configured it. And yeah, I know - these sub-families are totally silly. They're actually just the last names of people - coming from the Faker library - I was being lazy.

Form Field Options

But I do have one problem with this EntityType field: it auto-selects the first option. That's lame - I'd rather have an option on top that says "Choose a Sub-Family".

Check out the options for EntityType: there's one called placeholder. Click to read more about that. Yes! This is exactly what we want!

Open the form class back up. We know that Symfony is guessing the EntityType for the subFamily field. We could now manually pass EntityType::class as the second argument. But don't! Pass null instead. Now, add a placeholder option set to Choose a Sub-Family:

39 lines src/AppBundle/Form/GenusFormType.php
... lines 1 - 10
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... line 16
->add('subFamily', null, [
'placeholder' => 'Choose a Sub Family'
])
... lines 20 - 28
;
}
... lines 31 - 37
}

But wait, why did I pass null as the second argument? Well first, because I can! I can be lazy here: if I pass null, Symfony will guess the EntityType. That's cool.

Second, when you use the EntityType, there's a required option called class. Let me show you an example: 'class' => 'AppBundle:User'. You have to tell it which entity to query from. But if you let Symfony guess the field type for you, then it will also guess any options it can, including this one. So by being lazy and passing null, it will continue to guess the field type and a few other options for me, like class.

Anyways, go back, refresh, and there it is. Here are the key takeaways. First, you have a giant dictionary of built-in form field types. And second: you configure each field by passing a third argument to the add() function.

Now, how could we further control the query that's made for the SubFamily options? What if we need them to be listed alphabetically?

Leave a comment!

  • 2016-12-05 weaverryan

    Hey Matt!

    You got it! It's important that your form field name "chapters" matches a key on your incoming/starting data. Usually, we bind a form to an object, so we would expect to have a "chapters" property on our object, which it would use to know which boxes to check. But in this case, since we're binding the form to an array, it's simply an array key. You got the "aha!" moment I was hoping you'd get :).

    Cheers!

  • 2016-12-05 thisismattness

    Hey Ryan,

    that really solved it! It took me a while to get my head around it, but I got it working :-) I guess, behind the scenes, the data is passed as an option to the form and it happens that both - the option name and the child name - are equal (e.g. 'chapters'), so Symfony sticks those two together and then: checked are the checkboxes...

    Cheers!
    Matt

  • 2016-12-02 weaverryan

    Hey Matt!

    Coding for a few weeks now! Wow! Congrats! And don't sweat too much about this form stuff - forms is the hardest part of Symfony (it's also powerful and rewarding... but can easily be frustrating, even for really seasoned devs!).

    Ok, I think I definitely understand your setup - the jsfiddle was great. Afterall, backend code ultimately exists just to power some UI :). Here's why I think this is confusing you: the purpose of your form is *not* to update an entity. What I mean is, most of the time, we create a form so that we can update or create some entity. For example, if you wanted to create a form to allow the user to create a Chapter, that would be pretty easy for you: your form would be bound to the Chapter class, you would have a field for each property in your entity, and the types of those fields would naturally be simple.

    But in your case, your form isn't doing that: the purpose of you form is simply to create a checkbox of chapters. And when you submit, you just want to fetch what chapters were checked, so you can redirect the user. So, here's the key: I want you to create a form that is *not* bound to an object. If you build it in your controller, it should look something like this:

    // the form is not bound to an object. Instead, we just pass it an associative array of
    // "starting" data, that should be used to pre-fill the form
    $defaultData = [
    // I don't know if this is right - the point is, you should set a "chapters" key to the
    // array of Chapter objects that should be pre-filled
    'chapters' => $quotes->getChapters()
    ];
    $myForm = $this->createFormBuilder($defaultData)
    ->add('chapters', EntityType::class, [
    // this should be the entity class that should be used for the checkboxes
    // I'm assuming you want a checkbox for each Chapter
    // notice there is no logic here to say which chapters should be pre-checked, that happens above
    'class' => 'AppBundle\Entity\Chapter',
    // I'm assuming the Chapter has a name property - just kinda made that up :)
    'choice_label' => 'name',
    'multiple' => true,
    'expanded' => true
    ]
    )
    ->getForm();

    $form->handleRequest($request);
    if ($form->isValid()) {
    $formData = $myForm->getData();
    // this will be an array of Chapter objects, the ones the user selected
    /** @var Chapter[] $chapters */
    $chapters = $formData['chapters'];

    // loop over $chapters to create your ?chapter=chapter_foo&chapter_bar query string, and then redirect to a route with this appended to it
    $queryString = '...';
    $url = $this->generateUrl('result_route_name').'?'.$queryString;

    return $this->redirect($url);
    }

    Let me know if this setup makes sense, and if I've finally hit on the problem! Focus on this idea: the EntityType's job is simply to create a bunch of checkboxes (in this case) for all the records for some entity (e.g. Chapter). You can of course use the query_builder option to filter that list from all Chapters to some Chapters. But, the options you pass to EntityType have nothing to do with *which* checkboxes should be checked by default: that's entirely up to what data you pass into your form. And in this case, we're not binding our form to any entity, we're just passing it an associative array, and it's giving us an associative array after (i.e. $form->getData() is an associative array). We're not binding it to an entity because it's not really necessary: it's a really cool "nice-to-have" when you eventually want to update an entity and persist it. But in this case, all we need is that our form gives us an array of the Chapter objects that were checked. Then, we're happy :).

    Cheers!

  • 2016-12-02 thisismattness

    Hi,

    well it does kind of both :) Unfortunately, the Symfony EntityType documentation is not helpful for me and I'm struggling reading the symfony classes (just coding for a few weeks now), though your OO Tut really got me started (or I'd say *dangerous*). But anyway.

    Maybe I should tackle the problem from another perspective, so I think it's best explained by an example what I like to achieve:

    https://jsfiddle.net/y83vsc50/1/

    Basically, my table setup looks like this:

    quotes ManyToOne Chapter (which should be your mentioned property whose value is an *object*). For now, I skipped the book property.

    If I do setup like this, I get a list of checkboxes with chapters from quotes. But I do want a list with *all* chapters (from Chapter), and only those checked by default, which have a quote and the others disabled:


    $myForm = $this->createFormBuilder()
    ->add('chapter', EntityType::class, [
    'class' => 'AppBundle\Entity\Quotes',
    'choice_label' => 'chapter',
    'multiple' => true,
    'expanded' => true
    ])
    ->getForm();

    So at the end, I need to populate checkboxes with the mentioned constraints and after submit the user is redirected to a route /results and the URI string localhost:8000/results?chapter=chapter_foo&chapter_bar

    I hope this makes sense, somehow.

    Cheers!
    Matt

  • 2016-12-01 weaverryan

    Hey Matt!

    Ah, it *does* help :). So, what you're doing makes great sense, but is a little non-traditional. The EntityType is meant to be attached to a property whose value is an *object*... basically a ManyToOne property. But, your name field is a string. Based on what I'm seeing, if you submit right now and dump your final value, the name property will be an array of Chapter objects (not a string). (I'm also a little confused about how you have a "name" field, but the user is selecting multiple checkboxes - so I could be misunderstanding something!)

    But, you totally *can* do this, you just need to do a little bit more work :). Basically, you should use the ChoiceType field with a custom data transformer that's able to convert to and from the "text" of the name field and the Chapter entity object (or objects, if you really do intend for this to be multiple checkboxes). You'll approximately be following this tutorial: http://symfony.com/doc/current...

    Let me know if this helps... or confuses further :).

    Cheers!

  • 2016-12-01 thisismattness

    Hey Ryan,

    thanks for your fast reply. I think that does perfectly make sense. But I tried without any luck. Here's my code (a made a testform to reduce complexity):

    Form (inside the controller for testing purpose)


    /**
    * @Route("/doc")
    */
    public function docTest(Request $request) {



    $myForm = $this->createFormBuilder()
    ->add('name', EntityType::class, [
    'class' => 'AppBundle\Entity\Chapter',
    'query_builder' => function(ChapterRepo $chapterRepo) {
    return $chapterRepo->createQueryBuilder('c')
    ->distinct('c.name')
    ->innerJoin('c.quotes','quotes')
    ->andWhere('quotes.isReviewed = true')
    ->groupBy('c.book')
    ->orderBy('c.id','ASC');
    },
    'multiple' => true,
    'expanded' => true,
    'attr' => ['data-pos' => '4']
    ])
    ->getForm();


    return $this->render('Quotes/test.html.twig', [
    'testForm' => $myForm->createView()]);

    Btw: Don't worry about the join (and the other stuff), it doens't work without it either.

    And here's my entity definitions.



    /**
    * @ORM\Entity(repositoryClass="AppBundle\Repository\ChapterRepo")
    * @ORM\Table(name="chapter")
    */
    class Chapter
    {
    // *******************
    // Unique Keys
    // *******************

    /**
    * @ORM\Column(type="integer", unique=true)
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    private $id;

    // *******************
    // Joins
    // *******************
    // Inversed Side: One Chapter -> Many Quotes
    /**
    * @ORM\OneToMany(targetEntity="AppBundle\Entity\Quotes", mappedBy="chapter")
    */
    private $quotes;

    // *******************
    // Properties
    // *******************

    /**
    * @ORM\Column(type="integer", name="chapter_number")
    * @Assert\NotBlank()
    * @Assert\NotNull()
    */
    private $chapterNumber;

    /**
    * @ORM\Column(type="text", length=100)
    * @Assert\NotBlank()
    * @Assert\NotNull()
    */
    private $name;

    /**
    * @ORM\Column(type="string")
    */
    private $book;

    // and so on...
    // getters and setters here
    }

    Hope that helps.

    Cheers, Matt

  • 2016-11-30 weaverryan

    Hey thisismattness!

    Yes! In fact, that's exactly how the EntityType field *should* work out-of-the-box... but a lot depends on your setup :). Basically, when you have an EntityType with multiple=true, when you submit, Symfony process the incoming, checked ids, turn those into your entity objects, and then set that array of entities onto the property on your main entity (whatever field name you're using in your form for the EntityType). And also, when the form *renders*, it does the opposite: it uses your query to get all of the entity objects for the choices, then gets the array of entity objects from that same property and compares them. For example, suppose I have a Category entity with a products property. I then add a products field to my form, which is an EntityType. When the form renders, it uses my custom query on that field to load all (for example) 25 products in my system. It then looks at the products property on Category, which holds (for example) 10 of those products. So now, it knows to render 25 checkboxes, with 10 of them checked.

    That's a long way of saying that something might not be setup correctly if you're not getting your boxes auto-checked. Let me know if this explanation helped! And feel free to come back with some of your code - it'll help show what's going on :).

    Cheers!

  • 2016-11-30 thisismattness

    Hi guys,

    I just created a form with an entity-type field which is fed by a small query (some distinct values of a table.column) in the background, which then renders as several checkboxes (with options multiple and expanded true). However, when the page is loaded and the form displayed, the checkboxes are not checked by default. Is there any way to tell symfony that those boxes should be checked by default? Or even better, tell which box should be checked by default?
    btw: my model does not have an object with choice.a true/false, choice.b true/false. that would not be possible as the choices can get more (i.e. the values in the table)

    Cheers!

  • 2016-11-01 Hugo Leon

    It's a boring form indeed! is because I was just getting started :P, but soon they'll be fun hopefully. I see, I thought it had something to do with the Team entity.
    I was thinking about creating a select element instead, but those tags you suggested are way better, much easier and totally match with its goal.

    I love this Simfony Forms.

    Thanks a lot Ryan, you're the best!

  • 2016-11-01 weaverryan

    Hey Hugo!

    Hmm, that form is perfectly boring :). But I think I might see the problem: it's the "roles" field. Is that an array (e.g. json_array) type in your User class like how we setup in this tutorial? In that case, Symfony is likely guessing that this is a text field (that's it's default) and then choking when it tries to get that array value and print it in the value="" attribute. If I'm right - and you look closely - you might see the word "role" somewhere in the stacktrace of those errors (it might be a bit buried!).

    Instead of using a text field, you'll probably want something like a ChoiceType with options 'multiple' => true and 'expand' => true. You would set the choices option to an array of whatever roles that you want the user to be able to select :). Or, if you have some more complex scenario, let me know!

    Cheers!

  • 2016-11-01 Hugo Leon

    Sure!

    Here it is the form class


    class UserFormType extends AbstractType
    {
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder
    ->add('email')
    ->add('name')
    ->add('lastName')
    ->add('interiorNumber')
    ->add('phoneNumber')
    ->add('mobileNumber')
    ->add('plainPassword')
    ->add('roles');
    }

    public function configureOptions(OptionsResolver $resolver)
    {
    $resolver->setDefaults([
    'data_class' => 'AppBundle\Entity\User'
    ]);
    }

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

    Here is the ContextErrorException 1/2

    Here is the Twig_Error_Runtime 2/2

    Please tell me if you need something more about the exception page.

    Thanks!

  • 2016-10-31 weaverryan

    Hey Hugo!

    The setup looks *great* - seriously, I can't see any issues... which is kind of a problem since you have this error! :). It looks like something weird is happening when rendering one of your input fields (line 13 in that file is responsible for that (https://github.com/symfony/sym.... Can you show me your form class? And also, a screenshot of the full exception page when you get that error? It's going to be something subtle...

    Cheers!

  • 2016-10-31 Hugo Leon

    Thanks Ryan!

    I did what you said, makes perfect sense. Now I have this:


    public function newAction(Request $request)
    {
    $user = new User();
    $user->setTeam($this->getUser()->getTeam());

    $form = $this->createForm(UserFormType::class, $user);

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
    $user = $form->getData();
    $em = $this->getDoctrine()->getManager();
    $em->persist($user);
    $em->flush();

    $this->addFlash('success', 'The user has been created');

    return $this->redirectToRoute('admin_users_list');
    }

    return $this->render('admin/users/new.html.twig', [
    'userForm' => $form->createView()
    ]);


    {# admin/users/new.html.twig #}
    {#...#}
    {{ form_start(userForm) }}
    {{ form_widget(userForm) }}


    <button type="submit" class="btn btn-primary">{% trans %}Save{% endtrans %}</button>
    {{ form_end(userForm) }}

    But now I get this error:


    An exception has been thrown during the rendering of a template ("Notice: Array to string conversion") in form_div_layout.html.twig at line 13.

    I couldn't get it to work, what could be the problem? I saw that the form has no Team, but I guess it's fine because like you said, the User object is created when I submit the form.

  • 2016-10-28 weaverryan

    Yo Hugo!

    You've caught onto my favorite thing to say - getting "dangerous" :). I'm really glad to hear it!

    And yes, I (hopefully) *do* have an easy solution for you :). To summarize:

    1) You have a form that modifies the User object
    2) You would like the team field to automatically be set to the team of the currently-logged in User.

    To do this, I want you to actually *remove* the team field entirely from your form. And instead, just do a little bit of work in your controller. Here's some "faux" code:


    public function addUserToTeamAction()
    {
    $newUser = new User();
    $newUser = $newUser->setTeam($this->getUser()->getTeam());

    $form = $this->createForm(NewUseTeamForm::class, $newUser);
    // normal form processing
    }

    By doing this, when you submit the form, a new User() object is created, we set its team immediately, and then the form data is added to it. By the time we persist+flush, the $newUser variable will have the team set AND any data from the form. Unless I'm misunderstanding something, this should be your solution.

    Let me know if I understood correctly! And cheers!

  • 2016-10-28 Hugo Leon

    Hi there!

    Let me tell you that I'm really having fun with your courses, laughing all the time. You have a gift Ryan. I'm feeling more dangerous day by day :)

    Well, I know you have the easy solution to this. I have a User Entity which has a Team Entity (manyToOne relationship). The logged user can add another one just to his/her team. I was wondering if I could set a hidden field to automatically select the team where the logged user belongs to. The second option is to have a visible Choice Type, but with just one Team by default.

    I've already tried with dependency injection (ha-ha-ha) using TokenStorageInterface in the constructor of the FormType class, and a HiddenType field:

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $user = $this->tokenStorage->getToken()->getUser();

    $builder
    ->add('email')
    ->add('name')
    ->add('lastName')
    ->add('team', HiddenType::class, [
    'data' => $user->getTeam()
    ])
    ;
    }

    But it gets the _name_ of the team only, not the entity object. I read about DataTransformers, and another not so clean solutions.

    What do you recommend me to do?

    Thanks in advance and congratulations for your great work.

  • 2016-06-26 Grzegorz Szaliński

    Hi Ryan,
    Thanks for the link. I knew casting or mapping was broken, but would never think it's that complex.

  • 2016-06-26 weaverryan

    Hi Grzegorz!

    Hmm, you're right! The behavior is not seen in Chrome, but it is seen in Firefox - this is one of the crazy things about HTML5 validation :).

    In short, I think this may be a bug in Symfony - these areas can be quite complex: https://github.com/symfony/sym.... These types of things aren't easy to fix without breaking backwards compatibility.

    So, that's the short answer :). And other than the HTML5 issue, Symfony is really doing the correct thing - it needs to somehow map our "true" and "false" to values, and 1 and an empty string are a valid way to do this (though "1" and "0" would have made more sense).

    Thanks for the question about this - it was not something I had realized!

  • 2016-06-26 Grzegorz Szaliński

    Hello,
    I have a problem with the isPublished field. The HTML for options is generated as follows:

    <option value="1">Yes</option>
    <option value="" selected="selected">No</option>

    so, "false" is parsed as an empty string and the option is marked as default. On form submit, with "No" selected, the browser (Firefox 47.0) shows the balloon "Please select an item in the list". When "Yes" is selected there's no balloon.

    When I change the field settings and map "Yes" to "1" and "No" to "0", it works:

    <option value="1">Yes</option>
    <option value="0">No</option>

    Why is that?