Passed
Push — master ( 26c234...c8a6d1 )
by Marwan
09:32
created

Exception::handle()   A

Complexity

Conditions 3
Paths 14

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 17
c 1
b 0
f 0
nc 14
nop 3
dl 0
loc 26
ccs 18
cts 18
cp 1
crap 3
rs 9.7
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 8
    }
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 1
            'class'   => static::class,
58 1
            'code'    => $this->getCode(),
59 1
            'message' => $this->getMessage(),
60 1
            'trace'   => $this->getTraceAsString(),
61 1
            '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 '\\' . $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);
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 23
            '<?php namespace {namespace}; class {class} extends \\{parent} { /* TEMP */ }',
99 23
            compact('namespace', 'class', 'parent')
100
        );
101
102 23
        $classFQN = Misc::interpolate(
103 23
            '\\{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 \Exception|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, \Exception $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
        throw new $exception((string)$message, (int)$code, $previous);
157
    }
158
159
    /**
160
     * Handles the passed callback in a safe context where PHP errors (and exceptions) result in exceptions that can be caught.
161
     *
162
     * @param callable $callback The callback to be executed.
163
     * @param string $signature [optional] Exception signature.
164
     *      This can be a class FQN like `SomeException` or `Namespace\SomeException` or a class FQN with a parent FQN like `Namespace\SomeException:RuntimeException`.
165
     *      Note that exception class will be created at runtime if it does not exist.
166
     * @param string $message [optional] The exception message if the callback raised an error or throw an exception.
167
     *
168
     * @return void
169
     *
170
     * @throws \Exception
171
     */
172 4
    public static function handle(callable $callback, ?string $signature = null, ?string $message = null): void
173
    {
174 4
        static $handler = null;
175
176 4
        if ($handler === null) {
177 1
            $handler = function (int $code, string $message, string $file, int $line) {
178 3
                throw new \ErrorException($message, $code, E_ERROR, $file, $line);
179 1
            };
180
        }
181
182 4
        set_error_handler($handler, E_ALL);
183
184
        try {
185 4
            $callback();
186 3
        } catch (\Throwable $exception) {
187 3
            $message = $message ?? Misc::interpolate('{method}() failed in {file} on line {line}', [
188 2
                'method' => __METHOD__,
189 2
                'file'   => $exception->getFile(),
190 3
                'line'   => $exception->getLine(),
191
            ]);
192 3
            $message = $message . ': ' . $exception->getMessage();
193 3
            $code    = $exception->getCode();
194
195 3
            static::throw($signature ?? static::class, $message, $code, $exception);
196 2
        } finally {
197 4
            restore_error_handler();
198
        }
199 2
    }
200
201
    /**
202
     * Triggers a user-level error, warning, notice, or deprecation with backtrace info.
203
     *
204
     * @param string $message Error message.
205
     * @param int $severity Error severity (`E_USER_*` family error). Default and fallback is `E_USER_ERROR`.
206
     * - `E_USER_ERROR => 256`,
207
     * - `E_USER_WARNING => 512`,
208
     * - `E_USER_NOTICE => 1024`,
209
     * - `E_USER_DEPRECATED => 16384`.
210
     *
211
     * @return void
212
     */
213 1
    public static function trigger(string $message, int $severity = E_USER_ERROR): void
214
    {
215 1
        $trace = Misc::backtrace(['file', 'line'], 1);
216
217 1
        $error = Misc::interpolate('{message} in {file} on line {line} ', [
218 1
            'file'    => $trace['file'],
219 1
            'line'    => $trace['line'],
220 1
            'message' => $message,
221
        ]);
222
223 1
        $severities = [E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED];
224
225 1
        trigger_error($error, in_array($severity, $severities, true) ? $severity : E_USER_ERROR);
226
    }
227
}
228