DATE/DATETIME STRING FORMAT - MIGRATION GUIDE Date: 2024-12-24 SUMMARY RSpade now stores and transmits dates and datetimes as strings throughout the application, not as Carbon objects. This eliminates timezone bugs with date columns and ensures PHP/JavaScript parity with identical formats on both sides. - Dates: "YYYY-MM-DD" strings (e.g., "2024-12-24") - Datetimes: ISO 8601 UTC strings (e.g., "2024-12-24T15:30:45.123Z") Model properties like $model->created_at now return strings, not Carbon instances. Calling Carbon methods like ->format() or ->diffForHumans() on these properties will cause errors. Use Rsx_Time and Rsx_Date helper classes instead. A code quality rule (MODEL-FETCH-DATE-01) now detects server-side date formatting in model fetch() methods at manifest-time. Dates should be formatted client-side using the JavaScript Rsx_Time and Rsx_Date classes. WHY THIS CHANGE 1. Timezone Safety Carbon objects assume a timezone, but DATE columns have no timezone. "2024-12-24" is just a calendar day - not midnight in any timezone. Treating it as Carbon caused subtle bugs when serializing to JSON. 2. PHP/JavaScript Parity With string-based dates, both PHP and JavaScript see identical values. No serialization surprises, no format mismatches. 3. Client-Side Formatting Formatting should happen in the browser where the user's locale and timezone are known. Server-side formatting creates implicit API contracts that confuse frontend developers. AFFECTED CODE PATTERNS 1. Model Property Access ------------------------------------------------------------------------- Model datetime/date properties now return strings, not Carbon. Before (BROKEN): $user->created_at->format('M d, Y'); $user->created_at->diffForHumans(); $project->due_date->format('Y-m-d'); After (CORRECT): use App\RSpade\Core\Time\Rsx_Time; use App\RSpade\Core\Time\Rsx_Date; Rsx_Time::format_date($user->created_at); // "Dec 24, 2024" Rsx_Time::relative($user->created_at); // "2 hours ago" Rsx_Date::format($project->due_date); // "Dec 24, 2024" 2. Formatted Fields in fetch() Methods ------------------------------------------------------------------------- Model fetch() methods should NOT return pre-formatted date fields. The MODEL-FETCH-DATE-01 rule now blocks this at manifest-time. Before (BLOCKED BY CODE QUALITY RULE): public static function fetch($id) { $data = $model->toArray(); $data['created_at_formatted'] = $model->created_at->format('M d, Y'); $data['created_at_human'] = $model->created_at->diffForHumans(); return $data; } After (CORRECT): public static function fetch($id) { $data = $model->toArray(); // created_at is already included as ISO string from toArray() // DO NOT add formatted versions - format client-side return $data; } 3. Controller Ajax Responses ------------------------------------------------------------------------- Controllers returning date data should pass raw values, not formatted. Before: return [ 'created_at_formatted' => $user->created_at->format('F j, Y'), 'updated_at_human' => $user->updated_at->diffForHumans(), ]; After: return [ 'created_at' => $user->created_at, // ISO string automatically 'updated_at' => $user->updated_at, ]; 4. Carbon::parse() in PHP ------------------------------------------------------------------------- If you need Carbon for calculations, use Rsx_Time::parse() instead. It returns Carbon internally but validates the input format. Before: $carbon = \Carbon\Carbon::parse($project->start_date); $days_until = $carbon->diffInDays(now()); After: $carbon = Rsx_Time::parse($user->created_at); // For datetimes $days_until = $carbon->diffInDays(Rsx_Time::now()); // For date comparisons: $days = Rsx_Date::diff_days($project->start_date, Rsx_Date::today()); CLIENT-SIDE FORMATTING JavaScript templates should format dates using Rsx_Time and Rsx_Date: 1. Datetime Formatting (Rsx_Time) ------------------------------------------------------------------------- For datetime columns (created_at, updated_at, timestamps): Template (jqhtml): <%-- Display formatted date --%>
<%= Rsx_Time.format_date(this.data.user.created_at) %>
<%-- Display with time --%>
<%= Rsx_Time.format_datetime(this.data.event.start_time) %>
<%-- Display with timezone --%>
<%= Rsx_Time.format_datetime_with_tz(this.data.event.start_time) %>
<%-- Relative time (e.g., "2 hours ago") --%> <%= Rsx_Time.relative(this.data.user.created_at) %> 2. Date Formatting (Rsx_Date) ------------------------------------------------------------------------- For date-only columns (due_date, birth_date, start_date): Template (jqhtml): <%-- Format date --%>
<%= Rsx_Date.format(this.data.project.due_date) %>
<%-- Check if past due --%> <% if (Rsx_Date.is_past(this.data.project.due_date)) { %> Overdue <% } %> 3. Handling Null Values ------------------------------------------------------------------------- Check for null before formatting:
<%= this.data.user.last_login_at ? Rsx_Time.format_datetime(this.data.user.last_login_at) : 'Never' %>
MIGRATION STEPS Step 1: Find Carbon method calls on model properties ------------------------------------------------------------------------- Search for ->format( and ->diffForHumans( on model properties: grep -rn "->format\s*(" rsx/ --include="*.php" | grep -v "Rsx_Time" grep -rn "->diffForHumans" rsx/ --include="*.php" Step 2: Find formatted date fields in fetch() methods ------------------------------------------------------------------------- Search for _formatted and _human field assignments: grep -rn "_formatted\s*=" rsx/models/ --include="*.php" grep -rn "_human\s*=" rsx/models/ --include="*.php" Step 3: Find Carbon::parse() calls ------------------------------------------------------------------------- These may need conversion to Rsx_Time::parse() or Rsx_Date::parse(): grep -rn "Carbon::parse" rsx/ --include="*.php" Step 4: Find template usages of _formatted fields ------------------------------------------------------------------------- Templates referencing removed fields need updating: grep -rn "_formatted\|_human" rsx/app/ --include="*.jqhtml" grep -rn "_formatted\|_human" rsx/app/ --include="*.js" Step 5: Update JavaScript defaults ------------------------------------------------------------------------- JS files with this.data defaults for _formatted fields: grep -rn "created_at_formatted\|updated_at_formatted" rsx/ --include="*.js" EXAMPLE MIGRATION Complete example: Converting a view action PHP Controller (before): return [ 'id' => $user->id, 'name' => $user->name, 'created_at_formatted' => $user->created_at->format('F j, Y'), ]; PHP Controller (after): return [ 'id' => $user->id, 'name' => $user->name, 'created_at' => $user->created_at, // Already ISO string ]; JavaScript (before): on_create() { this.data.user = { id: null, name: '', created_at_formatted: '', }; } JavaScript (after): on_create() { this.data.user = { id: null, name: '', created_at: null, }; } Template (before):
<%= this.data.user.created_at_formatted %>
Template (after):
<%= Rsx_Time.format_date(this.data.user.created_at) %>
CUSTOM ELOQUENT CASTS The framework uses custom Eloquent casts to ensure string-based values: - Rsx_DateTime_Cast: DATETIME/TIMESTAMP columns return ISO 8601 strings - Rsx_Date_Cast: DATE columns return YYYY-MM-DD strings These casts are automatically applied via Rsx_Model_Abstract::getCasts(). You do not need to configure anything - model properties automatically return strings. If a model needs Laravel's default Carbon behavior for a specific column, override $casts in the model: protected $casts = [ 'special_timestamp' => 'datetime', // Laravel default (Carbon) ]; AVAILABLE HELPER METHODS Rsx_Time (for datetimes) ------------------------------------------------------------------------- PHP: Rsx_Time::now() // Current time as Carbon Rsx_Time::now_iso() // Current time as ISO string Rsx_Time::parse($input) // Parse to Carbon (for calculations) Rsx_Time::to_iso($time) // Convert to ISO string Rsx_Time::format_date($time) // "Dec 24, 2024" Rsx_Time::format_datetime($time) // "Dec 24, 2024 3:30 PM" Rsx_Time::relative($time) // "2 hours ago" Rsx_Time::add($time, $seconds) // Add seconds, return ISO string Rsx_Time::is_past($time) // Boolean Rsx_Time::is_future($time) // Boolean JavaScript: Rsx_Time.now() // Current time as Date Rsx_Time.now_iso() // Current time as ISO string Rsx_Time.format_date(iso) // "Dec 24, 2024" Rsx_Time.format_datetime(iso) // "Dec 24, 2024 3:30 PM" Rsx_Time.format_datetime_with_tz(iso) // "Dec 24, 2024 3:30 PM CST" Rsx_Time.relative(iso) // "2 hours ago" Rsx_Time.countdown($el, target) // Live countdown Rsx_Time.countup($el, start) // Live elapsed time Rsx_Date (for dates) ------------------------------------------------------------------------- PHP: Rsx_Date::today() // Today as "YYYY-MM-DD" Rsx_Date::parse($input) // Validate and normalize Rsx_Date::format($date) // "Dec 24, 2024" Rsx_Date::is_past($date) // Boolean Rsx_Date::is_future($date) // Boolean Rsx_Date::diff_days($d1, $d2) // Days between dates JavaScript: Rsx_Date.today() // Today as "YYYY-MM-DD" Rsx_Date.format(date) // "Dec 24, 2024" Rsx_Date.is_past(date) // Boolean Rsx_Date.is_future(date) // Boolean Rsx_Date.diff_days(d1, d2) // Days between dates VERIFICATION 1. Run code quality check: php artisan rsx:check The MODEL-FETCH-DATE-01 rule will detect any remaining server-side date formatting in fetch() methods. 2. Search for remaining Carbon method calls: grep -rn "->format\s*(" rsx/ --include="*.php" | grep -v vendor 3. Test pages that display dates: - Verify dates render correctly - Check relative times update properly - Confirm timezone handling is correct REFERENCE php artisan rsx:man time - Complete time/date API documentation Rsx_Time.php, Rsx_Date.php - PHP implementation Rsx_Time.js, Rsx_Date.js - JavaScript implementation Rsx_DateTime_Cast.php - Custom Eloquent cast for datetimes Rsx_Date_Cast.php - Custom Eloquent cast for dates