Completed
Push — master ( 284e4a...669103 )
by Alexander
01:40
created

ClassDataCollector::getScopeName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 0
cts 6
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 1
crap 2
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 Aura\Sql\ExtendedPdoInterface;
15
use ConsoleHelpers\CodeInsight\KnowledgeBase\KnowledgeBase;
16
use Go\ParserReflection\ReflectionEngine;
17
use Go\ParserReflection\ReflectionFileNamespace;
18
19
class ClassDataCollector extends AbstractDataCollector
20
{
21
22
	const SCOPE_PRIVATE = 1;
23
24
	const SCOPE_PROTECTED = 2;
25
26
	const SCOPE_PUBLIC = 3;
27
28
	const TYPE_CLASS = 1;
29
30
	const TYPE_INTERFACE = 2;
31
32
	const TYPE_TRAIT = 3;
33
34
	const RELATION_TYPE_EXTENDS = 1;
35
36
	const RELATION_TYPE_IMPLEMENTS = 2;
37
38
	const RELATION_TYPE_USES = 3;
39
40
	/**
41
	 * Collect data from a namespace.
42
	 *
43
	 * @param integer                 $file_id   File id.
44
	 * @param ReflectionFileNamespace $namespace Namespace.
45
	 *
46
	 * @return void
47
	 */
48 15
	public function collectData($file_id, ReflectionFileNamespace $namespace)
49
	{
50 15
		$found_classes = array();
51
52 15
		foreach ( $namespace->getClasses() as $class ) {
53 14
			$found_classes[] = $class->getName();
54 14
			$this->processClass($file_id, $class);
55 15
		}
56
57 15
		if ( $found_classes ) {
58
			// FIXME: Would delete classes outside given namespace in same file.
59
			$sql = 'SELECT Id
60
					FROM Classes
61 14
					WHERE FileId = :file_id AND Name NOT IN (:classes)';
62 14
			$delete_classes = $this->db->fetchCol($sql, array(
63 14
				'file_id' => $file_id,
64 14
				'classes' => $found_classes,
65 14
			));
66
67 14
			foreach ( $delete_classes as $delete_class_id ) {
68 1
				$this->deleteClass($delete_class_id);
69 14
			}
70 14
		}
71
		else {
72 1
			$this->deleteData(array($file_id));
73
		}
74 15
	}
75
76
	/**
77
	 * Aggregate previously collected data.
78
	 *
79
	 * @param KnowledgeBase $knowledge_base Knowledge base.
80
	 *
81
	 * @return void
82
	 */
83 4
	public function aggregateData(KnowledgeBase $knowledge_base)
84
	{
85 4
		parent::aggregateData($knowledge_base);
86
87 4
		$this->processClassRawRelations($knowledge_base);
88 4
	}
89
90
	/**
91
	 * Delete previously collected data for a files.
92
	 *
93
	 * @param array $file_ids File IDs.
94
	 *
95
	 * @return void
96
	 */
97 2
	public function deleteData(array $file_ids)
98
	{
99
		$sql = 'SELECT Id
100
				FROM Classes
101 2
				WHERE FileId IN (:file_ids)';
102 2
		$delete_classes = $this->db->fetchCol($sql, array(
103 2
			'file_ids' => $file_ids,
104 2
		));
105
106 2
		foreach ( $delete_classes as $delete_class_id ) {
107 1
			$this->deleteClass($delete_class_id);
108 2
		}
109 2
	}
110
111
	/**
112
	 * Returns statistics about the code.
113
	 *
114
	 * @return array
115
	 */
116 1
	public function getStatistics()
117
	{
118 1
		$ret = array();
119
120
		$sql = 'SELECT ClassType, COUNT(*)
121
				FROM Classes
122 1
				GROUP BY ClassType';
123 1
		$classes_count = $this->db->fetchPairs($sql);
124
125 1
		foreach ( $classes_count as $class_type => $class_count ) {
126 1
			$title = 'Unknowns';
127
128 1
			if ( $class_type === self::TYPE_CLASS ) {
129 1
				$title = 'Classes';
130 1
			}
131 1
			elseif ( $class_type === self::TYPE_INTERFACE ) {
132 1
				$title = 'Interfaces';
133 1
			}
134 1
			elseif ( $class_type === self::TYPE_TRAIT ) {
135 1
				$title = 'Traits';
136 1
			}
137
138 1
			$ret[$title] = $class_count;
139 1
		}
140
141
		$sql = 'SELECT FileId
142
				FROM Classes
143
				GROUP BY FileId
144 1
				HAVING COUNT(*) > 1';
145 1
		$ret['Files With Multiple Classes'] = count($this->db->fetchCol($sql));
146
147 1
		return $ret;
148
	}
149
150
	/**
151
	 * Processes class.
152
	 *
153
	 * @param integer          $file_id File ID.
154
	 * @param \ReflectionClass $class   Class.
155
	 *
156
	 * @return void
157
	 */
158 14
	protected function processClass($file_id, \ReflectionClass $class)
159
	{
160
		$sql = 'SELECT Id
161
				FROM Classes
162 14
				WHERE FileId = :file_id AND Name = :name';
163 14
		$class_id = $this->db->fetchValue($sql, array(
164 14
			'file_id' => $file_id,
165 14
			'name' => $class->getName(),
166 14
		));
167
168 14
		$raw_class_relations = $this->getRawClassRelations($class);
169
170 14
		if ( $class_id === false ) {
171
			$sql = 'INSERT INTO Classes (Name, ClassType, IsAbstract, IsFinal, FileId, RawRelations)
172 14
					VALUES (:name, :class_type, :is_abstract, :is_final, :file_id, :raw_relations)';
173
174 14
			$this->db->perform(
175 14
				$sql,
176
				array(
177 14
					'name' => $class->getName(),
178 14
					'class_type' => $this->getClassType($class),
179 14
					'is_abstract' => $class->isTrait() ? 0 : (int)$class->isAbstract(),
180 14
					'is_final' => (int)$class->isFinal(),
181 14
					'file_id' => $file_id,
182 14
					'raw_relations' => $raw_class_relations ? json_encode($raw_class_relations) : null,
183
				)
184 14
			);
185
186 14
			$class_id = $this->db->lastInsertId();
187 14
		}
188
		else {
189
			$sql = 'UPDATE Classes
190
					SET	ClassType = :class_type,
191
						IsAbstract = :is_abstract,
192
						IsFinal = :is_final,
193 1
						RawRelations = :raw_relations
194 12
					WHERE Id = :class_id';
195
196
			// Always store relations as-is to detect fact, when all relations are removed.
197 12
			$this->db->perform(
198 12
				$sql,
199
				array(
200 12
					'class_type' => $this->getClassType($class),
201 12
					'is_abstract' => $class->isTrait() ? 0 : (int)$class->isAbstract(),
202 12
					'is_final' => (int)$class->isFinal(),
203 12
					'raw_relations' => json_encode($raw_class_relations),
204 12
					'class_id' => $class_id,
205
				)
206 12
			);
207
		}
208
209 14
		$this->processClassConstants($class_id, $class);
210 14
		$this->processClassProperties($class_id, $class);
211 14
		$this->processClassMethods($class_id, $class);
212 14
	}
213
214
	/**
215
	 * Returns class type.
216
	 *
217
	 * @param \ReflectionClass $class Class.
218
	 *
219
	 * @return integer
220
	 */
221 14
	protected function getClassType(\ReflectionClass $class)
222
	{
223 14
		if ( $class->isInterface() ) {
224 4
			return self::TYPE_INTERFACE;
225
		}
226
227 14
		if ( $class->isTrait() ) {
228 4
			return self::TYPE_TRAIT;
229
		}
230
231 14
		return self::TYPE_CLASS;
232
	}
233
234
	/**
235
	 * Get relations.
236
	 *
237
	 * @param \ReflectionClass $class Class.
238
	 *
239
	 * @return array
240
	 */
241 14
	protected function getRawClassRelations(\ReflectionClass $class)
242
	{
243 14
		$raw_relations = array();
244 14
		$parent_class = $class->getParentClass();
245
246 14
		if ( $parent_class ) {
247 4
			$raw_relations[] = array(
248 4
				$parent_class->getName(),
249 4
				self::RELATION_TYPE_EXTENDS,
250 4
				$parent_class->isInternal(),
251
			);
252 4
		}
253
254 14
		foreach ( $class->getInterfaces() as $interface ) {
255 2
			$raw_relations[] = array(
256 2
				$interface->getName(),
257 2
				self::RELATION_TYPE_IMPLEMENTS,
258 2
				$interface->isInternal(),
259
			);
260 14
		}
261
262 14
		foreach ( $class->getTraits() as $trait ) {
263 2
			$raw_relations[] = array(
264 2
				$trait->getName(),
265 2
				self::RELATION_TYPE_USES,
266 2
				$trait->isInternal(),
267
			);
268 14
		}
269
270 14
		return $raw_relations;
271
	}
272
273
	/**
274
	 * Deletes a class.
275
	 *
276
	 * @param integer $class_id Class ID.
277
	 *
278
	 * @return void
279
	 */
280 2
	protected function deleteClass($class_id)
281
	{
282 2
		$sql = 'DELETE FROM Classes WHERE Id = :class_id';
283 2
		$this->db->perform($sql, array('class_id' => $class_id));
284
285 2
		$sql = 'DELETE FROM ClassConstants WHERE ClassId = :class_id';
286 2
		$this->db->perform($sql, array('class_id' => $class_id));
287
288 2
		$sql = 'DELETE FROM ClassProperties WHERE ClassId = :class_id';
289 2
		$this->db->perform($sql, array('class_id' => $class_id));
290
291 2
		$this->deleteClassMethods($class_id, array());
292
293 2
		$sql = 'DELETE FROM ClassRelations WHERE ClassId = :class_id';
294 2
		$this->db->perform($sql, array('class_id' => $class_id));
295
296 2
		$sql = 'DELETE FROM ClassRelations WHERE RelatedClassId = :class_id';
297 2
		$this->db->perform($sql, array('class_id' => $class_id));
298 2
	}
299
300
	/**
301
	 * Processes class constants.
302
	 *
303
	 * @param integer          $class_id Class ID.
304
	 * @param \ReflectionClass $class    Class.
305
	 *
306
	 * @return void
307
	 */
308 14
	protected function processClassConstants($class_id, \ReflectionClass $class)
309
	{
310 14
		$constants = $class->getConstants();
311
312
		$sql = 'SELECT Name
313
				FROM ClassConstants
314 14
				WHERE ClassId = :class_id';
315 14
		$old_constants = $this->db->fetchCol($sql, array(
316 14
			'class_id' => $class_id,
317 14
		));
318
319
		$insert_sql = '	INSERT INTO ClassConstants (ClassId, Name, Value)
320 14
						VALUES (:class_id, :name, :value)';
321
		$update_sql = '	UPDATE ClassConstants
322
						SET Value = :value
323 14
						WHERE ClassId = :class_id AND Name = :name';
324
325 14
		foreach ( $constants as $constant_name => $constant_value ) {
326 3
			$this->db->perform(
327 3
				in_array($constant_name, $old_constants) ? $update_sql : $insert_sql,
328
				array(
329 3
					'class_id' => $class_id,
330 3
					'name' => $constant_name,
331 3
					'value' => json_encode($constant_value),
332
				)
333 3
			);
334 14
		}
335
336 14
		$delete_constants = array_diff($old_constants, array_keys($constants));
337
338 14
		if ( $delete_constants ) {
339
			$sql = 'DELETE FROM ClassConstants
340 1
					WHERE ClassId = :class_id AND Name IN (:names)';
341 1
			$this->db->perform($sql, array(
342 1
				'class_id' => $class_id,
343 1
				'names' => $delete_constants,
344 1
			));
345 1
		}
346 14
	}
347
348
	/**
349
	 * Processes class properties.
350
	 *
351
	 * @param integer          $class_id Class ID.
352
	 * @param \ReflectionClass $class    Class.
353
	 *
354
	 * @return void
355
	 */
356 14
	protected function processClassProperties($class_id, \ReflectionClass $class)
357
	{
358
		$sql = 'SELECT Name
359
				FROM ClassProperties
360 14
				WHERE ClassId = :class_id';
361 14
		$old_properties = $this->db->fetchCol($sql, array(
362 14
			'class_id' => $class_id,
363 14
		));
364
365
		$insert_sql = '	INSERT INTO ClassProperties (ClassId, Name, Value, Scope, IsStatic)
366 14
						VALUES (:class_id, :name, :value, :scope, :is_static)';
367
		$update_sql = '	UPDATE ClassProperties
368
						SET	Value = :value,
369
							Scope = :scope,
370
							IsStatic = :is_static
371 14
						WHERE ClassId = :class_id AND Name = :name';
372
373 14
		$new_properties = array();
374 14
		$property_defaults = $class->getDefaultProperties();
375 14
		$static_properties = $class->getStaticProperties();
376 14
		$class_name = $class->getName();
377
378 14
		foreach ( $class->getProperties() as $property ) {
379 5
			if ( $property->class !== $class_name ) {
380 1
				continue;
381
			}
382
383 5
			$property_name = $property->getName();
384 5
			$property_value = isset($property_defaults[$property_name]) ? $property_defaults[$property_name] : null;
385 5
			$new_properties[] = $property_name;
386
387 5
			$this->db->perform(
388 5
				in_array($property_name, $old_properties) ? $update_sql : $insert_sql,
389
				array(
390 5
					'class_id' => $class_id,
391 5
					'name' => $property_name,
392 5
					'value' => json_encode($property_value),
393 5
					'scope' => $this->getPropertyScope($property),
394 5
					'is_static' => (int)array_key_exists($property_name, $static_properties),
395
				)
396 5
			);
397 14
		}
398
399 14
		$delete_properties = array_diff($old_properties, $new_properties);
400
401 14
		if ( $delete_properties ) {
402
			$sql = 'DELETE FROM ClassProperties
403 1
					WHERE ClassId = :class_id AND Name IN (:names)';
404 1
			$this->db->perform($sql, array(
405 1
				'class_id' => $class_id,
406 1
				'names' => $delete_properties,
407 1
			));
408 1
		}
409 14
	}
410
411
	/**
412
	 * Returns property scope.
413
	 *
414
	 * @param \ReflectionProperty $property Property.
415
	 *
416
	 * @return integer
417
	 */
418 5
	protected function getPropertyScope(\ReflectionProperty $property)
419
	{
420 5
		if ( $property->isPrivate() ) {
421 1
			return self::SCOPE_PRIVATE;
422
		}
423
424 5
		if ( $property->isProtected() ) {
425 2
			return self::SCOPE_PROTECTED;
426
		}
427
428 5
		return self::SCOPE_PUBLIC;
429
	}
430
431
	/**
432
	 * Processes methods.
433
	 *
434
	 * @param integer          $class_id Class ID.
435
	 * @param \ReflectionClass $class    Class.
436
	 *
437
	 * @return void
438
	 */
439 14
	protected function processClassMethods($class_id, \ReflectionClass $class)
440
	{
441
		$sql = 'SELECT Name, Id
442
				FROM ClassMethods
443 14
				WHERE ClassId = :class_id';
444 14
		$old_methods = $this->db->fetchPairs($sql, array(
445 14
			'class_id' => $class_id,
446 14
		));
447
448
		$insert_sql = '	INSERT INTO ClassMethods (ClassId, Name, ParameterCount, RequiredParameterCount, Scope, IsAbstract, IsFinal, IsStatic, IsVariadic, ReturnsReference, HasReturnType, ReturnType)
449 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)';
450
		$update_sql = '	UPDATE ClassMethods
451
						SET	ParameterCount = :parameter_count,
452
							RequiredParameterCount = :required_parameter_count,
453
							Scope = :scope,
454
							IsAbstract = :is_abstract,
455
							IsFinal = :is_final,
456
							IsStatic = :is_static,
457
							IsVariadic = :is_variadic,
458
							ReturnsReference = :returns_reference,
459
							ReturnType = :return_type,
460
							HasReturnType = :has_return_type
461 14
						WHERE ClassId = :class_id AND Name = :name';
462
463 14
		$new_methods = array();
464 14
		$class_name = $class->getName();
465
466 14
		foreach ( $class->getMethods() as $method ) {
467 5
			if ( $method->class !== $class_name ) {
468 1
				continue;
469
			}
470
471 5
			$method_name = $method->getName();
472 5
			$new_methods[] = $method_name;
473
474
			// Doesn't work for parent classes (see https://github.com/goaop/parser-reflection/issues/16).
475 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...
476 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...
477
478 5
			$this->db->perform(
479 5
				isset($old_methods[$method_name]) ? $update_sql : $insert_sql,
480
				array(
481 5
					'class_id' => $class_id,
482 5
					'name' => $method_name,
483 5
					'parameter_count' => $method->getNumberOfParameters(),
484 5
					'required_parameter_count' => $method->getNumberOfRequiredParameters(),
485 5
					'scope' => $this->getMethodScope($method),
486 5
					'is_abstract' => (int)$method->isAbstract(),
487 5
					'is_final' => (int)$method->isFinal(),
488 5
					'is_static' => (int)$method->isStatic(),
489 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...
490 5
					'returns_reference' => (int)$method->returnsReference(),
491 5
					'has_return_type' => (int)$has_return_type,
492 5
					'return_type' => $return_type,
493
				)
494 5
			);
495
496 5
			$method_id = isset($old_methods[$method_name]) ? $old_methods[$method_name] : $this->db->lastInsertId();
497 5
			$this->processClassMethodParameters($method_id, $method);
498 14
		}
499
500 14
		$delete_methods = array_diff(array_keys($old_methods), $new_methods);
501
502 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...
503 1
			$this->deleteClassMethods($class_id, $delete_methods);
504 1
		}
505 14
	}
506
507
	/**
508
	 * Deletes methods.
509
	 *
510
	 * @param integer $class_id Class ID.
511
	 * @param array   $methods  Methods.
512
	 *
513
	 * @return void
514
	 */
515 2
	protected function deleteClassMethods($class_id, array $methods)
516
	{
517 2
		if ( $methods ) {
518
			// Delete only given methods.
519
			$sql = 'SELECT Id
520
					FROM ClassMethods
521 1
					WHERE ClassId = :class_id AND Name IN (:names)';
522 1
			$method_ids = $this->db->fetchCol($sql, array(
523 1
				'class_id' => $class_id,
524 1
				'names' => $methods,
525 1
			));
526 1
		}
527
		else {
528
			// Delete all methods in a class.
529
			$sql = 'SELECT Id
530
					FROM ClassMethods
531 2
					WHERE ClassId = :class_id';
532 2
			$method_ids = $this->db->fetchCol($sql, array(
533 2
				'class_id' => $class_id,
534 2
			));
535
		}
536
537
		// @codeCoverageIgnoreStart
538
		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...
539
			return;
540
		}
541
		// @codeCoverageIgnoreEnd
542
543 2
		$sql = 'DELETE FROM ClassMethods WHERE Id IN (:method_ids)';
544 2
		$this->db->perform($sql, array('method_ids' => $method_ids));
545
546 2
		$sql = 'DELETE FROM MethodParameters WHERE MethodId IN (:method_ids)';
547 2
		$this->db->perform($sql, array('method_ids' => $method_ids));
548 2
	}
549
550
	/**
551
	 * Processes method parameters.
552
	 *
553
	 * @param integer           $method_id Method ID.
554
	 * @param \ReflectionMethod $method    Method.
555
	 *
556
	 * @return void
557
	 */
558 5
	protected function processClassMethodParameters($method_id, \ReflectionMethod $method)
559
	{
560
		$sql = 'SELECT Name
561
				FROM MethodParameters
562 5
				WHERE MethodId = :method_id';
563 5
		$old_parameters = $this->db->fetchCol($sql, array(
564 5
			'method_id' => $method_id,
565 5
		));
566
567
		$insert_sql = '	INSERT INTO MethodParameters (MethodId, Name, Position, TypeClass, HasType, TypeName, AllowsNull, IsArray, IsCallable, IsOptional, IsVariadic, CanBePassedByValue, IsPassedByReference, HasDefaultValue, DefaultValue, DefaultConstant)
568 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)';
569
		$update_sql = '	UPDATE MethodParameters
570
						SET	Position = :position,
571
							TypeClass = :type_class,
572
							HasType = :has_type,
573
							TypeName = :type_name,
574
							AllowsNull = :allows_null,
575
							IsArray = :is_array,
576
							IsCallable = :is_callable,
577
							IsOptional = :is_optional,
578
							IsVariadic = :is_variadic,
579
							CanBePassedByValue = :can_be_passed_by_value,
580
							IsPassedByReference = :is_passed_by_reference,
581
							HasDefaultValue = :has_default_value,
582
							DefaultValue = :default_value,
583
							DefaultConstant = :default_constant
584 5
						WHERE MethodId = :method_id AND Name = :name';
585
586 5
		$new_parameters = array();
587
588 5
		foreach ( $method->getParameters() as $position => $parameter ) {
589 4
			$parameter_name = $parameter->getName();
590 4
			$new_parameters[] = $parameter_name;
591
592 4
			$type_class = $parameter->getClass();
593 4
			$type_class = $type_class ? $type_class->getName() : null;
594
595
			// Doesn't work for parent classes (see https://github.com/goaop/parser-reflection/issues/16).
596 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...
597 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...
598
599 4
			$has_default_value = $parameter->isDefaultValueAvailable();
600 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...
601
602 4
			$this->db->perform(
603 4
				in_array($parameter_name, $old_parameters) ? $update_sql : $insert_sql,
604
				array(
605 4
					'method_id' => $method_id,
606 4
					'name' => $parameter_name,
607 4
					'position' => $position,
608 4
					'type_class' => $type_class,
609 4
					'has_type' => (int)$has_type,
610 4
					'type_name' => $type_name,
611 4
					'allows_null' => (int)$parameter->allowsNull(),
612 4
					'is_array' => (int)$parameter->isArray(),
613 4
					'is_callable' => (int)$parameter->isCallable(),
614 4
					'is_optional' => (int)$parameter->isOptional(),
615 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...
616 4
					'can_be_passed_by_value' => (int)$parameter->canBePassedByValue(),
617 4
					'is_passed_by_reference' => (int)$parameter->isPassedByReference(),
618 4
					'has_default_value' => (int)$has_default_value,
619 4
					'default_value' => $has_default_value ? json_encode($parameter->getDefaultValue()) : null,
620 4
					'default_constant' => $default_value_is_constant ? $parameter->getDefaultValueConstantName() : null,
621
				)
622 4
			);
623 5
		}
624
625 5
		$delete_parameters = array_diff($old_parameters, $new_parameters);
626
627 5
		if ( $delete_parameters ) {
628
			$sql = 'DELETE FROM MethodParameters
629 2
					WHERE MethodId = :method_id AND Name IN (:names)';
630 2
			$this->db->perform($sql, array(
631 2
				'method_id' => $method_id,
632 2
				'names' => $delete_parameters,
633 2
			));
634 2
		}
635 5
	}
636
637
	/**
638
	 * Returns method scope.
639
	 *
640
	 * @param \ReflectionMethod $method Method.
641
	 *
642
	 * @return integer
643
	 */
644 5
	protected function getMethodScope(\ReflectionMethod $method)
645
	{
646 5
		if ( $method->isPrivate() ) {
647 1
			return self::SCOPE_PRIVATE;
648
		}
649
650 5
		if ( $method->isProtected() ) {
651 1
			return self::SCOPE_PROTECTED;
652
		}
653
654 5
		return self::SCOPE_PUBLIC;
655
	}
656
657
	/**
658
	 * Processes raw relations for all classes.
659
	 *
660
	 * @param KnowledgeBase $knowledge_base Knowledge base.
661
	 *
662
	 * @return void
663
	 */
664 4
	protected function processClassRawRelations(KnowledgeBase $knowledge_base)
665
	{
666
		$sql = 'SELECT Id, RawRelations
667
				FROM Classes
668 4
				WHERE RawRelations IS NOT NULL';
669 4
		$raw_relations = $this->db->yieldPairs($sql);
670
671 4
		foreach ( $raw_relations as $class_id => $class_raw_relations ) {
672
			$sql = 'SELECT RelatedClass
673
					FROM ClassRelations
674 4
					WHERE ClassId = :class_id';
675 4
			$old_class_relations = $this->db->fetchCol($sql, array(
676 4
				'class_id' => $class_id,
677 4
			));
678
679 4
			$new_class_relations = array();
680
681 4
			foreach ( json_decode($class_raw_relations, true) as $class_raw_relation ) {
682 4
				list ($related_class, $relation_type, $is_internal) = $class_raw_relation;
683
684 4
				$new_class_relations[] = $this->addRelation(
685 4
					$knowledge_base,
686 4
					$class_id,
687 4
					$related_class,
688 4
					$relation_type,
689 4
					$is_internal,
690
					$old_class_relations
691 4
				);
692 4
			}
693
694 4
			$delete_class_relations = array_diff($old_class_relations, $new_class_relations);
695
696 4
			if ( $delete_class_relations ) {
697
				$sql = 'DELETE FROM ClassRelations
698 1
						WHERE ClassId = :class_id AND RelatedClass IN (:related_classes)';
699 1
				$this->db->perform($sql, array(
700 1
					'class_id' => $class_id,
701 1
					'related_classes' => $delete_class_relations,
702 1
				));
703 1
			}
704 4
		}
705
706
		$sql = 'UPDATE Classes
707 4
				SET RawRelations = NULL';
708 4
		$this->db->perform($sql);
709 4
	}
710
711
	/**
712
	 * Adds a relation.
713
	 *
714
	 * @param KnowledgeBase $knowledge_base Knowledge base.
715
	 * @param integer       $class_id       Class ID.
716
	 * @param string        $related_class  Related class.
717
	 * @param integer       $relation_type  Relation type.
718
	 * @param boolean       $is_internal    Is internal.
719
	 * @param array         $old_relations  Old relations.
720
	 *
721
	 * @return string
722
	 */
723 4
	protected function addRelation(
724
		KnowledgeBase $knowledge_base,
725
		$class_id,
726
		$related_class,
727
		$relation_type,
728
		$is_internal,
729
		array $old_relations
730
	) {
731
		$insert_sql = '	INSERT INTO ClassRelations (ClassId, RelatedClass, RelatedClassId, RelationType)
732 4
						VALUES (:class_id, :related_class, :related_class_id, :relation_type)';
733
		$update_sql = ' UPDATE ClassRelations
734
						SET RelationType = :relation_type
735 4
						WHERE ClassId = :class_id AND RelatedClassId = :related_class_id';
736
737 4
		if ( $is_internal ) {
738 3
			$related_class_id = 0;
739 3
		}
740
		else {
741 2
			$related_class_file = realpath(ReflectionEngine::locateClassFile($related_class));
742
743
			$sql = 'SELECT Id
744
					FROM Classes
745 2
					WHERE FileId = :file_id AND Name = :name';
746 2
			$related_class_id = $this->db->fetchValue($sql, array(
747 2
				'file_id' => $knowledge_base->processFile($related_class_file),
748 2
				'name' => $related_class,
749 2
			));
750
		}
751
752 4
		$this->db->perform(
753 4
			in_array($related_class, $old_relations) ? $update_sql : $insert_sql,
754
			array(
755 4
				'class_id' => $class_id,
756 4
				'related_class' => $related_class,
757 4
				'related_class_id' => $related_class_id,
758 4
				'relation_type' => $relation_type,
759
			)
760 4
		);
761
762 4
		return $related_class;
763
	}
764
765
	/**
766
	 * Finds backward compatibility breaks.
767
	 *
768
	 * @param ExtendedPdoInterface $source_db Source database.
769
	 *
770
	 * @return array
771
	 */
772
	public function getBackwardsCompatibilityBreaks(ExtendedPdoInterface $source_db)
773
	{
774
		$ret = array(
775
			'Class Deleted' => array(),
776
			'Class Made Abstract' => array(),
777
			'Class Made Final' => array(),
778
			'Method Deleted' => array(),
779
			'Method Made Abstract' => array(),
780
			'Method Made Final' => array(),
781
			'Method Scope Reduced' => array(),
782
			'Method Signature Changed' => array(),
783
		);
784
785
		// 1. Get old->new class id mapping.
786
		$classes_sql = 'SELECT Name, Id, IsAbstract, IsFinal 
787
						FROM Classes';
788
		$source_classes = $source_db->fetchAssoc($classes_sql);
789
		$target_classes = $this->db->fetchAssoc($classes_sql);
790
791
		// 2. Deleted methods in kept classes.
792
		$source_class_methods_sql = '	SELECT Name, Id, Scope, IsAbstract, IsFinal
793
										FROM ClassMethods
794
										WHERE ClassId = :class_id AND Scope IN (' . self::SCOPE_PUBLIC . ',' . self::SCOPE_PROTECTED . ')';
795
		$target_class_methods_sql = '	SELECT Name, Id, Scope, IsAbstract, IsFinal
796
										FROM ClassMethods
797
										WHERE ClassId = :class_id';
798
799
		foreach ( $source_classes as $class_name => $source_class_data ) {
800
			// Deleted classes.
801
			if ( !isset($target_classes[$class_name]) ) {
802
				$ret['Class Deleted'][] = $class_name;
803
				continue;
804
			}
805
806
			$target_class_data = $target_classes[$class_name];
807
808
			if ( !$source_class_data['IsAbstract'] && $target_class_data['IsAbstract'] ) {
809
				$ret['Class Made Abstract'][] = $class_name;
810
			}
811
812
			if ( !$source_class_data['IsFinal'] && $target_class_data['IsFinal'] ) {
813
				$ret['Class Made Final'][] = $class_name;
814
			}
815
816
			$target_class_id = $target_class_data['Id'];
817
818
			$source_methods = $source_db->fetchAssoc($source_class_methods_sql, array('class_id' => $source_class_data['Id']));
819
			$target_methods = $this->db->fetchAssoc($target_class_methods_sql, array('class_id' => $target_class_id));
820
821
			foreach ( $source_methods as $source_method_name => $source_method_data ) {
822
				$method_name = $class_name . '::' . $source_method_name;
823
824
				// Deleted methods.
825
				if ( !isset($target_methods[$source_method_name]) ) {
826
					$ret['Method Deleted'][] = $method_name;
827
					continue;
828
				}
829
830
				$target_method_data = $target_methods[$source_method_name];
831
832
				if ( !$source_method_data['IsAbstract'] && $target_method_data['IsAbstract'] ) {
833
					$ret['Method Made Abstract'][] = $method_name;
834
				}
835
836
				if ( !$source_method_data['IsFinal'] && $target_method_data['IsFinal'] ) {
837
					$ret['Method Made Final'][] = $method_name;
838
				}
839
840
				if ( $source_method_data['Scope'] > $target_method_data['Scope'] ) {
841
					$incident = '<fg=white;options=bold>' . $method_name . '</>' . PHP_EOL;
842
					$incident .= 'OLD: ' . $this->getScopeName($source_method_data['Scope']) . PHP_EOL;
843
					$incident .= 'NEW: ' . $this->getScopeName($target_method_data['Scope']) . PHP_EOL;
844
845
					$ret['Method Scope Reduced'][] = $incident;
846
				}
847
848
				$source_signature = $this->calculateMethodParameterSignature($source_db, $source_method_data['Id']);
849
				$target_signature = $this->calculateMethodParameterSignature($this->db, $target_method_data['Id']);
850
851
				if ( $source_signature !== $target_signature ) {
852
					$incident = '<fg=white;options=bold>' . $method_name . '</>' . PHP_EOL;
853
					$incident .= 'OLD: ' . $source_signature . PHP_EOL;
854
					$incident .= 'NEW: ' . $target_signature . PHP_EOL;
855
856
					$ret['Method Signature Changed'][] = $incident;
857
				}
858
			}
859
		}
860
861
		return array_filter($ret);
862
	}
863
864
	/**
865
	 * Returns scope name.
866
	 *
867
	 * @param integer $scope Scope.
868
	 *
869
	 * @return string
870
	 */
871
	protected function getScopeName($scope)
872
	{
873
		$mapping = array(
874
			self::SCOPE_PRIVATE => 'private',
875
			self::SCOPE_PROTECTED => 'protected',
876
			self::SCOPE_PUBLIC => 'public',
877
		);
878
879
		return $mapping[$scope];
880
	}
881
882
	/**
883
	 * Calculates method parameter signature.
884
	 *
885
	 * @param ExtendedPdoInterface $db        Database.
886
	 * @param integer              $method_id Method ID.
887
	 *
888
	 * @return integer
889
	 */
890
	protected function calculateMethodParameterSignature(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...
891
	{
892
		$sql = 'SELECT *
893
				FROM MethodParameters
894
				WHERE MethodId = :method_id
895
				ORDER BY Position ASC';
896
		$method_parameters = $db->fetchAll($sql, array('method_id' => $method_id));
897
898
		$hash_parts = array();
899
900
		foreach ( $method_parameters as $method_parameter_data ) {
901
			if ( $method_parameter_data['HasType'] ) {
902
				$type = $method_parameter_data['TypeName'];
903
			}
904
			elseif ( $method_parameter_data['IsArray'] ) {
905
				$type = 'array';
906
			}
907
			elseif ( $method_parameter_data['IsCallable'] ) {
908
				$type = 'callable';
909
			}
910
			else {
911
				$type = $method_parameter_data['TypeClass'];
912
			}
913
914
			$hash_part = strlen($type) ? $type . ' ' : '';
915
916
			if ( $method_parameter_data['IsPassedByReference'] ) {
917
				$hash_part .= '&$' . $method_parameter_data['Name'];
918
			}
919
			else {
920
				$hash_part .= '$' . $method_parameter_data['Name'];
921
			}
922
923
			if ( $method_parameter_data['HasDefaultValue'] ) {
924
				$hash_part .= ' = ';
925
926
				if ( $method_parameter_data['DefaultConstant'] ) {
927
					$hash_part .= $method_parameter_data['DefaultConstant'];
928
				}
929
				else {
930
					$hash_part .= $method_parameter_data['DefaultValue'];
931
				}
932
			}
933
934
			$hash_parts[] = $hash_part;
935
		}
936
937
		return implode(', ', $hash_parts);
938
	}
939
940
}
941