Completed
Push — master ( b85bec...862a1a )
by Alexander
05:15
created

ClassChecker   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 475
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 56
lcom 1
cbo 3
dl 0
loc 475
ccs 162
cts 162
cp 1
rs 6.5957
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A coveredScopes() 0 4 1
A getName() 0 4 1
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
B processMethods() 0 44 6
C getMethodsRecursively() 0 30 7
A getMethodParameterSignature() 0 16 2
C processMethod() 0 33 7
A getScopeName() 0 10 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
			}
110
111 4
			if ( !$this->sourceClassData['IsFinal'] && $this->targetClassData['IsFinal'] ) {
112 2
				$this->addIncident(self::TYPE_CLASS_MADE_FINAL, $class_name);
113
			}
114
115 4
			$this->processConstants();
116 4
			$this->processProperties();
117 4
			$this->processMethods();
118
		}
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 4
				continue;
146
			}
147
		}
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
				}
177
			}
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
		}
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
		);
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
		}
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
			}
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
					}
251
				}
252
			}
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
		}
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
				$full_property_name,
277 2
				$this->getScopeName($this->sourcePropertyData['Scope']),
278 2
				$this->getScopeName($this->targetPropertyData['Scope'])
279
			);
280
		}
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
		);
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
			}
306
307
			// Report incidents for processed (not inherited) methods only.
308 4
			if ( $source_method_data['ClassId'] !== $this->sourceClassData['Id'] ) {
309 4
				continue;
310
			}
311
312 4
			if ( !isset($target_methods[$target_method_name]) ) {
313 2
				$this->addIncident(self::TYPE_METHOD_DELETED, $full_method_name);
314 2
				continue;
315
			}
316
317 4
			$this->sourceMethodData = $source_method_data;
318 4
			$this->sourceMethodData['ParameterSignature'] = $this->getMethodParameterSignature(
319 4
				$this->sourceDatabase,
320 4
				$this->sourceMethodData['Id']
321
			);
322
323 4
			$this->targetMethodData = $target_methods[$target_method_name];
324 4
			$this->targetMethodData['ParameterSignature'] = $this->getMethodParameterSignature(
325 4
				$this->targetDatabase,
326 4
				$this->targetMethodData['Id']
327
			);
328
329 4
			$this->processMethod();
330
		}
331 4
	}
332
333
	/**
334
	 * Returns class methods.
335
	 *
336
	 * @param ExtendedPdoInterface $db       Database.
337
	 * @param integer              $class_id Class ID.
338
	 * @param string               $scopes   Scopes.
339
	 *
340
	 * @return array
341
	 */
342 4
	protected function getMethodsRecursively(ExtendedPdoInterface $db, $class_id, $scopes)
343
	{
344 4
		$cache_key = $this->getCacheKey($db, 'class_methods[' . $class_id . ']_scopes[' . ($scopes ?: '*') . ']');
345 4
		$cached_value = $this->cache->fetch($cache_key);
346
347 4
		if ( $cached_value === false ) {
348
			$sql = 'SELECT Name, Id, Scope, IsAbstract, IsFinal, ClassId
349
					FROM ClassMethods
350 4
					WHERE ClassId = :class_id';
351
352 4
			if ( $scopes ) {
353 4
				$sql .= ' AND Scope IN (' . $scopes . ')';
354
			}
355
356 4
			$cached_value = $db->fetchAssoc($sql, array('class_id' => $class_id));
357
358 4
			foreach ( $this->getClassRelations($db, $class_id) as $related_class_id => $related_class_name ) {
359 4
				foreach ( $this->getMethodsRecursively($db, $related_class_id, $scopes) as $name => $data ) {
360 4
					if ( !array_key_exists($name, $cached_value) ) {
361 4
						$cached_value[$name] = $data;
362
					}
363
				}
364
			}
365
366
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
367 4
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
368
		}
369
370 4
		return $cached_value;
371
	}
372
373
	/**
374
	 * Calculates method parameter signature.
375
	 *
376
	 * @param ExtendedPdoInterface $db        Database.
377
	 * @param integer              $method_id Method ID.
378
	 *
379
	 * @return integer
380
	 */
381 4
	protected function getMethodParameterSignature(ExtendedPdoInterface $db, $method_id)
382
	{
383
		$sql = 'SELECT *
384
				FROM MethodParameters
385
				WHERE MethodId = :method_id
386 4
				ORDER BY Position ASC';
387 4
		$method_parameters = $db->fetchAll($sql, array('method_id' => $method_id));
388
389 4
		$hash_parts = array();
390
391 4
		foreach ( $method_parameters as $method_parameter_data ) {
392 4
			$hash_parts[] = $this->paramToString($method_parameter_data);
393
		}
394
395 4
		return implode(', ', $hash_parts);
396
	}
397
398
	/**
399
	 * Processes method.
400
	 *
401
	 * @return void
402
	 */
403 4
	protected function processMethod()
404
	{
405 4
		$class_name = $this->sourceClassData['Name'];
406 4
		$method_name = $this->sourceMethodData['Name'];
407
408 4
		$full_method_name = $class_name . '::' . $method_name;
409
410 4
		if ( !$this->sourceMethodData['IsAbstract'] && $this->targetMethodData['IsAbstract'] ) {
411 2
			$this->addIncident(self::TYPE_METHOD_MADE_ABSTRACT, $full_method_name);
412
		}
413
414 4
		if ( !$this->sourceMethodData['IsFinal'] && $this->targetMethodData['IsFinal'] ) {
415 2
			$this->addIncident(self::TYPE_METHOD_MADE_FINAL, $full_method_name);
416
		}
417
418 4
		if ( $this->sourceMethodData['ParameterSignature'] !== $this->targetMethodData['ParameterSignature'] ) {
419 2
			$this->addIncident(
420 2
				self::TYPE_METHOD_SIGNATURE_CHANGED,
421
				$full_method_name,
422 2
				$this->sourceMethodData['ParameterSignature'],
423 2
				$this->targetMethodData['ParameterSignature']
424
			);
425
		}
426
427 4
		if ( $this->sourceMethodData['Scope'] > $this->targetMethodData['Scope'] ) {
428 2
			$this->addIncident(
429 2
				self::TYPE_METHOD_SCOPE_REDUCED,
430
				$full_method_name,
431 2
				$this->getScopeName($this->sourceMethodData['Scope']),
432 2
				$this->getScopeName($this->targetMethodData['Scope'])
433
			);
434
		}
435 4
	}
436
437
	/**
438
	 * Returns scope name.
439
	 *
440
	 * @param integer $scope Scope.
441
	 *
442
	 * @return string
443
	 */
444 2
	protected function getScopeName($scope)
445
	{
446
		$mapping = array(
447 2
			ClassDataCollector::SCOPE_PRIVATE => 'private',
448
			ClassDataCollector::SCOPE_PROTECTED => 'protected',
449
			ClassDataCollector::SCOPE_PUBLIC => 'public',
450
		);
451
452 2
		return $mapping[$scope];
453
	}
454
455
	/**
456
	 * Scopes covered by backwards compatibility checks.
457
	 *
458
	 * @return string
459
	 */
460 4
	protected function coveredScopes()
461
	{
462 4
		return ClassDataCollector::SCOPE_PUBLIC . ',' . ClassDataCollector::SCOPE_PROTECTED;
463
	}
464
465
	/**
466
	 * Returns class constants.
467
	 *
468
	 * @param ExtendedPdoInterface $db       Database.
469
	 * @param integer              $class_id Class ID.
470
	 *
471
	 * @return array
472
	 */
473 4
	protected function getClassRelations(ExtendedPdoInterface $db, $class_id)
474
	{
475 4
		$cache_key = $this->getCacheKey($db, 'class_relations[' . $class_id . ']');
476 4
		$cached_value = $this->cache->fetch($cache_key);
477
478 4
		if ( $cached_value === false ) {
479
			$sql = 'SELECT RelatedClassId, RelatedClass
480
					FROM ClassRelations
481 4
					WHERE ClassId = :class_id AND RelatedClassId <> 0';
482 4
			$cached_value = $db->fetchPairs($sql, array('class_id' => $class_id));
483
484
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
485 4
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
486
		}
487
488 4
		return $cached_value;
489
	}
490
491
}
492