All guides
No guide matches your search.
Upgrade guide
Changes that can affect your code before 1.0, with before and after examples and the steps to migrate, plus the highlights of the changelog for the 0.x series.
On this page
Before 1.0.0, a minor release may contain breaking changes. Each one is listed here with the steps to migrate. The policy is on API stability. This page starts with the newest version. The last section covers the early pre-release builds, from before 0.1.0.
From 0.2.1 to 0.3.0
The changes below can break code that worked before. Most of them turn a guessed or wrong result into an exception or an invalid Result. Each item gives the old behaviour, the new behaviour and what to do.
Dates from text
- Text that only looks like a date.
make()onJalali,HijriandHebrewreads the exact shapesY/m/dandY-m-d(with an optional time and zone designator) as a date of the calendar. Before, text that started like such a date but had more or less was handed to PHP and read as a Gregorian date, which gave wrong results. Now it throwsInvalidDateException. Examples are'1403-01-01 10:00 UTC', a time withPM, one-digit minutes and'1403-01'. What to do: pass aDateTimeImmutablefor such values, or cut the text to one of the accepted shapes. - Bare digits. Before, a run of 3 to 8 digits such as
'1403'or'14030101'was read by PHP as a clock time. Now it throwsInvalidDateException. Eight digits that start with a year of another calendar, such as'20240101', are still read as a Gregorian date. A 5-digit year in a Jalali or Hijri string throws with the codedate_out_of_range. What to do: usecreate()or aDateTimeImmutable. - Impossible Gregorian days.
make()on the three calendar classes, andJalaliCast, throwInvalidDateExceptionfor a Gregorian string with a day that does not exist. Text with a negative year such as'-0100/01/01'throws a clearInvalidDateExceptiontoo. For negative years, usecreate()or a timestamp.
| Call | Before | After |
|---|---|---|
Jalali::make('2024-02-30') | the date of 2024-03-01 | InvalidDateException |
createFromFormat()tokens.Jalali::createFromFormat()rejects the tokensz y F M D l S e T P O p u vwithInvalidDateException, because they have a Gregorian or time-zone meaning and gave wrong dates. The formatUalone reads a Unix timestamp. What to do: use the tokensd j m n Y H G i s g h A a, and pass the time zone as the third argument.- End of a period.
endOfDay(),endOfMonth()andendOfYear()end at 23:59:59.999999 and not at 23:59:59.000000. A format withH:i:sprints the same text. A strict comparison with a value built at23:59:59can now give another result. What to do: compare against the new end value, or usestartOfDay()of the next day. - Year tokens in
format(). The tokenYwrites at least 4 digits, with a minus sign for negative years. The tokenyuses floor modulo.
| Call | Before | After |
|---|---|---|
Jalali::create(-5, 1, 1)->format('Y') | -005 | -0005 |
Jalali::create(-620, 1, 1)->format('y') | -20 | 80 |
Eloquent cast
- Unreadable stored values. When the model reads the column,
JalaliCastthrowsInvalidDateExceptionif the stored value is an array, a boolean, a float or an object. Before, it returnednull.nulland an empty string still givenull. What to do: fix the column type, or catch the exception where you read the attribute.
Validators
- Allowed characters.
Mobile,PostalCodeandBankCardaccept only digits plus spaces, no-break spaces, half-spaces (ZWNJ), LRM and RLM marks, hyphens, parentheses and dots.Mobilealso accepts one+at the start of a+98number. Everything else givesinvalid_format, andnormalize()returns an empty string. Before, these classes kept only the digits and dropped the rest. What to do: clean the input on your side if you want to allow more.
| Input | Before | After |
|---|---|---|
Mobile::validate('call 09121234567 now') | valid | invalid_format |
Mobile::validate('-9121234567') | valid | invalid_format |
- Tab, newline and NUL at the ends. These characters are no longer trimmed.
NationalCode,Sheba,VehiclePlate,Format::withSeparator()andNumberToWordsreject them. What to do: trim the input yourself when you want that. - Sheba check digits.
Shebagivesinvalid_checksumfor the check digits 00, 01 and 99, even when the modulo comes out right. The standard produces only 02 to 98. - Postal codes.
PostalCodegivesinvalid_formatfor a code with the digit 0 or 2 in its first five digits. The rule comes from three public sources. Iran Post publishes no official rule set. What to do: if you must accept codes that this rule rejects, check the shape yourself.
| Call | Before | After |
|---|---|---|
PostalCode::isValid('1234567890') | true | false |
PostalCode::isValid('1593715416') | true | true |
- Sheba bank names. The names for the codes 013 and 019 are now
بانک رفاه کارگرانandبانک صادرات ایران, the same as in the BIN table. If you compare the names as text, update the comparison.
Numbers and text
- Grouping marks.
NumberToWords::convert()andFormat::withSeparator()accept thousands separators only in proper grouping: a first group of 1 to 3 digits, then groups of exactly 3. What to do: remove stray spaces and commas from the input.
| Input | Before | After |
|---|---|---|
'1 234 567' | 1234567 | 1234567 |
'1,234,567' | 1234567 | 1234567 |
'12 34' | 1234 | InvalidNumberException (invalid_number) |
'1 2' | 12 | InvalidNumberException (invalid_number) |
- Locale names.
NumberToWordsaccepts a locale only when its language part is exactlyfaorar.arnandfarsiwere accepted by prefix. Now they throwUnsupportedLocaleException. - Invalid UTF-8.
Normalizer::normalize(),fixHalfSpace()andclean()replace bytes that are not valid UTF-8 with U+FFFD and then apply every step. Before, they applied only the byte steps to such text.Detectorignores invalid bytes and no longer returnsfalseorltrbecause of them. - Locale direction.
Detector::isRtlLocale()follows an explicit script subtag, sofa-Latn,ar-Latnandsd-Devagivefalse.Detector::direction()uses the same right-to-left scripts ascontainsRtl().
Prayer times
- Input ranges.
new PrayerTimes()throwsInvalidPrayerConfigExceptionforNaN, infinity, a latitude outside -90 to 90, a longitude outside -180 to 180 and an elevation beyond 20000 m in either direction.getTimes()andnextPrayer()throwInvalidDateExceptionwhen the local year is outside 1 to 9999. Before, they returned wrong times. - Missing times. A prayer is
nullwhen its local date would fall after 9999-12-31 or before 0001-01-01.nextPrayer()returnsnullwhen no prayer is left up to the end of 9999. Near the poles, a time that would break the order fajr, sunrise, dhuhr, asr, maghrib, isha isnulltoo. Asr after Isha at 89.9 degrees is an example. What to do: handlenullin your output, as you already do for polar days. - Rounding. For time zones whose offset includes seconds (mostly dates before 1970), times are rounded to the nearest minute and not cut. A time can differ by one minute from before.
Holidays
- Date strings.
HolidayCalendar::withHoliday()andwithoutHoliday()accept only a Jalali date string:Y/m/dorY-m-d, with a 3 or 4 digit year. Persian and Arabic digits are allowed. A year of 1700 or later, a time part and free text throwInvalidDateException. Before, a Gregorian-looking string was read as another day. A Hijri month start that contains a NUL byte throwsInvalidDateException.
| Call | Before | After |
|---|---|---|
withHoliday('2027/05/05', 'x') | another day than intended | InvalidDateException |
withHoliday('1406/02/03', 'x') | works | works |
- Laravel config. A
rtly-kit.holidaysvalue that is not an array throwsInvalidDateExceptionwhen the holiday calendar is resolved. Before, it was ignored. What to do: set the entry to an array, or remove it.
Also check
These changes are not breaking for most code, but they can show up in tests or stored values.
serialize()ofJalali,HijriandHebrewstores the moment (and the Hijri variant) as a small versioned array. Values serialized by earlier versions still load. If you compare or store the raw serialized string, you will see a different value.Hijri::make()andHebrew::make()with an instance of their own class now honour the time zone argument. Before, they ignored it.Hijri::make()takes?HijriVariant $variant = null. The valuenullkeeps the variant of a Hijri instance, and a variant you pass always applies.- Comparison methods and
between()use the whole instant, microseconds included.addMonths()andaddYears()keep the microseconds. - For the Tehran method with the
OneSeventhandNightMiddlerules at high latitudes, Maghrib and Isha are no longer identical, so those times can change slightly. For zones far from their longitude, such asPacific/Kiritimati, prayer times now belong to the requested local date.
From 0.2.0 to 0.2.1
The Composer package name changed to enaxon/rtly-kit. The namespace is still RtlyKit\, so your PHP code does not change. Remove the earlier package from your composer.json and require this one:
composer require enaxon/rtly-kitFrom 0.1.x to 0.2.0
Most applications need no change. Check the items below. The items on official holiday dates, high-latitude prayer times, Mobile validation and Arabic number words change what the library returns for some inputs.
ext-mbstringis no longer required.composer.jsonused to ask for the extension. Now it needs only PHP. There is nothing to do. If you addedext-mbstringto your owncomposer.jsononly for this package, you can remove it.- Float formatting.
Format::withSeparator()andformat_number()now print a float as the shortest decimal that reads back as the same float, cut to 15 significant digits. Binary noise is gone. Strings are unchanged and stay exact. Pass a string when you need more digits.
| Call | Before | After |
|---|---|---|
withSeparator(-1234567.891) | -۱٬۲۳۴٬۵۶۷٫۸۹۱۰۰۰۰۰۰۰۶۱۴۶۷ | -۱٬۲۳۴٬۵۶۷٫۸۹۱ |
withSeparator(0.1 + 0.2) | ۰٫۳ | ۰٫۳ (unchanged) |
withSeparator(-0.0) | ۰ | ۰ |
If you format the result of float arithmetic, round first (round($x, 2)) or pass a string.
NumberToWords::fromWords()is strict. It used to add the words up in any order.
| Input | Before | After |
|---|---|---|
دو صد | 102 | InvalidNumberException (invalid_number_words) |
بیست یک | 21 | InvalidNumberException |
صد و بیست و | 120 | InvalidNumberException |
پنج و بیست | 25 | InvalidNumberException |
سی و پنج | 35 | 35 |
Write و between parts and keep the order hundreds, tens, units. Everything convert() returns still parses.
Slugify::make()throwsRtlyKitException(invalid_argument,argument=text) for text that is not valid UTF-8. Before, only the separator was checked. CatchRtlyKitThrowableor clean the input first.Hijri::format()andHebrew::format()reject a pattern longer than 256 bytes withInvalidDateException(input_too_long,argument=format,limit= 256).Jalali::format()already did. The limit is inHijri::MAX_FORMAT_LENGTHandHebrew::MAX_FORMAT_LENGTH.- A trailing backslash in a format pattern is dropped in all three calendars.
format('Y\')returns just the year. IranHolidays::all(),allTitles()andallFixed()work for every Jalali year from -620 to 9377. Before, years before the Hijri epoch and the last day of 9377 threwinvalid_date. Where Islamic holidays cannot be derived, only the fixed holidays are returned. For a year outside the range they throwInvalidDateException(date_out_of_range, contextyear,min,max).- Error messages that repeat your input cut it to 40 characters and stay valid UTF-8. If you match on message text, match on
getErrorCode()instead. - Official holiday dates for 1380 to 1405. In these Jalali years the static methods (
IranHolidays,is_iran_holiday()) now return the published dates instead of the Umm al-Qura estimate. Other years are unchanged.
| Day | Before (estimate) | After (official) |
|---|---|---|
| 1404/01/10 | عید فطر | no holiday |
| 1404/01/11 | تعطیل عید فطر | عید فطر |
| 1404/01/01 | جشن نوروز + شهادت امام علی | جشن نوروز |
| 1404/12/29 | ملی شدن صنعت نفت + عید فطر | ملی شدن صنعت نفت |
| 1405/01/01 | جشن نوروز + تعطیل عید فطر | جشن نوروز + عید فطر |
In official years only the titles in the official table appear. Years 1380 to 1393 and 1395 are reported: the dates come from one source, and the list of days may be incomplete. To get the old behaviour for all years, use HolidayCalendar::default()->withOfficialData(false). To see where a year comes from, use IranHolidays::sourceOf($year). More in Holiday calendar.
- High-latitude prayer times.
PrayerTimeshas a newHighLatitudeRule. The default isAngleBased. It acts only where a Fajr or Isha time would be missing, or farther from sunrise or sunset than the rule allows. Below about 44 degrees north nothing changes. North of about 44 to 46 degrees, around the June solstice, some values differ. A time that wasnullnow has a value.
| Place and day (MWL) | Before (same as None) | After (default) |
|---|---|---|
| Stockholm, 2026-06-21 | Fajr null, Isha null | Fajr 01:54, Isha 23:40 |
| Munich, 2026-06-21 | Fajr 01:50, Isha 00:12 | Fajr 02:51, Isha 23:32 |
To get the old output, call $prayer->withHighLatitudeRule(HighLatitudeRule::None). nextPrayer() now also finds a prayer that falls after midnight. See High latitudes.
- Mobile validation. The shape check is the same, so no input changes validity. Two things are new. Every result has an
allocateddetail andMobile::isAllocated()returns it as a bool. The operator table follows the numbering plan more closely: the Shatel Mobile key998is narrowed to09981and09982, the Aptel key is widened from99910to9991, and0923,0931,0932and0934are added.
| Number | Operator before | Operator after |
|---|---|---|
09981234567 | شاتل موبایل | شاتل موبایل |
09983112345 | شاتل موبایل | null (allocated, holder not confirmed) |
09231234567 | null | رایتل |
09321234567 | null | تالیا |
If you compare the whole details() array, expect the extra key allocated.
- Arabic number words.
convert($n, 'ar')without options gives the same text as before for every number below 10^9. It now also accepts numbers up to 10^27, where it used to thrownumber_too_large. New options and ordinals are on the Arabic number words page. - Platforms. PHP 8.2 to 8.5, Laravel 11, 12 and 13, and Carbon 3 are supported. See API stability .
From the early pre-release builds (before 0.1.0): namespaced helpers
1. Global helper functions are no longer defined by default
Breaking change. Earlier pre-release builds defined jdate(), to_persian(), is_national_code() and the other helpers as global functions as soon as Composer loaded the package. That could clash with your code or another package, so it was removed. The helpers now live in the RtlyKit namespace and nothing is defined globally.
The 27 helpers: jdate, hdate, hebrew_date, to_persian_digits, to_english_digits, to_persian, to_english, is_national_code, is_sheba, is_bank_card, is_mobile, is_postal_code, is_vehicle_plate, validate_national_code, validate_sheba, validate_bank_card, validate_mobile, validate_postal_code, validate_vehicle_plate, number_to_words, normalize_text, contains_rtl, text_direction, is_iran_holiday, prayer_times, format_number and ordinal.
Option A: import what you use (recommended).
use function RtlyKit\jdate;
use function RtlyKit\is_national_code;
echo jdate('2026-03-21')->format('Y/m/d'); // 1405/01/01Or call the fully qualified name: \RtlyKit\jdate('2026-03-21').
Option B: bring back the old short global names. Call Globals::register() once, for example in your bootstrap file:
\RtlyKit\Globals::register();
echo jdate('2026-03-21')->format('Y/m/d'); // 1405/01/01, works as beforeregister() defines only the names that are still free. It never replaces an existing function and never throws. It returns the names it skipped because something else already uses them. You can call it more than once:
$skipped = \RtlyKit\Globals::register();
if ($skipped !== []) {
error_log('RTLY-Kit globals skipped: '.implode(', ', $skipped)); // use \RtlyKit\name() for those
}The Laravel service provider does not register globals either. In Blade, write {{ \RtlyKit\jdate($post->created_at)->format('j F Y') }}, or call Globals::register() from your AppServiceProvider::register(). More on Helpers and globals.
2. Other changes to be aware of
| Change | What to do |
|---|---|
Validators take any value. All six implement RtlyKit\Contracts\Validator (validate(mixed): Result, isValid(mixed): bool). Non-string input (null, arrays, objects) returns an invalid Result with error invalid_type. Input over 4096 bytes returns input_too_long. They never throw for bad input. | Remove defensive try blocks and type checks around validators. See Validators overview. |
Exceptions carry codes. getErrorCode() (an ErrorCode enum) and getContext() exist on every library exception. | Nothing to change. Match on the code and not on the message text. See Error handling. |
Slugify::make() throws RtlyKitException (input_too_long or invalid_argument) for an invalid UTF-8 separator or one longer than 64 bytes. | This matters only if you pass a separator that users control. Catch RtlyKitThrowable. |
Calendars share a contract. Jalali, Hijri and Hebrew implement RtlyKit\Contracts\CalendarDate. Comparisons and diffIn*() accept a CalendarDate or any DateTimeInterface, make() accepts a CalendarDate, and equals(), isBefore(), isAfter() are new aliases. | Existing calls keep working. See Convert and compare. |
Reference data moved to resources/data/*.php. | If you read the old table classes directly (they were never public API), switch to public classes such as Hijri, Sheba::getBankName() or NationalCode::getLocation(). |
Makkah Isha in Ramadan changed. PrayerTimes::METHOD_MAKKAH now returns Isha as Maghrib + 120 minutes during Ramadan (Umm al-Qura practice) and + 90 minutes otherwise. The previous build always used 90. | Ramadan Isha moves 30 minutes later. This is a bug fix. Update any stored expected values. See the example below. |
A check of the Makkah behaviour (2026-03-01 is in Ramadan, 2026-03-21 is not):
use RtlyKit\Prayer\PrayerTimes;
$mecca = PrayerTimes::forCity('mecca', PrayerTimes::METHOD_MAKKAH);
foreach (['2026-03-01', '2026-03-21'] as $d) {
$t = $mecca->getTimes(new DateTimeImmutable($d));
echo $d, ' maghrib ', $t['maghrib'], ' isha ', $t['isha'], "\n";
}
// 2026-03-01 maghrib 18:25 isha 20:25
// 2026-03-21 maghrib 18:32 isha 20:02Changelog highlights for the 0.x series
These are the points of the changelog that users see. The repository's CHANGELOG.md has the full list.
Behaviour that changed
- Out-of-range calendar input throws. Years, timestamps and huge
add*()andsub*()amounts outside the supported ranges raiseInvalidDateExceptionand not aTypeErroror a wrapped-around date. Ranges: Jalali -620 to 9377, Hijri 1 to 9665, Hebrew 3762 to 13759 ( Limits ). make()string rules are fixed. A year below 1700 (Hebrew: 3000 or more) is read in the calendar of the class. Everything else is read as Gregorian. Blank strings throw, andnullis now. A Gregorian ISO string with a year below 1700 is read as Jalali or Hijri, so pass aDateTimeImmutablefor such dates ( FAQ ).- Holidays. Official dates for Jalali 1380 to 1405 (see above). For other years, Islamic holidays are reported only for AH 1300 to 1500, and a year outside the Jalali range throws
InvalidDateException. - Number input.
NumberToWords::convert()andFormat::ordinal()accept whole floats (3.0) and throwInvalidNumberExceptionfor fractional floats,NaNandINF. - Input caps (security): strings over 4096 bytes are rejected by
NumberToWordsandFormat, andFormat::withSeparator()rejects numbers over 1000 characters. They throwInvalidNumberException(input_too_long). - Data tables keep confirmed entries.
NationalCode::getLocation()covers 547 prefixes and returnsnullfor the rest. The bank BIN (39), Sheba code (38) and operator tables keep only entries confirmed by two sources. A name that was returned before may now benull.BankCardrejects numbers made of one repeated digit. - Prayer-time core rewritten from standard astronomical equations. The difference from the earlier implementation was at most 2 minutes on evening events (mean 0.5 minute or less) across the comparison grid. Daylight saving is applied per event, and the
$elevationconstructor argument is new.
Added
- A pure-PHP Hebrew calendar (
Hebrew) with noext-calendarneeded. The Hijri calendar was rebuilt around the Umm al-Qura table for AH 1300 to 1500, with aHijriVariant::Tabularfallback. - Structured validation:
Result(isValid(),errors(),details()) andvalidate_*()helpers with stable error codes. NumberToWords::fromWords(), numbers up to 21 digits, negatives, and Arabic number words (number_to_words($n, 'ar')).Normalizer::fixHalfSpace()andclean(),Detector::isRtlLocale()andisHebrew(),IranHolidays::getTitles(),isBusinessDay()andnextBusinessDay(), andPrayerTimes::nextPrayer().- Laravel: the
Jalalifacade and the factory in the container, auto-discovery, six validation rules with fa, en and ar messages, andJalaliCast. - Carbon macros
toJalali,jformat,createFromJalali,toHijri,toHebrew,createFromHijri,createFromHebrew. - The exception hierarchy under
RtlyKitThrowable, theErrorCodeenum,Globals::register(), and theCalendarDateandValidatorcontracts.
Fixed
- Makkah-method Isha in Ramadan (described above).
Format::ordinal()for 30 and 23.Format::withSeparator()accepts Persian and Arabic digits without rounding.Mobileaccepts the0098,98and bare9xxxxxxxxxforms.Shebaaccepts the bare 24-digit form.- Prayer-time handling of time zones around daylight saving, and errors for an unknown method or Asr factor.
- The duplicate
hdate()helper declaration.
Note. While the library is below 1.0.0, pin a minor range (for example ~0.3.0), read this page before each minor upgrade, and run your own tests against any date and prayer-time outputs that you store.
Was this page helpful?