Completed
Pull Request — master (#501)
by Markus
02:24
created

Inspector::getExceptionDocrefUrl()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 0
cts 7
cp 0
rs 9.2
c 0
b 0
f 0
cc 4
eloc 6
nc 3
nop 0
crap 20
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
        return $this->exception->getMessage();
58
    }
59
60
    /**
61
     * Returns a url to the php-manual related to the underlying error - when available.
62
     *
63
     * @return string|null
64
     */
65
    public function getExceptionDocrefUrl()
66
    {
67
        // php embbeds urls to the manual into the Exception message with the following ini-settings defined
68
        // http://php.net/manual/en/errorfunc.configuration.php#ini.docref-root
69
        if (ini_get('html_errors') && ini_get('docref_root')) {
70
            $message = $this->exception->getMessage();
71
72
            if (preg_match("/\[<a href='([^']+)'>(?:[^<]+)<\/a>\]/", $message, $matches)) {
73
                return $matches[1];
74
            }
75
        }
76
77
        return null;
78
    }
79
80
    /**
81
     * Does the wrapped Exception has a previous Exception?
82
     * @return bool
83
     */
84 2
    public function hasPreviousException()
85
    {
86 2
        return $this->previousExceptionInspector || $this->exception->getPrevious();
87
    }
88
89
    /**
90
     * Returns an Inspector for a previous Exception, if any.
91
     * @todo   Clean this up a bit, cache stuff a bit better.
92
     * @return Inspector
93
     */
94 2
    public function getPreviousExceptionInspector()
95
    {
96 2
        if ($this->previousExceptionInspector === null) {
97 2
            $previousException = $this->exception->getPrevious();
98
99 2
            if ($previousException) {
100 1
                $this->previousExceptionInspector = new Inspector($previousException);
101 1
            }
102 2
        }
103
104 2
        return $this->previousExceptionInspector;
105
    }
106
107
    /**
108
     * Returns an iterator for the inspected exception's
109
     * frames.
110
     * @return \Whoops\Exception\FrameCollection
111
     */
112 3
    public function getFrames()
113
    {
114 3
        if ($this->frames === null) {
115 3
            $frames = $this->getTrace($this->exception);
116
117
            // Fill empty line/file info for call_user_func_array usages (PHP Bug #44428)
118 3
            foreach ($frames as $k => $frame) {
119
120 3
                if (empty($frame['file'])) {
121
                    // Default values when file and line are missing
122 3
                    $file = '[internal]';
123 3
                    $line = 0;
124
125 3
                    $next_frame = !empty($frames[$k + 1]) ? $frames[$k + 1] : [];
126
127 3
                    if ($this->isValidNextFrame($next_frame)) {
128
                        $file = $next_frame['file'];
129
                        $line = $next_frame['line'];
130
                    }
131
132 3
                    $frames[$k]['file'] = $file;
133 3
                    $frames[$k]['line'] = $line;
134 3
                }
135
136 3
            }
137
138
            // Find latest non-error handling frame index ($i) used to remove error handling frames
139 3
            $i = 0;
140 3
            foreach ($frames as $k => $frame) {
141 3
                if ($frame['file'] == $this->exception->getFile() && $frame['line'] == $this->exception->getLine()) {
142
                    $i = $k;
143
                }
144 3
            }
145
146
            // Remove error handling frames
147 3
            if ($i > 0) {
148
                array_splice($frames, 0, $i);
149
            }
150
151 3
            $firstFrame = $this->getFrameFromException($this->exception);
152 3
            array_unshift($frames, $firstFrame);
153
154 3
            $this->frames = new FrameCollection($frames);
155
156 3
            if ($previousInspector = $this->getPreviousExceptionInspector()) {
157
                // Keep outer frame on top of the inner one
158 1
                $outerFrames = $this->frames;
159 1
                $newFrames = clone $previousInspector->getFrames();
160
                // I assume it will always be set, but let's be safe
161 1
                if (isset($newFrames[0])) {
162 1
                    $newFrames[0]->addComment(
163 1
                        $previousInspector->getExceptionMessage(),
164
                        'Exception message:'
165 1
                    );
166 1
                }
167 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...
168 1
                $this->frames = $newFrames;
169 1
            }
170 3
        }
171
172 3
        return $this->frames;
173
    }
174
175
    /**
176
     * Gets the backtrace from an exception.
177
     *
178
     * If xdebug is installed
179
     *
180
     * @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...
181
     * @return array
182
     */
183 1
    protected function getTrace($e)
184
    {
185 1
        $traces = $e->getTrace();
186
187
        // Get trace from xdebug if enabled, failure exceptions only trace to the shutdown handler by default
188 1
        if (!$e instanceof \ErrorException) {
189 1
            return $traces;
190
        }
191
192
        if (!Misc::isLevelFatal($e->getSeverity())) {
193
            return $traces;
194
        }
195
196
        if (!extension_loaded('xdebug') || !xdebug_is_enabled()) {
197
            return [];
198
        }
199
200
        // Use xdebug to get the full stack trace and remove the shutdown handler stack trace
201
        $stack = array_reverse(xdebug_get_function_stack());
202
        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
203
        $traces = array_diff_key($stack, $trace);
204
205
        return $traces;
206
    }
207
208
    /**
209
     * Given an exception, generates an array in the format
210
     * generated by Exception::getTrace()
211
     * @param  \Throwable $exception
212
     * @return array
213
     */
214 1
    protected function getFrameFromException($exception)
215
    {
216
        return [
217 1
            'file'  => $exception->getFile(),
218 1
            'line'  => $exception->getLine(),
219 1
            'class' => get_class($exception),
220
            'args'  => [
221 1
                $exception->getMessage(),
222 1
            ],
223 1
        ];
224
    }
225
226
    /**
227
     * Given an error, generates an array in the format
228
     * generated by ErrorException
229
     * @param  ErrorException $exception
230
     * @return array
231
     */
232
    protected function getFrameFromError(ErrorException $exception)
233
    {
234
        return [
235
            'file'  => $exception->getFile(),
236
            'line'  => $exception->getLine(),
237
            'class' => null,
238
            'args'  => [],
239
        ];
240
    }
241
242
    /**
243
     * Determine if the frame can be used to fill in previous frame's missing info
244
     * happens for call_user_func and call_user_func_array usages (PHP Bug #44428)
245
     *
246
     * @param array $frame
247
     * @return bool
248
     */
249 1
    protected function isValidNextFrame(array $frame)
250
    {
251 1
        if (empty($frame['file'])) {
252
            return false;
253
        }
254
255 1
        if (empty($frame['line'])) {
256
            return false;
257
        }
258
259 1
        if (empty($frame['function']) || !stristr($frame['function'], 'call_user_func')) {
260 1
            return false;
261
        }
262
263
        return true;
264
    }
265
}
266