Validation rules and Eloquent cast

Use the Iranian validation rules in Laravel forms, store Jalali dates in Eloquent models with JalaliCast, and show them in Blade.

On this page
  1. Validation rules
  2. Using them
  3. What input the rules accept
  4. The JalaliCast Eloquent cast
    1. Accepted values on assignment
    2. Errors
    3. Serialising a model
  5. Form requests
  6. Blade

Validation rules#

Once the package is installed (Laravel setup), six rules are available in any validator, form request or $request->validate() call. They have seven names, because mobile is a short alias of iran_mobile. They wrap the validators described on the validation pages.

RuleAcceptsDetails
national_codeIranian national codeNational code
shebaSheba / IBAN, with or without the IR prefixSheba and bank card
bank_card16-digit bank card number (Luhn check)Sheba and bank card
iran_mobileIranian mobile numberMobile, postal code, plate
mobileShort alias of iran_mobilesame behaviour
postal_code10-digit Iranian postal codeMobile, postal code, plate
vehicle_plateIranian vehicle plate, for example 12ب345-67Mobile, postal code, plate
Good to know. mobile is a generic word. If another package or your own code registers a rule named mobile, use iran_mobile everywhere so there is no doubt which rule runs.

Using them#

use Illuminate\Http\Request;

public function store(Request $request)
{
    $data = $request->validate([
        'national_code' => 'required|national_code',
        'mobile'        => 'required|iran_mobile',
        'card'          => 'nullable|bank_card',
        'iban'          => 'nullable|sheba',
        'postal'        => 'required|postal_code',
        'plate'         => 'nullable|vehicle_plate',
    ]);
}

A real validation run with bad data and the locale fa gave:

// national_code = '1234567890', mobile = '0912', iban = 'IR000000000000000000000000'
// card, postal and plate hold valid values and pass.
//
// national_code: national code کد ملی معتبر نیست.
// mobile: mobile شماره موبایل معتبر نیست.
// iban: iban شماره شبا معتبر نیست.

The English text is "The national code is not a valid Iranian national code." and an Arabic text is available too. See translations and overriding messages.

What input the rules accept#

  • Strings. Persian and Arabic digits are accepted. ۰۰۱۳۵۴۲۴۱۹ passes national_code.
  • Integers and floats are converted to a string first. Be careful with numbers that start with zero. The JSON number 13542419 has lost the leading zeros of 0013542419 and fails. Send national codes, mobile numbers and postal codes as strings.
  • Arrays, objects and booleans never reach the validator and fail the rule.
  • Empty values. As with Laravel's own rules, an empty string and a missing key are skipped unless the field is required. An explicit null fails unless the field is nullable. Say what you mean with required or nullable.
$f->make(['n' => '۰۰۱۳۵۴۲۴۱۹'], ['n' => 'national_code'])->passes();          // true
$f->make(['n' => 13542419],      ['n' => 'national_code'])->passes();          // false (leading zeros lost)
$f->make(['n' => null],          ['n' => 'nullable|national_code'])->passes(); // true
$f->make(['n' => null],          ['n' => 'national_code'])->passes();          // false
$f->make(['n' => ''],            ['n' => 'national_code'])->passes();          // true  (skipped)
$f->make(['n' => ''],            ['n' => 'required|national_code'])->passes(); // false
$f->make([],                     ['n' => 'national_code'])->passes();          // true  (key missing)

If you need the reason a value failed (wrong length, bad checksum, wrong type), call the standalone validators. They return a structured Result. See Validators overview.

The JalaliCast Eloquent cast#

RtlyKit\Laravel\Casts\JalaliCast keeps the database column in Gregorian (Y-m-d H:i:s), as every tool expects. On the model, the attribute is an immutable RtlyKit\Calendar\Jalali object.

use Illuminate\Database\Eloquent\Model;
use RtlyKit\Laravel\Casts\JalaliCast;

class Post extends Model
{
    protected $guarded = [];

    protected $casts = [
        'published_at' => JalaliCast::class,
    ];
}

$post = new Post();
$post->published_at = '1404/01/15 10:30';

echo $post->getAttributes()['published_at'];          // 2025-04-04 10:30:00   (stored value)
echo $post->published_at->format('Y/m/d H:i');        // 1404/01/15 10:30
echo get_class($post->published_at);                  // RtlyKit\Calendar\Jalali
echo $post->published_at->addDays(20)->format('Y/m/d'); // 1404/02/04

The column type stays datetime or timestamp. You need no migration change.

Accepted values on assignment#

Value assignedStored (Gregorian)
Jalali string '1404/01/15 10:30'2025-04-04 10:30:00
Jalali date only, Persian digits and dashes '۱۴۰۴-۰۱-۱۵'2025-04-04 00:00:00
Gregorian string '2025-04-04 08:00:00'2025-04-04 08:00:00
Unix timestamp (int) 17437536002025-04-04 08:00:00 (with a UTC default time zone)
Any DateTimeInterface (Carbon included)the same moment, as Y-m-d H:i:s
A Jalali objectits Gregorian equivalent
null or ''null

A string is read as Jalali when its year is 1200 to 1599 and the separator is / or -, with an optional time HH:MM or HH:MM:SS after a space or T. Any other string goes through the normal date parser as Gregorian.

Errors#

Bad input throws RtlyKit\Exceptions\InvalidDateException when you assign it, never a raw PHP error:

$post->published_at = '1404/13/01';   // Invalid Jalali date: 1404/13/1
$post->published_at = '1404/07/31';   // Invalid Jalali date: 1404/7/31   (Mehr has 30 days)
$post->published_at = 'not a date';   // Unable to parse date: not a date
$post->published_at = 1.5;            // Cannot cast value for 'published_at' to a date.
$post->published_at = ['x'];          // Cannot cast value for 'published_at' to a date.

Validate user input before it reaches the model (see the form request below), so a typo gives a form error and not an exception. Reading is relaxed for null and the empty string (both give null). An attribute that holds unreadable text in the database throws InvalidDateException when you read it.

Serialising a model#

Jalali, Hijri and Hebrew implement JsonSerializable. toJson() on a model with a JalaliCast attribute gives the same text as (string) $date, for example {"published_at":"1404/01/01 10:00:00"}. toArray() keeps the Jalali object. Format it yourself if you need another pattern.

Form requests#

There is no built-in rule for a Jalali date field. Check the shape with Laravel's regex rule or a small custom rule, then let the cast do the conversion:

public function rules(): array
{
    return [
        'published_at' => ['required', 'regex:/^1[2-5]\d\d[\/-]\d{1,2}[\/-]\d{1,2}$/u'],
    ];
}

A well-formed but impossible date such as 1404/07/31 still reaches the cast and throws there. Catch InvalidDateException in a custom rule or in the controller if users type free text.

Blade#

The cast gives you a Jalali object, so Blade needs no helper:

{{ $post->published_at?->format('Y/m/d') }}            {{-- 1404/01/15 --}}
{{ $post->published_at?->format('l j F Y') }}          {{-- جمعه 15 فروردین 1404 --}}
{{ \RtlyKit\to_persian_digits($post->published_at->format('Y/m/d')) }}  {{-- ۱۴۰۴/۰۱/۱۵ --}}

For a column that is not cast, such as the standard created_at (a Carbon instance), use the namespaced helper. Namespaced helpers are always loaded, so you register nothing:

{{ \RtlyKit\jdate($post->created_at)->format('Y/m/d H:i') }}   {{-- 1404/01/15 08:00 --}}

If you use it often, import the function at the top of a Blade file with @php use function RtlyKit\jdate; @endphp. With the Carbon macros the same call is $post->created_at->jformat('Y/m/d'). See Carbon macros.

Note. A null attribute is not formatted for you. Use the nullsafe operator (?->) as shown, or Blade throws on null. For the full list of format letters, see the Jalali page.