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

Time Laravel Package

grisaia/time

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and check the runtime floor first.

composer require grisaia/time

Before writing a line of code, open vendor/grisaia/time/composer.json and read the require.php constraint. The package advertises "latest PHP features" (readonly classes, enums, first-class callable syntax), which usually means PHP 8.2+ or 8.3+. If your app or CI matrix still runs 8.1, stop here — that constraint will decide whether this is even an option.

2. Read tests/ before the README.

This is a 0-star package with 0 dependents and a single release (2025-05-27). The README will tell you the pitch; the test suite will tell you the truth about method names, factory signatures, and — critically — how errors are signalled, since the package promises "no exceptions." Read tests/ in this order:

  • Any test file named *Parse*, *Create*, *From* — this is your constructor/factory surface.
  • Any test named *Invalid*, *Fail*, *Null* — this is your error-handling contract. This is the single most important thing to learn.
  • Any test touching timezones or DST.

3. Identify the core value type and its companion types.

Most "DateTime alternative" packages ship a small set: one immutable instant/date value class, one enum or value object for timezone, possibly one for units/periods, and possibly a Clock/now() abstraction. Grep the src/ tree:

find vendor/grisaia/time/src -name '*.php' | head -50
grep -rn "final readonly class\|^enum " vendor/grisaia/time/src

4. First use case: swap one boundary, not the whole app.

Do not attempt a global DateTimeImmutable → grisaia/time migration. Pick the narrowest place where you currently suffer: a service that does date math, or a value object in a domain model. Replace it there, convert back to DateTimeImmutable at the edges, and ship. Concretely:

// before
final class Subscription
{
    public function __construct(
        private DateTimeImmutable $startedAt,
    ) {}
}

// after — package type stays inside the domain
final class Subscription
{
    public function __construct(
        private \Grisaia\Time\Instant $startedAt, // verify exact FQCN in src/
    ) {}
}

Then measure: if the boundary conversion boilerplate outweighs the benefit within a day or two of real use, you have your answer about adoption.


Implementation Patterns

Boundary conversion (the pattern you'll use most)

Because the package almost certainly does not implement DateTimeInterface (it's an alternative, not a decorator), every framework edge needs an adapter. Centralise them in one place rather than scattering ->toNative() calls:

final class TimeBridge
{
    public static function toNative(\Grisaia\Time\Instant $t): \DateTimeImmutable
    {
        return new \DateTimeImmutable($t->toIso8601(), new \DateTimeZone('UTC'));
    }

    public static function fromNative(\DateTimeImmutable $t): \Grisaia\Time\Instant
    {
        return \Grisaia\Time\Instant::fromString($t->format(DATE_ATOM)); // verify factory name
    }
}

Having exactly one file that imports the package type alongside native types makes the eventual "rip it out" decision a one-file change.

Eloquent cast / Doctrine type

You will need a custom cast; the built-in datetime cast expects DateTimeInterface.

final class InstantCast implements CastsAttributes
{
    public function get($model, string $key, $value, array $attributes): ?Instant
    {
        return $value === null ? null : TimeBridge::fromNative(new DateTimeImmutable($value));
    }

    public function set($model, string $key, $value, array $attributes): ?string
    {
        return $value === null ? null : TimeBridge::toNative($value)->format('Y-m-d H:i:s');
    }
}

Keep storage in UTC as DATETIME/TIMESTAMP and convert to a display timezone only at the presentation layer.

Immutable chaining in domain logic

The whole point of the package is that arithmetic returns new instances. Write it fluently and don't fight it:

$renewal = $subscription->startedAt()
    ->plusMonths(1)          // name may differ — check src/
    ->atStartOfDay();        // calendar-aware, not 24h math

if ($now->isAfter($renewal)) { /* ... */ }

Because the value is immutable, it's safe to store in readonly DTOs and pass across layers without defensive copying — a real ergonomic win over mutable DateTime.

Deterministic tests without a global clock hack

If the package ships a Clock interface or a now() seam, inject it. This replaces Carbon::setTestNow()/Carbon::setTestNow(null) teardown noise:

final class ReportBuilder
{
    public function __construct(private Clock $clock) {}

    public function build(): Report
    {
        $today = $this->clock->now()->atStartOfDay();
        // ...
    }
}

// test
$builder = new ReportBuilder(new FixedClock(Instant::fromString('2025-01-15T00:00:00Z')));

If no clock abstraction exists, that's your extension point: define one in your own namespace and delegate to the package.

"No exceptions" as an explicit result

The headline feature means parsing and construction presumably return null or a result object rather than throwing. Treat that as a forcing function at input boundaries — convert it to your app's error style immediately so a silent null doesn't leak three layers deep:

$instant = Instant::fromString($request->input('starts_at'));

if ($instant === null) {
    throw ValidationException::withMessages([
        'starts_at' => 'Not a valid ISO-8601 timestamp.',
    ]);
}

Never let a package-level null reach a controller response as a silent fallback.

Serialisation for APIs

Expose ISO-8601/UTC explicitly rather than relying on json_encode of the value object (which may serialise {} if properties are private readonly):

final class EventResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'starts_at' => $this->startsAt->toIso8601(), // confirm method exists
            'timezone'  => $this->startsAt->timezone()->name(),
        ];
    }
}

Gotchas and Tips

"No exceptions" is a double-edged sword. Missing or malformed input returns a null-ish value instead of failing loudly. The failure mode is a null propagating through your domain until it explodes at an unrelated ->format() call. Mitigation: wrap every external entry point (HTTP request, DB row, config) in a small assert helper that throws your own domain exception. Keep the package's no-throw style inside the domain, throw at the boundary.

It is not a DateTimeInterface. This will bite you in more places than you expect:

  • Laravel's date, date_format, after:, before: validation rules won't accept the object.
  • ->format(), ->getTimestamp(), ->diff() calls from third-party code (logging, mailers, Sanctum token expiry, cache TTLs) won't apply.
  • Doctrine DBAL's datetime type will refuse it.
  • Carbon interop ($carbon->diffForHumans()) is gone unless you convert.

Write the bridge once and route all framework interactions through it.

Strict types means no coercion. '2025-01-15' (string) and 2025 (int) are not interchangeable the way they are with DateTime's constructor. JSON bodies always arrive as strings; form inputs are always strings. Do the cast explicitly and early. Conversely, unit arguments (add 3 what?) may be enums rather than ints — check whether plus(3, 'days') even compiles or whether it's plusDays(3).

Immutability means you must reassign. $dt->plusDays(1); on its own line is a no-op with no warning. This is the single most common bug when porting from DateTime::modify(). If you're migrating, grep for statements that discard the return value.

Timezone defaults are the most likely surprise. Find out fast: is the timezone required at construction (good, explicit) or does it fall back to date_default_timezone_get() (a hidden global)? If the latter, set it explicitly everywhere and consider asserting the default in a bootstrapping test — otherwise a php.ini difference between your laptop and production silently shifts every timestamp.

DST arithmetic and month-end overflow. plusMonths(1) on Jan 31 is ambiguous across libraries — some clamp to Feb 28, some roll to Mar 3. plusDays(1) should be calendar-aware (23h/25h across DST) whereas plusHours(24) is not. Write a test with a DST boundary in your own suite before you rely on this in billing or scheduling logic.

Debugging. var_export()/print_r() on a value object with a private constructor and private readonly props will not round-trip, and json_encode may yield {}. Depend on __toString/toIso8601() for logs, and use dump()/dd() (which respects object internals better) during development. If the class is final, you cannot mock it — which is exactly why injecting a Clock matters more here than usual.

Zero dependents, zero stars, one release. Bus factor is 1 and there is no ecosystem to absorb a breaking change. Concrete mitigations:

  • Pin the exact version ("grisaia/time": "1.0.0", not ^1.0).
  • Never reference the package's FQCNs outside your bridge/DTO layer — your own App\Time\Instant wrapper or a thin interface keeps the swap to one commit.
  • Vendor the source into a private fork only if you actually adopt it broadly; otherwise prefer the bridge.

Check the extension points before you commit. Look for: interfaces (a clock, a formatter, a parser), non-final classes, static factories, and whether the value object is readonly. If everything is final readonly with private constructors, you get safety but you must rely on factories for test fixtures — that's usually fine, but confirm the factory accepts the inputs you need (arbitrary microseconds, fixed offsets like +05:30, pre-1970 dates, year 0/negative years).

Validate your real data, not the happy path. Point the parser at 200 rows of actual production timestamps — mixed offsets, Z vs +00:00, leap days, 24:00:00, microsecond precision, and nulls. A 0-star package's test suite rarely covers your data's weirdness, and "no exceptions" means bad input will fail quietly rather than tell you.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator