Completed
Push — master ( 33e8f2...d52aae )
by Denis
439:43 queued 438:12
created

Inspector::getPreviousExceptions()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.576

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 6
cts 10
cp 0.6
rs 9.7998
c 0
b 0
f 0
cc 3
nc 2
nop 0
crap 3.576
1
<?php
2
/**
3
 * Whoops - php errors for cool kids
4
 * @author Filipe Dobreira <http://github.com/filp>
5
 */
6
7
namespace Whoops\Exception;
8
9
use Whoops\Util\Misc;
10
11
class Inspector
12
{
13
    /**
14
     * @var \Throwable
15
     */
16
    private $exception;
17
18
    /**
19
     * @var \Whoops\Exception\FrameCollection
20
     */
21
    private $frames;
22
23
    /**
24
     * @var \Whoops\Exception\Inspector
25
     */
26
    private $previousExceptionInspector;
27
28
    /**
29
     * @var \Throwable[]
30
     */
31 3
    private $previousExceptions;
32
33 3
    /**
34 3
     * @param \Throwable $exception The exception to inspect
35
     */
36
    public function __construct($exception)
37
    {
38
        $this->exception = $exception;
39 3
    }
40
41 3
    /**
42
     * @return \Throwable
43
     */
44
    public function getException()
45
    {
46
        return $this->exception;
47 2
    }
48
49 2
    /**
50
     * @return string
51
     */
52
    public function getExceptionName()
53
    {
54
        return get_class($this->exception);
55
    }
56
57
    /**
58
     * @return string
59
     */
60
    public function getExceptionMessage()
61
    {
62
        return $this->extractDocrefUrl($this->exception->getMessage())['message'];
63
    }
64
65
    /**
66
     * @return string[]
67
     */
68
    public function getPreviousExceptionMessages()
69
    {
70
        return array_map(function ($prev) {
71
            /** @var \Throwable $prev */
72
            return $this->extractDocrefUrl($prev->getMessage())['message'];
73
        }, $this->getPreviousExceptions());
74
    }
75
76
    /**
77
     * @return int[]
78
     */
79
    public function getPreviousExceptionCodes()
80
    {
81
        return array_map(function ($prev) {
82
            /** @var \Throwable $prev */
83
            return $prev->getCode();
84
        }, $this->getPreviousExceptions());
85
    }
86
87
    /**
88
     * Returns a url to the php-manual related to the underlying error - when available.
89
     *
90
     * @return string|null
91
     */
92
    public function getExceptionDocrefUrl()
93
    {
94
        return $this->extractDocrefUrl($this->exception->getMessage())['url'];
95
    }
96
97 2
    private function extractDocrefUrl($message)
98
    {
99 2
        $docref = [
100
            'message' => $message,
101
            'url' => null,
102
        ];
103
104
        // php embbeds urls to the manual into the Exception message with the following ini-settings defined
105
        // http://php.net/manual/en/errorfunc.configuration.php#ini.docref-root
106
        if (!ini_get('html_errors') || !ini_get('docref_root')) {
107 2
            return $docref;
108
        }
109 2
110 2
        $pattern = "/\[<a href='([^']+)'>(?:[^<]+)<\/a>\]/";
111
        if (preg_match($pattern, $message, $matches)) {
112 2
            // -> strip those automatically generated links from the exception message
113 1
            $docref['message'] = preg_replace($pattern, '', $message, 1);
114 1
            $docref['url'] = $matches[1];
115 2
        }
116
117 2
        return $docref;
118
    }
119
120
    /**
121
     * Does the wrapped Exception has a previous Exception?
122
     * @return bool
123
     */
124
    public function hasPreviousException()
125 3
    {
126
        return $this->previousExceptionInspector || $this->exception->getPrevious();
127 3
    }
128 3
129
    /**
130
     * Returns an Inspector for a previous Exception, if any.
131 3
     * @todo   Clean this up a bit, cache stuff a bit better.
132 3
     * @return Inspector
133
     */
134 3
    public function getPreviousExceptionInspector()
135 3
    {
136
        if ($this->previousExceptionInspector === null) {
137 3
            $previousException = $this->exception->getPrevious();
138
139 3
            if ($previousException) {
140
                $this->previousExceptionInspector = new Inspector($previousException);
141
            }
142
        }
143
144 3
        return $this->previousExceptionInspector;
145 3
    }
146 3
147 3
148
    /**
149
     * Returns an array of all previous exceptions for this inspector's exception
150 3
     * @return \Throwable[]
151 3
     */
152 3
    public function getPreviousExceptions()
153
    {
154
        if ($this->previousExceptions === null) {
155 3
            $this->previousExceptions = [];
156
157
            $prev = $this->exception->getPrevious();
158 3
            while ($prev !== null) {
159
                $this->previousExceptions[] = $prev;
160
                $prev = $prev->getPrevious();
161
            }
162 3
        }
163 3
164
        return $this->previousExceptions;
165 3
    }
166
167 3
    /**
168
     * Returns an iterator for the inspected exception's
169 1
     * frames.
170 1
     * @return \Whoops\Exception\FrameCollection
171
     */
172 1
    public function getFrames()
173 1
    {
174 1
        if ($this->frames === null) {
175
            $frames = $this->getTrace($this->exception);
176 1
177 1
            // Fill empty line/file info for call_user_func_array usages (PHP Bug #44428)
178 1
            foreach ($frames as $k => $frame) {
179 1
                if (empty($frame['file'])) {
180 1
                    // Default values when file and line are missing
181 3
                    $file = '[internal]';
182
                    $line = 0;
183 3
184
                    $next_frame = !empty($frames[$k + 1]) ? $frames[$k + 1] : [];
185
186
                    if ($this->isValidNextFrame($next_frame)) {
187
                        $file = $next_frame['file'];
188
                        $line = $next_frame['line'];
189
                    }
190
191
                    $frames[$k]['file'] = $file;
192
                    $frames[$k]['line'] = $line;
193
                }
194 1
            }
195
196 1
            // Find latest non-error handling frame index ($i) used to remove error handling frames
197
            $i = 0;
198
            foreach ($frames as $k => $frame) {
199 1
                if ($frame['file'] == $this->exception->getFile() && $frame['line'] == $this->exception->getLine()) {
200 1
                    $i = $k;
201
                }
202
            }
203
204
            // Remove error handling frames
205
            if ($i > 0) {
206
                array_splice($frames, 0, $i);
207
            }
208
209
            $firstFrame = $this->getFrameFromException($this->exception);
210
            array_unshift($frames, $firstFrame);
211
212
            $this->frames = new FrameCollection($frames);
213
214
            if ($previousInspector = $this->getPreviousExceptionInspector()) {
215
                // Keep outer frame on top of the inner one
216
                $outerFrames = $this->frames;
217
                $newFrames = clone $previousInspector->getFrames();
218
                // I assume it will always be set, but let's be safe
219
                if (isset($newFrames[0])) {
220
                    $newFrames[0]->addComment(
221
                        $previousInspector->getExceptionMessage(),
222
                        'Exception message:'
223
                    );
224
                }
225 1
                $newFrames->prependFrames($outerFrames->topDiff($newFrames));
0 ignored issues
show
Documentation introduced by
$outerFrames->topDiff($newFrames) is of type array<integer,array>, but the function expects a array<integer,object<Whoops\Exception\Frame>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
226
                $this->frames = $newFrames;
227
            }
228 1
        }
229 1
230 1
        return $this->frames;
231
    }
232 1
233 1
    /**
234 1
     * Gets the backtrace from an exception.
235
     *
236
     * If xdebug is installed
237
     *
238
     * @param \Throwable $e
239
     * @return array
240
     */
241
    protected function getTrace($e)
242
    {
243
        $traces = $e->getTrace();
244
245
        // Get trace from xdebug if enabled, failure exceptions only trace to the shutdown handler by default
246
        if (!$e instanceof \ErrorException) {
247
            return $traces;
248
        }
249
250
        if (!Misc::isLevelFatal($e->getSeverity())) {
251
            return $traces;
252
        }
253
254
        if (!extension_loaded('xdebug') || !xdebug_is_enabled()) {
255
            return [];
256
        }
257
258
        // Use xdebug to get the full stack trace and remove the shutdown handler stack trace
259
        $stack = array_reverse(xdebug_get_function_stack());
260 1
        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
261
        $traces = array_diff_key($stack, $trace);
262 1
263
        return $traces;
264
    }
265
266 1
    /**
267
     * Given an exception, generates an array in the format
268
     * generated by Exception::getTrace()
269
     * @param  \Throwable $exception
270 1
     * @return array
271 1
     */
272
    protected function getFrameFromException($exception)
273
    {
274
        return [
275
            'file'  => $exception->getFile(),
276
            'line'  => $exception->getLine(),
277
            'class' => get_class($exception),
278
            'args'  => [
279
                $exception->getMessage(),
280
            ],
281
        ];
282
    }
283
284
    /**
285
     * Given an error, generates an array in the format
286
     * generated by ErrorException
287
     * @param  ErrorException $exception
288
     * @return array
289
     */
290
    protected function getFrameFromError(ErrorException $exception)
291
    {
292
        return [
293
            'file'  => $exception->getFile(),
294
            'line'  => $exception->getLine(),
295
            'class' => null,
296
            'args'  => [],
297
        ];
298
    }
299
300
    /**
301
     * Determine if the frame can be used to fill in previous frame's missing info
302
     * happens for call_user_func and call_user_func_array usages (PHP Bug #44428)
303
     *
304
     * @param array $frame
305
     * @return bool
306
     */
307
    protected function isValidNextFrame(array $frame)
308
    {
309
        if (empty($frame['file'])) {
310
            return false;
311
        }
312
313
        if (empty($frame['line'])) {
314
            return false;
315
        }
316
317
        if (empty($frame['function']) || !stristr($frame['function'], 'call_user_func')) {
318
            return false;
319
        }
320
321
        return true;
322
    }
323
}
324