Psr3ErrorHandler   A
last analyzed

Complexity

Total Complexity 20

Size/Duplication

Total Lines 156
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 20
lcom 1
cbo 1
dl 0
loc 156
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A ignoreSeverity() 0 4 1
A allowNonPsrLevels() 0 4 1
A handle() 0 17 1
D getLevel() 0 29 10
A checkLevel() 0 4 1
A validateLevel() 0 4 2
A getType() 0 10 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Nofw\Error;
6
7
use Psr\Log\LoggerInterface;
8
use Psr\Log\LogLevel;
9
10
/**
11
 * This handler can be used to log errors using a PSR-3 logger.
12
 *
13
 * @author Márk Sági-Kazár <[email protected]>
14
 */
15
final class Psr3ErrorHandler implements ErrorHandler
16
{
17
    /**
18
     * Defines which error levels should be mapped to certain error types by default.
19
     */
20
    private const DEFAULT_ERROR_LEVEL_MAP = [
21
        \ParseError::class => LogLevel::CRITICAL,
22
        \Throwable::class => LogLevel::ERROR,
23
    ];
24
25
    /**
26
     * The default log level.
27
     */
28
    private const DEFAULT_LOG_LEVEL = LogLevel::ERROR;
29
30
    /**
31
     * The key under which the error should be passed to the log context.
32
     */
33
    public const ERROR_KEY = 'error';
34
35
    /**
36
     * @var LoggerInterface
37
     */
38
    private $logger;
39
40
    /**
41
     * Defines which error levels should be mapped to certain error types.
42
     *
43
     * Note: The errors are checked in order, so if you want to define fallbacks for classes higher in the tree
44
     * make sure to add them to the end of the map.
45
     *
46
     * @var array
47
     */
48
    private $levelMap = [];
49
50
    /**
51
     * Ignores the severity when detecting the log level.
52
     *
53
     * @var bool
54
     */
55
    private $ignoreSeverity = false;
56
57
    /**
58
     * Enables the handler to accept detecting non-PSR-3 log levels.
59
     *
60
     * Note: when detecting an invalid level the handler will silently fall back to the default.
61
     *
62
     * @var bool
63
     */
64
    private $allowNonPsrLevels = false;
65
66
    public function __construct(LoggerInterface $logger, array $levelMap = [])
67
    {
68
        $this->logger = $logger;
69
70
        // Keep user maintained order
71
        $this->levelMap = array_replace($levelMap, self::DEFAULT_ERROR_LEVEL_MAP, $levelMap);
72
    }
73
74
    /**
75
     * Ignores the severity when detecting the log level.
76
     */
77
    public function ignoreSeverity(bool $ignoreSeverity = true): void
78
    {
79
        $this->ignoreSeverity = $ignoreSeverity;
80
    }
81
82
    /**
83
     * Enables the handler to accept detecting non-PSR-3 log levels.
84
     */
85
    public function allowNonPsrLevels(bool $allowNonPsrLevels = true): void
86
    {
87
        $this->allowNonPsrLevels = $allowNonPsrLevels;
88
    }
89
90
    public function handle(\Throwable $t, array $context = []): void
91
    {
92
        $context[self::ERROR_KEY] = $t;
93
94
        $this->logger->log(
95
            $this->getLevel($t, $context),
96
            sprintf(
97
                '%s \'%s\' with message \'%s\' in %s(%s)',
98
                $this->getType($t),
99
                get_class($t),
100
                $t->getMessage(),
101
                $t->getFile(),
102
                $t->getLine()
103
            ),
104
            $context
105
        );
106
    }
107
108
    /**
109
     * Determines the level for the error.
110
     */
111
    private function getLevel(\Throwable $t, array $context): string
112
    {
113
        // Check if the severity matches a PSR-3 log level
114
        if (
115
            false === $this->ignoreSeverity &&
116
            isset($context[Context::SEVERITY]) &&
117
            is_string($context[Context::SEVERITY]) &&
118
            $this->validateLevel($context[Context::SEVERITY])
119
        ) {
120
            return $context[Context::SEVERITY];
121
        }
122
123
        // Find the log level based on the error in the level map (avoid looping through the whole array)
124
        // Note: this ignores the order defined in the map.
125
        $class = get_class($t);
126
        if (isset($this->levelMap[$class]) && $this->validateLevel($this->levelMap[$class])) {
127
            return $this->levelMap[$class];
128
        }
129
130
        // Find the log level based on the error in the level map
131
        foreach ($this->levelMap as $className => $candidate) {
132
            if ($t instanceof $className && $this->validateLevel($candidate)) {
133
                return $candidate;
134
            }
135
        }
136
137
        // Return the default log level
138
        return self::DEFAULT_LOG_LEVEL;
139
    }
140
141
    /**
142
     * Checks whether a log level exists.
143
     */
144
    private function checkLevel(string $level): bool
145
    {
146
        return defined(sprintf('%s::%s', LogLevel::class, strtoupper($level)));
147
    }
148
149
    /**
150
     * Validates whether a log level exists (if non-PSR levels are not allowed).
151
     */
152
    private function validateLevel(string $level): bool
153
    {
154
        return $this->allowNonPsrLevels || $this->checkLevel($level);
155
    }
156
157
    /**
158
     * Determines the error type.
159
     */
160
    private function getType(\Throwable $t): string
161
    {
162
        if ($t instanceof \Exception) {
163
            return 'Exception';
164
        } elseif ($t instanceof \Error) {
165
            return 'Error';
166
        }
167
168
        return 'Throwable';
169
    }
170
}
171