From 63c5f0e63e126c0bb1a8cdfa43e6b67ccdb76a1a Mon Sep 17 00:00:00 2001 From: Tim Soslow Date: Mon, 27 Apr 2026 14:44:24 -0500 Subject: [PATCH 1/6] Support psr/log v3 Composer require updated to "^1.1 || ^3.0" Updated StderrLogger log() to explicitly return void Relaxed integration test's error callback message parameter so Stringable-compatible values from psr/log v3 won't error --- composer.json | 2 +- src/StderrLogger.php | 2 +- tests/Integration/IntegTestCase.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index f5555f8..4d48fe3 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "psr/http-message": "^1.0", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", - "psr/log": "^1.1" + "psr/log": "^1.1 || ^3.0" }, "autoload": { "psr-4": { diff --git a/src/StderrLogger.php b/src/StderrLogger.php index 1436fd2..77e31e7 100644 --- a/src/StderrLogger.php +++ b/src/StderrLogger.php @@ -46,7 +46,7 @@ public function __construct(string $minLevel = LogLevel::WARNING, $stream = 'php } } - public function log($level, $message, array $context = []) + public function log($level, $message, array $context = []): void { if (!isset(self::LOG_LEVEL_MAP[$level])) { throw new InvalidArgumentException("Invalid log level: {$level}"); diff --git a/tests/Integration/IntegTestCase.php b/tests/Integration/IntegTestCase.php index 3631743..719cdfc 100644 --- a/tests/Integration/IntegTestCase.php +++ b/tests/Integration/IntegTestCase.php @@ -102,7 +102,7 @@ protected function createHttpServer(ServerRequestInterface $request): HttpServer protected function failOnLoggedErrors(): void { - $this->logger->method('error')->willReturnCallback(function (string $message, array $context) { + $this->logger->method('error')->willReturnCallback(function ($message, array $context) { $message = "Logged an error: {$message}\nContext:\n"; foreach ($context as $key => $value) { $message .= "- {$key}: {$value}\n"; From e656396b03e80e90a461744d1a0e66ba3422ee3f Mon Sep 17 00:00:00 2001 From: Tim Soslow Date: Mon, 27 Apr 2026 15:53:45 -0500 Subject: [PATCH 2/6] Support psr/log v2 Make context optional for LoggerInterface error mock callback --- composer.json | 2 +- tests/Integration/IntegTestCase.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 4d48fe3..cad883d 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "psr/http-message": "^1.0", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", - "psr/log": "^1.1 || ^3.0" + "psr/log": "^1.1 || ^2.0 || ^3.0" }, "autoload": { "psr-4": { diff --git a/tests/Integration/IntegTestCase.php b/tests/Integration/IntegTestCase.php index 719cdfc..8c84e74 100644 --- a/tests/Integration/IntegTestCase.php +++ b/tests/Integration/IntegTestCase.php @@ -102,7 +102,7 @@ protected function createHttpServer(ServerRequestInterface $request): HttpServer protected function failOnLoggedErrors(): void { - $this->logger->method('error')->willReturnCallback(function ($message, array $context) { + $this->logger->method('error')->willReturnCallback(function ($message, array $context = []) { $message = "Logged an error: {$message}\nContext:\n"; foreach ($context as $key => $value) { $message .= "- {$key}: {$value}\n"; From a283e7566c7945ae269320003e88bb9da19dc1a5 Mon Sep 17 00:00:00 2001 From: Tim Soslow Date: Mon, 27 Apr 2026 16:59:45 -0500 Subject: [PATCH 3/6] Handle level and message sent as object to log() --- src/StderrLogger.php | 12 +++++++ tests/Integration/StderrLoggerTest.php | 43 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/Integration/StderrLoggerTest.php diff --git a/src/StderrLogger.php b/src/StderrLogger.php index 77e31e7..4de6f25 100644 --- a/src/StderrLogger.php +++ b/src/StderrLogger.php @@ -48,6 +48,14 @@ public function __construct(string $minLevel = LogLevel::WARNING, $stream = 'php public function log($level, $message, array $context = []): void { + if (!is_string($level)) { + if (is_object($level) && method_exists($level, '__toString')) { + $level = (string) $level; + } else { + throw new InvalidArgumentException('Invalid log level: must be a string'); + } + } + if (!isset(self::LOG_LEVEL_MAP[$level])) { throw new InvalidArgumentException("Invalid log level: {$level}"); } @@ -67,6 +75,10 @@ public function log($level, $message, array $context = []): void $context['exception'] = explode("\n", (string) $exception); } + if (is_object($message) && method_exists($message, '__toString')) { + $message = (string) $message; + } + fwrite($this->stream, json_encode(compact('level', 'message', 'context')) . "\n"); } } diff --git a/tests/Integration/StderrLoggerTest.php b/tests/Integration/StderrLoggerTest.php new file mode 100644 index 0000000..8695b3d --- /dev/null +++ b/tests/Integration/StderrLoggerTest.php @@ -0,0 +1,43 @@ +log(LogLevel::ERROR, $message, ['foo' => 'bar']); + + rewind($stream); + $line = trim((string) stream_get_contents($stream)); + $payload = json_decode($line, true); + + $this->assertSame('stringable-message', $payload['message']); + $this->assertSame(LogLevel::ERROR, $payload['level']); + $this->assertSame(['foo' => 'bar'], $payload['context']); + } + + public function testNonStringLevelThrowsInvalidArgumentException(): void + { + $stream = fopen('php://temp', 'a+'); + $logger = new StderrLogger(LogLevel::DEBUG, $stream); + + $this->expectException(InvalidArgumentException::class); + $logger->log([], 'hello'); + } +} From 8eaabf186feeeb31903294563db3c5a54c632e39 Mon Sep 17 00:00:00 2001 From: Tim Soslow Date: Mon, 27 Apr 2026 17:33:10 -0500 Subject: [PATCH 4/6] Throw exception for non-stringable message --- src/StderrLogger.php | 8 ++++++-- tests/Integration/StderrLoggerTest.php | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/StderrLogger.php b/src/StderrLogger.php index 4de6f25..35683cc 100644 --- a/src/StderrLogger.php +++ b/src/StderrLogger.php @@ -75,8 +75,12 @@ public function log($level, $message, array $context = []): void $context['exception'] = explode("\n", (string) $exception); } - if (is_object($message) && method_exists($message, '__toString')) { - $message = (string) $message; + if (!is_string($message)) { + if (is_object($message) && method_exists($message, '__toString')) { + $message = (string) $message; + } else { + throw new InvalidArgumentException('Invalid log message: must be a string'); + } } fwrite($this->stream, json_encode(compact('level', 'message', 'context')) . "\n"); diff --git a/tests/Integration/StderrLoggerTest.php b/tests/Integration/StderrLoggerTest.php index 8695b3d..86724bb 100644 --- a/tests/Integration/StderrLoggerTest.php +++ b/tests/Integration/StderrLoggerTest.php @@ -40,4 +40,13 @@ public function testNonStringLevelThrowsInvalidArgumentException(): void $this->expectException(InvalidArgumentException::class); $logger->log([], 'hello'); } + + public function testNonStringMessageThrowsInvalidArgumentException(): void + { + $stream = fopen('php://temp', 'a+'); + $logger = new StderrLogger(LogLevel::DEBUG, $stream); + + $this->expectException(InvalidArgumentException::class); + $logger->log(LogLevel::ERROR, []); + } } From 577c2ed9274e287926c32bb013d15d416bd2e5be Mon Sep 17 00:00:00 2001 From: Tim Soslow Date: Mon, 27 Apr 2026 21:02:55 -0500 Subject: [PATCH 5/6] Add unit tests for psr/log v3 --- .github/workflows/continuous-integration.yml | 25 +++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 5f3e9d3..446c622 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -72,7 +72,7 @@ jobs: - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: - php-version: 7.4" + php-version: "7.4" ini-values: "memory_limit=-1" - name: "Install dependencies (Composer)" @@ -81,3 +81,26 @@ jobs: - name: "Run unit tests (PHPUnit)" shell: "bash" run: "composer test" + + unit-tests-psr-log-v3: + name: "Unit tests (psr/log v3)" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout repository" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.1" + ini-values: "memory_limit=-1" + + - name: "Install dependencies with psr/log v3" + shell: "bash" + run: | + composer config platform.php 8.1 + composer update --with psr/log:^3 --with-all-dependencies --no-interaction + + - name: "Run unit tests (PHPUnit)" + shell: "bash" + run: "composer test" From 36595aafc3446cb90e08322085044c4737050742 Mon Sep 17 00:00:00 2001 From: Tim Soslow Date: Mon, 27 Apr 2026 21:58:01 -0500 Subject: [PATCH 6/6] Clearer error message --- src/StderrLogger.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StderrLogger.php b/src/StderrLogger.php index 35683cc..430df56 100644 --- a/src/StderrLogger.php +++ b/src/StderrLogger.php @@ -52,7 +52,7 @@ public function log($level, $message, array $context = []): void if (is_object($level) && method_exists($level, '__toString')) { $level = (string) $level; } else { - throw new InvalidArgumentException('Invalid log level: must be a string'); + throw new InvalidArgumentException('Invalid log level: must be a string or stringable object'); } } @@ -79,7 +79,7 @@ public function log($level, $message, array $context = []): void if (is_object($message) && method_exists($message, '__toString')) { $message = (string) $message; } else { - throw new InvalidArgumentException('Invalid log message: must be a string'); + throw new InvalidArgumentException('Invalid log message: must be a string or stringable object'); } }