Timestamps on Symfony Forms


Let's say you want to capture timestamps inside your Symfony site.

I did recently.

I had incoming API requests that contained a timestamp to indicate the time the event took place. You might think: why not just use the typical createdAt / updatedAt combo?

Well, in this instance, one of the business requirements was to be able to import data that was out of date order.

We did have the timestamp of the original data creation date.

I figured we could easily create a Symfony form that matched the incoming data 'shape'. But when I wanted to add in a form field type to capture dates formatted as timestamps, I came up blank.

Strange.

Often when I can't find a solution to this in Symfony it's that I am simply not using the right combination of options. So if this is the case, do please let me know of the official / better way of doing this.

Anyway, it's as good an opportunity as ever to have a look at Symfony form Data Transformers, should you never have seen or used one before.

Data |> Lore

In Symfony, a data transformer converts between one format and another.

A really common example of this is in handling dates.

Whether they come in as strings, or choices, or whatever else, the date in some format needs to be converted / transformed into a date format that PHP can understand.

In the olden days you may have been tempted to inline this logic.

This would inevitably lead to functions well over the 100 line mark, and a complexity so advanced only an Elon Musk brain enhancement implant could see you through.

This Is Why You Use Symfony

Instead, Symfony (or other frameworks) aim to make stuff like this possible in a manner which is separated, and therefore, re-usable.

However, it does come at the cost of been, initially, another notch on the learning incline.

Transform

We're going to create ourselves a data transformer that allows us / our users to send in data in the format of a numerical timestamp.

To hold this data, we will need to add in a new field to our entity:

// /src/AppBundle/Entity/BlogPost.php

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="accurate_at", type="datetime")
     */
    protected $accurateAt;

    // snip

    /**
     * @return \DateTime
     */
    public function getAccurateAt()
    {
        return $this->accurateAt;
    }

    /**
     * @param \DateTime $accurateAt
     * @return BlogPost
     */
    public function setAccurateAt(\DateTime $accurateAt)
    {
        $this->accurateAt = $accurateAt;
        return $this;
    }

In the video used in this example I am re-using the code from this course, but the concept is the same no matter where you apply it.

Given that our entity field is called accurateAt, to capture this information on our associated form type, we must add a form field also called accurateAt.

However, here we hit upon the problem.

We can add in a form field type that accepts date input, but our issue is that we cannot add in a form field type that accepts dates in the form of a timestamp.

At least, not without extra work on our part. That is to say, at least, not that I know of ;)

Firstly, let's update the form:

<?php

// /src/AppBundle/Form/Type/BlogPostType.php

namespace AppBundle\Form\Type;

use AppBundle\Form\DataTransformer\TimestampToDateTimeTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class BlogPostType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title', TextType::class)
            ->add('body', TextType::class)
            ->add('accurateAt', NumberType::class)
            ->add('submit', SubmitType::class)
        ;

        $builder
            ->get('accurateAt')
            ->addModelTransformer(new TimestampToDateTimeTransformer())
        ;
    }

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

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

We make two changes here.

Firstly we add in the accurateAt field.

Then we do something a little unusual:

use AppBundle\Form\DataTransformer\TimestampToDateTimeTransformer;

    // snip

    public function configureOptions(OptionsResolver $resolver)
    {
        $builder
            ->get('accurateAt')
            ->addModelTransformer(new TimestampToDateTimeTransformer())
        ;
    }

This is how our form will know how to translate our data from a timestamp to a \DateTime and back again.

We haven't actually declared that class though, so let's do so:

<?php

// /src/AppBundle/Form/DataTransformer/TimestampToDateTimeTransformer.php

namespace AppBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;

class TimestampToDateTimeTransformer implements DataTransformerInterface
{
    /**
     * @param \DateTime $datetime
     */
    public function transform($datetime)
    {
        if ($datetime === null) {
            return (new \DateTime('now'))->getTimestamp();
        }

        return $datetime->getTimestamp();
    }

    public function reverseTransform($timestamp)
    {
        return (new \DateTime())->setTimestamp($timestamp);
    }
}

If the single line new and set looks unfamiliar to you, this is syntax available from PHP 5.4 onwards called "Class member access on instantiation".

Really the tricky parts here at the names.

At least, they are for me.

You want to transform when you are going from database to your form.

You want to reverseTransform when going from your form to your database.

If you get confused - it's almost inevitable - then dump is your friend, along with a few runs through your application to brute force it into your brain.

Asserting Things

It makes sense to check input.

Stopping bad input at source is the key to avoiding one of the worst problems possible in the life of a typical web developer. As a consultant developer being brought into problematic projects, this is usually one of the most frequent problems I see.

Thankfully, the fix is really easy.

Assertions.

Symfony Is Trying To Making Your Life Easier

With Symfony it is easy to add annotations to your entities to ensure only data in a particular 'shape' can ever get persisted / saved to your database.

I've covered validation with Symfony forms before, so I won't go into it in depth. But suffice to say, if you are accepting input, for your own and the sanity of your fellow developers: validate.

By converting our data from a number into a DateTime we can leverage a bunch of Symfony's Validation Constraints for dates, but also nicely "comparisons". It's ok to be excited by this. I am :)

// /src/AppBundle/Entity/BlogPost.php

     /**
      * @var \DateTime
      *
      * @ORM\Column(name="accurate_at", type="datetime")
      * @Assert\DateTime()
      * @Assert\GreaterThan("January 1st, 2016")
      * @Assert\LessThanOrEqual("now")
      */
    protected $accurateAt;

Personally I think this is super cool.

However, if you know of a better way, please share it in the comments below. I'm always happy to improve.

Code For This Course

Get the code for this course.

Code For This Video

Get the code for this video.

Episodes