Skip to content

4.x: Auto-append pipe modifier in createFromFormat#504

Open
dereuromark wants to merge 1 commit into4.xfrom
auto-append-pipe-modifier
Open

4.x: Auto-append pipe modifier in createFromFormat#504
dereuromark wants to merge 1 commit into4.xfrom
auto-append-pipe-modifier

Conversation

@dereuromark
Copy link
Member

@dereuromark dereuromark commented Mar 14, 2026

Attempt of #494 redo of "deterministic behavior"

Summary

Auto-append | modifier in createFromFormat() for deterministic behavior.

The Change

Unlike PHP's native DateTimeImmutable::createFromFormat(), this method now automatically appends the | modifier if no reset modifier (| or !) is present.

This ensures that unparsed components are reset to zero instead of being filled from the current time.

The 4.x philosophy: Always deterministic, everywhere

Examples

// Before (PHP native behavior): time filled from current system time
// After (Chronos 4.x): time is 00:00:00.000000
Chronos::createFromFormat('Y-m-d', '2024-03-14');

// Before: seconds filled from current system time
// After: seconds is 00
Chronos::createFromFormat('Y-m-d H:i', '2024-03-14 12:30');

// Missing date components use Unix epoch (1970-01-01)
Chronos::createFromFormat('H:i:s', '12:30:45');
// Result: 1970-01-01 12:30:45

This is what most users intuitively expect. When you parse just a date, you get midnight - not "midnight plus whatever seconds happen to be on the clock."

The only "BC break" scenario is someone who intentionally wanted:

  // "Give me this date but with current time" (unusual use case)                                                                                         
  $d = Chronos::createFromFormat('Y-m-d', '2024-03-14');                                                                                                  
  // They expected 2024-03-14 14:32:45 (current time)                                                                                                     
  // Now they get 2024-03-14 00:00:00                                                                                                                     

That's a rare/unusual pattern. If someone actually needs current time for missing components, they'd explicitly do:

  $d = Chronos::parse('2024-03-14'); // This keeps current time                                                                                           
  // or construct it explicitly    

Benefits

  • Deterministic: No more flaky tests due to time-dependent behavior
  • Intuitive: Missing = zero is what most users expect
  • No workarounds needed: No need to add | to format strings manually

Migration

Test Code that relied on missing components being filled from current time will need adjustment.
But this "test" BC break is more theoretical than practical:

  1. People who wanted zeros → already using setTestNow() or adding | manually
  2. People who wanted current time → this is the edge case that breaks, but it's likely unintentional/accidental rather than deliberate

@dereuromark dereuromark changed the title 4.x: Auto-append pipe modifier in createFromFormat 3next: Auto-append pipe modifier in createFromFormat Mar 14, 2026
@dereuromark dereuromark changed the title 3next: Auto-append pipe modifier in createFromFormat 4.x: Auto-append pipe modifier in createFromFormat Mar 14, 2026
@dereuromark dereuromark changed the base branch from 3.next to 4.x March 14, 2026 17:23
@dereuromark dereuromark added this to the 4.x milestone Mar 14, 2026
Unlike PHP's native DateTimeImmutable::createFromFormat(), this method
now automatically appends the | modifier if no reset modifier (| or \!)
is present. This ensures that unparsed components are reset to zero
instead of being filled from the current time.

This provides more predictable and deterministic behavior, especially
useful for testing where flaky tests can occur due to missing components
being filled with the current system time.

Examples:
- createFromFormat('Y-m-d', '2024-03-14') -> time is 00:00:00 (not current)
- createFromFormat('Y-m-d H:i', '2024-03-14 12:30') -> seconds is 00

This is a behavior change from PHP's native method, but arguably the
more intuitive default.
@dereuromark dereuromark force-pushed the auto-append-pipe-modifier branch from 93101ba to a7011fc Compare March 14, 2026 17:25
@dereuromark dereuromark requested a review from markstory March 14, 2026 17:25
@dereuromark
Copy link
Member Author

Approach Comparison

Behavior Matrix

Scenario PHP Native #494 (testNow) #504 (auto-pipe) Carbon
No testNow, Y-m-d current time current time 00:00:00 current time
testNow=14:30:45, Y-m-d current time 14:30:45 00:00:00 14:30:45
No testNow, Y-m-d H:i current seconds current seconds :00 current seconds
testNow, Y-m-d H:i current seconds testNow seconds :00 testNow seconds
Format with | zero zero zero zero
Format with ! zero zero zero zero

Feature Comparison

Aspect #494 (testNow) #504 (auto-pipe)
Test determinism Yes (with setTestNow) Yes (always)
Production determinism No Yes
Matches Carbon Yes No
Matches PHP native Yes (without testNow) No
Test = Production behavior No Yes
Format parsing required Yes (complex) No (simple)
Edge cases to handle U, U.u, etc. None

Pros and Cons

#494 (testNow approach)

Pros:

  • Matches Carbon behavior - familiar to users migrating
  • Less BC break in production (native PHP behavior preserved when no testNow)
  • Only changes behavior in test context

Cons:

  • Production still has footgun (missing = random current time)
  • Tests behave differently than production (could mask bugs)
  • Complex implementation (must parse format specifiers)
  • Edge cases like U need special handling (Fix createFromFormat with Unix timestamp format 'U' #502)
  • More code to maintain

#504 (auto-append pipe)

Pros:

  • Deterministic everywhere - no surprises in tests OR production
  • Test behavior = production behavior (what you test is what you get)
  • Simple implementation (just append | if no reset modifier)
  • No edge cases - PHP's | modifier handles all formats correctly
  • Less code to maintain

Cons:

  • BC break for anyone relying on "missing = current time" (rare)
  • Different from Carbon behavior
  • Different from PHP native behavior

Implementation Complexity

#494 (testNow) - Format Parsing Required

// Must detect which components are present in format
// Handle escaped characters (\Y, \\, etc.)
// Special cases: U, U.u, timezone formats
// ~50+ lines of parsing logic

#504 (auto-pipe) - Simple String Check

if (!str_contains($format, '|') && !str_contains($format, '!')) {
    $format .= '|';
}
// 3 lines, delegates complexity to PHP

Real-World Impact

Who is affected by #504 (auto-pipe)?

Code that intentionally does this:

// "Give me this date with current system time"
$d = Chronos::createFromFormat('Y-m-d', $dateString);
// Expected: 2024-03-14 14:32:45 (current time)
// With #504: 2024-03-14 00:00:00

This is a rare/unusual pattern. More common is users being surprised that they got random seconds.

Who is affected by #494 (testNow)?

Code that uses createFromFormat without setTestNow:

  • No change (native PHP behavior)

Code that uses createFromFormat WITH setTestNow:

  • Missing components now come from testNow instead of current time
  • This is usually the desired behavior for tests

@dereuromark dereuromark marked this pull request as ready for review March 14, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant