Failed Conditions
Push — master ( 17cdc3...604e06 )
by Alexander
05:14
created

KnowledgeBase::processClassMethods()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 57
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 57
ccs 0
cts 47
cp 0
rs 7.6759
cc 7
eloc 34
nc 12
nop 2
crap 56

How to fix   Long Method   

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;
12
13
14
use Aura\Sql\ExtendedPdoInterface;
15
use Composer\Autoload\ClassLoader;
16
use ConsoleHelpers\ConsoleKit\ConsoleIO;
17
use Go\ParserReflection\Locator\CallableLocator;
18
use Go\ParserReflection\Locator\ComposerLocator;
19
use Go\ParserReflection\LocatorInterface;
20
use Go\ParserReflection\ReflectionEngine;
21
use Go\ParserReflection\ReflectionFile;
22
use Symfony\Component\Finder\Finder;
23
24
class KnowledgeBase
25
{
26
27
	const SCOPE_PRIVATE = 1;
28
29
	const SCOPE_PROTECTED = 2;
30
31
	const SCOPE_PUBLIC = 3;
32
33
	const CLASS_TYPE_CLASS = 1;
34
35
	const CLASS_TYPE_INTERFACE = 2;
36
37
	const CLASS_TYPE_TRAIT = 3;
38
39
	const RELATION_TYPE_EXTENDS = 1;
40
41
	const RELATION_TYPE_IMPLEMENTS = 2;
42
43
	/**
44
	 * Project path.
45
	 *
46
	 * @var string
47
	 */
48
	protected $projectPath = '';
49
50
	/**
51
	 * Regular expression for removing project path.
52
	 *
53
	 * @var string
54
	 */
55
	protected $projectPathRegExp = '';
56
57
	/**
58
	 * Database.
59
	 *
60
	 * @var ExtendedPdoInterface
61
	 */
62
	protected $db;
63
64
	/**
65
	 * Config
66
	 *
67
	 * @var array
68
	 */
69
	protected $config = array();
70
71
	/**
72
	 * Console IO.
73
	 *
74
	 * @var ConsoleIO
75
	 */
76
	protected $io;
77
78
	/**
79
	 * Creates knowledge base instance.
80
	 *
81
	 * @param string               $project_path Project path.
82
	 * @param ExtendedPdoInterface $db           Database.
83
	 * @param ConsoleIO            $io           Console IO.
84
	 *
85
	 * @throws \InvalidArgumentException When project path doesn't exist.
86
	 */
87
	public function __construct($project_path, ExtendedPdoInterface $db, ConsoleIO $io = null)
88
	{
89
		if ( !file_exists($project_path) || !is_dir($project_path) ) {
90
			throw new \InvalidArgumentException('The project path doesn\'t exist.');
91
		}
92
93
		$this->projectPath = $project_path;
94
		$this->projectPathRegExp = '#^' . preg_quote($project_path, '#') . '/#';
95
96
		$this->db = $db;
97
		$this->config = $this->getConfiguration();
98
		$this->io = $io;
99
	}
100
101
	/**
102
	 * Returns project configuration.
103
	 *
104
	 * @return array
105
	 * @throws \LogicException When configuration file is not found.
106
	 * @throws \LogicException When configuration file isn't in JSON format.
107
	 */
108
	protected function getConfiguration()
109
	{
110
		$config_file = $this->projectPath . '/.code-insight.json';
111
112
		if ( !file_exists($config_file) ) {
113
			throw new \LogicException(
114
				'Configuration file ".code-insight.json" not found at "' . $this->projectPath . '".'
115
			);
116
		}
117
118
		$config = json_decode(file_get_contents($config_file), true);
119
120
		if ( $config === null ) {
121
			throw new \LogicException('Configuration file ".code-insight.json" is not in JSON format.');
122
		}
123
124
		return $config;
125
	}
126
127
	/**
128
	 * Refreshes database.
129
	 *
130
	 * @return void
131
	 * @throws \LogicException When "$this->io" wasn't set upfront.
132
	 */
133
	public function refresh()
134
	{
135
		if ( !isset($this->io) ) {
136
			throw new \LogicException('The "$this->io" must be set prior to calling "$this->refresh()".');
137
		}
138
139
		//ReflectionEngine::$maximumCachedFiles = 10;
0 ignored issues
show
introduced by
No space found before comment text; expected "// ReflectionEngine::$maximumCachedFiles = 10;" but found "//ReflectionEngine::$maximumCachedFiles = 10;"
Loading history...
introduced by
Inline comments must end in full-stops, exclamation marks, or question marks
Loading history...
140
		ReflectionEngine::init($this->detectClassLocator());
141
142
		$sql = 'UPDATE Files
143
				SET Found = 0';
144
		$this->db->perform($sql);
145
146
		$files = array();
147
		$this->io->write('Searching for files ... ');
148
149
		foreach ( $this->getFinders() as $finder ) {
150
			$files = array_merge($files, array_keys(iterator_to_array($finder)));
151
		}
152
153
		$file_count = count($files);
154
		$this->io->writeln(array('<info>' . $file_count . ' found</info>', ''));
155
0 ignored issues
show
Coding Style introduced by
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
156
157
		$progress_bar = $this->io->createProgressBar($file_count + 2);
158
		$progress_bar->setMessage('');
159
		$progress_bar->setFormat(
160
			'%message%' . PHP_EOL . '%current%/%max% [%bar%] <info>%percent:3s%%</info> %elapsed:6s%/%estimated:-6s% <info>%memory:-10s%</info>'
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 140 characters; contains 144 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
161
		);
162
		$progress_bar->start();
163
164
		foreach ( $files as $file ) {
165
			$progress_bar->setMessage('Processing File: <info>' . $this->removeProjectPath($file) . '</info>');
166
			$progress_bar->display();
167
168
			$this->processFile($file);
169
170
			$progress_bar->advance();
171
		}
172
173
		$sql = 'SELECT Id
174
				FROM Files
175
				WHERE Found = 0';
176
		$deleted_files = $this->db->fetchCol($sql);
177
178
		if ( $deleted_files ) {
179
			$progress_bar->setMessage('Deleting Files ...');
180
			$progress_bar->display();
181
182
			$sql = 'SELECT Id
183
					FROM Classes
184
					WHERE FileId IN (:file_ids)';
185
			$deleted_classes = $this->db->fetchCol($sql, array(
186
				'file_ids' => $deleted_files,
187
			));
188
189
			foreach ( $deleted_classes as $deleted_class_id ) {
190
				$this->deleteClass($deleted_class_id);
191
			}
192
193
			$progress_bar->advance();
194
		}
195
196
		$progress_bar->setMessage('Processing Class Relations ...');
197
		$progress_bar->display();
198
199
		$this->processClassRawRelations();
200
201
		$progress_bar->advance();
202
203
		$progress_bar->finish();
204
		$progress_bar->clear();
205
0 ignored issues
show
Coding Style introduced by
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
206
207
	}
208
209
	/**
210
	 * Prints statistics about the code.
211
	 *
212
	 * @return array
213
	 */
214
	public function getStatistics()
215
	{
216
		$ret = array();
217
218
		$sql = 'SELECT COUNT(*)
219
				FROM Files';
220
		$file_count = $this->db->fetchValue($sql);
221
222
		$ret['Files'] = $file_count;
223
224
		$sql = 'SELECT ClassType, COUNT(*)
225
				FROM Classes
226
				GROUP BY ClassType';
227
		$classes_count = $this->db->fetchPairs($sql);
228
229
		foreach ( $classes_count as $class_type => $class_count ) {
230
			$title = 'Unknowns';
231
232
			if ( $class_type === self::CLASS_TYPE_CLASS ) {
233
				$title = 'Classes';
234
			}
235
			elseif ( $class_type === self::CLASS_TYPE_INTERFACE ) {
236
				$title = 'Interfaces';
237
			}
238
			elseif ( $class_type === self::CLASS_TYPE_TRAIT ) {
239
				$title = 'Traits';
240
			}
241
242
			$ret[$title] = $class_count;
243
		}
244
245
		return $ret;
246
	}
247
248
	/**
249
	 * Processes file.
250
	 *
251
	 * @param string $file File.
252
	 *
253
	 * @return integer
254
	 */
255
	protected function processFile($file)
256
	{
257
		$size = filesize($file);
258
		$relative_file = $this->removeProjectPath($file);
259
260
		$sql = 'SELECT Id, Size
261
				FROM Files
262
				WHERE Name = :name';
263
		$file_data = $this->db->fetchOne($sql, array(
264
			'name' => $relative_file,
265
		));
266
267
		$this->db->beginTransaction();
268
269
		if ( $file_data === false ) {
270
			$sql = 'INSERT INTO Files (Name, Size) VALUES (:name, :size)';
271
			$this->db->perform($sql, array(
272
				'name' => $relative_file,
273
				'size' => $size,
274
			));
275
276
			$file_id = $this->db->lastInsertId();
277
		}
278
		else {
279
			$file_id = $file_data['Id'];
280
		}
281
282
		// File is not changed since last time it was indexed.
283
		if ( $file_data !== false && (int)$file_data['Size'] === $size ) {
284
			$sql = 'UPDATE Files
285
					SET Found = 1
286
					WHERE Id = :file_id';
287
			$this->db->perform($sql, array(
288
				'file_id' => $file_data['Id'],
289
			));
290
291
			$this->db->commit();
292
293
			return $file_data['Id'];
294
		}
295
296
		$sql = 'UPDATE Files
297
				SET Found = 1
298
				WHERE Id = :file_id';
299
		$this->db->perform($sql, array(
300
			'file_id' => $file_data['Id'],
301
		));
302
303
		$new_classes = array();
304
		$parsed_file = new ReflectionFile($file);
305
306
		foreach ( $parsed_file->getFileNamespaces() as $namespace ) {
307
			foreach ( $namespace->getClasses() as $class ) {
308
				$new_classes[] = $class->getName();
309
				$this->processClass($file_id, $class);
310
			}
311
		}
312
313
		if ( $new_classes ) {
314
			$sql = 'SELECT Id
315
					FROM Classes
316
					WHERE FileId = :file_id AND Name NOT IN (:classes)';
317
			$deleted_classes = $this->db->fetchCol($sql, array(
318
				'file_id' => $file_id,
319
				'classes' => $new_classes,
320
			));
321
		}
322
		else {
323
			$sql = 'SELECT Id
324
					FROM Classes
325
					WHERE FileId = :file_id';
326
			$deleted_classes = $this->db->fetchCol($sql, array(
327
				'file_id' => $file_id,
328
			));
329
		}
330
331
		foreach ( $deleted_classes as $deleted_class_id ) {
332
			$this->deleteClass($deleted_class_id);
333
		}
334
335
		$this->db->commit();
336
337
		ReflectionEngine::unsetFile($file);
338
339
		return $file_id;
340
	}
341
342
	/**
343
	 * Processes class.
344
	 *
345
	 * @param integer          $file_id File ID.
346
	 * @param \ReflectionClass $class   Class.
347
	 *
348
	 * @return void
349
	 */
350
	protected function processClass($file_id, \ReflectionClass $class)
351
	{
352
		$sql = 'SELECT Id
353
				FROM Classes
354
				WHERE FileId = :file_id AND Name = :name';
355
		$class_id = $this->db->fetchValue($sql, array(
356
			'file_id' => $file_id,
357
			'name' => $class->getName(),
358
		));
359
360
		$raw_class_relations = $this->getRawClassRelations($class);
361
362
		if ( $class_id === false ) {
363
			$sql = 'INSERT INTO Classes (Name, ClassType, IsAbstract, IsFinal, FileId, RawRelations)
364
					VALUES (:name, :class_type, :is_abstract, :is_final, :file_id, :raw_relations)';
365
366
			$this->db->perform(
367
				$sql,
368
				array(
369
					'name' => $class->getName(),
370
					'class_type' => $this->getClassType($class),
371
					'is_abstract' => (int)$class->isAbstract(),
372
					'is_final' => (int)$class->isFinal(),
373
					'file_id' => $file_id,
374
					'raw_relations' => $raw_class_relations ? json_encode($raw_class_relations) : null,
375
				)
376
			);
377
378
			$class_id = $this->db->lastInsertId();
379
		}
380
		else {
381
			$sql = 'UPDATE Classes
382
					SET ClassType = :class_type, IsAbstract = :is_abstract, IsFinal = :is_final, RawRelations = :raw_relations
383
					WHERE Id = :class_id';
384
385
			$this->db->perform(
386
				$sql,
387
				array(
388
					'class_type' => $this->getClassType($class),
389
					'is_abstract' => (int)$class->isAbstract(),
390
					'is_final' => (int)$class->isFinal(),
391
					'raw_relations' => $raw_class_relations ? json_encode($raw_class_relations) : null,
392
					'class_id' => $class_id,
393
				)
394
			);
395
		}
396
397
		$this->processClassConstants($class_id, $class);
398
		$this->processClassProperties($class_id, $class);
399
		$this->processClassMethods($class_id, $class);
400
	}
401
402
	/**
403
	 * Returns class type.
404
	 *
405
	 * @param \ReflectionClass $class Class.
406
	 *
407
	 * @return integer
408
	 */
409
	protected function getClassType(\ReflectionClass $class)
410
	{
411
		if ( $class->isInterface() ) {
412
			return self::CLASS_TYPE_INTERFACE;
413
		}
414
415
		if ( $class->isTrait() ) {
416
			return self::CLASS_TYPE_TRAIT;
417
		}
418
419
		return self::CLASS_TYPE_CLASS;
420
	}
421
422
	/**
423
	 * Get relations.
424
	 *
425
	 * @param \ReflectionClass $class Class.
426
	 *
427
	 * @return array
428
	 */
429
	protected function getRawClassRelations(\ReflectionClass $class)
430
	{
431
		$raw_relations = array();
432
		$parent_class = $class->getParentClass();
433
434
		if ( $parent_class ) {
435
			$raw_relations[] = array(
436
				$parent_class->getName(),
437
				self::RELATION_TYPE_EXTENDS,
438
				$parent_class->isInternal(),
439
			);
440
		}
441
442
		foreach ( $class->getInterfaces() as $interface ) {
443
			$raw_relations[] = array(
444
				$interface->getName(),
445
				self::RELATION_TYPE_IMPLEMENTS,
446
				$interface->isInternal(),
447
			);
448
		}
449
450
		return $raw_relations;
451
	}
452
453
	/**
454
	 * Deletes a class.
455
	 *
456
	 * @param integer $class_id Class ID.
457
	 *
458
	 * @return void
459
	 */
460
	protected function deleteClass($class_id)
461
	{
462
		$sql = 'DELETE FROM ClassConstants WHERE ClassId = :class_id';
463
		$this->db->perform($sql, array('class_id' => $class_id));
464
465
		$sql = 'DELETE FROM ClassProperties WHERE ClassId = :class_id';
466
		$this->db->perform($sql, array('class_id' => $class_id));
467
468
		$this->deleteClassMethods($class_id, array());
469
470
		$sql = 'DELETE FROM ClassRelations WHERE ClassId = :class_id';
471
		$this->db->perform($sql, array('class_id' => $class_id));
472
473
		$sql = 'DELETE FROM ClassRelations WHERE RelatedClassId = :class_id';
474
		$this->db->perform($sql, array('class_id' => $class_id));
475
	}
476
477
	/**
478
	 * Processes constants.
479
	 *
480
	 * @param integer          $class_id Class ID.
481
	 * @param \ReflectionClass $class    Class.
482
	 *
483
	 * @return void
484
	 */
485
	protected function processClassConstants($class_id, \ReflectionClass $class)
486
	{
487
		$constants = $class->getConstants();
488
489
		$sql = 'SELECT Name
490
				FROM ClassConstants
491
				WHERE ClassId = :class_id';
492
		$old_constants = $this->db->fetchCol($sql, array(
493
			'class_id' => $class_id,
494
		));
495
496
		$insert_sql = 'INSERT INTO ClassConstants (ClassId, Name, Value) VALUES (:class_id, :name, :value)';
497
		$update_sql = 'UPDATE ClassConstants SET Value = :value WHERE ClassId = :class_id AND Name = :name';
498
499
		foreach ( $constants as $constant_name => $constant_value ) {
500
			$this->db->perform(
501
				in_array($constant_name, $old_constants) ? $update_sql : $insert_sql,
502
				array(
503
					'class_id' => $class_id,
504
					'name' => $constant_name,
505
					'value' => json_encode($constant_value),
506
				)
507
			);
508
		}
509
510
		$deleted_constants = array_diff($old_constants, array_keys($constants));
511
512
		if ( $deleted_constants ) {
513
			$sql = 'DELETE FROM ClassConstants
514
					WHERE ClassId = :class_id AND Name IN (:names)';
515
			$this->db->perform($sql, array(
516
				'class_id' => $class_id,
517
				'names' => $deleted_constants,
518
			));
519
		}
520
	}
521
522
	/**
523
	 * Processes properties.
524
	 *
525
	 * @param integer          $class_id Class ID.
526
	 * @param \ReflectionClass $class    Class.
527
	 *
528
	 * @return void
529
	 */
530
	protected function processClassProperties($class_id, \ReflectionClass $class)
531
	{
532
		$sql = 'SELECT Name
533
				FROM ClassProperties
534
				WHERE ClassId = :class_id';
535
		$old_properties = $this->db->fetchCol($sql, array(
536
			'class_id' => $class_id,
537
		));
538
539
		$insert_sql = '	INSERT INTO ClassProperties (ClassId, Name, Value, Scope, IsStatic)
540
						VALUES (:class_id, :name, :value, :scope, :is_static)';
541
		$update_sql = '	UPDATE ClassProperties
542
						SET Value = :value, Scope = :scope, IsStatic = :is_static
543
						WHERE ClassId = :class_id AND Name = :name';
544
545
		$new_properties = array();
546
		$property_defaults = $class->getDefaultProperties();
547
		$static_properties = $class->getStaticProperties();
548
		$class_name = $class->getName();
549
550
		foreach ( $class->getProperties() as $property ) {
551
			if ( $property->class !== $class_name ) {
552
				continue;
553
			}
554
555
			$property_name = $property->getName();
556
			$property_value = isset($property_defaults[$property_name]) ? $property_defaults[$property_name] : null;
557
			$new_properties[] = $property_name;
558
559
			$this->db->perform(
560
				in_array($property_name, $old_properties) ? $update_sql : $insert_sql,
561
				array(
562
					'class_id' => $class_id,
563
					'name' => $property_name,
564
					'value' => json_encode($property_value),
565
					'scope' => $this->getPropertyScope($property),
566
					'is_static' => (int)in_array($property_name, $static_properties),
567
				)
568
			);
569
		}
570
571
		$deleted_properties = array_diff($old_properties, $new_properties);
572
573
		if ( $deleted_properties ) {
574
			$sql = 'DELETE FROM ClassProperties
575
					WHERE ClassId = :class_id AND Name IN (:names)';
576
			$this->db->perform($sql, array(
577
				'class_id' => $class_id,
578
				'names' => $deleted_properties,
579
			));
580
		}
581
	}
582
583
	/**
584
	 * Returns property scope.
585
	 *
586
	 * @param \ReflectionProperty $property Property.
587
	 *
588
	 * @return integer
589
	 */
590
	protected function getPropertyScope(\ReflectionProperty $property)
591
	{
592
		if ( $property->isPrivate() ) {
593
			return self::SCOPE_PRIVATE;
594
		}
595
596
		if ( $property->isProtected() ) {
597
			return self::SCOPE_PROTECTED;
598
		}
599
600
		return self::SCOPE_PUBLIC;
601
	}
602
603
	/**
604
	 * Processes methods.
605
	 *
606
	 * @param integer          $class_id Class ID.
607
	 * @param \ReflectionClass $class    Class.
608
	 *
609
	 * @return void
610
	 */
611
	protected function processClassMethods($class_id, \ReflectionClass $class)
612
	{
613
		$sql = 'SELECT Name, Id
614
				FROM ClassMethods
615
				WHERE ClassId = :class_id';
616
		$old_methods = $this->db->fetchPairs($sql, array(
617
			'class_id' => $class_id,
618
		));
619
620
		$insert_sql = '	INSERT INTO ClassMethods (ClassId, Name, ParameterCount, RequiredParameterCount, Scope, IsAbstract, IsFinal, IsStatic, ReturnsReference, HasReturnType, ReturnType)
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 140 characters; contains 187 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
621
						VALUES (:class_id, :name, :parameter_count, :required_parameter_count, :scope, :is_abstract, :is_final, :is_static, :returns_reference, :has_return_type, :return_type)';
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 140 characters; contains 193 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
622
		$update_sql = '	UPDATE ClassMethods
623
						SET ParameterCount = :parameter_count, RequiredParameterCount = :required_parameter_count, Scope = :scope, IsAbstract = :is_abstract, IsFinal = :is_final, IsStatic = :is_static, ReturnsReference = :returns_reference, ReturnType = :return_type, HasReturnType = :has_return_type
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 140 characters; contains 300 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
624
						WHERE ClassId = :class_id AND Name = :name';
625
626
		$new_methods = array();
627
		$class_name = $class->getName();
628
629
		foreach ( $class->getMethods() as $method ) {
630
			if ( $method->class !== $class_name ) {
631
				continue;
632
			}
633
634
			$method_name = $method->getName();
635
			$new_methods[] = $method_name;
636
637
			// Doesn't work for parent classes (see https://github.com/goaop/parser-reflection/issues/16).
638
			$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...
639
			$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...
640
641
			$this->db->perform(
642
				isset($old_methods[$method_name]) ? $update_sql : $insert_sql,
643
				array(
644
					'class_id' => $class_id,
645
					'name' => $method_name,
646
					'parameter_count' => $method->getNumberOfParameters(),
647
					'required_parameter_count' => $method->getNumberOfRequiredParameters(),
648
					'scope' => $this->getMethodScope($method),
649
					'is_abstract' => (int)$method->isAbstract(),
650
					'is_final' => (int)$method->isFinal(),
651
					'is_static' => (int)$method->isStatic(),
652
					'returns_reference' => (int)$method->returnsReference(),
653
					'has_return_type' => (int)$has_return_type,
654
					'return_type' => $return_type,
655
				)
656
			);
657
658
			$method_id = isset($old_methods[$method_name]) ? $old_methods[$method_name] : $this->db->lastInsertId();
659
			$this->processClassMethodParameters($method_id, $method);
660
		}
661
662
		$deleted_methods = array_diff($old_methods, $new_methods);
663
664
		if ( $deleted_methods ) {
665
			$this->deleteClassMethods($class_id, $deleted_methods);
666
		}
667
	}
668
669
	/**
670
	 * Deletes methods.
671
	 *
672
	 * @param integer $class_id Class ID.
673
	 * @param array   $methods  Methods.
674
	 *
675
	 * @return void
676
	 */
677
	protected function deleteClassMethods($class_id, array $methods)
678
	{
679
		if ( $methods ) {
680
			$sql = 'SELECT Id
681
					FROM ClassMethods
682
					WHERE ClassId = :class_id AND Name IN (:names)';
683
			$method_ids = $this->db->fetchCol($sql, array(
684
				'class_id' => $class_id,
685
				'names' => $methods,
686
			));
687
		}
688
		else {
689
			$sql = 'SELECT Id
690
					FROM ClassMethods
691
					WHERE ClassId = :class_id';
692
			$method_ids = $this->db->fetchCol($sql, array(
693
				'class_id' => $class_id,
694
			));
695
		}
696
697
		if ( !$method_ids ) {
698
			return;
699
		}
700
701
		$sql = 'DELETE FROM ClassMethods WHERE Id IN (:method_ids)';
702
		$this->db->perform($sql, array('method_ids' => $method_ids));
703
704
		$sql = 'DELETE FROM MethodParameters WHERE MethodId IN (:method_ids)';
705
		$this->db->perform($sql, array('method_ids' => $method_ids));
706
707
	}
708
709
	/**
710
	 * Processes method parameters.
711
	 *
712
	 * @param integer           $method_id Method ID.
713
	 * @param \ReflectionMethod $method    Method.
714
	 *
715
	 * @return void
716
	 */
717
	protected function processClassMethodParameters($method_id, \ReflectionMethod $method)
718
	{
719
		$sql = 'SELECT Name
720
				FROM MethodParameters
721
				WHERE MethodId = :method_id';
722
		$old_parameters = $this->db->fetchCol($sql, array(
723
			'method_id' => $method_id,
724
		));
725
726
		$insert_sql = '	INSERT INTO MethodParameters (MethodId, Name, TypeClass, HasType, TypeName, AllowsNull, IsArray, IsCallable, IsOptional, IsVariadic, CanBePassedByValue, IsPassedByReference, HasDefaultValue, DefaultValue, DefaultConstant)
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 140 characters; contains 245 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
727
						VALUES (:method_id, :name, :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)';
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 140 characters; contains 260 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
728
		$update_sql = '	UPDATE MethodParameters
729
						SET TypeClass = :type_class, HasType = :has_type, TypeName = :type_name, AllowsNull = :allows_null, IsArray = :is_array, IsCallable = :is_callable, IsOptional = :is_optional, IsVariadic = :is_variadic, CanBePassedByValue = :can_be_passed_by_value, IsPassedByReference = :is_passed_by_reference, HasDefaultValue = :has_default_value, DefaultValue = :default_value, DefaultConstant = :default_constant
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 140 characters; contains 423 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
730
						WHERE MethodId = :method_id AND Name = :name';
731
732
		$new_parameters = array();
733
734
		foreach ( $method->getParameters() as $parameter ) {
735
			$parameter_name = $parameter->getName();
736
			$new_parameters[] = $parameter_name;
737
738
			$type_class = $parameter->getClass();
739
			$type_class = $type_class ? $type_class->getName() : null;
740
741
			// Doesn't work for parent classes (see https://github.com/goaop/parser-reflection/issues/16).
742
			$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...
743
			$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...
744
745
			$has_default_value = $parameter->isDefaultValueAvailable();
746
			$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...
747
748
			$this->db->perform(
749
				isset($old_parameters[$parameter_name]) ? $update_sql : $insert_sql,
750
				array(
751
					'method_id' => $method_id,
752
					'name' => $parameter_name,
753
					'type_class' => $type_class,
754
					'has_type' => (int)$has_type,
755
					'type_name' => $type_name,
756
					'allows_null' => (int)$parameter->allowsNull(),
757
					'is_array' => (int)$parameter->isArray(),
758
					'is_callable' => (int)$parameter->isCallable(),
759
					'is_optional' => (int)$parameter->isOptional(),
760
					'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...
761
					'can_be_passed_by_value' => (int)$parameter->canBePassedByValue(),
762
					'is_passed_by_reference' => (int)$parameter->isPassedByReference(),
763
					'has_default_value' => (int)$has_default_value,
764
					'default_value' => $has_default_value ? json_encode($parameter->getDefaultValue()) : null,
765
					'default_constant' => $default_value_is_constant ? $parameter->getDefaultValueConstantName() : null,
0 ignored issues
show
Bug introduced by
The method getDefaultValueConstantName() does not exist on ReflectionParameter. Did you maybe mean getDefaultValue()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
766
				)
767
			);
768
		}
769
770
		$deleted_parameters = array_diff($old_parameters, $new_parameters);
771
772
		if ( $deleted_parameters ) {
773
			$sql = 'DELETE FROM MethodParameters
774
					WHERE MethodId = :method_id AND Name IN (:names)';
775
			$this->db->perform($sql, array(
776
				'method_id' => $method_id,
777
				'names' => $deleted_parameters,
778
			));
779
		}
780
	}
781
782
	/**
783
	 * Returns method scope.
784
	 *
785
	 * @param \ReflectionMethod $method Method.
786
	 *
787
	 * @return integer
788
	 */
789
	protected function getMethodScope(\ReflectionMethod $method)
790
	{
791
		if ( $method->isPrivate() ) {
792
			return self::SCOPE_PRIVATE;
793
		}
794
795
		if ( $method->isProtected() ) {
796
			return self::SCOPE_PROTECTED;
797
		}
798
799
		return self::SCOPE_PUBLIC;
800
	}
801
802
	/**
803
	 * Processes raw relations for all classes.
804
	 *
805
	 * @return void
806
	 */
807
	protected function processClassRawRelations()
808
	{
809
		$sql = 'SELECT Id, RawRelations
810
				FROM Classes
811
				WHERE RawRelations IS NOT NULL';
812
		$raw_relations = $this->db->yieldPairs($sql, array(
813
			'empty_relations' => json_encode(array()),
814
		));
815
816
		foreach ( $raw_relations as $class_id => $class_raw_relations ) {
817
			$sql = 'SELECT RelatedClass
818
					FROM ClassRelations
819
					WHERE ClassId = :class_id';
820
			$old_class_relations = $this->db->fetchCol($sql, array(
821
				'class_id' => $class_id,
822
			));
823
824
			$new_class_relations = array();
825
826
			foreach ( json_decode($class_raw_relations, true) as $class_raw_relation ) {
827
				list ($related_class, $relation_type, $is_internal) = $class_raw_relation;
828
829
				$new_class_relations[] = $this->addRelation(
830
					$class_id,
831
					$related_class,
832
					$relation_type,
833
					$is_internal,
834
					$old_class_relations
835
				);
836
			}
837
838
			$delete_class_relations = array_diff($old_class_relations, $new_class_relations);
839
840
			if ( $delete_class_relations ) {
841
				$sql = 'DELETE FROM ClassRelations
842
						WHERE ClassId = :class_id AND RelatedClassId IN (:related_class_ids)';
843
				$this->db->perform($sql, array(
844
					'class_id' => $class_id,
845
					'related_class_ids' => $delete_class_relations,
846
				));
847
			}
848
		}
849
850
		$sql = 'UPDATE Classes
851
				SET RawRelations = NULL';
852
		$this->db->perform($sql);
853
	}
854
855
	/**
856
	 * Adds a relation.
857
	 *
858
	 * @param integer $class_id      Class ID.
859
	 * @param string  $related_class Related class.
860
	 * @param integer $relation_type Relation type.
861
	 * @param boolean $is_internal   Is internal.
862
	 * @param array   $old_relations Old relations.
863
	 *
864
	 * @return string
865
	 */
866
	protected function addRelation($class_id, $related_class, $relation_type, $is_internal, array $old_relations)
867
	{
868
		$insert_sql = '	INSERT INTO ClassRelations (ClassId, RelatedClass, RelatedClassId, RelationType)
869
						VALUES (:class_id, :related_class, :related_class_id, :relation_type)';
870
		$update_sql = ' UPDATE ClassRelations
871
						SET RelationType = :relation_type
872
						WHERE ClassId = :class_id AND RelatedClassId = :related_class_id';
873
874
		if ( $is_internal ) {
875
			$related_class_id = 0;
876
		}
877
		else {
878
			$related_class_file = realpath(ReflectionEngine::locateClassFile($related_class));
879
880
			$sql = 'SELECT Id
881
					FROM Classes
882
					WHERE FileId = :file_id AND Name = :name';
883
			$related_class_id = $this->db->fetchValue($sql, array(
884
				'file_id' => $this->processFile($related_class_file),
885
				'name' => $related_class,
886
			));
887
		}
888
889
		$this->db->perform(
890
			in_array($related_class, $old_relations) ? $update_sql : $insert_sql,
891
			array(
892
				'class_id' => $class_id,
893
				'related_class' => $related_class,
894
				'related_class_id' => $related_class_id,
895
				'relation_type' => $relation_type,
896
			)
897
		);
898
899
		return $related_class;
900
	}
901
902
	/**
903
	 * Determines class locator.
904
	 *
905
	 * @return LocatorInterface
906
	 * @throws \LogicException When file in "class_locator" setting doesn't exist.
907
	 */
908
	protected function detectClassLocator()
909
	{
910
		$class_locator = null;
911
912
		if ( isset($this->config['class_locator']) ) {
913
			$class_locator_file = $this->resolveProjectPath($this->config['class_locator']);
914
915
			if ( !file_exists($class_locator_file) || !is_file($class_locator_file) ) {
916
				throw new \LogicException(
917
					'The "' . $this->config['class_locator'] . '" class locator doesn\'t exist.'
918
				);
919
			}
920
921
			$class_locator = require $class_locator_file;
922
		}
923
		else {
924
			$class_locator_file = $this->resolveProjectPath('vendor/autoload.php');
925
926
			if ( file_exists($class_locator_file) && is_file($class_locator_file) ) {
927
				$class_locator = require $class_locator_file;
928
			}
929
		}
930
931
		// Make sure memory limit isn't changed by class locator.
932
		ini_restore('memory_limit');
933
934
		if ( is_callable($class_locator) ) {
935
			return new CallableLocator($class_locator);
936
		}
937
		elseif ( $class_locator instanceof ClassLoader ) {
938
			return new ComposerLocator($class_locator);
939
		}
940
941
		throw new \LogicException(
942
			'The "class_loader" setting must point to "vendor/autoload.php" or a file, that would return closure.'
943
		);
944
	}
945
946
	/**
947
	 * Processes the Finders configuration list.
948
	 *
949
	 * @return Finder[]
950
	 * @throws \LogicException If "finder" setting doesn't exist.
951
	 * @throws \LogicException If the configured method does not exist.
952
	 */
953
	protected function getFinders()
954
	{
955
		// Process "finder" config setting.
956
		if ( !isset($this->config['finder']) ) {
957
			throw new \LogicException('The "finder" setting must be present in config file.');
958
		}
959
960
		$finders = array();
961
962
		foreach ( $this->config['finder'] as $methods ) {
963
			$finder = Finder::create()->files();
964
965
			if ( isset($methods['in']) ) {
966
				$methods['in'] = (array)$methods['in'];
967
968
				foreach ( $methods['in'] as $folder_index => $in_folder ) {
969
					$methods['in'][$folder_index] = $this->resolveProjectPath($in_folder);
970
				}
971
			}
972
973
			foreach ( $methods as $method => $arguments ) {
974
				if ( !method_exists($finder, $method) ) {
975
					throw new \LogicException(sprintf(
976
						'The method "Finder::%s" does not exist.',
977
						$method
978
					));
979
				}
980
981
				$arguments = (array)$arguments;
982
983
				foreach ( $arguments as $argument ) {
984
					$finder->$method($argument);
985
				}
986
			}
987
988
			$finders[] = $finder;
989
		}
990
991
		return $finders;
992
	}
993
994
	/**
995
	 * Resolves path within project.
996
	 *
997
	 * @param string $relative_path Relative path.
998
	 *
999
	 * @return string
1000
	 */
1001
	protected function resolveProjectPath($relative_path)
1002
	{
1003
		return realpath($this->projectPath . DIRECTORY_SEPARATOR . $relative_path);
1004
	}
1005
1006
	/**
1007
	 * Removes project path from file path.
1008
	 *
1009
	 * @param string $absolute_path Absolute path.
1010
	 *
1011
	 * @return string
1012
	 */
1013
	protected function removeProjectPath($absolute_path)
1014
	{
1015
		return preg_replace($this->projectPathRegExp, '', $absolute_path, 1);
1016
	}
1017
1018
}
1019