Failed Conditions
Push — master ( 9ab98a...04484b )
by Alexander
03:08
created

KnowledgeBase::addRelation()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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