atymic/laravel-dateinterval-cast
A Laravel Eloquent cast that converts database values to and from PHP DateInterval objects, letting you store intervals in your models and access them as native DateInterval instances without manual parsing or conversion code.
composer require atymic/laravel-dateinterval-cast
There is nothing to register: no service provider, no config file, no migrations. The package is effectively one CastsAttributes implementation, so "where to look first" is the README, then src/ (read it end-to-end, it is minutes of work), then the tests — the tests are the real specification for what string ends up in the column. Confirm two things before committing:
set() writes (the natural contract for a DateInterval cast is an ISO-8601 duration string such as P1DT2H30M).illuminate/* version constraints in composer.json. The only release is dated 2020-09-09, so on Laravel 9/10/11 you may need -W, a constraint bump, or a fork.The first use case is a duration column on a model:
use Atymic\DateIntervalCast\DateIntervalCast;
class Task extends Model
{
protected $casts = [
'duration' => DateIntervalCast::class, // varchar column
];
}
$task->duration = new DateInterval('PT45M');
$task->save();
$task->duration->format('%h hours %i minutes'); // "0 hours 45 minutes"
From there, $task->duration is a real object you can pass to DateTime::add(), diff(), or wrap in CarbonInterval — that is the entire value proposition.
Read/write round-trip. Treat the cast as the single conversion boundary. Assign DateInterval (or a CarbonInterval, if the cast accepts it on set() — verify) and never hand-format strings into the attribute. Any place you write the raw column string, you've bypassed the cast and will eventually write something the get() side cannot parse.
CarbonInterval for ergonomics. Native DateInterval is spartan: no forHumans(), no totalSeconds(), awkward arithmetic. Wrap when you need more than format():
$ci = CarbonInterval::instance($task->duration);
$ci->forHumans(); // "45 minutes"
$ci->totalSeconds; // int
$ci->addMinutes(15); // mutable
If the package ships a separate CarbonIntervalCast, prefer it on models where every consumer wants the fluent API; otherwise a tiny accessor (Attribute in Laravel 9+) that wraps the cast value is usually cleaner than casting the same column twice.
Querying. Casts are an Eloquent hydration concern only — they do not translate into SQL. where('duration', '>', ...) compares strings. If you need to sort, filter, or aggregate durations, add a duration_seconds integer column and maintain it alongside the cast (observer, saving hook, or a mutator that derives both). This is the pattern that keeps reports correct without fighting the cast.
API output. Do not let DateInterval reach toArray()/toJson() directly. Format explicitly in a resource:
return [
'duration' => $this->duration?->format('P%yY%mM%dDT%hH%iM%sS'),
'duration_human' => $this->duration
? CarbonInterval::instance($this->duration)->forHumans()
: null,
];
Input normalization. Validate the incoming string as ISO-8601 in a FormRequest (regex:/^P(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/ is a serviceable first pass) and let the cast construct the object. If you accept free text like "1h30m", parse it yourself with CarbonInterval::fromString() and assign the resulting interval.
Extension. If you need a fixed serialization format or special null handling, subclass the cast and override set()/get() rather than duplicating the logic in mutators. Register the subclass in $casts; custom casts are just classes.
Testing. Assert on the storage format, not the object: $this->assertDatabaseHas('tasks', ['duration' => 'PT45M']), then assert the hydrated type with assertInstanceOf(DateInterval::class, $task->fresh()->duration).
DateInterval arithmetic is not fluent. add()/sub() live on DateTime, not DateInterval. Use CarbonInterval::instance() for anything beyond a single format() call.%-prefixed and non-obvious. %h is hours within the day (0–23), not total hours; %a is total days; %R/%r are sign prefixes. Durations over a day silently look wrong if you use %h. Use CarbonInterval for total hours/seconds.P1M is not 30 days in this representation; converting to seconds via days * 86400 will drift. Decide early whether a column means "elapsed seconds" or "calendar interval" and don't mix them.new DateInterval('1 day') throws; the string must start with P. Fractional seconds (PT0.5S) are not supported by PHP's parser.invert property, not a leading -. If you need to persist negatives, confirm the cast handles invert — many simple implementations do not, and you'll silently store a positive value.PT0S vs null. Zero is a valid interval and will hydrate as an object; null is a different state. Guard with ?-> and don't rely on empty checks.DateInterval is unstable across PHP versions. Don't assume {} or a property bag; always format in resources. Same for (string) $task->duration — DateInterval is not stringable.Task::query()->update([...]), DB::table(), and raw SQL bypass set(). You will write unparseable values and only notice when the model hydrates.$casts vs accessors. In Laravel 7+, a cast and an accessor for the same attribute conflict; the cast wins in most paths. Pick one.$model->getRawOriginal('duration') shows exactly what is in the column; $model->getAttributes() shows the pre-hydration state. If get() returns garbage, the raw value is almost always the culprit, not the cast.app/Casts/ and own it — it's ~40 lines — rather than pinning a dependency you can't patch.How can I help you explore Laravel packages today?