/src/Symfony/Component/HttpKernel/Log/Logger.php

https://github.com/hhamon/symfony · PHP · 118 lines · 81 code · 17 blank · 20 comment · 19 complexity · 7536167fc141f70e5be0d8d6ee946976 MD5 · raw file

  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpKernel\Log;
  11. use Psr\Log\AbstractLogger;
  12. use Psr\Log\InvalidArgumentException;
  13. use Psr\Log\LogLevel;
  14. /**
  15. * Minimalist PSR-3 logger designed to write in stderr or any other stream.
  16. *
  17. * @author Kévin Dunglas <dunglas@gmail.com>
  18. */
  19. class Logger extends AbstractLogger
  20. {
  21. private const LEVELS = [
  22. LogLevel::DEBUG => 0,
  23. LogLevel::INFO => 1,
  24. LogLevel::NOTICE => 2,
  25. LogLevel::WARNING => 3,
  26. LogLevel::ERROR => 4,
  27. LogLevel::CRITICAL => 5,
  28. LogLevel::ALERT => 6,
  29. LogLevel::EMERGENCY => 7,
  30. ];
  31. private int $minLevelIndex;
  32. private \Closure $formatter;
  33. /** @var resource|null */
  34. private $handle;
  35. /**
  36. * @param string|resource|null $output
  37. */
  38. public function __construct(string $minLevel = null, $output = null, callable $formatter = null)
  39. {
  40. if (null === $minLevel) {
  41. $minLevel = null === $output || 'php://stdout' === $output || 'php://stderr' === $output ? LogLevel::ERROR : LogLevel::WARNING;
  42. if (isset($_ENV['SHELL_VERBOSITY']) || isset($_SERVER['SHELL_VERBOSITY'])) {
  43. switch ((int) ($_ENV['SHELL_VERBOSITY'] ?? $_SERVER['SHELL_VERBOSITY'])) {
  44. case -1: $minLevel = LogLevel::ERROR; break;
  45. case 1: $minLevel = LogLevel::NOTICE; break;
  46. case 2: $minLevel = LogLevel::INFO; break;
  47. case 3: $minLevel = LogLevel::DEBUG; break;
  48. }
  49. }
  50. }
  51. if (!isset(self::LEVELS[$minLevel])) {
  52. throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $minLevel));
  53. }
  54. $this->minLevelIndex = self::LEVELS[$minLevel];
  55. $this->formatter = $formatter instanceof \Closure ? $formatter : \Closure::fromCallable($formatter ?? [$this, 'format']);
  56. if ($output && false === $this->handle = \is_resource($output) ? $output : @fopen($output, 'a')) {
  57. throw new InvalidArgumentException(sprintf('Unable to open "%s".', $output));
  58. }
  59. }
  60. /**
  61. * {@inheritdoc}
  62. */
  63. public function log($level, $message, array $context = []): void
  64. {
  65. if (!isset(self::LEVELS[$level])) {
  66. throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level));
  67. }
  68. if (self::LEVELS[$level] < $this->minLevelIndex) {
  69. return;
  70. }
  71. $formatter = $this->formatter;
  72. if ($this->handle) {
  73. @fwrite($this->handle, $formatter($level, $message, $context));
  74. } else {
  75. error_log($formatter($level, $message, $context, false));
  76. }
  77. }
  78. private function format(string $level, string $message, array $context, bool $prefixDate = true): string
  79. {
  80. if (str_contains($message, '{')) {
  81. $replacements = [];
  82. foreach ($context as $key => $val) {
  83. if (null === $val || is_scalar($val) || $val instanceof \Stringable) {
  84. $replacements["{{$key}}"] = $val;
  85. } elseif ($val instanceof \DateTimeInterface) {
  86. $replacements["{{$key}}"] = $val->format(\DateTime::RFC3339);
  87. } elseif (\is_object($val)) {
  88. $replacements["{{$key}}"] = '[object '.\get_class($val).']';
  89. } else {
  90. $replacements["{{$key}}"] = '['.\gettype($val).']';
  91. }
  92. }
  93. $message = strtr($message, $replacements);
  94. }
  95. $log = sprintf('[%s] %s', $level, $message).\PHP_EOL;
  96. if ($prefixDate) {
  97. $log = date(\DateTime::RFC3339).' '.$log;
  98. }
  99. return $log;
  100. }
  101. }