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
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:
- GitHub: https://github.com/hpakdaman/multicarbon
- Packagist: https://packagist.org/packages/hpakdaman/multicarbon
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)