Completed
Push — master ( c9d2e1...b23897 )
by Denis
14s
created

Inspector::extractDocrefUrl()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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