Browse Source

Support Chronos 3 change to not extend DateTimeImmutable

Corey Taylor 3 years ago
parent
commit
78480cd632

+ 1 - 1
composer.json

@@ -115,7 +115,7 @@
         ],
         "stan-tests": "phpstan.phar analyze -c tests/phpstan.neon",
         "stan-baseline": "phpstan.phar --generate-baseline",
-        "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:~1.9.0 psalm/phar:~5.0-beta && mv composer.backup composer.json",
+        "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:~1.9.0 psalm/phar:5.0.0-beta1 && mv composer.backup composer.json",
         "lowest": "validate-prefer-lowest",
         "lowest-setup": "composer update --prefer-lowest --prefer-stable --prefer-dist --no-interaction && cp composer.json composer.backup && composer require --dev dereuromark/composer-prefer-lowest && mv composer.backup composer.json",
         "test": "phpunit",

+ 0 - 20
phpstan-baseline.neon

@@ -56,26 +56,6 @@ parameters:
 			path: src/Http/Session.php
 
 		-
-			message: "#^Call to an undefined method Cake\\\\Chronos\\\\DifferenceFormatterInterface\\:\\:dateAgoInWords\\(\\)\\.$#"
-			count: 1
-			path: src/I18n/Date.php
-
-		-
-			message: "#^Unsafe usage of new static\\(\\)\\.$#"
-			count: 1
-			path: src/I18n/Date.php
-
-		-
-			message: "#^Call to an undefined method Cake\\\\Chronos\\\\DifferenceFormatterInterface\\:\\:timeAgoInWords\\(\\)\\.$#"
-			count: 1
-			path: src/I18n/DateTime.php
-
-		-
-			message: "#^Unsafe usage of new static\\(\\)\\.$#"
-			count: 1
-			path: src/I18n/DateTime.php
-
-		-
 			message: "#^Unsafe usage of new static\\(\\)\\.$#"
 			count: 2
 			path: src/ORM/EagerLoader.php

+ 29 - 0
psalm-baseline.xml

@@ -79,11 +79,40 @@
       <code>pid Dev</code>
     </MethodSignatureMustProvideReturnType>
   </file>
+  <file src="src/Database/Statement/SqlserverStatement.php">
+    <UndefinedConstant occurrences="1">
+      <code>PDO::SQLSRV_ENCODING_BINARY</code>
+    </UndefinedConstant>
+  </file>
   <file src="src/Datasource/Paging/PaginatedResultSet.php">
     <MethodSignatureMustProvideReturnType occurrences="1">
       <code>://cakephp.org)</code>
     </MethodSignatureMustProvideReturnType>
   </file>
+  <file src="src/I18n/Date.php">
+    <ImpureMethodCall occurrences="2">
+      <code>dateAgoInWords</code>
+      <code>diffFormatter</code>
+    </ImpureMethodCall>
+  </file>
+  <file src="src/I18n/DateFormatTrait.php">
+    <InaccessibleProperty occurrences="2">
+      <code>$this-&gt;native</code>
+      <code>$time-&gt;native</code>
+    </InaccessibleProperty>
+    <RedundantCondition occurrences="1">
+      <code>$time instanceof DateTime</code>
+    </RedundantCondition>
+    <TypeDoesNotContainType occurrences="1">
+      <code>$time instanceof DateTime</code>
+    </TypeDoesNotContainType>
+  </file>
+  <file src="src/I18n/DateTime.php">
+    <ImpureMethodCall occurrences="2">
+      <code>diffFormatter</code>
+      <code>timeAgoInWords</code>
+    </ImpureMethodCall>
+  </file>
   <file src="src/TestSuite/Constraint/EventFired.php">
     <InternalClass occurrences="1"/>
     <InternalMethod occurrences="1"/>

+ 2 - 1
src/Database/Expression/CaseExpressionTrait.php

@@ -16,6 +16,7 @@ declare(strict_types=1);
  */
 namespace Cake\Database\Expression;
 
+use Cake\Chronos\Chronos;
 use Cake\Chronos\ChronosDate;
 use Cake\Database\ExpressionInterface;
 use Cake\Database\Query;
@@ -52,7 +53,7 @@ trait CaseExpressionTrait
             $type = 'boolean';
         } elseif ($value instanceof ChronosDate) {
             $type = 'date';
-        } elseif ($value instanceof DateTimeInterface) {
+        } elseif ($value instanceof Chronos || $value instanceof DateTimeInterface) {
             $type = 'datetime';
         } elseif (
             is_object($value) &&

+ 30 - 57
src/Database/Type/DateTimeType.php

@@ -16,10 +16,10 @@ declare(strict_types=1);
  */
 namespace Cake\Database\Type;
 
+use Cake\Chronos\Chronos;
 use Cake\Database\Driver;
 use Cake\Database\Exception\DatabaseException;
 use Cake\I18n\DateTime;
-use Cake\I18n\I18nDateTimeInterface;
 use DateTime as NativeDateTime;
 use DateTimeImmutable;
 use DateTimeInterface;
@@ -36,16 +36,6 @@ use PDO;
 class DateTimeType extends BaseType implements BatchCastingInterface
 {
     /**
-     * Whether we want to override the time of the converted Time objects
-     * so it points to the start of the day.
-     *
-     * This is primarily to avoid subclasses needing to re-implement the same functionality.
-     *
-     * @var bool
-     */
-    protected bool $setToDateStart = false;
-
-    /**
      * The DateTime format used when converting to string.
      *
      * @var string
@@ -84,8 +74,7 @@ class DateTimeType extends BaseType implements BatchCastingInterface
     /**
      * The classname to use when creating objects.
      *
-     * @var string
-     * @psalm-var class-string<\DateTimeImmutable>
+     * @var class-string<\Cake\I18n\DateTime>|class-string<\DateTimeImmutable>
      */
     protected string $_className;
 
@@ -203,9 +192,9 @@ class DateTimeType extends BaseType implements BatchCastingInterface
      *
      * @param mixed $value Value to be converted to PHP equivalent
      * @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted
-     * @return \DateTimeInterface|null
+     * @return \Cake\I18n\DateTime|\DateTimeImmutable|null
      */
-    public function toPHP(mixed $value, Driver $driver): ?DateTimeInterface
+    public function toPHP(mixed $value, Driver $driver): DateTime|DateTimeImmutable|null
     {
         if ($value === null) {
             return null;
@@ -228,10 +217,6 @@ class DateTimeType extends BaseType implements BatchCastingInterface
             $instance = $instance->setTimezone($this->defaultTimezone);
         }
 
-        if ($this->setToDateStart) {
-            $instance = $instance->setTime(0, 0, 0);
-        }
-
         return $instance;
     }
 
@@ -286,10 +271,6 @@ class DateTimeType extends BaseType implements BatchCastingInterface
                 $instance = $instance->setTimezone($this->defaultTimezone);
             }
 
-            if ($this->setToDateStart) {
-                $instance = $instance->setTime(0, 0, 0);
-            }
-
             $values[$field] = $instance;
         }
 
@@ -300,11 +281,11 @@ class DateTimeType extends BaseType implements BatchCastingInterface
      * Convert request data into a datetime object.
      *
      * @param mixed $value Request data
-     * @return \DateTimeInterface|null
+     * @return \Cake\Chronos\Chronos|\DateTimeInterface|null
      */
-    public function marshal(mixed $value): ?DateTimeInterface
+    public function marshal(mixed $value): Chronos|DateTimeInterface|null
     {
-        if ($value instanceof DateTimeInterface) {
+        if ($value instanceof DateTimeInterface || $value instanceof Chronos) {
             if ($value instanceof NativeDateTime) {
                 $value = clone $value;
             }
@@ -312,17 +293,10 @@ class DateTimeType extends BaseType implements BatchCastingInterface
             return $value->setTimezone($this->defaultTimezone);
         }
 
-        /** @var class-string<\DateTimeInterface> $class */
         $class = $this->_className;
         try {
-            if ($value === '' || $value === null || is_bool($value)) {
-                return null;
-            }
-
             if (is_int($value) || (is_string($value) && ctype_digit($value))) {
-                /** @var \DateTime|\DateTimeImmutable $dateTime */
                 $dateTime = new $class('@' . $value);
-                assert($dateTime instanceof NativeDateTime || $dateTime instanceof DateTimeImmutable);
 
                 return $dateTime->setTimezone($this->defaultTimezone);
             }
@@ -334,7 +308,7 @@ class DateTimeType extends BaseType implements BatchCastingInterface
                     $dateTime = $this->_parseValue($value);
                 }
 
-                if ($dateTime instanceof NativeDateTime || $dateTime instanceof DateTimeImmutable) {
+                if ($dateTime) {
                     $dateTime = $dateTime->setTimezone($this->defaultTimezone);
                 }
 
@@ -344,21 +318,20 @@ class DateTimeType extends BaseType implements BatchCastingInterface
             return null;
         }
 
-        if (is_array($value) && implode('', $value) === '') {
+        if (!is_array($value)) {
             return null;
         }
-        $value += ['hour' => 0, 'minute' => 0, 'second' => 0, 'microsecond' => 0];
 
-        $format = '';
+        $value += [
+            'year' => null, 'month' => null, 'day' => null,
+            'hour' => 0, 'minute' => 0, 'second' => 0, 'microsecond' => 0,
+        ];
         if (
-            isset($value['year'], $value['month'], $value['day']) &&
-            (
-                is_numeric($value['year']) &&
-                is_numeric($value['month']) &&
-                is_numeric($value['day'])
-            )
+            !is_numeric($value['year']) || !is_numeric($value['month']) || !is_numeric($value['day']) ||
+            !is_numeric($value['hour']) || !is_numeric($value['minute']) || !is_numeric($value['second']) ||
+            !is_numeric($value['microsecond'])
         ) {
-            $format .= sprintf('%d-%02d-%02d', $value['year'], $value['month'], $value['day']);
+            return null;
         }
 
         if (isset($value['meridian']) && (int)$value['hour'] === 12) {
@@ -367,9 +340,11 @@ class DateTimeType extends BaseType implements BatchCastingInterface
         if (isset($value['meridian'])) {
             $value['hour'] = strtolower($value['meridian']) === 'am' ? $value['hour'] : $value['hour'] + 12;
         }
-        $format .= sprintf(
-            '%s%02d:%02d:%02d.%06d',
-            empty($format) ? '' : ' ',
+        $format = sprintf(
+            '%d-%02d-%02d %02d:%02d:%02d.%06d',
+            $value['year'],
+            $value['month'],
+            $value['day'],
             $value['hour'],
             $value['minute'],
             $value['second'],
@@ -377,7 +352,6 @@ class DateTimeType extends BaseType implements BatchCastingInterface
         );
 
         $dateTime = new $class($format, $value['timezone'] ?? $this->userTimezone);
-        assert($dateTime instanceof DateTime || $dateTime instanceof DateTimeImmutable);
 
         return $dateTime->setTimezone($this->defaultTimezone);
     }
@@ -396,7 +370,7 @@ class DateTimeType extends BaseType implements BatchCastingInterface
 
             return $this;
         }
-        if (is_subclass_of($this->_className, I18nDateTimeInterface::class)) {
+        if (is_a($this->_className, DateTime::class, true)) {
             $this->_useLocaleMarshal = $enable;
 
             return $this;
@@ -425,7 +399,7 @@ class DateTimeType extends BaseType implements BatchCastingInterface
     /**
      * Get the classname used for building objects.
      *
-     * @return class-string<\DateTime>|class-string<\DateTimeImmutable>
+     * @return class-string<\Cake\I18n\DateTime>|class-string<\DateTimeImmutable>
      */
     public function getDateTimeClassName(): string
     {
@@ -437,11 +411,11 @@ class DateTimeType extends BaseType implements BatchCastingInterface
      * aware parser with the format set by `setLocaleFormat()`.
      *
      * @param string $value The value to parse and convert to an object.
-     * @return \Cake\I18n\I18nDateTimeInterface|null
+     * @return \Cake\I18n\DateTime|null
      */
-    protected function _parseLocaleValue(string $value): ?I18nDateTimeInterface
+    protected function _parseLocaleValue(string $value): ?DateTime
     {
-        /** @psalm-var class-string<\Cake\I18n\I18nDateTimeInterface> $class */
+        /** @psalm-var class-string<\Cake\I18n\DateTime> $class */
         $class = $this->_className;
 
         return $class::parseDateTime($value, $this->_localeMarshalFormat, $this->userTimezone);
@@ -452,16 +426,15 @@ class DateTimeType extends BaseType implements BatchCastingInterface
      * formats in `_marshalFormats`.
      *
      * @param string $value The value to parse and convert to an object.
-     * @return \DateTimeInterface|null
+     * @return \Cake\I18n\DateTime|\DateTimeImmutable|null
      */
-    protected function _parseValue(string $value): ?DateTimeInterface
+    protected function _parseValue(string $value): DateTime|DateTimeImmutable|null
     {
         $class = $this->_className;
-
         foreach ($this->_marshalFormats as $format) {
             try {
                 $dateTime = $class::createFromFormat($format, $value, $this->userTimezone);
-                // Check for false in case DateTime is used directly
+                // Check for false in case DateTimeImmutable is used
                 if ($dateTime !== false) {
                     return $dateTime;
                 }

+ 228 - 15
src/Database/Type/DateType.php

@@ -16,15 +16,20 @@ declare(strict_types=1);
  */
 namespace Cake\Database\Type;
 
+use Cake\Chronos\ChronosDate;
+use Cake\Database\Driver;
+use Cake\Database\Exception\DatabaseException;
 use Cake\I18n\Date;
-use Cake\I18n\I18nDateTimeInterface;
+use DateTime as NativeDateTime;
 use DateTimeImmutable;
 use DateTimeInterface;
+use Exception;
+use InvalidArgumentException;
 
 /**
  * Class DateType
  */
-class DateType extends DateTimeType
+class DateType extends BaseType implements BatchCastingInterface
 {
     /**
      * @inheritDoc
@@ -41,12 +46,27 @@ class DateType extends DateTimeType
     ];
 
     /**
-     * In this class we want Date objects to  have their time
-     * set to the beginning of the day.
+     * Whether `marshal()` should use locale-aware parser with `_localeMarshalFormat`.
      *
      * @var bool
      */
-    protected bool $setToDateStart = true;
+    protected bool $_useLocaleMarshal = false;
+
+    /**
+     * The locale-aware format `marshal()` uses when `_useLocaleParser` is true.
+     *
+     * See `Cake\I18n\Time::parseDateTime()` for accepted formats.
+     *
+     * @var string|int|null
+     */
+    protected string|int|null $_localeMarshalFormat = null;
+
+    /**
+     * The classname to use when creating objects.
+     *
+     * @var class-string<\Cake\I18n\Date>|class-string<\DateTimeImmutable>
+     */
+    protected string $_className;
 
     /**
      * @inheritDoc
@@ -59,31 +79,224 @@ class DateType extends DateTimeType
     }
 
     /**
+     * Convert DateTime instance into strings.
+     *
+     * @param mixed $value The value to convert.
+     * @param \Cake\Database\Driver $driver The driver instance to convert with.
+     * @return string|null
+     */
+    public function toDatabase(mixed $value, Driver $driver): ?string
+    {
+        if ($value === null || is_string($value)) {
+            return $value;
+        }
+        if (is_int($value)) {
+            $class = $this->_className;
+            $value = new $class('@' . $value);
+        }
+
+        return $value->format($this->_format);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param mixed $value Value to be converted to PHP equivalent
+     * @param \Cake\Database\Driver $driver Object from which database preferences and configuration will be extracted
+     * @return \Cake\I18n\Date|\DateTimeImmutable|null
+     */
+    public function toPHP(mixed $value, Driver $driver): Date|DateTimeImmutable|null
+    {
+        if ($value === null) {
+            return null;
+        }
+
+        $class = $this->_className;
+        if (is_int($value)) {
+            $instance = new $class('@' . $value);
+        } else {
+            if (str_starts_with($value, '0000-00-00')) {
+                return null;
+            }
+            $instance = new $class($value);
+        }
+
+        if ($instance instanceof DateTimeImmutable) {
+            $instance = $instance->setTime(0, 0, 0);
+        }
+
+        return $instance;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function manyToPHP(array $values, array $fields, Driver $driver): array
+    {
+        foreach ($fields as $field) {
+            if (!isset($values[$field])) {
+                continue;
+            }
+
+            $value = $values[$field];
+            if (str_starts_with($value, '0000-00-00')) {
+                $values[$field] = null;
+                continue;
+            }
+
+            $class = $this->_className;
+            if (is_int($value)) {
+                $instance = new $class('@' . $value);
+            } else {
+                $instance = new $class($value);
+            }
+
+            $values[$field] = $instance;
+        }
+
+        return $values;
+    }
+
+    /**
      * Convert request data into a datetime object.
      *
      * @param mixed $value Request data
-     * @return \DateTimeInterface|null
+     * @return \Cake\Chronos\ChronosDate|\DateTimeInterface|null
+     */
+    public function marshal(mixed $value): ChronosDate|DateTimeInterface|null
+    {
+        if ($value instanceof DateTimeInterface || $value instanceof ChronosDate) {
+            if ($value instanceof NativeDateTime) {
+                $value = clone $value;
+            }
+
+            if (!$value instanceof ChronosDate) {
+                $value = $value->setTime(0, 0, 0);
+            }
+
+            return $value;
+        }
+
+        $class = $this->_className;
+        try {
+            if (is_int($value) || (is_string($value) && ctype_digit($value))) {
+                return new $class('@' . $value);
+            }
+
+            if (is_string($value)) {
+                if ($this->_useLocaleMarshal) {
+                    return $this->_parseLocaleValue($value);
+                }
+
+                return $this->_parseValue($value);
+            }
+        } catch (Exception) {
+            return null;
+        }
+
+        if (
+            !is_array($value) ||
+            !isset($value['year'], $value['month'], $value['day']) ||
+            !is_numeric($value['year']) || !is_numeric($value['month']) || !is_numeric($value['day'])
+        ) {
+            return null;
+        }
+
+        $format = sprintf('%d-%02d-%02d', $value['year'], $value['month'], $value['day']);
+        $dateTime = new $class($format);
+
+        if ($dateTime instanceof DateTimeImmutable) {
+            $dateTime = $dateTime->setTime(0, 0, 0);
+        }
+
+        return $dateTime;
+    }
+
+    /**
+     * Sets whether to parse strings passed to `marshal()` using
+     * the locale-aware format set by `setLocaleFormat()`.
+     *
+     * @param bool $enable Whether to enable
+     * @return $this
      */
-    public function marshal(mixed $value): ?DateTimeInterface
+    public function useLocaleParser(bool $enable = true)
     {
-        $date = parent::marshal($value);
-        /** @psalm-var \DateTime|\DateTimeImmutable|null $date */
-        if ($date && !$date instanceof I18nDateTimeInterface) {
-            // Clear time manually when I18n types aren't available and raw DateTime used
-            $date = $date->setTime(0, 0, 0);
+        if ($enable === false) {
+            $this->_useLocaleMarshal = $enable;
+
+            return $this;
+        }
+        if (is_a($this->_className, Date::class, true)) {
+            $this->_useLocaleMarshal = $enable;
+
+            return $this;
         }
+        throw new DatabaseException(
+            sprintf('Cannot use locale parsing with %s', $this->_className)
+        );
+    }
 
-        return $date;
+    /**
+     * Sets the locale-aware format used by `marshal()` when parsing strings.
+     *
+     * See `Cake\I18n\Time::parseDateTime()` for accepted formats.
+     *
+     * @param array|string $format The locale-aware format
+     * @see \Cake\I18n\Time::parseDateTime()
+     * @return $this
+     */
+    public function setLocaleFormat(array|string $format)
+    {
+        $this->_localeMarshalFormat = $format;
+
+        return $this;
+    }
+
+    /**
+     * Get the classname used for building objects.
+     *
+     * @return class-string<\Cake\I18n\Date>|class-string<\DateTimeImmutable>
+     */
+    public function getDateClassName(): string
+    {
+        return $this->_className;
     }
 
     /**
      * @inheritDoc
      */
-    protected function _parseLocaleValue(string $value): ?I18nDateTimeInterface
+    protected function _parseLocaleValue(string $value): ?Date
     {
-        /** @psalm-var class-string<\Cake\I18n\I18nDateTimeInterface> $class */
+        /** @psalm-var class-string<\Cake\I18n\Date> $class */
         $class = $this->_className;
 
         return $class::parseDate($value, $this->_localeMarshalFormat);
     }
+
+    /**
+     * Converts a string into a DateTime object after parsing it using the
+     * formats in `_marshalFormats`.
+     *
+     * @param string $value The value to parse and convert to an object.
+     * @return \Cake\I18n\Date|\DateTimeImmutable|null
+     */
+    protected function _parseValue(string $value): Date|DateTimeImmutable|null
+    {
+        $class = $this->_className;
+        foreach ($this->_marshalFormats as $format) {
+            try {
+                $dateTime = $class::createFromFormat($format, $value);
+                // Check for false in case DateTimeImmutable is used
+                if ($dateTime !== false) {
+                    return $dateTime;
+                }
+            } catch (InvalidArgumentException) {
+                // Chronos wraps DateTime::createFromFormat and throws
+                // exception if parse fails.
+                continue;
+            }
+        }
+
+        return null;
+    }
 }

+ 98 - 3
src/Database/Type/TimeType.php

@@ -16,7 +16,13 @@ declare(strict_types=1);
  */
 namespace Cake\Database\Type;
 
-use Cake\I18n\I18nDateTimeInterface;
+use Cake\Chronos\Chronos;
+use Cake\I18n\DateTime;
+use DateTime as NativeDateTime;
+use DateTimeImmutable;
+use DateTimeInterface;
+use Exception;
+use InvalidArgumentException;
 
 /**
  * Time type converter.
@@ -41,14 +47,103 @@ class TimeType extends DateTimeType
     ];
 
     /**
+     * Convert request data into a datetime object.
+     *
+     * @param mixed $value Request data
+     * @return \Cake\Chronos\Chronos|\DateTimeInterface|null
+     */
+    public function marshal(mixed $value): Chronos|DateTimeInterface|null
+    {
+        if ($value instanceof DateTimeInterface || $value instanceof Chronos) {
+            if ($value instanceof NativeDateTime) {
+                $value = clone $value;
+            }
+
+            return $value;
+        }
+
+        $class = $this->_className;
+        try {
+            if (is_int($value) || (is_string($value) && ctype_digit($value))) {
+                return new $class('@' . $value);
+            }
+
+            if (is_string($value)) {
+                if ($this->_useLocaleMarshal) {
+                    return $this->_parseLocaleTimeValue($value);
+                } else {
+                    return $this->_parseTimeValue($value);
+                }
+            }
+        } catch (Exception $e) {
+            return null;
+        }
+
+        if (!is_array($value)) {
+            return null;
+        }
+
+        $value += ['hour' => null, 'minute' => null, 'second' => 0, 'microsecond' => 0];
+        if (
+            !is_numeric($value['hour']) || !is_numeric($value['minute']) || !is_numeric($value['second']) ||
+            !is_numeric($value['microsecond'])
+        ) {
+            return null;
+        }
+
+        if (isset($value['meridian']) && (int)$value['hour'] === 12) {
+            $value['hour'] = 0;
+        }
+        if (isset($value['meridian'])) {
+            $value['hour'] = strtolower($value['meridian']) === 'am' ? $value['hour'] : $value['hour'] + 12;
+        }
+        $format = sprintf(
+            '%02d:%02d:%02d.%06d',
+            $value['hour'],
+            $value['minute'],
+            $value['second'],
+            $value['microsecond']
+        );
+
+        return new $class($format);
+    }
+
+    /**
      * @inheritDoc
      */
-    protected function _parseLocaleValue(string $value): ?I18nDateTimeInterface
+    protected function _parseLocaleTimeValue(string $value): ?DateTime
     {
-        /** @psalm-var class-string<\Cake\I18n\I18nDateTimeInterface> $class */
+        /** @psalm-var class-string<\Cake\I18n\DateTime> $class */
         $class = $this->_className;
 
         /** @psalm-suppress PossiblyInvalidArgument */
         return $class::parseTime($value, $this->_localeMarshalFormat);
     }
+
+    /**
+     * Converts a string into a DateTime object after parsing it using the
+     * formats in `_marshalFormats`.
+     *
+     * @param string $value The value to parse and convert to an object.
+     * @return \Cake\I18n\DateTime|\DateTimeImmutable|null
+     */
+    protected function _parseTimeValue(string $value): DateTime|DateTimeImmutable|null
+    {
+        $class = $this->_className;
+        foreach ($this->_marshalFormats as $format) {
+            try {
+                $dateTime = $class::createFromFormat($format, $value);
+                // Check for false in case DateTime is used directly
+                if ($dateTime !== false) {
+                    return $dateTime;
+                }
+            } catch (InvalidArgumentException) {
+                // Chronos wraps DateTime::createFromFormat and throws
+                // exception if parse fails.
+                continue;
+            }
+        }
+
+        return null;
+    }
 }

+ 8 - 15
src/Http/Cookie/Cookie.php

@@ -15,6 +15,7 @@ declare(strict_types=1);
  */
 namespace Cake\Http\Cookie;
 
+use Cake\Chronos\Chronos;
 use Cake\Utility\Hash;
 use DateTimeImmutable;
 use DateTimeInterface;
@@ -74,9 +75,9 @@ class Cookie implements CookieInterface
     /**
      * Expiration time
      *
-     * @var \DateTimeInterface|null
+     * @var \Cake\Chronos\Chronos|\DateTimeInterface|null
      */
-    protected ?DateTimeInterface $expiresAt = null;
+    protected Chronos|DateTimeInterface|null $expiresAt = null;
 
     /**
      * Path
@@ -138,7 +139,7 @@ class Cookie implements CookieInterface
      * @link https://php.net/manual/en/function.setcookie.php
      * @param string $name Cookie name
      * @param array|string|float|int|bool $value Value of the cookie
-     * @param \DateTimeInterface|null $expiresAt Expiration time and date
+     * @param \Cake\Chronos\Chronos|\DateTimeInterface|null $expiresAt Expiration time and date
      * @param string|null $path Path
      * @param string|null $domain Domain
      * @param bool|null $secure Is secure
@@ -148,7 +149,7 @@ class Cookie implements CookieInterface
     public function __construct(
         string $name,
         array|string|float|int|bool $value = '',
-        ?DateTimeInterface $expiresAt = null,
+        Chronos|DateTimeInterface|null $expiresAt = null,
         ?string $path = null,
         ?string $domain = null,
         ?bool $secure = null,
@@ -167,10 +168,6 @@ class Cookie implements CookieInterface
         $this->sameSite = static::resolveSameSiteEnum($sameSite ?? static::$defaults['samesite']);
 
         if ($expiresAt) {
-            /**
-             * @psalm-suppress UndefinedInterfaceMethod
-             * @phpstan-ignore-next-line
-             */
             $expiresAt = $expiresAt->setTimezone(new DateTimeZone('GMT'));
         } else {
             $expiresAt = static::$defaults['expires'];
@@ -536,13 +533,9 @@ class Cookie implements CookieInterface
     /**
      * @inheritDoc
      */
-    public function withExpiry(DateTimeInterface $dateTime): static
+    public function withExpiry(Chronos|DateTimeInterface $dateTime): static
     {
         $new = clone $this;
-        /**
-         * @psalm-suppress UndefinedInterfaceMethod
-         * @phpstan-ignore-next-line
-         */
         $new->expiresAt = $dateTime->setTimezone(new DateTimeZone('GMT'));
 
         return $new;
@@ -551,7 +544,7 @@ class Cookie implements CookieInterface
     /**
      * @inheritDoc
      */
-    public function getExpiry(): ?DateTimeInterface
+    public function getExpiry(): Chronos|DateTimeInterface|null
     {
         return $this->expiresAt;
     }
@@ -583,7 +576,7 @@ class Cookie implements CookieInterface
     /**
      * @inheritDoc
      */
-    public function isExpired(?DateTimeInterface $time = null): bool
+    public function isExpired(Chronos|DateTimeInterface|null $time = null): bool
     {
         $time = $time ?: new DateTimeImmutable('now', new DateTimeZone('UTC'));
         if (!$this->expiresAt) {

+ 7 - 6
src/Http/Cookie/CookieInterface.php

@@ -15,6 +15,7 @@ declare(strict_types=1);
  */
 namespace Cake\Http\Cookie;
 
+use Cake\Chronos\Chronos;
 use DateTimeInterface;
 
 /**
@@ -142,9 +143,9 @@ interface CookieInterface
     /**
      * Get the current expiry time
      *
-     * @return \DateTimeInterface|null Timestamp of expiry or null
+     * @return \Cake\Chronos\Chronos|\DateTimeInterface|null Timestamp of expiry or null
      */
-    public function getExpiry(): ?DateTimeInterface;
+    public function getExpiry(): Chronos|DateTimeInterface|null;
 
     /**
      * Get the timestamp from the expiration time
@@ -163,10 +164,10 @@ interface CookieInterface
     /**
      * Create a cookie with an updated expiration date
      *
-     * @param \DateTimeInterface $dateTime Date time object
+     * @param \Cake\Chronos\Chronos|\DateTimeInterface $dateTime Date time object
      * @return static
      */
-    public function withExpiry(DateTimeInterface $dateTime): static;
+    public function withExpiry(Chronos|DateTimeInterface $dateTime): static;
 
     /**
      * Create a new cookie that will virtually never expire.
@@ -189,10 +190,10 @@ interface CookieInterface
      *
      * Cookies without an expiration date always return false.
      *
-     * @param \DateTimeInterface $time The time to test against. Defaults to 'now' in UTC.
+     * @param \Cake\Chronos\Chronos|\DateTimeInterface|null $time The time to test against. Defaults to 'now' in UTC.
      * @return bool
      */
-    public function isExpired(?DateTimeInterface $time = null): bool;
+    public function isExpired(Chronos|DateTimeInterface|null $time = null): bool;
 
     /**
      * Check if the cookie is HTTP only

+ 38 - 22
src/I18n/Date.php

@@ -18,9 +18,8 @@ namespace Cake\I18n;
 
 use Cake\Chronos\ChronosDate;
 use Closure;
-use DateTimeInterface;
-use DateTimeZone;
 use IntlDateFormatter;
+use JsonSerializable;
 use Stringable;
 
 /**
@@ -30,7 +29,7 @@ use Stringable;
  *
  * @psalm-immutable
  */
-class Date extends ChronosDate implements I18nDateTimeInterface, Stringable
+class Date extends ChronosDate implements JsonSerializable, Stringable
 {
     use DateFormatTrait;
 
@@ -118,28 +117,46 @@ class Date extends ChronosDate implements I18nDateTimeInterface, Stringable
     public static string $wordEnd = '+1 month';
 
     /**
-     * Create a new Date instance.
+     * Sets the default format used when type converting instances of this type to string
      *
-     * You can specify the timezone for the $time parameter. This timezone will
-     * not be used in any future modifications to the Date instance.
+     * The format should be either the formatting constants from IntlDateFormatter as
+     * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
+     * as specified in (https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/classSimpleDateFormat.html#details)
      *
-     * The `$timezone` parameter is ignored if `$time` is a DateTimeInterface
-     * instance.
+     * It is possible to provide an array of 2 constants. In this case, the first position
+     * will be used for formatting the date part of the object and the second position
+     * will be used to format the time part.
      *
-     * Date instances lack time components, however due to limitations in PHP's
-     * internal Datetime object the time will always be set to 00:00:00, and the
-     * timezone will always be the server local time. Normalizing the timezone allows for
-     * subtraction/addition to have deterministic results.
+     * @param array<int>|string|int $format Format.
+     * @return void
+     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
+     */
+    public static function setToStringFormat($format): void
+    {
+        static::$_toStringFormat = $format;
+    }
+
+    /**
+     * Sets the default format used when converting this object to JSON
      *
-     * @param \DateTimeInterface|string|int|null $time Fixed or relative time
-     * @param \DateTimeZone|string|null $tz The timezone in which the date is taken.
-     *                                  Ignored if `$time` is a DateTimeInterface instance.
+     * The format should be either the formatting constants from IntlDateFormatter as
+     * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
+     * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
+     *
+     * It is possible to provide an array of 2 constants. In this case, the first position
+     * will be used for formatting the date part of the object and the second position
+     * will be used to format the time part.
+     *
+     * Alternatively, the format can provide a callback. In this case, the callback
+     * can receive this datetime object and return a formatted string.
+     *
+     * @see \Cake\I18n\Time::i18nFormat()
+     * @param \Closure|array|string|int $format Format.
+     * @return void
      */
-    public function __construct(
-        DateTimeInterface|string|int|null $time = 'now',
-        DateTimeZone|string|null $tz = null
-    ) {
-        parent::__construct($time, $tz);
+    public static function setJsonEncodeFormat(Closure|array|string|int $format): void
+    {
+        static::$_jsonEncodeFormat = $format;
     }
 
     /**
@@ -178,7 +195,6 @@ class Date extends ChronosDate implements I18nDateTimeInterface, Stringable
      */
     public function timeAgoInWords(array $options = []): string
     {
-        /** @psalm-suppress UndefinedInterfaceMethod,ImpureMethodCall */
-        return static::getDiffFormatter()->dateAgoInWords($this, $options);
+        return static::diffFormatter()->dateAgoInWords($this, $options);
     }
 }

+ 43 - 219
src/I18n/DateFormatTrait.php

@@ -16,7 +16,7 @@ declare(strict_types=1);
  */
 namespace Cake\I18n;
 
-use Cake\Chronos\ChronosInterface;
+use Cake\Chronos\ChronosDate;
 use Cake\Chronos\DifferenceFormatterInterface;
 use Cake\Core\Exception\CakeException;
 use Closure;
@@ -24,6 +24,7 @@ use DateTimeImmutable;
 use DateTimeInterface;
 use DateTimeZone;
 use IntlDateFormatter;
+use InvalidArgumentException;
 
 /**
  * Trait for date formatting methods shared by both Time & Date.
@@ -33,84 +34,6 @@ use IntlDateFormatter;
 trait DateFormatTrait
 {
     /**
-     * The default locale to be used for displaying formatted date strings.
-     *
-     * Use static::setDefaultLocale() and static::getDefaultLocale() instead.
-     *
-     * @var string|null
-     */
-    protected static ?string $defaultLocale = null;
-
-    /**
-     * Whether lenient parsing is enabled for IntlDateFormatter.
-     *
-     * Defaults to true which is the default for IntlDateFormatter.
-     *
-     * @var bool
-     */
-    protected static bool $lenientParsing = true;
-
-    /**
-     * In-memory cache of date formatters
-     *
-     * @var array<\IntlDateFormatter>
-     */
-    protected static array $_formatters = [];
-
-    /**
-     * Gets the default locale.
-     *
-     * @return string|null The default locale string to be used or null.
-     */
-    public static function getDefaultLocale(): ?string
-    {
-        return static::$defaultLocale;
-    }
-
-    /**
-     * Sets the default locale.
-     *
-     * Set to null to use IntlDateFormatter default.
-     *
-     * @param string|null $locale The default locale string to be used.
-     * @return void
-     */
-    public static function setDefaultLocale(?string $locale = null): void
-    {
-        static::$defaultLocale = $locale;
-    }
-
-    /**
-     * Gets whether locale format parsing is set to lenient.
-     *
-     * @return bool
-     */
-    public static function lenientParsingEnabled(): bool
-    {
-        return static::$lenientParsing;
-    }
-
-    /**
-     * Enables lenient parsing for locale formats.
-     *
-     * @return void
-     */
-    public static function enableLenientParsing(): void
-    {
-        static::$lenientParsing = true;
-    }
-
-    /**
-     * Enables lenient parsing for locale formats.
-     *
-     * @return void
-     */
-    public static function disableLenientParsing(): void
-    {
-        static::$lenientParsing = false;
-    }
-
-    /**
      * Returns a nicely formatted date string for this object.
      *
      * The format to be used is stored in the static property `Time::niceFormat`.
@@ -185,21 +108,23 @@ trait DateFormatTrait
         ?string $locale = null
     ): string|int {
         if ($format === DateTime::UNIX_TIMESTAMP_FORMAT) {
-            return $this->getTimestamp();
+            if ($this instanceof ChronosDate) {
+                return $this->native->getTimestamp();
+            } else {
+                return $this->getTimestamp();
+            }
         }
 
         $time = $this;
 
-        if ($timezone) {
-            // Handle the immutable and mutable object cases.
-            $time = clone $this;
+        if ($time instanceof DateTime && $timezone) {
             $time = $time->setTimezone($timezone);
         }
 
         $format = $format ?? static::$_toStringFormat;
-        $locale = $locale ?: static::$defaultLocale;
+        $locale = $locale ?: DateTime::getDefaultLocale();
 
-        return $this->_formatObject($time, $format, $locale);
+        return $this->_formatObject($time instanceof DateTimeInterface ? $time : $time->native, $format, $locale);
     }
 
     /**
@@ -241,32 +166,29 @@ trait DateFormatTrait
         }
 
         $timezone = $date->getTimezone()->getName();
-        $key = "{$locale}.{$dateFormat}.{$timeFormat}.{$timezone}.{$calendar}.{$pattern}";
+        if ($timezone === '+00:00' || $timezone === 'Z') {
+            $timezone = 'UTC';
+        } elseif ($timezone[0] === '+' || $timezone[0] === '-') {
+            $timezone = 'GMT' . $timezone;
+        }
 
-        if (!isset(static::$_formatters[$key])) {
-            if ($timezone === '+00:00' || $timezone === 'Z') {
-                $timezone = 'UTC';
-            } elseif ($timezone[0] === '+' || $timezone[0] === '-') {
-                $timezone = 'GMT' . $timezone;
-            }
-            $formatter = datefmt_create(
-                $locale,
-                $dateFormat,
-                $timeFormat,
-                $timezone,
-                $calendar,
-                $pattern
+        $formatter = datefmt_create(
+            $locale,
+            $dateFormat,
+            $timeFormat,
+            $timezone,
+            $calendar,
+            $pattern
+        );
+        if (empty($formatter)) {
+            $key = "{$locale}.{$dateFormat}.{$timeFormat}.{$timezone}.{$calendar}.{$pattern}";
+            throw new CakeException(
+                'Your version of icu does not support creating a date formatter for ' .
+                "`$key`. You should try to upgrade libicu and the intl extension."
             );
-            if (empty($formatter)) {
-                throw new CakeException(
-                    'Your version of icu does not support creating a date formatter for ' .
-                    "`$key`. You should try to upgrade libicu and the intl extension."
-                );
-            }
-            static::$_formatters[$key] = $formatter;
         }
 
-        return static::$_formatters[$key]->format($date->format('U'));
+        return $formatter->format($date->format('U'));
     }
 
     /**
@@ -278,60 +200,6 @@ trait DateFormatTrait
     }
 
     /**
-     * Resets the format used to the default when converting an instance of this type to
-     * a string
-     *
-     * @return void
-     */
-    public static function resetToStringFormat(): void
-    {
-        static::setToStringFormat([IntlDateFormatter::SHORT, IntlDateFormatter::SHORT]);
-    }
-
-    /**
-     * Sets the default format used when type converting instances of this type to string
-     *
-     * The format should be either the formatting constants from IntlDateFormatter as
-     * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
-     * as specified in (https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/classSimpleDateFormat.html#details)
-     *
-     * It is possible to provide an array of 2 constants. In this case, the first position
-     * will be used for formatting the date part of the object and the second position
-     * will be used to format the time part.
-     *
-     * @param array<int>|string|int $format Format.
-     * @return void
-     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
-     */
-    public static function setToStringFormat($format): void
-    {
-        static::$_toStringFormat = $format;
-    }
-
-    /**
-     * Sets the default format used when converting this object to JSON
-     *
-     * The format should be either the formatting constants from IntlDateFormatter as
-     * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
-     * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
-     *
-     * It is possible to provide an array of 2 constants. In this case, the first position
-     * will be used for formatting the date part of the object and the second position
-     * will be used to format the time part.
-     *
-     * Alternatively, the format can provide a callback. In this case, the callback
-     * can receive this datetime object and return a formatted string.
-     *
-     * @see \Cake\I18n\Time::i18nFormat()
-     * @param \Closure|array|string|int $format Format.
-     * @return void
-     */
-    public static function setJsonEncodeFormat(Closure|array|string|int $format): void
-    {
-        static::$_jsonEncodeFormat = $format;
-    }
-
-    /**
      * Returns a new Time object after parsing the provided time string based on
      * the passed or configured date time format. This method is locale dependent,
      * Any string that is passed to this function will be interpreted as a locale
@@ -375,7 +243,7 @@ trait DateFormatTrait
             $pattern = $format;
         }
 
-        $locale = static::$defaultLocale ?? I18n::getLocale();
+        $locale = DateTime::getDefaultLocale() ?? I18n::getLocale();
         $formatter = datefmt_create(
             $locale,
             $dateFormat,
@@ -387,7 +255,7 @@ trait DateFormatTrait
         if (!$formatter) {
             throw new CakeException('Unable to create IntlDateFormatter instance');
         }
-        $formatter->setLenient(static::$lenientParsing);
+        $formatter->setLenient(DateTime::lenientParsingEnabled());
 
         $time = $formatter->parse($time);
         if ($time === false) {
@@ -483,66 +351,22 @@ trait DateFormatTrait
     /**
      * Get the difference formatter instance.
      *
-     * @return \Cake\Chronos\DifferenceFormatterInterface
+     * @param \Cake\Chronos\DifferenceFormatterInterface $formatter Difference formatter
+     * @return \Cake\I18n\RelativeTimeFormatter
      */
-    public static function getDiffFormatter(): DifferenceFormatterInterface
+    public static function diffFormatter(?DifferenceFormatterInterface $formatter = null): RelativeTimeFormatter
     {
-        /** @phpstan-ignore-next-line */
-        return static::$diffFormatter ??= new RelativeTimeFormatter();
-    }
+        if ($formatter) {
+            if (!$formatter instanceof RelativeTimeFormatter) {
+                throw new InvalidArgumentException('Formatter for I18n must extend RelativeTimeFormatter.');
+            }
 
-    /**
-     * Set the difference formatter instance.
-     *
-     * @param \Cake\Chronos\DifferenceFormatterInterface $formatter The formatter instance when setting.
-     * @return void
-     */
-    public static function setDiffFormatter(DifferenceFormatterInterface $formatter): void
-    {
-        static::$diffFormatter = $formatter;
-    }
+            return static::$diffFormatter = $formatter;
+        }
 
-    /**
-     * Get the difference in a human readable format.
-     *
-     * When comparing a value in the past to default now:
-     * 1 hour ago
-     * 5 months ago
-     *
-     * When comparing a value in the future to default now:
-     * 1 hour from now
-     * 5 months from now
-     *
-     * When comparing a value in the past to another value:
-     * 1 hour before
-     * 5 months before
-     *
-     * When comparing a value in the future to another value:
-     * 1 hour after
-     * 5 months after
-     *
-     * @param \Cake\Chronos\ChronosInterface|null $dateTime The datetime to compare with.
-     * @param bool $absolute removes time difference modifiers ago, after, etc
-     * @return string
-     */
-    public function diffForHumans(?ChronosInterface $dateTime = null, bool $absolute = false): string
-    {
-        return static::getDiffFormatter()->diffForHumans($this, $dateTime, $absolute);
-    }
+        /** @var \Cake\I18n\RelativeTimeFormatter $formatter */
+        $formatter = static::$diffFormatter ??= new RelativeTimeFormatter();
 
-    /**
-     * Returns the data that should be displayed when debugging this object
-     *
-     * @return array<string, mixed>
-     * @psalm-suppress MissingImmutableAnnotation
-     */
-    public function __debugInfo(): array
-    {
-        /** @psalm-suppress PossiblyNullReference */
-        return [
-            'time' => $this->format('Y-m-d H:i:s.uP'),
-            'timezone' => $this->getTimezone()->getName(),
-            'fixedNowTime' => static::hasTestNow() ? static::getTestNow()->format('Y-m-d\TH:i:s.uP') : false,
-        ];
+        return $formatter;
     }
 }

+ 120 - 19
src/I18n/DateTime.php

@@ -18,9 +18,9 @@ namespace Cake\I18n;
 
 use Cake\Chronos\Chronos;
 use Closure;
-use DateTimeInterface;
 use DateTimeZone;
 use IntlDateFormatter;
+use JsonSerializable;
 use Stringable;
 
 /**
@@ -29,11 +29,29 @@ use Stringable;
  *
  * @psalm-immutable
  */
-class DateTime extends Chronos implements I18nDateTimeInterface, Stringable
+class DateTime extends Chronos implements JsonSerializable, Stringable
 {
     use DateFormatTrait;
 
     /**
+     * The default locale to be used for displaying formatted date strings.
+     *
+     * Use static::setDefaultLocale() and static::getDefaultLocale() instead.
+     *
+     * @var string|null
+     */
+    protected static ?string $defaultLocale = null;
+
+    /**
+     * Whether lenient parsing is enabled for IntlDateFormatter.
+     *
+     * Defaults to true which is the default for IntlDateFormatter.
+     *
+     * @var bool
+     */
+    protected static bool $lenientParsing = true;
+
+    /**
      * The format to use when formatting a time using `Cake\I18n\Time::i18nFormat()`
      * and `__toString`. This format is also used by `parseDateTime()`.
      *
@@ -124,26 +142,110 @@ class DateTime extends Chronos implements I18nDateTimeInterface, Stringable
     public const UNIX_TIMESTAMP_FORMAT = 'unixTimestampFormat';
 
     /**
-     * Create a new immutable time instance.
+     * Gets the default locale.
      *
-     * @param \DateTimeInterface|string|int|null $time Fixed or relative time
-     * @param \DateTimeZone|string|null $tz The timezone for the instance
-     * @psalm-immutable
+     * @return string|null The default locale string to be used or null.
      */
-    public function __construct(DateTimeInterface|string|int|null $time = null, DateTimeZone|string|null $tz = null)
+    public static function getDefaultLocale(): ?string
     {
-        if ($time instanceof DateTimeInterface) {
-            /** @psalm-suppress ImpureMethodCall */
-            $tz = $time->getTimezone();
-            /** @psalm-suppress ImpureMethodCall */
-            $time = $time->format('Y-m-d H:i:s.u');
-        }
+        return static::$defaultLocale;
+    }
 
-        if (is_numeric($time)) {
-            $time = '@' . $time;
-        }
+    /**
+     * Sets the default locale.
+     *
+     * Set to null to use IntlDateFormatter default.
+     *
+     * @param string|null $locale The default locale string to be used.
+     * @return void
+     */
+    public static function setDefaultLocale(?string $locale = null): void
+    {
+        static::$defaultLocale = $locale;
+    }
+
+    /**
+     * Gets whether locale format parsing is set to lenient.
+     *
+     * @return bool
+     */
+    public static function lenientParsingEnabled(): bool
+    {
+        return static::$lenientParsing;
+    }
+
+    /**
+     * Enables lenient parsing for locale formats.
+     *
+     * @return void
+     */
+    public static function enableLenientParsing(): void
+    {
+        static::$lenientParsing = true;
+    }
+
+    /**
+     * Enables lenient parsing for locale formats.
+     *
+     * @return void
+     */
+    public static function disableLenientParsing(): void
+    {
+        static::$lenientParsing = false;
+    }
+
+    /**
+     * Sets the default format used when type converting instances of this type to string
+     *
+     * The format should be either the formatting constants from IntlDateFormatter as
+     * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
+     * as specified in (https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/classSimpleDateFormat.html#details)
+     *
+     * It is possible to provide an array of 2 constants. In this case, the first position
+     * will be used for formatting the date part of the object and the second position
+     * will be used to format the time part.
+     *
+     * @param array<int>|string|int $format Format.
+     * @return void
+     * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
+     */
+    public static function setToStringFormat($format): void
+    {
+        static::$_toStringFormat = $format;
+    }
 
-        parent::__construct($time, $tz);
+    /**
+     * Resets the format used to the default when converting an instance of this type to
+     * a string
+     *
+     * @return void
+     */
+    public static function resetToStringFormat(): void
+    {
+        static::setToStringFormat([IntlDateFormatter::SHORT, IntlDateFormatter::SHORT]);
+    }
+
+    /**
+     * Sets the default format used when converting this object to JSON
+     *
+     * The format should be either the formatting constants from IntlDateFormatter as
+     * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
+     * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
+     *
+     * It is possible to provide an array of 2 constants. In this case, the first position
+     * will be used for formatting the date part of the object and the second position
+     * will be used to format the time part.
+     *
+     * Alternatively, the format can provide a callback. In this case, the callback
+     * can receive this datetime object and return a formatted string.
+     *
+     * @see \Cake\I18n\Time::i18nFormat()
+     * @param \Closure|array|string|int $format Format.
+     * @return void
+     */
+    public static function setJsonEncodeFormat(Closure|array|string|int $format): void
+    {
+        static::$_jsonEncodeFormat = $format;
     }
 
     /**
@@ -185,8 +287,7 @@ class DateTime extends Chronos implements I18nDateTimeInterface, Stringable
      */
     public function timeAgoInWords(array $options = []): string
     {
-        /** @psalm-suppress UndefinedInterfaceMethod,ImpureMethodCall */
-        return static::getDiffFormatter()->timeAgoInWords($this, $options);
+        return static::diffFormatter()->timeAgoInWords($this, $options);
     }
 
     /**

+ 0 - 246
src/I18n/I18nDateTimeInterface.php

@@ -1,246 +0,0 @@
-<?php
-declare(strict_types=1);
-
-/**
- * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
- * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
- *
- * Licensed under The MIT License
- * For full copyright and license information, please see the LICENSE.txt
- * Redistributions of files must retain the above copyright notice.
- *
- * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
- * @link          https://cakephp.org CakePHP(tm) Project
- * @since         4.0.0
- * @license       https://opensource.org/licenses/mit-license.php MIT License
- */
-namespace Cake\I18n;
-
-use Cake\Chronos\ChronosInterface;
-use Cake\Chronos\DifferenceFormatterInterface;
-use Closure;
-use DateTimeZone;
-use JsonSerializable;
-
-/**
- * Interface for date formatting methods shared by both Time & Date.
- */
-interface I18nDateTimeInterface extends ChronosInterface, JsonSerializable
-{
-    /**
-     * Gets the default locale.
-     *
-     * @return string|null The default locale string to be used or null.
-     */
-    public static function getDefaultLocale(): ?string;
-
-    /**
-     * Sets the default locale.
-     *
-     * @param string|null $locale The default locale string to be used or null.
-     * @return void
-     */
-    public static function setDefaultLocale(?string $locale = null): void;
-
-    /**
-     * Returns a nicely formatted date string for this object.
-     *
-     * The format to be used is stored in the static property `Time::niceFormat`.
-     *
-     * @param \DateTimeZone|string|null $timezone Timezone string or DateTimeZone object
-     * in which the date will be displayed. The timezone stored for this object will not
-     * be changed.
-     * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR)
-     * @return string Formatted date string
-     */
-    public function nice(DateTimeZone|string|null $timezone = null, ?string $locale = null): string;
-
-    /**
-     * Returns a formatted string for this time object using the preferred format and
-     * language for the specified locale.
-     *
-     * It is possible to specify the desired format for the string to be displayed.
-     * You can either pass `IntlDateFormatter` constants as the first argument of this
-     * function, or pass a full ICU date formatting string as specified in the following
-     * resource: https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/classSimpleDateFormat.html#details.
-     *
-     * Additional to `IntlDateFormatter` constants and date formatting string you can use
-     * Time::UNIX_TIMESTAMP_FORMAT to get a unix timestamp
-     *
-     * ### Examples
-     *
-     * ```
-     * $time = new Time('2014-04-20 22:10');
-     * $time->i18nFormat(); // outputs '4/20/14, 10:10 PM' for the en-US locale
-     * $time->i18nFormat(\IntlDateFormatter::FULL); // Use the full date and time format
-     * $time->i18nFormat([\IntlDateFormatter::FULL, \IntlDateFormatter::SHORT]); // Use full date but short time format
-     * $time->i18nFormat('yyyy-MM-dd HH:mm:ss'); // outputs '2014-04-20 22:10'
-     * $time->i18nFormat(Time::UNIX_TIMESTAMP_FORMAT); // outputs '1398031800'
-     * ```
-     *
-     * If you wish to control the default format to be used for this method, you can alter
-     * the value of the static `Time::$defaultLocale` variable and set it to one of the
-     * possible formats accepted by this function.
-     *
-     * You can read about the available IntlDateFormatter constants at
-     * https://secure.php.net/manual/en/class.intldateformatter.php
-     *
-     * If you need to display the date in a different timezone than the one being used for
-     * this Time object without altering its internal state, you can pass a timezone
-     * string or object as the second parameter.
-     *
-     * Finally, should you need to use a different locale for displaying this time object,
-     * pass a locale string as the third parameter to this function.
-     *
-     * ### Examples
-     *
-     * ```
-     * $time = new Time('2014-04-20 22:10');
-     * $time->i18nFormat(null, null, 'de-DE');
-     * $time->i18nFormat(\IntlDateFormatter::FULL, 'Europe/Berlin', 'de-DE');
-     * ```
-     *
-     * You can control the default locale to be used by setting the static variable
-     * `Time::$defaultLocale` to a valid locale string. If empty, the default will be
-     * taken from the `intl.default_locale` ini config.
-     *
-     * @param string|int|null $format Format string.
-     * @param \DateTimeZone|string|null $timezone Timezone string or DateTimeZone object
-     * in which the date will be displayed. The timezone stored for this object will not
-     * be changed.
-     * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR)
-     * @return string|int Formatted and translated date string
-     */
-    public function i18nFormat(
-        string|int|null $format = null,
-        DateTimeZone|string|null $timezone = null,
-        ?string $locale = null
-    ): string|int;
-
-    /**
-     * Resets the format used to the default when converting an instance of this type to
-     * a string
-     *
-     * @return void
-     */
-    public static function resetToStringFormat(): void;
-
-    /**
-     * Sets the default format used when type converting instances of this type to string
-     *
-     * @param array<int>|string|int $format Format.
-     * @return void
-     */
-    public static function setToStringFormat(array|string|int $format): void;
-
-    /**
-     * Sets the default format used when converting this object to JSON
-     *
-     * The format should be either the formatting constants from IntlDateFormatter as
-     * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
-     * as specified in (https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/classSimpleDateFormat.html#details)
-     *
-     * It is possible to provide an array of 2 constants. In this case, the first position
-     * will be used for formatting the date part of the object and the second position
-     * will be used to format the time part.
-     *
-     * Alternatively, the format can provide a callback. In this case, the callback
-     * can receive this datetime object and return a formatted string.
-     *
-     * @see \Cake\I18n\Time::i18nFormat()
-     * @param \Closure|array|string|int $format Format.
-     * @return void
-     */
-    public static function setJsonEncodeFormat(Closure|array|string|int $format): void;
-
-    /**
-     * Returns a new Time object after parsing the provided time string based on
-     * the passed or configured date time format. This method is locale dependent,
-     * Any string that is passed to this function will be interpreted as a locale
-     * dependent string.
-     *
-     * When no $format is provided, the `toString` format will be used.
-     *
-     * If it was impossible to parse the provided time, null will be returned.
-     *
-     * Example:
-     *
-     * ```
-     *  $time = Time::parseDateTime('10/13/2013 12:54am');
-     *  $time = Time::parseDateTime('13 Oct, 2013 13:54', 'dd MMM, y H:mm');
-     *  $time = Time::parseDateTime('10/10/2015', [IntlDateFormatter::SHORT, -1]);
-     * ```
-     *
-     * @param string $time The time string to parse.
-     * @param array<int>|string|null $format Any format accepted by IntlDateFormatter.
-     * @param \DateTimeZone|string|null $tz The timezone for the instance
-     * @return static|null
-     * @throws \InvalidArgumentException If $format is a single int instead of array of constants
-     */
-    public static function parseDateTime(
-        string $time,
-        array|string|null $format = null,
-        DateTimeZone|string|null $tz = null
-    ): ?static;
-
-    /**
-     * Returns a new Time object after parsing the provided $date string based on
-     * the passed or configured date time format. This method is locale dependent,
-     * Any string that is passed to this function will be interpreted as a locale
-     * dependent string.
-     *
-     * When no $format is provided, the `wordFormat` format will be used.
-     *
-     * If it was impossible to parse the provided time, null will be returned.
-     *
-     * Example:
-     *
-     * ```
-     *  $time = Time::parseDate('10/13/2013');
-     *  $time = Time::parseDate('13 Oct, 2013', 'dd MMM, y');
-     *  $time = Time::parseDate('13 Oct, 2013', IntlDateFormatter::SHORT);
-     * ```
-     *
-     * @param string $date The date string to parse.
-     * @param array|string|int|null $format Any format accepted by IntlDateFormatter.
-     * @return static|null
-     */
-    public static function parseDate(string $date, array|string|int|null $format = null): ?static;
-
-    /**
-     * Returns a new Time object after parsing the provided $time string based on
-     * the passed or configured date time format. This method is locale dependent,
-     * Any string that is passed to this function will be interpreted as a locale
-     * dependent string.
-     *
-     * When no $format is provided, the IntlDateFormatter::SHORT format will be used.
-     *
-     * If it was impossible to parse the provided time, null will be returned.
-     *
-     * Example:
-     *
-     * ```
-     *  $time = Time::parseTime('11:23pm');
-     * ```
-     *
-     * @param string $time The time string to parse.
-     * @param string|int|null $format Any format accepted by IntlDateFormatter.
-     * @return static|null
-     */
-    public static function parseTime(string $time, string|int|null $format = null): ?static;
-
-    /**
-     * Get the difference formatter instance.
-     *
-     * @return \Cake\Chronos\DifferenceFormatterInterface The formatter instance.
-     */
-    public static function getDiffFormatter(): DifferenceFormatterInterface;
-
-    /**
-     * Set the difference formatter instance.
-     *
-     * @param \Cake\Chronos\DifferenceFormatterInterface $formatter The formatter instance when setting.
-     * @return void
-     */
-    public static function setDiffFormatter(DifferenceFormatterInterface $formatter): void;
-}

+ 23 - 18
src/I18n/RelativeTimeFormatter.php

@@ -16,8 +16,10 @@ declare(strict_types=1);
  */
 namespace Cake\I18n;
 
-use Cake\Chronos\ChronosInterface;
+use Cake\Chronos\Chronos;
+use Cake\Chronos\ChronosDate;
 use Cake\Chronos\DifferenceFormatterInterface;
+use DateTimeInterface;
 
 /**
  * Helper class for formatting relative dates & times.
@@ -29,23 +31,26 @@ class RelativeTimeFormatter implements DifferenceFormatterInterface
     /**
      * Get the difference in a human readable format.
      *
-     * @param \Cake\Chronos\ChronosInterface $dateTime1 The datetime to start with.
-     * @param \Cake\Chronos\ChronosInterface|null $dateTime2 The datetime to compare against.
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate $first The datetime to start with.
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|null $second The datetime to compare against.
      * @param bool $absolute Removes time difference modifiers ago, after, etc.
      * @return string The difference between the two days in a human readable format.
-     * @see \Cake\Chronos\ChronosInterface::diffForHumans
+     * @see \Cake\Chronos\Chronos::diffForHumans
      */
     public function diffForHumans(
-        ChronosInterface $dateTime1,
-        ?ChronosInterface $dateTime2 = null,
+        Chronos|ChronosDate $first,
+        Chronos|ChronosDate|DateTimeInterface|null $second = null,
         bool $absolute = false
     ): string {
-        $isNow = $dateTime2 === null;
-        if ($isNow) {
-            $dateTime2 = $dateTime1->now($dateTime1->getTimezone());
+        $isNow = $second === null;
+        if ($second === null) {
+            if ($first instanceof ChronosDate) {
+                $second = $first->now();
+            } else {
+                $second = $first->now($first->getTimezone());
+            }
         }
-        /** @psalm-suppress PossiblyNullArgument */
-        $diffInterval = $dateTime1->diff($dateTime2);
+        $diffInterval = $first->diff($second);
 
         switch (true) {
             case $diffInterval->y > 0:
@@ -58,8 +63,8 @@ class RelativeTimeFormatter implements DifferenceFormatterInterface
                 break;
             case $diffInterval->d > 0:
                 $count = $diffInterval->d;
-                if ($count >= I18nDateTimeInterface::DAYS_PER_WEEK) {
-                    $count = (int)($count / I18nDateTimeInterface::DAYS_PER_WEEK);
+                if ($count >= DateTime::DAYS_PER_WEEK) {
+                    $count = (int)($count / DateTime::DAYS_PER_WEEK);
                     $message = __dn('cake', '{0} week', '{0} weeks', $count, $count);
                 } else {
                     $message = __dn('cake', '{0} day', '{0} days', $count, $count);
@@ -92,12 +97,12 @@ class RelativeTimeFormatter implements DifferenceFormatterInterface
     /**
      * Format a into a relative timestring.
      *
-     * @param \Cake\I18n\I18nDateTimeInterface $time The time instance to format.
+     * @param \Cake\I18n\DateTime|\Cake\I18n\Date $time The time instance to format.
      * @param array<string, mixed> $options Array of options.
      * @return string Relative time string.
      * @see \Cake\I18n\Time::timeAgoInWords()
      */
-    public function timeAgoInWords(I18nDateTimeInterface $time, array $options = []): string
+    public function timeAgoInWords(DateTime|Date $time, array $options = []): string
     {
         $options = $this->_options($options, DateTime::class);
         if ($options['timezone']) {
@@ -314,15 +319,15 @@ class RelativeTimeFormatter implements DifferenceFormatterInterface
     /**
      * Format a into a relative date string.
      *
-     * @param \Cake\I18n\I18nDateTimeInterface $date The date to format.
+     * @param \Cake\I18n\DateTime|\Cake\I18n\Date $date The date to format.
      * @param array<string, mixed> $options Array of options.
      * @return string Relative date string.
      * @see \Cake\I18n\Date::timeAgoInWords()
      */
-    public function dateAgoInWords(I18nDateTimeInterface $date, array $options = []): string
+    public function dateAgoInWords(DateTime|Date $date, array $options = []): string
     {
         $options = $this->_options($options, Date::class);
-        if ($options['timezone']) {
+        if ($date instanceof DateTime && $options['timezone']) {
             $date = $date->setTimezone($options['timezone']);
         }
 

+ 3 - 2
src/ORM/Behavior/TimestampBehavior.php

@@ -16,6 +16,7 @@ declare(strict_types=1);
  */
 namespace Cake\ORM\Behavior;
 
+use Cake\Chronos\Chronos;
 use Cake\Database\Type\DateTimeType;
 use Cake\Database\TypeFactory;
 use Cake\Datasource\EntityInterface;
@@ -145,11 +146,11 @@ class TimestampBehavior extends Behavior
      * If an explicit date time is passed, the config option `refreshTimestamp` is
      * automatically set to false.
      *
-     * @param \DateTimeInterface|null $ts Timestamp
+     * @param \Cake\Chronos\Chronos|\DateTimeInterface|null $ts Timestamp
      * @param bool $refreshTimestamp If true timestamp is refreshed.
      * @return \Cake\I18n\DateTime
      */
-    public function timestamp(?DateTimeInterface $ts = null, bool $refreshTimestamp = false): DateTimeInterface
+    public function timestamp(Chronos|DateTimeInterface|null $ts = null, bool $refreshTimestamp = false): DateTime
     {
         if ($ts) {
             if ($this->_config['refreshTimestamp']) {

+ 66 - 52
src/View/Helper/TimeHelper.php

@@ -16,6 +16,8 @@ declare(strict_types=1);
  */
 namespace Cake\View\Helper;
 
+use Cake\Chronos\Chronos;
+use Cake\Chronos\ChronosDate;
 use Cake\I18n\DateTime;
 use Cake\View\Helper;
 use Cake\View\StringTemplateTrait;
@@ -64,12 +66,12 @@ class TimeHelper extends Helper
     /**
      * Returns a UNIX timestamp, given either a UNIX timestamp or a valid strtotime() date string.
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return \Cake\I18n\DateTime
      */
     public function fromString(
-        DateTimeInterface|string|int $dateString,
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
         DateTimeZone|string|null $timezone = null
     ): DateTime {
         $time = new DateTime($dateString);
@@ -89,7 +91,7 @@ class TimeHelper extends Helper
      * @return string Formatted date string
      */
     public function nice(
-        DateTimeInterface|string|int|null $dateString = null,
+        Chronos|ChronosDate|DateTimeInterface|string|int|null $dateString = null,
         DateTimeZone|string|null $timezone = null,
         ?string $locale = null
     ): string {
@@ -101,48 +103,54 @@ class TimeHelper extends Helper
     /**
      * Returns true, if the given datetime string is today.
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return bool True if the given datetime string is today.
      */
-    public function isToday(DateTimeInterface|string|int $dateString, DateTimeZone|string|null $timezone = null): bool
-    {
+    public function isToday(
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
+        DateTimeZone|string|null $timezone = null
+    ): bool {
         return (new DateTime($dateString, $timezone))->isToday();
     }
 
     /**
      * Returns true, if the given datetime string is in the future.
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return bool True if the given datetime string lies in the future.
      */
-    public function isFuture(DateTimeInterface|string|int $dateString, DateTimeZone|string|null $timezone = null): bool
-    {
+    public function isFuture(
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
+        DateTimeZone|string|null $timezone = null
+    ): bool {
         return (new DateTime($dateString, $timezone))->isFuture();
     }
 
     /**
      * Returns true, if the given datetime string is in the past.
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return bool True if the given datetime string lies in the past.
      */
-    public function isPast(DateTimeInterface|string|int $dateString, DateTimeZone|string|null $timezone = null): bool
-    {
+    public function isPast(
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
+        DateTimeZone|string|null $timezone = null
+    ): bool {
         return (new DateTime($dateString, $timezone))->isPast();
     }
 
     /**
      * Returns true if given datetime string is within this week.
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return bool True if datetime string is within current week
      */
     public function isThisWeek(
-        DateTimeInterface|string|int $dateString,
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
         DateTimeZone|string|null $timezone = null
     ): bool {
         return (new DateTime($dateString, $timezone))->isThisWeek();
@@ -151,12 +159,12 @@ class TimeHelper extends Helper
     /**
      * Returns true if given datetime string is within this month
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return bool True if datetime string is within the current month
      */
     public function isThisMonth(
-        DateTimeInterface|string|int $dateString,
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
         DateTimeZone|string|null $timezone = null
     ): bool {
         return (new DateTime($dateString, $timezone))->isThisMonth();
@@ -165,12 +173,12 @@ class TimeHelper extends Helper
     /**
      * Returns true if given datetime string is within the current year.
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return bool True if datetime string is within current year
      */
     public function isThisYear(
-        DateTimeInterface|string|int $dateString,
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
         DateTimeZone|string|null $timezone = null
     ): bool {
         return (new DateTime($dateString, $timezone))->isThisYear();
@@ -179,12 +187,12 @@ class TimeHelper extends Helper
     /**
      * Returns true if given datetime string was yesterday.
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return bool True if datetime string was yesterday
      */
     public function wasYesterday(
-        DateTimeInterface|string|int $dateString,
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
         DateTimeZone|string|null $timezone = null
     ): bool {
         return (new DateTime($dateString, $timezone))->isYesterday();
@@ -193,12 +201,12 @@ class TimeHelper extends Helper
     /**
      * Returns true if given datetime string is tomorrow.
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return bool True if datetime string was yesterday
      */
     public function isTomorrow(
-        DateTimeInterface|string|int $dateString,
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
         DateTimeZone|string|null $timezone = null
     ): bool {
         return (new DateTime($dateString, $timezone))->isTomorrow();
@@ -207,39 +215,45 @@ class TimeHelper extends Helper
     /**
      * Returns the quarter
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param bool $range if true returns a range in Y-m-d format
      * @return array<string>|int 1, 2, 3, or 4 quarter of year or array if $range true
      * @see \Cake\I18n\Time::toQuarter()
      */
-    public function toQuarter(DateTimeInterface|string|int $dateString, bool $range = false): array|int
-    {
+    public function toQuarter(
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
+        bool $range = false
+    ): array|int {
         return (new DateTime($dateString))->toQuarter($range);
     }
 
     /**
      * Returns a UNIX timestamp from a textual datetime description.
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return string UNIX timestamp
      * @see \Cake\I18n\Time::toUnix()
      */
-    public function toUnix(DateTimeInterface|string|int $dateString, DateTimeZone|string|null $timezone = null): string
-    {
+    public function toUnix(
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
+        DateTimeZone|string|null $timezone = null
+    ): string {
         return (new DateTime($dateString, $timezone))->toUnixString();
     }
 
     /**
      * Returns a date formatted for Atom RSS feeds.
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return string Formatted date string
      * @see \Cake\I18n\Time::toAtom()
      */
-    public function toAtom(DateTimeInterface|string|int $dateString, DateTimeZone|string|null $timezone = null): string
-    {
+    public function toAtom(
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
+        DateTimeZone|string|null $timezone = null
+    ): string {
         $timezone = $this->_getTimezone($timezone) ?: date_default_timezone_get();
 
         return (new DateTime($dateString))->setTimezone($timezone)->toAtomString();
@@ -248,12 +262,14 @@ class TimeHelper extends Helper
     /**
      * Formats date for RSS feeds
      *
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return string Formatted date string
      */
-    public function toRss(DateTimeInterface|string|int $dateString, DateTimeZone|string|null $timezone = null): string
-    {
+    public function toRss(
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
+        DateTimeZone|string|null $timezone = null
+    ): string {
         $timezone = $this->_getTimezone($timezone) ?: date_default_timezone_get();
 
         return (new DateTime($dateString))->setTimezone($timezone)->toRssString();
@@ -270,25 +286,23 @@ class TimeHelper extends Helper
      *   - `class` - The class name to use, defaults to `time-ago-in-words`.
      *   - `title` - Defaults to the $dateTime input.
      *
-     * @param \DateTimeInterface|string|int $dateTime UNIX timestamp, strtotime() valid
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateTime UNIX timestamp, strtotime() valid
      *   string or DateTime object.
      * @param array<string, mixed> $options Default format if timestamp is used in $dateString
      * @return string Relative time string.
      * @see \Cake\I18n\Time::timeAgoInWords()
      */
-    public function timeAgoInWords(DateTimeInterface|string|int $dateTime, array $options = []): string
-    {
+    public function timeAgoInWords(
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateTime,
+        array $options = []
+    ): string {
         $element = null;
         $options += [
             'element' => null,
             'timezone' => null,
         ];
         $options['timezone'] = $this->_getTimezone($options['timezone']);
-        if ($options['timezone'] && $dateTime instanceof DateTimeInterface) {
-            /**
-             * @psalm-suppress UndefinedInterfaceMethod
-             * @phpstan-ignore-next-line
-             */
+        if ($options['timezone'] && ($dateTime instanceof Chronos || $dateTime instanceof DateTimeInterface)) {
             $dateTime = $dateTime->setTimezone($options['timezone']);
             unset($options['timezone']);
         }
@@ -327,14 +341,14 @@ class TimeHelper extends Helper
      *
      * @param string $timeInterval the numeric value with space then time type.
      *    Example of valid types: 6 hours, 2 days, 1 minute.
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return bool
      * @see \Cake\I18n\Time::wasWithinLast()
      */
     public function wasWithinLast(
         string $timeInterval,
-        DateTimeInterface|string|int $dateString,
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
         DateTimeZone|string|null $timezone = null
     ): bool {
         return (new DateTime($dateString, $timezone))->wasWithinLast($timeInterval);
@@ -345,14 +359,14 @@ class TimeHelper extends Helper
      *
      * @param string $timeInterval the numeric value with space then time type.
      *    Example of valid types: 6 hours, 2 days, 1 minute.
-     * @param \DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int $dateString UNIX timestamp, strtotime() valid string or DateTime object
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
      * @return bool
      * @see \Cake\I18n\Time::wasWithinLast()
      */
     public function isWithinNext(
         string $timeInterval,
-        DateTimeInterface|string|int $dateString,
+        Chronos|ChronosDate|DateTimeInterface|string|int $dateString,
         DateTimeZone|string|null $timezone = null
     ): bool {
         return (new DateTime($dateString, $timezone))->isWithinNext($timeInterval);
@@ -361,11 +375,11 @@ class TimeHelper extends Helper
     /**
      * Returns gmt as a UNIX timestamp.
      *
-     * @param \DateTimeInterface|string|int|null $string UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int|null $string UNIX timestamp, strtotime() valid string or DateTime object
      * @return string UNIX timestamp
      * @see \Cake\I18n\Time::gmt()
      */
-    public function gmt(DateTimeInterface|string|int|null $string = null): string
+    public function gmt(Chronos|ChronosDate|DateTimeInterface|string|int|null $string = null): string
     {
         return (new DateTime($string))->toUnixString();
     }
@@ -376,7 +390,7 @@ class TimeHelper extends Helper
      *
      * This method is an alias for TimeHelper::i18nFormat().
      *
-     * @param \DateTimeInterface|string|int|null $date UNIX timestamp, strtotime() valid string
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int|null $date UNIX timestamp, strtotime() valid string
      *   or DateTime object (or a date format string).
      * @param string|int|null $format date format string (or a UNIX timestamp,
      *   `strtotime()` valid string or DateTime object).
@@ -386,7 +400,7 @@ class TimeHelper extends Helper
      * @see \Cake\I18n\Time::i18nFormat()
      */
     public function format(
-        DateTimeInterface|string|int|null $date,
+        Chronos|ChronosDate|DateTimeInterface|string|int|null $date,
         string|int|null $format = null,
         string|false $invalid = false,
         DateTimeZone|string|null $timezone = null
@@ -398,7 +412,7 @@ class TimeHelper extends Helper
      * Returns a formatted date string, given either a Datetime instance,
      * UNIX timestamp or a valid strtotime() date string.
      *
-     * @param \DateTimeInterface|string|int|null $date UNIX timestamp, strtotime() valid string or DateTime object
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int|null $date UNIX timestamp, strtotime() valid string or DateTime object
      * @param array|string|int|null $format Intl compatible format string.
      * @param string|false $invalid Default value to display on invalid dates
      * @param \DateTimeZone|string|null $timezone User's timezone string or DateTimeZone object
@@ -407,7 +421,7 @@ class TimeHelper extends Helper
      * @see \Cake\I18n\Time::i18nFormat()
      */
     public function i18nFormat(
-        DateTimeInterface|string|int|null $date,
+        Chronos|ChronosDate|DateTimeInterface|string|int|null $date,
         array|string|int|null $format = null,
         string|false $invalid = false,
         DateTimeZone|string|null $timezone = null

+ 9 - 5
src/View/Widget/DateTimeWidget.php

@@ -16,6 +16,8 @@ declare(strict_types=1);
  */
 namespace Cake\View\Widget;
 
+use Cake\Chronos\Chronos;
+use Cake\Chronos\ChronosDate;
 use Cake\Database\Schema\TableSchemaInterface;
 use Cake\View\Form\ContextInterface;
 use Cake\View\StringTemplate;
@@ -175,20 +177,22 @@ class DateTimeWidget extends BasicWidget
     /**
      * Formats the passed date/time value into required string format.
      *
-     * @param \DateTimeInterface|string|int|null $value Value to deconstruct.
+     * @param \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTimeInterface|string|int|null $value Value to deconstruct.
      * @param array<string, mixed> $options Options for conversion.
      * @return string
      * @throws \InvalidArgumentException If invalid input type is passed.
      */
-    protected function formatDateTime(DateTimeInterface|string|int|null $value, array $options): string
-    {
+    protected function formatDateTime(
+        Chronos|ChronosDate|DateTimeInterface|string|int|null $value,
+        array $options
+    ): string {
         if ($value === '' || $value === null) {
             return '';
         }
 
         try {
-            if ($value instanceof DateTimeInterface) {
-                /** @var \DateTime|\DateTimeImmutable $dateTime Expand type for phpstan */
+            if ($value instanceof Chronos || $value instanceof ChronosDate || $value instanceof DateTimeInterface) {
+                /** @var \Cake\Chronos\Chronos|\Cake\Chronos\ChronosDate|\DateTime|\DateTimeImmutable $dateTime Expand type for phpstan */
                 $dateTime = clone $value;
             } elseif (is_string($value) && !is_numeric($value)) {
                 $dateTime = new DateTime($value);

+ 7 - 1
src/View/Widget/YearWidget.php

@@ -16,6 +16,8 @@ declare(strict_types=1);
  */
 namespace Cake\View\Widget;
 
+use Cake\Chronos\Chronos;
+use Cake\Chronos\ChronosDate;
 use Cake\View\Form\ContextInterface;
 use Cake\View\StringTemplate;
 use DateTimeInterface;
@@ -84,7 +86,11 @@ class YearWidget extends BasicWidget
         $data['min'] = (int)$data['min'];
         $data['max'] = (int)$data['max'];
 
-        if ($data['val'] instanceof DateTimeInterface) {
+        if (
+            $data['val'] instanceof Chronos ||
+            $data['val'] instanceof ChronosDate ||
+            $data['val'] instanceof DateTimeInterface
+        ) {
             $data['val'] = $data['val']->format('Y');
         }
 

+ 3 - 4
tests/TestCase/Database/Query/SelectQueryTest.php

@@ -35,10 +35,9 @@ use Cake\Database\TypeFactory;
 use Cake\Database\TypeMap;
 use Cake\Database\ValueBinder;
 use Cake\Datasource\ConnectionManager;
+use Cake\I18n\DateTime;
 use Cake\Test\TestCase\Database\QueryAssertsTrait;
 use Cake\TestSuite\TestCase;
-use DateTime;
-use DateTimeImmutable;
 use InvalidArgumentException;
 use ReflectionProperty;
 use stdClass;
@@ -3749,8 +3748,8 @@ class SelectQueryTest extends TestCase
 
         $result = $query->execute()->fetchAll('assoc');
         $this->assertIsInt($result[0]['id']);
-        $this->assertInstanceOf(DateTimeImmutable::class, $result[0]['the_date']);
-        $this->assertInstanceOf(DateTimeImmutable::class, $result[0]['updated']);
+        $this->assertInstanceOf(DateTime::class, $result[0]['the_date']);
+        $this->assertInstanceOf(DateTime::class, $result[0]['updated']);
     }
 
     /**

+ 3 - 7
tests/TestCase/Database/Type/DateTimeFractionalTypeTest.php

@@ -365,22 +365,18 @@ class DateTimeFractionalTypeTest extends TestCase
             // Invalid array types
             [
                 ['year' => 'farts', 'month' => 'derp'],
-                new DateTime(date('Y-m-d 00:00:00')),
+                null,
             ],
             [
                 ['year' => 'farts', 'month' => 'derp', 'day' => 'farts'],
-                new DateTime(date('Y-m-d 00:00:00')),
+                null,
             ],
             [
                 [
                     'year' => '2014', 'month' => '02', 'day' => '14',
                     'hour' => 'farts', 'minute' => 'farts',
                 ],
-                new DateTime('2014-02-14 00:00:00'),
-            ],
-            [
-                DateTime::now(),
-                DateTime::now(),
+                null,
             ],
         ];
     }

+ 3 - 7
tests/TestCase/Database/Type/DateTimeTimezoneTypeTest.php

@@ -403,22 +403,18 @@ class DateTimeTimezoneTypeTest extends TestCase
             // Invalid array types
             [
                 ['year' => 'farts', 'month' => 'derp'],
-                new DateTime(date('Y-m-d 00:00:00')),
+                null,
             ],
             [
                 ['year' => 'farts', 'month' => 'derp', 'day' => 'farts'],
-                new DateTime(date('Y-m-d 00:00:00')),
+                null,
             ],
             [
                 [
                     'year' => '2014', 'month' => '02', 'day' => '14',
                     'hour' => 'farts', 'minute' => 'farts',
                 ],
-                new DateTime('2014-02-14 00:00:00'),
-            ],
-            [
-                DateTime::now(),
-                DateTime::now(),
+                null,
             ],
         ];
     }

+ 5 - 14
tests/TestCase/Database/Type/DateTimeTypeTest.php

@@ -209,8 +209,8 @@ class DateTimeTypeTest extends TestCase
             ['2017-04-05T17:18:00+00:00', new DateTime('2017-04-05T17:18:00+00:00')],
 
             [new DateTime('2017-04-05T17:18:00+00:00'), new DateTime('2017-04-05T17:18:00+00:00')],
-            [new NativeDateTime('2017-04-05T17:18:00+00:00'), new DateTime('2017-04-05T17:18:00+00:00')],
-            [new DateTimeImmutable('2017-04-05T17:18:00+00:00'), new DateTime('2017-04-05T17:18:00+00:00')],
+            [new NativeDateTime('2017-04-05T17:18:00+00:00'), new NativeDateTime('2017-04-05T17:18:00+00:00')],
+            [new DateTimeImmutable('2017-04-05T17:18:00+00:00'), new DateTimeImmutable('2017-04-05T17:18:00+00:00')],
 
             // valid array types
             [
@@ -261,18 +261,18 @@ class DateTimeTypeTest extends TestCase
             // Invalid array types
             [
                 ['year' => 'farts', 'month' => 'derp'],
-                new DateTime(date('Y-m-d 00:00:00')),
+                null,
             ],
             [
                 ['year' => 'farts', 'month' => 'derp', 'day' => 'farts'],
-                new DateTime(date('Y-m-d 00:00:00')),
+                null,
             ],
             [
                 [
                     'year' => '2014', 'month' => '02', 'day' => '14',
                     'hour' => 'farts', 'minute' => 'farts',
                 ],
-                new DateTime('2014-02-14 00:00:00'),
+                null,
             ],
         ];
     }
@@ -377,13 +377,4 @@ class DateTimeTypeTest extends TestCase
         $result = $this->type->marshal('13 Oct, 2013 01:54pm');
         $this->assertEquals($expected, $result);
     }
-
-    /**
-     * Test that toImmutable changes all the methods to create frozen time instances.
-     */
-    public function testToImmutableAndToMutable(): void
-    {
-        $this->assertInstanceOf('DateTimeImmutable', $this->type->marshal('2015-11-01 11:23:00'));
-        $this->assertInstanceOf('DateTimeImmutable', $this->type->toPHP('2015-11-01 11:23:00', $this->driver));
-    }
 }

+ 12 - 21
tests/TestCase/Database/Type/DateTypeTest.php

@@ -57,7 +57,7 @@ class DateTypeTest extends TestCase
         $this->assertNull($this->type->toPHP('0000-00-00', $this->driver));
 
         $result = $this->type->toPHP('2001-01-04', $this->driver);
-        $this->assertInstanceOf(DateTimeImmutable::class, $result);
+        $this->assertInstanceOf(Date::class, $result);
         $this->assertSame('2001', $result->format('Y'));
         $this->assertSame('01', $result->format('m'));
         $this->assertSame('04', $result->format('d'));
@@ -127,8 +127,8 @@ class DateTypeTest extends TestCase
             ['2014-02-14', new Date('2014-02-14')],
 
             [new Date('2014-02-14'), new Date('2014-02-14')],
-            [new NativeDateTime('2014-02-14'), new Date('2014-02-14')],
-            [new DateTimeImmutable('2014-02-14'), new Date('2014-02-14')],
+            [new NativeDateTime('2014-02-14'), new NativeDateTime('2014-02-14')],
+            [new DateTimeImmutable('2014-02-14'), new DateTimeImmutable('2014-02-14')],
 
             // valid array types
             [
@@ -161,22 +161,22 @@ class DateTypeTest extends TestCase
                 ],
                 new Date('2014-02-14'),
             ],
+            [
+                [
+                    'year' => '2014', 'month' => '02', 'day' => '14',
+                    'hour' => 'farts', 'minute' => 'farts',
+                ],
+                new Date('2014-02-14'),
+            ],
 
             // Invalid array types
             [
                 ['year' => 'farts', 'month' => 'derp'],
-                new Date(date('Y-m-d')),
+                null,
             ],
             [
                 ['year' => 'farts', 'month' => 'derp', 'day' => 'farts'],
-                new Date(date('Y-m-d')),
-            ],
-            [
-                [
-                    'year' => '2014', 'month' => '02', 'day' => '14',
-                    'hour' => 'farts', 'minute' => 'farts',
-                ],
-                new Date('2014-02-14'),
+                null,
             ],
         ];
     }
@@ -219,13 +219,4 @@ class DateTypeTest extends TestCase
         $result = $this->type->marshal('13 Oct, 2013');
         $this->assertSame($expected->format('Y-m-d'), $result->format('Y-m-d'));
     }
-
-    /**
-     * Test that toImmutable changes all the methods to create frozen time instances.
-     */
-    public function testToImmutableAndToMutable(): void
-    {
-        $this->assertInstanceOf('DateTimeImmutable', $this->type->marshal('2015-11-01'));
-        $this->assertInstanceOf('DateTimeImmutable', $this->type->toPHP('2015-11-01', $this->driver));
-    }
 }

+ 22 - 25
tests/TestCase/Database/Type/TimeTypeTest.php

@@ -71,7 +71,7 @@ class TimeTypeTest extends TestCase
         $this->assertSame('15', $result->format('s'));
 
         $result = $this->type->toPHP('16:30:15', $this->driver);
-        $this->assertInstanceOf(DateTimeImmutable::class, $result);
+        $this->assertInstanceOf(DateTime::class, $result);
         $this->assertSame('16', $result->format('H'));
         $this->assertSame('30', $result->format('i'));
         $this->assertSame('15', $result->format('s'));
@@ -140,21 +140,13 @@ class TimeTypeTest extends TestCase
             ['14:15', new DateTime('14:15:00')],
 
             [new DateTime('13:10:10'), new DateTime('13:10:10')],
-            [new NativeDateTime('13:10:10'), new DateTime('13:10:10')],
-            [new DateTimeImmutable('13:10:10'), new DateTime('13:10:10')],
+            [new NativeDateTime('13:10:10'), new NativeDateTime('13:10:10')],
+            [new DateTimeImmutable('13:10:10'), new DateTimeImmutable('13:10:10')],
 
             // valid array types
             [
-                ['hour' => '', 'minute' => '', 'second' => ''],
-                null,
-            ],
-            [
-                ['hour' => '', 'minute' => '', 'meridian' => ''],
-                null,
-            ],
-            [
                 ['year' => 2014, 'month' => 2, 'day' => 14, 'hour' => 13, 'minute' => 14, 'second' => 15],
-                new DateTime('2014-02-14 13:14:15'),
+                new DateTime('13:14:15'),
             ],
             [
                 [
@@ -162,7 +154,7 @@ class TimeTypeTest extends TestCase
                     'hour' => 1, 'minute' => 14, 'second' => 15,
                     'meridian' => 'am',
                 ],
-                new DateTime('2014-02-14 01:14:15'),
+                new DateTime('01:14:15'),
             ],
             [
                 [
@@ -170,7 +162,7 @@ class TimeTypeTest extends TestCase
                     'hour' => 1, 'minute' => 14, 'second' => 15,
                     'meridian' => 'pm',
                 ],
-                new DateTime('2014-02-14 13:14:15'),
+                new DateTime('13:14:15'),
             ],
             [
                 [
@@ -178,18 +170,32 @@ class TimeTypeTest extends TestCase
                 ],
                 new DateTime('01:14:15'),
             ],
+            [
+                [
+                    'hour' => 1, 'minute' => 14,
+                ],
+                new DateTime('01:14:00'),
+            ],
 
             // Invalid array types
             [
+                ['hour' => '', 'minute' => '', 'second' => ''],
+                null,
+            ],
+            [
+                ['hour' => '', 'minute' => '', 'meridian' => ''],
+                null,
+            ],
+            [
                 ['hour' => 'nope', 'minute' => 14, 'second' => 15],
-                new DateTime(date('Y-m-d 00:14:15')),
+                null,
             ],
             [
                 [
                     'year' => '2014', 'month' => '02', 'day' => '14',
                     'hour' => 'nope', 'minute' => 'nope',
                 ],
-                new DateTime('2014-02-14 00:00:00'),
+                null,
             ],
         ];
     }
@@ -237,13 +243,4 @@ class TimeTypeTest extends TestCase
         $result = $this->type->useLocaleParser()->marshal('03.20');
         $this->assertSame($expected->format('H:i'), $result->format('H:i'));
     }
-
-    /**
-     * Test that toImmutable changes all the methods to create frozen time instances.
-     */
-    public function testToImmutableAndToMutable(): void
-    {
-        $this->assertInstanceOf('DateTimeImmutable', $this->type->marshal('11:23:12'));
-        $this->assertInstanceOf('DateTimeImmutable', $this->type->toPHP('11:23:12', $this->driver));
-    }
 }

+ 9 - 17
tests/TestCase/I18n/DateTest.php

@@ -18,6 +18,7 @@ namespace Cake\Test\TestCase\I18n;
 
 use Cake\Cache\Cache;
 use Cake\I18n\Date;
+use Cake\I18n\DateTime;
 use Cake\I18n\I18n;
 use Cake\I18n\Package;
 use Cake\TestSuite\TestCase;
@@ -53,7 +54,7 @@ class DateTest extends TestCase
     public function tearDown(): void
     {
         parent::tearDown();
-        Date::setDefaultLocale(null);
+        DateTime::setDefaultLocale(null);
         date_default_timezone_set('UTC');
     }
 
@@ -87,7 +88,7 @@ class DateTest extends TestCase
         $expected = '00:00:00';
         $this->assertSame($expected, $result);
 
-        Date::setDefaultLocale('fr-FR');
+        DateTime::setDefaultLocale('fr-FR');
         $result = $time->i18nFormat(IntlDateFormatter::FULL);
         $result = str_replace(' à', '', $result);
         $expected = 'jeudi 14 janvier 2010 00:00:00';
@@ -168,7 +169,7 @@ class DateTest extends TestCase
         $date = Date::parseDate('11/6/15');
         $this->assertSame('2015-11-06 00:00:00', $date->format('Y-m-d H:i:s'));
 
-        Date::setDefaultLocale('fr-FR');
+        DateTime::setDefaultLocale('fr-FR');
         $date = Date::parseDate('13 10, 2015');
         $this->assertSame('2015-10-13 00:00:00', $date->format('Y-m-d H:i:s'));
     }
@@ -181,7 +182,7 @@ class DateTest extends TestCase
         $date = Date::parseDate('11/6/15 12:33:12');
         $this->assertSame('2015-11-06 00:00:00', $date->format('Y-m-d H:i:s'));
 
-        Date::setDefaultLocale('fr-FR');
+        DateTime::setDefaultLocale('fr-FR');
         $date = Date::parseDate('13 10, 2015 12:54:12');
         $this->assertSame('2015-10-13 00:00:00', $date->format('Y-m-d H:i:s'));
     }
@@ -191,13 +192,13 @@ class DateTest extends TestCase
      */
     public function testLenientParseDate(): void
     {
-        Date::setDefaultLocale('pt_BR');
+        DateTime::setDefaultLocale('pt_BR');
 
-        Date::disableLenientParsing();
+        DateTime::disableLenientParsing();
         $date = Date::parseDate('04/21/2013');
         $this->assertSame(null, $date);
 
-        Date::enableLenientParsing();
+        DateTime::enableLenientParsing();
         $date = Date::parseDate('04/21/2013');
         $this->assertSame('2014-09-04', $date->format('Y-m-d'));
     }
@@ -482,21 +483,12 @@ class DateTest extends TestCase
     /**
      * Tests the default locale setter.
      */
-    public function testGetSetDefaultLocale(): void
-    {
-        Date::setDefaultLocale('fr-FR');
-        $this->assertSame('fr-FR', Date::getDefaultLocale());
-    }
-
-    /**
-     * Tests the default locale setter.
-     */
     public function testDefaultLocaleEffectsFormatting(): void
     {
         $result = Date::parseDate('12/03/2015');
         $this->assertSame('Dec 3, 2015', $result->nice());
 
-        Date::setDefaultLocale('fr-FR');
+        DateTime::setDefaultLocale('fr-FR');
 
         $result = Date::parseDate('12/03/2015');
         $this->assertSame('12 mars 2015', $result->nice());

+ 0 - 14
tests/TestCase/I18n/DateTimeTest.php

@@ -685,20 +685,6 @@ class DateTimeTest extends TestCase
     }
 
     /**
-     * Tests debugInfo
-     */
-    public function testDebugInfo(): void
-    {
-        $time = new DateTime('2014-04-20 10:10:10');
-        $expected = [
-            'time' => '2014-04-20 10:10:10.000000+00:00',
-            'timezone' => 'UTC',
-            'fixedNowTime' => DateTime::getTestNow()->format('Y-m-d\TH:i:s.uP'),
-        ];
-        $this->assertEquals($expected, $time->__debugInfo());
-    }
-
-    /**
      * Tests parsing a string into a Time object based on the locale format.
      */
     public function testParseDateTime(): void

+ 1 - 1
tests/TestCase/ORM/Behavior/TimestampBehaviorTest.php

@@ -273,7 +273,7 @@ class TimestampBehaviorTest extends TestCase
 
         $return = $behavior->timestamp();
         $this->assertInstanceOf(
-            'DateTimeImmutable',
+            DateTime::class,
             $return,
             'Should return a timestamp object'
         );

+ 1 - 1
tests/TestCase/View/Helper/TimeHelperTest.php

@@ -197,7 +197,7 @@ class TimeHelperTest extends TestCase
         $dateTime = new DateTime();
         $vancouver = clone $dateTime;
         $vancouver = $vancouver->setTimezone('America/Vancouver');
-        $this->assertSame($vancouver->format(DateTime::ATOM), $this->Time->toAtom($vancouver));
+        $this->assertSame($vancouver->format(DATE_ATOM), $this->Time->toAtom($vancouver));
     }
 
     /**