kachnitel/duration-bundle
A Laravel/PHP package for working with and formatting durations, helping you convert time values into readable strings and handle duration logic cleanly in your applications.
This is a v0.0.1 package, so treat the API as unstable and pin it ("vendor/duration": "0.0.1" exact, not ^0.0.1) until 1.0.
Install and orient yourself
composer require vendor/duration
There is no service provider or config file to publish in the core — that is the first thing worth knowing. The release is layered:
| Layer | Relevant to Laravel? |
|---|---|
DurationUtil (parse/format, seconds, DateInterval) |
Yes — this is the package for you |
DurationType (Symfony form type) |
No, unless you run Symfony Forms in Laravel |
Twig extension (duration, toSeconds, hhmm) |
No, unless you use Twig via a bridge |
So read DurationUtil and its unit tests first. The test suite is effectively the API documentation for edge cases (negative values, HH:MM vs HH:MM:SS, overflow past 24h) that the README examples won't cover.
First use case: user-entered durations on a time-tracking feature
The highest-value day-one win is normalising messy human input into a single storable representation — integer seconds — and formatting it back for display:
use Vendor\Duration\DurationUtil;
$seconds = DurationUtil::toSeconds($request->input('duration')); // "1h 30m", "01:30", "5400"
$display = DurationUtil::hhmm($seconds); // "01:30"
If all you need is toSeconds() on input and hhmm() on output, you are using the package correctly. Everything below is about wrapping it so the rest of the app never touches the package directly.
1. Store integers, format at the edges
Persist seconds (unsignedInteger) and never store the raw string. This keeps aggregation, sorting, and arithmetic in SQL, and makes a future swap of the package a non-event.
$table->unsignedInteger('budget_seconds')->default(0);
2. Wrap DurationUtil in one app-owned service
Given 0.0.1 status, isolate the dependency. One class, one place to change:
final class Duration
{
public static function parse(?string $input): int
{
return DurationUtil::toSeconds((string) $input); // throws on invalid
}
public static function parseOrNull(?string $input): ?int
{
try { return static::parse($input); } catch (\Throwable) { return null; }
}
public static function format(int $seconds): string
{
return DurationUtil::hhmm($seconds);
}
}
3. Custom Eloquent cast for read/write symmetry
class DurationCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes): ?string
{
return $value === null ? null : Duration::format((int) $value);
}
public function set($model, $key, $value, $attributes): ?int
{
return $value === null ? null : Duration::parse($value);
}
}
4. Normalise in the form request, not the controller
Use prepareForValidation() so validation rules run against a known shape, then validate the integer rather than the free-text:
protected function prepareForValidation(): void
{
if ($this->filled('duration')) {
$this->merge(['duration' => Duration::parseOrNull($this->input('duration'))]);
}
}
public function rules(): array
{
return ['duration' => ['required', 'integer', 'min:1', 'max:86400']];
}
5. Handle DateInterval for elapsed/derived time
The DateInterval conversion utilities shine when your source is timestamps rather than typed input — e.g. started_at/finished_at on a timesheet, or a queued job's runtime. Convert the diff, then hand it to the formatter instead of reimplementing %H:%I maths:
$elapsed = DurationUtil::fromDateInterval($startedAt->diff($finishedAt));
Duration::format($elapsed);
6. Expose both representations in API resources
Let clients pick; it costs nothing:
return [
'duration_seconds' => $this->duration_seconds,
'duration' => Duration::format($this->duration_seconds),
];
7. Blade presentation
Skip the Twig extension — add a one-line directive or a component in AppServiceProvider:
Blade::directive('duration', fn ($expr) => "<?php echo e(\\App\\Support\\Duration::format((int) ($expr))); ?>");
8. Aggregation flows
For reports ("time by project this week"), sum seconds in SQL, then format once per row at render. Parsing per row in PHP is the common performance mistake here.
HH:MM is genuinely ambiguous. 01:30 reads equally as 1h30m or 1m30s depending on which parse branch wins. Find out from the tests which the package favours, document your choice, and — if the input is user-facing — disambiguate at the UI layer (separate hour/minute fields, or require a unit suffix). This is the single most likely source of silent bugs.>= 24 hours wraps if formatting goes through DateInterval and uses %H. For durations like "36:00" (long projects, log windows), verify against a >24h case before trusting the output, and fall back to computing total hours yourself if needed.-01:30 round-trips depends on parsing and formatting both supporting the sign. Test it; if unsupported, clamp at the boundary rather than letting it reach storage.null? Confirm this early and wrap it once (see parseOrNull). Validation rules that assume null will blow up if the package throws, and vice versa.1,5h from a European keyboard is a realistic input that a naive parser rejects or truncates. Normalise commas to dots in prepareForValidation().DurationType and Twig filters are not Blade/Symfony-in-Laravel features. If you reach for DurationType, you're either on a hybrid stack or on the wrong tool.DateInterval — rather than the final string. It immediately tells you whether the fault is in parsing or formatting.parse(format($n)) === $n over a table of boundary values (0, 59, 60, 3599, 3600, 86399, 86400, 90000) is ten lines of PHPUnit and catches most of the issues above.How can I help you explore Laravel packages today?