PhpFileParser::computeNestingParentTokens()   C
last analyzed

Complexity

Conditions 11
Paths 14

Size

Total Lines 38
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 11.6653

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 27
c 1
b 0
f 0
nc 14
nop 1
dl 0
loc 38
ccs 28
cts 34
cp 0.8235
crap 11.6653
rs 5.2653

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
}