How to handle Laravel application across multiple timezones - laravel

I've been reading up about the best approach to handling localised times, when a Laravel application is used across multiple timezones.
My understanding is that the app timezone should remain set as the default, which is UTC.
This means that all datetime / timestamps are recorded in the database (MySQL in my case) as their UTC value - in other words, consistently.
For Eloquent models to have the correct (localised) date / time values, the user's timezone must be obeyed. It is at this point that I am less clear on how to proceed - specifically, in terms of:
How the user's timezone is best obtained
How this timezone can be used in a transparent way with Eloquent, so that
All model dates are output in local time
All dates are recorded in the database correctly (as UTC)
Edit
I should mention that my app supports both anonymous and authenticated users, so I don't want to force the user to explicitly select their timezone.

I ended up implementing this with my own model trait, primarily because I needed to implement this in a transparent way.
Firstly, I created my own getAttribute() method, to retrieve the stored values (stored as the app's default timezone - likely UTC) and then apply the current timezone.
The trait also alters the model's create() and update() methods, to support fields in a model's dates property being stored as the app's timezone, when they've been set by the user in the current active timezone.
The self::getLocale() static method in the trait in my case is provided by another trait in my app, although this logic can be adjusted to suit your own app.
trait LocalTime
{
/**
* Override create() to save user supplied dates as app timezone
*
* #param array $attributes
* #param bool|mixed $allow_empty_translations
*/
public static function create(array $attributes = [], $allow_empty_translations=false)
{
// get empty model so we can access properties (like table name and fillable fields) that really should be static!
// https://github.com/laravel/framework/issues/1436
$emptyModel = new static;
// ensure dates are stored with the app's timezone
foreach ($attributes as $attribute_name => $attribute_value) {
// do we have date value, that isn't Carbon instance? (assumption with Carbon is timezone value will be correct)
if (!empty($attribute_value) && !$attribute_value instanceof Carbon && in_array($attribute_name, $emptyModel->dates)) {
// update attribute to Carbon instance, created with current timezone and converted to app timezone
$attributes[$attribute_name] = Carbon::parse($attribute_value, self::getLocale()->timezone)->setTimezone(config('app.timezone'));
}
}
// https://github.com/laravel/framework/issues/17876#issuecomment-279026028
$model = static::query()->create($attributes);
return $model;
}
/**
* Override update(), to save user supplied dates as app timezone
*
* #param array $attributes
* #param array $options
*/
public function update(array $attributes = [], array $options = [])
{
// ensure dates are stored with the app's timezone
foreach ($attributes as $attribute_name => $attribute_value) {
// do we have date value, that isn't Carbon instance? (assumption with Carbon is timezone value will be correct)
if (!empty($attribute_value) && !$attribute_value instanceof Carbon && in_array($attribute_name, $this->dates)) {
// update attribute to Carbon instance, created with current timezone and converted to app timezone
$attributes[$attribute_name] = Carbon::parse($attribute_value, self::getLocale()->timezone)->setTimezone(config('app.timezone'));
}
}
// update model
return parent::update($attributes, $options);
}
/**
* Override getAttribute() to get times in local time
*
* #param mixed $key
*/
public function getAttribute($key)
{
$attribute = parent::getAttribute($key);
// we apply current timezone to any timestamp / datetime columns (these are Carbon objects)
if ($attribute instanceof Carbon) {
$attribute->tz(self::getLocale()->timezone);
}
return $attribute;
}
}
I'd be interested in feedback of the above approach.

I like to add helper function just for that. I keep the app in UTC so all dates are stored the same way ... and then I pass them through the helper either in the controller or in my blade template.
public static function date($date, $format = 'n/j/Y g:i a T'){
$timezone = empty(Auth::user()->timezone) ? 'America/New_York' : Auth::user()->timezone;
if( empty($date) ){
return '--';
}
return Carbon::parse($date)->timezone($timezone)->format($format);
}
This uses a timezone field that was added to the user model -- but will default to Eastern Time if they are a guest. (Eastern just because that's were the business and majority of clients are located.)

Related

Right way to save timestamps to database in laravel?

As part of a standard laravel application with a vuejs and axios front-end, when I try to save an ISO8601 value to the action_at field, I get an exception.
class Thing extends Model {
protected $table = 'things';
// timestamp columns in postgres
protected $dates = ['action_at', 'created_at', 'updated_at'];
protected $fillable = ['action_at'];
}
class ThingController extends Controller {
public function store(Request $request) {
$data = $request->validate([
'action_at' => 'nullable',
]);
// throws \Carbon\Exceptions\InvalidFormatException(code: 0): Unexpected data found.
$thing = Thing::create($data);
}
}
My primary requirement is that the database saves exactly what time the client thinks it saved. If another process decides to act on the "action_at" column, it should not be a few hours off because of timezones.
I can change the laravel code or I can pick a different time format to send to Laravel. What's the correct laravel way to solve this?
The default created_at and updated_at should work fine.
You should always set your timezone in your config/app.php to UTC
Add a timezone column or whichever you prefer in your users table
Do the time-offsets in your frontend or api response
Here's a sample code to do the time offset in backend
$foo = new Foo;
$foo->created_at->setTimezone('America/Los_Angeles');
or frontend using momentjs
moment(1650037709).utcOffset(60).format('YYYY-MM-DD HH:mm')
or using moment-timezone
moment(1650037709).tz('America/Los_Angeles').format('YYYY-MM-DD HH:mm')
class Thing extends Model {
protected $table = 'things';
// timestamp columns in postgres
protected $dates = ['action_at', 'created_at', 'updated_at'];
protected $fillable = ['action_at'];
}
class ThingController extends Controller {
public function store(Request $request) {
$data = $request->validate([
'action_at' => 'nullable',
]);
// convert ISO8601 value, if not null
if ($data['action_at'] ?? null && is_string($data['action_at'])) {
// note that if the user passes something not in IS08601
// it is possible that Carbon will accept it
// but it might not be what the user expected.
$action_at = \Carbon\Carbon::parse($data['action_at'])
// default value from config files: 'UTC'
->setTimezone(config('app.timezone'));
// Postgres timestamp column format
$data['action_at'] = $action_at->format('Y-m-d H:i:s');
}
$thing = Thing::create($data);
}
}
Other tips: don't use the postgres timestamptz column if you want to use it with protected $dates, laravel doesn't know how to give it to carbon the way postgres returns the extra timezone data.
See the carbon docs for other things you can do with the $action_at instance of \Carbon\Carbon, such as making sure the date is not too far in the future or too far in the past. https://carbon.nesbot.com/docs/

How to input the date from YYYY-MM-DD to DD-MM-YYYY in laravel

Some people know how to change the YYYY-MM-DD format to become DD-MM-YYYY when inputting to the database and how to display it from the database.
StudentsController.php
public function store(Request $request)
{
//
$students = new Student();
$students->nis = $request->nis;
$students->nama = $request->nama;
$students->jk=$request->jk;
$students->nama_sekolah = $request->nama_sekolah;
$students->alamat_sekolah = $request->alamat_sekolah;
$students->tanggal_mulai = $request->tanggal_mulai;
$students->tanggal_selesai=$request->tanggal_selesai;
$students->email=$request->email;
$students->alamat_siswa=$request->alamat_siswa;
$students->no_hp=$request->no_hp;
$students->email_siswa=$request->email_siswa;
$students->nama_guru=$request->nama_guru;
$students->save();
return back()->with('success','Data Berhasil Ditambahkan');
}
PHP can parse most date formats automatically, so to parse either format you can just create a Datetime or Carbon with the date string as the first argument.
Laravel can then automatically transform Datetime and Carbon instances to the correct format for your database, so you can do something like:
$dateX = new Carbon($request->get('date_attr')
$students = new Student();
// other attributes
$students->date = $dateX;
$students->save();
return back()->with('success','Data Berhasil Ditambahkan');
MySQL expects date to be formatted like YYYY-MM-DD. So while saving data to the database keep it as it is. When you are displaying the date, you can format it as you wish. To do so make your date as a date in your model.
class Student extends Model
{
protected $guarded = [];
/**
* The attributes that should be mutated to dates.
*
* #var array
*/
protected $dates = ['date'];
}
Now you can format your date attribute anywhere like
$student->date->format("d-m-Y")
It will give you the date in DD-MM-YYYY format.
In Laravel 5, you can do it using carbon class.
\Carbon\Carbon::parse('2019-04-24')->format('d-m-Y');
Use the strtotime date format:
date("d-m-Y", strtotime($date));

How does laravel/lumen dateformat 'U' actually works?

I'm not able get the timestamps with dateformat 'U' working in lumen.
In migration:
$table->timestamps();
In Model:
protected $dateFormat = 'U';
protected $dates = [
'created_at',
'updated_at',
'deleted_at'
];
public function getDateFormat()
{
return 'U';
}
Insert row from controller:
$model = new ApiKey;
$model->random= rand();
$model->name = $name;
$model->scope = $scope;
$model->save();
It does insert the row in the database but with 0000-00-00 00:00:00 values for created_at and updated_at columns.
Also, while retrieving a model via toArray or toJson it thrown exception:
I want lumen to autoupdate the timestamps and retrive timestamps as unixtimestamp format i.e. number of seconds from 1st Jan 1970.
Also, $table->timestamps() didn't create deleted_at column. What do I need to do get this column created via laravel.
Is there any other option than $table->timestamp('deleted_at');?
I've found a solution bay changing timestamps columns to int. But I want the things to be done in laravel way.
Unix timestamps are integers, not compatible with SQL datetime/timestamp fields. If you want to use unix timestamps, use integer field types for storage.
The timestamps() method only creates created_at and updated_at, soft deletes are not enabled by default. This method should not be used if you want integer storage.
If you only want to change the output format when serializing data, use casting:
/**
* The attributes that should be cast to native types.
*
* #var array
*/
protected $casts = [
'created_at' => 'datetime:U',
];
This is likely caused by the fact that Laravel/Lumen created the date/time columns as the type timestamp not int so you're trying to save wrong type of data in the field, causing the 0000-00-00 00:00:00.
This would also cause the carbon issue as you are trying to createFromFormat with the wrong format compared to the content.
You can use $table->integer('deleted_at'); in your migration to create a deleted_at column.
TL;DR:
Manually create your date time columns with $table->integer('updated_at').
<?php
namespace App\Traits;
use Illuminate\Support\Carbon;
trait sqlServerDateFormat
{
public function fromDateTime($value)
{
return Carbon::parse(parent::fromDateTime($value))->format('d-m-Y H:i:s');
}
}

Laravel 5.4 save() in a loop

I am trying to 'conquer' Laravel, but some concepts still elude me.
Lets take this code:
public function updateMenuGroup($groupId, $label, $icon, $sort, $userId)
{
/*get model to be updated by id*/
$updateGroups = MenuGroups::findMany($groupId);
/**
* set attributes to be updated
* I loop, since I get collection of models fetched by findMany()
*/
foreach($updateGroups as $update)
{
/**
* label (group name) is passed as an array of group names
* for every language, so I am fetching them by using
* language passed by model fetched by findMany (find gets only first mode)
*/
$update->label = $label[$update->lang]; //array
$update->icon = $icon;
$update->sort = $sort;
$update->system_employee_id = $userId;
$update->save();
}
}
$update->label carries an array, with different values for different language - say I have 'en', 'de' languages.
When I run code above, only 'en' is being written - overwriting values of 'de'.
Can someone, please, give me some pointers, as to why above happens?
You need to instantiate update model class inside loop.
foreach($updateGroups as $update)
{
/**
* label (group name) is passed as an array of group names
* for every language, so I am fetching them by using
* language passed by model fetched by findMany (find gets only first mode)
*/
$update=New ModelClass();
$update->label = $label[$update->lang]; //array
$update->icon = $icon;
$update->sort = $sort;
$update->system_employee_id = $userId;
$update->save();
}
You can't insert array to one attribute.
Try to use implode function.
like this.
$update->label = implode(",", $label[$update->lang]);

Automatically selecting dates from databases as Zend_Date objects

From what I understand, the best way to deal with dates in the Zend Framework is to select them as a Unix timestamp from the database.
Quick Creation of Dates from Database Date Values
// SELECT UNIX_TIMESTAMP(my_datetime_column) FROM my_table
$date = new Zend_Date($unixtimestamp, Zend_Date::TIMESTAMP);
I think it's a pain that there is actually no easy way in Oracle to either select dates as Unix timestamps or in ISO-8601 format - which are the two formats Zend_Date knows best.
But I did write a function to select dates as unix timestamps in PL/SQL, so I can actually do this now.
Using Zend_Db_Expr, I can now select my dates as Unix timestamps:
$select = $db->select()
->from(array('p' => 'products'),
array(
'product_id',
'product_date' => new Zend_Db_Expr('toUnixTimestamp(product_date)')
)
);
$results = $db->fetchAll($select);
You would use a similar query for any RDMS - most have a timestamp function.
I find this anoying because now I have to loop through $results to transform the timestamp to a Zend_Date object manually:
foreach($results as $result){
$productDate = new Zend_Date($result['product_date'], Zend_Date::TIMESTAMP);
echo $productDate->toString('dd/MMM/yyyy HH:mm:ss');
}
I want my Model to return $results where the timestamps are already transformed to Zend_Date. I don't want to have to write a loop in every data-access function to do this for me.
So to get to the point of my actual question:
Does anyone know of a way with Zend_Db, to set up some sort of post-processing on the result set, thus converting the timestamps to Zend_Date objects automatically?
I've encountered scenarios where I've wanted to do this. Here is the solution that I've used:
Created an extended row class for Zend_Db_Table_Row and overloaded the __get() and __set() super-methods
In the specific classes/tables that I want to use date objects, created the appropriate methods to do the heavy lifting
Here is a dumbed-down version of the extended row class that I use on my projects:
/**
* #category FireUp
* #package FireUp_Db
* #copyright Copyright (c) 2007-2009 Fire Up Media, Inc. (http://www.fireup.net)
* #license http://dev.fireup.net/license/mit MIT License
* #uses Zend_Db_Table_Row
*/
class FireUp_Db_Table_Row extends Zend_Db_Table_Row_Abstract
{
/**
* Retrieve row field value
*
* Checks for the existence of a special method to apply additional handling for the field data and calls the method if it exists
*
* #param string $columnName The user-specified column name.
* #return string The corresponding column value.
* #throws Zend_Db_Table_Row_Exception if the $columnName is not a column in the row.
*/
public function __get($key)
{
$inflector = new Zend_Filter_Word_UnderscoreToCamelCase();
$method = '_get' . $inflector->filter($key);
if (method_exists($this, $method))
{
return $this->{$method}();
}
return parent::__get($key);
}
/**
* Set row field value
*
* Checks for the existence of a special method to apply additional handling for the field data and calls the method if it exists
*
* #param string $columnName The column key.
* #param mixed $value The value for the property.
* #return void
* #throws Zend_Db_Table_Row_Exception
*/
public function __set($key, $value)
{
$inflector = new Zend_Filter_Word_UnderscoreToCamelCase();
$method = '_set' . $inflector->filter($key);
if (method_exists($this, $method))
{
return $this->{$method}($value);
}
return parent::__set($key, $value);
}
}
For our individual table classes, we override the functions as such:
class EntityRecord extends FireUp_Db_Table_Row
{
protected function _getDateCreated()
{
return new Zend_Date($this->_data['date_created'], Zend_Date::ISO_8601);
}
protected function _setDateCreated($value)
{
if ($value instanceof Zend_Date)
{
$value = $value->toString('YYYY-MM-dd HH:mm:ss');
}
$this->_data['date_created'] = $value;
$this->_modifiedFields['date_created'] = true;
}
}
Now, creating a new Zend_Date object everytime that the field would be accessed has some overhead, so in our classes, we take additional measures to cache the date objects, etc, but I didn't want that to get in the way of showing you the solution.
Use Zend_Table and have your table return you custom row objects that extend Zend_Db_Table_Row_Abstract. Then just have a method on that row like
function getDate() {
return new Zend_Date($this->datecol, Zend_Date::TIMESTAMP);
}

Resources