Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

DateTimeType & Data "Transforming"

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Let's use our new powers to add another field to the form. Our Article class has a publishedAt DateTime property. Depending on your app, you might not want this to be a field in your form. You might just want a "Publish" button that sets this to today's date when you click it.

But, in our app, I want to allow whoever is writing the article to specifically set the publish date. So, add publishedAt to the form... but don't set the type.

... lines 1 - 10
class ArticleFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 16 - 19
->add('publishedAt')
;
}
... lines 23 - 29
}

So... ah... this is interesting! How will Symfony render a "date" field? Let's find out! Refresh! Woh... it's a bunch of dropdowns for the year, month, day and time. That... will technically work... but that's not my favorite.

Which Field Type was Guessed?

Go back to the list of field types. Obviously, this is working because the field guessing system guessed... some field type. But... which one? To find out, go back to the web debug toolbar, click to open the profiler and select publishedAt. Ha! Right on top: DateTimeType. Nice!

Let's click into the DateTimeType documentation. Hmm... it has a bunch of options, and most of these are special to this type. For example, you can't pass a with_seconds option to a TextType: it makes no sense, and Symfony will yell at you.

Anyways, one of the options is called widget. Ah! This defines how the field is rendered. And if you did a little bit of digging, you would learn that we can set this to single_text to get a more user-friendly field.

Passing Options but No Type

To set an option on the publishedAt field, pass null as the second argument and set up the array as the third. null just tells Symfony to continue "guessing" this field type. Basically, I'm being lazy: we could pass DateTimeType::class ... but we don't need to!

Under the options, set widget to single_text.

... lines 1 - 19
->add('publishedAt', null, [
'widget' => 'single_text'
])
... lines 23 - 33

Let's see what that did! Find your form, refresh and... cool! It's a text field! Right click and "Inspect Element" on that. Double cool! It's an <input type="datetime-local" ...>. That's an HTML5 input type that gives us a cool calendar widget. Unfortunately, while this will work on most browsers, it will not work on all browsers. If the user's browser has no idea how to handle a datetime-local input, it will fall back to a normal text field.

If you need a fancy calendar widget for all browsers, you'll need to add some JavaScript to do that. We did that in our Symfony 3 forms tutorial and, later, we'll talk a bit about JavaScript and forms in Symfony 4.

Data Transforming

But, the reason I wanted to show you the DateTimeType was not because of this HTML5 fanciness. Nope! The really important thing I want you to notice is that, regardless of browser support, when we submit this form, it will send this field as a simple, date string. But... wait! We know that, on submit, the form system will call the setPublishedAt() method. And... that requires a DateTime object, not a string! Won't this totally explode?

Actually... no! It will work perfectly.

In reality, each field type - like DateTimeType - has two superpowers. First, it determines how the field is rendered. Like, an input type="text" field or, a bunch of drop-downs, or a fancy datetime-local input. Second... and this is the real superpower, a field type is able to transform the data to and from your object and the form. This is called "data transformation".

I won't do it now, but when we submit, the DateTimeType will transform the submitted date string into a DateTime object and then call setPublishedAt(). Later, when we create a page to edit an existing Article, the form system will call getPublishedAt() to fetch the DateTime object, and then the DateTimeType will transform that into a string so it can be rendered as the value of the input.

We'll talk more about data transformers later. Heck, we're going to create one! Right now, I just want you to realize that this is happening behind the scenes. Well, not all fields have transformers: simple fields that hold text, like an input text field or textarea don't need one.

Next: let's talk about one of Symfony's most important and most versatile field types: ChoiceType. It's the over-achiever in the group: you can use it to create a select drop down, multi-select, radio buttons or checkboxes. Heck, I'm pretty sure it even knows how to fix a flat tire.

Let's work with it - and its brother the EntityType - to create a drop-down list populated from the database.

Leave a comment!

23
Login or Register to join the conversation
davidmintz Avatar
davidmintz Avatar davidmintz | posted 1 year ago | edited

I have a project (Symfony 6.0.4 ) with a form bound to an entity that has a date field. Symfony is guessing Date (correctly, of course) and I am using the widget => single_text option to render the field as an HTML5 date input. When I select a date with my browser's datepicker and submit, and the POSTed value is something like "2022-02-24", I get the validation error "This value should be of type string." Any idea why? It certainly <i>is</i> a string.

In the form class I am saying: $builder->add('date',DateType::class, ['widget'=>'single_text','empty_data'=>''])

In the underlying entity:



/**
 * @ORM\Column(type="date")
 * @Assert\NotBlank(message="date is required")
 * @Assert\Date(message="a valid date is required")
 */
private $date;```


And in the template, I am rendering the field with 
`form_widget(form.date)`



Thanks!
Reply

Hey David,

Hm, usually it might be invalid date format, because browser's widget may pass date's day/month/year in an incorrect order that your server expects, but I suppose you get "a valid date is required" message instead, so probably it's not the reason here. It's difficult to say without some debugging. Please, try to dump some things, i.e. dump the form data you're going to process on post request. Probably, the field is actually empty because you forget to add "name" attribute to it, or the form has another field with the same name that overwrites your date value with an empty string, etc.

Also, I'm not sure about that "empty_data => null" in your form type, could you comment it out and try again? It might be the reason btw.

But, in general I'd not use Browser's built-in datetime picker and try a JS library instead.

I hope this helps!

Reply
davidmintz Avatar
davidmintz Avatar davidmintz | Victor | posted 1 year ago | edited

Hi. Thank you for the advice. When I look at the Profiler's Validator Calls, the date field's invalid value is indeed a DateTime object. When I look at the $_POST parameters in the Profiler, it's "date'" => "2022-02-15" as it should be. The Firefox console shows the same thing.

I changed the field type to TextType; attached a CallbackTransformer to convert between string and DateTime; and tried manually entering "2022-02-15". The result was that I still get the same validation error message "This value should be of type string." I tried commenting out the data-transformation bit, and the result is 'Expected argument of type "?DateTimeInterface", "string" given at property path "date".' -- which would make sense, were it not for the fact that this is failng either way.

I also removed ['empty_data'=>''] but that made no difference....

<b>UPDATE!</b>

I changed the form field back to native HTML5 Date type, but removed the @Assert\Date from the entity class -- <i>and that made the issue go away!</i> Going back to the <a href="https://symfony.com/doc/current/reference/constraints/Date.html&quot;&gt;docs&lt;/a&gt;, I finally understand. That constraint is for <i>strings</i>! So when it was getting a DateTime it was rightly complaining that it wasn't getting a string! Agh. Sometimes it's telling you the simple truth as plainly as can be, and it still takes a couple hours to understand :-) Silly mistake, made harder to track down because it wasn't displaying the custom error message I had configured.

1 Reply

Hey David,

Ah, I see now! :) Yeah, a really tricky case that easily misunderstood, I'm happy you were able to track it down and figured out the problem yourself, well done! And thanks for sharing the final solution!

Cheers!

Reply
Matteo S. Avatar
Matteo S. Avatar Matteo S. | posted 1 year ago

I have a form with an email field of type EmailType::class, but when I submit the form, $form->isValid() returns true even if the value of the email field is not a valid email address (no "@"). How come?

Reply

Hey Matteo S.

Having email field has nothing with validation, you should add validation constraints to the form or to the entity property. You will learn more about validation on chapter 10 of this course =)

Cheers!

Reply
Matteo S. Avatar

isValid() has nothing to do with validation?? If I add a field of IntegerType, it gets validated automatically (if I submit a non-numeric string isValid() returns false and I get an error message in the form widget), but if I add a field of EmailType type, it is not? How does that make sense? See my full example code here: https://github.com/symfony/...

Reply

The message you got with IntegerType is more core type validation, because value should be bound to Integer type property, and you got string, that's why it fails, but email field is bound to simple String and it is a valid string. To have proper validation you should have constraints configured in your form or entity, for example


use Symfony\Component\Validator\Constraints\Email;

class RegisterFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('email', EmailType::class, [
                'constraints' => [
                    new Email()
                ]
            ])
            ->add('password', PasswordType::class)
            ->add('name')
        ;
    }
}

BTW you should have symfony/validator installed

Cheers!

Reply

Using Symfony/Forms 4.4. When I tried to add widget as an option I get an error saying that widget is not an option that is available. Did I miss something when I did the composer require that makes this an option?

Reply

Hey captsulu32

What FormFieldType are you using?

Cheers!

Reply

`
->add('ssn', null,

                [
                    'label' => 'Social Security Number',
                    'attr' => [
                        'placeholder' => '#########',
                    ]
                ])

`
As you can see null

Reply

Ok, so you're letting the TypeGuesser sytem to guess the Type of your "ssn" field. Try specifying a Date or DateTime type instead.

Reply
Mike P. Avatar
Mike P. Avatar Mike P. | posted 3 years ago | edited

<b>Goal:</b>
I need a field "time spent to write an article".
The form does have two fields, hours and minutes.

But the hours can exceed 24, so it is possible that the user enters 99 hours and 59 minutes.
Since TimeType cant do this and DateTimeType (like in this tutorial) seems to be like a "hacky solution", I chose DateIntervalType (I think this is best practise?).

My form:


->add('timeSpent', DateIntervalType::class, [
                'widget'      => 'integer', // render a text field for each part
                // 'input'    => 'string',  // if you want the field to return a ISO 8601 string back to you
                // customize which text boxes are shown
                'with_years'  => false,
                'with_months' => false,
                'with_days'   => false,
                'with_hours'  => true,
                'with_minutes' => true,
//Not working: 'empty_data' => 0,
                'required' => false
            ])

This field is 100% optional, but should NOT be prefilled with 00:00 on the front end (which would work), because the field is easier to "forget" if its prefilled on the front end.
So on the front end, it should be empty unless the user typed something in.

<b>Problem:</b>
Even if I set required = false, I get an transformation failed exception if the two fields (hours & minutes) are empty:

Symfony\Component\Validator\ConstraintViolation {#1949 ▼
  -message: "This value is not valid."
  -messageTemplate: "This value is not valid."
  -parameters: [▶]
  -plural: null
  -root: Symfony\Component\Form\Form {#1412 ▶}
  -propertyPath: "children[spendTime].children[hours]"
  -invalidValue: ""
  -constraint: Symfony\Component\Form\Extension\Validator\Constraints\Form {#1605 …}
  -code: "1dafa156-89e1-4736-b832-419c2e501fca"
  -cause: Symfony\Component\Form\Exception\TransformationFailedException {#1559 …}```


<b>Question:</b>
How can I define a DateIntervalType with two empty fields (for hours & minutes, if the user doesn't type in anything on the front end) without a transformation failed exception from SF?
(Because it seems that SF cant handle an empty string even if required = false? Is this maybe even a SF bug?)

The only possible solution I've found at this time is to set:
'data' => new \DateInterval('P0Y'),

But I don't want to have 00:00 on the front end.
Reply
Mike P. Avatar

I've found the solution!

Instead of:
'widget' => 'integer', // render a text field for each part

I need to use:
'widget' => 'text', // render a text field for each part

SF seems to be pretty "type strict", meaning if I set "integer", it really wants an integer every time, "" (empty string) is not accepted.

Yet I don't understand exactly, what "text" can do / accepts, what "integer" can't. (Despite the "text" allows an empty string)

Reply

Hey Mike P.

I'm glad that you could find a solution. Regarding to your question, the only difference I can tell between text and integer type is the input field that will be rendered. You can read more about it in the docs: https://symfony.com/doc/cur...

Cheers!

Reply
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | posted 3 years ago

If we do not set a type and leave it for guessing, does it slows down performance on each page load, or it is cached and so only on cache build?

Reply

Hey Lijana Z.!

Wow, really great question! It's honestly not something I had thought about :). From checking out the code, I don't see any evidence of this stuff being cached. Here is the method that is responsible for all this "guessing: (and, like I said, it looks like the code that calls this does not include any caching): https://github.com/symfony/...

I'll say 2 things about this:

1) The "guessers" themselves often *do* contain caching. For example, Doctrine metadata is cached - so checking whether or not a column is "nullable=false" is nearly instant. Or, the property-info component, which is responsible for looking at a class to determine if a property is mutable (e.g. does it have a setter? is it public? does it have a constructor arg?) also caches its metadata. So while the system overall isn't cached, the guessers *do* tend to have caching.

2) I've never profiled this guessing vs not-using-guessing directly, but I've never seen (while using Blackfire.io) that the guessing system took any amount of significant performance. If I was worried, that's what I would check: profile your form load with Blackfire and see if it's *really* a problem. We have a free screencast on Blackfire... which is fun anyways ;) https://symfonycasts.com/sc...

Cheers!

1 Reply
Maik T. Avatar
Maik T. Avatar Maik T. | posted 3 years ago

Actually Chrome Browser renders a Text Field instead of the expected Date and Time dropdowns if set no type and options.

Version 78.0.3904.108 (November 2019)

Reply

Hey Maik,

Hm, this sounds like Symfony was not able to guess the field type correctly, that's why it renders text field instead of date field. It might depends on your mapping configuration for that field in your entity I think.

Cheers!

Reply
Akavir S. Avatar
Akavir S. Avatar Akavir S. | posted 3 years ago

Warning, Mozilia does not support the calendar widget.
https://developer.mozilla.o...

Reply

Hey Virgile,

Thank you for this warning. Yeah, Firefox is the browser that does not support it on desktops... though probably it does on mobiles. Anyway, to support it well, it's still better to use a JS library for it.

Cheers!

1 Reply

I have done something a little bit advanced compared to the tutorial, but I have a problem with that, the idea is ok but actually doens't work.

I have build a FilterFrom that I'll use to filter my articles, and is built like that:

`public function buildForm(FormBuilderInterface $builder, array $options)

{
    $builder
        ->add('date1', DateType::class, [
            'widget' => 'single_text',
            // this is actually the default format for single_text
            'attr' => ['class' => 'form-control datetimepicker-input',  'data-target' => '#datetimepicker7'],
            'html5' => false,
            'required' => false,
        ])
        ->add('date2', DateType::class, [
            'widget' => 'single_text',
            // this is actually the default format for single_text
            'attr' => ['class' => 'form-control datetimepicker-input',  'data-target' => '#datetimepicker8'],
            'html5' => false,
        ])
        ->add('text', null)
        ->add('tag', EntityType::class, [
            'class' => Tag::class,
            'choices' => $this->tagRepository->findAllTagsAlphabetical(),
            'choice_label' => 'name',
            'by_reference' => false,

        ]);
}`

I want to use datatimepicker7-8 which are the one who are linked one to another so you can select a interval between 2 dates, the template look like this:

           ` {{ form_start(filterForm, {'attr': {'novalidate': 'novalidate', class: 'form-horizontal collapse in', id: 'content-viewer-list-filters_1'}}) }}

            <div class="col-lg-12 col-md-8 col-sm-12 col-xs-12 calendario">
                <div class="filtri-ricerca-item">
                    <div class="row">
                        <div class="col-lg-6 col-md-6 col-sm-6 col-xs-6">
                            <div class="form-control">
                                {{ form_label(filterForm.date1, {'attr': {'for': 'from_frm'}})}}
                                <div class="input-group date" id="datetimepicker7" data-target-input="nearest">
                                {{ form_widget(filterForm.date1) }}
                                    <div class="input-group-append" data-target="#datetimepicker7"
                                         data-toggle="datetimepicker">
                                        <div class="input-group-text"><i class="fas fa-calendar-alt"></i></div>
                                    </div>
                                <small>{{ form_help(filterForm.date1) }}</small>
                                <div class="form-error">
                                    {{ form_errors(filterForm.date1) }}
                                </div>
                                </div>
                            </div>

                        </div>
                        <div class="col-lg-6 col-md-6 col-sm-6 col-xs-6">
                            <div class="form-control">
                                {{ form_label(filterForm.date1, {'attr': {'for': 'from_frm'}})}}
                                <div class="input-group date" id="datetimepicker8" data-target-input="nearest">
                                    {{ form_widget(filterForm.date2) }}
                                    <div class="input-group-append" data-target="#datetimepicker8"
                                         data-toggle="datetimepicker">
                                        <div class="input-group-text"><i class="fas fa-calendar-alt"></i></div>
                                    </div>
                                    <small>{{ form_help(filterForm.date2) }}</small>
                                    <div class="form-error">
                                        {{ form_errors(filterForm.date2) }}
                                    </div>
                                </div>
                            </div>

                        </div>

                    </div>
                </div>

            </div>
            <div class="col-lg-12 col-md-8 col-sm-12 col-xs-12 calendario">

                <div class="filtri-ricerca-item">
                    <div class="row">
                        <div class="col-lg-6 col-md-4 col-md-4 col-sm-12 col-xs-12">

                            {{ form_row(filterForm.text) }}

                        </div>
                        <div class="col-lg-6 col-md-4 col-md-4 col-sm-12 col-xs-12">

                            {{ form_row(filterForm.tag) }}
                        </div>
                    </div>
                </div>
            </div>
        <!--</fieldset>-->
        <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
            <button type="submit" class="btn btn-primary" formnovalidate>Imposta</button>
            <input type="reset" value="Reset" class="btn" id="clearfilter_1"/>
        </div>
    {{ form_end(filterForm) }}

`
I tried to adjust everything in a way that could work, but I get the error:
An exception has been thrown during the rendering of a template ("Notice: Array to string conversion") in "bootstrap_4_layout.html.twig".

Js:
`
<script src={{ asset('js/moment.js') }}></script>

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/tempusdominus-bootstrap-4/5.0.0-alpha14/js/tempusdominus-bootstrap-4.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tempusdominus-bootstrap-4/5.0.0-alpha14/css/tempusdominus-bootstrap-4.min.css" />
<script type="text/javascript">
    $(document).ready(function() {
        $('#datetimepicker7').datetimepicker();
        $('#datetimepicker8').datetimepicker({
            useCurrent: false
        });
        $("#datetimepicker7").on("change.datetimepicker", function (e) {
            $('#datetimepicker8').datetimepicker('minDate', e.date);
        });
        $("#datetimepicker8").on("change.datetimepicker", function (e) {
            $('#datetimepicker7').datetimepicker('maxDate', e.date);
        });
    });
</script>

`
Can you help me?
P.S: I'm not using encore

Reply

Hey Gballocc7

Something inside bootstrap_4_layout.html.twig is trying to print an array (it should be a string). I believe it comes from your tag field. Try removing it just to be sure that's the problem and if that's the problem, check your repository method, in what form is it returning the data?

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.6
        "symfony/console": "^4.0", // v4.1.6
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.6
        "symfony/framework-bundle": "^4.0", // v4.1.6
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.6
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.6
        "symfony/validator": "^4.0", // v4.1.6
        "symfony/web-server-bundle": "^4.0", // v4.1.6
        "symfony/yaml": "^4.0", // v4.1.6
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.6
        "symfony/dotenv": "^4.0", // v4.1.6
        "symfony/maker-bundle": "^1.0", // v1.8.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.6
    }
}
userVoice