Completed
Push — master ( 24da06...a9a4e0 )
by Aivis
03:00 queued 59s
created

ExceptionEncoder::getCurrentStackTrace()   C

Complexity

Conditions 7
Paths 13

Size

Total Lines 36
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 36
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 15
nc 13
nop 0
1
<?php namespace Understand\UnderstandLaravel5;
2
3
use Exception;
4
use Throwable;
5
use InvalidArgumentException;
6
use SplFileObject;
7
8
class ExceptionEncoder
9
{
10
11
    /**
12
     * @var string
13
     */
14
    protected $projectRoot;
15
16
    /**
17
     * @var int
18
     */
19
    protected $stackTraceLimit = 100;
20
21
    /**
22
     * @param integer $limit
23
     */
24
    public function setStackTraceLimit($limit)
25
    {
26
        $this->stackTraceLimit = $limit;
27
    }
28
29
    /**
30
     * @param string $projectRoot
31
     */
32
    public function setProjectRoot($projectRoot)
33
    {
34
        $this->projectRoot = $projectRoot;
35
    }
36
37
    /**
38
     * Serialize exception object
39
     *
40
     * @param mixed $exception
41
     * @return array
42
     */
43
    public function exceptionToArray($exception)
44
    {
45
        if ( ! ($exception instanceof Exception || $exception instanceof Throwable))
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
46
        {
47
            throw new InvalidArgumentException('$exception must be instance of Exception or Throwable');
48
        }
49
50
        $trace = $exception->getTrace();
51
        $className = get_class($exception);
52
        $message = $exception->getMessage() ? $exception->getMessage() : $className;
53
54
        return [
55
            'message' => $message,
56
            'class' => $className,
57
            'code' => $exception->getCode(),
58
            'file' => $this->removeProjectRoot($exception->getFile()),
59
            'line' => $exception->getLine(),
60
            'stack' => $this->stackTraceToArray($trace, $exception->getFile(), $exception->getLine())
61
        ];
62
    }
63
64
    /**
65
     * @param array $errorLog
66
     * @return array
67
     */
68
    public function setCurrentStackTrace(array $errorLog)
69
    {
70
        $level = isset($errorLog['level']) ? $errorLog['level'] : null;
71
        $stackTrace = $this->getCurrentStackTrace($level);
0 ignored issues
show
Unused Code introduced by
The call to ExceptionEncoder::getCurrentStackTrace() has too many arguments starting with $level.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
72
        $firstLineSet = false;
73
74
        foreach($stackTrace as $trace)
75
        {
76
            if ($firstLineSet)
77
            {
78
                break;
79
            }
80
81
            $firstLineSet = true;
82
83
            $errorLog['class'] = null;
84
            $errorLog['file'] = isset($trace['file']) ? $trace['file'] : null;
85
            $errorLog['line'] = isset($trace['line']) ? $trace['line'] : null;
86
        }
87
88
        $errorLog['stack'] = $stackTrace;
89
90
        return $errorLog;
91
    }
92
93
    /**
94
     * @return array
95
     */
96
    protected function getCurrentStackTrace()
97
    {
98
        $stackTrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $this->stackTraceLimit);
99
        $vendorExcluded = false;
100
101
        foreach($stackTrace as $index => $trace)
102
        {
103
            // exclude Understand service provider and helper classes
104
            if (isset($trace['class']) && strpos($trace['class'], 'Understand\UnderstandLaravel5\\') === 0)
105
            {
106
                unset($stackTrace[$index]);
107
            }
108
109
            if ( ! isset($trace['file']))
110
            {
111
                $vendorExcluded = true;
112
            }
113
114
            if ($vendorExcluded)
115
            {
116
                continue;
117
            }
118
119
            // exclude `vendor` folder until project path reached
120
            if (strpos($trace['file'], $this->projectRoot . 'vendor' . DIRECTORY_SEPARATOR) === 0)
121
            {
122
                unset($stackTrace[$index]);
123
            }
124
            else
125
            {
126
                $vendorExcluded = true;
127
            }
128
        }
129
130
        return $this->stackTraceToArray($stackTrace);
131
    }
132
133
    /**
134
     * Serialize stack trace to array
135
     *
136
     * @param array $stackTrace
137
     * @return array
138
     */
139
    public function stackTraceToArray(array $stackTrace, $topFile = null, $topLine = null)
140
    {
141
        $stack = [];
142
        $counter = 0;
143
144
        foreach ($stackTrace as $trace)
145
        {
146
            // Exception object `getTrace` does not return file and line number for the first line
147
            // http://php.net/manual/en/exception.gettrace.php#107563
148
            if (isset($topFile, $topLine) && $topFile && $topLine)
149
            {
150
                $trace['file'] = $topFile;
151
                $trace['line'] = $topLine;
152
153
                unset($topFile, $topLine);
154
            }
155
156
            $type = $this->stackTraceCallToString($trace);
157
            $args = $this->stackTraceArgsToArray($trace);
158
159
            $stack[] = [
160
                'class' => isset($trace['class']) ? $trace['class'] : null,
161
                'function' => isset($trace['function']) ? $trace['function'] : null,
162
                'args' => $args,
163
                'type' => $type,
164
                'file' => $this->getStackTraceFile($trace),
165
                'line' => $this->getStackTraceLine($trace),
166
                'code' => $this->getCode($this->getStackTraceFile($trace), $this->getStackTraceLine($trace)),
167
            ];
168
169
            $counter++;
170
171
            if ($counter >= $this->stackTraceLimit)
172
            {
173
                break;
174
            }
175
        }
176
177
        return $stack;
178
    }
179
180
    /**
181
     * @param $relativePath
182
     * @param $line
183
     * @param int $linesAround
184
     * @return array|void
185
     */
186
    public function getCode($relativePath, $line, $linesAround = 6)
187
    {
188
        if ( ! $relativePath || ! $line)
189
        {
190
            return;
191
        }
192
193
        $filePath = $this->projectRoot . $relativePath;
194
195
        try
196
        {
197
            $file = new SplFileObject($filePath);
198
            $file->setMaxLineLen(250);
199
            $file->seek(PHP_INT_MAX);
200
            $codeLines = [];
201
202
            $from = max(0, $line - $linesAround - 2);
203
            $to = min($line + $linesAround -1, $file->key() + 1);
204
205
            $file->seek($from);
206
207
            while ($file->key() < $to && ! $file->eof())
208
            {
209
                $file->next();
210
                // `key()` returns 0 as the first line
211
                $codeLines[] = [
212
                    'line' => $file->key() + 1,
213
                    'code' => rtrim($file->current())
214
                ];
215
            }
216
217
            return $codeLines;
218
        }
219
        catch (\Exception $e)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
220
        {}
221
    }
222
223
    /**
224
     * Return stack trace line number
225
     *
226
     * @param array $trace
227
     * @return mixed
228
     */
229
    protected function getStackTraceLine(array $trace)
230
    {
231
        if (isset($trace['line']))
232
        {
233
            return $trace['line'];
234
        }
235
    }
236
237
    /**
238
     * Return stack trace file
239
     *
240
     * @param array $trace
241
     * @return mixed
242
     */
243
    protected function getStackTraceFile(array $trace)
244
    {
245
        if (isset($trace['file']))
246
        {
247
            return $this->removeProjectRoot($trace['file']);
248
        }
249
    }
250
251
    /**
252
     * Return call type
253
     *
254
     * @param array $trace
255
     * @return string
256
     */
257
    protected function stackTraceCallToString(array $trace)
258
    {
259
        if (! isset($trace['type']))
260
        {
261
            return 'function';
262
        }
263
264
        if ($trace['type'] == '::')
265
        {
266
            return 'static';
267
        }
268
269
        if ($trace['type'] == '->')
270
        {
271
            return 'method';
272
        }
273
    }
274
275
    /**
276
     * Serialize stack trace function arguments
277
     *
278
     * @param array $trace
279
     * @return array
280
     */
281
    protected function stackTraceArgsToArray(array $trace)
282
    {
283
        $params = [];
284
285
        if (! isset($trace['args']))
286
        {
287
            return $params;
288
        }
289
290
        foreach ($trace['args'] as $arg)
291
        {
292
            if (is_array($arg))
293
            {
294
                $params[] = 'array(' . count($arg) . ')';
295
            }
296
            else if (is_object($arg))
297
            {
298
                $params[] = get_class($arg);
299
            }
300
            else if (is_string($arg))
301
            {
302
                $params[] = 'string(' . $arg . ')';
303
            }
304
            else if (is_int($arg))
305
            {
306
                $params[] = 'int(' . $arg . ')';
307
            }
308
            else if (is_float($arg))
309
            {
310
                $params[] = 'float(' . $arg . ')';
311
            }
312
            else if (is_bool($arg))
313
            {
314
                $params[] = 'bool(' . ($arg ? 'true' : 'false') . ')';
315
            }
316
            else if ($arg instanceof \__PHP_Incomplete_Class)
317
            {
318
                $params[] = 'object(__PHP_Incomplete_Class)'; 
319
            }
320
            else
321
            {
322
                $params[] = gettype($arg);
323
            }
324
        }
325
326
        return $params;
327
    }
328
329
    /**
330
     * @param $path
331
     * @return string
332
     */
333
    protected function removeProjectRoot($path)
334
    {
335
        if (substr($path, 0, strlen($this->projectRoot)) == $this->projectRoot)
336
        {
337
            return substr($path, strlen($this->projectRoot));
338
        }
339
    }
340
}
341