Code Coverage  | 
      ||||||||||
Lines  | 
       Functions and Methods  | 
       Classes and Traits  | 
      ||||||||
| Total |         | 
       79.37%  | 
       50 / 63  | 
               | 
       61.54%  | 
       8 / 13  | 
       CRAP |         | 
       0.00%  | 
       0 / 1  | 
      
| SimpleLogger |         | 
       79.37%  | 
       50 / 63  | 
               | 
       61.54%  | 
       8 / 13  | 
       42.57 |         | 
       0.00%  | 
       0 / 1  | 
      
| __construct |         | 
       71.43%  | 
       5 / 7  | 
               | 
       0.00%  | 
       0 / 1  | 
       3.21 | |||
| log |         | 
       100.00%  | 
       7 / 7  | 
               | 
       100.00%  | 
       1 / 1  | 
       2 | |||
| validateLevel |         | 
       100.00%  | 
       3 / 3  | 
               | 
       100.00%  | 
       1 / 1  | 
       2 | |||
| interpolate |         | 
       100.00%  | 
       13 / 13  | 
               | 
       100.00%  | 
       1 / 1  | 
       9 | |||
| isException |         | 
       40.00%  | 
       2 / 5  | 
               | 
       0.00%  | 
       0 / 1  | 
       4.94 | |||
| formatObject |         | 
       100.00%  | 
       4 / 4  | 
               | 
       100.00%  | 
       1 / 1  | 
       3 | |||
| formatException |         | 
       0.00%  | 
       0 / 6  | 
               | 
       0.00%  | 
       0 / 1  | 
       2 | |||
| createLine |         | 
       100.00%  | 
       7 / 7  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| writeLine |         | 
       100.00%  | 
       5 / 5  | 
               | 
       100.00%  | 
       1 / 1  | 
       4 | |||
| writeToErrorLog |         | 
       0.00%  | 
       0 / 1  | 
               | 
       0.00%  | 
       0 / 1  | 
       2 | |||
| writeLineToFile |         | 
       0.00%  | 
       0 / 1  | 
               | 
       0.00%  | 
       0 / 1  | 
       2 | |||
| logStore |         | 
       100.00%  | 
       3 / 3  | 
               | 
       100.00%  | 
       1 / 1  | 
       2 | |||
| setLogFormat |         | 
       100.00%  | 
       1 / 1  | 
               | 
       100.00%  | 
       1 / 1  | 
       1 | |||
| 1 | <?php | 
| 2 | |
| 3 | declare(strict_types=1); | 
| 4 | |
| 5 | namespace Projom\Storage\Logger; | 
| 6 | |
| 7 | use Stringable; | 
| 8 | |
| 9 | use Psr\Log\AbstractLogger; | 
| 10 | use Psr\Log\InvalidArgumentException; | 
| 11 | use Psr\Log\LogLevel; | 
| 12 | |
| 13 | class SimpleLogger extends AbstractLogger | 
| 14 | { | 
| 15 | const RFC5424_LEVELS = [ | 
| 16 | LogLevel::EMERGENCY => 0, | 
| 17 | LogLevel::ALERT => 1, | 
| 18 | LogLevel::CRITICAL => 2, | 
| 19 | LogLevel::ERROR => 3, | 
| 20 | LogLevel::WARNING => 4, | 
| 21 | LogLevel::NOTICE => 5, | 
| 22 | LogLevel::INFO => 6, | 
| 23 | LogLevel::DEBUG => 7, | 
| 24 | ]; | 
| 25 | |
| 26 | private readonly string $absoluteFilePath; | 
| 27 | private readonly string $logLevel; | 
| 28 | private readonly LoggerType $type; | 
| 29 | private array $logStore = []; | 
| 30 | private string $logFormat = '[{datetime}] [{level}] {message}'; | 
| 31 | |
| 32 | public function __construct( | 
| 33 | LoggerType $type = LoggerType::ERROR_LOG, | 
| 34 | string $logLevel = LogLevel::DEBUG, | 
| 35 | string $absoluteFilePath = '' | 
| 36 | ) { | 
| 37 | |
| 38 | if ($type === LoggerType::FILE) | 
| 39 | if (!file_exists($absoluteFilePath)) | 
| 40 | throw new InvalidArgumentException("File: $absoluteFilePath does not exist."); | 
| 41 | |
| 42 | $this->validateLevel($logLevel); | 
| 43 | |
| 44 | $this->absoluteFilePath = $absoluteFilePath; | 
| 45 | $this->logLevel = $logLevel; | 
| 46 | $this->type = $type; | 
| 47 | } | 
| 48 | |
| 49 | public function log($level, string|Stringable $message, array $context = []): void | 
| 50 | { | 
| 51 | $this->validateLevel($level); | 
| 52 | |
| 53 | $level = strtolower($level); | 
| 54 | if (static::RFC5424_LEVELS[$level] > static::RFC5424_LEVELS[$this->logLevel]) | 
| 55 | return; | 
| 56 | |
| 57 | $message = $this->interpolate($message, $context); | 
| 58 | $line = $this->createLine($level, $message); | 
| 59 | $this->writeLine($line); | 
| 60 | } | 
| 61 | |
| 62 | private function validateLevel(string $level): void | 
| 63 | { | 
| 64 | $level = strtolower($level); | 
| 65 | if (!array_key_exists($level, static::RFC5424_LEVELS)) | 
| 66 | throw new InvalidArgumentException("Invalid log level: $level"); | 
| 67 | } | 
| 68 | |
| 69 | private function interpolate(string $message, array $context): string | 
| 70 | { | 
| 71 | $replace = []; | 
| 72 | foreach ($context as $key => $val) { | 
| 73 | |
| 74 | $val = match (true) { | 
| 75 | is_null($val) => 'null', | 
| 76 | is_bool($val) => $val ? 'true' : 'false', | 
| 77 | is_array($val) => json_encode($val), | 
| 78 | $this->isException($key, $val) => $this->formatException($val), | 
| 79 | is_object($val) => $this->formatObject($val), | 
| 80 | default => (string) $val, | 
| 81 | }; | 
| 82 | |
| 83 | $key = '{' . $key . '}'; | 
| 84 | $replace[$key] = $val; | 
| 85 | } | 
| 86 | |
| 87 | return strtr($message, $replace); | 
| 88 | } | 
| 89 | |
| 90 | private function isException(string $key, mixed $exception): bool | 
| 91 | { | 
| 92 | if ($key !== 'exception') | 
| 93 | return false; | 
| 94 | if (!is_subclass_of($exception, \Throwable::class)) | 
| 95 | return false; | 
| 96 | return true; | 
| 97 | } | 
| 98 | |
| 99 | private function formatObject(object $object): string | 
| 100 | { | 
| 101 | if ($object instanceof Stringable || method_exists($object, '__toString')) | 
| 102 | return (string) $object; | 
| 103 | |
| 104 | $class = get_class($object); | 
| 105 | return "Class: $class."; | 
| 106 | } | 
| 107 | |
| 108 | private function formatException(\Throwable $exception): string | 
| 109 | { | 
| 110 | $trace = $exception->getTraceAsString(); | 
| 111 | $code = $exception->getCode(); | 
| 112 | $message = $exception->getMessage(); | 
| 113 | $file = $exception->getFile(); | 
| 114 | $line = $exception->getLine(); | 
| 115 | return "Exception \"$message\" with code $code in \"$file\" on line $line.\nStack trace:\n$trace"; | 
| 116 | } | 
| 117 | |
| 118 | private function createLine(string $level, string $message): string | 
| 119 | { | 
| 120 | $vars = [ | 
| 121 | '{datetime}' => date('Y-m-d H:i:s'), | 
| 122 | '{level}' => strtoupper($level), | 
| 123 | '{message}' => $message | 
| 124 | ]; | 
| 125 | |
| 126 | $line = strtr($this->logFormat, $vars); | 
| 127 | |
| 128 | return $line . PHP_EOL; | 
| 129 | } | 
| 130 | |
| 131 | private function writeLine(string $line): void | 
| 132 | { | 
| 133 | match ($this->type) { | 
| 134 | LoggerType::ERROR_LOG => $this->writeToErrorLog($line), | 
| 135 | LoggerType::FILE => $this->writeLineToFile($line), | 
| 136 | LoggerType::LOG_STORE => $this->logStore[] = $line | 
| 137 | }; | 
| 138 | } | 
| 139 | |
| 140 | private function writeToErrorLog(string $line): void | 
| 141 | { | 
| 142 | error_log($line, message_type: 0); | 
| 143 | } | 
| 144 | |
| 145 | private function writeLineToFile(string $line): void | 
| 146 | { | 
| 147 | file_put_contents($this->absoluteFilePath, $line, FILE_APPEND | LOCK_EX); | 
| 148 | } | 
| 149 | |
| 150 | public function logStore(bool $asString = true): string|array | 
| 151 | { | 
| 152 | if ($asString) | 
| 153 | return implode(', ', $this->logStore); | 
| 154 | return $this->logStore; | 
| 155 | } | 
| 156 | |
| 157 | public function setLogFormat(string $logFormat): void | 
| 158 | { | 
| 159 | $this->logFormat = $logFormat; | 
| 160 | } | 
| 161 | } |