laravel/framework: [13.x] Fix duration helpers truncating float values
Contributed by xurshudyan
Part of the framework v13.35.0 release
Laravel's duration helpers gained a subtle but consequential bug when they started accepting floats in v13.34.0 (via #58006): although the signatures promised int|float support, the decimal part of any float argument was silently dropped before the interval was built. In practice, hours(0.5) produced a zero-second interval — meaning cache entries with a hours(0.5) TTL stored nothing at all, and any delay or expiration built from a fractional duration collapsed to zero. Pull request #61825 fixes this by routing values through CarbonInterval::make(), which preserves fractions. Integer arguments behave exactly as before, so this is a pure bug fix with no API change.
What changed
The fix lives in src/Illuminate\Support\functions.php and touches all four duration helpers — seconds(), minutes(), hours() and days(). Each one swaps a Carbon named constructor for CarbonInterval::make() with an explicit unit:
// Before (v13.34.0)
return CarbonInterval::hours($hours);
// After
return CarbonInterval::make($hours, 'hours');
The same one-line change is applied to the other three helpers, and milliseconds() is deliberately left untouched. The pull request also adds a dedicated test file, tests/Support/SupportDurationFunctionsTest.php, which locks in both behaviors: whole numbers keep producing the intervals they always did, and fractional values now survive intact:
seconds(1.4)->totalMilliseconds; // 1400
minutes(1.5)->totalSeconds; // 90
hours(0.5)->totalSeconds; // 1800
days(1.5)->totalHours; // 36
Why the fraction disappeared
Carbon's named constructors — CarbonInterval::seconds(), ::minutes(), ::hours(), ::days() — expect integer parameters, so a float argument gets coerced to int along the way. hours(0.5) therefore became hours(0): a perfectly valid, perfectly useless zero-length interval. Nothing errored, nothing warned; the value was just wrong.
CarbonInterval::make($value, $unit) takes a value/unit pair and keeps the float intact, so CarbonInterval::make(0.5, 'hours') genuinely represents half an hour. For whole numbers the two approaches are equivalent — make(2, 'hours') and hours(2) produce identical intervals — which is why callers passing integers see no difference after this patch.
The bug in practice
The truncation was easy to miss because nothing failed loudly. The helpers' type signatures said int|float, so the code read as correct, but the runtime betrayed it:
use function Illuminate\Support\hours;
hours(0.5)->totalSeconds; // 0 in v13.34.0, expected 1800
Cache::put('key', 'value', hours(0.5)); // TTL resolves to 0 → nothing is stored
Anything that consumes a DateInterval-compatible interval was affected: cache TTLs, queue delays and backoff windows, token or lock expirations. A fractional duration silently meant "no duration", which is the worst kind of bug — the kind that looks like a caching or queueing problem somewhere else entirely.
Upgrade impact
This is a drop-in patch for anyone upgrading from v13.34.0. Signatures and return types are unchanged: the helpers still accept int|float and still return a CarbonInterval. A few practical notes:
- Integer callers: nothing changes.
hours(2)is byte-for-byte the same interval before and after the fix. - Float callers: your durations become correct. Cache entries written with
hours(0.5)now actually live for 30 minutes; delays ofminutes(1.5)now wait 90 seconds. Audit any tests or assertions that knowingly or unknowingly relied on the truncated zero-duration behavior — that's the only realistic source of surprises. - Workarounds: if you coded around the bug, for example by writing
minutes(30)instead ofhours(0.5), or by manually converting to integer units, those workarounds still work but can now be removed in favor of the clearer form.
After the upgrade, the natural code just works:
Cache::put('key', 'value', hours(0.5)); // kept for 30 minutes
$job->delay(minutes(1.5)); // delayed by 90 seconds
Takeaways
seconds(),minutes(),hours()anddays()now preserve fractional values by usingCarbonInterval::make($value, $unit)instead of Carbon's int-casting named constructors.- In v13.34.0 and earlier, float arguments were truncated to integers, so
hours(0.5)was effectively zero seconds — and aCache::put()with that TTL stored nothing. - Integer behavior is identical before and after; there are no signature or return-type changes.
- The upgrade is safe and mechanical; only code depending on the buggy zero-duration behavior needs a second look.
- New regression tests cover both integer and fractional inputs, so this behavior is now pinned down.