Completed
Push — master ( b6a2ec...b3bd8f )
by Alexander
01:55
created

ClassChecker::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 22
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 1

Importance

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