Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple Timezoning in Laravel/Carbon

I was wondering if this was possible, so let's say I have a model like so:

MyModel
   SomeDate - Carbon

Now, I also have a timezone for the current user like so:

User
   MyTimezone

the timezones stored in the database are always stored in UTC format (to ensure everything is consistent), and the outputted dates should always be formatted to a specific Timezone (but timezone differs per user), for example America/Chicago for User1 and America/Denver for User2.

Is there a way to automatically format the timezones per Carbon instance to a given one before outputting, or will I have to loop through the collection and set each one accordingly?

Setting app.timezone doesn't work because it also causes Carbon instances to be saved to the database in the app.timezone timezone, whereas all dates in the database should be in UTC, therefore I lose consistency.

I currently have app.timezone set to UTC in the App config but I'm also forced to convert all Carbon instances to the correct timezone before outputting. Is there a better way, maybe by trapping execution before Carbon gets turned into a string and doing it there?

EDIT:

Things i've tried:

Override setAttribute & getAttribute:

public function setAttribute($property, $value) {
    if ($value instanceof Carbon) {
        $value->timezone = 'UTC';
    }

    parent::setAttribute($property, $value);
}

public function getAttribute($key) {
    $stuff = parent::getAttribute($key);

    if ($stuff instanceof Carbon) {
        $stuff->timezone = Helper::fetchUserTimezone();
    }

    return $stuff;
}

overriding asDateTime:

protected function asDateTime($value)
{
    // If this value is an integer, we will assume it is a UNIX timestamp's value
    // and format a Carbon object from this timestamp. This allows flexibility
    // when defining your date fields as they might be UNIX timestamps here.
    $timezone = Helper::fetchUserTimezone();

    if (is_numeric($value))
    {
        return Carbon::createFromTimestamp($value, $timezone);
    }

    // If the value is in simply year, month, day format, we will instantiate the
    // Carbon instances from that format. Again, this provides for simple date
    // fields on the database, while still supporting Carbonized conversion.
    elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value))
    {
        return Carbon::createFromFormat('Y-m-d', $value, $timezone)->startOfDay();
    }

    // Finally, we will just assume this date is in the format used by default on
    // the database connection and use that format to create the Carbon object
    // that is returned back out to the developers after we convert it here.
    elseif ( ! $value instanceof DateTime)
    {
        $format = $this->getDateFormat();

        return Carbon::createFromFormat($format, $value, $timezone);
    }

    return Carbon::instance($value);
}
like image 535
David Xu Avatar asked Jan 06 '15 06:01

David Xu


1 Answers

Running into the same issue for my application where remote websites would store dates in UTC and I'd have to show the actual dates based on the logged in user, I came up with overriding the Laravel Eloquent Model.

Just extend the Illuminate\Database\Eloquent\Model, like so:

<?php namespace Vendor\Package;

use Illuminate\Database\Eloquent\Model as EloquentModel;

class Model extends EloquentModel
{

    /**
    * Return a timestamp as a localized DateTime object.
    *
    * @param  mixed  $value
    * @return \Carbon\Carbon
    */
    protected function asDateTime($value)
    {
        $carbon = parent::asDateTime($value);
        // only make localized if timezone is known
        if(Auth::check() && Auth::user()->timezone)
        {
            $timezone = new DateTimeZone(Auth::user()->timezone);
            // mutates the carbon object immediately
            $carbon->setTimezone($timezone);
        }

        return $carbon;
    }
    /**
    * Convert a localized DateTime to a normalized storable string.
    *
    * @param  \DateTime|int  $value
    * @return string
    */
    public function fromDateTime($value)
    {
        $save = parent::fromDateTime($value);

        // only make localized if timezone is known
        if(Auth::check() && Auth::user()->timezone)
        {
            // the format the value is saved to
            $format = $this->getDateFormat();

            // user timezone
            $timezone = new DateTimeZone(Auth::user()->timezone);

            $carbon = Carbon::createFromFormat($format, $value, $timezone);
            // mutates the carbon object immediately
            $carbon->setTimezone(Config::get('app.timezone'));

            // now save to format
            $save = $carbon->format($format);
        }

        return $save;
    }
}

Perhaps this is useful for others stumbling upon this question.

As a reference

  • laravel 5 (2015-03-18): Illuminate\Database\Eloquent\Model:2809-2889
  • laravel 4.2 (2015-03-18): Illuminate\Database\Eloquent\Model:2583-2662
like image 70
Luceos Avatar answered Oct 02 '22 13:10

Luceos