Multiple Timezoning in Laravel/Carbon - php

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);
}

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

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;
}

Related

Get DateTime object from Phalcon Model for DateTime column in PHP

In the database table there are some columns with DATETIME type. Is there any way I can get Phalcon Model to convert them automatically from string to DateTime object in PHP and vice versa without using beforeSave() and afterFetch()?
I tried to use annotation #Column(type='datetime'), but the data is still returned as string. And #Column(type='boolean') works fine for me though.
Could you please advise me on the right approach?
#Ярослав Рахматуллин is one way to go
or you can use phalcon setters and getters ( much simpler way )
check the documentation here
and this an example of the implementation for you situation to my understanding
use \DateTime;
class Foo extends \Phalcon\Mvc\Model
{
// set your column property to protected
protected $date;
// will be called when protected property $date is accessed
public function getDate()
{
// add your validation here to check if the date is not null
return new DateTime($this->date ?: 'now');
}
// will be called when protected property $date is set
public function setDate(DateTime $date = null)
{
// add your validation here to check if $date is not null
if (!is_object($date)) {
$date = new DateTime;
}
// this is mysql format for timestamp
$this->date = $date->format('Y-m-d H:i:s');
return $this;
}
}
// find first row
$foo = Foo::findFirst();
// check if the row exists
if ($foo) {
// $foo->date would return getDate()
var_dump($foo->date); // DateTime object
// this would call setDate()
// column date will convert DateTime object to the format Y-m-d H:i:s
$foo->date = new DateTime('2019-09-28'); // 2019-09-28 00:00:00
$foo->save();
}

Change automatically the format of date fields (Laravel)

I'm looking for a DateTime Mutator that change the format of dates, I'm working with Oracle DB and the admitted Format is (DD/MM/YYYY) and the input type "date" stores dates in (YYYY,MM,DD) format.
I found the $date function and a trait by Torzer, but I have to indicate the fields that I want to convert the format.
is there some trait or function that detect all date fields and convert them automatically in a format (DD/MM/YYYY)? this without indicate the field.
nowadays I use protected $date in my model:
protected $dates = [ 'fecha_nac', 'fecha_nac1', 'fecha_nac2', ];
By default laravel uses date formate 'Y-m-d H:i:s' if you want to use a different format you can customize it in your model in the following way.
protected $dateFormat = 'your date formate';
in your case it will be.
protected $dateFormat = 'd-m-Y';
You can override the getDates method on HasAttributes trait.
/**
* Get the attributes that should be converted to dates.
*
* #return array
*/
public function getDates()
{
$defaults = [static::CREATED_AT, static::UPDATED_AT];
return $this->usesTimestamps()
? array_unique(array_merge($this->dates, $defaults))
: $this->dates;
}
On your model:
public function getDates()
{
$dates = parent::getDates();
// add your dynamic logic here
return $dates;
}
I would really go for explicitly defining which fields should be converted as these dynamic operations can be expensive if you are working with the model quite a lot.

Laravel - Make only 3 functions for getters and setters

I believe that there are more developers who need to have a locale date format (shown in app front end) than the ones who use the default format in browsers which is 12/22/2016.
So i have made a small trait in my Laravel project for standard dates like created_at, updated_at and deleted_at:
<?php
namespace App\Traits;
use Carbon\Carbon;
trait FormatDates
{
protected $localFormat = 'd.m.Y H:i';
// save the date in UTC format in DB table
public function setCreatedAtAttribute($date)
{
$this->attributes['created_at'] = Carbon::parse($date);
}
// convert the UTC format to local format
public function getCreatedAtAttribute($date)
{
return Carbon::parse($date)->format($this->localFormat);
}
// get diffForHumans for this attribute
public function getCreatedAtHumanAttribute()
{
return Carbon::parse($this->attributes['created_at'])->diffForHumans();
}
// save the date in UTC format in DB table
public function setUpdatedAtAttribute($date)
{
$this->attributes['updated_at'] = Carbon::parse($date);
}
// convert the UTC format to local format
public function getUpdatedAtAttribute($date)
{
return Carbon::parse($date)->format($this->localFormat);
}
// get diffForHumans for this attribute
public function getUpdatedAtHumanAttribute()
{
return Carbon::parse($this->attributes['updated_at'])->diffForHumans();
}
// save the date in UTC format in DB table
public function setPublishedAtAttribute($date)
{
$this->attributes['published_at'] = Carbon::parse($date);
}
// convert the UTC format to local format
public function getPublishedAtAttribute($date)
{
return Carbon::parse($date)->format($this->localFormat);
}
// get diffForHumans for this attribute
public function getPublishedAtHumanAttribute()
{
return Carbon::parse($this->attributes['published_at'])->diffForHumans();
}
// save the date in UTC format in DB table
public function setDeletedAtAttribute($date)
{
$this->attributes['deleted_at'] = Carbon::parse($date);
}
// convert the UTC format to local format
public function getDeletedAtAttribute($date)
{
return Carbon::parse($date)->format($this->localFormat);
}
// get diffForHumans for this attribute
public function getDeletedAtHumanAttribute()
{
return Carbon::parse($this->attributes['deleted_at'])->diffForHumans();
}
}
There are actually only 3 functions for those dates and those functions are:
set the date so it can be saved with date time picker
get the date in locale format (22.12.2016 14:39)
get the date in human readable format
So my question is how to make this trait to have only 3 functions instead of repeating it all the time for every single variable? Is this doable?
You could set it up similar to Custom setters and getters in Laravel.
The __get() / __set() methods in your trait will be called prior to the getXAttribute() / setXAttribute() method in the eloquent model.
You can just fetch the dates per model with $this->getDates() and create a helper method to define which method you should call on which date field.
Though less code is required for this solution, personally I don't see a whole lot wrong with having specific accessors & mutators in the FormatDates trait, looking at readability.

PHP DateTime get format in UTC without converting object

I have an object that has a property that is a DateTime object that has a set timezone depending on the user's timezone. At some point I need to store the object where in my storage layer the property is a UTC datetime string, however I may need to continue using the object after it has been persisted and currently my approach converts the object to another timezone and then sets it back to what it used to be:
/* "starts" is the name of the property and this is just a "magic" method my driver
will call before persisting the object. */
public function __persist_starts() {
$tz = $this->starts->getTimezone();
$this->starts->setTimezone(new DateTimeZone("UTC"));
$value = $this->starts->format('Y-m-d H:i:s');
$this->starts->setTimezone($tz);
return $value;
}
This appears to be a "hacky" solution, isn't there something clearer I can use. I'm imagining something like
public function __persist_starts() {
return $this->starts->formatUTC('Y-m-d H:i:s');
}
Although there is nothing like formatUTC, there are few other options, slightly better than the "hacky" solution:
if you have such luxury, use DateTimeImmutable instead of DateTime.
clone the original object:
public function __persist_starts() {
$utc = clone $this->starts;
return $utc->setTimezone(new \DateTimeZone("UTC"))
->format('Y-m-d H:i:s');
}
create a new object:
public function __persist_starts() {
return \DateTime::createFromFormat(
'U',
$this->starts->getTimestamp(),
new \DateTimeZone("UTC")
)->format('Y-m-d H:i:s');
}

What is the best way to change the user's timezone in Laravel 4?

At the moment I have saved the users timezone in their database row and each time I print a date I am converting it to the user's timezone. How can I do this in a DRY manner?
Should I override where Eloquent returns a Carbon DateTime object. If so should I put this in a trait as I have below so I only have to write it once?
<?php
use Carbon\Carbon;
use Illuminate\Database\Eloquent;
trait ConvertTimeZone {
/**
* Return a timestamp as DateTime object.
*
* #param mixed $value
* #return DateTime
*/
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.
if (is_numeric($value))
{
return Carbon::createFromTimestamp($value);
}
// If the value is in simply year, month, day format, we will instantiate the
// Carbon instances from that fomrat. 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);
}
// 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();
$timezone = \Auth::user()->timezone;
return Carbon::createFromFormat($format, $value, $timezone);
}
return Carbon::instance($value);
}
}
I would create a BaseModel class, extending Eloquent, from which I'd extend the models I need such functionality from. Just have to remember to check if the user is logged in, so that we can get its timezone. Example:
models/BaseModel.php
class BaseModel extends Illuminate\Database\Eloquent\Model {
protected function asDateTime($value) {
// If Carbon receives null, it knows to use the default timezone
$tz = null;
// If the user is logged in, get it's timezone
if (Auth::check()) {
$tz = Auth::user()->timezone;
}
// 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.
if (is_numeric($value)) {
return Carbon::createFromTimestamp($value, $tz);
}
// If the value is in simply year, month, day format, we will instantiate the
// Carbon instances from that fomrat. 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, $tz);
}
// 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, $tz);
}
return Carbon::instance($value);
}
}
models/User.php
class User extends BaseModel {
// ...
}

Categories