Completed
Pull Request — v1 (#450)
by Denis
05:48
created

Frame::getRawFrame()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
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 InvalidArgumentException;
10
use Serializable;
11
12
class Frame implements Serializable
13
{
14
    /**
15
     * @var array
16
     */
17
    protected $frame;
18
19
    /**
20
     * @var string
21
     */
22
    protected $fileContentsCache;
23
24
    /**
25
     * @var array[]
26
     */
27
    protected $comments = [];
28
29
    /**
30
     * @param array[]
31
     */
32 1
    public function __construct(array $frame)
33
    {
34 1
        $this->frame = $frame;
35 1
    }
36
37
    /**
38
     * @param  bool        $shortened
39
     * @return string|null
40
     */
41 2
    public function getFile($shortened = false)
42
    {
43 2
        if (empty($this->frame['file'])) {
44
            return null;
45
        }
46
47 2
        $file = $this->frame['file'];
48
49
        // Check if this frame occurred within an eval().
50
        // @todo: This can be made more reliable by checking if we've entered
51
        // eval() in a previous trace, but will need some more work on the upper
52
        // trace collector(s).
53 2
        if (preg_match('/^(.*)\((\d+)\) : (?:eval\(\)\'d|assert) code$/', $file, $matches)) {
54
            $file = $this->frame['file'] = $matches[1];
55
            $this->frame['line'] = (int) $matches[2];
56
        }
57
58 2
        if ($shortened && is_string($file)) {
59
            // Replace the part of the path that all frames have in common, and add 'soft hyphens' for smoother line-breaks.
60
            $dirname = dirname(dirname(dirname(dirname(dirname(dirname(__DIR__))))));
61
            if ($dirname !== '/') {
62
                $file = str_replace($dirname, "&hellip;", $file);
63
            }
64
            $file = str_replace("/", "/&shy;", $file);
65
        }
66
67 2
        return $file;
68
    }
69
70
    /**
71
     * @return int|null
72
     */
73 2
    public function getLine()
74
    {
75 2
        return isset($this->frame['line']) ? $this->frame['line'] : null;
76
    }
77
78
    /**
79
     * @return string|null
80
     */
81 2
    public function getClass()
82
    {
83 2
        return isset($this->frame['class']) ? $this->frame['class'] : null;
84
    }
85
86
    /**
87
     * @return string|null
88
     */
89 2
    public function getFunction()
90
    {
91 2
        return isset($this->frame['function']) ? $this->frame['function'] : null;
92
    }
93
94
    /**
95
     * @return array
96
     */
97 1
    public function getArgs()
98
    {
99 1
        return isset($this->frame['args']) ? (array) $this->frame['args'] : [];
100
    }
101
102
    /**
103
     * Returns the full contents of the file for this frame,
104
     * if it's known.
105
     * @return string|null
106
     */
107 1
    public function getFileContents()
108
    {
109 1
        if ($this->fileContentsCache === null && $filePath = $this->getFile()) {
110
            // Leave the stage early when 'Unknown' is passed
111
            // this would otherwise raise an exception when
112
            // open_basedir is enabled.
113 1
            if ($filePath === "Unknown") {
114
                return null;
115
            }
116
117
            // Return null if the file doesn't actually exist.
118 1
            if (!is_file($filePath)) {
119
                return null;
120
            }
121
122 1
            $this->fileContentsCache = file_get_contents($filePath);
123 1
        }
124
125 1
        return $this->fileContentsCache;
126
    }
127
128
    /**
129
     * Adds a comment to this frame, that can be received and
130
     * used by other handlers. For example, the PrettyPage handler
131
     * can attach these comments under the code for each frame.
132
     *
133
     * An interesting use for this would be, for example, code analysis
134
     * & annotations.
135
     *
136
     * @param string $comment
137
     * @param string $context Optional string identifying the origin of the comment
138
     */
139 2
    public function addComment($comment, $context = 'global')
140
    {
141 2
        $this->comments[] = [
142 2
            'comment' => $comment,
143 2
            'context' => $context,
144
        ];
145 2
    }
146
147
    /**
148
     * Returns all comments for this frame. Optionally allows
149
     * a filter to only retrieve comments from a specific
150
     * context.
151
     *
152
     * @param  string  $filter
153
     * @return array[]
154
     */
155 2
    public function getComments($filter = null)
156
    {
157 2
        $comments = $this->comments;
158
159 2
        if ($filter !== null) {
160 1
            $comments = array_filter($comments, function ($c) use ($filter) {
161 1
                return $c['context'] == $filter;
162 1
            });
163 1
        }
164
165 2
        return $comments;
166
    }
167
168
    /**
169
     * Returns the array containing the raw frame data from which
170
     * this Frame object was built
171
     *
172
     * @return array
173
     */
174
    public function getRawFrame()
175
    {
176
        return $this->frame;
177
    }
178
179
    /**
180
     * Returns the contents of the file for this frame as an
181
     * array of lines, and optionally as a clamped range of lines.
182
     *
183
     * NOTE: lines are 0-indexed
184
     *
185
     * @example
186
     *     Get all lines for this file
187
     *     $frame->getFileLines(); // => array( 0 => '<?php', 1 => '...', ...)
188
     * @example
189
     *     Get one line for this file, starting at line 10 (zero-indexed, remember!)
190
     *     $frame->getFileLines(9, 1); // array( 10 => '...', 11 => '...')
191
     *
192
     * @throws InvalidArgumentException if $length is less than or equal to 0
193
     * @param  int                      $start
194
     * @param  int                      $length
195
     * @return string[]|null
196
     */
197 2
    public function getFileLines($start = 0, $length = null)
198
    {
199 2
        if (null !== ($contents = $this->getFileContents())) {
200 2
            $lines = explode("\n", $contents);
201
202
            // Get a subset of lines from $start to $end
203 2
            if ($length !== null) {
204 1
                $start  = (int) $start;
205 1
                $length = (int) $length;
206 1
                if ($start < 0) {
207
                    $start = 0;
208
                }
209
210 1
                if ($length <= 0) {
211
                    throw new InvalidArgumentException(
212
                        "\$length($length) cannot be lower or equal to 0"
213
                    );
214
                }
215
216 1
                $lines = array_slice($lines, $start, $length, true);
217 1
            }
218
219 2
            return $lines;
220
        }
221
    }
222
223
    /**
224
     * Implements the Serializable interface, with special
225
     * steps to also save the existing comments.
226
     *
227
     * @see Serializable::serialize
228
     * @return string
229
     */
230 1
    public function serialize()
231
    {
232 1
        $frame = $this->frame;
233 1
        if (!empty($this->comments)) {
234 1
            $frame['_comments'] = $this->comments;
235 1
        }
236
237 1
        return serialize($frame);
238
    }
239
240
    /**
241
     * Unserializes the frame data, while also preserving
242
     * any existing comment data.
243
     *
244
     * @see Serializable::unserialize
245
     * @param string $serializedFrame
246
     */
247 1
    public function unserialize($serializedFrame)
248
    {
249 1
        $frame = unserialize($serializedFrame);
250
251 1
        if (!empty($frame['_comments'])) {
252 1
            $this->comments = $frame['_comments'];
253 1
            unset($frame['_comments']);
254 1
        }
255
256 1
        $this->frame = $frame;
257 1
    }
258
259
    /**
260
     * Compares Frame against one another
261
     * @param  Frame $frame
262
     * @return bool
263
     */
264 1
    public function equals(Frame $frame)
265
    {
266 1
        if (!$this->getFile() || $this->getFile() === 'Unknown' || !$this->getLine()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getFile() of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->getLine() of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
267
            return false;
268
        }
269 1
        return $frame->getFile() === $this->getFile() && $frame->getLine() === $this->getLine();
270
    }
271
}
272