5

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);
}
David Xu
  • 5,555
  • 3
  • 28
  • 50
  • Just an idea but pass the Time data to JS and have the JS do the calculation for you. http://stackoverflow.com/questions/1091372/getting-the-clients-timezone-in-javascript – clonerworks Jan 06 '15 at 06:06

2 Answers2

5

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
Luceos
  • 6,629
  • 1
  • 35
  • 65
1

If I understand correctly, what you are trying to achieve is to convert timezone from A format to B format and send it to the user, where A format is stored in database and B format is converted to after retrieving records from database.

Here is a neat way to do that.

In the models such as User and MyModel where conversion is needed, add a function in model:

public function getConversionAttribute()
{
    $conversion = Convert($this->SomeDate);
    //Convert is the customized function to convert data format
    //SomeDate is the original column name of dates stored in your database
    return $conversion;
}

Now if you query User model or MyModel using $user = User::find(1), you can now get the converted date by accessing the conversion attribute using $user->conversion.

Cheers!

However, attribute added this way will not included in converted array. You need to add another function in your model.

public function toArray()
{
    $array = parent::toArray();
    //if you want to override the original attribute
    $array['SomeDate'] = $this->conversion;
    //if you want to keep both the original format and the current format
    //use this: $array['Conversion'] = $this->conversion;
    return $array;
}

General Version:

public function toArray() {
    $array = parent::toArray();
    //if you want to override the original attribute
    $dates = $this->getDates();

    foreach ($dates as $date) {
        $local = $this->{$date}->copy();
        $local->timezone = ...
        $array[$date] = (string)$local;
    }
    //if you want to keep both the original format and the current format
    //use this: $array['Conversion'] = $this->conversion;
    return $array;
}
David Xu
  • 5,555
  • 3
  • 28
  • 50
Ray
  • 631
  • 5
  • 6
  • The problem with this is if I call `->toArray()` on the model, the value I want, eg `start_time` is still in the database timezone. – David Xu Jan 06 '15 at 07:29
  • Try this: public function getConversionAttribute() { $this->SomeDate = Convert($this->SomeDate); //Convert is the customized function to convert data format //SomeDate is the original column name of dates stored in your database return $this->SomeDate; } – Ray Jan 06 '15 at 07:34
  • It should alter original format as you wish. As long as you do not save it in the database using `$this->save()`, the database entries will not be affected. – Ray Jan 06 '15 at 07:35
  • The problem goes with toArray(). Attribute added this way will not be included in converted array. But you can access them using `$this->conversion` – Ray Jan 06 '15 at 07:42
  • I need a way to output them by just using toArray(), as that is what all my code uses to convert objects to JSON format. – David Xu Jan 06 '15 at 07:44
  • getConversionAttribute() as the name suggests, should add the attribute name 'conversion' in between get and attribute. You miss that in your implementation. – Ray Jan 06 '15 at 07:44
  • Hey, please see my edited answer. it should work now as you wish. – Ray Jan 06 '15 at 07:49
  • 1
    Cool, I just made a much more generalized version of your function, I'll accept yours now and I'll also add the generalized version. – David Xu Jan 06 '15 at 08:06