I'm having problems getting Laravel to cast a custom attribute as a Carbon date. Here's an example model:
class Organisation extends Model
{
protected $dates = [
'my_date'
];
public function getMyDateAttribute()
{
return "2018-01-01 00:00:00";
}
}
I'd expect my_date to be cast to a Carbon date however if I do dd($organisation->my_date) it just returns the date as a string.
I've seen people suggest to just return a Carbon instance from the custom attribute, and this partially works, the my_date attribute is availabe as a Carbon instance within the application, however if you then return the model as Json you end up with:
{
"name": "Big Business",
"my_date": {
"date": "2018-01-01 00:00:00.000000",
"timezone_type": 3,
"timezone": "Europe/London"
}
}
Instead of the desired:
{
"name": "Big Business",
"my_date": "2018-01-01 00:00:00"
}
Has anyone else come across this and if so have you found a solution?
Update
Upon further investigation I've tracked down the problem (but I don't have a solution yet). When you return an Eloquent model the __toString magic method which runs the toJson method and as you trace down the chain it serializes any Carbon dates in the $dates variable. It also completely skips over this serialization for mutated attributes, which is what I'm seeing.
I need to find a way to seralize mutated attributes that return a Carbon date when __toString is called.
edit your Organization model to this:
use Carbon\Carbon;
class Organisation extends Model
{
public function getMyDateAttribute($value)
{
//you can manipulate the date here:
$date = Carbon::parse($value,config('timezone'));
return $date;
}
}
You if your model represents a table, you can change the data type to timestamp and laravel will put that attribute into a carbon object.
Once the attribute is a carbon object, you can change the format in the blade view.
{{ $organisation->mydate->format('Y-m-d') }}
If either cannot change the data type, or you need to a default format different from a timestamp you can use the 'cast' eloquent model property.
class Organisation extends Model{
protected $cast = [
'mydate'=>'datetime:Y-m-d H:i:s'
];
}
Casting the attribute effectively works the same as an accessor that wraps the date value with a carbon object. This is much cleaner way to write it, however.
As far as timezone is concerned, you should look to change that in the config/app.php file.
Here is the documentation.... https://laravel.com/docs/5.8/eloquent-mutators#attribute-casting
Related
I am trying to seed some data using Factories. One of the fields I'm trying to seed is a date field -- and I do so with the following code in my factory:
return [
...
'date' => Carbon::now()->subDays(rand(1, 365))->startOfDay()
];
The problem is, this is getting saved in the database as a string -- which means that I CANNOT do the following in my blade templates: {{ $transaction->date->format('M, d Y') }},
When I try that, I get the following error message: Call to a member function format() on string.
Just as a test, I tried in my blade template the same exact code, just switching out created_at for date - and it works as I want. I.e., this works: {{ $transaction->created_at->format('M, d Y') }}.
In case it matters, the created_at field is created using $table->timestamps() in my migration file whereas the date field is created as follows: $table->date('date');.
Any idea what I need to do in order to get this to work?
Thanks.
Laravel provides a method to "cast" certain Model attributes to specific datatypes, including strings, integers, dates, etc. Since Carbon is built in to Laravel, specifying the date type auto-converts Model attributes to a Carbon instance. All you need to do is provide that logic to your model:
class Transaction extends Model {
protected $casts = [
'date' => 'date'
];
...
}
Now, when you retrieve a Transaction model record, the date attribute will automatically be a Carbon instance:
$transaction = Transaction::first();
dd($transaction->date, get_class($transaction->date));
// ^ Carbon\Carbon #1646769789^ {#4514 ... }, `Carbon\Carbon`
Now, you can perform Carbon logic, simply by chaining:
{{ $transaction->date->format('Y-m-d') }}
// `2022-03-08`
More casting types are available, and you can specify multiple attribute casts by simply adding them as key/value pairs to the $casts array. Full documentation is here:
https://laravel.com/docs/9.x/eloquent-mutators#attribute-casting
By default, Eloquent will convert the created_at and updated_at columns to instances of Carbon. When retrieving attributes that are listed in the $dates property, they will automatically be cast to Carbon instances, allowing you to use any of Carbon's methods on your attributes.
I have the following in dates property - i've not included the created_at and updated_at columns as these are converted by default as per above:
protected $dates = ['deleted_at'];
Then I have the following accessor on the model:
public function getCreatedAtAttribute($datetime)
{
return $datetime->timezone('Europe/London');
}
However the above throws the following error:
Call to a member function timezone() on string
If I change the method to the following it works:
public function getCreatedAtAttribute($datetime)
{
return Carbon::parse($datetime)->timezone('Europe/London');
}
The question is why do I need to parse it since it's suppose cast it to a carbon instance when it's retrieved according to docs https://laravel.com/docs/6.x/eloquent-mutators#date-mutators ?
That completely depends on what $datetime is and how you're passing it this this function. It's clearly a string, and not a Carbon instance, but you didn't include the definition for $datetime in you question, so I can only speculate.
That being said, I haven't see mutators that use an external variable, as they are generally designed to access properties of the class you're applying them to, via $this:
public function getCreatedAtAttribute(){
return $this->created_at->timezone('Europe/London');
}
The only caveat I could see with this is naming conflict when trying to use $model->created_at. It should handle it, but something like getCreatedAtTzAttribute(), accesses via $model->created_at_tz might be necessary if you come across issues.
If you check the source code (here), you'll see that accessors have priority over date casts.
If Eloquent finds an accessor for your date attribute (getCreatedAtAttribute), date casting will be ignored. So you'll need to cast it manually within your accessor.
when i print the updated_at field into the views, it returns something like this:
{ "date": "2017-12-08 15:07:26.000000", "timezone_type": 3, "timezone": "Australia/Melbourne" }
why this field behave like this?
i check the model and i explicitly cast the field to carbon (expiry_date was there before and it's a correct format):
protected $dates = [
'expiry_date',
'updated_at',
];
but the issue still persists.
do you have idea on how to solve this problem?
try setting the date format on your model.
protected $dateFormat = 'Y-m-d H:i:s';
If you echo, or do something else to either implicitly or explicitly call the __toString() method, you'll get the plain date.
The format that you're showing, however, is what you see when you json_encode() a Carbon object.
Make sure that whatever you're doing to display your data is not using json_encode() on the date.
I have an Organisation model that has a two data attributes, allowance_starts_at and allowance_ends_at, when returning this model as JSON I get the following back:
allowance_starts_at: "2017-06-01 00:00:00",
allowance_ends_at: "2018-05-31 23:59:59",
However I want to override these attributes to set the year to be the current year (or allowance period) so come 2018-06-01 I want the attributes to return:
allowance_starts_at: "2018-06-01 00:00:00",
allowance_ends_at: "2019-05-31 23:59:59",
To achieve this I'm overriding the attributes in the model like so:
public function getAllowanceStartsAtAttribute($value)
{
$currentYear = Carbon::now()->format('Y');
$d = Carbon::createFromFormat('Y-m-d H:i:s', $value);
$d->year($currentYear);
return $d;
}
public function getAllowanceEndsAtAttribute($value)
{
$d = Carbon::createFromFormat('Y-m-d H:i:s', $value);
if ($d->copy()->subYear()->addSecond() != $this->allowance_starts_at) {
$d->addYear();
}
return $d;
}
This works and gives me the result I want however, now when I output the model as JSON the Carbon date isn't serialised:
allowance_starts_at: {
date: "2017-01-01 00:00:00.000000",
timezone_type: 3,
timezone: "Europe/London"
},
allowance_ends_at: {
date: "2017-12-31 23:59:59.000000",
timezone_type: 3,
timezone: "Europe/London"
},
I've also tested this with appended attributes, so instead of overriding an existing attribute I create an appended attribute that just returned Carbon::now() and set the appended attribute in the $dates array and this outputs in the same way i.e. not serialized.
Does anyone now how Laravel determines how to output the attributes e.g. as Carbon or as a string?
Does anyone know how I can make this work?
Update
Thanks Paul. Using a setter mutator gave me the behaviour that I needed. I replaced my overriding methods with:
public function setAllowanceStartsAtAttribute($value)
{
$currentYear = Carbon::now()->format('Y');
$value->year($currentYear);
$this->attributes['allowance_starts_at'] = $value;
}
public function setAllowanceEndsAtAttribute($value)
{
if ($value->copy()->subYear()->addSecond() != $this->attributes['allowance_starts_at']) {
$value->addYear();
}
$this->attributes['allowance_ends_at'] = $value;
}
And now I get this output when returning JSON:
allowance_starts_at: "2018-06-01 00:00:00",
allowance_ends_at: "2019-05-31 23:59:59",
Have you tried using a mutator? Like instead of getAllowanceEndsAtAttribute, make it setAllowanceEndsAtAttribute?
I have a code like this:
public function getUpdatedAtAttribute($value) {
setlocale(LC_TIME, config('app.locale'));
return Carbon::parse($value)->formatLocalized(__('DateFormat'));
}
I want to run this accessor for each field specified in $dates array instead of manually specifying it for each date field in each model, just like default Carbon instance convertion works. How could I do this? And is there better ways of specifying default locale-dependant date format for Carbon?
I think you can use $dateFormat variable of a model to apply a common date format on all the model fields:
class Flight extends Model
{
/**
* The storage format of the model's date columns.
*
* #var string
*/
protected $dateFormat = 'U';
}
More info: https://laravel.com/docs/5.4/eloquent-mutators#date-mutators
Found an elegant and simple solution: LocalizedCarbon package. It works as simply as this:
use \Laravelrus\LocalizedCarbon\Traits\LocalizedEloquentTrait;
UPD: It seems that this package actually translating only DateDiff's, but anyway I can see how it works and use that logic in my models.
UPD2: I've dig deeper and found out what there is overloaded formatLocalized method, which allows useage of non-standard "%f" parameter, which represents month name in current application locale. So I ended up with one-liner date formatting into my View instead of Model, which is more correct.