Completed
Push — master ( 52046a...4ff8ff )
by Alexander
05:18
created

getBackwardsCompatibilityIgnoreRules()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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;
12
13
14
use Aura\Sql\ExtendedPdoInterface;
15
use Composer\Autoload\ClassLoader;
16
use ConsoleHelpers\CodeInsight\BackwardsCompatibility\Checker\AbstractChecker;
17
use ConsoleHelpers\CodeInsight\BackwardsCompatibility\Checker\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;
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;
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)
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 8
	public function getDatabase()
108
	{
109 8
		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
	 * Refreshes database silently.
214
	 *
215
	 * @return void
216
	 */
217
	public function silentRefresh()
218
	{
219
		ReflectionEngine::setMaximumCachedFiles(20);
220
		ReflectionEngine::init($this->detectClassLocator());
221
222
		$sql = 'UPDATE Files
223
				SET Found = 0';
224
		$this->db->perform($sql);
225
226
		$files = array();
227
228
		foreach ( $this->getFinders() as $finder ) {
229
			$files = array_merge($files, array_keys(iterator_to_array($finder)));
230
		}
231
232
		foreach ( $files as $file ) {
233
			$this->processFile($file);
234
		}
235
236
		$sql = 'SELECT Id
237
				FROM Files
238
				WHERE Found = 0';
239
		$deleted_files = $this->db->fetchCol($sql);
240
241
		if ( $deleted_files ) {
242
			foreach ( $this->dataCollectors as $data_collector ) {
243
				$data_collector->deleteData($deleted_files);
244
			}
245
		}
246
247
		foreach ( $this->dataCollectors as $data_collector ) {
248
			$data_collector->aggregateData($this);
249
		}
250
	}
251
252
	/**
253
	 * Prints statistics about the code.
254
	 *
255
	 * @return array
256
	 */
257
	public function getStatistics()
258
	{
259
		$ret = array();
260
261
		$sql = 'SELECT COUNT(*)
262
				FROM Files';
263
		$ret['Files'] = $this->db->fetchValue($sql);
264
265
		foreach ( $this->dataCollectors as $data_collector ) {
266
			$ret = array_merge($ret, $data_collector->getStatistics());
267
		}
268
269
		return $ret;
270
	}
271
272
	/**
273
	 * Processes file.
274
	 *
275
	 * @param string $file File.
276
	 *
277
	 * @return integer
278
	 */
279
	public function processFile($file)
280
	{
281
		$size = filesize($file);
282
		$relative_file = $this->removeProjectPath($file);
283
284
		$sql = 'SELECT Id, Size
285
				FROM Files
286
				WHERE Name = :name';
287
		$file_data = $this->db->fetchOne($sql, array(
288
			'name' => $relative_file,
289
		));
290
291
		$this->db->beginTransaction();
292
293
		if ( $file_data === false ) {
294
			$sql = 'INSERT INTO Files (Name, Size) VALUES (:name, :size)';
295
			$this->db->perform($sql, array(
296
				'name' => $relative_file,
297
				'size' => $size,
298
			));
299
300
			$file_id = $this->db->lastInsertId();
301
		}
302
		else {
303
			$file_id = $file_data['Id'];
304
		}
305
306
		// File is not changed since last time it was indexed.
307
		if ( $file_data !== false && (int)$file_data['Size'] === $size ) {
308
			$sql = 'UPDATE Files
309
					SET Found = 1
310
					WHERE Id = :file_id';
311
			$this->db->perform($sql, array(
312
				'file_id' => $file_data['Id'],
313
			));
314
315
			$this->db->commit();
316
317
			return $file_data['Id'];
318
		}
319
320
		$sql = 'UPDATE Files
321
				SET Found = 1, Size = :size
322
				WHERE Id = :file_id';
323
		$this->db->perform($sql, array(
324
			'file_id' => $file_data['Id'],
325
			'size' => $size,
326
		));
327
328
		$parsed_file = new ReflectionFile($file);
329
330
		foreach ( $parsed_file->getFileNamespaces() as $namespace ) {
331
			foreach ( $this->dataCollectors as $data_collector ) {
332
				$data_collector->collectData($file_id, $namespace);
333
			}
334
		}
335
336
		$this->db->commit();
337
338
		return $file_id;
339
	}
340
341
	/**
342
	 * Determines class locator.
343
	 *
344
	 * @return LocatorInterface
345
	 * @throws \LogicException When class locator from "class_locator" setting doesn't exist.
346
	 * @throws \LogicException When class locator from "class_locator" setting has non supported type.
347
	 */
348
	protected function detectClassLocator()
349
	{
350
		$class_locator = null;
351
		$raw_class_locator_file = $this->getConfigSetting('class_locator');
352
353
		if ( $raw_class_locator_file !== null ) {
354
			$class_locator_file = $this->resolveProjectPath($raw_class_locator_file);
355
356
			if ( !file_exists($class_locator_file) || !is_file($class_locator_file) ) {
357
				throw new \LogicException(
358
					'The "' . $raw_class_locator_file . '" class locator doesn\'t exist.'
359
				);
360
			}
361
362
			$class_locator = require $class_locator_file;
363
		}
364
		else {
365
			$class_locator_file = $this->resolveProjectPath('vendor/autoload.php');
366
367
			if ( file_exists($class_locator_file) && is_file($class_locator_file) ) {
368
				$class_locator = require $class_locator_file;
369
			}
370
		}
371
372
		// Make sure memory limit isn't changed by class locator.
373
		ini_restore('memory_limit');
374
375
		if ( is_callable($class_locator) ) {
376
			return new CallableLocator($class_locator);
377
		}
378
		elseif ( $class_locator instanceof ClassLoader ) {
379
			return new ComposerLocator($class_locator);
380
		}
381
382
		throw new \LogicException(
383
			'The "class_loader" setting must point to "vendor/autoload.php" or a file, that would return the closure.'
384
		);
385
	}
386
387
	/**
388
	 * Processes the Finders configuration list.
389
	 *
390
	 * @return Finder[]
391
	 * @throws \LogicException If "finder" setting doesn't exist.
392
	 * @throws \LogicException If the configured method does not exist.
393
	 */
394
	protected function getFinders()
395
	{
396
		$finder_config = $this->getConfigSetting('finder');
397
398
		// Process "finder" config setting.
399
		if ( $finder_config === null ) {
400
			throw new \LogicException('The "finder" setting must be present in config file.');
401
		}
402
403
		$finders = array();
404
405
		foreach ( $finder_config as $methods ) {
406
			$finder = Finder::create()->files();
407
408
			if ( isset($methods['in']) ) {
409
				$methods['in'] = (array)$methods['in'];
410
411
				foreach ( $methods['in'] as $folder_index => $in_folder ) {
412
					$methods['in'][$folder_index] = $this->resolveProjectPath($in_folder);
413
				}
414
			}
415
416
			foreach ( $methods as $method => $arguments ) {
417
				if ( !method_exists($finder, $method) ) {
418
					throw new \LogicException(sprintf(
419
						'The method "Finder::%s" does not exist.',
420
						$method
421
					));
422
				}
423
424
				$arguments = (array)$arguments;
425
426
				foreach ( $arguments as $argument ) {
427
					$finder->$method($argument);
428
				}
429
			}
430
431
			$finders[] = $finder;
432
		}
433
434
		return $finders;
435
	}
436
437
	/**
438
	 * Resolves path within project.
439
	 *
440
	 * @param string $relative_path Relative path.
441
	 *
442
	 * @return string
443
	 */
444
	protected function resolveProjectPath($relative_path)
445
	{
446
		return realpath($this->projectPath . DIRECTORY_SEPARATOR . $relative_path);
447
	}
448
449
	/**
450
	 * Removes project path from file path.
451
	 *
452
	 * @param string $absolute_path Absolute path.
453
	 *
454
	 * @return string
455
	 */
456
	protected function removeProjectPath($absolute_path)
457
	{
458
		return preg_replace($this->projectPathRegExp, '', $absolute_path, 1);
459
	}
460
461
	/**
462
	 * Returns backwards compatibility checkers.
463
	 *
464
	 * @param CheckerFactory $factory Factory.
465
	 *
466
	 * @return AbstractChecker[]
467
	 */
468
	public function getBackwardsCompatibilityCheckers(CheckerFactory $factory)
469
	{
470
		$ret = array();
471
		$default_names = array('class', 'function', 'constant');
472
473
		foreach ( $this->getConfigSetting('bc_checkers', $default_names) as $name ) {
474
			$ret[] = $factory->get($name);
475
		}
476
477
		return $ret;
478
	}
479
480
	/**
481
	 * Returns backwards compatibility ignore rules.
482
	 *
483
	 * @return array
484
	 */
485
	public function getBackwardsCompatibilityIgnoreRules()
486
	{
487
		return $this->getConfigSetting('bc_ignore', array());
488
	}
489
490
	/**
491
	 * Returns value of configuration setting.
492
	 *
493
	 * @param string     $name    Name.
494
	 * @param mixed|null $default Default value.
495
	 *
496
	 * @return mixed
497
	 */
498
	protected function getConfigSetting($name, $default = null)
499
	{
500
		return array_key_exists($name, $this->config) ? $this->config[$name] : $default;
501
	}
502
503
}
504