Framework: Serialized date format should not be tied to the database format

Created on 10 Feb 2017  路  13Comments  路  Source: laravel/framework

  • Laravel Version: 5.x.x
  • PHP Version: *
  • Database Driver & Version: *

Description:

Laravel uses a simplified format for storing dates in the database. By convention these values are only ever UTC so it is internally consistent and works well. However by serializing the date format to this format without timezone information Laravel leaks the underlying storage implementation logic into the data structures and causes problems for integrating systems by not using an inter-operable format.

Problems:

  1. Is arguably wrong in JSON. JSON doesn't have a native format for dates but it is fairly well accepted dates should be in a Javascript compatible format like RFC8601.
  2. Lack explicit timezone information requires understanding of the underlying system or is up to the consuming system to define. If we assume the date _is_ 8601 since the timezone is technicall not required, in RFC8601 systems should use the local time. While this is not generally a problem on servers since a seasoned sysadmin will always set the timezones to UTC it is a problem in Javascript since the local time in a browser is seldom going to be UTC. The Angular snippet {{::f.updated_date | dateString | date : 'short'}} only works out of the box if you live in Greenwich for example.
  3. Requires complicated translations and/or mutation of the model classes to fix. #10414
  4. Causes confusion #13876 (maybe other places, I just did a quick search and skim for similar issues)

Steps To Reproduce:

  • Create a model with timestamps.
  • Return that model from a controller or otherwise serialize it.
  • See non-standard date format.

Most helpful comment

That does seem to address the issue but are they still connected by default with just a different "easier" method for replacing the serialization format? Seems like we should get things correct out of the box.

All 13 comments

The laravel docs recommend overriding the serializeDate method on your model, which allows you to customize how you wish.

    /**
     * Prepare a date for array / JSON serialization.
     *
     * @param  \DateTimeInterface  $date
     * @return string
     */
    protected function serializeDate(DateTimeInterface $date)
    {
        return $date->format($this->getDateFormat());
    }

_From HasAttributes.php_

I guess the Laravel docs agree its wrong then too?

There's also a use case for having different formats for different columns. For example, in the case when there are a combination of dates and datetimes on the table, we might want to serialize dates with the format 'Y-m-d' and datetimes with 'Y-m-d H:i:s'.

I m agree with @neclimdul . Something is wrong with the dates in laravel.

As input too. By default, laravel can't parse iso8601. It try to parse it as a database date. I think Model should be able to parse most common date format or at least we need a way to set a second date format for output.

I'm also having issues with this. I've implemented serializeDate to get ISO8601 dates serialized into JSON. But I need a function (unserializeDate?) that makes Carbon accept different date formats. Right now I send a ISO8601 formatted date back and I get this error:

InvalidArgumentException
Unexpected data found.
Trailing data

in Carbon.php (line 582)
  Carbon::createFromFormat('Y-m-d H:i:s', '2017-06-12T17:10:02Z')
...

Another solution would be if I could set the default format for Carbon to use. If I could have Carbon store dates in the database as ISO8601 and it always parses dates as ISO8601 I would be set.

I would actually prefer setting a default format over adding serializeDate, accessors, and mutators to all of my models that have dates in them (practically all of them).

Laravel 5.5 and after [UPDATED]

Use date serialization from the documentation.

Laravel 5.4 and before

@AustP I have a work around for unserialization. It's not the best, but it works for me.

Define this trait in your custom request namespace (or anywhere else)

use Carbon\Carbon;
use Illuminate\Validation\Validator;

/**
 * Trait NormalizesISO8601Dates
 * @package App\Models
 *
 * This traits allows to convert momentjs toISOString() into a Carbon UTC date Laravel
 * is able to use. This trait should be use on custom form requests. This trait will alter the
 * state of the incoming request.
 */
trait NormalizesISO8601Dates
{
    public function withValidator(Validator $validator)
    {
        if (! isset(static::$normalizedDates)) {
            throw new \Exception('Requests that uses ' . __TRAIT__ . ' trait need to have a $normalizedDates attribute');
        }

        // Grab the input
        $data = $this->getInputSource()->all();

        foreach (static::$normalizedDates as $field) {
            if (isset($data[$field]) && strtotime($data[$field])) {
                $data[$field] = (new Carbon($data[$field]))->toDateTimeString();
            }
        }

        // Replace modified data back into input.
        $this->getInputSource()->replace($data);
    }
}

Then after, in a custom form request

class MyCustomFormRequest extends FormRequest
{
    use NormalizesISO8601Dates;

    // This property is mendatory with the trait. List all parameters you wish to convert from ISO 8601 to Laravel parsable format.
    protected static $normalizedDates = ['starts_at', 'ends_at'];

    ...
}

Unfortunately, the date is converted twice, once to convert it into Laravel's readable format and again by Laravel when casted to date. Honestly, I think this should be native into the framework.

@lunfel See this... i think that is related 19920

Edited:
removed mark as related.

@ptsilva I don't see why it's related. But maybe my last comment was confusing.

I was meant that it is converted twice because in my workaround I have to parse the date $data[$field] = (new Carbon($data[$field]))->toDateTimeString(); to fix the format. I put it right back into the request and then let Laravel handle it. Which then, it will cast again to Carbon object assuming we are using the $dates property on the model.

Really... this is not related. I thought that converted twice it was because of $this->getInputSource(), i think that have something wrong with this method. Anyway, sorry.

Agree completely. I'll reiterate what I just posted in #20404:

Nowadays, API consumers expect ISO 8601 format dates. It would be great if the serialized/return date format could be configured instead of having to override the serializeDate method on all models, preferably also configured just once.

Having dateFormat property on models define both the database and the return format is not very useful, when MySQL for instance supports only three types of date/time column types, while output of date/time has so many more facets.

Alternatively, if Laravel would respect the Carbon setting for dateTime format, or keep some global default for datetime formatting, that would be cool too, so you could just define this once, for instance:

Carbon::setToStringFormat(DateTime::ATOM);

The serializeDate function actually calls the getDateFormat function on the model. So actually the only thing you need to do is overriding this function:

<?php

namespace App\Entities;

use Illuminate\Database\Eloquent\Model;

class Person extends Model
{
    ...

    protected $casts = [
        'born'  =>  'date',
        'died'  =>  'date'
    ];

    // don't include time in json-string
    protected function getDateFormat() {
        return "d-m-Y";
    }
}

Actually, this has been addressed in 5.5. See documentation .

I think we can close this issue now.

That does seem to address the issue but are they still connected by default with just a different "easier" method for replacing the serialization format? Seems like we should get things correct out of the box.

Was this page helpful?
0 / 5 - 0 ratings