Completed
Pull Request — master (#501)
by Markus
01:59
created

Inspector   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 264
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 65.05%

Importance

Changes 0
Metric Value
wmc 39
lcom 1
cbo 3
dl 0
loc 264
ccs 67
cts 103
cp 0.6505
rs 8.2857
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getExceptionName() 0 4 1
A __construct() 0 4 1
A getException() 0 4 1
A getExceptionMessage() 0 13 3
A getExceptionDocrefUrl() 0 14 4
A hasPreviousException() 0 4 2
A getPreviousExceptionInspector() 0 12 3
C getFrames() 0 62 12
B getTrace() 0 24 5
A getFrameFromException() 0 11 1
A getFrameFromError() 0 9 1
B isValidNextFrame() 0 16 5
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
     * @param \Throwable $exception The exception to inspect
30
     */
31 3
    public function __construct($exception)
32
    {
33 3
        $this->exception = $exception;
34 3
    }
35
36
    /**
37
     * @return \Throwable
38
     */
39 3
    public function getException()
40
    {
41 3
        return $this->exception;
42
    }
43
44
    /**
45
     * @return string
46
     */
47 2
    public function getExceptionName()
48
    {
49 2
        return get_class($this->exception);
50
    }
51
52
    /**
53
     * @return string
54
     */
55
    public function getExceptionMessage()
56
    {
57
        // php embbeds urls to the manual into the Exception message with the following ini-settings defined
58
        // http://php.net/manual/en/errorfunc.configuration.php#ini.docref-root
59
        // -> strip those automatically generated links from the exception message
60
        if (ini_get('html_errors') && ini_get('docref_root')) {
61
            $message = $this->exception->getMessage();
62
63
            return preg_replace("/\s+\[<a href='([^']+)'>(?:[^<]+)<\/a>\]/", '', $message, 1);
64
        }
65
66
        return $this->exception->getMessage();
67
    }
68
69
    /**
70
     * Returns a url to the php-manual related to the underlying error - when available.
71
     *
72
     * @return string|null
73
     */
74
    public function getExceptionDocrefUrl()
75
    {
76
        // php embbeds urls to the manual into the Exception message with the following ini-settings defined
77
        // http://php.net/manual/en/errorfunc.configuration.php#ini.docref-root
78
        if (ini_get('html_errors') && ini_get('docref_root')) {
79
            $message = $this->exception->getMessage();
80
81
            if (preg_match("/\[<a href='([^']+)'>(?:[^<]+)<\/a>\]/", $message, $matches)) {
82
                return $matches[1];
83
            }
84
        }
85
86
        return null;
87
    }
88
89
    /**
90
     * Does the wrapped Exception has a previous Exception?
91
     * @return bool
92
     */
93 2
    public function hasPreviousException()
94
    {
95 2
        return $this->previousExceptionInspector || $this->exception->getPrevious();
96
    }
97
98
    /**
99
     * Returns an Inspector for a previous Exception, if any.
100
     * @todo   Clean this up a bit, cache stuff a bit better.
101
     * @return Inspector
102
     */
103 2
    public function getPreviousExceptionInspector()
104
    {
105 2
        if ($this->previousExceptionInspector === null) {
106 2
            $previousException = $this->exception->getPrevious();
107
108 2
            if ($previousException) {
109 1
                $this->previousExceptionInspector = new Inspector($previousException);
110 1
            }
111 2
        }
112
113 2
        return $this->previousExceptionInspector;
114
    }
115
116
    /**
117
     * Returns an iterator for the inspected exception's
118
     * frames.
119
     * @return \Whoops\Exception\FrameCollection
120
     */
121 3
    public function getFrames()
122
    {
123 3
        if ($this->frames === null) {
124 3
            $frames = $this->getTrace($this->exception);
125
126
            // Fill empty line/file info for call_user_func_array usages (PHP Bug #44428)
127 3
            foreach ($frames as $k => $frame) {
128
129 3
                if (empty($frame['file'])) {
130
                    // Default values when file and line are missing
131 3
                    $file = '[internal]';
132 3
                    $line = 0;
133
134 3
                    $next_frame = !empty($frames[$k + 1]) ? $frames[$k + 1] : [];
135
136 3
                    if ($this->isValidNextFrame($next_frame)) {
137
                        $file = $next_frame['file'];
138
                        $line = $next_frame['line'];
139
                    }
140
141 3
                    $frames[$k]['file'] = $file;
142 3
                    $frames[$k]['line'] = $line;
143 3
                }
144
145 3
            }
146
147
            // Find latest non-error handling frame index ($i) used to remove error handling frames
148 3
            $i = 0;
149 3
            foreach ($frames as $k => $frame) {
150 3
                if ($frame['file'] == $this->exception->getFile() && $frame['line'] == $this->exception->getLine()) {
151
                    $i = $k;
152
                }
153 3
            }
154
155
            // Remove error handling frames
156 3
            if ($i > 0) {
157
                array_splice($frames, 0, $i);
158
            }
159
160 3
            $firstFrame = $this->getFrameFromException($this->exception);
161 3
            array_unshift($frames, $firstFrame);
162
163 3
            $this->frames = new FrameCollection($frames);
164
165 3
            if ($previousInspector = $this->getPreviousExceptionInspector()) {
166
                // Keep outer frame on top of the inner one
167 1
                $outerFrames = $this->frames;
168 1
                $newFrames = clone $previousInspector->getFrames();
169
                // I assume it will always be set, but let's be safe
170 1
                if (isset($newFrames[0])) {
171 1
                    $newFrames[0]->addComment(
172 1
                        $previousInspector->getExceptionMessage(),
173
                        'Exception message:'
174 1
                    );
175 1
                }
176 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...
177 1
                $this->frames = $newFrames;
178 1
            }
179 3
        }
180
181 3
        return $this->frames;
182
    }
183
184
    /**
185
     * Gets the backtrace from an exception.
186
     *
187
     * If xdebug is installed
188
     *
189
     * @param  \Throwable $exception
0 ignored issues
show
Bug introduced by
There is no parameter named $exception. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
190
     * @return array
191
     */
192 1
    protected function getTrace($e)
193
    {
194 1
        $traces = $e->getTrace();
195
196
        // Get trace from xdebug if enabled, failure exceptions only trace to the shutdown handler by default
197 1
        if (!$e instanceof \ErrorException) {
198 1
            return $traces;
199
        }
200
201
        if (!Misc::isLevelFatal($e->getSeverity())) {
202
            return $traces;
203
        }
204
205
        if (!extension_loaded('xdebug') || !xdebug_is_enabled()) {
206
            return [];
207
        }
208
209
        // Use xdebug to get the full stack trace and remove the shutdown handler stack trace
210
        $stack = array_reverse(xdebug_get_function_stack());
211
        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
212
        $traces = array_diff_key($stack, $trace);
213
214
        return $traces;
215
    }
216
217
    /**
218
     * Given an exception, generates an array in the format
219
     * generated by Exception::getTrace()
220
     * @param  \Throwable $exception
221
     * @return array
222
     */
223 1
    protected function getFrameFromException($exception)
224
    {
225
        return [
226 1
            'file'  => $exception->getFile(),
227 1
            'line'  => $exception->getLine(),
228 1
            'class' => get_class($exception),
229
            'args'  => [
230 1
                $exception->getMessage(),
231 1
            ],
232 1
        ];
233
    }
234
235
    /**
236
     * Given an error, generates an array in the format
237
     * generated by ErrorException
238
     * @param  ErrorException $exception
239
     * @return array
240
     */
241
    protected function getFrameFromError(ErrorException $exception)
242
    {
243
        return [
244
            'file'  => $exception->getFile(),
245
            'line'  => $exception->getLine(),
246
            'class' => null,
247
            'args'  => [],
248
        ];
249
    }
250
251
    /**
252
     * Determine if the frame can be used to fill in previous frame's missing info
253
     * happens for call_user_func and call_user_func_array usages (PHP Bug #44428)
254
     *
255
     * @param array $frame
256
     * @return bool
257
     */
258 1
    protected function isValidNextFrame(array $frame)
259
    {
260 1
        if (empty($frame['file'])) {
261
            return false;
262
        }
263
264 1
        if (empty($frame['line'])) {
265
            return false;
266
        }
267
268 1
        if (empty($frame['function']) || !stristr($frame['function'], 'call_user_func')) {
269 1
            return false;
270
        }
271
272
        return true;
273
    }
274
}
275