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

KnowledgeBase::refresh()   B

Complexity

Conditions 7
Paths 17

Size

Total Lines 67
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 67
ccs 0
cts 45
cp 0
rs 7.0237
c 1
b 0
f 0
nc 17
cc 7
eloc 38
nop 0
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\CodeInsight\BackwardsCompatibility\AbstractChecker;
17
use ConsoleHelpers\CodeInsight\BackwardsCompatibility\CheckerFactory;
18
use ConsoleHelpers\CodeInsight\KnowledgeBase\DataCollector\AbstractDataCollector;
19
use ConsoleHelpers\CodeInsight\KnowledgeBase\DataCollector\ClassDataCollector;
20
use ConsoleHelpers\CodeInsight\KnowledgeBase\DataCollector\ConstantDataCollector;
21
use ConsoleHelpers\CodeInsight\KnowledgeBase\DataCollector\FunctionDataCollector;
22
use ConsoleHelpers\ConsoleKit\ConsoleIO;
23
use Go\ParserReflection\Locator\CallableLocator;
24
use Go\ParserReflection\Locator\ComposerLocator;
25
use Go\ParserReflection\LocatorInterface;
26
use Go\ParserReflection\ReflectionEngine;
27
use Go\ParserReflection\ReflectionFile;
28
use Symfony\Component\Finder\Finder;
29
30
class KnowledgeBase
31
{
32
33
	/**
34
	 * Project path.
35
	 *
36
	 * @var string
37
	 */
38
	protected $projectPath = '';
39
40
	/**
41
	 * Regular expression for removing project path.
42
	 *
43
	 * @var string
44
	 */
45
	protected $projectPathRegExp = '';
46
47
	/**
48
	 * Database.
49
	 *
50
	 * @var ExtendedPdoInterface
51
	 */
52
	protected $db;
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...
53
54
	/**
55
	 * Config
56
	 *
57
	 * @var array
58
	 */
59
	protected $config = array();
60
61
	/**
62
	 * Data collectors.
63
	 *
64
	 * @var AbstractDataCollector[]
65
	 */
66
	protected $dataCollectors = array();
67
68
	/**
69
	 * Console IO.
70
	 *
71
	 * @var ConsoleIO
72
	 */
73
	protected $io;
1 ignored issue
show
Comprehensibility introduced by
Avoid variables with short names like $io. 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...
74
75
	/**
76
	 * Creates knowledge base instance.
77
	 *
78
	 * @param string               $project_path Project path.
79
	 * @param ExtendedPdoInterface $db           Database.
80
	 * @param ConsoleIO            $io           Console IO.
81
	 *
82
	 * @throws \InvalidArgumentException When project path doesn't exist.
83
	 */
84
	public function __construct($project_path, ExtendedPdoInterface $db, ConsoleIO $io = null)
2 ignored issues
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...
Comprehensibility introduced by
Avoid variables with short names like $io. 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...
85
	{
86
		if ( !file_exists($project_path) || !is_dir($project_path) ) {
87
			throw new \InvalidArgumentException('The project path doesn\'t exist.');
88
		}
89
90
		$this->projectPath = $project_path;
91
		$this->projectPathRegExp = '#^' . preg_quote($project_path, '#') . '/#';
92
93
		$this->db = $db;
94
		$this->config = $this->getConfiguration();
95
		$this->io = $io;
96
97
		$this->dataCollectors[] = new ClassDataCollector($db);
98
		$this->dataCollectors[] = new ConstantDataCollector($db);
99
		$this->dataCollectors[] = new FunctionDataCollector($db);
100
	}
101
102
	/**
103
	 * Returns database.
104
	 *
105
	 * @return ExtendedPdoInterface
106
	 */
107
	public function getDatabase()
108
	{
109
		return $this->db;
110
	}
111
112
	/**
113
	 * Returns project configuration.
114
	 *
115
	 * @return array
116
	 * @throws \LogicException When configuration file is not found.
117
	 * @throws \LogicException When configuration file isn't in JSON format.
118
	 */
119
	protected function getConfiguration()
120
	{
121
		$config_file = $this->projectPath . '/.code-insight.json';
122
123
		if ( !file_exists($config_file) ) {
124
			throw new \LogicException(
125
				'Configuration file ".code-insight.json" not found at "' . $this->projectPath . '".'
126
			);
127
		}
128
129
		$config = json_decode(file_get_contents($config_file), true);
130
131
		if ( $config === null ) {
132
			throw new \LogicException('Configuration file ".code-insight.json" is not in JSON format.');
133
		}
134
135
		return $config;
136
	}
137
138
	/**
139
	 * Refreshes database.
140
	 *
141
	 * @return void
142
	 * @throws \LogicException When "$this->io" wasn't set upfront.
143
	 */
144
	public function refresh()
145
	{
146
		if ( !isset($this->io) ) {
147
			throw new \LogicException('The "$this->io" must be set prior to calling "$this->refresh()".');
148
		}
149
150
		ReflectionEngine::setMaximumCachedFiles(20);
151
		ReflectionEngine::init($this->detectClassLocator());
152
153
		$sql = 'UPDATE Files
154
				SET Found = 0';
155
		$this->db->perform($sql);
156
157
		$files = array();
158
		$this->io->write('Searching for files ... ');
159
160
		foreach ( $this->getFinders() as $finder ) {
161
			$files = array_merge($files, array_keys(iterator_to_array($finder)));
162
		}
163
164
		$file_count = count($files);
165
		$this->io->writeln(array('<info>' . $file_count . ' found</info>', ''));
166
167
		$progress_bar = $this->io->createProgressBar($file_count + 2);
168
		$progress_bar->setMessage('');
169
		$progress_bar->setFormat(
170
			'%message%' . PHP_EOL . '%current%/%max% [%bar%] <info>%percent:3s%%</info> %elapsed:6s%/%estimated:-6s% <info>%memory:-10s%</info>'
171
		);
172
		$progress_bar->start();
173
174
		foreach ( $files as $file ) {
175
			$progress_bar->setMessage('Processing file: <info>' . $this->removeProjectPath($file) . '</info>');
176
			$progress_bar->display();
177
178
			$this->processFile($file);
179
180
			$progress_bar->advance();
181
		}
182
183
		$sql = 'SELECT Id
184
				FROM Files
185
				WHERE Found = 0';
186
		$deleted_files = $this->db->fetchCol($sql);
187
188
		if ( $deleted_files ) {
189
			$progress_bar->setMessage('Erasing information about deleted files ...');
190
			$progress_bar->display();
191
192
			foreach ( $this->dataCollectors as $data_collector ) {
193
				$data_collector->deleteData($deleted_files);
194
			}
195
196
			$progress_bar->advance();
197
		}
198
199
		$progress_bar->setMessage('Aggregating processed data ...');
200
		$progress_bar->display();
201
202
		foreach ( $this->dataCollectors as $data_collector ) {
203
			$data_collector->aggregateData($this);
204
		}
205
206
		$progress_bar->advance();
207
208
		$progress_bar->finish();
209
		$progress_bar->clear();
210
	}
211
212
	/**
213
	 * Prints statistics about the code.
214
	 *
215
	 * @return array
216
	 */
217
	public function getStatistics()
218
	{
219
		$ret = array();
220
221
		$sql = 'SELECT COUNT(*)
222
				FROM Files';
223
		$ret['Files'] = $this->db->fetchValue($sql);
224
225
		foreach ( $this->dataCollectors as $data_collector ) {
226
			$ret = array_merge($ret, $data_collector->getStatistics());
227
		}
228
229
		return $ret;
230
	}
231
232
	/**
233
	 * Processes file.
234
	 *
235
	 * @param string $file File.
236
	 *
237
	 * @return integer
238
	 */
239
	public function processFile($file)
240
	{
241
		$size = filesize($file);
242
		$relative_file = $this->removeProjectPath($file);
243
244
		$sql = 'SELECT Id, Size
245
				FROM Files
246
				WHERE Name = :name';
247
		$file_data = $this->db->fetchOne($sql, array(
248
			'name' => $relative_file,
249
		));
250
251
		$this->db->beginTransaction();
252
253
		if ( $file_data === false ) {
254
			$sql = 'INSERT INTO Files (Name, Size) VALUES (:name, :size)';
255
			$this->db->perform($sql, array(
256
				'name' => $relative_file,
257
				'size' => $size,
258
			));
259
260
			$file_id = $this->db->lastInsertId();
261
		}
262
		else {
263
			$file_id = $file_data['Id'];
264
		}
265
266
		// File is not changed since last time it was indexed.
267
		if ( $file_data !== false && (int)$file_data['Size'] === $size ) {
268
			$sql = 'UPDATE Files
269
					SET Found = 1
270
					WHERE Id = :file_id';
271
			$this->db->perform($sql, array(
272
				'file_id' => $file_data['Id'],
273
			));
274
275
			$this->db->commit();
276
277
			return $file_data['Id'];
278
		}
279
280
		$sql = 'UPDATE Files
281
				SET Found = 1
282
				WHERE Id = :file_id';
283
		$this->db->perform($sql, array(
284
			'file_id' => $file_data['Id'],
285
		));
286
287
		$parsed_file = new ReflectionFile($file);
288
289
		foreach ( $parsed_file->getFileNamespaces() as $namespace ) {
290
			foreach ( $this->dataCollectors as $data_collector ) {
291
				$data_collector->collectData($file_id, $namespace);
292
			}
293
		}
294
295
		$this->db->commit();
296
297
		return $file_id;
298
	}
299
300
	/**
301
	 * Determines class locator.
302
	 *
303
	 * @return LocatorInterface
304
	 * @throws \LogicException When class locator from "class_locator" setting doesn't exist.
305
	 * @throws \LogicException When class locator from "class_locator" setting has non supported type.
306
	 */
307
	protected function detectClassLocator()
308
	{
309
		$class_locator = null;
310
		$raw_class_locator_file = $this->getConfigSetting('class_locator');
311
312
		if ( $raw_class_locator_file !== null ) {
313
			$class_locator_file = $this->resolveProjectPath($raw_class_locator_file);
314
315
			if ( !file_exists($class_locator_file) || !is_file($class_locator_file) ) {
316
				throw new \LogicException(
317
					'The "' . $raw_class_locator_file . '" class locator doesn\'t exist.'
318
				);
319
			}
320
321
			$class_locator = require $class_locator_file;
322
		}
323
		else {
324
			$class_locator_file = $this->resolveProjectPath('vendor/autoload.php');
325
326
			if ( file_exists($class_locator_file) && is_file($class_locator_file) ) {
327
				$class_locator = require $class_locator_file;
328
			}
329
		}
330
331
		// Make sure memory limit isn't changed by class locator.
332
		ini_restore('memory_limit');
333
334
		if ( is_callable($class_locator) ) {
335
			return new CallableLocator($class_locator);
336
		}
337
		elseif ( $class_locator instanceof ClassLoader ) {
1 ignored issue
show
Bug introduced by
The class Composer\Autoload\ClassLoader does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
338
			return new ComposerLocator($class_locator);
339
		}
340
341
		throw new \LogicException(
342
			'The "class_loader" setting must point to "vendor/autoload.php" or a file, that would return the closure.'
343
		);
344
	}
345
346
	/**
347
	 * Processes the Finders configuration list.
348
	 *
349
	 * @return Finder[]
350
	 * @throws \LogicException If "finder" setting doesn't exist.
351
	 * @throws \LogicException If the configured method does not exist.
352
	 */
353
	protected function getFinders()
354
	{
355
		$finder_config = $this->getConfigSetting('finder');
356
357
		// Process "finder" config setting.
358
		if ( $finder_config === null ) {
359
			throw new \LogicException('The "finder" setting must be present in config file.');
360
		}
361
362
		$finders = array();
363
364
		foreach ( $finder_config as $methods ) {
365
			$finder = Finder::create()->files();
366
367
			if ( isset($methods['in']) ) {
368
				$methods['in'] = (array)$methods['in'];
369
370
				foreach ( $methods['in'] as $folder_index => $in_folder ) {
371
					$methods['in'][$folder_index] = $this->resolveProjectPath($in_folder);
372
				}
373
			}
374
375
			foreach ( $methods as $method => $arguments ) {
376
				if ( !method_exists($finder, $method) ) {
377
					throw new \LogicException(sprintf(
378
						'The method "Finder::%s" does not exist.',
379
						$method
380
					));
381
				}
382
383
				$arguments = (array)$arguments;
384
385
				foreach ( $arguments as $argument ) {
386
					$finder->$method($argument);
387
				}
388
			}
389
390
			$finders[] = $finder;
391
		}
392
393
		return $finders;
394
	}
395
396
	/**
397
	 * Resolves path within project.
398
	 *
399
	 * @param string $relative_path Relative path.
400
	 *
401
	 * @return string
402
	 */
403
	protected function resolveProjectPath($relative_path)
404
	{
405
		return realpath($this->projectPath . DIRECTORY_SEPARATOR . $relative_path);
406
	}
407
408
	/**
409
	 * Removes project path from file path.
410
	 *
411
	 * @param string $absolute_path Absolute path.
412
	 *
413
	 * @return string
414
	 */
415
	protected function removeProjectPath($absolute_path)
416
	{
417
		return preg_replace($this->projectPathRegExp, '', $absolute_path, 1);
418
	}
419
420
	/**
421
	 * Returns backwards compatibility checkers.
422
	 *
423
	 * @param CheckerFactory $factory Factory.
424
	 *
425
	 * @return AbstractChecker[]
426
	 */
427
	public function getBackwardsCompatibilityCheckers(CheckerFactory $factory)
428
	{
429
		$ret = array();
430
		$default_names = array('class', 'function', 'constant');
431
432
		foreach ( $this->getConfigSetting('bc_checkers', $default_names) as $name ) {
433
			$ret[] = $factory->get($name);
434
		}
435
436
		return $ret;
437
	}
438
439
	/**
440
	 * Returns value of configuration setting.
441
	 *
442
	 * @param string     $name    Name.
443
	 * @param mixed|null $default Default value.
444
	 *
445
	 * @return mixed
446
	 */
447
	protected function getConfigSetting($name, $default = null)
448
	{
449
		return array_key_exists($name, $this->config) ? $this->config[$name] : $default;
450
	}
451
452
}
453