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

Nova Time Field Laravel Package

oneduo/nova-time-field

Adds a dedicated Time field to Laravel Nova, giving you a clean, configurable time picker for resource forms and detail views. Easy to install and customize for any Nova project.

View on GitHub
Deep Wiki
Context7

Getting Started

Install against your actual matrix, not the README. One command, then immediately verify the constraints it resolved:

composer require oneduo/nova-time-field
composer show oneduo/nova-time-field   # confirm the version you actually got

Open vendor/oneduo/nova-time-field/composer.json and compare its require block against your Nova major (4 vs 5), laravel/framework, and php. The README's compatibility claim is not authoritative — the installed constraint is. A mismatch here surfaces as a blank or unstyled widget in a form, hours later, with no exception thrown.

First use case: one nullable, low-traffic time column. Pick something genuinely time-of-day — cutoff_at, opens_at, shift_start. Not anything that is secretly an instant.

// migration
$table->time('cutoff_at')->nullable()->after('name');
// app/Nova/Location.php
use Oneduo\NovaTimeField\Time;   // verify the exact namespace in vendor/ before trusting this line

public function fields(NovaRequest $request): array
{
    return [
        Time::make('Cutoff', 'cutoff_at')
            ->nullable()
            ->rules(['nullable', 'date_format:H:i:s']),
    ];
}

The smoke test that de-risks everything else. Submit 09:30, then:

  1. Confirm the column contains 09:30:00 (not 1899-12-31 09:30:00, not 9:30 AM).
  2. Reload the detail view — is it displayed the way you entered it?
  3. Edit it to something else and save.
  4. Clear it and save — confirm NULL, not 00:00:00.

That four-step round-trip, especially the clear, is your regression test for every subsequent time field you add. Run it once per Nova upgrade too.

Where to spend your first hour. The package is small: a Nova Field subclass plus a Vue component. Read both before you write your second field. You want to know, concretely:

  • The exact method names for display vs. storage format (this is where the 12/24-hour support lives).
  • What resolve() and fill() actually do to the value — whether they hand you a string or a Carbon instance.
  • Whether compiled JS assets ship in the package or it expects a Node build pinned to Nova's tooling.

That audit answers the questions the README won't, and it's the difference between using this field confidently and guessing at it.

Implementation Patterns

Keep storage naive; push interpretation into the domain. A TIME column is wall-clock with no timezone. Let the field own capture and display. Let your model expose the meaning:

// app/Models/Location.php
protected function cutoffAtLocal(): Attribute
{
    return Attribute::make(
        get: fn (?string $value) => $value
            ? CarbonImmutable::createFromFormat('H:i:s', $value, $this->timezone)
            : null,
    );
}

Do not put 'cutoff_at' => 'datetime' in $casts. You'll staple today's date onto every value, and index queries plus JSON serialization start leaking a fake date nobody asked for. If a specific feature needs a sortable instant, derive it at the query layer:

->where('cutoff_at', '<=', now()->format('H:i:s'))

Pair the field with an explicit validation rule, always. Nova fields are input controls, not validators:

Time::make('Cutoff', 'cutoff_at')
    ->nullable()
    ->rules(['nullable', 'date_format:H:i:s'])
    ->hideFromIndex(),

Pick H:i or H:i:s and enforce it project-wide. Mixed rules are exactly how you end up with 09:30 rows sitting next to 09:30:00 rows, and then every comparison and whereBetween in the codebase becomes suspect.

Decide per field what appears in which of the three views. The field renders in form, detail, and index. Index real estate is scarce:

Time::make('Cutoff', 'cutoff_at')
    ->sortable()
    ->hideFromIndex(),

Time::make('Opens At', 'opens_at')
    ->sortable()
    ->displayUsing(fn ($value) => $value ? substr($value, 0, 5) : '—'),

displayUsing is the right escape hatch for compact columns. It changes presentation only — storage is untouched. Because HH:mm:ss sorts lexicographically, ->sortable() works correctly as long as storage format stays machine-readable.

Extract repeated groups instead of copy-pasting. Opening hours is ten near-identical fields. Return them from a helper so format config lives in exactly one place:

private function hoursFields(): array
{
    return collect(['mon', 'tue', 'wed', 'thu', 'fri'])
        ->flatMap(fn ($d) => [
            Time::make(ucfirst($d).' Open', "{$d}_open")->nullable()->hideFromIndex(),
            Time::make(ucfirst($d).' Close', "{$d}_close")->nullable()->hideFromIndex(),
        ])
        ->all();
}

Cross-field rules (close must be after_or_equal open) belong in a FormRequest. The field has no notion of that relationship and shouldn't.

Compose with Nova's conditional APIs, not custom JS. ->dependsOn(), ->readonly(), ->onlyOnForms(), ->hideWhenCreating() all work normally because this is a standard Field. The pattern that pays off:

Boolean::make('Closed', 'is_closed'),
Time::make('Opens At', 'opens_at')
    ->dependsOn(['is_closed'], fn ($field, $isClosed) => $isClosed ? $field->hide() : $field),

Migrating off a legacy datetime field. Add the time column, backfill with a reviewed, explicit transform, keep the old field read-only for one release, then drop it. The backfill is the whole risk: converting a UTC instant to local wall-clock is lossy and DST-ambiguous. Write that policy down before the backfill script exists, not after someone reviews the diff.

Gotchas and Tips

00:00:00 is a value; null is not. Midnight is a legitimate close time. Never test a time column with empty() — '00:00:00' is falsy in PHP and will silently vanish from "is this configured?" logic. Use $value !== null. And assert null persistence in a test: clearing the field must write NULL, not midnight.

The package captures time; it does not schedule. Timezones, DST, recurrence, availability, conflict detection, duration math — all yours. Don't let the presence of a time picker imply any of that is solved. It's a form control with a Vue component behind it.

12/24-hour display becomes a bug the moment it leaks into storage. HH:mm:ss sorts lexicographically and compares correctly in SQL — that's why ->sortable() and whereBetween work. Store g:i A (9:30 AM) and both break silently. Treat display format as cosmetic, storage format as a contract you never change casually.

MySQL TIME is looser than you assume. It accepts values past 24 hours and negative values (±838 hours) — it doubles as a duration type. The field produces sane clock times, but hand-written seeders and ad-hoc queries can insert 25:30:00 and nothing will stop them. Don't assume the column enforces 00:00:00–23:59:59.

Class-name collision on future Nova majors. If Nova ships its own Time field, an unqualified use ...\Time; in a resource can resolve to the wrong class — and the failure looks like a rendering bug, not a type error. Alias defensively in resources importing both, and re-check at every Nova upgrade.

Asset breakage is the actual upgrade failure mode. Nova majors move Vue/Tailwind/tooling, and compiled field assets are the standard break vector. You won't get a failing test — you'll get a blank or unstyled widget in an internal form. Find out on day one whether the package ships prebuilt JS or expects a Node build pinned to Nova's tooling. Add "open every resource using this field" to the Nova upgrade checklist.

Pin, mirror, own it. Small package, MIT, one maintainer, recent release. Copy the repo into your VCS or vendor it and treat your copy as the artifact of record. The surface is about an hour to audit and roughly half a day to reimplement — exactly the kind of dependency you can carry, provided someone named owns it.

Debugging order when it misbehaves:

  1. composer show oneduo/nova-time-field — is the resolved version what you think it is?
  2. Nova version against the package's composer.json constraints.
  3. Browser console / network tab for a failed asset load.
  4. dd() the raw model attribute.

Then split the diagnosis: if the value is right in the DB and wrong on screen, it's formatting (displayUsing, format config). If it's wrong in the DB, it's fill() or validation — check your rules() and whether the request is even reaching the field.

Tests worth writing (three cover most of the risk):

// 1. round-trip
// 2. clearing persists NULL, not 00:00:00
// 3. a fixed wall-clock value survives a DST transition date unchanged

That third one should pass trivially — a naive time isn't shifted — which is exactly why it's useful: if it ever fails, someone introduced timezone handling into the storage path.

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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route