Skip to main content

DateBasedOnCalendarCast

The DateBasedOnCalendarCast is an Eloquent cast that automatically converts date/datetime values between storage format (Gregorian) and display format (user's language calendar system). It provides seamless multi-calendar support with automatic conversion, digit transliteration, and timezone handling.

Namespace​

JobMetric\Language\Casts\DateBasedOnCalendarCast

Overview​

DateBasedOnCalendarCast provides:

  • Automatic Calendar Conversion: Converts dates between Gregorian (storage) and user's calendar (display)
  • Multi-Calendar Support: Supports Jalali, Hijri, Hebrew, Buddhist, Coptic, Ethiopian, Chinese calendars
  • Digit Transliteration: Automatically converts between English, Persian, and Arabic numerals
  • Timezone Awareness: Handles timezone conversion based on client timezone
  • Flexible Formatting: Configurable date separators and modes (date/datetime)
  • Graceful Fallbacks: Falls back to Gregorian if language/calendar not available

How It Works​

Storage vs Display​

  • Storage: Always Gregorian in Y-m-d H:i:s format in app timezone (typically UTC)
  • Display: Converted to user's calendar system based on current language settings

Conversion Flow​

Getting (Reading from Database)​

  1. Reads Gregorian date from database
  2. Converts to client timezone
  3. Converts Y-m-d part to user's calendar system
  4. Applies digit transliteration (English/Persian/Arabic)
  5. Returns formatted string

Setting (Writing to Database)​

  1. Accepts date in user's calendar system (or Gregorian)
  2. Transliterates digits to English
  3. Converts from user's calendar to Gregorian
  4. Normalizes timezone to app timezone
  5. Stores as Y-m-d H:i:s in database

Basic Usage​

Simple Date Cast​

use JobMetric\Language\Casts\DateBasedOnCalendarCast;

class Event extends Model
{
protected $casts = [
'event_date' => DateBasedOnCalendarCast::class,
];
}

Example:

// Database: "2024-12-25 00:00:00" (Gregorian)
// User's language: Persian (Jalali calendar)

$event = Event::find(1);
echo $event->event_date; // "1403-09-05" (Jalali, Persian digits: ۱۴۰۳-۰۹-۰۵)

DateTime Cast​

protected $casts = [
'starts_at' => DateBasedOnCalendarCast::class . ':datetime',
];

Example:

// Database: "2024-12-25 14:30:00" (Gregorian)
// User's language: Persian (Jalali calendar)

$event = Event::find(1);
echo $event->starts_at; // "1403-09-05 14:30:00" (Jalali with time)

Constructor Parameters​

The cast accepts three optional parameters:

new DateBasedOnCalendarCast(
string $mode = 'date', // 'date' or 'datetime'
string $dateSep = '-', // '-', '/', or '.'
string $digits = 'auto' // 'en', 'fa', 'ar', or 'auto'
)

Mode​

Controls whether the attribute handles date-only or full datetime:

  • 'date': Date only (e.g., "1403-09-05")
  • 'datetime': Date with time (e.g., "1403-09-05 14:30:00")

Date Separator​

Controls the separator used in formatted output:

  • '-': "1403-09-05" (default)
  • '/': "1403/09/05"
  • '.': "1403.09.05"

Note: Input accepts all separators (-, /, .) regardless of this setting.

Digits​

Controls digit transliteration in output:

  • 'auto': Automatically uses digits based on current locale (fa → Persian, ar → Arabic, else → English)
  • 'en': English digits (0-9)
  • 'fa': Persian digits (۰-۹)
  • 'ar': Arabic digits (٠-٩)

Advanced Usage​

Using castUsing() Method​

Laravel's castUsing() allows passing parameters via cast string:

protected $casts = [
'event_date' => DateBasedOnCalendarCast::class . ':date,-,fa',
// Mode: date
// Separator: -
// Digits: fa (Persian)
];

Different Configurations​

protected $casts = [
// Date only, slash separator, Persian digits
'birth_date' => DateBasedOnCalendarCast::class . ':date,/,fa',

// DateTime, dot separator, auto digits
'created_at' => DateBasedOnCalendarCast::class . ':datetime,.,auto',

// Date only, dash separator, English digits
'published_at' => DateBasedOnCalendarCast::class . ':date,-,en',
];

Complete Examples​

Example 1: Event Model with Jalali Dates​

// app/Models/Event.php
use JobMetric\Language\Casts\DateBasedOnCalendarCast;

class Event extends Model
{
protected $casts = [
'event_date' => DateBasedOnCalendarCast::class . ':date,/,fa',
'starts_at' => DateBasedOnCalendarCast::class . ':datetime,-,auto',
'ends_at' => DateBasedOnCalendarCast::class . ':datetime,-,auto',
];
}

Usage:

// Create event
$event = Event::create([
'title' => 'Conference',
'event_date' => '1403/09/05', // Jalali date
'starts_at' => '1403-09-05 09:00:00', // Jalali datetime
'ends_at' => '1403-09-05 17:00:00', // Jalali datetime
]);

// Database stores as Gregorian:
// event_date: "2024-12-25 00:00:00"
// starts_at: "2024-12-25 09:00:00"
// ends_at: "2024-12-25 17:00:00"

// When reading (Persian user):
$event = Event::find(1);
echo $event->event_date; // "1403/09/05" (Jalali, Persian digits)
echo $event->starts_at; // "1403-09-05 09:00:00" (Jalali, auto digits)

Example 2: Article Model with Publication Date​

// app/Models/Article.php
class Article extends Model
{
protected $casts = [
'published_at' => DateBasedOnCalendarCast::class . ':date,-,auto',
];
}

Usage:

// Persian user creates article
$article = Article::create([
'title' => 'New Article',
'published_at' => '1403-09-05', // Jalali date
]);

// English user views article
// (Language changes, but date remains in database as Gregorian)
$article = Article::find(1);
echo $article->published_at; // "2024-12-25" (Gregorian, English digits)

Example 3: Appointment Model​

// app/Models/Appointment.php
class Appointment extends Model
{
protected $casts = [
'appointment_date' => DateBasedOnCalendarCast::class . ':date,/,fa',
'appointment_time' => DateBasedOnCalendarCast::class . ':datetime,-,fa',
];
}

Usage:

// User books appointment in Jalali
$appointment = Appointment::create([
'doctor_id' => 1,
'patient_id' => 1,
'appointment_date' => '1403/09/05', // Jalali
'appointment_time' => '1403-09-05 14:30:00', // Jalali with time
]);

// Display to user
echo $appointment->appointment_date; // "1403/09/05" (Persian digits: ۱۴۰۳/۰۹/۰۵)
echo $appointment->appointment_time; // "1403-09-05 14:30:00"

Example 4: Task Model with Deadlines​

// app/Models/Task.php
class Task extends Model
{
protected $casts = [
'due_date' => DateBasedOnCalendarCast::class . ':date,-,auto',
'completed_at' => DateBasedOnCalendarCast::class . ':datetime,-,auto',
];
}

Usage:

// Create task
$task = Task::create([
'title' => 'Complete project',
'due_date' => '1403-09-10', // Jalali
]);

// Complete task
$task->update([
'completed_at' => now(), // Automatically converts to Gregorian for storage
]);

// Display
echo $task->due_date; // "1403-09-10" (in user's calendar)
echo $task->completed_at; // "1403-09-05 15:45:00" (in user's calendar)

Example 5: Form Input Handling​

// app/Http/Controllers/EventController.php
class EventController extends Controller
{
public function store(Request $request)
{
// User submits: "1403/09/05" (Jalali)
$event = Event::create([
'title' => $request->input('title'),
'event_date' => $request->input('event_date'), // Cast handles conversion
]);

// Database stores: "2024-12-25 00:00:00" (Gregorian)

return redirect()->back();
}

public function show(Event $event)
{
// Automatically displays in user's calendar
return view('events.show', [
'event' => $event, // $event->event_date is in user's calendar
]);
}
}

Example 6: API Response​

// app/Http/Controllers/Api/EventController.php
class EventController extends Controller
{
public function index()
{
$events = Event::all();

return response()->json([
'data' => $events->map(function ($event) {
return [
'id' => $event->id,
'title' => $event->title,
'event_date' => $event->event_date, // Already in user's calendar
'starts_at' => $event->starts_at, // Already in user's calendar
];
}),
]);
}
}

Response (Persian user):

{
"data": [
{
"id": 1,
"title": "Conference",
"event_date": "1403/09/05",
"starts_at": "1403-09-05 09:00:00"
}
]
}

Response (English user):

{
"data": [
{
"id": 1,
"title": "Conference",
"event_date": "2024-12-25",
"starts_at": "2024-12-25 09:00:00"
}
]
}

Supported Input Formats​

Calendar Formats​

The cast accepts dates in various calendar systems:

  • Gregorian: "2024-12-25", "2024/12/25", "2024.12.25"
  • Jalali: "1403-09-05", "1403/09/05", "1403.09.05"
  • Hijri: "1446-06-15", "1446/06/15", "1446.06.15"
  • Other calendars: Hebrew, Buddhist, Coptic, Ethiopian, Chinese

Date Separators​

All separators are accepted in input:

  • Dash: "1403-09-05"
  • Slash: "1403/09/05"
  • Dot: "1403.09.05"

Digit Formats​

Input accepts digits in any format:

  • English: "1403-09-05"
  • Persian: "۱۴۰۳-۰۹-۰۵"
  • Arabic: "١٤٠٣-٠٩-٠٥"

DateTime Formats​

For datetime mode:

  • Date + Time: "1403-09-05 14:30:00"
  • Date + Time (space): "1403-09-05 14:30"
  • ISO Format: "2024-12-25T14:30:00Z" (treated as Gregorian)

Special Inputs​

  • DateTimeInterface: Carbon::now(), new DateTime()
  • Unix Timestamp: 1703520000 (integer or string)
  • Null: Returns null (handles gracefully)

Calendar Systems​

The cast supports all calendar types from CalendarTypeEnum:

CalendarExample InputExample Output
Gregorian"2024-12-25""2024-12-25"
Jalali"1403-09-05""1403-09-05"
Hijri"1446-06-15""1446-06-15"
Hebrew"5785-03-15""5785-03-15"
Buddhist"2567-12-25""2567-12-25"
Coptic"1741-04-16""1741-04-16"
Ethiopian"2017-04-16""2017-04-16"
Chinese"4722-11-15""4722-11-15"

Timezone Handling​

The cast automatically handles timezone conversion:

  1. Storage: Converts to app timezone (typically UTC)
  2. Display: Converts to client timezone (from Accept-Timezone header)

Example:

// User in Tehran (UTC+3:30) creates event at 14:30 local time
$event = Event::create([
'starts_at' => '1403-09-05 14:30:00', // Tehran time
]);

// Database stores: "2024-12-25 11:00:00" (UTC)

// User in New York (UTC-5) views event
// Displays: "1403-09-05 06:30:00" (New York time, converted)

Fallback Behavior​

The cast gracefully handles edge cases:

No Language Set​

// If no language is set or language has no calendar
$event->event_date; // Returns Gregorian format

Invalid Calendar​

// If calendar converter fails
$event->event_date; // Falls back to Gregorian format

Invalid Input​

// If input cannot be parsed
$event->event_date = 'invalid-date'; // May throw exception or return null

When to Use DateBasedOnCalendarCast​

Primary Use Cases​

1. Multi-Calendar Applications​

When: Your application serves users with different calendar preferences.

Why: Automatically displays dates in user's preferred calendar.

Example:

// Persian users see Jalali dates
// Arabic users see Hijri dates
// English users see Gregorian dates

2. Localized Date Display​

When: You need dates displayed in user's local calendar system.

Why: Provides native date format for better user experience.

Example:

// Persian user sees: "1403/09/05"
// Instead of: "2024-12-25"

3. Form Input Handling​

When: Users input dates in their local calendar system.

Why: Automatically converts to Gregorian for storage.

Example:

// User inputs: "1403/09/05" (Jalali)
// Stored as: "2024-12-25" (Gregorian)

4. API Responses​

When: Building APIs that return date data.

Why: Automatically formats dates based on user's language.

Example:

// API returns dates in user's calendar automatically
return new EventResource($event);

When NOT to Use​

  • Simple Gregorian-Only Apps: If all users use Gregorian calendar
  • Internal Calculations: For server-side date calculations, use Carbon directly
  • Timestamp Fields: Use timestamp cast for Unix timestamps
  • Date Ranges: For date range queries, work with Gregorian in database

Best Practices​

1. Always Store in Gregorian​

// ✅ Good - Cast handles conversion
protected $casts = [
'event_date' => DateBasedOnCalendarCast::class,
];

// Database always stores Gregorian
// Display automatically converts

// ❌ Bad - Don't manually convert before storage
$event->event_date = convertToGregorian($jalaliDate); // Unnecessary

2. Use Appropriate Mode​

// ✅ Good - Use datetime for time-sensitive data
'starts_at' => DateBasedOnCalendarCast::class . ':datetime',

// ✅ Good - Use date for date-only fields
'birth_date' => DateBasedOnCalendarCast::class . ':date',

// ❌ Bad - Using date mode for datetime data
'starts_at' => DateBasedOnCalendarCast::class . ':date', // Loses time

3. Configure Digits Based on Locale​

// ✅ Good - Auto digits adapts to locale
'event_date' => DateBasedOnCalendarCast::class . ':date,-,auto',

// ✅ Good - Explicit digits for specific needs
'event_date' => DateBasedOnCalendarCast::class . ':date,-,fa', // Always Persian

// ❌ Bad - English digits for Persian users
'event_date' => DateBasedOnCalendarCast::class . ':date,-,en', // Poor UX

4. Handle Null Values​

// ✅ Good - Cast handles null gracefully
$event->event_date = null; // Works fine

// ❌ Bad - Don't set empty strings
$event->event_date = ''; // May cause issues

Common Mistakes to Avoid​

  1. Manual conversion before storage:

    // ❌ Wrong - Unnecessary conversion
    $gregorian = convertToGregorian($jalaliDate);
    $event->event_date = $gregorian;

    // ✅ Correct - Cast handles it
    $event->event_date = $jalaliDate;
  2. Using wrong mode:

    // ❌ Wrong - Date mode loses time
    'starts_at' => DateBasedOnCalendarCast::class . ':date',

    // ✅ Correct - Datetime mode preserves time
    'starts_at' => DateBasedOnCalendarCast::class . ':datetime',
  3. Not configuring digits:

    // ❌ Wrong - May show English digits to Persian users
    'event_date' => DateBasedOnCalendarCast::class,

    // ✅ Correct - Auto adapts to locale
    'event_date' => DateBasedOnCalendarCast::class . ':date,-,auto',
  4. Expecting consistent format across users:

    // ❌ Wrong - Format depends on user's language
    $date = $event->event_date; // Different format for different users

    // ✅ Correct - Use raw attribute for consistent format
    $date = $event->getRawOriginal('event_date'); // Always Gregorian

Performance Considerations​

Converter Caching​

The cast caches calendar converters per request to avoid repeated factory calls:

// First call creates converter
$event->event_date; // Creates converter, caches it

// Subsequent calls use cached converter
$event2->event_date; // Uses cached converter

Language Resolution​

The cast uses CurrentLanguage::get() which is cached based on config('language.cache_time'):

// Configure cache for better performance
// config/language.php
'cache_time' => 60, // Cache for 60 minutes