Number, Integer, Or Custom Float?


In this video we are going to take a look at how we can accept number values when using the Symfony form component. This includes whole numbers (integers), and decimal values.

Now, Symfony's form component has two built in form fields for handling numbers:

  • IntegerType
  • NumberType

The general rule of thumb - use the IntegerType when dealing with whole numbers (Countdown maths), and for decimals / floats, use the NumberType.

Also, with the corresponding entity property definitions:

// src/AppBundle/Entity/Product.php

    /**
     * @ORM\Column(name="`integer`", type="integer")
     */
    protected $integer;

    /**
     * @ORM\Column(type="decimal", scale=3)
     */
    protected $number;

If you are unsure about these annotations, firstly I'd advise reading the Doctrine documentation, and then checking out the Doctrine Databasics series here at Code Review Videos.

One other thing to note from these entity annotations is the name attribute for the $integer field. Notice that the field name is set to integer, which is a reserved keyword and must be quoted using the backtick character. More on this here.

Adding either of these to a form type is really very straightforward:

use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;

class YourFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('integer', IntegerType::class)
            ->add('number', NumberType::class)
        ;
    }

And if we use the standard way of rendering this form out:

class SomeController extends Controller
{
    /**
     * @Route("/", name="form_add_example")
     */
    public function formAddExampleAction(Request $request)
    {
        $form = $this->createForm(YourFormType::class);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // etc
        }

        return $this->render(':form-example:index.html.twig', [
            'myForm' => $form->createView()
        ]);
    }

Then we should get a nice block of HTML rendered out for us:

<div class="form-group">
    <label class="col-sm-2 control-label required" for="product_integer">Integer</label>
    <div class="col-sm-10">
        <input type="number" id="product_integer" name="product[integer]" required="required" class="form-control" />
    </div>
<div class="form-group">
    <label class="col-sm-2 control-label required" for="product_number">Number</label>
    <div class="col-sm-10">
        <input type="text" id="product_number" name="product[number]" required="required" class="form-control" />
    </div>
</div>

(excess form HTML removed for brevity / clarity).

But this throws up a - possibly - unanticipated input type.

For our IntegerType, Symfony will render out an input of type number.

For our NumberType, Symfony will render out an input of type text. Hmmm.

If we dig a little deeper into this, we can look at the widget for the number field type:

<!-- vendor/symfony/symfony/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig -->

{%- block number_widget -%}
    {# type="number" doesn't work with floats #}
    {%- set type = type|default('text') -%}
    {{ block('form_widget_simple') }}
{%- endblock number_widget -%}

By the way, if you are unsure on this, we covered it in a previous video.

The thing is, I don't understand this comment. The number type does seem to work just fine with floats. There's a good possibility I haven't tried every combination, but from my initial testing, I'd say this is wrong. At the time of writing, there is a 3 year old ticket open about this very issue.

Having the input type set to number for a more precise number (floating point / decimals) may be exactly what we want. It will give us the nice little up and down arrows on our input, and also allow a user to use their up and down arrow keys to increase / decrease the number precisely. Of course, we don't get this functionality on a text field input, as it wouldn't make sense.

You may be satisfied with this as-is. If you are using the form independently of Twig then having these extra HTML elements won't be of any use to you anyway, so use the most appropriate numeric field and move on with your task.

If you are using Twig, however, it is nice to get as much 'free' stuff as possible. After all, that is one of the main reasons to use Twig with Symfony in the first place.

Simple Fix

The simple way to fix this is to override the number_widget inside our current form:

<!-- app/Resources/views/form-example/index.html.twig -->

{% extends '::base.html.twig' %}

{% form_theme myForm _self %}

{%- block number_widget -%}
    {%- set type = type|default('number') -%}
    {{ block('form_widget_simple') }}
{%- endblock number_widget -%}

{% block body %}

    <h2>Symfony 3 Form Example</h2>

    <hr />

    {{ form(myForm) }}

{% endblock %}

Now, whenever we render out the form on this particular template it will use the number_widget with the HTML input type of number. This solves our immediate problem.

However, we will need to include this block override in every single template that we want to override the number_widget for. This isn't so good. But it may be enough, if you only have one number input you need to 'fix'.

Fix With A Custom Form Type

A more robust way would be to create our own form field type. Let's call it FloatType, but of course you are free to call it anything you like.

<?php

// src/AppBundle/Form/Type/FloatType.php

namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class FloatType extends AbstractType
{
    public function getParent()
    {
        return NumberType::class;
    }
}

Simple enough. Our FloatType is inheriting all the 'stuff' that NumberType has. So far, we have effectively just duplicated the NumberType but renamed it to FloatType.

Next, we need to extract the number_widget code into somewhere more central:

<!-- app/Resources/views/forms/fields.html.twig -->

{%- block float_widget -%}
    {%- set type = type|default('number') -%}
    {{ block('form_widget_simple') }}
{%- endblock float_widget -%}

Note here that our block is called float_widget. This matches up directly to our FloatType. If we don't declare this block, we are going to also inherit the number_widget block by default.

But we must also tell Symfony about our new float_widget:

# app/config/config.yml

# Twig Configuration
twig:
    debug:            "%kernel.debug%"
    strict_variables: "%kernel.debug%"
    form_themes:
        - 'forms/fields.html.twig'
        - 'bootstrap_3_horizontal_layout.html.twig'

Unsure on this? Watch this video.

Cool. So now Symfony will look at our form themes and find our float_widget and apply the type of number to our input, making the whole process work for us.

We can go ahead and start using this on our original form:

use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;

class YourFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('integer', IntegerType::class)
            ->add('number', NumberType::class)
            ->add('float', FloatType::class)
        ;
    }

Of course, you will need to add in a 'float' field to your entity to make this actually work - but this is as easy as duplicating the existing number field:

// src/AppBundle/Entity/Product.php

    /**
     * @ORM\Column(type="decimal", scale=3)
     */
    protected $float;

And we can override the default values for the input type of number by either passing them in-line:

$builder
    ->add('float', FloatType::class, [
        'attr' => [
            'min'  => 20,
            'max'  => 40,
            'step' => 0.05
        ]
    ])
;

Or add them as defaults to the FloatType:

<?php

// src/AppBundle/Form/Type/FloatType.php

namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class FloatType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'scale' => 3,
            'attr'  => [
                'step' => 1.9,
                'min'  => 55,
                'max'  => 550,
            ]
        ]);
    }

    public function getParent()
    {
        return NumberType::class;
    }
}

As ever, Symfony gives us options. It is up to us as developers to choose the right choice for our project.

Removing The Up / Down Arrows With CSS

This feels like a real hack, but what if you want the form field input of type number, but you don't want the up and down arrows? This snippet of CSS will get you started:

// removes the up / down arrows from a numeric input field
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  margin: 0;
}

By default, this will affect every single number input in your site, so adapt accordingly.

Code For This Course

Get the code for this course.

Episodes