PHP Packagev8.6.0MIT License

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.

Quick Tip

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

Terminal
composer require pharaonic/php-hijri

Register 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:

bootstrap.php
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.

Without the Mixin

You can skip the mixin and call the Hijri class directly: Hijri::parse(), Hijri::fromGregorian(), Hijri::fromHijri() and Hijri::parseHijri() work on their own.

Installation Complete

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:

index.php
1use Pharaonic\Hijri\Hijri;
2 
3Hijri::getInstance()->getHijriAdjustment(); // -1

Change It Globally

Set it once at boot. Every later conversion that doesn't pass its own adjustment uses this value:

bootstrap.php
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(); // 0

Override It Per Call

Every conversion method accepts an $adjustment argument. It affects that call only and never changes the global value:

index.php
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"
Shared State

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:

index.php
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; // 1445
2$hijri->month; // 9
3$hijri->day; // 1
4$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:

index.php
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:

index.php
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 Hijri

With 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"
Timezones Matter

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:

index.php
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:

index.php
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:

index.php
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:

#ArabicEnglish
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:

index.php
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:

bootstrap.php
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"
Tip

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.

index.php
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); // true
10HijriCalendar::isValidDate(1445, 2, 30); // false
11HijriCalendar::isValidDate(1445, 13, 1); // false

Calendar 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 0 and negative years are invalid.
Observed vs Tabular

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.

MethodDescriptionReturns
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.

MethodDescriptionReturns
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:

PropertyDescriptionExample
yearHijri year1445
monthHijri month (1–12)9
dayHijri day of month1
monthNameLocalized Hijri month name"Ramadan"
dayNameLocalized weekday name"Monday"
hour, minute, secondUnchanged from the source date9

HijriCalendar

All methods are static on Pharaonic\Hijri\Calendar\HijriCalendar.

MethodDescriptionReturns
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

ClassThrown byWhen
Pharaonic\Hijri\Exception\InvalidHijriDateExceptionfromHijri(), 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):

index.php
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:

src/EidCountdown.php
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')); // 21

3. Validating a Hijri Date From a Form

Check the parts with HijriCalendar before converting, then store the Gregorian date:

src/BirthDateInput.php
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 DATE
12}

4. Bilingual Date Line

Print the same Hijri date in Arabic and English side by side. copy() keeps the original instance's locale untouched:

index.php
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):

src/DualDate.php
1<?php
2 
3namespace App;
4 
5use Carbon\Carbon;
6use Pharaonic\Hijri\HijriCarbon;
7 
8Carbon::mixin(HijriCarbon::class);
9 
10final class DualDate
11{
12 public function __construct(
13 private Carbon $date,
14 private string $locale = 'en',
15 ) {
16 }
17 
18 public function gregorian(): string
19 {
20 return $this->date->format('d/m/Y');
21 }
22 
23 public function hijri(): string
24 {
25 return $this->date->toHijri()->locale($this->locale)->isoFormat('D MMMM YYYY');
26 }
27}
templates/article.php
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!

Moamen Eltouny

@MoamenEltouny

65 contributions

Moemen Gaballah

@Moemen-Gaballah

2 contributions

Want to Contribute?

We welcome contributions from everyone! Whether it's bug fixes, new features, or documentation improvements - every contribution counts.