Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

lewis4u's avatar

Is it possible to make a mutator (setter) only for date attributes in Laravel?

I was thinking something like this:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;


class WorkHour extends Model
{

    protected $fillable = [
        'user_id',
        'start_time',
        'end_time'
    ];

    protected $dates = ['start_time', 'end_time'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    // something like this:
    public function setDatesAttribute($dates)
    {
        foreach ($dates as $date){
            $this->attributes[$date] = Carbon::parse($date);
        }
    }
}

This of course doesn't work, but the main point here is that I have a lot of models with dates in it and with localized date in form request that looks like this "23.09.2017 22:12" I get an error on form submit (Invalid datetime format) so I need to have a setter to convert the date to be able to save it. So I created a setter for each date attribute in every model and that was a temporary fix and it works all fine. But I don't like it.

So is there a way to make one universal setter for $dates array (a setter to be applied on every attribute inside an array)?

This could then be extracted to a Trait or a Class to be able to apply it to mutate every date inside dates array for any model!

0 likes
27 replies
shez1983's avatar

you can do something similar on UPDATING event that laravel fires when creating or updating a model..

lewis4u's avatar

Can you please post where is that exactly? which laravel file?

Snapey's avatar

In your model, you can add dates to the $dates[] array and then set $dateFormat to be your preferred format.

This will then be used as the Carbon parse() format whenever you pass a date to your model

lewis4u's avatar

I need to parse it on store() method in Controller.... Everything works fine... I just want to make it better so I can use it in every Laravel App...

I have made your suggestion like this:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Article extends Model
{

    protected $fillable = [
        'user_id',
        'title',
        'body',
        'published_at'
    ];

    protected $dateFormat = "d.m.Y H:i";

    protected $dates = ['published_at'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

and I get this error:

SQLSTATE[22007]: Invalid datetime format: 1292 Incorrect datetime value: '22.09.2017 22:50' for column 'published_at' at row 1
lewis4u's avatar

@Snapey if we could somehow live chat on facebook or something i could explain it better so we could make some solution from which we all could benefit from.

Snapey's avatar

A possible solution; (what I said before does not work)

The eloquent model calls in a Trait called HasAttributes.php, in which all dates are formatted.

Being a trait, we can overload any of the methods in our own model, or in a model that our own models extend.

protected $localDateFormat='d.m.Y';

declares the localised format that we want to use

/**
 * Determine if the given value is a localised date format.
 *
 * @param  string  $value
 * @return bool
 */
protected function isLocalDateFormat($value)
{
    return preg_match('/^(\d{1,2}).(\d{1,2}).(\d{4})$/', $value);
}

tests using regex if the date provided matches the local date format. This looks complicated on first glance, but is 1 or 2 digits, period, 1 or 2 digits, period, 4 digits.


/**
 * Return a timestamp as DateTime object.
 *
 * @param  mixed  $value
 * @return \Carbon\Carbon
 */
protected function asDateTime($value)
{
    // If this value is already a Carbon instance, we shall just return it as is.
    // This prevents us having to re-instantiate a Carbon instance when we know
    // it already is one, which wouldn't be fulfilled by the DateTime check.
    if ($value instanceof Carbon) {
        return $value;
    }
    // If the value is already a DateTime instance, we will just skip the rest of
    // these checks since they will be a waste of time, and hinder performance
    // when checking the field. We will just return the DateTime right away.
    if ($value instanceof DateTimeInterface) {
        return new Carbon(
            $value->format('Y-m-d H:i:s.u'), $value->getTimezone()
        );
    }
    // 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 format. Again, this provides for simple date
    // fields on the database, while still supporting Carbonized conversion.
    if ($this->isStandardDateFormat($value)) {
        return Carbon::createFromFormat('Y-m-d', $value)->startOfDay();
    }

    //try to parse as localised format
    if ($this->isLocalDateFormat($value)) {
        return Carbon::createFromFormat($this->localDateFormat,$value)->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.
    return Carbon::createFromFormat(
        $this->getDateFormat(), $value
    );
}

So the above is our new function that is replacing the one in the Eloquent trait. After trying other formats, it checks if the date is in the localised format and if so, parses the date according to the local format, converting it to Carbon instance.

The above, in a model, seems to handle all conditions as far as I can tell using Tinker.

lewis4u's avatar

I came up with a much more elegant solution I think. Check this out:


<?php

namespace App\Traits;

use Carbon\Carbon;

trait FormatDates
{

    public function setAttribute($key, $value)
    {
        parent::setAttribute($key, $value);

        if (strtotime($value))
            $this->attributes[$key] = Carbon::parse($value);
    }
}

What do you think?

Now I need a getter like that.

lewis4u's avatar

This is the best I could think of:

    public function getAttributeValue($key)
    {
        $value = parent::getAttributeValue($key);

        return strtotime($value) ? 
             Carbon::parse($value)->format($this->localFormat) : $value;
    }

Snapey's avatar

isn't everything treated as a date?

lewis4u's avatar

No it's not, that is why this getter doesn't work....I need help... I get this error because of it:

The separation symbol could not be found Unexpected data found

Snapey's avatar

I spent a fair bit of time working out a solution... you could try it?

lewis4u's avatar

Can you explain what did you do actually?

When exactly are those functions called?

Snapey's avatar

Can you explain what did you do actually?

Extended the model;

Added protected $localDateFormat='d.m.Y';

Created a totally new function

protected function isLocalDateFormat($value)
{
    return preg_match('/^(\d{1,2}).(\d{1,2}).(\d{4})$/', $value);
}

Then borrowed the asDateTime() function from the HasAttributes trait and added these lines;

    //try to parse as localised format
    if ($this->isLocalDateFormat($value)) {
        return Carbon::createFromFormat($this->localDateFormat,$value)->startOfDay();
    }

Now, any date passed to the model (as long as it is in the $dates array) will be converted into a Carbon instance using the following rules, and in this order

  • return if it is already Carbon
  • Convert to Carbon if it is a php date/time object
  • Convert to Carbon if it is a unix timestamp
  • Convert to Carbon if it is in the format Y-m-d
  • Convert to Carbon if it is in our new local date format
  • Convert to Carbon if it is in the native format of the database server
lewis4u's avatar

OK I had to make some changes but your solution works! Thank you @Snapey

And this is my final solution:

-to use it you need to delete the $dates and $dateFormat variables from model and just use this Trait:


<?php

namespace App\Traits;

use Carbon\Carbon;

trait FormatDates
{
    /**
     * Get Model dates as Carbon Object
     *
     * @param $key
     * @return static
     */
    public function __get($key)
    {
        if (strtotime($this->getAttributeValue($key))) {
            $date = Carbon::parse($this->getAttributeValue($key));
            return $date;
        } else {
            return $this->getAttributeValue($key);
        }
    }

    /**
     * Convert Model dates from Datepicker to Carbon date
     * so it can be saved to DB
     *
     * @param $key
     * @param $value
     */
    public function setAttribute($key, $value)
    {
        parent::setAttribute($key, $value);

        if (strtotime($value))
            $this->attributes[$key] = Carbon::parse($value);
    }

}

Please leave a comment what you think about it.

Snapey's avatar

I don't get it? Its nothing like what I was doing.

Where do you define the date format you want to use?

lewis4u's avatar

On store method the problem is that my date format doesn't have the right format and if i call Carbon::parse($date); on it then the format is good for Database.... and it gets saved without a problem.

And with getter I convert it back to Carbon again.

And BTW I get an error on validation with your Trait;

If i make a validation rule

'published_at' => 'date'

And if I try to submit the form with 'published_at' field blank the validation rule pops out saying that The published at is not a valid date.

And with mine also :( oh man now what....

Cronix's avatar

use date_format rule instead of date?

lewis4u's avatar

Ah it's ok now....I needed to set it like this

'published_at' => 'nullable|date'

Now if the field is empty then it wont trigger the validation

Snapey's avatar

i still don't understand how carbon->parse() automatically recognises your dd.mm.yyyy format

lewis4u's avatar

It's because of this function in HasAttributes.php Trait:


/**
 * Return a timestamp as DateTime object.
 *
 * @param  mixed  $value
 * @return \Carbon\Carbon
 */
protected function asDateTime($value)
{
    // If this value is already a Carbon instance, we shall just return it as is.
    // This prevents us having to re-instantiate a Carbon instance when we know
    // it already is one, which wouldn't be fulfilled by the DateTime check.
    if ($value instanceof Carbon) {
        return $value;
    }

    ...

lewis4u's avatar

Now i run into another problem...

Because of my getter I can't get related objects. For example an Article has Tags and if I call $article->tags i get null because of my getter.

How can I fix this? So that my getter changes only the date values and if it's not a date then continue as usual.

lewis4u's avatar

How do you mean "moved on". You don't want to discuss it anymore?

Snapey's avatar

I came up with a much more elegant solution I think

this getter doesn't work....I need help... I get this error because of it: The separation symbol could not be found Unexpected data found

OK I had to make some changes but your solution works!

(then does not use working solution)

And BTW I get an error on validation with your Trait;

(no you don't, validation does not touch the model)

Now i run into another problem... Because of my getter I can't get related objects.

because you f'd over the model

lewis4u's avatar
lewis4u
OP
Best Answer
Level 8

I got the validation error only because the field was empty so I added nullable to validation rule.

This is my final solution:

<?php

namespace App\Traits;

trait FormatDates
{

    protected $databaseFormat = 'Y-m-d H:i:s';

    /**
     * Convert Model dates from Datepicker to date instance
     * so it can be saved to DB
     *
     * @param $key
     * @param $value
     */
    public function setAttribute($key, $value)
    {
        if (!is_null($value) && strtotime($value)) {
            $date = date($this->databaseFormat, strtotime($value));
            parent::setAttribute($key, $date);
        } else {
            parent::setAttribute($key, $value);
        }
    }
}

and in Model to get the Carbon instances in views i just created $casts variable

protected $casts = [
    'published_at' => 'datetime'
];
1 like

Please or to participate in this conversation.