Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
23 / 23 |
|
91.67% |
22 / 24 |
|
61.11% |
11 / 18 |
|
83.33% |
5 / 6 |
CRAP | |
0.00% |
0 / 1 |
| Logger | |
100.00% |
23 / 23 |
|
91.67% |
22 / 24 |
|
61.11% |
11 / 18 |
|
100.00% |
6 / 6 |
20.47 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| formatter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| withFormatter | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| log | |
100.00% |
11 / 11 |
|
81.82% |
9 / 11 |
|
42.86% |
3 / 7 |
|
100.00% |
1 / 1 |
4.68 | |||
| validateLevel | |
100.00% |
3 / 3 |
|
100.00% |
5 / 5 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
3.14 | |||
| printLevel | |
100.00% |
3 / 3 |
|
100.00% |
5 / 5 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
4.12 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celemas\Log; |
| 6 | |
| 7 | use Celemas\Log\Formatter\TextFormatter; |
| 8 | use Override; |
| 9 | use Psr\Log\InvalidArgumentException; |
| 10 | use Psr\Log\LoggerInterface as PsrLogger; |
| 11 | use Psr\Log\LoggerTrait; |
| 12 | use Psr\Log\LogLevel; |
| 13 | use Stringable; |
| 14 | |
| 15 | /** @api */ |
| 16 | final class Logger implements PsrLogger |
| 17 | { |
| 18 | use LoggerTrait; |
| 19 | |
| 20 | public const string DEBUG = LogLevel::DEBUG; |
| 21 | public const string INFO = LogLevel::INFO; |
| 22 | public const string NOTICE = LogLevel::NOTICE; |
| 23 | public const string WARNING = LogLevel::WARNING; |
| 24 | public const string ERROR = LogLevel::ERROR; |
| 25 | public const string CRITICAL = LogLevel::CRITICAL; |
| 26 | public const string ALERT = LogLevel::ALERT; |
| 27 | public const string EMERGENCY = LogLevel::EMERGENCY; |
| 28 | |
| 29 | private const int ERROR_LOG_APPEND_TO_FILE = 3; |
| 30 | |
| 31 | /** @var array<string, positive-int> */ |
| 32 | private const array LEVEL_SEVERITY = [ |
| 33 | self::DEBUG => 100, |
| 34 | self::INFO => 200, |
| 35 | self::NOTICE => 300, |
| 36 | self::WARNING => 400, |
| 37 | self::ERROR => 500, |
| 38 | self::CRITICAL => 600, |
| 39 | self::ALERT => 700, |
| 40 | self::EMERGENCY => 800, |
| 41 | ]; |
| 42 | |
| 43 | /** @var array<string, non-empty-string> */ |
| 44 | private const array LEVEL_LABELS = [ |
| 45 | self::DEBUG => 'DEBUG', |
| 46 | self::INFO => 'INFO', |
| 47 | self::NOTICE => 'NOTICE', |
| 48 | self::WARNING => 'WARNING', |
| 49 | self::ERROR => 'ERROR', |
| 50 | self::CRITICAL => 'CRITICAL', |
| 51 | self::ALERT => 'ALERT', |
| 52 | self::EMERGENCY => 'EMERGENCY', |
| 53 | ]; |
| 54 | |
| 55 | protected Formatter $formatter; |
| 56 | |
| 57 | public function __construct( |
| 58 | protected ?string $file = null, |
| 59 | protected string $level = self::DEBUG, |
| 60 | ?Formatter $formatter = null, |
| 61 | ) { |
| 62 | $this->formatter = $formatter ?? new TextFormatter(); |
| 63 | $this->level = $this->validateLevel($level); |
| 64 | } |
| 65 | |
| 66 | public function formatter(Formatter $formatter): void |
| 67 | { |
| 68 | $this->formatter = $formatter; |
| 69 | } |
| 70 | |
| 71 | public function withFormatter(Formatter $formatter): self |
| 72 | { |
| 73 | $new = clone $this; |
| 74 | $new->formatter($formatter); |
| 75 | |
| 76 | return $new; |
| 77 | } |
| 78 | |
| 79 | #[Override] |
| 80 | public function log( |
| 81 | mixed $level, |
| 82 | string|Stringable $message, |
| 83 | array $context = [], |
| 84 | ): void { |
| 85 | $level = $this->validateLevel($level); |
| 86 | |
| 87 | if (self::LEVEL_SEVERITY[$level] < self::LEVEL_SEVERITY[$this->level]) { |
| 88 | return; |
| 89 | } |
| 90 | |
| 91 | $message = $this->formatter->format((string) $message, $context); |
| 92 | $message = str_replace("\0", '', $message); |
| 93 | $time = date(DATE_ATOM); |
| 94 | $line = "[{$time}] " . self::LEVEL_LABELS[$level] . ": {$message}"; |
| 95 | |
| 96 | if (is_string($this->file)) { |
| 97 | error_log($line . PHP_EOL, self::ERROR_LOG_APPEND_TO_FILE, $this->file); |
| 98 | |
| 99 | return; |
| 100 | } |
| 101 | |
| 102 | error_log(str_replace(["\r\n", "\r", "\n"], ' ', $line)); |
| 103 | } |
| 104 | |
| 105 | /** @return key-of<self::LEVEL_SEVERITY> */ |
| 106 | private function validateLevel(mixed $level): string |
| 107 | { |
| 108 | if (is_string($level) && array_key_exists($level, self::LEVEL_SEVERITY)) { |
| 109 | return $level; |
| 110 | } |
| 111 | |
| 112 | throw new InvalidArgumentException('Unknown log level: ' . $this->printLevel($level)); |
| 113 | } |
| 114 | |
| 115 | private function printLevel(mixed $level): string |
| 116 | { |
| 117 | if (is_scalar($level) || $level instanceof Stringable) { |
| 118 | return (string) $level; |
| 119 | } |
| 120 | |
| 121 | return get_debug_type($level); |
| 122 | } |
| 123 | } |