Completed
Push — master ( 951a34...0736c8 )
by Alexander
02:55
created

ClassChecker   C

Complexity

Total Complexity 58

Size/Duplication

Total Lines 480
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 58
lcom 1
cbo 3
dl 0
loc 480
ccs 205
cts 205
cp 1
rs 6.3005
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
C doCheck() 0 29 7
B processConstants() 0 23 4
B getConstantsRecursively() 0 27 5
B processProperties() 0 29 4
C getPropertiesRecursively() 0 30 7
A processProperty() 0 16 2
A getName() 0 4 1
C processMethods() 0 49 8
C getMethodsRecursively() 0 30 7
A getMethodParameterSignature() 0 16 2
C processMethod() 0 33 7
A getScopeName() 0 10 1
A coveredScopes() 0 4 1
A getClassRelations() 0 17 2

How to fix   Complexity   

Complex Class

Complex classes like ClassChecker often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ClassChecker, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the Code-Insight library.
4
 * For the full copyright and license information, please view
5
 * the LICENSE file that was distributed with this source code.
6
 *
7
 * @copyright Alexander Obuhovich <[email protected]>
8
 * @link      https://github.com/console-helpers/code-insight
9
 */
10
11
namespace ConsoleHelpers\CodeInsight\BackwardsCompatibility\Checker;
12
13
14
use Aura\Sql\ExtendedPdoInterface;
15
use ConsoleHelpers\CodeInsight\KnowledgeBase\DataCollector\ClassDataCollector;
16
17
class ClassChecker extends AbstractChecker
18
{
19
20
	const CACHE_DURATION = 3600;
21
22
	const TYPE_CLASS_DELETED = 'class.deleted';
23
	const TYPE_CLASS_MADE_ABSTRACT = 'class.made_abstract';
24
	const TYPE_CLASS_MADE_FINAL = 'class.made_final';
25
	const TYPE_CLASS_CONSTANT_DELETED = 'class.constant.deleted';
26
	const TYPE_PROPERTY_DELETED = 'property.deleted';
27
	const TYPE_PROPERTY_SCOPE_REDUCED = 'property.scope_reduced';
28
	const TYPE_METHOD_DELETED = 'method.deleted';
29
	const TYPE_METHOD_MADE_ABSTRACT = 'method.made_abstract';
30
	const TYPE_METHOD_MADE_FINAL = 'method.made_final';
31
	const TYPE_METHOD_SCOPE_REDUCED = 'method.scope_reduced';
32
	const TYPE_METHOD_SIGNATURE_CHANGED = 'method.signature_changed';
33
34
	/**
35
	 * Source class data.
36
	 *
37
	 * @var array
38
	 */
39
	protected $sourceClassData = array();
40
41
	/**
42
	 * Target class data.
43
	 *
44
	 * @var array
45
	 */
46
	protected $targetClassData = array();
47
48
	/**
49
	 * Source property data.
50
	 *
51
	 * @var array
52
	 */
53
	protected $sourcePropertyData = array();
54
55
	/**
56
	 * Target property data.
57
	 *
58
	 * @var array
59
	 */
60
	protected $targetPropertyData = array();
61
62
	/**
63
	 * Source method data.
64
	 *
65
	 * @var array
66
	 */
67
	protected $sourceMethodData = array();
68
69
	/**
70
	 * Target method data.
71
	 *
72
	 * @var array
73
	 */
74
	protected $targetMethodData = array();
75
76
	/**
77
	 * Returns backwards compatibility checker name.
78
	 *
79
	 * @return string
80
	 */
81 1
	public function getName()
82
	{
83 1
		return 'class';
84
	}
85
86
	/**
87
	 * Collects backwards compatibility violations.
88
	 *
89
	 * @return void
90
	 */
91 4
	protected function doCheck()
92
	{
93
		$classes_sql = 'SELECT Name, Id, IsAbstract, IsFinal
94 4
						FROM Classes';
95 4
		$source_classes = $this->sourceDatabase->fetchAssoc($classes_sql);
96 4
		$target_classes = $this->targetDatabase->fetchAssoc($classes_sql);
97
98 4
		foreach ( $source_classes as $class_name => $source_class_data ) {
99 4
			if ( !isset($target_classes[$class_name]) ) {
100 2
				$this->addIncident(self::TYPE_CLASS_DELETED, $class_name);
101 2
				continue;
102
			}
103
104 4
			$this->sourceClassData = $source_class_data;
105 4
			$this->targetClassData = $target_classes[$class_name];
106
107 4
			if ( !$this->sourceClassData['IsAbstract'] && $this->targetClassData['IsAbstract'] ) {
108 2
				$this->addIncident(self::TYPE_CLASS_MADE_ABSTRACT, $class_name);
109 2
			}
110
111 4
			if ( !$this->sourceClassData['IsFinal'] && $this->targetClassData['IsFinal'] ) {
112 2
				$this->addIncident(self::TYPE_CLASS_MADE_FINAL, $class_name);
113 2
			}
114
115 4
			$this->processConstants();
116 4
			$this->processProperties();
117 4
			$this->processMethods();
118 4
		}
119 4
	}
120
121
	/**
122
	 * Checks constants.
123
	 *
124
	 * @return void
125
	 */
126 4
	protected function processConstants()
127
	{
128 4
		$class_name = $this->sourceClassData['Name'];
129
130 4
		$source_constants = $this->getConstantsRecursively($this->sourceDatabase, $this->sourceClassData['Id']);
131 4
		$target_constants = $this->getConstantsRecursively($this->targetDatabase, $this->targetClassData['Id']);
132
133 4
		foreach ( $source_constants as $source_constant_name => $source_constant_data ) {
134 4
			$full_constant_name = $class_name . '::' . $source_constant_name;
135
136
			// @codeCoverageIgnoreStart
137
			// Report incidents for processed (not inherited) constants only.
138
			if ( $source_constant_data['ClassId'] !== $this->sourceClassData['Id'] ) {
139
				continue;
140
			}
141
			// @codeCoverageIgnoreEnd
142
143 4
			if ( !isset($target_constants[$source_constant_name]) ) {
144 2
				$this->addIncident(self::TYPE_CLASS_CONSTANT_DELETED, $full_constant_name);
145 2
				continue;
146
			}
147 4
		}
148 4
	}
149
150
	/**
151
	 * Returns class constants.
152
	 *
153
	 * @param ExtendedPdoInterface $db       Database.
154
	 * @param integer              $class_id Class ID.
155
	 *
156
	 * @return array
157
	 */
158 4
	protected function getConstantsRecursively(ExtendedPdoInterface $db, $class_id)
159
	{
160 4
		$cache_key = $this->getCacheKey($db, 'class_constants[' . $class_id . ']');
161 4
		$cached_value = $this->cache->fetch($cache_key);
162
163 4
		if ( $cached_value === false ) {
164
			$sql = 'SELECT Name, ClassId
165
					FROM ClassConstants
166 4
					WHERE ClassId = :class_id';
167 4
			$cached_value = $db->fetchAssoc($sql, array('class_id' => $class_id));
168
169 4
			foreach ( $this->getClassRelations($db, $class_id) as $related_class_id => $related_class_name ) {
170 4
				foreach ( $this->getConstantsRecursively($db, $related_class_id) as $name => $data ) {
171
					// @codeCoverageIgnoreStart
172
					if ( !array_key_exists($name, $cached_value) ) {
173
						$cached_value[$name] = $data;
174
					}
175
					// @codeCoverageIgnoreEnd
176 4
				}
177 4
			}
178
179
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
180 4
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
181 4
		}
182
183 4
		return $cached_value;
184
	}
185
186
	/**
187
	 * Checks properties.
188
	 *
189
	 * @return void
190
	 */
191 4
	protected function processProperties()
192
	{
193 4
		$class_name = $this->sourceClassData['Name'];
194 4
		$source_properties = $this->getPropertiesRecursively(
195 4
			$this->sourceDatabase,
196 4
			$this->sourceClassData['Id'],
197 4
			$this->coveredScopes()
198 4
		);
199 4
		$target_properties = $this->getPropertiesRecursively($this->targetDatabase, $this->targetClassData['Id'], '');
200
201 4
		foreach ( $source_properties as $source_property_name => $source_property_data ) {
202 4
			$full_property_name = $class_name . '::$' . $source_property_name;
203
204
			// Report incidents for processed (not inherited) properties only.
205 4
			if ( $source_property_data['ClassId'] !== $this->sourceClassData['Id'] ) {
206 4
				continue;
207
			}
208
209 4
			if ( !isset($target_properties[$source_property_name]) ) {
210 2
				$this->addIncident(self::TYPE_PROPERTY_DELETED, $full_property_name);
211 2
				continue;
212
			}
213
214 4
			$this->sourcePropertyData = $source_property_data;
215 4
			$this->targetPropertyData = $target_properties[$source_property_name];
216
217 4
			$this->processProperty();
218 4
		}
219 4
	}
220
221
	/**
222
	 * Returns class properties.
223
	 *
224
	 * @param ExtendedPdoInterface $db       Database.
225
	 * @param integer              $class_id Class ID.
226
	 * @param string               $scopes   Scopes.
227
	 *
228
	 * @return array
229
	 */
230 4
	protected function getPropertiesRecursively(ExtendedPdoInterface $db, $class_id, $scopes)
231
	{
232 4
		$cache_key = $this->getCacheKey($db, 'class_properties[' . $class_id . ']_scopes[' . ($scopes ?: '*') . ']');
233 4
		$cached_value = $this->cache->fetch($cache_key);
234
235 4
		if ( $cached_value === false ) {
236
			$sql = 'SELECT Name, Scope, ClassId
237
					FROM ClassProperties
238 4
					WHERE ClassId = :class_id';
239
240 4
			if ( $scopes ) {
241 4
				$sql .= ' AND Scope IN (' . $scopes . ')';
242 4
			}
243
244 4
			$cached_value = $db->fetchAssoc($sql, array('class_id' => $class_id));
245
246 4
			foreach ( $this->getClassRelations($db, $class_id) as $related_class_id => $related_class_name ) {
247 4
				foreach ( $this->getPropertiesRecursively($db, $related_class_id, $scopes) as $name => $data ) {
248 4
					if ( !array_key_exists($name, $cached_value) ) {
249 4
						$cached_value[$name] = $data;
250 4
					}
251 4
				}
252 4
			}
253
254
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
255 4
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
256 4
		}
257
258 4
		return $cached_value;
259
	}
260
261
	/**
262
	 * Processes property.
263
	 *
264
	 * @return void
265
	 */
266 4
	protected function processProperty()
267
	{
268 4
		$class_name = $this->sourceClassData['Name'];
269 4
		$property_name = $this->sourcePropertyData['Name'];
270
271 4
		$full_property_name = $class_name . '::$' . $property_name;
272
273 4
		if ( $this->sourcePropertyData['Scope'] > $this->targetPropertyData['Scope'] ) {
274 2
			$this->addIncident(
275 2
				self::TYPE_PROPERTY_SCOPE_REDUCED,
276 2
				$full_property_name,
277 2
				$this->getScopeName($this->sourcePropertyData['Scope']),
278 2
				$this->getScopeName($this->targetPropertyData['Scope'])
279 2
			);
280 2
		}
281 4
	}
282
283
	/**
284
	 * Checks methods.
285
	 *
286
	 * @return void
287
	 */
288 4
	protected function processMethods()
289
	{
290 4
		$class_name = $this->sourceClassData['Name'];
291 4
		$source_methods = $this->getMethodsRecursively(
292 4
			$this->sourceDatabase,
293 4
			$this->sourceClassData['Id'],
294 4
			$this->coveredScopes()
295 4
		);
296 4
		$target_methods = $this->getMethodsRecursively($this->targetDatabase, $this->targetClassData['Id'], '');
297
298 4
		foreach ( $source_methods as $source_method_name => $source_method_data ) {
299 4
			$target_method_name = $source_method_name;
300 4
			$full_method_name = $class_name . '::' . $source_method_name;
301
302
			// Ignore PHP4 constructor rename into PHP5 constructor.
303 4
			if ( !isset($target_methods[$target_method_name]) && $target_method_name === $class_name ) {
304 2
				$target_method_name = '__construct';
305 2
			}
306
307
			// Ignore PHP5 constructor rename into PHP4 constructor.
308 4
			if ( !isset($target_methods[$target_method_name]) && $target_method_name === '__construct' ) {
309 2
				$target_method_name = $class_name;
310 2
			}
311
312
			// Report incidents for processed (not inherited) methods only.
313 4
			if ( $source_method_data['ClassId'] !== $this->sourceClassData['Id'] ) {
314 4
				continue;
315
			}
316
317 4
			if ( !isset($target_methods[$target_method_name]) ) {
318 2
				$this->addIncident(self::TYPE_METHOD_DELETED, $full_method_name);
319 2
				continue;
320
			}
321
322 4
			$this->sourceMethodData = $source_method_data;
323 4
			$this->sourceMethodData['ParameterSignature'] = $this->getMethodParameterSignature(
324 4
				$this->sourceDatabase,
325 4
				$this->sourceMethodData['Id']
326 4
			);
327
328 4
			$this->targetMethodData = $target_methods[$target_method_name];
329 4
			$this->targetMethodData['ParameterSignature'] = $this->getMethodParameterSignature(
330 4
				$this->targetDatabase,
331 4
				$this->targetMethodData['Id']
332 4
			);
333
334 4
			$this->processMethod();
335 4
		}
336 4
	}
337
338
	/**
339
	 * Returns class methods.
340
	 *
341
	 * @param ExtendedPdoInterface $db       Database.
342
	 * @param integer              $class_id Class ID.
343
	 * @param string               $scopes   Scopes.
344
	 *
345
	 * @return array
346
	 */
347 4
	protected function getMethodsRecursively(ExtendedPdoInterface $db, $class_id, $scopes)
348
	{
349 4
		$cache_key = $this->getCacheKey($db, 'class_methods[' . $class_id . ']_scopes[' . ($scopes ?: '*') . ']');
350 4
		$cached_value = $this->cache->fetch($cache_key);
351
352 4
		if ( $cached_value === false ) {
353
			$sql = 'SELECT Name, Id, Scope, IsAbstract, IsFinal, ClassId
354
					FROM ClassMethods
355 4
					WHERE ClassId = :class_id';
356
357 4
			if ( $scopes ) {
358 4
				$sql .= ' AND Scope IN (' . $scopes . ')';
359 4
			}
360
361 4
			$cached_value = $db->fetchAssoc($sql, array('class_id' => $class_id));
362
363 4
			foreach ( $this->getClassRelations($db, $class_id) as $related_class_id => $related_class_name ) {
364 4
				foreach ( $this->getMethodsRecursively($db, $related_class_id, $scopes) as $name => $data ) {
365 4
					if ( !array_key_exists($name, $cached_value) ) {
366 4
						$cached_value[$name] = $data;
367 4
					}
368 4
				}
369 4
			}
370
371
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
372 4
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
373 4
		}
374
375 4
		return $cached_value;
376
	}
377
378
	/**
379
	 * Calculates method parameter signature.
380
	 *
381
	 * @param ExtendedPdoInterface $db        Database.
382
	 * @param integer              $method_id Method ID.
383
	 *
384
	 * @return integer
385
	 */
386 4
	protected function getMethodParameterSignature(ExtendedPdoInterface $db, $method_id)
387
	{
388
		$sql = 'SELECT *
389
				FROM MethodParameters
390
				WHERE MethodId = :method_id
391 4
				ORDER BY Position ASC';
392 4
		$method_parameters = $db->fetchAll($sql, array('method_id' => $method_id));
393
394 4
		$hash_parts = array();
395
396 4
		foreach ( $method_parameters as $method_parameter_data ) {
397 4
			$hash_parts[] = $this->paramToString($method_parameter_data);
398 4
		}
399
400 4
		return implode(', ', $hash_parts);
401
	}
402
403
	/**
404
	 * Processes method.
405
	 *
406
	 * @return void
407
	 */
408 4
	protected function processMethod()
409
	{
410 4
		$class_name = $this->sourceClassData['Name'];
411 4
		$method_name = $this->sourceMethodData['Name'];
412
413 4
		$full_method_name = $class_name . '::' . $method_name;
414
415 4
		if ( !$this->sourceMethodData['IsAbstract'] && $this->targetMethodData['IsAbstract'] ) {
416 2
			$this->addIncident(self::TYPE_METHOD_MADE_ABSTRACT, $full_method_name);
417 2
		}
418
419 4
		if ( !$this->sourceMethodData['IsFinal'] && $this->targetMethodData['IsFinal'] ) {
420 2
			$this->addIncident(self::TYPE_METHOD_MADE_FINAL, $full_method_name);
421 2
		}
422
423 4
		if ( $this->sourceMethodData['ParameterSignature'] !== $this->targetMethodData['ParameterSignature'] ) {
424 2
			$this->addIncident(
425 2
				self::TYPE_METHOD_SIGNATURE_CHANGED,
426 2
				$full_method_name,
427 2
				$this->sourceMethodData['ParameterSignature'],
428 2
				$this->targetMethodData['ParameterSignature']
429 2
			);
430 2
		}
431
432 4
		if ( $this->sourceMethodData['Scope'] > $this->targetMethodData['Scope'] ) {
433 2
			$this->addIncident(
434 2
				self::TYPE_METHOD_SCOPE_REDUCED,
435 2
				$full_method_name,
436 2
				$this->getScopeName($this->sourceMethodData['Scope']),
437 2
				$this->getScopeName($this->targetMethodData['Scope'])
438 2
			);
439 2
		}
440 4
	}
441
442
	/**
443
	 * Returns scope name.
444
	 *
445
	 * @param integer $scope Scope.
446
	 *
447
	 * @return string
448
	 */
449 2
	protected function getScopeName($scope)
450
	{
451
		$mapping = array(
452 2
			ClassDataCollector::SCOPE_PRIVATE => 'private',
453 2
			ClassDataCollector::SCOPE_PROTECTED => 'protected',
454 2
			ClassDataCollector::SCOPE_PUBLIC => 'public',
455 2
		);
456
457 2
		return $mapping[$scope];
458
	}
459
460
	/**
461
	 * Scopes covered by backwards compatibility checks.
462
	 *
463
	 * @return string
464
	 */
465 4
	protected function coveredScopes()
466
	{
467 4
		return ClassDataCollector::SCOPE_PUBLIC . ',' . ClassDataCollector::SCOPE_PROTECTED;
468
	}
469
470
	/**
471
	 * Returns class constants.
472
	 *
473
	 * @param ExtendedPdoInterface $db       Database.
474
	 * @param integer              $class_id Class ID.
475
	 *
476
	 * @return array
477
	 */
478 4
	protected function getClassRelations(ExtendedPdoInterface $db, $class_id)
479
	{
480 4
		$cache_key = $this->getCacheKey($db, 'class_relations[' . $class_id . ']');
481 4
		$cached_value = $this->cache->fetch($cache_key);
482
483 4
		if ( $cached_value === false ) {
484
			$sql = 'SELECT RelatedClassId, RelatedClass
485
					FROM ClassRelations
486 4
					WHERE ClassId = :class_id AND RelatedClassId <> 0';
487 4
			$cached_value = $db->fetchPairs($sql, array('class_id' => $class_id));
488
489
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
490 4
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
491 4
		}
492
493 4
		return $cached_value;
494
	}
495
496
}
497