Exception::create()   B
last analyzed

Complexity

Conditions 7
Paths 19

Size

Total Lines 48
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 7

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 28
c 2
b 0
f 0
nc 19
nop 1
dl 0
loc 48
ccs 27
cts 27
cp 1
crap 7
rs 8.5386
1
<?php
2
3
/**
4
 * @author Marwan Al-Soltany <[email protected]>
5
 * @copyright Marwan Al-Soltany 2021
6
 * For the full copyright and license information, please view
7
 * the LICENSE file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace MAKS\Velox\Backend;
13
14
use MAKS\Velox\Helper\Misc;
15
16
/**
17
 * A class that serves as a base exception class with helpers to assist with errors/exceptions handling.
18
 *
19
 * Example:
20
 * ```
21
 * // throw an exception
22
 * $signature = 'YException:XException'; // YException extends YException and will get created if it does not exist.
23
 * Exception::throw($signature, $message, $code, $previous);
24
 *
25
 * // handle the passed callback in a safe context where errors get converted to exceptions
26
 * Exception::handle($callback, $signature, $message);
27
 *
28
 * // trigger an E_USER_* error, warning, notice, or deprecated with backtrace info
29
 * Exception::trigger($message, $severity);
30
 * ```
31
 *
32
 * @package Velox\Backend
33
 * @since 1.5.5
34
 * @api
35
 */
36
class Exception extends \Exception
37
{
38
    /**
39
     * Class constructor.
40
     * {@inheritDoc}
41
     *
42
     * @param string $message The Exception message.
43
     */
44 8
    public function __construct(string $message, int $code = 0, \Throwable $previous = null)
45
    {
46 8
        parent::__construct($message, $code, $previous);
47
    }
48
49
    /**
50
     * Returns a string representation of the exception object.
51
     *
52
     * @return string
53
     */
54 1
    public function __toString()
55
    {
56 1
        return Misc::interpolate('{class}: {message} [Code: {code}] {eol}{trace}{eol}', [
57
            'class'   => static::class,
58 1
            'code'    => $this->getCode(),
59 1
            'message' => $this->getMessage(),
60 1
            'trace'   => $this->getTraceAsString(),
61
            'eol'     => PHP_EOL,
62
        ]);
63
    }
64
65
66
    /**
67
     * Creates an exception class dynamically and returns its class FQN.
68
     *
69
     * @param string $signature
70
     *
71
     * @return string
72
     */
73 26
    final protected static function create(string $signature): string
74
    {
75 26
        if (class_exists($signature, false)) {
76 3
            return '\\' . trim($signature, '\\');
77
        }
78
79 23
        $namespace = static::class;
80 23
        $parent    = static::class;
81 23
        $class     = $signature = trim($signature, '\\');
82
83 23
        if (strpos($signature, ':') !== false) {
84 23
            [$class, $parent] = explode(':', $signature, 2);
85
86 23
            if (strpos($class, '\\') !== false) {
87 3
                $namespace = implode('\\', explode('\\', $class, -1));
88 3
                $parts     = explode('\\', $class);
89 3
                $class     = $parts[count($parts) - 1];
90
            }
91
92 23
            $parent = class_exists($parent) && is_subclass_of($parent, \Exception::class)
93 22
                ? trim($parent, '\\')
94 23
                : static::class;
95
        }
96
97 23
        $content = Misc::interpolate(
98
            '<?php namespace {namespace}; class {class} extends \\{parent} { /* TEMP */ }',
99 23
            compact('namespace', 'class', 'parent')
100
        );
101
102 23
        $classFQN = Misc::interpolate(
103
            '\\{namespace}\\{class}',
104 23
            compact('namespace', 'class')
105
        );
106
107 23
        if (class_exists($classFQN, false)) {
108 10
            return $classFQN;
109
        }
110
111 14
        $file = tmpfile();
112 14
        $path = stream_get_meta_data($file)['uri'];
113
114 14
        fwrite($file, $content);
115
116 14
        require $path;
117
118 14
        fclose($file);
119
120 14
        return $classFQN;
121
    }
122
123
    /**
124
     * Throws an exception using the given signature.
125
     *
126
     * NOTE:
127
     * Exceptions thrown via this method will be created at runtime if they are not build-in or defined explicitly (actual classes).
128
     * This means that the catch block that catches them must type-hint a fully qualified class name, because the `use` statement
129
     * will trigger a call to the autoloader, and the autoloader may not know about the magic exception class at that point of time.
130
     *
131
     * @param string $signature Exception signature.
132
     *      This can be a class FQN like `SomeException` or `Namespace\SomeException` or a class FQN with a parent FQN like `Namespace\SomeException:RuntimeException`.
133
     *      Note that exception class will be created at runtime if it does not exist.
134
     * @param string $message [optional] Exception message. An auto-generated exception message will be created using backtrace if this is left empty.
135
     * @param int|string $code [optional] Exception code. The code will be casted into an integer.
136
     * @param \Throwable|null $previous [optional] Previous exception.
137
     *
138
     * @return void
139
     *
140
     * @throws \Exception Throws the given or the created exception.
141
     */
142 26
    public static function throw(string $signature, ?string $message = null, $code = 0, \Throwable $previous = null): void
143
    {
144 26
        $exception = static::create($signature);
145
146 26
        if ($message === null) {
147 2
            $trace   = Misc::backtrace(['file', 'line', 'class', 'function'], 1);
148 2
            $message = Misc::interpolate('{prefix}{suffix} in {file} on line {line} ', [
149 2
                'prefix'  => isset($trace['class']) ? "{$trace['class']}::" : '',
150 2
                'suffix'  => isset($trace['function']) ? "{$trace['function']}() failed!" : '',
151 2
                'file'    => $trace['file'],
152 2
                'line'    => $trace['line'],
153
            ]);
154
        }
155
156 26
        if ($previous instanceof \Throwable) {
157 8
            $message = $message . Misc::interpolate(': [{class}] {message}', [
158 8
                'class'   => (new \ReflectionClass($previous))->getShortName(),
159 8
                'message' => $previous->getMessage(),
160
            ]);
161
        }
162
163 26
        throw new $exception((string)$message, (int)$code, $previous);
164
    }
165
166
    /**
167
     * Handles the passed callback in a safe context where PHP errors (and exceptions) result in exceptions that can be caught.
168
     *
169
     * @param callable $callback The callback to be executed.
170
     * @param string $signature [optional] Exception signature.
171
     *      This can be a class FQN like `SomeException` or `Namespace\SomeException` or a class FQN with a parent FQN like `Namespace\SomeException:RuntimeException`.
172
     *      Note that exception class will be created at runtime if it does not exist.
173
     * @param string $message [optional] The exception message if the callback raised an error or throw an exception.
174
     *
175
     * @return void
176
     *
177
     * @throws \Exception
178
     */
179 4
    public static function handle(callable $callback, ?string $signature = null, ?string $message = null): void
180
    {
181
        static $handler = null;
182
183 4
        if ($handler === null) {
184 1
            $handler = function (int $code, string $message, string $file, int $line) {
185 3
                throw new \ErrorException($message, $code, E_ERROR, $file, $line);
186
            };
187
        }
188
189 4
        set_error_handler($handler, E_ALL);
190
191
        try {
192 4
            $callback();
193 3
        } catch (\Throwable $exception) {
194 3
            $message = $message ?? Misc::interpolate('{method}() failed in {file} on line {line}', [
195
                'method' => __METHOD__,
196 2
                'file'   => $exception->getFile(),
197 3
                'line'   => $exception->getLine(),
198
            ]);
199 3
            $message = $message . ': ' . $exception->getMessage();
200 3
            $code    = $exception->getCode();
201
202 3
            static::throw($signature ?? static::class, $message, $code);
203 2
        } finally {
204 4
            restore_error_handler();
205
        }
206
    }
207
208
    /**
209
     * Triggers a user-level error, warning, notice, or deprecation with backtrace info.
210
     *
211
     * @param string $message Error message.
212
     * @param int $severity Error severity (`E_USER_*` family error). Default and fallback is `E_USER_ERROR`.
213
     * - `E_USER_ERROR => 256`,
214
     * - `E_USER_WARNING => 512`,
215
     * - `E_USER_NOTICE => 1024`,
216
     * - `E_USER_DEPRECATED => 16384`.
217
     *
218
     * @return void
219
     */
220 1
    public static function trigger(string $message, int $severity = E_USER_ERROR): void
221
    {
222 1
        $trace = Misc::backtrace(['file', 'line'], 1);
223
224 1
        $error = Misc::interpolate('{message} in {file} on line {line} ', [
225 1
            'file'    => $trace['file'],
226 1
            'line'    => $trace['line'],
227
            'message' => $message,
228
        ]);
229
230 1
        $severities = [E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED];
231
232 1
        trigger_error($error, in_array($severity, $severities, true) ? $severity : E_USER_ERROR);
233
    }
234
}
235