PhpFileParser::next()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 3
cts 3
cp 1
crap 1
rs 10
1
<?php
2
namespace CodeReview;
3
4
/**
5
 * Splits file source to tokens, provides ways to manipulate tokens list and output modified source.
6
 * Intended to help in code replacements on language syntax level.
7
 */
8
class PhpFileParser implements \Iterator, \ArrayAccess {
9
10
	/**
11
	 * @var string original file name
12
	 */
13
	private $fileName = null;
14
15
	/**
16
	 * @var array
17
	 */
18
	private $tokens = null;
19
20
	/**
21
	 * @var string
22
	 */
23
	private $sha1hash = null;
24
25
	/**
26
	 * @param $fileName
27
	 * @throws \CodeReview\IOException
28
	 * @throws Exception
29
	 */
30 21
	public function __construct($fileName) {
31 21
		$this->validateFilePath($fileName);
32 19
		$this->fileName = $fileName;
33
34 19
		$contents = file_get_contents($fileName);
35 19
		if ($contents === false) {
36
			throw new IOException("Error while fetching contents of file $fileName");
37
		}
38
39 19
		$this->sha1hash = sha1_file($fileName);
40 19
		if ($this->sha1hash === false) {
41
			throw new IOException("Error while computing SHA1 hash of file $fileName");
42
		}
43
44 19
		$this->tokens = token_get_all($contents);
45 19
		if (!is_array($this->tokens)) {
46
			throw new \Exception("Failed to parse PHP contents of $fileName");
47
		}
48 19
		$this->computeNestingParentTokens();
49 19
	}
50
51
	/**
52
	 * Return fileds to serialize.
53
	 *
54
	 * @return array
55
	 */
56 6
	public function __sleep() {
57 6
		return array('fileName', 'sha1hash', 'tokens');
58
	}
59
60
	/**
61
	 * Verify class contents against original file to detect changes.
62
	 */
63 6
	public function __wakeup() {
64 6
		$this->validateFileContents();
65 2
	}
66
67
	/**
68
	 * Uses SHA1 hash to determine if file contents has changed since analysis.
69
	 *
70
	 * @return bool
71
	 * @throws \CodeReview\IOException
72
	 * @throws LogicException
73
	 */
74 6
	protected function validateFileContents() {
75 6
		if (!$this->fileName) {
76 1
			throw new \LogicException("Missing file's path. Looks like severe internal error.");
77
		}
78 5
		$this->validateFilePath($this->fileName);
79 4
		if (!$this->sha1hash) {
80 1
			throw new \LogicException("Missing file's SHA1 hash. Looks like severe internal error.");
81
		}
82 3
		if ($this->sha1hash !== sha1_file($this->fileName)) {
83 1
			throw new IOException("The file on disk has changed and this " . get_class($this) . " class instance is no longer valid for use. Please create fresh instance.");
84
		}
85 2
		return true;
86
	}
87
88
	/**
89
	 * Checks if file exists and is readable.
90
	 *
91
	 * @param $fileName
92
	 * @return bool
93
	 * @throws \CodeReview\IOException
94
	 */
95 21
	protected function validateFilePath($fileName) {
96 21
		if (!file_exists($fileName)) {
97 2
			throw new IOException("File $fileName does not exists");
98
		}
99 20
		if (!is_file($fileName)) {
100 1
			throw new IOException("$fileName must be a file");
101
		}
102 19
		if (!is_readable($fileName)) {
103
			throw new IOException("File $fileName is not readable");
104
		}
105 19
		return true;
106
	}
107
108
	/**
109
	 * Compute parents of the tokens to easily determine containing methods and classes.
110
	 *
111
	 * @param bool $debug
112
	 */
113 19
	private function computeNestingParentTokens($debug = false) {
114 19
		$nesting = 0;
115 19
		$parents = array();
116 19
		$lastParent = null;
117 19
		foreach ($this->tokens as $key => $token) {
118 19
			if (is_array($token)) {
119
				//add info about parent to array
120 19
				$parent = $parents ? $parents[count($parents)-1] : null;
121 19
				$this->tokens[$key][3] = $parent;
122 19
				$this->tokens[$key][4] = $nesting;
123
124
				//is current token possible parent in current level?
125 19
				if ($this->isEqualToAnyToken(array(T_CLASS, T_INTERFACE, T_FUNCTION), $key)) {
126 19
					$lastParent = $key + 2;
127 19
				} elseif ($this->isEqualToAnyToken(array(T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES), $key)) {
128 7
					$nesting++;
129 7
					array_push($parents, '');//just a placeholder
130 7
					if ($debug) {
131
						echo "$nesting\{\$\n";
132
					}
133 7
				}
134 19
			} else {
135 19
				if ($token == '{') {
136 19
					$nesting++;
137 19
					if ($debug) {
138
						echo "$nesting{\n";
139
					}
140 19
					array_push($parents, $lastParent);
141 19
				} elseif ($token == '}') {
142 19
					if ($debug) {
143
						echo "$nesting}\n";
144
					}
145 19
					$nesting--;
146 19
					array_pop($parents);
147 19
				}
148
			}
149 19
		}
150 19
	}
151
152
	/**
153
	 * @param array $tokens
154
	 * @param int   $offset
155
	 * @return bool
156
	 */
157 19
	public function isEqualToAnyToken($tokens, $offset = null) {
158 19
		foreach ($tokens as $token) {
159 19
			if ($this->isEqualToToken($token, $offset)) {
160 19
				return true;
161
			}
162 19
		}
163 19
		return false;
164
	}
165
166
	/**
167
	 * @param $token string|int individual token identifier or predefined T_* constant value for complex tokens
168
	 * @param int $offset optional offset when checking other than current
169
	 * @return bool
170
	 */
171 19
	public function isEqualToToken($token, $offset = null) {
172 19
		if ($offset === null) {
173
			$offset = $this->key();
174
		}
175 19
		if (!isset($this[$offset])) {
176
			return false;
177
		}
178 19
		$val = $this[$offset];
179 19
		if (is_string($token)) {
180
			//assume one char token that gets passed directly as string
181
			return $val == $token;
182
		}
183 19
		return is_array($val) && $val[0] == $token;
184
	}
185
186
	/**
187
	 * @param int $offset optional offset when checking other than current
188
	 * @return mixed
189
	 */
190 7
	public function getDefiningFunctionName($offset = null) {
191 7
		if ($offset === null) {
192
			$offset = $this->key();
193
		}
194 7
		$parentKey = $this->tokens[$offset][3];
195 7
		while ($parentKey !== null && !$this->isEqualToToken(T_FUNCTION, $parentKey - 2)) {
196
			$parentKey = $this->tokens[$parentKey][3];
197
		}
198 7
		if ($parentKey !== null) {
199 7
			$class = $this->getDefiningClassName($parentKey);
200 7
			if ($class) {
201 5
				return $class . '::' . $this->tokens[$parentKey][1];
202
			} else {
203 5
				return $this->tokens[$parentKey][1];
204
			}
205
		}
206
		return null;
207
	}
208
209
	/**
210
	 * @param int $offset optional offset when checking other than current
211
	 * @return mixed
212
	 */
213 7
	public function getDefiningClassName($offset = null) {
214 7
		if ($offset === null) {
215
			$offset = $this->key();
216
		}
217 7
		$parentKey = $this->tokens[$offset][3];
218 7
		while ($parentKey !== null && !$this->isEqualToToken(T_CLASS, $parentKey - 2)) {
219
			$parentKey = $this->tokens[$parentKey][3];
220
		}
221 7
		if ($parentKey !== null) {
222 5
			return $this->tokens[$parentKey][1];
223
		}
224 5
		return null;
225
	}
226
227
	/**
228
	 * @param string $fileName
229
	 * @return bool|string
230
	 * @throws \CodeReview\IOException
231
	 */
232 1
	public function exportPhp($fileName = null) {
233 1
		$source = '';
234 1
		$data = $this->tokens;
235 1
		reset($data);
236 1
		foreach ($data as $val) {
237 1
			if (is_array($val)) {
238 1
				$source .= $val[1];
239 1
			} else {
240 1
				$source .= $val;
241
			}
242 1
		}
243
244 1
		if ($fileName !== null) {
245
			if (!is_writable($fileName)) {
246
				throw new IOException("$fileName must be writable");
247
			}
248
			return file_put_contents($fileName, $source) !== false;
249
		} else {
250 1
			return $source;
251
		}
252
	}
253
254
	/**
255
	 * (PHP 5 &gt;= 5.0.0)<br/>
256
	 * Return the current element
257
	 *
258
	 * @link http://php.net/manual/en/iterator.current.php
259
	 * @return mixed Can return any type.
260
	 */
261 12
	public function current() {
262 12
		return current($this->tokens);
263
	}
264
265
	/**
266
	 * (PHP 5 &gt;= 5.0.0)<br/>
267
	 * Move forward to next element
268
	 * @link http://php.net/manual/en/iterator.next.php
269
	 * @return void Any returned value is ignored.
270
	 */
271 12
	public function next() {
272 12
		next($this->tokens);
273 12
	}
274
275
	/**
276
	 * (PHP 5 &gt;= 5.0.0)<br/>
277
	 * Return the key of the current element
278
	 * @link http://php.net/manual/en/iterator.key.php
279
	 * @return mixed scalar on success, or null on failure.
280
	 */
281 12
	public function key() {
282 12
		return key($this->tokens);
283
	}
284
285
	/**
286
	 * (PHP 5 &gt;= 5.0.0)<br/>
287
	 * Checks if current position is valid
288
	 * @link http://php.net/manual/en/iterator.valid.php
289
	 * @return boolean The return value will be casted to boolean and then evaluated.
290
	 *       Returns true on success or false on failure.
291
	 */
292 12
	public function valid() {
293 12
		$key = key($this->tokens);
294 12
		$var = ($key !== null && $key !== false);
295 12
		return $var;
296
	}
297
298
	/**
299
	 * (PHP 5 &gt;= 5.0.0)<br/>
300
	 * Rewind the Iterator to the first element
301
	 * @link http://php.net/manual/en/iterator.rewind.php
302
	 * @return void Any returned value is ignored.
303
	 */
304 12
	public function rewind() {
305 12
		reset($this->tokens);
306 12
	}
307
308
	/**
309
	 * (PHP 5 &gt;= 5.0.0)<br/>
310
	 * Whether a offset exists
311
	 * @link http://php.net/manual/en/arrayaccess.offsetexists.php
312
	 * @param mixed $offset <p>
313
	 *                      An offset to check for.
314
	 * </p>
315
	 * @return boolean true on success or false on failure.
316
	 * </p>
317
	 * <p>
318
	 *       The return value will be casted to boolean if non-boolean was returned.
319
	 */
320 19
	public function offsetExists($offset) {
321 19
		return isset($this->tokens[$offset]);
322
	}
323
324
	/**
325
	 * (PHP 5 &gt;= 5.0.0)<br/>
326
	 * Offset to retrieve
327
	 * @link http://php.net/manual/en/arrayaccess.offsetget.php
328
	 * @param mixed $offset <p>
329
	 *                      The offset to retrieve.
330
	 * </p>
331
	 * @return mixed Can return all value types.
332
	 */
333 19
	public function offsetGet($offset) {
334 19
		return $this->tokens[$offset];
335
	}
336
337
	/**
338
	 * (PHP 5 &gt;= 5.0.0)<br/>
339
	 * Offset to set
340
	 * @link http://php.net/manual/en/arrayaccess.offsetset.php
341
	 * @param mixed $offset <p>
342
	 *                      The offset to assign the value to.
343
	 * </p>
344
	 * @param mixed $value  <p>
345
	 *                      The value to set.
346
	 * </p>
347
	 * @return void
348
	 */
349
	public function offsetSet($offset, $value) {
350
		$this->tokens[$offset] = $value;
351
	}
352
353
	/**
354
	 * (PHP 5 &gt;= 5.0.0)<br/>
355
	 * Offset to unset
356
	 * @link http://php.net/manual/en/arrayaccess.offsetunset.php
357
	 * @param mixed $offset <p>
358
	 *                      The offset to unset.
359
	 * </p>
360
	 * @return void
361
	 */
362
	public function offsetUnset($offset) {
363
		unset($this->tokens[$offset]);
364
	}
365
}