Completed
Push — master ( fa28f0...71ba35 )
by Denis
12s
created

Inspector::extractDocrefUrl()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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