_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"; } }