Hi,
I'm in the middle of a nightmare, where monsters are Carbon dates, UTC, timezones and objects, so please help me to wake up :)
This project is my first dealing with timezone and I had really a very hard time. Everything is quite almost done and I just discovered yesterday while testing that my queries are good but biased because of my timezone lake of experience...
So, again:
- I store everything in UTC
- a User has a Profile where I store its timezone.
- a User can add many Records a day (not sure at all but for now, I'm also storing for each Record the user's timezone)
- for the abstraction purpose lets say for instance a User adds Note records (9/10 ; 8.5/10 ; ...)
Let's dig into my issue:
-
With my test user's timezone I added a first record on 2015-07-19 and the record in UTC was also the 2015-07-19.
-
With my test user's timezone I added (later) a second record on 2015-07-20 (it was something like 01:00:00 in the night) so the record in UTC was still in the previous day 2015-07-19 because my test user's timezone is UTC+8
-
In my app I need to display some insights and so I need to build my queries..
Until yesterday I was doing something like the following code to fetch the "last record for today"
public function lastForProfileToday(XxxProfile $profile, $columns = ['*'])
{
$today = Carbon::now()->toDateString();
return $this->model
->where('xxx_profile_id', $profile->id)
->where(DB::raw('date(created_at)'), '=', $today)
->orderBy('created_at', 'desc')
->limit(1)
->first($columns);
}
If the query seems correct I sadly discovered the logic is not at all.
Do you remember, the second record added by my test user in its timezone was 2015-07-20 but in the database it was stored in UTC as 2015-07-19.
The query will return all records for that user for 2015-07-19 which is not correct because at the time of the query the user date was 2015-07-20. I could still add more logic after the query to extract only related records corresponding to the user timezone but it sucks, right?
- So I need to deal with the user's timezone in my query! I tried...
public function lastForProfileToday(XxxProfile $profile, $columns = ['*'])
{
$today = Carbon::now($profile->timezone)->toDateString();
return $this->model
->where('xxx_profile_id', $profile->id)
->where(DB::raw('date(created_at)'), '=', $today)
->orderBy('created_at', 'desc')
->limit(1)
->first($columns);
}
At the time of the query today was 2015-07-20 based on the test user's timezone. So this query should return the last record added by the test user for today but of course it won't return anything because the records in UTC are 2015-07-19
Hum ...
Do you see how lost I am? Are you laughing? :D
Don't give up... (talking to myself :D)
First conclusion:
I also need to take care of the time and not only of the date! Right? Is it the good path?
A good busy soul told me on Slack's Larachat:
You should query against time and not just day. So if you wish to retrieve all records for the user’s 2015-07-20, and they are offset by 6h from UTC, then query on all records between 2015-07-19 18:00:00 and 2015=07=20 18:00:00.
Great, but how to...
Can someone please take the time to help me, teach me and make me say "Ahhhhhhhhhhhh"!
I think I understand the logic but I have a hard time to implement it and of course my app uses something like 20 insights like that so I will need to replicate the logic everywhere and for now, I'm totally stuck and this is a real nightmare.
Thank you in advance :)
Update:
I don't know at all if this is the solution neither if it is the best way to handle the situation but what if I do something like.
- Get the current date of the user based on its timezone with
$today = Carbon::today($timezone);
- Convert to UTC based on the offset. For instance if the user is UTC + 8 I will do
$from = $today->subHours($offset); //$offset = 8 and $to = $from->addDay();
So I could update my query like:
public function lastForProfileToday(XxxProfile $profile, $columns = ['*'])
{
$today = Carbon::today($profile->timezone);
// $offset = 0
//$from = $today
// $offset > 0
$from = $today->subHours($profile->timezoneOffset);
// or $offset < 0
// $from = $today->addHours($profile->timezoneOffset);
$to = $from->addDay();
return $this->model
->where('xxx_profile_id', $profile->id)
->whereBetween(DB::raw('date(created_at)'), [$from, $to])
->orderBy('created_at', 'desc')
->limit(1)
->first($columns);
}
Is this ok? Stupid? Is there a better way to handle that?
If this is the good way, I need to update my migration and also store the offset ...