mziraki/nova-persian-date-field
A Laravel Nova field for Persian (Jalali) dates, letting you pick and display dates in the Persian calendar while storing standard Gregorian values. Drop it into your Nova resources for a localized date experience.
Prerequisites and install. You need a licensed Laravel Nova app on Nova 4+ (the package targets the Vue 2 field API of Nova 4; treat Nova 5 as unverified until you spike it). Then:
composer require mziraki/nova-persian-date-field
php artisan nova:publish # only if the package ships publishable assets
php artisan nova:cache # after adding/removing fields in dev
Find the field class first. The README is thin, so open vendor/mziraki/nova-persian-date-field/src/ before writing anything. You want three facts: the exact FQCN (e.g. Mziraki\PersianDate\PersianDate), what it extends (likely Laravel\Nova\Fields\Date or DateTime), and whether a conversion library (morilog/jalali, hekmatinasser/verta) is in composer.json or the conversion is hand-rolled. That last one determines how much you trust the math.
Verify the storage contract before you use it anywhere. This is the single most important first step. Add the field to a scratch Resource, save a value, then:
php artisan tinker
>>> Model::latest()->first()->your_date_column; // must be a Gregorian datetime string
If it writes a Jalali string to the DB, stop and wrap it: ordering, whereBetween, and indexes all degrade to string comparison, and that is the one mistake you cannot retroactively fix cheaply.
First use case. Pick one low-traffic Resource with a real date column — an order delivered_at, a subscription renews_at — and expose it to the operator who actually uses the Persian calendar. Don't roll it out across every Resource until the round-trip is proven.
Wrap it, don't reference it directly. Land an internal field before any Resource consumes the vendor class, so a fork or swap touches one file:
namespace App\Nova\Fields;
use Mziraki\PersianDate\PersianDate; // confirm FQCN in vendor/src
class JalaliDateTime extends PersianDate
{
public function __construct($name, $attribute = null, $resolveCallback = null)
{
parent::__construct($name, $attribute ?? $name, $resolveCallback);
$this->sortable(); // sort resolves against the Gregorian column
}
}
Then in a Resource: JalaliDateTime::make('Delivered At', 'delivered_at')->nullable()->rules('nullable', 'date').
Keep Nova's native date handling for everything non-visual. Filters, sortable(), and the index column all resolve against the underlying Gregorian value; do not try to make Nova filter on the Persian display string. Use Laravel\Nova\Filters\DateFilter (or a custom filter) on the same column — users pick in Jalali, the query runs against Gregorian.
Validation and nulls. The picker is UI only; server-side rules still run on whatever the form posts. If the component posts Gregorian (likely, given "automatic conversion"), date and after_or_equal work normally. Confirm this — if it posts a Jalali string, your rules will pass on strings Eloquent can't cast, and you need a mutator or a prepareRulesForValidation override in your wrapper.
Round-trip test as a fixture, not an afterthought. Put this next to your wrapper:
// tests/Feature/JalaliFieldTest.php
$cases = [
['2024-03-19', '1402-12-29'], // pre-leap 1403
['2024-03-20', '1403-01-01'], // Nowruz boundary
['2025-03-20', '1403-12-30'], // leap day
];
Assert Gregorian-in-DB, Persian-on-render, null-in/null-out. This is what makes a future Nova upgrade fail loudly.
Server-side date math goes somewhere else entirely. Report bucketing, fiscal-period rollups (Farvardin 1 start), cron schedules, and cohort analysis belong in your domain layer with morilog/jalali or ICU's fa-IR@calendar=persian — never in a Nova field. If you find yourself calling the field class outside Nova, you've taken a wrong turn.
Nova 5 will probably break this. Nova 5 moved field components to Vue 3/Inertia; Vue 2 field components do not port automatically. Pin your Nova major in composer.json and treat any Nova bump as a gated change that requires re-running your round-trip tests.
Timezone is the silent bug. Iran abolished DST in 2022, so a date saved at 23:30 Tehran in 2021 and one saved in 2024 have different UTC offsets. Check whether the field honors config('app.timezone'), Nova's per-user timezone preference, or hardcodes Tehran — and pick one contract, document it, and stick to it. Near-midnight entries are where off-by-one-day tickets come from, and they look plausible enough to survive review.
Verify dist/ is committed. If the package ships compiled Vue assets, a Nova upgrade is a fork-and-rebuild task. If it doesn't, you inherit a Node build step you didn't budget for. Either way, know which before the upgrade deadline.
Sorting and searching follow the column, not the display. sortable() on the field sorts by the Gregorian DB value — usually what you want. But if you have any custom index column that formats the value yourself, be aware Nova's search doesn't index the Persian rendering; users searching "1403" won't find records.
Null and empty handling. Nova date fields have historically been inconsistent about empty strings vs null vs Carbon. Test clearing a field and saving — if you get 0000-00-00 or a Jalali "", add a ->nullable() and confirm the model cast. This bites on first save in production if not caught in the spike.
Debugging a wrong date, in order: (1) what's in the DB, (2) what did the form post — Network tab, (3) what does your conversion library say for that Gregorian value, (4) what does the browser show. The field is presentation-only, so steps 1 and 3 will almost always localise the bug.
Extension points are thin, and that's fine. This is a ~200-line field plus a Vue component. If you need RTL labels, Persian numerals in the input, or locale-aware validation messages, you're extending your wrapper or forking — not configuring. Budget a named owner, a pinned version, and a documented fork policy; with 5 stars and a single maintainer since December 2024, upstream is a bonus, not a contract.
Roll out in sequence. One Resource, one operator, one week of real use — then the rest. The blast radius of a wrong date is highest in billing and delivery, so those Resources should be last, not first.
How can I help you explore Laravel packages today?