From e09e51e2e864bc166eb8057f51ce37764a9337d1 Mon Sep 17 00:00:00 2001 From: Frugan Date: Mon, 22 Sep 2025 22:23:40 +0200 Subject: [PATCH 1/2] ci: fix conditions in main workflow --- .github/workflows/main.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a0be9e0..24ba40f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -79,25 +79,27 @@ jobs: run: composer test:coverage - name: Upload coverage to Codecov - if: matrix.php == '8.4' && secrets.CODECOV_TOKEN != '' + if: matrix.php == '8.4' + continue-on-error: true uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml - fail_ci_if_error: true + fail_ci_if_error: false # Alternative - name: Upload coverage to Qlty - if: matrix.php == '8.4' && secrets.QLTY_TOKEN != '' + if: matrix.php == '8.4' + continue-on-error: true uses: qltysh/qlty-action/coverage@v2 with: token: ${{ secrets.QLTY_TOKEN }} files: ./coverage.xml - continue-on-error: true # Alternative #- name: Upload coverage to Scrutinizer - # if: matrix.php == '8.4' && secrets.SCRUTINIZER_ACCESS_TOKEN != '' + # if: matrix.php == '8.4' + # continue-on-error: true # uses: scrutinizer-ci/ocular@v1 # with: # access-token: ${{ secrets.SCRUTINIZER_ACCESS_TOKEN }} @@ -163,7 +165,6 @@ jobs: # - Enables security-focused code review workflow # - Creates security alerts for repository maintainers - name: Run Snyk to check for vulnerabilities (PHP) - if: secrets.SNYK_TOKEN != '' continue-on-error: true uses: snyk/actions/php@e2221410bff24446ba09102212d8bc75a567237d env: @@ -172,7 +173,7 @@ jobs: args: --severity-threshold=high --sarif-file-output=snyk.sarif --file=composer.lock - name: Upload Snyk results to GitHub Code Scanning - if: hashFiles('snyk.sarif') != '' + continue-on-error: true uses: github/codeql-action/upload-sarif@v3 with: sarif_file: snyk.sarif From 2c966a073f7a614b89e57ff34eeb7d518d68f2d2 Mon Sep 17 00:00:00 2001 From: Frugan Date: Mon, 22 Sep 2025 22:27:03 +0200 Subject: [PATCH 2/2] feat: add optional psr_log_namespace param --- README.md | 27 +++++-- composer.json | 1 + src/Logger.php | 169 +++++++++++++++++++++++++++++++++++-------- tests/HooksTest.php | 127 ++++++++++++++++++++++++++++++++ tests/LoggerTest.php | 120 ++++++++++++++++++++++++++++++ 5 files changed, 407 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 7fb4be8..778baad 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,11 @@ $config = [ 'min_level' => 'info', // Optional: Wonolog namespace (default: 'Inpsyde\Wonolog') - 'wonolog_namespace' => 'MyApp\Wonolog', + 'wonolog_namespace' => 'MyPlugin\Vendor\Wonolog', + + // Optional: PSR Log namespace (default: 'Psr\Log') + // Useful for Mozart-isolated dependencies + 'psr_log_namespace' => 'MyPlugin\Vendor\Psr\Log', // Optional: WordPress constant to disable logging // (default: auto-generated from component_name) @@ -201,16 +205,18 @@ $config = [ ```bash # Global logger settings -LOGGER_COMPONENT_NAME=my-plugin # Plugin/Theme identifier -LOGGER_RETENTION_DAYS=30 # Log retention period -LOGGER_MIN_LEVEL=info # Minimum log level -LOGGER_DISABLED=false # Disable all logging -LOGGER_WONOLOG_NAMESPACE=Inpsyde\Wonolog +LOGGER_COMPONENT_NAME=my-plugin # Plugin/Theme identifier +LOGGER_RETENTION_DAYS=30 # Log retention period +LOGGER_MIN_LEVEL=info # Minimum log level +LOGGER_DISABLED=false # Disable all logging +LOGGER_WONOLOG_NAMESPACE=Inpsyde\Wonolog # Wonolog namespace +LOGGER_PSR_LOG_NAMESPACE=Psr\Log # PSR Log namespace # Plugin-specific settings (higher priority) MY_PLUGIN_LOGGER_RETENTION_DAYS=60 -MY_PLUGIN_LOGGER_DISABLED=false MY_PLUGIN_LOGGER_MIN_LEVEL=warning +MY_PLUGIN_LOGGER_DISABLED=false +MY_PLUGIN_LOGGER_PSR_LOG_NAMESPACE=MyPlugin\Vendor\Psr\Log ``` ### WordPress Constants (wp-config.php) @@ -218,7 +224,7 @@ MY_PLUGIN_LOGGER_MIN_LEVEL=warning ```php // Control logging behavior per plugin define('MY_PLUGIN_LOGGER_DISABLED', true); // Disable all logging -define('MY_PLUGIN_LOGGER_RETENTION_DAYS', 90); // Keep logs for 90 days +define('MY_PLUGIN_LOGGER_RETENTION_DAYS', 90); // Keep logs for 90 days // Global WordPress debug (affects fallback behavior) define('WP_DEBUG', true); // Forces error_log() usage in fallback mode @@ -530,6 +536,11 @@ add_filter('wp_logger_wonolog_namespace', function($namespace) { return 'MyApp\Logger'; }); +// Change PSR Log namespace +add_filter('wp_logger_psr_log_namespace', function($namespace) { + return 'MyPlugin\Vendor\Psr\Log'; +}); + // Modify Wonolog prefix add_filter('wp_logger_wonolog_prefix', function($prefix, $level, $message, $context, $config) { return 'myapp.log'; diff --git a/composer.json b/composer.json index 58b701a..9b49b95 100644 --- a/composer.json +++ b/composer.json @@ -74,6 +74,7 @@ "ci": "@check --no-interaction", "lint": "@check --tasks=phpcsfixer,phplint,phpstan,rector", "quality": "@check --tasks=phpmnd,phpparser", + "rector": "@check --tasks=rector", "security": "@check --tasks=securitychecker_roave", "test": "@check --tasks=phpunit", "test:coverage": "vendor/bin/phpunit --coverage-clover=coverage.xml" diff --git a/src/Logger.php b/src/Logger.php index 49b5d88..35435ae 100644 --- a/src/Logger.php +++ b/src/Logger.php @@ -33,6 +33,7 @@ * - Environment-aware log levels and retention * - Configurable log retention and cleanup * - Hook-based customization system + * - Support for custom PSR Log namespace (Mozart compatibility) */ class Logger implements LoggerInterface { @@ -60,27 +61,12 @@ class Logger implements LoggerInterface 'component_name' => null, // Required - can come from environment 'retention_days' => self::DEFAULT_RETENTION_DAYS, 'wonolog_namespace' => 'Inpsyde\Wonolog', + 'psr_log_namespace' => 'Psr\Log', 'min_level' => LogLevel::DEBUG, 'disabled_constant' => null, // Will be auto-generated if not provided 'retention_days_constant' => null, // Will be auto-generated if not provided ]; - /** - * Log level priority mapping for minimum level filtering. - * - * @var array - */ - private const LOG_LEVEL_PRIORITIES = [ - LogLevel::DEBUG => 0, - LogLevel::INFO => 1, - LogLevel::NOTICE => 2, - LogLevel::WARNING => 3, - LogLevel::ERROR => 4, - LogLevel::CRITICAL => 5, - LogLevel::ALERT => 6, - LogLevel::EMERGENCY => 7, - ]; - /** * Configuration array. * @@ -103,11 +89,23 @@ class Logger implements LoggerInterface */ private ?string $wonologNamespace = null; + /** + * Cache for PSR Log namespace. + */ + private ?string $psrLogNamespace = null; + /** * Cache for log directory path. */ private ?string $logDirectoryCache = null; + /** + * Cache for PSR Log level priority mapping with custom namespace. + * + * @var null|array + */ + private ?array $psrLogLevels = null; + /** * Initialize logger with configuration and environment integration. * @@ -115,6 +113,7 @@ class Logger implements LoggerInterface * - component_name: string - Unique identifier (can come from LOGGER_COMPONENT_NAME env var) * - retention_days: int (default: 30) - Days to keep log files * - wonolog_namespace: string (default: 'Inpsyde\Wonolog') - Wonolog namespace + * - psr_log_namespace: string (default: 'Psr\Log') - PSR Log namespace * - min_level: string (default: 'debug') - Minimum log level to record * - disabled_constant: string - WordPress constant name to disable logging * - retention_days_constant: string - WordPress constant name for retention days @@ -214,7 +213,7 @@ public function log($level, $message, array $context = []): void */ public function emergency($message, array $context = []): void { - $this->log(LogLevel::EMERGENCY, $message, $context); + $this->log($this->getPsrLogLevel('EMERGENCY'), $message, $context); } /** @@ -224,7 +223,7 @@ public function emergency($message, array $context = []): void */ public function alert($message, array $context = []): void { - $this->log(LogLevel::ALERT, $message, $context); + $this->log($this->getPsrLogLevel('ALERT'), $message, $context); } /** @@ -234,7 +233,7 @@ public function alert($message, array $context = []): void */ public function critical($message, array $context = []): void { - $this->log(LogLevel::CRITICAL, $message, $context); + $this->log($this->getPsrLogLevel('CRITICAL'), $message, $context); } /** @@ -245,7 +244,7 @@ public function critical($message, array $context = []): void */ public function error($message, array $context = []): void { - $this->log(LogLevel::ERROR, $message, $context); + $this->log($this->getPsrLogLevel('ERROR'), $message, $context); } /** @@ -255,7 +254,7 @@ public function error($message, array $context = []): void */ public function warning($message, array $context = []): void { - $this->log(LogLevel::WARNING, $message, $context); + $this->log($this->getPsrLogLevel('WARNING'), $message, $context); } /** @@ -265,7 +264,7 @@ public function warning($message, array $context = []): void */ public function notice($message, array $context = []): void { - $this->log(LogLevel::NOTICE, $message, $context); + $this->log($this->getPsrLogLevel('NOTICE'), $message, $context); } /** @@ -275,7 +274,7 @@ public function notice($message, array $context = []): void */ public function info($message, array $context = []): void { - $this->log(LogLevel::INFO, $message, $context); + $this->log($this->getPsrLogLevel('INFO'), $message, $context); } /** @@ -285,7 +284,7 @@ public function info($message, array $context = []): void */ public function debug($message, array $context = []): void { - $this->log(LogLevel::DEBUG, $message, $context); + $this->log($this->getPsrLogLevel('DEBUG'), $message, $context); } /** @@ -316,6 +315,15 @@ public function refreshWonologCache(): void $this->wonologNamespace = null; } + /** + * Force refresh the PSR Log namespace cache. + */ + public function refreshPsrLogCache(): void + { + $this->psrLogNamespace = null; + $this->psrLogLevels = null; + } + /** * Get comprehensive debug information including environment details. * @@ -348,6 +356,7 @@ public function getDebugInfo(): array // Logging state 'wonolog_active' => $this->isWonologActive(), 'wonolog_namespace' => $this->getWonologNamespace(), + 'psr_log_namespace' => $this->getPsrLogNamespace(), 'logging_disabled' => $this->isLoggingDisabled(), 'log_directory' => $this->getLogDirectory(), @@ -420,6 +429,11 @@ private function applyEnvironmentOverrides(array &$config): void $config['wonolog_namespace'] = $globalWonolog; } + $globalPsrLog = Environment::get('LOGGER_PSR_LOG_NAMESPACE'); + if ($globalPsrLog) { + $config['psr_log_namespace'] = $globalPsrLog; + } + // Plugin-specific environment variables (highest priority) $componentRetention = Environment::getInt($this->buildConstantName('RETENTION_DAYS')); if ($componentRetention > 0) { @@ -430,6 +444,11 @@ private function applyEnvironmentOverrides(array &$config): void if ($componentMinLevel) { $config['min_level'] = $componentMinLevel; } + + $componentPsrLog = Environment::get($this->buildConstantName('PSR_LOG_NAMESPACE')); + if ($componentPsrLog) { + $config['psr_log_namespace'] = $componentPsrLog; + } } /** @@ -453,8 +472,10 @@ private function generateMissingConstants(array &$config): void */ private function shouldLog(string $level): bool { - $minPriority = self::LOG_LEVEL_PRIORITIES[$this->config['min_level']] ?? 0; - $currentPriority = self::LOG_LEVEL_PRIORITIES[$level] ?? 0; + // Get the log levels with current PSR Log namespace + $logLevels = $this->getPsrLogLevels(); + $minPriority = $logLevels[$this->config['min_level']] ?? 0; + $currentPriority = $logLevels[$level] ?? 0; return $currentPriority >= $minPriority; } @@ -500,6 +521,93 @@ private function getWonologNamespace(): string return $this->wonologNamespace ?? $this->config['wonolog_namespace']; } + /** + * Get PSR Log namespace with hook support. + */ + private function getPsrLogNamespace(): string + { + if (null === $this->psrLogNamespace) { + if (\function_exists('apply_filters')) { + $result = apply_filters('wp_logger_psr_log_namespace', $this->config['psr_log_namespace']); + $this->psrLogNamespace = \is_string($result) && '' !== $result ? $result : $this->config['psr_log_namespace']; + } else { + $this->psrLogNamespace = $this->config['psr_log_namespace']; + } + } + + // Ensure we always return a string + return $this->psrLogNamespace ?? $this->config['psr_log_namespace']; + } + + /** + * Get PSR Log level constant from custom namespace. + */ + private function getPsrLogLevel(string $level): string + { + $psrNamespace = $this->getPsrLogNamespace(); + + // For default namespace, use static constants for better performance + if ('Psr\Log' === $psrNamespace) { + return match ($level) { + 'EMERGENCY' => LogLevel::EMERGENCY, + 'ALERT' => LogLevel::ALERT, + 'CRITICAL' => LogLevel::CRITICAL, + 'ERROR' => LogLevel::ERROR, + 'WARNING' => LogLevel::WARNING, + 'NOTICE' => LogLevel::NOTICE, + 'INFO' => LogLevel::INFO, + 'DEBUG' => LogLevel::DEBUG, + default => LogLevel::DEBUG, + }; + } + + // For custom namespace, construct dynamically + $logLevelClass = $psrNamespace.'\LogLevel'; + if (class_exists($logLevelClass) && \defined($logLevelClass.'::'.$level)) { + return \constant($logLevelClass.'::'.$level); + } + + // Fallback to standard values if custom namespace doesn't work + return match ($level) { + 'EMERGENCY' => 'emergency', + 'ALERT' => 'alert', + 'CRITICAL' => 'critical', + 'ERROR' => 'error', + 'WARNING' => 'warning', + 'NOTICE' => 'notice', + 'INFO' => 'info', + 'DEBUG' => 'debug', + default => 'debug', + }; + } + + /** + * Get PSR Log level priority mapping with custom namespace support. + * + * @return array + */ + private function getPsrLogLevels(): array + { + if (null !== $this->psrLogLevels) { + return $this->psrLogLevels; + } + + // Use standard level names as keys, priorities as values + // This works regardless of PSR Log namespace + $this->psrLogLevels = [ + 'debug' => 0, + 'info' => 1, + 'notice' => 2, + 'warning' => 3, + 'error' => 4, + 'critical' => 5, + 'alert' => 6, + 'emergency' => 7, + ]; + + return $this->psrLogLevels; + } + /** * Log via Wonolog with native mixed type support. * @@ -934,12 +1042,14 @@ private function generateReadme(string $componentFolder): string $config .= "# Global logger settings:\n"; $config .= "LOGGER_DISABLED=false\n"; $config .= \sprintf('LOGGER_RETENTION_DAYS=%d%s', $retentionDays, PHP_EOL); - $config .= "LOGGER_MIN_LEVEL=info\n\n"; + $config .= "LOGGER_MIN_LEVEL=info\n"; + $config .= "LOGGER_PSR_LOG_NAMESPACE=Psr\\Log\n\n"; $config .= "# Plugin/Theme-specific settings (higher priority):\n"; $config .= $this->buildConstantName('DISABLED')."=false\n"; $config .= $this->buildConstantName('RETENTION_DAYS').\sprintf('=%d%s', $retentionDays, PHP_EOL); - $config .= $this->buildConstantName('MIN_LEVEL')."=warning\n\n"; + $config .= $this->buildConstantName('MIN_LEVEL')."=warning\n"; + $config .= $this->buildConstantName('PSR_LOG_NAMESPACE')."=MyPlugin\\Vendor\\Psr\\Log\n\n"; $config .= "# WordPress Constants (wp-config.php):\n"; $config .= "# Enable debug logging (uses error_log):\n"; @@ -954,8 +1064,9 @@ private function generateReadme(string $componentFolder): string $config .= 'Plugin/Theme: '.$this->config['component_name']."\n"; $config .= 'Environment: '.Environment::getEnvironment()."\n"; $config .= "Log retention: {$retentionDays} days\n"; + $config .= 'Min log level: '.$this->config['min_level']."\n"; - return $config.('Min log level: '.$this->config['min_level']."\n"); + return $config.('PSR Log namespace: '.$this->getPsrLogNamespace()."\n"); } /** diff --git a/tests/HooksTest.php b/tests/HooksTest.php index e46393b..7ca3388 100644 --- a/tests/HooksTest.php +++ b/tests/HooksTest.php @@ -504,4 +504,131 @@ public function testMinimumLogLevelFilteringInHooks(): void self::assertNotContains('info', $loggedLevels); self::assertNotContains('warning', $loggedLevels); } + + public function testPsrLogNamespaceHookWithEnvironment(): void + { + // Set environment PSR Log namespace + set_mock_env_var('LOGGER_PSR_LOG_NAMESPACE', 'CustomApp\Psr\Log'); + + $logger = new Logger(['component_name' => 'test-plugin']); + + // This will trigger the hook internally when debug info is requested + $logger->getDebugInfo(); + + $appliedFilters = get_applied_filters(); + + // Check if the hook was called with environment value + $namespaceFilterCalled = false; + foreach ($appliedFilters as $appliedFilter) { + if ('wp_logger_psr_log_namespace' === $appliedFilter['hook']) { + $namespaceFilterCalled = true; + self::assertSame('CustomApp\Psr\Log', $appliedFilter['value']); + + break; + } + } + + self::assertTrue($namespaceFilterCalled, 'wp_logger_psr_log_namespace filter should be called'); + } + + public function testCustomPsrLogNamespaceFromEnvironment(): void + { + // Set custom namespace via environment + set_mock_env_var('LOGGER_PSR_LOG_NAMESPACE', 'MyCustomApp\PsrLog'); + + $logger = new Logger(['component_name' => 'test-plugin']); + + $debugInfo = $logger->getDebugInfo(); + + $appliedFilters = get_applied_filters(); + + // Check that the environment namespace was passed through the filter + $namespaceFilter = null; + foreach ($appliedFilters as $appliedFilter) { + if ('wp_logger_psr_log_namespace' === $appliedFilter['hook']) { + $namespaceFilter = $appliedFilter; + + break; + } + } + + self::assertNotNull($namespaceFilter); + self::assertSame('MyCustomApp\PsrLog', $namespaceFilter['value']); + self::assertSame('MyCustomApp\PsrLog', $debugInfo['psr_log_namespace']); + } + + public function testHookParameterConsistencyWithEnvironmentAndPsrLog(): void + { + // Set environment context including PSR Log namespace + set_mock_env_var('WP_ENVIRONMENT_TYPE', 'production'); + set_mock_env_var('LOGGER_WONOLOG_NAMESPACE', 'Production\Logger'); + set_mock_env_var('LOGGER_PSR_LOG_NAMESPACE', 'Production\Psr\Log'); + + $logger = new Logger(['component_name' => 'test-plugin']); + $context = ['user' => 'admin', 'ip' => '192.168.1.1']; + + $logger->alert('Test alert message', $context); + + $appliedFilters = get_applied_filters(); + + // Check that override hook receives correct parameters with environment config + $overrideFilter = null; + foreach ($appliedFilters as $appliedFilter) { + if ('wp_logger_override_log' === $appliedFilter['hook']) { + $overrideFilter = $appliedFilter; + + break; + } + } + + self::assertNotNull($overrideFilter); + + // Check config contains environment-based values including PSR Log namespace + $config = $overrideFilter['args'][3]; + self::assertIsArray($config); + self::assertArrayHasKey('wonolog_namespace', $config); + self::assertArrayHasKey('psr_log_namespace', $config); + self::assertSame('Production\Logger', $config['wonolog_namespace']); + self::assertSame('Production\Psr\Log', $config['psr_log_namespace']); + } + + public function testPluginSpecificPsrLogEnvironmentHooks(): void + { + // Set component-specific PSR Log environment variables + set_mock_env_var('MY_SPECIAL_PLUGIN_LOGGER_PSR_LOG_NAMESPACE', 'MySpecialPlugin\Psr\Log'); + + $logger = new Logger(['component_name' => 'my-special-plugin']); + + // Trigger logging to test environment-based configuration + $logger->warning('Plugin-specific PSR Log configuration test'); + + $debugInfo = $logger->getDebugInfo(); + + // Should use component-specific PSR Log namespace from environment + self::assertSame('MySpecialPlugin\Psr\Log', $debugInfo['psr_log_namespace']); + } + + public function testExpectedPsrLogHooks(): void + { + $expectedHooks = [ + 'wp_logger_wonolog_namespace', + 'wp_logger_psr_log_namespace', // New hook + 'wp_logger_wonolog_prefix', + 'wp_logger_wonolog_action', + 'wp_logger_override_log', + 'wp_logger_logged', + 'wp_logger_fallback', + ]; + + foreach ($expectedHooks as $expectedHook) { + // Check that hook names are properly prefixed + self::assertStringStartsWith('wp_logger_', $expectedHook); + + // Check that hook names use underscores (WordPress convention) + self::assertMatchesRegularExpression('/^[a-z_]+$/', $expectedHook); + + // Check that hook names are not too long (WordPress recommendation) + self::assertLessThanOrEqual(50, \strlen($expectedHook)); + } + } } diff --git a/tests/LoggerTest.php b/tests/LoggerTest.php index 306a425..9e3fcc2 100644 --- a/tests/LoggerTest.php +++ b/tests/LoggerTest.php @@ -493,6 +493,126 @@ public function testFallbackLoggingBehaviorInDifferentEnvironments(): void self::assertFalse($debugInfo['is_debug']); } + public function testPsrLogNamespaceConfiguration(): void + { + // Test default PSR Log namespace + $logger = new Logger(['component_name' => 'test-plugin']); + + $debugInfo = $logger->getDebugInfo(); + self::assertSame('Psr\Log', $debugInfo['psr_log_namespace']); + } + + public function testCustomPsrLogNamespaceFromConfig(): void + { + $logger = new Logger([ + 'component_name' => 'test-plugin', + 'psr_log_namespace' => 'MyPlugin\Vendor\Psr\Log', + ]); + + $debugInfo = $logger->getDebugInfo(); + self::assertSame('MyPlugin\Vendor\Psr\Log', $debugInfo['psr_log_namespace']); + } + + public function testPsrLogNamespaceFromEnvironment(): void + { + // Set global environment variable + set_mock_env_var('LOGGER_PSR_LOG_NAMESPACE', 'Custom\Psr\Log'); + + $logger = new Logger(['component_name' => 'test-plugin']); + + $debugInfo = $logger->getDebugInfo(); + self::assertSame('Custom\Psr\Log', $debugInfo['psr_log_namespace']); + } + + public function testPluginSpecificPsrLogNamespace(): void + { + // Set component-specific environment variable (should override global one) + set_mock_env_var('LOGGER_PSR_LOG_NAMESPACE', 'Global\Psr\Log'); // Global + set_mock_env_var('MY_PLUGIN_LOGGER_PSR_LOG_NAMESPACE', 'MyPlugin\Vendor\Psr\Log'); // Plugin-specific + + $logger = new Logger(['component_name' => 'my-plugin']); + + $debugInfo = $logger->getDebugInfo(); + self::assertSame('MyPlugin\Vendor\Psr\Log', $debugInfo['psr_log_namespace']); + } + + public function testPsrLogNamespaceConfigurationPriority(): void + { + // Test configuration priority: Plugin-specific env > Global env > Config array > Defaults + set_mock_env_var('LOGGER_PSR_LOG_NAMESPACE', 'Global\Psr\Log'); // Environment (higher priority) + + $logger = new Logger([ + 'component_name' => 'test-plugin', + 'psr_log_namespace' => 'Config\Psr\Log', // Config array (lower priority) + ]); + + $debugInfo = $logger->getDebugInfo(); + self::assertSame('Global\Psr\Log', $debugInfo['psr_log_namespace']); + } + + public function testPsrLogCacheRefresh(): void + { + $logger = new Logger([ + 'component_name' => 'test-plugin', + 'psr_log_namespace' => 'Initial\Psr\Log', + ]); + + // Initial namespace + $debugInfo = $logger->getDebugInfo(); + self::assertSame('Initial\Psr\Log', $debugInfo['psr_log_namespace']); + + // Refresh cache (simulates runtime namespace change) + $logger->refreshPsrLogCache(); + + // Should still return the same value since config hasn't changed + $debugInfo = $logger->getDebugInfo(); + self::assertSame('Initial\Psr\Log', $debugInfo['psr_log_namespace']); + } + + public function testPsrLogLevelFilteringWithCustomNamespace(): void + { + // Test that minimum level filtering works with custom PSR Log namespace + $logger = new Logger([ + 'component_name' => 'test-plugin', + 'psr_log_namespace' => 'Custom\Psr\Log', + 'min_level' => 'warning', + ]); + + $logger->debug('Debug message'); // Should be filtered out + $logger->info('Info message'); // Should be filtered out + $logger->warning('Warning message'); // Should be logged + $logger->error('Error message'); // Should be logged + + $actions = get_triggered_actions(); + $loggedActions = array_filter($actions, static fn (array $action): bool => 'wp_logger_logged' === $action['hook']); + + // Should only have 2 logged actions (warning and error) + self::assertCount(2, $loggedActions); + } + + public function testProtectionFilesIncludePsrLogNamespace(): void + { + // Set custom PSR Log namespace + set_mock_env_var('WP_ENVIRONMENT_TYPE', 'production'); + set_mock_env_var('WP_DEBUG', 'false'); + + $logger = new Logger([ + 'component_name' => 'test-plugin', + 'psr_log_namespace' => 'MyPlugin\Vendor\Psr\Log', + ]); + + $logger->info('Create protection files'); + + $logDir = $this->testLogDir.'/test-plugin/logs'; + + // Check README includes PSR Log namespace information + $readmeContent = file_get_contents($logDir.'/README'); + self::assertIsString($readmeContent); + self::assertStringContainsString('PSR Log namespace: MyPlugin\Vendor\Psr\Log', $readmeContent); + self::assertStringContainsString('LOGGER_PSR_LOG_NAMESPACE', $readmeContent); + self::assertStringContainsString('TEST_PLUGIN_LOGGER_PSR_LOG_NAMESPACE', $readmeContent); + } + /** * Recursively remove directory. */