rohanadhikari/nepali-date
rohanadhikari/nepali-date is a Laravel/PHP package for working with Nepali (Bikram Sambat) dates. It helps convert between AD and BS, format and parse Nepali dates, and integrate date handling into Laravel apps.
Install the package and let Laravel's package discovery register the service provider:
composer require rohanadhikari/nepali-date
php artisan vendor:publish --tag=nepali-date-config
A plain composer require resolves to the latest stable line (1.x). A v2.0-beta exists upstream, but Composer will not select a pre-release unless you ask for it explicitly (^2.0@beta or a lowered minimum-stability), so a fresh install today is still on 1.x. Don't opt into the beta on a project that has a deadline; do read the upstream changelog diff before you do.
The config file is the first thing to open — it controls the default output format and the timezone used when "now" is resolved to a BS date. Set the timezone explicitly to Asia/Kathmandu rather than relying on app.timezone, because date boundaries in Nepal do not line up with UTC or with your app's default.
Where to look first: the package exposes two value objects — a BS date (NepaliDate) and an AD date (AdDate). Almost all day-to-day work is one of two conversions:
use RohanAdhikari\NepaliDate\NepaliDate;
// BS -> AD (e.g. parsing a form submission before saving)
$ad = NepaliDate::from('2081-04-01')->toAd(); // AdDate
// AD -> BS (e.g. rendering a stored timestamp)
$bs = NepaliDate::fromAd(now())->toBs(); // NepaliDate
echo $bs->format('Y-m-d'); // "2081-04-01"
First use case: an invoice or receipt that must show a Bikram Sambat date while the database keeps Gregorian timestamps. Add an accessor that converts created_at at render time — do not add a bs_date column on day one.
Store AD, render BS. Keep every date, datetime, and timestamp column in Gregorian. Convert at the edges: API resources, Blade views, PDF exports, and CSV downloads. This preserves whereBetween, orderBy, and index behaviour, and means a bad timezone assumption only breaks display, not data.
Convert once per request, not per row. In a report of 5,000 invoices, converting each row's created_at in a loop is measurable overhead. Convert the query bounds to AD instead:
// Nepali fiscal year 2081/82: Shrawan 1 to Ashadh end
$from = NepaliDate::from('2081-04-01')->toAd()->toDateTime();
$to = NepaliDate::from('2082-03-32')->toAd()->toDateTime();
$invoices = Invoice::whereBetween('issued_at', [$from, $to])->get();
Validate as BS, persist as AD. Use a form request that first checks the shape, then the calendar:
public function rules(): array
{
return ['issued_on_bs' => ['required', 'regex:/^\d{4}-\d{2}-\d{2}$/']];
}
public function toAd(): array
{
$bs = NepaliDate::from($this->input('issued_on_bs'));
// Round-trip guard: catches 2081-01-32 style input that BS month
// length rules reject.
abort_unless($bs->toAd()->toBs()->format('Y-m-d') === $this->input('issued_on_bs'), 422);
return ['issued_at' => $bs->toAd()->toDateTime()];
}
Expose both calendars in API resources. Clients differ: internal tools want ISO BS strings they can sort, public pages want preformatted Nepali names.
return [
'issued_at' => $this->issued_at->toIso8601String(), // AD, canonical
'issued_on_bs' => NepaliDate::fromAd($this->issued_at)->format('Y-m-d'),
'issued_on_bs_l' => NepaliDate::fromAd($this->issued_at)->format('j F Y'), // Nepali label
];
Add a thin application wrapper. Rather than importing the vendor namespace in 40 files, create App\Support\BsDate with fromAd(), fromBs(), today(), and label(). When the package's API shifts — or you swap libraries — you edit one file. This also gives you a single place to pin the numeral script (Devanagari vs Latin digits) and the format presets your product actually uses. The wrapper is what makes a future 1.x → 2.x move a one-file diff instead of a grep-and-patch exercise.
Blade and view helpers. Register a directive or a small component once in a service provider and use it everywhere:
Blade::directive('bs', fn ($expr) => "<?php echo e(\\App\\Support\\BsDate::label($expr)); ?>");
{{-- renders "१५ श्रावण २०८१" or "15 Shrawan 2081" --}}
@bs($invoice->issued_at)
Freeze "today" for scheduled jobs. In queued jobs, exports, and PDF generation, resolve BsDate::today() once at the top and pass the value down. A job that runs at 18:20 UTC is already on the next Nepali day, and a "yesterday vs today" comparison computed inside a loop can straddle the boundary mid-run.
strtotime() and checkdate() are useless here. checkdate(4, 1, 2081) is not a Gregorian date and PHP will happily accept or reject it wrongly. The only reliable validation is a parse plus a round-trip comparison.diffInHours() against a naive expectation will drift. Always convert via an explicit timezone, never date('Y-m-d') on a raw timestamp.now()->toDateString() then converting is one day off for roughly 18:15–24:00 UTC. Use now('Asia/Kathmandu').diffForHumans(), isoFormat(), or ->startOfMonth(), wrap it explicitly: Carbon::instance($ad->toDateTime()) or Carbon::parse($ad).format() returns a string; conversions return new instances. Don't write $bs->addDays(3) and assume $bs changed — use the return value.YYYY-MM-DD form with Latin digits."rohanadhikari/nepali-date": "^1.0" in composer.json (or ^2.0 only after you have read the changelog and run the test below against it), plus a feature test that round-trips a handful of boundary dates (first of each month across two BS years, and the last day of a 32-day month). That test is what tells you a composer update broke something.v2.0-beta as unproven until the changelog says otherwise. The release announcement is a bare compare link, so the only safe assumption is that a major bump may rename methods, change default formats, or alter the supported year range. Before any upgrade: read the diff, branch, bump the constraint with an explicit pre-release flag, run your round-trip test suite, and only then touch the wrapper class. Ship on 1.x until there is a stable 2.x.from(), fromAd(), toAd(), and toBs() against the version you installed before spreading them across the wrapper class — the wrapper exists precisely so a naming change stays contained.How can I help you explore Laravel packages today?