islamic-network/calendar
A PHP library for working with the Islamic (Hijri) calendar. Convert dates between Gregorian and Hijri, format Islamic dates, and handle calendar calculations with a simple, lightweight API suitable for Laravel and other PHP applications.
The package is a small, framework-agnostic PHP library — there is no Laravel service provider, no facade, and no auto-discovery. You install it and use its classes directly.
composer require islamic-network/calendar
First things to do, in order:
Hijri class.use IslamicNetwork\Calendar\Hijri; // confirm exact class in README
$h = Hijri::gregorianToHijri(2024, 3, 11); // -> [year, month, day]
$g = Hijri::hijriToGregorian($h[0], $h[1], $h[2]);
created_at, published_at, starts_at stay untouched) and render a Hijri line next to it. The second, riskier one is input-side: accept a Hijri date from a form and store the converted Gregorian value.If conversion is core to your domain, install it and write a 20-line test with a handful of known date pairs on day one. That test is your upgrade safety net.
Wrap it. Don't call it from controllers or Blade. Treat the library as an implementation detail behind your own interface, so you can swap in Umm al-Qura or a moon-sighting-adjusted calculator later without touching call sites.
namespace App\Support;
use IslamicNetwork\Calendar\Hijri;
class HijriConverter
{
public function __construct(private int $offsetDays = 0) {}
public function fromGregorian(\DateTimeInterface $date): HijriDate
{
[$y, $m, $d] = Hijri::gregorianToHijri(
(int) $date->format('Y'),
(int) $date->format('n'),
(int) $date->format('j'),
);
return new HijriDate($y, $m, $d);
}
public function toGregorian(HijriDate $h): \Carbon\CarbonImmutable
{
[$y, $m, $d] = Hijri::hijriToGregorian($h->year, $h->month, $h->day);
return CarbonImmutable::createFromDate($y, $m, $d, config('app.timezone'))
->addDays($this->offsetDays);
}
}
Bind it as a singleton in AppServiceProvider and drive $offsetDays from config/calendar.php — that's your only knob for regional adjustment.
Value object + accessor for Eloquent. Never store a Hijri string in a date column. Store Gregorian, expose Hijri:
protected function hijriPublishedAt(): Attribute
{
return Attribute::get(
fn () => app(HijriConverter::class)->fromGregorian($this->published_at)
);
}
A custom cast is fine too, but keep the underlying column Gregorian and timezone-aware.
Blade component, not a directive. A <x-hijri-date :date="$post->published_at" /> component lets you centralize month names, the "calculated" disclaimer, and <time datetime="..."> markup. Directive-based formatting is where you'll accidentally emit unescaped user-controlled strings.
Range queries are where the calendars fight you. "Everything in Rajab 1445" is not a whereMonth. Convert the Hijri month start and the next month's start to Gregorian and whereBetween:
[$sy, $sm, $sd] = Hijri::hijriToGregorian(1445, 7, 1);
[$ey, $em, $ed] = Hijri::hijriToGregorian(1445, 8, 1);
$q->whereBetween('starts_at', [
Carbon::create($sy, $sm, $sd)->startOfDay(),
Carbon::create($ey, $em, $ed)->startOfDay(),
]);
Validation rule for Hijri form input. Validate day against the actual month length (29 vs 30) rather than hardcoding 30 — use the library's month-length helper if it exposes one, otherwise convert (y, m, 1) and (y, m+1, 1) and diff.
API resources: emit both, canonicalize Gregorian.
return [
'date' => $model->starts_at->toIso8601String(),
'hijri' => ['year' => 1445, 'month' => 7, 'day' => 1],
'hijri_label' => '1 Rajab 1445',
];
Consumers who need Hijri get it precomputed; consumers who don't aren't forced to parse it.
Bulk work belongs in a job with a memoized converter. If you're converting a 5k-row export, instantiate the converter once outside the loop and cache any repeated day arithmetic — repeated gregorianToHijri calls in a tight loop are pure function calls, so it should be cheap, but don't re-resolve from the container per row.
This is a calculated calendar, not an observed one. The library implements an arithmetic/tabular conversion. Real-world Hijri dates depend on moon sighting and vary by country by ±1 day, sometimes ±2. Do not use this for Ramadan/Eid announcements, prayer-time boundaries, or anything a user's mosque might contradict. Surface it in the UI as "calculated" and keep the offset config.
Timezone off-by-one is the #1 bug. Always extract Y/n/j from a date already normalized to the display timezone. Carbon::parse('2024-03-11') in a UTC-configured app is fine, but a DateTime that's 2024-03-11 23:30 +05:00 formats to the 11th while its UTC instant is still the 10th. Convert first, format second.
Round-tripping can be lossy at boundaries. Verify greg → hijri → greg is identity across a 29-day month edge, a 30-day month edge, and a Hijri leap year (1445, 1446, 1447). If a tabular implementation disagrees with your expected pairs, you found it in a test rather than in production.
No facade means no IDE help. use the class explicitly, or add your own Hijri facade over HijriConverter for readability. Don't assume Calendar:: resolves to anything.
Pin an exact version and treat upgrades as breaking. Zero dependents and a near-zero score means the API stability guarantee is "whatever the maintainer felt like." "islamic-network/calendar": "1.0.3" in composer.json, plus the round-trip test above, is the cheapest insurance available.
Don't sort on the Hijri string. '1445-9-1' sorts before '1445-10-1' lexicographically. Sort on the Gregorian column and only format to Hijri at render time. If you must sort Hijri strings, zero-pad (1445-09-01).
Hijri years aren't 354 or 355 uniformly. There are 11 leap days in a 30-year cycle; don't compute year length as a constant anywhere in your code.
Arabic month names ship with the package (if at all) in one language. Map to your own locale in the Blade component via a 12-element array keyed on month number. Don't string-translate the package's output — you'll break on the next release.
Extension point to keep open: if your users are Gulf-based, Umm al-Qura may be expected rather than tabular; if South Asian, moon-sighting. The library likely implements one variant. Wrap it behind an interface now (HijriCalculator) so replacing the engine is a one-file change and not a refactor.
How can I help you explore Laravel packages today?