diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b543463..4ad62563 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Enh #132: Add benchmarks, improve performance of `Message::parse()` (@samdark) - Bug #130: Updated `Message::parse()` to correctly support multiple placeholders (@technicated) - Chg #130, #133: Changed `Message::parse()` to conform to PSR-3 (@technicated, @vjik) +- Enh #135: Add validation for `$traceLevel` in `SystemContextProvider` to ensure values are greater than or equal to zero (@rekmixa) - Enh #137: Explicitly import classes, functions, and constants in "use" section (@mspirkov) ## 2.2.0 December 13, 2025 diff --git a/infection.json.dist b/infection.json.dist index 3776e223..46279901 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -11,6 +11,9 @@ } }, "mutators": { - "@default": true + "@default": true, + "global-ignoreSourceCodeByRegex": [ + "register_shutdown_function" + ] } } diff --git a/src/ContextProvider/SystemContextProvider.php b/src/ContextProvider/SystemContextProvider.php index c9cf4fe8..972a5948 100644 --- a/src/ContextProvider/SystemContextProvider.php +++ b/src/ContextProvider/SystemContextProvider.php @@ -33,6 +33,7 @@ public function __construct( private int $traceLevel = 0, array $excludedTracePaths = [], ) { + $this->assertTraceLevelIsValid($this->traceLevel); /** @psalm-suppress DeprecatedMethod `setExcludedTracePaths` will be private and not deprecated */ $this->setExcludedTracePaths($excludedTracePaths); } @@ -61,7 +62,9 @@ public function getContext(): array */ public function setTraceLevel(int $traceLevel): self { + $this->assertTraceLevelIsValid($traceLevel); $this->traceLevel = $traceLevel; + return $this; } @@ -114,7 +117,7 @@ private function collectTrace(array $backtrace): array if (isset($trace['file'], $trace['line'])) { $excludedMatch = array_filter( $this->excludedTracePaths, - static fn($path) => str_contains($trace['file'], $path), + static fn(string $path): bool => str_contains($trace['file'], $path), ); if (empty($excludedMatch)) { @@ -129,4 +132,23 @@ private function collectTrace(array $backtrace): array return $traces; } + + /** + * Validates $traceLevel property + * + * @param int $traceLevel The number of call stack information. + * + * @see self::$traceLevel + */ + private function assertTraceLevelIsValid(int $traceLevel): void + { + if ($traceLevel < 0) { + throw new InvalidArgumentException( + sprintf( + 'Trace level must be greater than or equal to zero, %s received.', + $traceLevel, + ), + ); + } + } } diff --git a/src/Logger.php b/src/Logger.php index 01ecd576..e3decbe6 100644 --- a/src/Logger.php +++ b/src/Logger.php @@ -11,8 +11,8 @@ use RuntimeException; use Stringable; use Throwable; -use Yiisoft\Log\ContextProvider\SystemContextProvider; use Yiisoft\Log\ContextProvider\ContextProviderInterface; +use Yiisoft\Log\ContextProvider\SystemContextProvider; use function count; use function implode; @@ -105,24 +105,13 @@ public function __construct( * @throws \Psr\Log\InvalidArgumentException for invalid log message level. * * @return string The text display of the level. - * @deprecated since 2.1, to be removed in 3.0. Use {@see LogLevel::assertLevelIsValid()} instead. + * @deprecated since 2.1, to be removed in 3.0. Use {@see Logger::assertLevelIsValid()} instead. + * @psalm-suppress MixedInferredReturnType + * @psalm-suppress MixedReturnStatement */ public static function validateLevel(mixed $level): string { - if (!is_string($level)) { - throw new \Psr\Log\InvalidArgumentException(sprintf( - 'The log message level must be a string, %s provided.', - get_debug_type($level), - )); - } - - if (!in_array($level, self::LEVELS, true)) { - throw new \Psr\Log\InvalidArgumentException(sprintf( - 'Invalid log message level "%s" provided. The following values are supported: "%s".', - $level, - implode('", "', self::LEVELS), - )); - } + self::assertLevelIsValid($level); return $level; } diff --git a/tests/ContextProvider/SystemContextProviderTest.php b/tests/ContextProvider/SystemContextProviderTest.php new file mode 100644 index 00000000..2c0abaca --- /dev/null +++ b/tests/ContextProvider/SystemContextProviderTest.php @@ -0,0 +1,29 @@ +expectException(InvalidArgumentException::class); + new SystemContextProvider(-1); + } + + public function testContextHasNeededData(): void + { + $provider = new SystemContextProvider(); + $context = $provider->getContext(); + + $this->assertArrayHasKey('time', $context); + $this->assertArrayHasKey('trace', $context); + $this->assertArrayHasKey('memory', $context); + $this->assertArrayHasKey('category', $context); + } +} diff --git a/tests/LoggerTest.php b/tests/LoggerTest.php index 63b96154..297c1164 100644 --- a/tests/LoggerTest.php +++ b/tests/LoggerTest.php @@ -56,11 +56,24 @@ public function testLog(): void $this->assertGreaterThanOrEqual($memory, $messages[1]->context('memory')); } + public function testLogWithWrongLevel(): void + { + $this->expectException(\Psr\Log\InvalidArgumentException::class); + $this->logger->log(123, 'test1'); + } + + public function testLogWithUnsupportedLevel(): void + { + $this->expectException(\Psr\Log\InvalidArgumentException::class); + $this->logger->log('unsupported-level', 'test1'); + } + public function testLogWithTraceLevel(): void { $memory = memory_get_usage(); $this->logger->setTraceLevel($traceLevel = 3); + $line = __LINE__; $this->logger->log(LogLevel::INFO, 'test3'); $messages = $this->getInaccessibleMessages($this->logger); @@ -70,7 +83,7 @@ public function testLogWithTraceLevel(): void $this->assertSame('application', $messages[0]->context('category')); $this->assertSame([ 'file' => __FILE__, - 'line' => 64, + 'line' => $line + 1, 'function' => 'log', 'class' => Logger::class, 'type' => '->', @@ -145,6 +158,7 @@ public function testSetExcludedTracePaths(): void $this->logger->info('info message'); $messages = $this->getInaccessibleMessages($this->logger); + $this->assertNotEmpty($messages[1]->context('trace')); foreach ($messages[1]->context('trace') as $trace) { $this->assertNotSame(__FILE__, $trace['file']); }