diff --git a/app/RSpade/CodeQuality/Rules/Models/ModelFetchDateFormatting_CodeQualityRule.php b/app/RSpade/CodeQuality/Rules/Models/ModelFetchDateFormatting_CodeQualityRule.php new file mode 100755 index 000000000..303cff9ad --- /dev/null +++ b/app/RSpade/CodeQuality/Rules/Models/ModelFetchDateFormatting_CodeQualityRule.php @@ -0,0 +1,237 @@ +_extract_fetch_body($lines); + if (empty($fetch_body)) { + return; + } + + // Patterns that indicate server-side date formatting + $patterns = [ + // Carbon format() calls - e.g., ->format('M d, Y') + '/->format\s*\(\s*[\'"]/' => '->format() call', + + // Carbon diffForHumans() calls - e.g., ->diffForHumans() + '/->diffForHumans\s*\(/' => '->diffForHumans() call', + + // Carbon::parse()->format() chain + '/Carbon::parse\s*\([^)]+\)\s*->format\s*\(/' => 'Carbon::parse()->format() chain', + + // Rsx_Time formatting (which is fine on its own, but suggests formatted values in response) + // We still flag these because they shouldn't be in fetch() - format client-side + '/Rsx_Time::format_date(time)?(_with_tz)?\s*\(/' => 'Rsx_Time formatting call', + '/Rsx_Time::relative\s*\(/' => 'Rsx_Time::relative() call', + + // Rsx_Date formatting + '/Rsx_Date::format\s*\(/' => 'Rsx_Date::format() call', + ]; + + foreach ($fetch_body as $relative_line => $line_content) { + // Check for exception comment + if (str_contains($line_content, '@' . $this->get_id() . '-EXCEPTION')) { + continue; + } + + foreach ($patterns as $pattern => $description) { + if (preg_match($pattern, $line_content)) { + $this->add_violation( + $file_path, + $relative_line, + $this->_get_violation_message($description), + trim($line_content), + $this->_get_resolution_message(), + 'high' + ); + break; // Only one violation per line + } + } + } + } + + /** + * Extract the fetch() method body with line numbers + * + * @param array $lines File lines + * @return array [line_number => line_content] + */ + private function _extract_fetch_body(array $lines): array + { + $in_fetch = false; + $brace_count = 0; + $fetch_body = []; + $started = false; + + for ($i = 0; $i < count($lines); $i++) { + $line = $lines[$i]; + $line_num = $i + 1; + + // Look for fetch method declaration + if (!$in_fetch) { + if (preg_match('/\b(public\s+)?static\s+function\s+fetch\s*\(/', $line)) { + $in_fetch = true; + // Count opening brace if on same line + if (str_contains($line, '{')) { + $started = true; + $brace_count = 1; + } + } + continue; + } + + // We're inside fetch method + if (!$started) { + // Looking for opening brace + if (str_contains($line, '{')) { + $started = true; + $brace_count = 1; + } + continue; + } + + // Track braces to find method end + $brace_count += substr_count($line, '{') - substr_count($line, '}'); + + if ($brace_count <= 0) { + break; // End of fetch method + } + + $fetch_body[$line_num] = $line; + } + + return $fetch_body; + } + + /** + * Get the violation message + */ + private function _get_violation_message(string $pattern_description): string + { + return "Server-side date formatting detected in fetch() method ({$pattern_description}).\n\n" . + "Dates and datetimes should not be pre-formatted for Ajax responses. " . + "RSpade stores dates as 'YYYY-MM-DD' strings and datetimes as ISO 8601 UTC strings " . + "(e.g., '2024-12-24T15:30:45.123Z'). These values should be passed directly to the " . + "client and formatted using JavaScript.\n\n" . + "Problems with server-side formatting:\n" . + " 1. Creates implicit API contracts - frontend developers may expect these fields\n" . + " to exist on all model responses, leading to confusion\n" . + " 2. Prevents client-side locale customization\n" . + " 3. Increases response payload size with redundant data\n" . + " 4. Violates the principle that fetch() returns model data, not presentation"; + } + + /** + * Get the resolution message + */ + private function _get_resolution_message(): string + { + return "Remove the formatted date fields from fetch(). Format dates client-side:\n\n" . + " JavaScript datetime formatting:\n" . + " Rsx_Time.format_datetime(iso_string) // 'Dec 24, 2024 3:30 PM'\n" . + " Rsx_Time.format_datetime_with_tz(iso) // 'Dec 24, 2024 3:30 PM CST'\n" . + " Rsx_Time.relative(iso_string) // '2 hours ago'\n\n" . + " JavaScript date formatting:\n" . + " Rsx_Date.format(date_string) // 'Dec 24, 2024'\n\n" . + " Example in jqhtml template:\n" . + " <%= Rsx_Time.relative(this.data.model.created_at) %>\n\n" . + "See: php artisan rsx:man time"; + } +} diff --git a/app/RSpade/upstream_changes/datetime_strings_12_24.txt b/app/RSpade/upstream_changes/datetime_strings_12_24.txt new file mode 100755 index 000000000..28f2590e1 --- /dev/null +++ b/app/RSpade/upstream_changes/datetime_strings_12_24.txt @@ -0,0 +1,312 @@ +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 --%> +