rizqshadi / hijri-date
Hijri (Islamic) calendar for PHP: today's Hijri date by location, Gregorian <-> Hijri conversion (Umm al-Qura & tabular civil), and a Ramadan countdown.
Requires
- php: >=8.1
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
- ext-intl: Required for the Umm al-Qura calendar (the default). Without it, use CalendarType::Civil.
Provides
None
Conflicts
None
Replaces
None
README
A Hijri (Islamic) calendar library for PHP 8.1+.
- Today's Hijri date based on where the user is (timezone, coordinates, or country), with an optional day starts at sunset (maghrib) mode.
- Gregorian ↔ Hijri conversion. Umm al-Qura (Saudi Arabia's official calendar) is the default, and the tabular civil calendar is available in pure PHP.
- Ramadan countdown in Hijri months and days, e.g. "4 months and 16 days until Ramadan 1448" / "باقي 4 أشهر و16 يومًا على رمضان 1448".
- English and Arabic month and day names, Arabic-Indic digits, right-to-left display helpers, JSON serialization, and immutable value objects throughout.
Requirements
- PHP 8.1 or newer
- ext-intl for the Umm al-Qura calendar (recommended). Without it, you can still use the civil calendar.
Installation
composer require rizqshadi/hijri-date
About ext-intl
| Calendar | CalendarType |
Needs |
|---|---|---|
| Umm al-Qura (default) | CalendarType::UmmAlQura |
ext-intl (ICU) |
| Tabular / civil | CalendarType::Civil |
nothing, pure PHP |
If you request Umm al-Qura without intl, an IntlNotAvailableException explains how to install it. The library never silently falls back to the civil calendar, because the two can differ by a day or two.
# Debian/Ubuntu sudo apt install php-intl # Windows: enable it in php.ini extension=intl
Quick start
use RizqShadi\HijriDate\Hijri; use RizqShadi\HijriDate\Location; $hijri = new Hijri(Location::fromTimezone('Asia/Riyadh')); echo $hijri->today()->format('j F Y'); // e.g. 15 Rabi' al-Thani 1448 echo $hijri->today()->format('j F Y', 'ar'); // e.g. 15 ربيع الآخر 1448 echo $hijri->untilRamadan()->toString('en'); // e.g. 4 months and 16 days until Ramadan 1448
Usage
Location
"Today" depends on the local date: 1 am in Riyadh is still the previous day in New York.
use RizqShadi\HijriDate\Location; Location::fromTimezone('Asia/Riyadh'); // lat/lng + country from the tz database Location::fromCoordinates(21.4225, 39.8262); // nearest timezone → Asia/Riyadh Location::fromCoordinates(21.4225, 39.8262, 'Asia/Riyadh'); // explicit timezone Location::fromCountry('SA'); // the country's main timezone Location::default(); // PHP's default timezone
fromCoordinates()without a timezone picks the zone whose reference city is nearest. A few extra anchor cities (Mecca, Jeddah, Alexandria, Lahore, Delhi…) cover large zones. This is a heuristic, not a border lookup, so pass the timezone explicitly near borders.fromCountry()uses the main timezone of countries that have several (US→ America/New_York,RU→ Europe/Moscow,AU→ Australia/Sydney…). UsefromTimezone()when you know the exact zone.
Today's date
$hijri = new Hijri(Location::fromTimezone('Asia/Riyadh')); $today = $hijri->today(); $today->year; // 1448 $today->month; // 4 $today->day; // 15 $today->gregorian; // DateTimeImmutable 2026-09-26 00:00 Asia/Riyadh (string) $today; // "1448-04-15" // The Hijri date at a specific instant, as seen at the location: $hijri->at(new DateTimeImmutable('2026-09-26 22:30', new DateTimeZone('UTC'))); // 1448-04-16 in Riyadh
Gregorian → Hijri
$hijri->fromGregorian('2026-02-18'); // 1447-09-01 $hijri->fromGregorian('18/02/2026'); // day-first $hijri->fromGregorian('18-02-2026'); $hijri->fromGregorian('February 18, 2026'); $hijri->fromGregorian('tomorrow'); // anything DateTime understands $hijri->fromGregorian(new DateTime('2026-02-18')); $hijri->fromGregorian('2026-02-30'); // throws InvalidDateException (no silent rollover) $hijri->fromGregorian('0500-01-01'); // throws InvalidDateException (before the Hijri calendar began, July 622)
Hijri → Gregorian
$hijri->toGregorian(1446, 1, 1); // DateTimeImmutable 2024-07-07 00:00 (location timezone) $hijri->create(1447, 9, 1); // validated HijriDate $hijri->daysInMonth(1445, 8); // 29 $hijri->toGregorian(1445, 8, 30); // throws InvalidDateException (Sha'ban 1445 has 29 days) $hijri->create(1447, 13, 1); // throws InvalidDateException
Formatting
format($pattern, $locale = 'en', $arabicDigits = false, $rtl = false) accepts PHP date()-style tokens:
| Token | Meaning | Example |
|---|---|---|
j |
day | 1 |
d |
day, 2 digits | 01 |
n |
month | 9 |
m |
month, 2 digits | 09 |
F |
month name | Ramadan / رمضان |
Y |
year | 1447 |
y |
year, 2 digits | 47 |
l |
weekday name | Wednesday / الأربعاء |
Prefix a character with a backslash to print it literally.
$date = $hijri->fromGregorian('2026-02-18'); $date->format('l, j F Y'); // Wednesday, 1 Ramadan 1447 $date->format('d/m/Y'); // 01/09/1447 $date->format('l j F Y هـ', 'ar', true); // الأربعاء ١ رمضان ١٤٤٧ هـ $date->format('\Y\e\a\r: Y'); // Year: 1447 $date->monthName('ar'); // رمضان $date->dayName(); // Wednesday $date->isRamadan(); // true $date->toArray(); // ['year' => 1447, 'month' => 9, 'day' => 1, 'month_name' => 'Ramadan', ...] json_encode($date);
Ramadan countdown
$countdown = $hijri->untilRamadan(); // from today $countdown = $hijri->untilRamadan('2026-09-26'); // from a given date $countdown->months; // 4 $countdown->days; // 16 $countdown->totalDays; // 135 (real day difference) $countdown->ramadanStart; // HijriDate 1448-09-01 (->gregorian = 2027-02-08) $countdown->from; // HijriDate you counted from $countdown->isRamadanNow; // false $countdown->dayOfRamadan; // null (or 1-30 during Ramadan) echo $countdown->toString('en'); // 4 months and 16 days until Ramadan 1448 echo $countdown->toString('ar'); // باقي 4 أشهر و16 يومًا على رمضان 1448 json_encode($countdown);
- The months and days are Hijri months and days. The days are what is left of the current month, including today.
- During Ramadan (day 2 onward),
isRamadanNowistrue,dayOfRamadanis set, and the countdown targets next year's Ramadan. - On 1 Ramadan itself,
months = days = totalDays = 0,isRamadanNow = true, anddayOfRamadan = 1. - From Shawwal onward, it counts to next year's Ramadan.
- Arabic output uses the correct plural forms: شهر واحد، شهران، 3–10 أشهر، 11+ شهرًا (and likewise for days).
Arabic and right-to-left display
Arabic output is plain text in logical order. It displays correctly in any right-to-left context, such as an HTML element with dir="rtl":
<p dir="rtl" lang="ar"><?= htmlspecialchars($date->format('l j F Y هـ', 'ar', true)) ?></p>
When Arabic is mixed into left-to-right text (English sentences, logs, LTR UIs), the Unicode bidi algorithm can reorder the surrounding numbers. For example, (1 رمضان 1447) displays as (1447 رمضان 1). Pass rtl: true to wrap Arabic output in an invisible right-to-left isolate (U+2067 … U+2069):
echo "Today: " . $date->format('j F Y', 'ar', rtl: true); // renders correctly inside LTR text echo $countdown->toString('ar', arabicDigits: true, rtl: true); // باقي ٤ أشهر و١٦ يومًا على رمضان ١٤٤٨ use RizqShadi\HijriDate\Rtl; Rtl::isolate($anyArabicText); Rtl::strip($text); // remove bidi control characters before storing or comparing
rtl is off by default so that stored and compared strings contain no invisible characters. It only affects 'ar' output.
Terminals / CLI
Most terminals (the VS Code terminal, Windows Terminal, the Windows console) support neither right-to-left text nor Arabic letter joining. They draw characters left to right in stored order, so Arabic appears reversed with unjoined letters. Rtl::visual() prepares text for them. It joins the letters into their connected forms (including لا) and reorders each line with the Unicode Bidirectional Algorithm, keeping numbers left to right and honouring rtl: true isolates:
use RizqShadi\HijriDate\Rtl; echo Rtl::visual($countdown->toString('ar', arabicDigits: true, rtl: true)) . PHP_EOL; // or for a whole CLI script: ob_start(fn (string $out) => Rtl::visual($out));
Use Rtl::visual() only for such displays. Never use it for HTML, databases or APIs. A bidi-aware display (browser, GNOME Terminal, Konsole) reverses the text itself and would show it backwards.
Calendar choice
use RizqShadi\HijriDate\CalendarType; $civil = new Hijri($location, CalendarType::Civil); // no ext-intl required $hijri = $hijri->withCalendar(CalendarType::Civil);
Moon-sighting adjustment
Local announcements can differ from Umm al-Qura by a day. Shift the calendar by -2 to +2 days:
$hijri = new Hijri($location, adjustment: 1); // or $hijri->withAdjustment(-1) $hijri->fromGregorian('2026-02-18'); // 1447-09-02 instead of 1447-09-01 $hijri->toGregorian(1447, 9, 1); // 2026-02-17
Day starts at sunset
The Islamic day begins at maghrib. With this option, the Hijri date advances once local time passes sunset. Sunset is computed with PHP's built-in date_sun_info() from the location's coordinates:
$hijri = new Hijri(Location::fromCoordinates(21.4225, 39.8262), dayStartsAtSunset: true); // or $hijri->withDayStartsAtSunset(); $hijri->at(new DateTimeImmutable('2026-09-26 12:00', new DateTimeZone('Asia/Riyadh'))); // 1448-04-15 $hijri->at(new DateTimeImmutable('2026-09-26 20:00', new DateTimeZone('Asia/Riyadh'))); // 1448-04-16
- The location needs coordinates, or a
LocationExceptionis thrown (e.g.Location::fromTimezone('UTC')has none). - The result doesn't depend on your server's default timezone.
- At high latitudes in summer, sunset can come after midnight. The date then changes at that sunset.
- During polar day or polar night there is no sunset, so the day changes at midnight.
fromGregorian()andtoGregorian()work with calendar days, so sunset does not affect them.
Immutability
Hijri, Location, HijriDate and RamadanCountdown are immutable. The with*() methods return new instances: withLocation(), withCalendar(), withAdjustment(), withDayStartsAtSunset().
Exceptions
Every exception implements RizqShadi\HijriDate\Exception\HijriDateException, so one catch handles them all:
InvalidDateException(extendsInvalidArgumentException): a bad Gregorian or Hijri date, or one before the Hijri calendar began.LocationException(extendsInvalidArgumentException): an unknown timezone or country, invalid coordinates, or missing coordinates for sunset mode.InvalidArgumentException(extends PHP'sInvalidArgumentException): an unsupported locale or an out-of-range adjustment.IntlNotAvailableException(extendsRuntimeException): Umm al-Qura was requested without ext-intl.
Low-level converters
use RizqShadi\HijriDate\Converter\IntlConverter; use RizqShadi\HijriDate\Converter\TabularConverter; (new TabularConverter())->toHijri(2026, 2, 18); // [1447, 9, 1] (new IntlConverter('islamic-umalqura'))->toGregorian(1446, 1, 1); // [2024, 7, 7]
Conversions are done at 12:00 UTC, so DST and midnight edge cases cannot shift the result. The converters don't validate their input; Hijri does.
Example
A runnable example prints today's date in Riyadh (English and Arabic), both conversions and the Ramadan countdown:
php vendor/rizqshadi/hijri-date/examples/demo.php
On a terminal it passes its output through Rtl::visual(). If your terminal handles right-to-left text itself, set HIJRI_TERMINAL_BIDI=1.
Testing
From a clone of the repository:
composer install
composer test
The tests check known Umm al-Qura dates against ICU and round-trip conversions over 20 years. They also compare the pure-PHP civil calendar with ICU's islamic-civil day by day over 40 years, and cover timezones, sunset rollover, adjustments and every Ramadan countdown edge case.
Support
Found a bug or have a question? Open an issue at github.com/ShadiRq/hijri-date.
License
MIT © Shadi Rizq. See LICENSE.