Skip to content

Commit 094a06d

Browse files
committed
Revert testNow behavior in createFromFormat for BC
The change in 3.3.2 that made createFromFormat respect testNow for missing components was a BC break. Users expect createFromFormat to behave exactly like PHP's native DateTimeImmutable::createFromFormat. This reverts that behavior so createFromFormat uses PHP's native behavior (missing components come from current time, not testNow). testNow should only affect methods explicitly designed for it like now(), parse(), etc.
1 parent 77a248b commit 094a06d

File tree

2 files changed

+8
-187
lines changed

2 files changed

+8
-187
lines changed

src/Chronos.php

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -660,98 +660,9 @@ public static function createFromFormat(
660660
throw new InvalidArgumentException($message);
661661
}
662662

663-
$testNow = static::getTestNow();
664-
if ($testNow !== null) {
665-
$dateTime = static::applyTestNowToMissingComponents($dateTime, $format, $testNow);
666-
}
667-
668663
return $dateTime;
669664
}
670665

671-
/**
672-
* Apply testNow values to date/time components that weren't in the format string.
673-
*
674-
* @param static $dateTime The parsed datetime instance.
675-
* @param string $format The format string used for parsing.
676-
* @param \Cake\Chronos\Chronos $testNow The test now instance.
677-
* @return static
678-
*/
679-
protected static function applyTestNowToMissingComponents(
680-
self $dateTime,
681-
string $format,
682-
Chronos $testNow,
683-
): static {
684-
// Parse format string to find which characters are actual format specifiers (not escaped)
685-
$formatChars = static::getFormatCharacters($format);
686-
687-
// Check which components are present in the format
688-
$hasYear = (bool)array_intersect($formatChars, ['Y', 'y', 'o', 'X', 'x']);
689-
$hasMonth = (bool)array_intersect($formatChars, ['m', 'n', 'M', 'F']);
690-
$hasDay = (bool)array_intersect($formatChars, ['d', 'j', 'D', 'l', 'N', 'z', 'w', 'W', 'S']);
691-
$hasHour = (bool)array_intersect($formatChars, ['H', 'G', 'h', 'g']);
692-
$hasMinute = (bool)array_intersect($formatChars, ['i']);
693-
$hasSecond = (bool)array_intersect($formatChars, ['s']);
694-
$hasMicro = (bool)array_intersect($formatChars, ['u', 'v']);
695-
696-
// If the format includes '!' or '|', PHP resets unspecified components to Unix epoch or zero
697-
// If 'U' is present, all components are set from the Unix timestamp
698-
// In these cases, we should not override with testNow
699-
$hasReset = in_array('!', $formatChars, true) || in_array('|', $formatChars, true);
700-
$hasUnixTimestamp = in_array('U', $formatChars, true);
701-
if ($hasReset || $hasUnixTimestamp) {
702-
return $dateTime;
703-
}
704-
705-
// Replace missing components with testNow values
706-
$year = $hasYear ? $dateTime->year : $testNow->year;
707-
$month = $hasMonth ? $dateTime->month : $testNow->month;
708-
$day = $hasDay ? $dateTime->day : $testNow->day;
709-
$hour = $hasHour ? $dateTime->hour : $testNow->hour;
710-
$minute = $hasMinute ? $dateTime->minute : $testNow->minute;
711-
$second = $hasSecond ? $dateTime->second : $testNow->second;
712-
$micro = $hasMicro ? $dateTime->micro : $testNow->micro;
713-
714-
// Only modify if something needs to change
715-
if (
716-
!$hasYear || !$hasMonth || !$hasDay ||
717-
!$hasHour || !$hasMinute || !$hasSecond || !$hasMicro
718-
) {
719-
return $dateTime
720-
->setDate($year, $month, $day)
721-
->setTime($hour, $minute, $second, $micro);
722-
}
723-
724-
return $dateTime;
725-
}
726-
727-
/**
728-
* Extract format characters from a format string, handling escapes.
729-
*
730-
* @param string $format The format string.
731-
* @return array<string> Array of format characters.
732-
*/
733-
protected static function getFormatCharacters(string $format): array
734-
{
735-
$chars = [];
736-
$length = strlen($format);
737-
$i = 0;
738-
739-
while ($i < $length) {
740-
$char = $format[$i];
741-
742-
// Backslash escapes the next character
743-
if ($char === '\\' && $i + 1 < $length) {
744-
$i += 2;
745-
continue;
746-
}
747-
748-
$chars[] = $char;
749-
$i++;
750-
}
751-
752-
return $chars;
753-
}
754-
755666
/**
756667
* Returns parse warnings and errors from the last ``createFromFormat()``
757668
* call.

tests/TestCase/DateTime/CreateFromFormatTest.php

Lines changed: 8 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -29,104 +29,6 @@ public function testCreateFromFormatReturnsInstance()
2929
$this->assertTrue($d instanceof Chronos);
3030
}
3131

32-
public function testCreateFromFormatWithTestNowMissingYear()
33-
{
34-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
35-
$d = Chronos::createFromFormat('m-d H:i:s', '10-05 09:15:30');
36-
$this->assertDateTime($d, 2020, 10, 5, 9, 15, 30);
37-
}
38-
39-
public function testCreateFromFormatWithTestNowMissingDate()
40-
{
41-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
42-
$d = Chronos::createFromFormat('H:i:s', '09:15:30');
43-
$this->assertDateTime($d, 2020, 12, 1, 9, 15, 30);
44-
}
45-
46-
public function testCreateFromFormatWithTestNowMissingTime()
47-
{
48-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
49-
$d = Chronos::createFromFormat('Y-m-d', '2021-06-15');
50-
$this->assertDateTime($d, 2021, 6, 15, 14, 30, 45);
51-
}
52-
53-
public function testCreateFromFormatWithTestNowPartialDate()
54-
{
55-
Chronos::setTestNow(new Chronos('2020-12-01 00:00:00'));
56-
$d = Chronos::createFromFormat('m-d', '10-05');
57-
$this->assertDateTime($d, 2020, 10, 5, 0, 0, 0);
58-
}
59-
60-
public function testCreateFromFormatWithTestNowDayOnly()
61-
{
62-
Chronos::setTestNow(new Chronos('2020-12-01 00:00:00'));
63-
$d = Chronos::createFromFormat('d', '05');
64-
$this->assertDateTime($d, 2020, 12, 5, 0, 0, 0);
65-
}
66-
67-
public function testCreateFromFormatWithTestNowComplete()
68-
{
69-
// When format is complete, testNow should not affect the result
70-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
71-
$d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11');
72-
$this->assertDateTime($d, 1975, 5, 21, 22, 32, 11);
73-
}
74-
75-
public function testCreateFromFormatWithTestNowResetModifier()
76-
{
77-
// The '!' modifier resets to Unix epoch, should not use testNow
78-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
79-
$d = Chronos::createFromFormat('!Y-m-d', '2021-06-15');
80-
$this->assertDateTime($d, 2021, 6, 15, 0, 0, 0);
81-
}
82-
83-
public function testCreateFromFormatWithTestNowPipeModifier()
84-
{
85-
// The '|' modifier resets unspecified components to zero, should not use testNow
86-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
87-
$d = Chronos::createFromFormat('Y-m-d|', '2021-06-15');
88-
$this->assertDateTime($d, 2021, 6, 15, 0, 0, 0);
89-
}
90-
91-
public function testCreateFromFormatWithoutTestNow()
92-
{
93-
// Without testNow set, behavior should use real current time for missing components
94-
Chronos::setTestNow(null);
95-
$d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11');
96-
$this->assertDateTime($d, 1975, 5, 21, 22, 32, 11);
97-
}
98-
99-
public function testCreateFromFormatWithTestNowEscapedCharacters()
100-
{
101-
// Escaped format characters should not be treated as format specifiers
102-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
103-
$d = Chronos::createFromFormat('\Y\-m-d', 'Y-10-05');
104-
$this->assertDateTime($d, 2020, 10, 5, 14, 30, 45);
105-
}
106-
107-
public function testCreateFromFormatWithTestNowMicroseconds()
108-
{
109-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45.123456'));
110-
$d = Chronos::createFromFormat('Y-m-d H:i:s', '2021-06-15 09:15:30');
111-
$this->assertSame(123456, $d->micro);
112-
}
113-
114-
public function testCreateFromFormatWithTestNowUnixTimestamp()
115-
{
116-
// Unix timestamp ('U' format) sets all components, should not use testNow
117-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
118-
$d = Chronos::createFromFormat('U', '0');
119-
$this->assertDateTime($d, 1970, 1, 1, 0, 0, 0);
120-
}
121-
122-
public function testCreateFromFormatWithTestNowNegativeUnixTimestamp()
123-
{
124-
// Negative Unix timestamp should also not use testNow
125-
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
126-
$d = Chronos::createFromFormat('U', '-1000');
127-
$this->assertDateTime($d, 1969, 12, 31, 23, 43, 20);
128-
}
129-
13032
public function testCreateFromFormatWithTimezoneString()
13133
{
13234
$d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11', 'Europe/London');
@@ -160,4 +62,12 @@ public function testCreateFromFormatInvalidFormat()
16062
$this->assertIsArray(Chronos::getLastErrors());
16163
$this->assertNotEmpty(Chronos::getLastErrors()['errors']);
16264
}
65+
66+
public function testCreateFromFormatDoesNotUseTestNow()
67+
{
68+
// createFromFormat should not use testNow - it should behave like PHP's native method
69+
Chronos::setTestNow(new Chronos('2020-12-01 14:30:45'));
70+
$d = Chronos::createFromFormat('Y-m-d H:i:s', '1975-05-21 22:32:11');
71+
$this->assertDateTime($d, 1975, 5, 21, 22, 32, 11);
72+
}
16373
}

0 commit comments

Comments
 (0)