Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Date Time Laravel Package

brick/date-time

A powerful set of immutable PHP date-time classes built on native PHP and following ISO 8601, inspired by Java's JSR 310. Adds missing concepts such as LocalDate, LocalTime, YearMonth, and MonthDay. Requires PHP 8.2+.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and verify your runtime first

composer require brick/date-time

The library requires PHP 8.2+, which lines up with Laravel 11/12 — if you're still on Laravel 10 with PHP 8.1, you cannot install this at all. Check composer why-not brick/date-time before planning anything.

Where to look first

  1. The class list in the README — it maps 1:1 to ISO 8601 concepts (LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period, Interval, LocalDateRange, YearMonth, MonthDay, YearWeek, DayOfWeek, Month).
  2. Brick\DateTime\DefaultClock and the Clock\* implementations — this is the piece that changes how you write tests.
  3. IsoParsers / the static parse() methods plus Brick\DateTime\Parser\DateTimeParseException — this is your input-validation boundary.
  4. The *NativeDateTime*() conversion methods — this is how the library talks to Eloquent, Carbon, and your database.

Mental model before your first line of code

This is not a Carbon replacement and it will not slot into $table->timestamps() or Model::whereDate(). It is a set of immutable domain value objects to sit alongside Eloquent. The practical rule:

  • Inside the domain (business rules, availability, schedules, business-day math, period arithmetic) use brick/date-time.
  • At the edges (database columns, JSON responses, Blade output, Laravel's now() helper) use native DateTimeImmutable / Carbon and convert explicitly.

First use case

The highest-value first win is usually a date range with real semantics — a booking window, a promo period, a billing cycle — where Carbon leaves you hand-rolling overlap checks and inclusive/exclusive off-by-ones:

use Brick\DateTime\LocalDate;
use Brick\DateTime\LocalDateRange;

$booking  = LocalDateRange::of(
    LocalDate::parse('2026-03-01'),
    LocalDate::parse('2026-03-07'),
);
$promo    = LocalDateRange::of(
    LocalDate::parse('2026-03-05'),
    LocalDate::parse('2026-03-20'),
);

$booking->intersectsWith($promo);        // true
$booking->getIntersectionWith($promo);   // 2026-03-05/2026-03-07
$booking->toDatePeriod();                // native DatePeriod for iteration
$booking->getStart()->daysUntil($booking->getEnd()); // int

That snippet alone — overlap, intersection, inclusive range, native interop — is the pitch. Once a team sees it, adoption spreads to business-day math (plusWeekdays(), minusWeekDays(), DayOfWeek::isWeekend()) and to Period/Duration correctness.


Implementation Patterns

Pattern 1 — Explicit conversion at the persistence boundary (custom Eloquent cast)

Keep DB columns as native DATE / DATETIME / TIMESTAMP. Convert only in the cast. This is the single most important pattern; without it you will leak brick objects into query builders that don't understand them.

namespace App\Casts;

use Brick\DateTime\LocalDate;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class LocalDateCast implements CastsAttributes
{
    public function get($model, $key, $value, $attributes): ?LocalDate
    {
        return $value === null ? null : LocalDate::parse(substr($value, 0, 10));
    }

    public function set($model, $key, $value, $attributes): ?string
    {
        if ($value === null) {
            return null;
        }
        if (is_string($value)) {
            $value = LocalDate::parse($value);
        }
        return (string) $value; // ISO-8601 'Y-m-d'
    }
}

Then protected $casts = ['starts_on' => LocalDateCast::class];. Queries stay Carbon/native: ->whereDate('starts_on', '>=', $from->toNativeDateTimeImmutable()).

Pattern 2 — Adapter/service layer, not fat models

Put domain logic in a small service that accepts and returns brick types, and let controllers/actions do the conversion. This keeps DateTimeException handling in one place and keeps your models free of two competing date APIs.

final class AvailabilityService
{
    public function isAvailable(LocalDateRange $requested): bool
    {
        return ! $this->bookings()
            ->map(fn ($b) => LocalDateRange::of($b->starts_on, $b->ends_on))
            ->contains(fn (LocalDateRange $r) => $r->intersectsWith($requested));
    }
}

Pattern 3 — Time-zone discipline: Instant in storage, ZonedDateTime at the edges

Store UTC (timestamp/Instant), convert to a user's zone only when rendering. Prefer TimeZone::utc() and TimeZoneRegion::parse('Europe/London') over guessing. Because LocalDate has no zone, be explicit that "today" is a zone-dependent question:

use Brick\DateTime\LocalDate;
use Brick\DateTime\TimeZoneRegion;

$today = LocalDate::now(TimeZoneRegion::parse($user->timezone));

Pattern 4 — Testing with clocks instead of Carbon::setTestNow()

Two options, and the choice matters:

  • Preferred: inject a clock into the now() calls under test — LocalDate::now(TimeZone::utc(), $clock). Explicit, no global state, safe under parallel test runs and Octane.
  • Convenient: DefaultClock::freeze(...), travelTo(), travelBy(), scale(). Great for feature tests, but it is process-global static state — reset it in tearDown() without exception.
protected function tearDown(): void
{
    DefaultClock::reset();
    parent::tearDown();
}

Pattern 5 — Input validation via parse() + exception

Laravel has no built-in rule for these types; write one and centralize the try/catch on DateTimeParseException.

final class IsoDate implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        try {
            LocalDate::parse((string) $value);
        } catch (\Brick\DateTime\Parser\DateTimeParseException) {
            $fail("The {$attribute} must be a valid ISO-8601 date.");
        }
    }
}

Pattern 6 — Native interop for everything else

The changelog shows a consistent fromNative* / toNative* family (fromNativeDateTime, toNativeDateTimeImmutable, Period::fromNativeDateInterval(), TimeZone::fromNativeDateTimeZone()). Use these rather than string round-trips — they preserve offsets and microseconds/nanos correctly and are cheaper. For anything Eloquent-bound (whereBetween, Carbon::diffForHumans, Blade formatting), convert out and stay native.


Gotchas and Tips

The package is 0.x — pin the release cycle. The README explicitly states a new 0.x cycle starts on every breaking change. Lock to "brick/date-time": "0.9.*" (or whatever current) and read the release history before bumping. Method names have churned repeatedly (fromDateTime → fromNativeDateTime, Duration::ofMilliseconds → ofMillis, getStartDate → getStart, TimeZoneOffset losing seconds support). Copy-pasting old StackOverflow snippets is a real failure mode here.

Immutability means you must reassign. Every plus*/minus*/with* returns a new instance. $date->plusDays(1); is a silent no-op. This is the #1 day-to-day bug. Consider static analysis (PHPStan/Larastan) to catch unused-expression results.

DefaultClock is process-global static state. Two hazards:

  • Tests: a frozen clock that isn't reset poisons every subsequent test in the same process. Always DefaultClock::reset() in tearDown().
  • Octane / queue workers: a freeze() or scale() left set in one request persists across requests in a long-lived worker. Never set DefaultClock in application code — only in tests.

Most classes are final (since 0.2.0). You cannot extend them, and you cannot Mockery-mock them. This is fine — they're immutable value objects, so construct real ones in tests. But if you were planning a "Carbon-like" wrapper subclass, the design won't allow it; wrap by composition instead.

LocalDate has no time zone, ZonedDateTime has no "local" ambiguity resolution you might expect. LocalDate::now() needs a TimeZone, and DST arithmetic differs by type: adding a Period of 1 day to a ZonedDateTime across a DST boundary is not the same as adding a Duration of 24 hours. Choose Period for calendar math, Duration for elapsed time — and be deliberate.

Precision mismatch at the database. Instant/Duration carry nanoseconds; MySQL DATETIME(6) and PHP's DateTimeImmutable only give microseconds. Truncation is silent. Don't build equality assertions on nanosecond-precision values that have round-tripped through storage.

Illegal dates throw, they don't roll over. DateTimeException on LocalDate::of(2026, 2, 30). That's a feature (fail fast), but it means user-supplied components need validation before construction — or wrap with a parse-and-catch. Conversely, MonthDay handles the recurring --02-29 case that Carbon makes awkward.

TimeZoneOffset rejects seconds (since 0.4.0). Offsets must be whole minutes; ofTotalSeconds() throws otherwise. This also affects ZonedDateTime::parse(). If you ingest offsets like +05:30:30 from a legacy feed, it will now fail.

Doctrine mapping is not Eloquent mapping. brick/date-time-doctrine is irrelevant to a standard Laravel app — it only helps if you run Doctrine via something like laravel-doctrine. For Eloquent, write the casts yourself (Pattern 1). This is the biggest "missing feature" of the package in a Laravel context.

Comparison is via methods, not operators. $a < $b doesn't work. Use isBefore(), isAfter(), compareTo(), and range methods like intersectsWith(). For sorting collections, map to (string) or a native timestamp — ISO-8601 strings sort lexicographically for same-offset values, but don't rely on that across offsets.

ISO week-based years are a trap. YearWeek and DayOfWeek follow ISO-8601, so 2026-01-01 may belong to the previous week-year. If you do any YearWeek reporting, verify boundaries explicitly rather than assuming Year + Week composition.

Check JSON/string behaviour before exposing objects in an API. Verify what json_encode() produces for each type in your version and always render (string) or an explicit ->toISOString()-style call in API Resources. Don't let the serialization format of a 0.x dependency leak into a public contract.

Escape hatch when you need to leave. LocalDateRange::toDatePeriod() and Period::toNativeDateInterval() give you native objects for anything brick doesn't cover — PDF libraries, DatePeriod iteration, third-party SDKs. Keep conversions one-directional and near the edge, and the library stays an asset rather than a lockdown.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core