OutputInterface::VERBOSITY_NORMAL, LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL, LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL, LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL, LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL, LogLevel::NOTICE => OutputInterface::VERBOSITY_VERBOSE, LogLevel::INFO => OutputInterface::VERBOSITY_VERY_VERBOSE, LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG, ]; /** * Logger constructor. */ public function __construct(private readonly OutputInterface $output) {} /** * {@inheritdoc} */ public function log($level, string|\Stringable $message, array $context = []): void { if (!isset(self::VERBOSITY_LEVEL_MAP[$level])) { throw new InvalidArgumentException("The log level \"$level\" does not exist."); } $output = $this->output; // Write to the error output if necessary and available. $error_levels = [ LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR, ]; if (\in_array($level, $error_levels)) { if ($output instanceof ConsoleOutputInterface) { /** @var \Symfony\Component\Console\Output\ConsoleOutputInterface $output */ $output = $output->getErrorOutput(); } } // The if condition check isn't necessary -- it's the same one that $output // will do internally anyway. We only do it for efficiency here as the // message formatting is relatively expensive. if ($output->getVerbosity() >= self::VERBOSITY_LEVEL_MAP[$level]) { /** @psalm-suppress UnhandledMatchCondition */ $label = match ($level) { LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR => "$level", LogLevel::WARNING => "$level", LogLevel::NOTICE, LogLevel::INFO => "$level", LogLevel::DEBUG => "$level", }; $formatted_message = \sprintf( '[%s] %s', $label, $this->interpolate((string) $message, $context), ); $output->writeln($formatted_message, self::VERBOSITY_LEVEL_MAP[$level]); } } /** * Interpolates context values into the message placeholders. * * @see \Symfony\Component\Console\Logger::interpolate() */ private function interpolate(string $message, array $context): string { if (!\str_contains($message, '{')) { return $message; } $replacements = []; foreach ($context as $key => $value) { $replacements["{{$key}}"] = match(TRUE) { $value === NULL || \is_scalar($value) || (\is_object($value) && \method_exists($value, '__toString')) => $value, $value instanceof \DateTimeInterface => $value->format(\DateTimeInterface::RFC3339), \is_object($value) => '[object ' . $value::class . ']', default => '[' . \get_debug_type($value) . ']' }; } return \strtr($message, $replacements); } }