Repository: Seldaek/monolog (php)

Issue:
Rename `Monolog\DateTimeImmutable` to something else, e.g. `JsonSerializableDateTimeImmutable` 
First of all, thanks for this amazing package 💙 

So this is something that I'm bothered by for a long time. Every time I want to import `DateTimeImmutable`, PHPStorm asks me which one I want to import:

![Screenshot 2024-11-21 at 12 14 13@2x](https://github.com/user-attachments/assets/1db5f8ae-25e0-4b9e-9319-448c0b09d7d4)

Even though the version of Monolog is namespaced, it's still annoying to think which one to pick.

I want to propose the following:

* Rename `Monolog\DateTimeImmutable` to something different, for example: `Monolog\JsonSerializableDateTimeImmutable`
* Keep the old `Monolog\DateTimeImmutable` as an alias for `Monolog\JsonSerializableDateTimeImmutable`, and mark it as deprecated.
* In the next major, remove `Monolog\DateTimeImmutable`

Or if there is a better way to do this, I'm all ears. I would be happy to create the PR.

/cc @Seldaek 

Pull request:
Deprecate DateTimeImmutable
Fixes #1926

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ce16eecb..2b948ad3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+### Unreleased
+
+  * Deprecated Monolog\DateTimeImmutable in favor of Monolog\JsonSerializableDateTimeImmutable
+
 ### 3.8.0 (2024-11-12)
 
   * Added `$fileOpenMode` param to `StreamHandler` to define a custom fopen mode to open the log file (#1913)
diff --git a/UPGRADE.md b/UPGRADE.md
index 0446803f..5e82918f 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -1,3 +1,9 @@
+### 4.0.0
+
+Overall / notable changes:
+
+- Monolog\DateTimeImmutable has been removed in favor of Monolog\JsonSerializableDateTimeImmutable.
+
 ### 3.0.0
 
 Overall / notable changes:
diff --git a/doc/message-structure.md b/doc/message-structure.md
index b5e088c3..80090726 100644
--- a/doc/message-structure.md
+++ b/doc/message-structure.md
@@ -11,7 +11,7 @@ message    | string                    | The log message. When the `PsrLogMessag
 level      | Monolog\Level case        | Severity of the log message. See log levels described in [01-usage.md](01-usage.md#log-levels).
 context    | array                     | Arbitrary data passed with the construction of the message. For example the username of the current user or their IP address.
 channel    | string                    | The channel this message was logged to. This is the name that was passed when the logger was created with `new Logger('channel')`.
-datetime   | Monolog\DateTimeImmutable | Date and time when the message was logged. Class extends `\DateTimeImmutable`.
+datetime   | Monolog\JsonSerializableDateTimeImmutable | Date and time when the message was logged. Class extends `\DateTimeImmutable`.
 extra      | array                     | A placeholder array where processors can put additional data. Always available, but empty if there are no processors registered.
 
 At first glance `context` and `extra` look very similar, and they are in the sense that they both carry arbitrary data that is related to the log message somehow.
diff --git a/src/Monolog/DateTimeImmutable.php b/src/Monolog/DateTimeImmutable.php
index 3d9477f4..968c58a6 100644
--- a/src/Monolog/DateTimeImmutable.php
+++ b/src/Monolog/DateTimeImmutable.php
@@ -11,38 +11,13 @@
 
 namespace Monolog;
 
-use DateTimeZone;
+class_alias(JsonSerializableDateTimeImmutable::class, 'Monolog\DateTimeImmutable');
 
-/**
- * Overrides default json encoding of date time objects
- *
- * @author Menno Holtkamp
- * @author Jordi Boggiano <j.boggiano@seld.be>
- */
-class DateTimeImmutable extends \DateTimeImmutable implements \JsonSerializable
-{
-    private bool $useMicroseconds;
-
-    public function __construct(bool $useMicroseconds, ?DateTimeZone $timezone = null)
-    {
-        $this->useMicroseconds = $useMicroseconds;
-
-        // if you like to use a custom time to pass to Logger::addRecord directly,
-        // call modify() or setTimestamp() on this instance to change the date after creating it
-        parent::__construct('now', $timezone);
-    }
-
-    public function jsonSerialize(): string
-    {
-        if ($this->useMicroseconds) {
-            return $this->format('Y-m-d\TH:i:s.uP');
-        }
-
-        return $this->format('Y-m-d\TH:i:sP');
-    }
-
-    public function __toString(): string
+if (false) {
+    /**
+     * @deprecated Use \Monolog\JsonSerializableDateTimeImmutable instead.
+     */
+    class DateTimeImmutable extends JsonSerializableDateTimeImmutable
     {
-        return $this->jsonSerialize();
     }
 }
diff --git a/src/Monolog/Formatter/NormalizerFormatter.php b/src/Monolog/Formatter/NormalizerFormatter.php
index c2660359..60da29cf 100644
--- a/src/Monolog/Formatter/NormalizerFormatter.php
+++ b/src/Monolog/Formatter/NormalizerFormatter.php
@@ -11,7 +11,7 @@
 
 namespace Monolog\Formatter;
 
-use Monolog\DateTimeImmutable;
+use Monolog\JsonSerializableDateTimeImmutable;
 use Monolog\Utils;
 use Throwable;
 use Monolog\LogRecord;
@@ -322,9 +322,9 @@ class NormalizerFormatter implements FormatterInterface
 
     protected function formatDate(\DateTimeInterface $date): string
     {
-        // in case the date format isn't custom then we defer to the custom DateTimeImmutable
+        // in case the date format isn't custom then we defer to the custom JsonSerializableDateTimeImmutable
         // formatting logic, which will pick the right format based on whether useMicroseconds is on
-        if ($this->dateFormat === self::SIMPLE_DATE && $date instanceof DateTimeImmutable) {
+        if ($this->dateFormat === self::SIMPLE_DATE && $date instanceof JsonSerializableDateTimeImmutable) {
             return (string) $date;
         }
 
diff --git a/src/Monolog/Handler/ChromePHPHandler.php b/src/Monolog/Handler/ChromePHPHandler.php
index 61c9e83f..6598e822 100644
--- a/src/Monolog/Handler/ChromePHPHandler.php
+++ b/src/Monolog/Handler/ChromePHPHandler.php
@@ -16,7 +16,7 @@ use Monolog\Formatter\FormatterInterface;
 use Monolog\Level;
 use Monolog\Utils;
 use Monolog\LogRecord;
-use Monolog\DateTimeImmutable;
+use Monolog\JsonSerializableDateTimeImmutable;
 
 /**
  * Handler sending logs to the ChromePHP extension (http://www.chromephp.com/)
@@ -150,7 +150,7 @@ class ChromePHPHandler extends AbstractProcessingHandler
                 message: 'Incomplete logs, chrome header size limit reached',
                 level: Level::Warning,
                 channel: 'monolog',
-                datetime: new DateTimeImmutable(true),
+                datetime: new JsonSerializableDateTimeImmutable(true),
             );
             self::$json['rows'][\count(self::$json['rows']) - 1] = $this->getFormatter()->format($record);
             $json = Utils::jsonEncode(self::$json, Utils::DEFAULT_JSON_FLAGS & ~JSON_UNESCAPED_UNICODE, true);
diff --git a/src/Monolog/JsonSerializableDateTimeImmutable.php b/src/Monolog/JsonSerializableDateTimeImmutable.php
new file mode 100644
index 00000000..de2cc5e9
--- /dev/null
+++ b/src/Monolog/JsonSerializableDateTimeImmutable.php
@@ -0,0 +1,48 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Monolog;
+
+use DateTimeZone;
+
+/**
+ * Overrides default json encoding of date time objects
+ *
+ * @author Menno Holtkamp
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class JsonSerializableDateTimeImmutable extends \DateTimeImmutable implements \JsonSerializable
+{
+    private bool $useMicroseconds;
+
+    public function __construct(bool $useMicroseconds, ?DateTimeZone $timezone = null)
+    {
+        $this->useMicroseconds = $useMicroseconds;
+
+        // if you like to use a custom time to pass to Logger::addRecord directly,
+        // call modify() or setTimestamp() on this instance to change the date after creating it
+        parent::__construct('now', $timezone);
+    }
+
+    public function jsonSerialize(): string
+    {
+        if ($this->useMicroseconds) {
+            return $this->format('Y-m-d\TH:i:s.uP');
+        }
+
+        return $this->format('Y-m-d\TH:i:sP');
+    }
+
+    public function __toString(): string
+    {
+        return $this->jsonSerialize();
+    }
+}
diff --git a/src/Monolog/Logger.php b/src/Monolog/Logger.php
index f57fe332..e545c448 100644
--- a/src/Monolog/Logger.php
+++ b/src/Monolog/Logger.php
@@ -323,12 +323,13 @@ class Logger implements LoggerInterface, ResettableInterface
      * @param  int                    $level    The logging level (a Monolog or RFC 5424 level)
      * @param  string                 $message  The log message
      * @param  mixed[]                $context  The log context
-     * @param  DateTimeImmutable|null $datetime Optional log date to log into the past or future
+     * @param  JsonSerializableDateTimeImmutable|null $datetime Optional log date to log into the past or future
+     *
      * @return bool                   Whether the record has been processed
      *
      * @phpstan-param value-of<Level::VALUES>|Level $level
      */
-    public function addRecord(int|Level $level, string $message, array $context = [], DateTimeImmutable|null $datetime = null): bool
+    public function addRecord(int|Level $level, string $message, array $context = [], JsonSerializableDateTimeImmutable|null $datetime = null): bool
     {
         if (\is_int($level) && isset(self::RFC_5424_LEVELS[$level])) {
             $level = self::RFC_5424_LEVELS[$level];
@@ -356,7 +357,7 @@ class Logger implements LoggerInterface, ResettableInterface
             $recordInitialized = \count($this->processors) === 0;
 
             $record = new LogRecord(
-                datetime: $datetime ?? new DateTimeImmutable($this->microsecondTimestamps, $this->timezone),
+                datetime: $datetime ?? new JsonSerializableDateTimeImmutable($this->microsecondTimestamps, $this->timezone),
                 channel: $this->name,
                 level: self::toMonologLevel($level),
                 message: $message,
@@ -518,7 +519,7 @@ class Logger implements LoggerInterface, ResettableInterface
     public function isHandling(int|string|Level $level): bool
     {
         $record = new LogRecord(
-            datetime: new DateTimeImmutable($this->microsecondTimestamps, $this->timezone),
+            datetime: new JsonSerializableDateTimeImmutable($this->microsecondTimestamps, $this->timezone),
             channel: $this->name,
             message: '',
             level: self::toMonologLevel($level),
diff --git a/src/Monolog/Processor/PsrLogMessageProcessor.php b/src/Monolog/Processor/PsrLogMessageProcessor.php
index 76adf258..291361d2 100644
--- a/src/Monolog/Processor/PsrLogMessageProcessor.php
+++ b/src/Monolog/Processor/PsrLogMessageProcessor.php
@@ -60,7 +60,7 @@ class PsrLogMessageProcessor implements ProcessorInterface
             if (null === $val || \is_scalar($val) || (\is_object($val) && method_exists($val, "__toString"))) {
                 $replacements[$placeholder] = $val;
             } elseif ($val instanceof \DateTimeInterface) {
-                if (null === $this->dateFormat && $val instanceof \Monolog\DateTimeImmutable) {
+                if (null === $this->dateFormat && $val instanceof \Monolog\JsonSerializableDateTimeImmutable) {
                     // handle monolog dates using __toString if no specific dateFormat was asked for
                     // so that it follows the useMicroseconds flag
                     $replacements[$placeholder] = (string) $val;


Test patch:
diff --git a/src/Monolog/Test/TestCase.php b/src/Monolog/Test/TestCase.php
index a2ff3f7a..ec5751b7 100644
--- a/src/Monolog/Test/TestCase.php
+++ b/src/Monolog/Test/TestCase.php
@@ -14,7 +14,7 @@ namespace Monolog\Test;
 use Monolog\Level;
 use Monolog\Logger;
 use Monolog\LogRecord;
-use Monolog\DateTimeImmutable;
+use Monolog\JsonSerializableDateTimeImmutable;
 use Monolog\Formatter\FormatterInterface;
 use Psr\Log\LogLevel;
 use ReflectionProperty;
@@ -34,7 +34,7 @@ class TestCase extends \PHPUnit\Framework\TestCase
      *
      * @phpstan-param value-of<Level::VALUES>|value-of<Level::NAMES>|Level|LogLevel::* $level
      */
-    protected function getRecord(int|string|Level $level = Level::Warning, string|\Stringable $message = 'test', array $context = [], string $channel = 'test', \DateTimeImmutable $datetime = new DateTimeImmutable(true), array $extra = []): LogRecord
+    protected function getRecord(int|string|Level $level = Level::Warning, string|\Stringable $message = 'test', array $context = [], string $channel = 'test', \DateTimeImmutable $datetime = new JsonSerializableDateTimeImmutable(true), array $extra = []): LogRecord
     {
         return new LogRecord(
             message: (string) $message,
diff --git a/tests/Monolog/Formatter/ScalarFormatterTest.php b/tests/Monolog/Formatter/ScalarFormatterTest.php
index 2cdf6830..693d47da 100644
--- a/tests/Monolog/Formatter/ScalarFormatterTest.php
+++ b/tests/Monolog/Formatter/ScalarFormatterTest.php
@@ -11,7 +11,7 @@
 
 namespace Monolog\Formatter;
 
-use Monolog\DateTimeImmutable;
+use Monolog\JsonSerializableDateTimeImmutable;
 use Monolog\Test\TestCase;
 
 class ScalarFormatterTest extends TestCase
@@ -57,7 +57,7 @@ class ScalarFormatterTest extends TestCase
             'baz' => false,
             'bam' => [1, 2, 3],
             'bat' => ['foo' => 'bar'],
-            'bap' => $dt = new DateTimeImmutable(true),
+            'bap' => $dt = new JsonSerializableDateTimeImmutable(true),
             'ban' => $exception,
         ]));
 
diff --git a/tests/Monolog/LoggerTest.php b/tests/Monolog/LoggerTest.php
index 67a5534f..fab00622 100644
--- a/tests/Monolog/LoggerTest.php
+++ b/tests/Monolog/LoggerTest.php
@@ -556,7 +556,7 @@ class LoggerTest extends TestCase
 
     /**
      * @covers Logger::setTimezone
-     * @covers DateTimeImmutable::__construct
+     * @covers JsonSerializableDateTimeImmutable::__construct
      */
     public function testTimezoneIsRespectedInUTC()
     {
@@ -578,7 +578,7 @@ class LoggerTest extends TestCase
 
     /**
      * @covers Logger::setTimezone
-     * @covers DateTimeImmutable::__construct
+     * @covers JsonSerializableDateTimeImmutable::__construct
      */
     public function testTimezoneIsRespectedInOtherTimezone()
     {
@@ -807,7 +807,7 @@ class LoggerTest extends TestCase
             $logger->pushHandler($loggingHandler);
             $logger->pushHandler($testHandler);
 
-            $datetime = (new DateTimeImmutable($microseconds))->modify('2022-03-04 05:06:07');
+            $datetime = (new JsonSerializableDateTimeImmutable($microseconds))->modify('2022-03-04 05:06:07');
             $logger->addRecord(Level::Debug, 'test', [], $datetime);
 
             list($record) = $testHandler->getRecords();