Use string-based date/datetime casts instead of Carbon objects
Add TODO expectations and comments documentation to expect_files man page 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -430,9 +430,12 @@ abstract class Rsx_Model_Abstract extends Model
|
|||||||
*
|
*
|
||||||
* Auto-detected casts:
|
* Auto-detected casts:
|
||||||
* - TINYINT(1) columns → 'boolean'
|
* - TINYINT(1) columns → 'boolean'
|
||||||
* - DATETIME/TIMESTAMP columns → 'datetime'
|
* - DATETIME/TIMESTAMP columns → Rsx_DateTime_Cast (returns ISO 8601 UTC strings)
|
||||||
* - DATE columns → 'date'
|
* - DATE columns → Rsx_Date_Cast (returns YYYY-MM-DD strings)
|
||||||
* - TIME columns → 'time'
|
* - TIME columns → 'time' (string)
|
||||||
|
*
|
||||||
|
* IMPORTANT: Date and datetime columns return STRINGS, not Carbon objects.
|
||||||
|
* This prevents timezone bugs and keeps PHP/JS in sync with identical formats.
|
||||||
*
|
*
|
||||||
* Users can still override by defining their own $casts property.
|
* Users can still override by defining their own $casts property.
|
||||||
*
|
*
|
||||||
@@ -455,26 +458,29 @@ abstract class Rsx_Model_Abstract extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auto-detect datetime columns (DATETIME, TIMESTAMP)
|
// Auto-detect datetime columns (DATETIME, TIMESTAMP)
|
||||||
|
// Returns ISO 8601 UTC strings, NOT Carbon objects
|
||||||
$datetime_columns = \App\RSpade\Core\Manifest\Manifest::db_get_columns_by_type($table_name, 'datetime');
|
$datetime_columns = \App\RSpade\Core\Manifest\Manifest::db_get_columns_by_type($table_name, 'datetime');
|
||||||
foreach ($datetime_columns as $column) {
|
foreach ($datetime_columns as $column) {
|
||||||
if (!isset($casts[$column])) {
|
if (!isset($casts[$column])) {
|
||||||
$casts[$column] = 'datetime';
|
$casts[$column] = \App\RSpade\Core\Time\Rsx_DateTime_Cast::class;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-detect date columns (DATE)
|
// Auto-detect date columns (DATE)
|
||||||
|
// Returns YYYY-MM-DD strings, NOT Carbon objects
|
||||||
$date_columns = \App\RSpade\Core\Manifest\Manifest::db_get_columns_by_type($table_name, 'date');
|
$date_columns = \App\RSpade\Core\Manifest\Manifest::db_get_columns_by_type($table_name, 'date');
|
||||||
foreach ($date_columns as $column) {
|
foreach ($date_columns as $column) {
|
||||||
if (!isset($casts[$column])) {
|
if (!isset($casts[$column])) {
|
||||||
$casts[$column] = 'date';
|
$casts[$column] = \App\RSpade\Core\Time\Rsx_Date_Cast::class;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-detect time columns (TIME)
|
// Auto-detect time columns (TIME)
|
||||||
|
// Laravel's 'time' cast returns strings, which is what we want
|
||||||
$time_columns = \App\RSpade\Core\Manifest\Manifest::db_get_columns_by_type($table_name, 'time');
|
$time_columns = \App\RSpade\Core\Manifest\Manifest::db_get_columns_by_type($table_name, 'time');
|
||||||
foreach ($time_columns as $column) {
|
foreach ($time_columns as $column) {
|
||||||
if (!isset($casts[$column])) {
|
if (!isset($casts[$column])) {
|
||||||
$casts[$column] = 'time';
|
$casts[$column] = 'string';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
118
app/RSpade/Core/Time/Rsx_DateTime_Cast.php
Executable file
118
app/RSpade/Core/Time/Rsx_DateTime_Cast.php
Executable file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\RSpade\Core\Time;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\RSpade\Core\Time\Rsx_Time;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Eloquent cast for DATETIME/TIMESTAMP columns
|
||||||
|
*
|
||||||
|
* Converts between:
|
||||||
|
* - Database: MySQL format "YYYY-MM-DD HH:MM:SS" (stored as UTC)
|
||||||
|
* - PHP/JSON: ISO 8601 format "YYYY-MM-DDTHH:MM:SS.sssZ" (UTC)
|
||||||
|
*
|
||||||
|
* Returns strings, NOT Carbon objects. This keeps PHP/JS in sync
|
||||||
|
* with identical string formats on both sides.
|
||||||
|
*/
|
||||||
|
#[Instantiatable]
|
||||||
|
class Rsx_DateTime_Cast implements CastsAttributes
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Cast the value when reading from database
|
||||||
|
*
|
||||||
|
* Converts MySQL format to ISO 8601 UTC string
|
||||||
|
*
|
||||||
|
* @param Model $model
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
* @param array $attributes
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function get(Model $model, string $key, mixed $value, array $attributes): ?string
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already ISO format (from cached value or manual set)
|
||||||
|
if (is_string($value) && preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/', $value)) {
|
||||||
|
// Normalize to consistent format with milliseconds and Z
|
||||||
|
return Rsx_Time::to_iso(Rsx_Time::parse($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// MySQL format "YYYY-MM-DD HH:MM:SS" - convert to ISO
|
||||||
|
if (is_string($value) && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $value)) {
|
||||||
|
// Database stores in UTC, parse as UTC
|
||||||
|
$carbon = Carbon::createFromFormat(
|
||||||
|
strlen($value) > 19 ? 'Y-m-d H:i:s.v' : 'Y-m-d H:i:s',
|
||||||
|
$value,
|
||||||
|
'UTC'
|
||||||
|
);
|
||||||
|
return $carbon->format('Y-m-d\TH:i:s.v\Z');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carbon object (shouldn't happen but handle gracefully during transition)
|
||||||
|
if ($value instanceof Carbon) {
|
||||||
|
return $value->setTimezone('UTC')->format('Y-m-d\TH:i:s.v\Z');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unexpected type - fail loud
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"Rsx_DateTime_Cast: Expected datetime string, got " . gettype($value) . ": " . var_export($value, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast the value when writing to database
|
||||||
|
*
|
||||||
|
* Converts ISO 8601 format to MySQL format for storage
|
||||||
|
*
|
||||||
|
* @param Model $model
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
* @param array $attributes
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function set(Model $model, string $key, mixed $value, array $attributes): ?string
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISO 8601 format - convert to MySQL format
|
||||||
|
if (is_string($value) && preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/', $value)) {
|
||||||
|
$carbon = Carbon::parse($value)->setTimezone('UTC');
|
||||||
|
return $carbon->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
// MySQL format already - validate and pass through
|
||||||
|
if (is_string($value) && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unix timestamp (seconds or milliseconds)
|
||||||
|
if (is_int($value)) {
|
||||||
|
// Detect milliseconds vs seconds
|
||||||
|
if ($value > 10000000000) {
|
||||||
|
$carbon = Carbon::createFromTimestampMs($value, 'UTC');
|
||||||
|
} else {
|
||||||
|
$carbon = Carbon::createFromTimestamp($value, 'UTC');
|
||||||
|
}
|
||||||
|
return $carbon->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carbon object (shouldn't happen but handle gracefully during transition)
|
||||||
|
if ($value instanceof Carbon) {
|
||||||
|
return $value->setTimezone('UTC')->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject other types - fail loud
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"Rsx_DateTime_Cast: Invalid datetime format for '$key': " . var_export($value, true) .
|
||||||
|
". Expected ISO 8601 string (YYYY-MM-DDTHH:MM:SS.sssZ) or Unix timestamp."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
app/RSpade/Core/Time/Rsx_Date_Cast.php
Executable file
87
app/RSpade/Core/Time/Rsx_Date_Cast.php
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\RSpade\Core\Time;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\RSpade\Core\Time\Rsx_Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Eloquent cast for DATE columns
|
||||||
|
*
|
||||||
|
* Returns dates as "YYYY-MM-DD" strings, NOT Carbon objects.
|
||||||
|
* This prevents timezone bugs - a date is just a calendar day, not a moment in time.
|
||||||
|
*
|
||||||
|
* Database: 2025-12-24 → PHP: "2025-12-24" → JSON: "2025-12-24"
|
||||||
|
*/
|
||||||
|
#[Instantiatable]
|
||||||
|
class Rsx_Date_Cast implements CastsAttributes
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Cast the value when reading from database
|
||||||
|
*
|
||||||
|
* @param Model $model
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
* @param array $attributes
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function get(Model $model, string $key, mixed $value, array $attributes): ?string
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already a string in YYYY-MM-DD format from MySQL
|
||||||
|
if (is_string($value)) {
|
||||||
|
// Validate it's a proper date format
|
||||||
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MySQL might return datetime format for some reason - extract date part
|
||||||
|
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $value, $matches)) {
|
||||||
|
return $matches[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unexpected type - fail loud
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"Rsx_Date_Cast: Expected date string, got " . gettype($value) . ": " . var_export($value, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast the value when writing to database
|
||||||
|
*
|
||||||
|
* @param Model $model
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
* @param array $attributes
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function set(Model $model, string $key, mixed $value, array $attributes): ?string
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept YYYY-MM-DD strings
|
||||||
|
if (is_string($value)) {
|
||||||
|
// Validate format
|
||||||
|
$parsed = Rsx_Date::parse($value);
|
||||||
|
if ($parsed === null) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"Rsx_Date_Cast: Invalid date format for '$key': '$value'. Expected YYYY-MM-DD."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject Carbon and other types - fail loud
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"Rsx_Date_Cast: Expected date string (YYYY-MM-DD), got " . gettype($value) .
|
||||||
|
". Use Rsx_Date::today() or string literals, not Carbon objects."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,14 +11,16 @@ use App\RSpade\Core\Session\Session;
|
|||||||
* Datetimes represent specific moments in time. They always have a time component
|
* Datetimes represent specific moments in time. They always have a time component
|
||||||
* and are timezone-aware. Stored in UTC, displayed in user's timezone.
|
* and are timezone-aware. Stored in UTC, displayed in user's timezone.
|
||||||
*
|
*
|
||||||
* All methods are static. Time values are represented as:
|
* STRINGS, NOT CARBON: All external-facing methods return ISO 8601 strings
|
||||||
* - Carbon objects (internal)
|
* (format: "2024-12-24T15:30:45.123Z"). This keeps PHP and JavaScript in sync
|
||||||
* - ISO 8601 strings for serialization: "2024-12-24T15:30:45.123Z"
|
* with identical formats on both sides. Carbon is used internally for calculations.
|
||||||
* - Unix timestamps (milliseconds) for JavaScript interop
|
*
|
||||||
|
* Model properties ($model->created_at) also return ISO strings via Rsx_DateTime_Cast.
|
||||||
*
|
*
|
||||||
* Core Principles:
|
* Core Principles:
|
||||||
* - All datetimes stored in database as UTC
|
* - All datetimes stored in database as UTC
|
||||||
* - All serialization uses ISO 8601 format
|
* - All serialization uses ISO 8601 format
|
||||||
|
* - External APIs return strings, not Carbon
|
||||||
* - User timezone stored per user (login_users.timezone)
|
* - User timezone stored per user (login_users.timezone)
|
||||||
* - Formatting happens on-demand, not on storage
|
* - Formatting happens on-demand, not on storage
|
||||||
* - PHP and JS APIs are parallel (same method names)
|
* - PHP and JS APIs are parallel (same method names)
|
||||||
@@ -372,15 +374,15 @@ class Rsx_Time
|
|||||||
*
|
*
|
||||||
* @param mixed $time
|
* @param mixed $time
|
||||||
* @param int $seconds
|
* @param int $seconds
|
||||||
* @return Carbon
|
* @return string ISO 8601 UTC string
|
||||||
*/
|
*/
|
||||||
public static function add($time, int $seconds): Carbon
|
public static function add($time, int $seconds): string
|
||||||
{
|
{
|
||||||
$carbon = static::parse($time);
|
$carbon = static::parse($time);
|
||||||
if (!$carbon) {
|
if (!$carbon) {
|
||||||
throw new \InvalidArgumentException("Cannot parse time");
|
throw new \InvalidArgumentException("Cannot parse time");
|
||||||
}
|
}
|
||||||
return $carbon->addSeconds($seconds);
|
return $carbon->addSeconds($seconds)->format('Y-m-d\TH:i:s.v\Z');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -388,9 +390,9 @@ class Rsx_Time
|
|||||||
*
|
*
|
||||||
* @param mixed $time
|
* @param mixed $time
|
||||||
* @param int $seconds
|
* @param int $seconds
|
||||||
* @return Carbon
|
* @return string ISO 8601 UTC string
|
||||||
*/
|
*/
|
||||||
public static function subtract($time, int $seconds): Carbon
|
public static function subtract($time, int $seconds): string
|
||||||
{
|
{
|
||||||
return static::add($time, -$seconds);
|
return static::add($time, -$seconds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,55 @@ Optional category prefix groups related expectations:
|
|||||||
EXPECT: Honor user timezone preference
|
EXPECT: Honor user timezone preference
|
||||||
...
|
...
|
||||||
|
|
||||||
|
TODO EXPECTATIONS
|
||||||
|
|
||||||
|
Use TODO: prefix for planned functionality that doesn't exist yet:
|
||||||
|
|
||||||
|
TODO: Batch timezone conversion
|
||||||
|
GIVEN: Array of timestamps
|
||||||
|
WHEN: Passed to convert_batch()
|
||||||
|
THEN: All timestamps converted in single operation
|
||||||
|
---
|
||||||
|
|
||||||
|
TODO: Timezone autodetection from browser
|
||||||
|
GIVEN: No user timezone preference set
|
||||||
|
WHEN: User visits page for first time
|
||||||
|
THEN: Browser timezone is detected and stored
|
||||||
|
---
|
||||||
|
|
||||||
|
TODO expectations document future requirements without implying the feature
|
||||||
|
exists. The test runner will skip these until converted to EXPECT blocks.
|
||||||
|
|
||||||
|
COMMENTS AND NOTES
|
||||||
|
|
||||||
|
Expect files can include freeform comments and explanations. These help
|
||||||
|
when eventually writing tests by capturing context, edge cases, and
|
||||||
|
implementation hints.
|
||||||
|
|
||||||
|
Use # for inline comments within blocks:
|
||||||
|
|
||||||
|
EXPECT: Parse Unix timestamps
|
||||||
|
GIVEN: Integer 1703432400
|
||||||
|
WHEN: Passed to parse()
|
||||||
|
THEN: Returns correct datetime
|
||||||
|
# Note: Must detect seconds vs milliseconds automatically
|
||||||
|
# Threshold: values > 10 billion are milliseconds
|
||||||
|
---
|
||||||
|
|
||||||
|
Use plain text between blocks for extended notes:
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
The following expectations cover boundary conditions. When implementing
|
||||||
|
tests, pay special attention to timezone transitions (DST changes) and
|
||||||
|
leap seconds. The framework deliberately ignores leap seconds.
|
||||||
|
|
||||||
|
EXPECT: Handle DST transition
|
||||||
|
...
|
||||||
|
|
||||||
|
These comments are preserved in the file but ignored by the test runner.
|
||||||
|
They serve as institutional knowledge for future test authors.
|
||||||
|
|
||||||
FUTURE: AUTOMATED TEST RUNNER
|
FUTURE: AUTOMATED TEST RUNNER
|
||||||
|
|
||||||
The planned test runner will:
|
The planned test runner will:
|
||||||
|
|||||||
@@ -44,6 +44,24 @@ DESCRIPTION
|
|||||||
"December 24, 2025" is the same day everywhere.
|
"December 24, 2025" is the same day everywhere.
|
||||||
Format: Always "YYYY-MM-DD" (e.g., "2024-12-24")
|
Format: Always "YYYY-MM-DD" (e.g., "2024-12-24")
|
||||||
|
|
||||||
|
STRINGS, NOT OBJECTS
|
||||||
|
All external-facing APIs return STRINGS, not Carbon or Date objects:
|
||||||
|
|
||||||
|
$model->created_at // "2024-12-24T15:30:45.123Z" (string)
|
||||||
|
$model->due_date // "2024-12-24" (string)
|
||||||
|
Rsx_Time::now_iso() // "2024-12-24T15:30:45.123Z" (string)
|
||||||
|
Rsx_Time::to_iso($x) // "2024-12-24T15:30:45.123Z" (string)
|
||||||
|
Rsx_Time::add($x, 60) // "2024-12-24T15:31:45.123Z" (string)
|
||||||
|
|
||||||
|
This design keeps PHP and JavaScript in sync - identical string formats
|
||||||
|
on both sides, no serialization surprises.
|
||||||
|
|
||||||
|
Carbon is used internally for calculations, but never exposed externally.
|
||||||
|
The only method returning Carbon is parse(), for internal calculations:
|
||||||
|
|
||||||
|
$carbon = Rsx_Time::parse($time); // Carbon for calculations
|
||||||
|
$result = Rsx_Time::to_iso($carbon); // Back to string for output
|
||||||
|
|
||||||
CRITICAL: Type Separation
|
CRITICAL: Type Separation
|
||||||
Date functions THROW if passed a datetime.
|
Date functions THROW if passed a datetime.
|
||||||
Datetime functions THROW if passed a date-only string.
|
Datetime functions THROW if passed a date-only string.
|
||||||
@@ -111,19 +129,22 @@ RSX_DATE CLASS
|
|||||||
Returns "YYYY-MM-DD" for database storage (same as ISO format).
|
Returns "YYYY-MM-DD" for database storage (same as ISO format).
|
||||||
|
|
||||||
RSX_TIME CLASS
|
RSX_TIME CLASS
|
||||||
All functions work with ISO 8601 datetime strings or Carbon/Date objects.
|
All functions accept ISO 8601 datetime strings or Carbon/Date objects.
|
||||||
|
All functions RETURN strings (except parse, which returns Carbon for
|
||||||
|
internal calculations).
|
||||||
|
|
||||||
Parsing & Validation
|
Parsing & Validation
|
||||||
parse($input)
|
parse($input)
|
||||||
Returns Carbon (PHP) or Date (JS) in UTC.
|
Returns Carbon (PHP) or Date (JS) in UTC.
|
||||||
|
Used for internal calculations, not for output.
|
||||||
THROWS on date-only string input.
|
THROWS on date-only string input.
|
||||||
|
|
||||||
is_datetime($input)
|
is_datetime($input)
|
||||||
Returns true if input is valid datetime (not date-only).
|
Returns true if input is valid datetime (not date-only).
|
||||||
|
|
||||||
Current Time
|
Current Time
|
||||||
now() Returns current time as Carbon/Date (UTC)
|
now() Returns current time as Carbon/Date (for calculations)
|
||||||
now_iso() Returns current time as ISO 8601 string
|
now_iso() Returns current time as ISO 8601 string (preferred)
|
||||||
now_ms() Returns current time as Unix milliseconds
|
now_ms() Returns current time as Unix milliseconds
|
||||||
|
|
||||||
Timezone Handling
|
Timezone Handling
|
||||||
@@ -175,10 +196,10 @@ RSX_TIME CLASS
|
|||||||
|
|
||||||
Arithmetic
|
Arithmetic
|
||||||
add($time, $seconds)
|
add($time, $seconds)
|
||||||
Add seconds to time.
|
Add seconds to time. Returns ISO 8601 string.
|
||||||
|
|
||||||
subtract($time, $seconds)
|
subtract($time, $seconds)
|
||||||
Subtract seconds from time.
|
Subtract seconds from time. Returns ISO 8601 string.
|
||||||
|
|
||||||
Comparison
|
Comparison
|
||||||
is_past($time) True if datetime is in the past
|
is_past($time) True if datetime is in the past
|
||||||
@@ -267,6 +288,7 @@ DATA FLOW
|
|||||||
Database: DATE column, value "2025-12-24"
|
Database: DATE column, value "2025-12-24"
|
||||||
|
|
|
|
||||||
PHP Model: $task->due_date = "2025-12-24" (string)
|
PHP Model: $task->due_date = "2025-12-24" (string)
|
||||||
|
| (Rsx_Date_Cast returns YYYY-MM-DD strings, NOT Carbon)
|
||||||
|
|
|
|
||||||
JSON Response: {"due_date": "2025-12-24"}
|
JSON Response: {"due_date": "2025-12-24"}
|
||||||
|
|
|
|
||||||
@@ -284,7 +306,8 @@ DATA FLOW
|
|||||||
|
|
||||||
Database: DATETIME(3), value "2025-12-24 15:30:45.123" (UTC)
|
Database: DATETIME(3), value "2025-12-24 15:30:45.123" (UTC)
|
||||||
|
|
|
|
||||||
PHP Model: $event->scheduled_at = Carbon instance (UTC)
|
PHP Model: $event->scheduled_at = "2025-12-24T15:30:45.123Z" (string)
|
||||||
|
| (Rsx_DateTime_Cast converts MySQL format to ISO 8601)
|
||||||
|
|
|
|
||||||
JSON Serialize: {"scheduled_at": "2025-12-24T15:30:45.123Z"}
|
JSON Serialize: {"scheduled_at": "2025-12-24T15:30:45.123Z"}
|
||||||
|
|
|
|
||||||
@@ -298,6 +321,7 @@ DATA FLOW
|
|||||||
Form Submit: {"scheduled_at": "2025-12-24T16:00:00.000Z"}
|
Form Submit: {"scheduled_at": "2025-12-24T16:00:00.000Z"}
|
||||||
|
|
|
|
||||||
PHP Controller: Rsx_Time::parse($params['scheduled_at']) -> Carbon
|
PHP Controller: Rsx_Time::parse($params['scheduled_at']) -> Carbon
|
||||||
|
| (only if calculations needed; can also save string directly)
|
||||||
|
|
|
|
||||||
Database: "2025-12-24 16:00:00.000"
|
Database: "2025-12-24 16:00:00.000"
|
||||||
|
|
||||||
@@ -374,8 +398,38 @@ CONFIGURATION
|
|||||||
|
|
||||||
User timezone stored in login_users.timezone column.
|
User timezone stored in login_users.timezone column.
|
||||||
|
|
||||||
|
MODEL CASTING
|
||||||
|
Model date/datetime columns are automatically cast to strings via custom
|
||||||
|
Eloquent casts. No configuration needed - columns are detected from the
|
||||||
|
database schema.
|
||||||
|
|
||||||
|
Rsx_DateTime_Cast (for DATETIME/TIMESTAMP columns)
|
||||||
|
- Database read: "2024-12-24 15:30:45" -> "2024-12-24T15:30:45.000Z"
|
||||||
|
- Database write: "2024-12-24T15:30:45.000Z" -> "2024-12-24 15:30:45"
|
||||||
|
- Accepts: ISO 8601 strings, MySQL datetime strings, Unix timestamps
|
||||||
|
- Rejects: Carbon objects (use to_iso() to convert first)
|
||||||
|
|
||||||
|
Rsx_Date_Cast (for DATE columns)
|
||||||
|
- Database read: "2024-12-24" -> "2024-12-24"
|
||||||
|
- Database write: "2024-12-24" -> "2024-12-24"
|
||||||
|
- Accepts: YYYY-MM-DD strings only
|
||||||
|
- Rejects: Carbon objects, datetime strings
|
||||||
|
|
||||||
|
Why strings instead of Carbon?
|
||||||
|
1. Prevents timezone bugs - dates have no timezone, Carbon assumes one
|
||||||
|
2. PHP/JS parity - identical formats on both sides
|
||||||
|
3. No serialization surprises - what you see is what you get
|
||||||
|
4. Simpler mental model - just strings everywhere
|
||||||
|
|
||||||
|
Overriding casts:
|
||||||
|
If a model needs different behavior, override $casts in the model:
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'special_date' => 'date', // Use Laravel's default
|
||||||
|
];
|
||||||
|
|
||||||
SEE ALSO
|
SEE ALSO
|
||||||
Reference document: /var/www/html/date_vs_datetime_refactor.md
|
Rsx_Time, Rsx_Date, Rsx_DateTime_Cast, Rsx_Date_Cast
|
||||||
|
|
||||||
AUTHOR
|
AUTHOR
|
||||||
RSpade Framework
|
RSpade Framework
|
||||||
|
|||||||
Reference in New Issue
Block a user