DEV Community

Cover image for MultiCarbon: Native Jalali & Hijri Calendar Support for PHP Carbon
hamed pakdaman
hamed pakdaman

Posted on

MultiCarbon: Native Jalali & Hijri Calendar Support for PHP Carbon

If you've ever worked on a project targeting users in Iran, Afghanistan, or Arabic-speaking countries, you know the pain of converting dates between Jalali (Solar Hijri), Hijri
(Islamic Lunar), and Gregorian calendars.

I built MultiCarbon to solve this once and for all — not as a wrapper, but as a direct extension of nesbot/carbon. Every Carbon method you already know works seamlessly in any
calendar mode.

## Install

  composer require hpakdaman/multicarbon
Enter fullscreen mode Exit fullscreen mode

Requires PHP 8.1+ and Carbon 3.


The Basics — One Timestamp, Three Calendars

The core idea is simple: the underlying timestamp never changes. You just switch the presentation layer.

use MultiCarbon\MultiCarbon;

$date = new MultiCarbon('2025-03-21');

echo $date->jalali()->format('l j F Y');
// جمعه 1 فروردین 1404

echo $date->hijri()->format('l j F Y');
// الجمعة 21 رمضان 1446

echo $date->gregorian()->format('l j F Y');
// Friday 21 March 2025

That's it. Same object, three calendars, fully fluent.


Create Dates Directly in Any Calendar

No need to mentally convert. Just think in the calendar you need:

// Nowruz (Persian New Year)
$nowruz = MultiCarbon::createJalali(1404, 1, 1);
echo $nowruz->gregorian()->format('Y-m-d'); // 2025-03-21

// First day of Ramadan
$ramadan = MultiCarbon::createHijri(1446, 9, 1);
echo $ramadan->gregorian()->format('Y-m-d'); // 2025-03-01


Calendar-Aware Arithmetic

This is where it gets interesting. Adding a month in Jalali isn't the same as adding a month in Gregorian — month lengths differ. MultiCarbon handles this automatically:

// Shahrivar has 31 days, Mehr has 30
$date = MultiCarbon::createJalali(1404, 6, 31);
$date->addMonth();
echo $date->format('Y/m/d'); // 1404/07/30 — clamped to Mehr's max

// Leap year handling
$date = MultiCarbon::createJalali(1403, 12, 30); // Esfand 30 (1403 is leap)
$date->addYear();
echo $date->format('Y/m/d'); // 1404/12/29 — clamped (1404 is not leap)


Localized Names — Persian & Arabic

Month names, weekday names, and even AM/PM are fully localized:

$date = MultiCarbon::createJalali(1404, 3, 15);

echo $date->jalali()->monthName; // خرداد
echo $date->jalali()->dayName; // پنجشنبه

echo $date->hijri()->monthName; // ذیحجه
echo $date->hijri()->dayName; // الخمیس


Farsi, Arabic & Latin Digits

Switch the digit system globally with one line:

MultiCarbon::setDigitsType(MultiCarbon::DIGITS_FARSI);
echo MultiCarbon::createJalali(1404, 1, 1)->format('Y/m/d');
// ۱۴۰۴/۰۱/۰۱

MultiCarbon::setDigitsType(MultiCarbon::DIGITS_ARABIC);
echo MultiCarbon::createHijri(1446, 9, 1)->format('Y/m/d');
// ١٤٤٦/٠٩/٠١

MultiCarbon::setDigitsType(MultiCarbon::DIGITS_LATIN); // reset


diffForHumans() in Persian & Arabic

echo MultiCarbon::createJalali(1403, 1, 1)->diffForHumans();
// 1 سال پیش

echo MultiCarbon::createHijri(1445, 1, 1)->diffForHumans();
// منذ 1 سنة


Calendar-Aware Boundaries

Start/end of month and year respect the active calendar:

$date = MultiCarbon::createJalali(1404, 6, 15, 14, 30, 0);

echo $date->copy()->startOfMonth()->format('Y/m/d H:i:s');
// 1404/06/01 00:00:00

echo $date->copy()->endOfMonth()->format('Y/m/d H:i:s');
// 1404/06/31 23:59:59

echo $date->copy()->endOfYear()->format('Y/m/d H:i:s');
// 1404/12/29 23:59:59 (not leap)


Leap Year Detection

MultiCarbon::createJalali(1403, 1, 1)->isLeapYear(); // true
MultiCarbon::createJalali(1404, 1, 1)->isLeapYear(); // false


Comparisons & Diff

All comparison methods work in the active calendar:

$a = MultiCarbon::createJalali(1404, 1, 1);
$b = MultiCarbon::createJalali(1404, 1, 25);

$a->isSameMonth($b); // true
$a->isSameDay($b); // false
$a->lessThan($b); // true
$a->diffInDays($b); // 24


Convert from Carbon

Already using Carbon in your project? Convert seamlessly:

$carbon = \Carbon\Carbon::parse('2025-03-21');
$mc = MultiCarbon::fromCarbon($carbon);

echo $mc->jalali()->format('Y/m/d'); // 1404/01/01
echo $mc->hijri()->format('Y/m/d'); // 1446/09/21


Calendar Properties

Access all date components in the active calendar:

$date = MultiCarbon::createJalali(1404, 6, 15);

$date->year; // 1404
$date->month; // 6
$date->day; // 15
$date->dayOfYear; // 170
$date->daysInMonth; // 31
$date->quarter; // 2
$date->weekOfYear; // 36
$date->isWeekend(); // false (Iranian week: Fri is weekend)


Serialization

$date = MultiCarbon::createJalali(1404, 7, 10, 8, 30, 0);

$date->toDateString(); // "1404-07-10"
$date->toArray();
// ['year' => 1404, 'month' => 7, 'day' => 10, 'hour' => 8, 'minute' => 30, 'second' => 0]

echo $date; // "1404/07/10 08:30:00"


Laravel Integration

MultiCarbon ships with a Laravel service provider, facade, and Blade directives out of the box:

// Global helpers
jdate('Y/m/d H:i:s'); // Current Jalali date
hdate('Y/m/d'); // Current Hijri date

// Blade directives
@jdate('Y/m/d H:i:s') // Current Jalali
@hdate('Y/m/d') // Current Hijri
@jalali($user->created_at, 'Y/m/d') // Convert to Jalali
@hijri($post->published_at, 'Y/m/d') // Convert to Hijri


How It Works Under the Hood

MultiCarbon uses debug_backtrace() to detect whether a property or method is accessed by your code or by Carbon's internal engine. This means:

  • When you call $date->year → returns the Jalali/Hijri year
  • When Carbon internally calls $this->year → returns Gregorian so parent logic doesn't break

Links:


I'd love to hear your feedback, suggestions, or feature requests. Feel free to open an issue or drop a comment below!

Top comments (0)