Failed Conditions
Push — master ( 26605a...607f8d )
by Alexander
01:54
created

ClassChecker::getMethodsRecursively()   C

Complexity

Conditions 7
Paths 3

Size

Total Lines 30
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 0
cts 23
cp 0
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 14
nc 3
nop 3
crap 56
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;
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
	/**
24
	 * Source class data.
25
	 *
26
	 * @var array
27
	 */
28
	protected $sourceClassData = array();
29
30
	/**
31
	 * Target class data.
32
	 *
33
	 * @var array
34
	 */
35
	protected $targetClassData = array();
36
37
	/**
38
	 * Source property data.
39
	 *
40
	 * @var array
41
	 */
42
	protected $sourcePropertyData = array();
43
44
	/**
45
	 * Target property data.
46
	 *
47
	 * @var array
48
	 */
49
	protected $targetPropertyData = array();
50
51
	/**
52
	 * Source method data.
53
	 *
54
	 * @var array
55
	 */
56
	protected $sourceMethodData = array();
57
58
	/**
59
	 * Target method data.
60
	 *
61
	 * @var array
62
	 */
63
	protected $targetMethodData = array();
64
65
	/**
66
	 * ClassChecker constructor.
67
	 *
68
	 * @param CacheProvider $cache Cache provider.
69
	 */
70
	public function __construct(CacheProvider $cache)
71
	{
72
		parent::__construct($cache);
73
74
		$this->defineIncidentGroups(array(
75
			'Class Deleted',
76
			'Class Made Abstract',
77
			'Class Made Final',
78
79
			'Class Constant Deleted',
80
81
			'Property Deleted',
82
			'Property Scope Reduced',
83
84
			'Method Deleted',
85
			'Method Made Abstract',
86
			'Method Made Final',
87
			'Method Scope Reduced',
88
			'Method Signature Changed',
89
		));
90
	}
91
92
	/**
93
	 * Returns backwards compatibility checker name.
94
	 *
95
	 * @return string
96
	 */
97
	public function getName()
98
	{
99
		return 'class';
100
	}
101
102
	/**
103
	 * Collects backwards compatibility violations.
104
	 *
105
	 * @return void
106
	 */
107
	protected function doCheck()
108
	{
109
		$classes_sql = 'SELECT Name, Id, IsAbstract, IsFinal
110
						FROM Classes';
111
		$source_classes = $this->sourceDatabase->fetchAssoc($classes_sql);
112
		$target_classes = $this->targetDatabase->fetchAssoc($classes_sql);
113
114
		foreach ( $source_classes as $class_name => $source_class_data ) {
115
			if ( !isset($target_classes[$class_name]) ) {
116
				$this->addIncident('Class Deleted', $class_name);
117
				continue;
118
			}
119
120
			$this->sourceClassData = $source_class_data;
121
			$this->targetClassData = $target_classes[$class_name];
122
123
			if ( !$this->sourceClassData['IsAbstract'] && $this->targetClassData['IsAbstract'] ) {
124
				$this->addIncident('Class Made Abstract', $class_name);
125
			}
126
127
			if ( !$this->sourceClassData['IsFinal'] && $this->targetClassData['IsFinal'] ) {
128
				$this->addIncident('Class Made Final', $class_name);
129
			}
130
131
			$this->processConstants();
132
			$this->processProperties();
133
			$this->processMethods();
134
		}
135
	}
136
137
	/**
138
	 * Checks constants.
139
	 *
140
	 * @return void
141
	 */
142
	protected function processConstants()
143
	{
144
		$class_name = $this->sourceClassData['Name'];
145
146
		$source_constants = $this->getConstantsRecursively($this->sourceDatabase, $this->sourceClassData['Id']);
147
		$target_constants = $this->getConstantsRecursively($this->targetDatabase, $this->targetClassData['Id']);
148
149
		foreach ( $source_constants as $source_constant_name => $source_constant_data ) {
150
			$full_constant_name = $class_name . '::' . $source_constant_name;
151
152
			// Report incidents for processed (not inherited) constants only.
153
			if ( $source_constant_data['ClassId'] !== $this->sourceClassData['Id'] ) {
154
				continue;
155
			}
156
157
			if ( !isset($target_constants[$source_constant_name]) ) {
158
				$this->addIncident('Class Constant Deleted', $full_constant_name);
159
				continue;
160
			}
161
		}
162
	}
163
164
	/**
165
	 * Returns class constants.
166
	 *
167
	 * @param ExtendedPdoInterface $db       Database.
168
	 * @param integer              $class_id Class ID.
169
	 *
170
	 * @return array
171
	 */
172
	protected function getConstantsRecursively(ExtendedPdoInterface $db, $class_id)
1 ignored issue
show
Comprehensibility introduced by
Avoid variables with short names like $db. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
173
	{
174
		$cache_key = $this->getCacheKey($db, 'class_constants[' . $class_id . ']');
175
		$cached_value = $this->cache->fetch($cache_key);
176
177
		if ( $cached_value === false ) {
178
			$sql = 'SELECT Name, ClassId
179
					FROM ClassConstants
180
					WHERE ClassId = :class_id';
181
			$cached_value = $db->fetchAssoc($sql, array('class_id' => $class_id));
182
183
			foreach ( $this->getClassRelations($db, $class_id) as $related_class_id => $related_class_name ) {
184
				foreach ( $this->getConstantsRecursively($db, $related_class_id) as $name => $data ) {
185
					if ( !array_key_exists($name, $cached_value) ) {
186
						$cached_value[$name] = $data;
187
					}
188
				}
189
			}
190
191
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
192
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
193
		}
194
195
		return $cached_value;
196
	}
197
198
	/**
199
	 * Checks properties.
200
	 *
201
	 * @return void
202
	 */
203
	protected function processProperties()
204
	{
205
		$class_name = $this->sourceClassData['Name'];
206
		$source_properties = $this->getPropertiesRecursively(
207
			$this->sourceDatabase,
208
			$this->sourceClassData['Id'],
209
			$this->coveredScopes()
210
		);
211
		$target_properties = $this->getPropertiesRecursively($this->targetDatabase, $this->targetClassData['Id'], '');
212
213
		foreach ( $source_properties as $source_property_name => $source_property_data ) {
214
			$full_property_name = $class_name . '::' . $source_property_name;
215
216
			// Report incidents for processed (not inherited) properties only.
217
			if ( $source_property_data['ClassId'] !== $this->sourceClassData['Id'] ) {
218
				continue;
219
			}
220
221
			if ( !isset($target_properties[$source_property_name]) ) {
222
				$this->addIncident('Property Deleted', $full_property_name);
223
				continue;
224
			}
225
226
			$this->sourcePropertyData = $source_property_data;
227
			$this->targetPropertyData = $target_properties[$source_property_name];
228
229
			$this->processProperty();
230
		}
231
	}
232
233
	/**
234
	 * Returns class properties.
235
	 *
236
	 * @param ExtendedPdoInterface $db       Database.
237
	 * @param integer              $class_id Class ID.
238
	 * @param string               $scopes   Scopes.
239
	 *
240
	 * @return array
241
	 */
242
	protected function getPropertiesRecursively(ExtendedPdoInterface $db, $class_id, $scopes)
1 ignored issue
show
Comprehensibility introduced by
Avoid variables with short names like $db. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
243
	{
244
		$cache_key = $this->getCacheKey($db, 'class_properties[' . $class_id . ']_scopes[' . ($scopes ?: '*') . ']');
245
		$cached_value = $this->cache->fetch($cache_key);
246
247
		if ( $cached_value === false ) {
248
			$sql = 'SELECT Name, Scope, ClassId
249
					FROM ClassProperties
250
					WHERE ClassId = :class_id';
251
252
			if ( $scopes ) {
253
				$sql .= ' AND Scope IN (' . $scopes . ')';
254
			}
255
256
			$cached_value = $db->fetchAssoc($sql, array('class_id' => $class_id));
257
258
			foreach ( $this->getClassRelations($db, $class_id) as $related_class_id => $related_class_name ) {
259
				foreach ( $this->getPropertiesRecursively($db, $related_class_id, $scopes) as $name => $data ) {
260
					if ( !array_key_exists($name, $cached_value) ) {
261
						$cached_value[$name] = $data;
262
					}
263
				}
264
			}
265
266
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
267
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
268
		}
269
270
		return $cached_value;
271
	}
272
273
	/**
274
	 * Processes property.
275
	 *
276
	 * @return void
277
	 */
278
	protected function processProperty()
279
	{
280
		$class_name = $this->sourceClassData['Name'];
281
		$property_name = $this->sourcePropertyData['Name'];
282
283
		$full_property_name = $class_name . '::' . $property_name;
284
285
		if ( $this->sourcePropertyData['Scope'] > $this->targetPropertyData['Scope'] ) {
286
			$this->addIncident(
287
				'Property Scope Reduced',
288
				$full_property_name,
289
				$this->getScopeName($this->sourcePropertyData['Scope']),
290
				$this->getScopeName($this->targetPropertyData['Scope'])
291
			);
292
		}
293
	}
294
295
	/**
296
	 * Checks methods.
297
	 *
298
	 * @return void
299
	 */
300
	protected function processMethods()
301
	{
302
		$class_name = $this->sourceClassData['Name'];
303
		$source_methods = $this->getMethodsRecursively(
304
			$this->sourceDatabase,
305
			$this->sourceClassData['Id'],
306
			$this->coveredScopes()
307
		);
308
		$target_methods = $this->getMethodsRecursively($this->targetDatabase, $this->targetClassData['Id'], '');
309
310
		foreach ( $source_methods as $source_method_name => $source_method_data ) {
311
			$target_method_name = $source_method_name;
312
			$full_method_name = $class_name . '::' . $source_method_name;
313
314
			// Ignore PHP4 constructor rename into PHP5 constructor.
315
			if ( !isset($target_methods[$target_method_name]) && $target_method_name === $class_name ) {
316
				$target_method_name = '__construct';
317
			}
318
319
			// Report incidents for processed (not inherited) methods only.
320
			if ( $source_method_data['ClassId'] !== $this->sourceClassData['Id'] ) {
321
				continue;
322
			}
323
324
			if ( !isset($target_methods[$target_method_name]) ) {
325
				$this->addIncident('Method Deleted', $full_method_name);
326
				continue;
327
			}
328
329
			$this->sourceMethodData = $source_method_data;
330
			$this->sourceMethodData['ParameterSignature'] = $this->getMethodParameterSignature(
331
				$this->sourceDatabase,
332
				$this->sourceMethodData['Id']
333
			);
334
335
			$this->targetMethodData = $target_methods[$target_method_name];
336
			$this->targetMethodData['ParameterSignature'] = $this->getMethodParameterSignature(
337
				$this->targetDatabase,
338
				$this->targetMethodData['Id']
339
			);
340
341
			$this->processMethod();
342
		}
343
	}
344
345
	/**
346
	 * Returns class methods.
347
	 *
348
	 * @param ExtendedPdoInterface $db       Database.
349
	 * @param integer              $class_id Class ID.
350
	 * @param string               $scopes   Scopes.
351
	 *
352
	 * @return array
353
	 */
354
	protected function getMethodsRecursively(ExtendedPdoInterface $db, $class_id, $scopes)
1 ignored issue
show
Comprehensibility introduced by
Avoid variables with short names like $db. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
355
	{
356
		$cache_key = $this->getCacheKey($db, 'class_methods[' . $class_id . ']_scopes[' . ($scopes ?: '*') . ']');
357
		$cached_value = $this->cache->fetch($cache_key);
358
359
		if ( $cached_value === false ) {
360
			$sql = 'SELECT Name, Id, Scope, IsAbstract, IsFinal, ClassId
361
					FROM ClassMethods
362
					WHERE ClassId = :class_id';
363
364
			if ( $scopes ) {
365
				$sql .= ' AND Scope IN (' . $scopes . ')';
366
			}
367
368
			$cached_value = $db->fetchAssoc($sql, array('class_id' => $class_id));
369
370
			foreach ( $this->getClassRelations($db, $class_id) as $related_class_id => $related_class_name ) {
371
				foreach ( $this->getMethodsRecursively($db, $related_class_id, $scopes) as $name => $data ) {
372
					if ( !array_key_exists($name, $cached_value) ) {
373
						$cached_value[$name] = $data;
374
					}
375
				}
376
			}
377
378
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
379
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
380
		}
381
382
		return $cached_value;
383
	}
384
385
	/**
386
	 * Calculates method parameter signature.
387
	 *
388
	 * @param ExtendedPdoInterface $db        Database.
389
	 * @param integer              $method_id Method ID.
390
	 *
391
	 * @return integer
392
	 */
393
	protected function getMethodParameterSignature(ExtendedPdoInterface $db, $method_id)
1 ignored issue
show
Comprehensibility introduced by
Avoid variables with short names like $db. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
394
	{
395
		$sql = 'SELECT *
396
				FROM MethodParameters
397
				WHERE MethodId = :method_id
398
				ORDER BY Position ASC';
399
		$method_parameters = $db->fetchAll($sql, array('method_id' => $method_id));
400
401
		$hash_parts = array();
402
403
		foreach ( $method_parameters as $method_parameter_data ) {
404
			$hash_parts[] = $this->paramToString($method_parameter_data);
405
		}
406
407
		return implode(', ', $hash_parts);
408
	}
409
410
	/**
411
	 * Processes method.
412
	 *
413
	 * @return void
414
	 */
415
	protected function processMethod()
416
	{
417
		$class_name = $this->sourceClassData['Name'];
418
		$method_name = $this->sourceMethodData['Name'];
419
420
		$full_method_name = $class_name . '::' . $method_name;
421
422
		if ( !$this->sourceMethodData['IsAbstract'] && $this->targetMethodData['IsAbstract'] ) {
423
			$this->addIncident('Method Made Abstract', $full_method_name);
424
		}
425
426
		if ( !$this->sourceMethodData['IsFinal'] && $this->targetMethodData['IsFinal'] ) {
427
			$this->addIncident('Method Made Final', $full_method_name);
428
		}
429
430
		if ( $this->sourceMethodData['ParameterSignature'] !== $this->targetMethodData['ParameterSignature'] ) {
431
			$this->addIncident(
432
				'Method Signature Changed',
433
				$full_method_name,
434
				$this->sourceMethodData['ParameterSignature'],
435
				$this->targetMethodData['ParameterSignature']
436
			);
437
		}
438
439
		if ( $this->sourceMethodData['Scope'] > $this->targetMethodData['Scope'] ) {
440
			$this->addIncident(
441
				'Method Scope Reduced',
442
				$full_method_name,
443
				$this->getScopeName($this->sourceMethodData['Scope']),
444
				$this->getScopeName($this->targetMethodData['Scope'])
445
			);
446
		}
447
	}
448
449
	/**
450
	 * Returns scope name.
451
	 *
452
	 * @param integer $scope Scope.
453
	 *
454
	 * @return string
455
	 */
456
	protected function getScopeName($scope)
457
	{
458
		$mapping = array(
459
			ClassDataCollector::SCOPE_PRIVATE => 'private',
460
			ClassDataCollector::SCOPE_PROTECTED => 'protected',
461
			ClassDataCollector::SCOPE_PUBLIC => 'public',
462
		);
463
464
		return $mapping[$scope];
465
	}
466
467
	/**
468
	 * Scopes covered by backwards compatibility checks.
469
	 *
470
	 * @return string
471
	 */
472
	protected function coveredScopes()
473
	{
474
		return ClassDataCollector::SCOPE_PUBLIC . ',' . ClassDataCollector::SCOPE_PROTECTED;
475
	}
476
477
	/**
478
	 * Returns class constants.
479
	 *
480
	 * @param ExtendedPdoInterface $db       Database.
481
	 * @param integer              $class_id Class ID.
482
	 *
483
	 * @return array
484
	 */
485
	protected function getClassRelations(ExtendedPdoInterface $db, $class_id)
1 ignored issue
show
Comprehensibility introduced by
Avoid variables with short names like $db. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
486
	{
487
		$cache_key = $this->getCacheKey($db, 'class_relations[' . $class_id . ']');
488
		$cached_value = $this->cache->fetch($cache_key);
489
490
		if ( $cached_value === false ) {
491
			$sql = 'SELECT RelatedClassId, RelatedClass
492
					FROM ClassRelations
493
					WHERE ClassId = :class_id AND RelatedClassId <> 0';
494
			$cached_value = $db->fetchPairs($sql, array('class_id' => $class_id));
495
496
			// TODO: Cache for longer period, when DB update will invalidate associated cache.
497
			$this->cache->save($cache_key, $cached_value, self::CACHE_DURATION);
498
		}
499
500
		return $cached_value;
501
	}
502
503
}
504