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

achatzi's avatar

Use Macro In Models Through Trait

Hello.

I want to make a trait for all the date attributes in my models. I want to have the attributes as is (mutated my Laravel) and for each attribute I want a function with the timzeone as parameter that returns a Carbon instance.

For example

class User extends Model {
	    protected $casts = [
        	'valid_from' => 'date',
    ];

	public function validFrom($timezone = null) {
		return Carbon::parse($this->valid_from, $timezone);
	}
}

$user = new User();

echo $user->valid_from->format('Y-m-d H:i:s'); //this is the default laravel accessor
echo $user->validFrom('Europe/Athens')->format('Y-m-d H:i:s'); //this is the function I want to create

Since not all my models have the same date attributes, I decided to create a trait and use macros for this.

use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Support\Carbon;
use Illuminate\Support\Traits\Macroable;
use Str;

trait HasDateFunctions
{
    use Macroable;

    public function initializeHasDateFunctions()
    {
        $date_attributes = $this->getDates();

        foreach ($this->casts as $attribute => $type) {
            if (Str::contains($type, ['date', 'datetime', 'timestamp'])) {
                $date_attributes[] = $attribute;
            }
        }

        foreach ($date_attributes as $date_attribute) {
            $this::macro(Str::camel($date_attribute), function($timezone = null) {
                $timezone ??= config('app.timezone');

                try {
                    return Carbon::parse($this->{$date_attribute}, $timezone);
                }
                catch (InvalidFormatException $e) {
                    return $this->{$date_attribute};
                }
            });
        }
    }
}

When I use this trait, I get this exception Method App\Models\FinancialProject::hydrate does not exist which leads me to believe that maybe Models are already macroable and I overwrite the macros property of the Macroable trait.

If I remove the Macroable trait from my trait, then I get this exception Call to undefined method App\Models\FinancialProject::validFrom()

EDIT: I also tried the boot method of the trait

trait HasDateFunctions
{
    public static function bootHasDateFunctions()
    {
        $class = static::class;
        $instance = new ($class);
        $date_attributes = $instance->getDates();

        foreach ($instance->casts as $attribute => $type) {
            if (Str::contains($type, ['date', 'datetime', 'timestamp'])) {
                $date_attributes[] = $attribute;
            }
        }

        foreach ($date_attributes as $date_attribute) {
            $class::macro(Str::camel($date_attribute), function($timezone = null) {
                $timezone ??= config('app.timezone');

                try {
                    return Carbon::parse($this->{$date_attribute}, $timezone);
                }
                catch (InvalidFormatException $e) {
                    return $this->{$date_attribute};
                }
            });
        }
    }
}

How can I handle this?

0 likes
1 reply
achatzi's avatar
achatzi
OP
Best Answer
Level 5

I decided to mimic the macroable trait and create something similar

namespace App\Concerns\Models;

use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Support\Carbon;
use Str;

trait HasDateFunctions
{
    protected $dateMacros = [];

    public function initializeHasDateFunctions()
    {
        $dateAttributes = $this->getDates();

        foreach ($this->casts as $attribute => $type) {
            if (Str::contains($type, ['date', 'datetime', 'timestamp'])) {
                $dateAttributes[] = $attribute;
            }
        }

        foreach ($dateAttributes as $dateAttribute) {
            $this->dateMacros[Str::camel($dateAttribute)] = function($timezone = null) use ($dateAttribute) {
                if (auth()->check()) {
                    $timezone ??= auth()->user()->timezone;
                }
                else {
                    $timezone ??= config('app.timezone');
                }

                try {
                    return Carbon::parse($this->{$dateAttribute})->timezone($timezone);
                }
                catch (InvalidFormatException $e) {
                    return $this->{$dateAttribute};
                }
            };
        }
    }

    public function __call($method, $parameters)
    {
        if (array_key_exists($method, $this->dateMacros)) {
            return $this->dateMacros[$method](...$parameters);
        }

        return parent::__call($method, $parameters);
    }
}

So now I can use

$user = User::find(1);
$user->valid_from->format('Y-m-d H:i:s');//this is in UTC from the app config
$user->validFrom('Europe/Athens')->format('Y-m-d H:i:s');//this is in Athens timezone

Please or to participate in this conversation.