Failed Conditions
Push — master ( a2f8a2...e02dc1 )
by Alexander
01:53
created

ClassDataCollector::getBackwardsCompatibilityBreaks()   D

Complexity

Conditions 17
Paths 142

Size

Total Lines 97
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 306

Importance

Changes 0
Metric Value
dl 0
loc 97
ccs 0
cts 62
cp 0
rs 4.5364
c 0
b 0
f 0
cc 17
eloc 54
nc 142
nop 1
crap 306

How to fix   Long Method    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
/**
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\KnowledgeBase\DataCollector;
12
13
14
use ConsoleHelpers\CodeInsight\KnowledgeBase\KnowledgeBase;
15
use Go\ParserReflection\ReflectionEngine;
16
use Go\ParserReflection\ReflectionFileNamespace;
17
18
class ClassDataCollector extends AbstractDataCollector
19
{
20
21
	const SCOPE_PRIVATE = 1;
22
23
	const SCOPE_PROTECTED = 2;
24
25
	const SCOPE_PUBLIC = 3;
26
27
	const TYPE_CLASS = 1;
28
29
	const TYPE_INTERFACE = 2;
30
31
	const TYPE_TRAIT = 3;
32
33
	const RELATION_TYPE_EXTENDS = 1;
34
35
	const RELATION_TYPE_IMPLEMENTS = 2;
36
37
	const RELATION_TYPE_USES = 3;
38
39
	/**
40
	 * Collect data from a namespace.
41
	 *
42
	 * @param integer                 $file_id   File id.
43
	 * @param ReflectionFileNamespace $namespace Namespace.
44
	 *
45
	 * @return void
46
	 */
47 15
	public function collectData($file_id, ReflectionFileNamespace $namespace)
48
	{
49 15
		$found_classes = array();
50
51 15
		foreach ( $namespace->getClasses() as $class ) {
52 14
			$found_classes[] = $class->getName();
53 14
			$this->processClass($file_id, $class);
54 15
		}
55
56 15
		if ( $found_classes ) {
57
			// FIXME: Would delete classes outside given namespace in same file.
58
			$sql = 'SELECT Id
59
					FROM Classes
60 14
					WHERE FileId = :file_id AND Name NOT IN (:classes)';
61 14
			$delete_classes = $this->db->fetchCol($sql, array(
62 14
				'file_id' => $file_id,
63 14
				'classes' => $found_classes,
64 14
			));
65
66 14
			foreach ( $delete_classes as $delete_class_id ) {
67 1
				$this->deleteClass($delete_class_id);
68 14
			}
69 14
		}
70
		else {
71 1
			$this->deleteData(array($file_id));
72
		}
73 15
	}
74
75
	/**
76
	 * Aggregate previously collected data.
77
	 *
78
	 * @param KnowledgeBase $knowledge_base Knowledge base.
79
	 *
80
	 * @return void
81
	 */
82 4
	public function aggregateData(KnowledgeBase $knowledge_base)
83
	{
84 4
		parent::aggregateData($knowledge_base);
85
86 4
		$this->processClassRawRelations($knowledge_base);
87 4
	}
88
89
	/**
90
	 * Delete previously collected data for a files.
91
	 *
92
	 * @param array $file_ids File IDs.
93
	 *
94
	 * @return void
95
	 */
96 2
	public function deleteData(array $file_ids)
97
	{
98
		$sql = 'SELECT Id
99
				FROM Classes
100 2
				WHERE FileId IN (:file_ids)';
101 2
		$delete_classes = $this->db->fetchCol($sql, array(
102 2
			'file_ids' => $file_ids,
103 2
		));
104
105 2
		foreach ( $delete_classes as $delete_class_id ) {
106 1
			$this->deleteClass($delete_class_id);
107 2
		}
108 2
	}
109
110
	/**
111
	 * Returns statistics about the code.
112
	 *
113
	 * @return array
114
	 */
115 1
	public function getStatistics()
116
	{
117 1
		$ret = array();
118
119
		$sql = 'SELECT ClassType, COUNT(*)
120
				FROM Classes
121 1
				GROUP BY ClassType';
122 1
		$classes_count = $this->db->fetchPairs($sql);
123
124 1
		foreach ( $classes_count as $class_type => $class_count ) {
125 1
			$title = 'Unknowns';
126
127 1
			if ( $class_type === self::TYPE_CLASS ) {
128 1
				$title = 'Classes';
129 1
			}
130 1
			elseif ( $class_type === self::TYPE_INTERFACE ) {
131 1
				$title = 'Interfaces';
132 1
			}
133 1
			elseif ( $class_type === self::TYPE_TRAIT ) {
134 1
				$title = 'Traits';
135 1
			}
136
137 1
			$ret[$title] = $class_count;
138 1
		}
139
140
		$sql = 'SELECT FileId
141
				FROM Classes
142
				GROUP BY FileId
143 1
				HAVING COUNT(*) > 1';
144 1
		$ret['Files With Multiple Classes'] = count($this->db->fetchCol($sql));
145
146 1
		return $ret;
147
	}
148
149
	/**
150
	 * Processes class.
151
	 *
152
	 * @param integer          $file_id File ID.
153
	 * @param \ReflectionClass $class   Class.
154
	 *
155
	 * @return void
156
	 */
157 14
	protected function processClass($file_id, \ReflectionClass $class)
158
	{
159
		$sql = 'SELECT Id
160
				FROM Classes
161 14
				WHERE FileId = :file_id AND Name = :name';
162 14
		$class_id = $this->db->fetchValue($sql, array(
163 14
			'file_id' => $file_id,
164 14
			'name' => $class->getName(),
165 14
		));
166
167 14
		$raw_class_relations = $this->getRawClassRelations($class);
168
169 14
		if ( $class_id === false ) {
170
			$sql = 'INSERT INTO Classes (Name, ClassType, IsAbstract, IsFinal, FileId, RawRelations)
171 14
					VALUES (:name, :class_type, :is_abstract, :is_final, :file_id, :raw_relations)';
172
173 14
			$this->db->perform(
174 14
				$sql,
175
				array(
176 14
					'name' => $class->getName(),
177 14
					'class_type' => $this->getClassType($class),
178 14
					'is_abstract' => $class->isTrait() ? 0 : (int)$class->isAbstract(),
179 14
					'is_final' => (int)$class->isFinal(),
180 14
					'file_id' => $file_id,
181 14
					'raw_relations' => $raw_class_relations ? json_encode($raw_class_relations) : null,
182
				)
183 14
			);
184
185 14
			$class_id = $this->db->lastInsertId();
186 14
		}
187
		else {
188
			$sql = 'UPDATE Classes
189
					SET	ClassType = :class_type,
190
						IsAbstract = :is_abstract,
191
						IsFinal = :is_final,
192
						RawRelations = :raw_relations
193 12
					WHERE Id = :class_id';
194
195
			// Always store relations as-is to detect fact, when all relations are removed.
196 12
			$this->db->perform(
197 12
				$sql,
198
				array(
199 12
					'class_type' => $this->getClassType($class),
200 12
					'is_abstract' => $class->isTrait() ? 0 : (int)$class->isAbstract(),
201 12
					'is_final' => (int)$class->isFinal(),
202 12
					'raw_relations' => json_encode($raw_class_relations),
203 12
					'class_id' => $class_id,
204
				)
205 12
			);
206
		}
207
208 14
		$this->processClassConstants($class_id, $class);
209 14
		$this->processClassProperties($class_id, $class);
210 14
		$this->processClassMethods($class_id, $class);
211 14
	}
212
213
	/**
214
	 * Returns class type.
215
	 *
216
	 * @param \ReflectionClass $class Class.
217
	 *
218
	 * @return integer
219
	 */
220 14
	protected function getClassType(\ReflectionClass $class)
221
	{
222 14
		if ( $class->isInterface() ) {
223 4
			return self::TYPE_INTERFACE;
224
		}
225
226 14
		if ( $class->isTrait() ) {
227 4
			return self::TYPE_TRAIT;
228
		}
229
230 14
		return self::TYPE_CLASS;
231
	}
232
233
	/**
234
	 * Get relations.
235
	 *
236
	 * @param \ReflectionClass $class Class.
237
	 *
238
	 * @return array
239
	 */
240 14
	protected function getRawClassRelations(\ReflectionClass $class)
241
	{
242 14
		$raw_relations = array();
243 14
		$parent_class = $class->getParentClass();
244
245 14
		if ( $parent_class ) {
246 4
			$raw_relations[] = array(
247 4
				$parent_class->getName(),
248 4
				self::RELATION_TYPE_EXTENDS,
249 4
				$parent_class->isInternal(),
250
			);
251 4
		}
252
253 14
		foreach ( $class->getInterfaces() as $interface ) {
254 2
			$raw_relations[] = array(
255 2
				$interface->getName(),
256 2
				self::RELATION_TYPE_IMPLEMENTS,
257 2
				$interface->isInternal(),
258
			);
259 14
		}
260
261 14
		foreach ( $class->getTraits() as $trait ) {
262 2
			$raw_relations[] = array(
263 2
				$trait->getName(),
264 2
				self::RELATION_TYPE_USES,
265 2
				$trait->isInternal(),
266
			);
267 14
		}
268
269 14
		return $raw_relations;
270
	}
271
272
	/**
273
	 * Deletes a class.
274
	 *
275
	 * @param integer $class_id Class ID.
276
	 *
277
	 * @return void
278
	 */
279 2
	protected function deleteClass($class_id)
280
	{
281 2
		$sql = 'DELETE FROM Classes WHERE Id = :class_id';
282 2
		$this->db->perform($sql, array('class_id' => $class_id));
283
284 2
		$sql = 'DELETE FROM ClassConstants WHERE ClassId = :class_id';
285 2
		$this->db->perform($sql, array('class_id' => $class_id));
286
287 2
		$sql = 'DELETE FROM ClassProperties WHERE ClassId = :class_id';
288 2
		$this->db->perform($sql, array('class_id' => $class_id));
289
290 2
		$this->deleteClassMethods($class_id, array());
291
292 2
		$sql = 'DELETE FROM ClassRelations WHERE ClassId = :class_id';
293 2
		$this->db->perform($sql, array('class_id' => $class_id));
294
295 2
		$sql = 'DELETE FROM ClassRelations WHERE RelatedClassId = :class_id';
296 2
		$this->db->perform($sql, array('class_id' => $class_id));
297 2
	}
298
299
	/**
300
	 * Processes class constants.
301
	 *
302
	 * @param integer          $class_id Class ID.
303
	 * @param \ReflectionClass $class    Class.
304
	 *
305
	 * @return void
306
	 */
307 14
	protected function processClassConstants($class_id, \ReflectionClass $class)
308
	{
309 14
		$constants = $class->getConstants();
310
311
		$sql = 'SELECT Name
312
				FROM ClassConstants
313 14
				WHERE ClassId = :class_id';
314 14
		$old_constants = $this->db->fetchCol($sql, array(
315 14
			'class_id' => $class_id,
316 14
		));
317
318
		$insert_sql = '	INSERT INTO ClassConstants (ClassId, Name, Value)
319 14
						VALUES (:class_id, :name, :value)';
320
		$update_sql = '	UPDATE ClassConstants
321
						SET Value = :value
322 14
						WHERE ClassId = :class_id AND Name = :name';
323
324 14
		foreach ( $constants as $constant_name => $constant_value ) {
325 3
			$this->db->perform(
326 3
				in_array($constant_name, $old_constants) ? $update_sql : $insert_sql,
327
				array(
328 3
					'class_id' => $class_id,
329 3
					'name' => $constant_name,
330 3
					'value' => json_encode($constant_value),
331
				)
332 3
			);
333 14
		}
334
335 14
		$delete_constants = array_diff($old_constants, array_keys($constants));
336
337 14
		if ( $delete_constants ) {
338
			$sql = 'DELETE FROM ClassConstants
339 1
					WHERE ClassId = :class_id AND Name IN (:names)';
340 1
			$this->db->perform($sql, array(
341 1
				'class_id' => $class_id,
342 1
				'names' => $delete_constants,
343 1
			));
344 1
		}
345 14
	}
346
347
	/**
348
	 * Processes class properties.
349
	 *
350
	 * @param integer          $class_id Class ID.
351
	 * @param \ReflectionClass $class    Class.
352
	 *
353
	 * @return void
354
	 */
355 14
	protected function processClassProperties($class_id, \ReflectionClass $class)
356
	{
357
		$sql = 'SELECT Name
358
				FROM ClassProperties
359 14
				WHERE ClassId = :class_id';
360 14
		$old_properties = $this->db->fetchCol($sql, array(
361 14
			'class_id' => $class_id,
362 14
		));
363
364
		$insert_sql = '	INSERT INTO ClassProperties (ClassId, Name, Value, Scope, IsStatic)
365 14
						VALUES (:class_id, :name, :value, :scope, :is_static)';
366
		$update_sql = '	UPDATE ClassProperties
367
						SET	Value = :value,
368
							Scope = :scope,
369
							IsStatic = :is_static
370 14
						WHERE ClassId = :class_id AND Name = :name';
371
372 14
		$new_properties = array();
373 14
		$property_defaults = $class->getDefaultProperties();
374 14
		$static_properties = $class->getStaticProperties();
375 14
		$class_name = $class->getName();
376
377 14
		foreach ( $class->getProperties() as $property ) {
378 5
			if ( $property->class !== $class_name ) {
379 1
				continue;
380
			}
381
382 5
			$property_name = $property->getName();
383 5
			$property_value = isset($property_defaults[$property_name]) ? $property_defaults[$property_name] : null;
384 5
			$new_properties[] = $property_name;
385
386 5
			$this->db->perform(
387 5
				in_array($property_name, $old_properties) ? $update_sql : $insert_sql,
388
				array(
389 5
					'class_id' => $class_id,
390 5
					'name' => $property_name,
391 5
					'value' => json_encode($property_value),
392 5
					'scope' => $this->getPropertyScope($property),
393 5
					'is_static' => (int)array_key_exists($property_name, $static_properties),
394
				)
395 5
			);
396 14
		}
397
398 14
		$delete_properties = array_diff($old_properties, $new_properties);
399
400 14
		if ( $delete_properties ) {
401
			$sql = 'DELETE FROM ClassProperties
402 1
					WHERE ClassId = :class_id AND Name IN (:names)';
403 1
			$this->db->perform($sql, array(
404 1
				'class_id' => $class_id,
405 1
				'names' => $delete_properties,
406 1
			));
407 1
		}
408 14
	}
409
410
	/**
411
	 * Returns property scope.
412
	 *
413
	 * @param \ReflectionProperty $property Property.
414
	 *
415
	 * @return integer
416
	 */
417 5
	protected function getPropertyScope(\ReflectionProperty $property)
418
	{
419 5
		if ( $property->isPrivate() ) {
420 1
			return self::SCOPE_PRIVATE;
421
		}
422
423 5
		if ( $property->isProtected() ) {
424 2
			return self::SCOPE_PROTECTED;
425
		}
426
427 5
		return self::SCOPE_PUBLIC;
428
	}
429
430
	/**
431
	 * Processes methods.
432
	 *
433
	 * @param integer          $class_id Class ID.
434
	 * @param \ReflectionClass $class    Class.
435
	 *
436
	 * @return void
437
	 */
438 14
	protected function processClassMethods($class_id, \ReflectionClass $class)
439
	{
440
		$sql = 'SELECT Name, Id
441
				FROM ClassMethods
442 14
				WHERE ClassId = :class_id';
443 14
		$old_methods = $this->db->fetchPairs($sql, array(
444 14
			'class_id' => $class_id,
445 14
		));
446
447
		$insert_sql = '	INSERT INTO ClassMethods (ClassId, Name, ParameterCount, RequiredParameterCount, Scope, IsAbstract, IsFinal, IsStatic, IsVariadic, ReturnsReference, HasReturnType, ReturnType)
448 14
						VALUES (:class_id, :name, :parameter_count, :required_parameter_count, :scope, :is_abstract, :is_final, :is_static, :is_variadic, :returns_reference, :has_return_type, :return_type)';
449
		$update_sql = '	UPDATE ClassMethods
450
						SET	ParameterCount = :parameter_count,
451
							RequiredParameterCount = :required_parameter_count,
452
							Scope = :scope,
453
							IsAbstract = :is_abstract,
454
							IsFinal = :is_final,
455
							IsStatic = :is_static,
456
							IsVariadic = :is_variadic,
457
							ReturnsReference = :returns_reference,
458
							ReturnType = :return_type,
459
							HasReturnType = :has_return_type
460 14
						WHERE ClassId = :class_id AND Name = :name';
461
462 14
		$new_methods = array();
463 14
		$class_name = $class->getName();
464
465 14
		foreach ( $class->getMethods() as $method ) {
466 5
			if ( $method->class !== $class_name ) {
467 1
				continue;
468
			}
469
470 5
			$method_name = $method->getName();
471 5
			$new_methods[] = $method_name;
472
473
			// Doesn't work for parent classes (see https://github.com/goaop/parser-reflection/issues/16).
474 5
			$has_return_type = $method->hasReturnType();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ReflectionMethod as the method hasReturnType() does only exist in the following sub-classes of ReflectionMethod: Go\ParserReflection\ReflectionMethod. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
475 5
			$return_type = $has_return_type ? (string)$method->getReturnType() : null;
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ReflectionMethod as the method getReturnType() does only exist in the following sub-classes of ReflectionMethod: Go\ParserReflection\ReflectionMethod. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
476
477 5
			$this->db->perform(
478 5
				isset($old_methods[$method_name]) ? $update_sql : $insert_sql,
479
				array(
480 5
					'class_id' => $class_id,
481 5
					'name' => $method_name,
482 5
					'parameter_count' => $method->getNumberOfParameters(),
483 5
					'required_parameter_count' => $method->getNumberOfRequiredParameters(),
484 5
					'scope' => $this->getMethodScope($method),
485 5
					'is_abstract' => (int)$method->isAbstract(),
486 5
					'is_final' => (int)$method->isFinal(),
487 5
					'is_static' => (int)$method->isStatic(),
488 5
					'is_variadic' => (int)$method->isVariadic(),
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ReflectionMethod as the method isVariadic() does only exist in the following sub-classes of ReflectionMethod: Go\ParserReflection\ReflectionMethod. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
489 5
					'returns_reference' => (int)$method->returnsReference(),
490 5
					'has_return_type' => (int)$has_return_type,
491 5
					'return_type' => $return_type,
492
				)
493 5
			);
494
495 5
			$method_id = isset($old_methods[$method_name]) ? $old_methods[$method_name] : $this->db->lastInsertId();
496 5
			$this->processClassMethodParameters($method_id, $method);
497 14
		}
498
499 14
		$delete_methods = array_diff(array_keys($old_methods), $new_methods);
500
501 14
		if ( $delete_methods ) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $delete_methods of type array<integer|string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
502 1
			$this->deleteClassMethods($class_id, $delete_methods);
503 1
		}
504 14
	}
505
506
	/**
507
	 * Deletes methods.
508
	 *
509
	 * @param integer $class_id Class ID.
510
	 * @param array   $methods  Methods.
511
	 *
512
	 * @return void
513
	 */
514 2
	protected function deleteClassMethods($class_id, array $methods)
515
	{
516 2
		if ( $methods ) {
517
			// Delete only given methods.
518
			$sql = 'SELECT Id
519
					FROM ClassMethods
520 1
					WHERE ClassId = :class_id AND Name IN (:names)';
521 1
			$method_ids = $this->db->fetchCol($sql, array(
522 1
				'class_id' => $class_id,
523 1
				'names' => $methods,
524 1
			));
525 1
		}
526
		else {
527
			// Delete all methods in a class.
528
			$sql = 'SELECT Id
529
					FROM ClassMethods
530 2
					WHERE ClassId = :class_id';
531 2
			$method_ids = $this->db->fetchCol($sql, array(
532 2
				'class_id' => $class_id,
533 2
			));
534
		}
535
536
		// @codeCoverageIgnoreStart
537
		if ( !$method_ids ) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $method_ids of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
538
			return;
539
		}
540
		// @codeCoverageIgnoreEnd
541
542 2
		$sql = 'DELETE FROM ClassMethods WHERE Id IN (:method_ids)';
543 2
		$this->db->perform($sql, array('method_ids' => $method_ids));
544
545 2
		$sql = 'DELETE FROM MethodParameters WHERE MethodId IN (:method_ids)';
546 2
		$this->db->perform($sql, array('method_ids' => $method_ids));
547 2
	}
548
549
	/**
550
	 * Processes method parameters.
551
	 *
552
	 * @param integer           $method_id Method ID.
553
	 * @param \ReflectionMethod $method    Method.
554
	 *
555
	 * @return void
556
	 */
557 5
	protected function processClassMethodParameters($method_id, \ReflectionMethod $method)
558
	{
559
		$sql = 'SELECT Name
560
				FROM MethodParameters
561 5
				WHERE MethodId = :method_id';
562 5
		$old_parameters = $this->db->fetchCol($sql, array(
563 5
			'method_id' => $method_id,
564 5
		));
565
566
		$insert_sql = '	INSERT INTO MethodParameters (MethodId, Name, Position, TypeClass, HasType, TypeName, AllowsNull, IsArray, IsCallable, IsOptional, IsVariadic, CanBePassedByValue, IsPassedByReference, HasDefaultValue, DefaultValue, DefaultConstant)
567 5
						VALUES (:method_id, :name, :position, :type_class, :has_type, :type_name, :allows_null, :is_array, :is_callable, :is_optional, :is_variadic, :can_be_passed_by_value, :is_passed_by_reference, :has_default_value, :default_value, :default_constant)';
568
		$update_sql = '	UPDATE MethodParameters
569
						SET	Position = :position,
570
							TypeClass = :type_class,
571
							HasType = :has_type,
572
							TypeName = :type_name,
573
							AllowsNull = :allows_null,
574
							IsArray = :is_array,
575
							IsCallable = :is_callable,
576
							IsOptional = :is_optional,
577
							IsVariadic = :is_variadic,
578
							CanBePassedByValue = :can_be_passed_by_value,
579
							IsPassedByReference = :is_passed_by_reference,
580
							HasDefaultValue = :has_default_value,
581
							DefaultValue = :default_value,
582
							DefaultConstant = :default_constant
583 5
						WHERE MethodId = :method_id AND Name = :name';
584
585 5
		$new_parameters = array();
586
587 5
		foreach ( $method->getParameters() as $position => $parameter ) {
588 4
			$parameter_name = $parameter->getName();
589 4
			$new_parameters[] = $parameter_name;
590
591 4
			$type_class = $parameter->getClass();
592 4
			$type_class = $type_class ? $type_class->getName() : null;
593
594
			// Doesn't work for parent classes (see https://github.com/goaop/parser-reflection/issues/16).
595 4
			$has_type = $parameter->hasType();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ReflectionParameter as the method hasType() does only exist in the following sub-classes of ReflectionParameter: Go\ParserReflection\ReflectionParameter. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
596 4
			$type_name = $has_type ? (string)$parameter->getType() : null;
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ReflectionParameter as the method getType() does only exist in the following sub-classes of ReflectionParameter: Go\ParserReflection\ReflectionParameter. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
597
598 4
			$has_default_value = $parameter->isDefaultValueAvailable();
599 4
			$default_value_is_constant = $has_default_value ? $parameter->isDefaultValueConstant() : false;
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ReflectionParameter as the method isDefaultValueConstant() does only exist in the following sub-classes of ReflectionParameter: Go\ParserReflection\ReflectionParameter. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
600
601 4
			$this->db->perform(
602 4
				in_array($parameter_name, $old_parameters) ? $update_sql : $insert_sql,
603
				array(
604 4
					'method_id' => $method_id,
605 4
					'name' => $parameter_name,
606 4
					'position' => $position,
607 4
					'type_class' => $type_class,
608 4
					'has_type' => (int)$has_type,
609 4
					'type_name' => $type_name,
610 4
					'allows_null' => (int)$parameter->allowsNull(),
611 4
					'is_array' => (int)$parameter->isArray(),
612 4
					'is_callable' => (int)$parameter->isCallable(),
613 4
					'is_optional' => (int)$parameter->isOptional(),
614 4
					'is_variadic' => (int)$parameter->isVariadic(),
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ReflectionParameter as the method isVariadic() does only exist in the following sub-classes of ReflectionParameter: Go\ParserReflection\ReflectionParameter. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
615 4
					'can_be_passed_by_value' => (int)$parameter->canBePassedByValue(),
616 4
					'is_passed_by_reference' => (int)$parameter->isPassedByReference(),
617 4
					'has_default_value' => (int)$has_default_value,
618 4
					'default_value' => $has_default_value ? json_encode($parameter->getDefaultValue()) : null,
619 4
					'default_constant' => $default_value_is_constant ? $parameter->getDefaultValueConstantName() : null,
620
				)
621 4
			);
622 5
		}
623
624 5
		$delete_parameters = array_diff($old_parameters, $new_parameters);
625
626 5
		if ( $delete_parameters ) {
627
			$sql = 'DELETE FROM MethodParameters
628 2
					WHERE MethodId = :method_id AND Name IN (:names)';
629 2
			$this->db->perform($sql, array(
630 2
				'method_id' => $method_id,
631 2
				'names' => $delete_parameters,
632 2
			));
633 2
		}
634 5
	}
635
636
	/**
637
	 * Returns method scope.
638
	 *
639
	 * @param \ReflectionMethod $method Method.
640
	 *
641
	 * @return integer
642
	 */
643 5
	protected function getMethodScope(\ReflectionMethod $method)
644
	{
645 5
		if ( $method->isPrivate() ) {
646 1
			return self::SCOPE_PRIVATE;
647
		}
648
649 5
		if ( $method->isProtected() ) {
650 1
			return self::SCOPE_PROTECTED;
651
		}
652
653 5
		return self::SCOPE_PUBLIC;
654
	}
655
656
	/**
657
	 * Processes raw relations for all classes.
658
	 *
659
	 * @param KnowledgeBase $knowledge_base Knowledge base.
660
	 *
661
	 * @return void
662
	 */
663 4
	protected function processClassRawRelations(KnowledgeBase $knowledge_base)
664
	{
665
		$sql = 'SELECT Id, RawRelations
666
				FROM Classes
667 4
				WHERE RawRelations IS NOT NULL';
668 4
		$raw_relations = $this->db->yieldPairs($sql);
669
670 4
		foreach ( $raw_relations as $class_id => $class_raw_relations ) {
671
			$sql = 'SELECT RelatedClass
672
					FROM ClassRelations
673 4
					WHERE ClassId = :class_id';
674 4
			$old_class_relations = $this->db->fetchCol($sql, array(
675 4
				'class_id' => $class_id,
676 4
			));
677
678 4
			$new_class_relations = array();
679
680 4
			foreach ( json_decode($class_raw_relations, true) as $class_raw_relation ) {
681 4
				list ($related_class, $relation_type, $is_internal) = $class_raw_relation;
682
683 4
				$new_class_relations[] = $this->addRelation(
684 4
					$knowledge_base,
685 4
					$class_id,
686 4
					$related_class,
687 4
					$relation_type,
688 4
					$is_internal,
689
					$old_class_relations
690 4
				);
691 4
			}
692
693 4
			$delete_class_relations = array_diff($old_class_relations, $new_class_relations);
694
695 4
			if ( $delete_class_relations ) {
696
				$sql = 'DELETE FROM ClassRelations
697 1
						WHERE ClassId = :class_id AND RelatedClass IN (:related_classes)';
698 1
				$this->db->perform($sql, array(
699 1
					'class_id' => $class_id,
700 1
					'related_classes' => $delete_class_relations,
701 1
				));
702 1
			}
703 4
		}
704
705
		$sql = 'UPDATE Classes
706 4
				SET RawRelations = NULL';
707 4
		$this->db->perform($sql);
708 4
	}
709
710
	/**
711
	 * Adds a relation.
712
	 *
713
	 * @param KnowledgeBase $knowledge_base Knowledge base.
714
	 * @param integer       $class_id       Class ID.
715
	 * @param string        $related_class  Related class.
716
	 * @param integer       $relation_type  Relation type.
717
	 * @param boolean       $is_internal    Is internal.
718
	 * @param array         $old_relations  Old relations.
719
	 *
720
	 * @return string
721
	 */
722 4
	protected function addRelation(
723
		KnowledgeBase $knowledge_base,
724
		$class_id,
725
		$related_class,
726
		$relation_type,
727
		$is_internal,
728
		array $old_relations
729
	) {
730
		$insert_sql = '	INSERT INTO ClassRelations (ClassId, RelatedClass, RelatedClassId, RelationType)
731 4
						VALUES (:class_id, :related_class, :related_class_id, :relation_type)';
732
		$update_sql = ' UPDATE ClassRelations
733
						SET RelationType = :relation_type
734 4
						WHERE ClassId = :class_id AND RelatedClassId = :related_class_id';
735
736 4
		if ( $is_internal ) {
737 3
			$related_class_id = 0;
738 3
		}
739
		else {
740 2
			$related_class_file = realpath(ReflectionEngine::locateClassFile($related_class));
741
742
			$sql = 'SELECT Id
743
					FROM Classes
744 2
					WHERE FileId = :file_id AND Name = :name';
745 2
			$related_class_id = $this->db->fetchValue($sql, array(
746 2
				'file_id' => $knowledge_base->processFile($related_class_file),
747 2
				'name' => $related_class,
748 2
			));
749
		}
750
751 4
		$this->db->perform(
752 4
			in_array($related_class, $old_relations) ? $update_sql : $insert_sql,
753
			array(
754 4
				'class_id' => $class_id,
755 4
				'related_class' => $related_class,
756 4
				'related_class_id' => $related_class_id,
757 4
				'relation_type' => $relation_type,
758
			)
759 4
		);
760
761 4
		return $related_class;
762
	}
763
764
}
765