Completed
Push — master ( b0b219...3b5b93 )
by Márk
140:06 queued 76:01
created

Psr3ErrorHandler::validateLevel()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 1
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) {
0 ignored issues
show
Bug introduced by
The class Error does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
165
            return 'Error';
166
        }
167
168
        return 'Throwable';
169
    }
170
}
171