Hijri
Hijri (Islamic) calendar support for PHP, built on Carbon. Convert any Gregorian date to Hijri, build Gregorian dates from Hijri parts, parse Hijri strings, and format Hijri month names in Arabic or English with the Carbon methods you already know.
Gregorian to Hijri
on any Carbon date, or on any date string.
Hijri to Gregorian
and return regular Carbon dates.
Localized Months
Arabic month names for locales, English transliteration for the rest.
The Hijri class extends Carbon\Carbon, so format(), isoFormat(), locale() and the year/month/day properties all work on a converted date.
Installation
Install the package with Composer and register the Carbon mixin once at boot.
Requirements
- PHP 8.6.x
- Carbon 2.x (
nesbot/carbon^2.20)
No PHP extensions are required. The Julian Day math is implemented in plain PHP, so ext-calendar isn't needed.
Composer Installation
composer require pharaonic/php-hijriRegister the Carbon Mixin
toHijri(), fromHijri() and parseHijri() live in the HijriCarbon trait. Mix it into Carbon once, as early as possible in your app's bootstrap:
1use Carbon\Carbon;2use Pharaonic\Hijri\HijriCarbon;3 4Carbon::mixin(HijriCarbon::class);In a Laravel app, put the same call in the boot() method of App\Providers\AppServiceProvider.
You can skip the mixin and call the Hijri class directly: Hijri::parse(), Hijri::fromGregorian(), Hijri::fromHijri() and Hijri::parseHijri() work on their own.
You're all set! Call Carbon::now()->toHijri() to get today's Hijri date.
Day Adjustment
The package has no config file. Its only setting is the day adjustment: a whole number of days added to the Gregorian date before it's converted to Hijri (and subtracted when converting back).
The conversion uses the tabular Islamic calendar, which can be a day or two off from the dates announced after local moon sighting. The adjustment lets you line results up with your region.
Default Value
The default adjustment is -1, which matches the package's historic results:
1use Pharaonic\Hijri\Hijri;2 3Hijri::getInstance()->getHijriAdjustment(); // -1Change It Globally
Set it once at boot. Every later conversion that doesn't pass its own adjustment uses this value:
1use Pharaonic\Hijri\Hijri;2 3Hijri::getInstance()->setHijriAdjustment(0);With the mixin registered, the same methods are available on any Carbon instance:
1Carbon::now()->setHijriAdjustment(0);2Carbon::now()->getHijriAdjustment(); // 0Override It Per Call
Every conversion method accepts an $adjustment argument. It affects that call only and never changes the global value:
1$date = Carbon::parse('2024-03-11');2 3$date->toHijri()->format('Y-m-d'); // "1445-09-01" (global -1)4$date->toHijri(0)->format('Y-m-d'); // "1445-09-02"5 6Carbon::fromHijri(1445, 9, 1)->toDateString(); // "2024-03-11"7Carbon::fromHijri(1445, 9, 1, null, 0)->toDateString(); // "2024-03-10"The global adjustment is a static value shared by the whole PHP process. In long-running workers (queues, Octane, Swoole), set it once at boot rather than changing it per request, and use the per-call argument for one-off conversions.
Basic Usage
With the mixin registered, convert any Carbon date to Hijri:
1use Carbon\Carbon;2 3$hijri = Carbon::parse('2024-03-11 09:30:00')->toHijri();4 5$hijri->format('Y-m-d'); // "1445-09-01"6$hijri->isoFormat('dddd D MMMM YYYY'); // "Monday 1 Ramadan 1445"7$hijri->isoFormat('LLLL'); // "Monday, Ramadan 1, 1445 9:30 AM"The result is a Pharaonic\Hijri\Hijri instance. Its year, month and day hold the Hijri values, while the time and weekday stay the same as the original date:
1$hijri->year; // 14452$hijri->month; // 93$hijri->day; // 14$hijri->monthName; // "Ramadan"5$hijri->dayName; // "Monday"Go the other way with fromHijri(), which returns a regular Gregorian Carbon instance:
1Carbon::fromHijri(1445, 10, 1)->toDateString(); // "2024-04-10"Or parse a Hijri date string:
1Carbon::parseHijri('1445-09-01 20:15')->toDateTimeString(); // "2024-03-11 20:15:00"To get today's Hijri date:
1Carbon::now()->toHijri()->isoFormat('D MMMM YYYY');Gregorian to Hijri
There are three ways to turn a Gregorian date into a Hijri instance. All of them return a Pharaonic\Hijri\Hijri object.
From a Carbon Instance
toHijri() is added to Carbon by the mixin. It keeps the time and timezone of the original date:
1use Carbon\Carbon;2 3Carbon::parse('2024-03-11 09:30:00')->toHijri()->format('Y-m-d H:i'); // "1445-09-01 09:30"Pass an adjustment to override the global one for this call only:
1Carbon::parse('2024-03-11')->toHijri(0)->format('Y-m-d'); // "1445-09-02"From a String or DateTime
Hijri::parse() accepts anything Carbon::parse() accepts (a string, a DateTimeInterface, or null for now) plus an optional timezone. It doesn't need the mixin:
1use Pharaonic\Hijri\Hijri;2 3Hijri::parse('2024-03-11')->format('Y-m-d'); // "1445-09-01"4Hijri::parse()->isoFormat('D MMMM YYYY'); // today, in HijriWith a Timezone and Adjustment
Hijri::fromGregorian() is the full form of parse(), with a third $adjustment argument:
1Hijri::fromGregorian('2024-03-11', 'Asia/Riyadh', 0)->format('Y-m-d e');2// "1445-09-02 Asia/Riyadh"The conversion uses the calendar date in the instance's timezone. Late at night a UTC date can be a different day than in your users' timezone, so convert dates in the timezone you display them in.
Hijri to Gregorian
These methods take Hijri values and return a regular Gregorian Carbon\Carbon instance, ready to store, compare or do math with.
From Hijri Parts
fromHijri(int $year, int $month, int $day, $tz = null, ?int $adjustment = null) builds the date at midnight:
1use Carbon\Carbon;2 3Carbon::fromHijri(1445, 9, 1)->toDateString(); // "2024-03-11" (1 Ramadan 1445)4Carbon::fromHijri(1445, 10, 1)->toDateString(); // "2024-04-10" (1 Shawwal 1445)5Carbon::fromHijri(1446, 1, 1)->toDateString(); // "2024-07-08" (1 Muharram 1446)From a Hijri String
parseHijri(string $date, $tz = null, ?int $adjustment = null) accepts YYYY-MM-DD, optionally followed by a space or T and an HH:MM or HH:MM:SS time:
1Carbon::parseHijri('1445-09-01')->toDateTimeString(); // "2024-03-11 00:00:00"2Carbon::parseHijri('1445-09-01 20:15')->toDateTimeString(); // "2024-03-11 20:15:00"3Carbon::parseHijri('1413-08-08 19:30:45')->toDateTimeString(); // "1993-02-01 19:30:45"4 5Carbon::parseHijri('1445-12-10', 'Asia/Riyadh')->format('Y-m-d e');6// "2024-06-17 Asia/Riyadh"Both methods are also available statically on the Hijri class without the mixin: Hijri::fromHijri(...) and Hijri::parseHijri(...). They still return Carbon\Carbon.
Invalid Input
Both methods throw Pharaonic\Hijri\Exception\InvalidHijriDateException (an InvalidArgumentException) when the input can't be a Hijri date:
1use Pharaonic\Hijri\Exception\InvalidHijriDateException; 2 3try { 4 Carbon::fromHijri(1445, 2, 30); // Safar has 29 days 5} catch (InvalidHijriDateException $e) { 6 $e->getMessage(); // "Invalid Hijri date: 1445-02-30." 7} 8 9Carbon::parseHijri('11/03/1445');10// InvalidHijriDateException: Hijri date must use YYYY-MM-DD with an optional HH:MM[:SS] time part.Formatting & Locales
Hijri overrides Carbon's month translations, so format(), isoFormat() and monthName print Hijri month names instead of Gregorian ones.
Month Names
Any locale starting with ar uses the Arabic names. Every other locale uses the English transliteration:
| # | Arabic | English |
|---|---|---|
| 1 | مُحرَّم | Muharram |
| 2 | صفَر | Safar |
| 3 | ربيع الأول | Rabi' Al-Awwal |
| 4 | ربيع الآخر | Rabi' Al-Akher |
| 5 | جمادى الأول | Jumada Al-Awwal |
| 6 | جمادى الآخرة | Jumada Al-Akherah |
| 7 | رَجب | Rajab |
| 8 | شَعبان | Sha'aban |
| 9 | رَمضان | Ramadan |
| 10 | شوّال | Shawwal |
| 11 | ذو القِعدة | Dhu Al-Qi'dah |
| 12 | ذو الحِجّة | Dhu Al-Hijjah |
Short month formats (M, MMM) print the full name, since Hijri months have no standard abbreviations.
Per-Date Locale
Call locale() on the converted date:
1$hijri = Carbon::parse('2024-03-11')->toHijri();2 3$hijri->locale('ar')->isoFormat('dddd D MMMM YYYY'); // "الاثنين 1 رَمضان 1445"4$hijri->locale('fr')->isoFormat('dddd D MMMM YYYY'); // "lundi 1 Ramadan 1445"5$hijri->locale('en')->isoFormat('dddd D MMMM YYYY'); // "Monday 1 Ramadan 1445"Weekday names come from Carbon's own translations for the locale, so they're always localized.
Global Locale
New Hijri instances take Carbon's global locale:
1Carbon::setLocale('ar');2 3Carbon::parse('2024-03-11')->toHijri()->isoFormat('LL'); // "1 رَمضان 1445"format() vs isoFormat()
Both are supported. format() replaces English day and month names in the output with the localized Hijri ones, so l, D, F and M work as expected:
1$hijri->format('l, j F Y'); // "Monday, 1 Ramadan 1445"2$hijri->format('D, M j'); // "Mon, Ramadan 1"Prefer isoFormat() for non-English output. It reads names straight from the translator, while format() relies on replacing the English names after formatting.
Hijri Calendar
Pharaonic\Hijri\Calendar\HijriCalendar exposes the rules of the tabular Hijri calendar the package uses. Use it to validate user input before converting.
1use Pharaonic\Hijri\Calendar\HijriCalendar; 2 3HijriCalendar::isLeapYear(1445); // true 4HijriCalendar::daysInMonth(1445, 9); // 30 5HijriCalendar::daysInMonth(1445, 12); // 30 (leap year) 6HijriCalendar::daysInMonth(1446, 12); // 29 7HijriCalendar::daysInMonth(1445, 13); // 0 (invalid month) 8 9HijriCalendar::isValidDate(1445, 9, 1); // true10HijriCalendar::isValidDate(1445, 2, 30); // false11HijriCalendar::isValidDate(1445, 13, 1); // falseCalendar Rules
- Odd months (Muharram, Rabi' Al-Awwal, …) have 30 days; even months have 29.
- Dhu Al-Hijjah (month 12) has 30 days in a leap year and 29 otherwise.
- A year is a leap year when
(11 × year + 14) mod 30 < 11, giving 11 leap years in every 30-year cycle. - Years start at 1; year
0and negative years are invalid.
These are the arithmetic rules, not the result of moon sighting. Use the day adjustment to match a local calendar.
API Reference
HijriCarbon Methods
Added to Carbon\Carbon by Carbon::mixin(HijriCarbon::class), and available on Pharaonic\Hijri\Hijri directly.
| Method | Description | Returns |
|---|---|---|
toHijri(?int $adjustment = null) | Convert this Gregorian date to Hijri. | Hijri |
fromHijri(int $year, int $month, int $day, $tz = null, ?int $adjustment = null) | Static. Build a Gregorian date at midnight from Hijri parts. | Carbon |
parseHijri(string $date, $tz = null, ?int $adjustment = null) | Static. Parse a YYYY-MM-DD[ HH:MM[:SS]] Hijri string into a Gregorian date. | Carbon |
setHijriAdjustment(int $days) | Set the global day adjustment. | void |
getHijriAdjustment() | Get the global day adjustment (default -1). | int |
Hijri Class
Pharaonic\Hijri\Hijri extends Carbon\Carbon, so every Carbon method is available too.
| Method | Description | Returns |
|---|---|---|
Hijri::parse($time = null, $tz = null) | Convert any Carbon-supported input to Hijri, using the global adjustment. | Hijri |
Hijri::fromGregorian($time = null, $tz = null, ?int $adjustment = null) | Same as parse(), with a per-call adjustment. | Hijri |
Hijri::getInstance() | The shared instance used to read and set the global adjustment. | Hijri |
locale(?string $locale = null, ...$fallbackLocales) | Set the locale and switch month names between Arabic and English. Returns the locale when called with no argument. | Hijri | string |
format($format) | Carbon's format() with localized Hijri month and day names. | string |
getTranslatedDayName($context = null, $keySuffix = '', $defaultValue = null) | Localized weekday name of the original date. | string |
Hijri Properties
The usual Carbon properties, read with Hijri values:
| Property | Description | Example |
|---|---|---|
year | Hijri year | 1445 |
month | Hijri month (1–12) | 9 |
day | Hijri day of month | 1 |
monthName | Localized Hijri month name | "Ramadan" |
dayName | Localized weekday name | "Monday" |
hour, minute, second | Unchanged from the source date | 9 |
HijriCalendar
All methods are static on Pharaonic\Hijri\Calendar\HijriCalendar.
| Method | Description | Returns |
|---|---|---|
isLeapYear(int $year) | Whether the Hijri year has 355 days. | bool |
daysInMonth(int $year, int $month) | 29 or 30, or 0 for a month outside 1–12. | int |
isValidDate(int $year, int $month, int $day) | Whether the Hijri date exists. | bool |
Exceptions
| Class | Thrown by | When |
|---|---|---|
Pharaonic\Hijri\Exception\InvalidHijriDateException | fromHijri(), parseHijri() | The Hijri date doesn't exist, or the string isn't YYYY-MM-DD[ HH:MM[:SS]]. |
Real-World Examples
1. Ramadan Banner
Show a banner only while it's Ramadan (month 9):
1use Carbon\Carbon;2 3$today = Carbon::now()->toHijri();4 5if ($today->month === 9) {6 echo "Ramadan Kareem! Day {$today->day} of Ramadan {$today->year}.";7}2. Countdown to Eid al-Fitr
Eid al-Fitr is 1 Shawwal (month 10). Convert it back to Gregorian and let Carbon count the days:
1use Carbon\Carbon; 2 3function daysUntilEidAlFitr(Carbon $today): int 4{ 5 $hijri = $today->toHijri(); 6 $year = $hijri->month >= 10 ? $hijri->year + 1 : $hijri->year; 7 8 return $today->copy()->startOfDay()->diffInDays(Carbon::fromHijri($year, 10, 1)); 9}10 11daysUntilEidAlFitr(Carbon::parse('2024-03-20')); // 213. Validating a Hijri Date From a Form
Check the parts with HijriCalendar before converting, then store the Gregorian date:
1use Carbon\Carbon; 2use Pharaonic\Hijri\Calendar\HijriCalendar; 3 4$year = (int) $_POST['hijri_year']; 5$month = (int) $_POST['hijri_month']; 6$day = (int) $_POST['hijri_day']; 7 8if (! HijriCalendar::isValidDate($year, $month, $day)) { 9 $errors[] = sprintf('That month only has %d days.', HijriCalendar::daysInMonth($year, $month));10} else {11 $birthDate = Carbon::fromHijri($year, $month, $day)->toDateString(); // store as DATE12}4. Bilingual Date Line
Print the same Hijri date in Arabic and English side by side. copy() keeps the original instance's locale untouched:
1$hijri = Carbon::parse('2024-03-11')->toHijri();2 3echo $hijri->copy()->locale('ar')->isoFormat('D MMMM YYYY'); // "1 رَمضان 1445"4echo ' / ';5echo $hijri->isoFormat('D MMMM YYYY'); // "1 Ramadan 1445"5. Dual-Calendar Date Helper
A small helper class that formats a stored Gregorian timestamp in both calendars, used from a plain PHP template. The Gregorian side is numeric on purpose (see Troubleshooting):
1<?php 2 3namespace App; 4 5use Carbon\Carbon; 6use Pharaonic\Hijri\HijriCarbon; 7 8Carbon::mixin(HijriCarbon::class); 9 10final class DualDate11{12 public function __construct(13 private Carbon $date,14 private string $locale = 'en',15 ) {16 }17 18 public function gregorian(): string19 {20 return $this->date->format('d/m/Y');21 }22 23 public function hijri(): string24 {25 return $this->date->toHijri()->locale($this->locale)->isoFormat('D MMMM YYYY');26 }27}1<?php $date = new App\DualDate(Carbon\Carbon::parse($article['published_at']), 'ar'); ?>2 3<time datetime="<?= $article['published_at'] ?>">4 <?= $date->hijri() ?> — <?= $date->gregorian() ?>5</time>Troubleshooting
Call to undefined method Carbon\Carbon::toHijri()
Cause: the mixin isn't registered, or it was registered after the call.
Fix: call Carbon::mixin(HijriCarbon::class) once at boot, before any conversion. If you can't control boot order, use Hijri::parse($date) instead, which works without the mixin.
The Hijri date is one day off
Cause: the package uses the tabular Islamic calendar with a default adjustment of -1. Local calendars based on moon sighting (or Umm al-Qura) can differ by a day or two.
Fix: change the global adjustment at boot with Hijri::getInstance()->setHijriAdjustment(0) (or another value), or pass an adjustment to a single call: $date->toHijri(0). See Day Adjustment.
The date is right in one timezone and wrong in another
Cause: the conversion uses the calendar day of the instance's timezone. A UTC timestamp just before midnight is already the next day in Riyadh.
Fix: set the timezone before converting: $date->setTimezone('Asia/Riyadh')->toHijri(), or Hijri::fromGregorian($value, 'Asia/Riyadh').
Month names are in English instead of Arabic
Cause: Arabic names are used only when the locale starts with ar.
Fix: call ->locale('ar') on the Hijri instance, or Carbon::setLocale('ar') before converting.
Date math gives strange results on a Hijri instance
Cause: a Hijri instance is a Carbon object whose year, month and day hold Hijri numbers. Carbon still applies Gregorian rules to it, so addDays(), diffInDays(), diffForHumans() and comparisons don't follow the Hijri calendar, and calling toHijri() on it converts it a second time.
Fix: do all math and comparisons on the Gregorian Carbon date, and call toHijri() only for display. To start from Hijri values, convert them with Carbon::fromHijri() first.
Don't store a Hijri instance's format('Y-m-d') in a DATE column. Store the Gregorian date and convert when you display it.
Gregorian dates show Hijri month names
Cause: Hijri sets its month names on Carbon's shared translator. After the first conversion in a process, ordinary Gregorian dates that print month names (F, M, MMMM, monthName) show Hijri names too, e.g. "Rabi' Al-Awwal" instead of "March".
Fix: until this is fixed in the package, print Gregorian dates with numeric formats (d/m/Y, toDateString()) in requests that also convert to Hijri.
InvalidHijriDateException when parsing
Cause: parseHijri() accepts only YYYY-MM-DD with an optional HH:MM or HH:MM:SS time, and fromHijri() rejects days that don't exist (for example 30 Safar).
Fix: normalize the input to YYYY-MM-DD first, and check the parts with HijriCalendar::isValidDate().
Contributors
Amazing people who made this package possible
This package wouldn't exist without the contributions of our amazing community. A huge thank you to everyone who has helped improve Hijri!
Want to Contribute?
We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.