PHP DateTime get format in UTC without converting object - php

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

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

Laravel 5.3 Carbon date format is not set on return?

I have my VitalSignSet model:
class VitalSignSet extends Model
{
protected $dates = [
'datetimetaken',
];
. . .
}
Now in my function I have this function which returns the json encoded encounter with the latest vital sign set. (Encounter has a hasMany relationship with VitalSignSet) Before the return though, I would like the datetimetaken field to be formatted for human readability, but just for this particular method. (which is why I did not use accessors)
public function get(Request $request, Encounter $encounter) {
// Setting the latest vital sign set
$encounter->latest_vitals = $encounter->VitalSignSets()
->orderBy('datetimetaken','desc')->get()->first();
// Formatting the date :
// Works when just returning the date.
// Does not return in this format when returning the model with the date.
$encounter->lastest_vitals->datetimetaken->format('M j, Y');
return $encounter->toJson();
}
The above method is accessed from a js ajax request. When I parse and log the response, the datetimetaken format hasn't changed. (still in YYYY-mm-dd H:i:s format) But when I return just $encounter->latest_vitals->datetimetaken; after formatting, a string is returned with the format I set. But when I return the containing VitalSignSet model $encounter->latest_vitals; (json response), the format is in YYYY-mm-dd. Why is that?
This is because you're only accessing the data object, you're not actually changing it.
Unfortunately, there is no way (that I've been able to find) to edit the format of the Carbon instance in the model. This is because Laravel uses the same format to parse the datetime from the database as it does to format it to a string.
Also, you won't be able to just assign the formatted string to the original as Eloquent will try and parse that string (and fail).
One way (if you want/need to keep the key as datetime) would be to convert the output to an array, edit the value, and then return that:
$latestVitals = $encounter->VitalSignSets()
->orderBy('datetimetaken', 'desc')->first();
$encounter->latest_vitals = collect($latestVitals->toArray())
->pipe(function ($item) use ($latestVitals) {
$item['datetimetaken'] = $latestVitals->datetimetaken->format('M j, Y');
return $item;
});
return $encounter;
If you don't mind changing the key to be something else (e.g. formatted_datetimetaken) you could add an accessor to what ever model is used for you VitalSignSet:
public function getFormattedDatetimetakenAttribute()
{
return $this->datetimetaken->format('M j, Y');
}
And then just use append() i.e.
$encounter->latest_vitals = $encounter->VitalSignSets()
->orderBy('datetimetaken','desc')
->first()->append('formatted_datetimetaken');
Finally, you could simply edit the datetime in your js with something like http://momentjs.com/docs. Assuming your response it assigned to the variable response:
response.latest_vitals.datetimetaken = moment(response.latest_vitals.datetimetaken, "YYYY-MM-DD HH:mm:ss")
.format("MMM D, YYYY")
Hope this helps!

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

Return child class from parent's static method

I just learned about this fancy new feature of PHP 5.4. JsonSerializable! This is perfect for my app.
My app uses DateTime objects, and when I json_encode them, I get the following (by running json_encode([new DateTime])):
[{"date":"2013-09-11 15:39:22","timezone_type":3,"timezone":"UTC"}]
Depending on what timezone_type is, the timezone value may be different. I haven't found a good way to parse this object in JavaScript.
So, I decided to create my own DateTime class, and have it serialize to JSON how I wanted.
class SerialDateTime extends DateTime implements JsonSerializable{
public function jsonSerialize(){
return ['timestamp' => $this->getTimestamp()];
}
}
When I now run json_encode([new SerialDateTime]), I get this:
[{"timestamp":1378914190}]
That's much easier to parse in JavaScript.
So, I figured this was a fine solution, but I discovered a problem. Static methods! SerialDateTime::createFromFormat returns a DateTime object!
If I do: json_encode([SerialDateTime::createFromFormat('m/d/Y', '10/31/2011')]), I get:
[{"date":"2011-10-31 15:46:07","timezone_type":3,"timezone":"UTC"}]
Why is this happening? Why doesn't SerialDateTime::createFromFormat return me a SerialDateTime object?!
How can I fix this, or do I need to override all the static methods from DateTime in SerialDateTime? If I do that, how would I even make a new SerialDateTime from the createFromFormat method? How can I "cast" a DateTime object to SerialDateTime?
I thought of a workaround, but there's got to be a better way:
public static function createFromFormat($f, $t, $tz=NULL){
$dateTime = call_user_func(
array('SerialDateTime', 'parent::createFromFormat'),
$f, $t, $tz
);
$ret = new self();
return $ret->setTimestamp($dateTime->getTimestamp());
}
Could I maybe use __callStatic and return call_user_func_array(array(__CLASS__ , 'parent::'.__FUNCTION__), func_get_args()); or something?
Too bad I can't magically convert DateTime to use late static bindings.
Like you already said & tried, override static method. Method createFromFormat by default returns the DateTime object, so you only need to fix returning part so it will return your object SerialDateTime instead of DateTime.
class SerialDateTime extends DateTime implements JsonSerializable {
public function jsonSerialize()
{
return ['timestamp' => $this->getTimestamp()];
}
public static function createFromFormat($format, $time, $timezone = null)
{
if ($timezone) {
$dt = parent::createFromFormat($format, $time, $timezone);
} else {
$dt = parent::createFromFormat($format, $time);
}
return new self($dt->format(self::W3C));
}
}
echo json_encode(new SerialDateTime);
echo json_encode(SerialDateTime::createFromFormat('Y', '2013'));
It doesn't matter how you call static method createFromFormat, it will always return DateTime object; so all your ideas of automatically rewriting static methods will fail, because you need to modify method with new logic (return instance of other object), and this cannot be accomplished with auto-call-method-magic-or-something.
Late static bindings would be great if it were implemented in the DateTime::createFromFormat method like :
public static function createFromFormat($format, $time, $timezone = null)
{
// logic of converting $time from format $format to some default format
return new static($newTime);
}
... but it is not ;( Source code
So, I will post my answer here.
In my opinion overriding static function createFromFormat is the best way to deal with your problem.
Because:
You code will stay clean (without any unnecessary call_user_func)
It is just right that you override parent class methods and keep the class logics inside class.
Your class SerialDateTime will be futher reusable. (if you want to import only class code)
It is not necessary though to override all methods (unless you implement an interface). Override just only those you need.

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