Changeset View
Changeset View
Standalone View
Standalone View
core/kernel/utility/ClassDiscovery/ClassMapBuilder.php
- This file was added.
Property | Old Value | New Value |
---|---|---|
svn:eol-style | null | LF |
<?php | |||||
/** | |||||
* @version $Id$ | |||||
* @package In-Portal | |||||
* @copyright Copyright (C) 1997 - 2015 Intechnic. All rights reserved. | |||||
* @license GNU/GPL | |||||
* In-Portal is Open Source software. | |||||
* This means that this software may have been modified pursuant | |||||
* the GNU General Public License, and as distributed it includes | |||||
* or is derivative of works licensed under the GNU General Public License | |||||
* or other free or open source software licenses. | |||||
* See http://www.in-portal.org/license for copyright notices and details. | |||||
*/ | |||||
namespace Intechnic\InPortal\Core\kernel\utility\ClassDiscovery; | |||||
use PhpParser\Lexer; | |||||
use PhpParser\NodeTraverser; | |||||
use PhpParser\NodeVisitor\NameResolver; | |||||
use PhpParser\Parser; | |||||
defined('FULL_PATH') or die('restricted access!'); | |||||
class ClassMapBuilder | |||||
{ | |||||
const CACHE_FORMAT = 1; | |||||
const CACHE_FILE_STRUCTURE = 'class_structure.php'; | |||||
const CACHE_FILE_HASHES = 'file_hashes.php'; | |||||
/** | |||||
* Path to scan. | |||||
* | |||||
* @var string | |||||
*/ | |||||
protected $scanPath; | |||||
/** | |||||
* Path to store cache into. | |||||
* | |||||
* @var string | |||||
*/ | |||||
protected $cachePath; | |||||
/** | |||||
* List of classes (key - class, value - file). | |||||
* | |||||
* @var array | |||||
*/ | |||||
protected $classToFileMap = array(); | |||||
/** | |||||
* List of classes, declared in each file (key - file, value - class list). | |||||
* | |||||
* @var array | |||||
*/ | |||||
protected $fileToClassMap = array(); | |||||
/** | |||||
* Stores hash of each file on given path. | |||||
* | |||||
* @var array | |||||
*/ | |||||
protected $fileHashes = array(); | |||||
/** | |||||
* Parser. | |||||
* | |||||
* @var Parser | |||||
*/ | |||||
protected $parser; | |||||
/** | |||||
* Node traverser. | |||||
* | |||||
* @var NodeTraverser | |||||
*/ | |||||
protected $traverser; | |||||
/** | |||||
* Name of file, that is currently processed. | |||||
* | |||||
* @var string | |||||
*/ | |||||
protected $currentFile; | |||||
/** | |||||
* Returns builder array for all eligible folders. | |||||
* | |||||
* @param array $module_info Module info. | |||||
* | |||||
* @return static[] | |||||
*/ | |||||
public static function createBuilders(array $module_info = null) | |||||
{ | |||||
$ret = array(); | |||||
if ( !isset($module_info) ) { | |||||
// No module information given > scan everything. | |||||
$ret[] = new static(FULL_PATH . DIRECTORY_SEPARATOR . 'core'); | |||||
foreach ( glob(MODULES_PATH . '/*', GLOB_ONLYDIR) as $module_folder ) { | |||||
if ( \kModulesHelper::isInPortalModule($module_folder) ) { | |||||
$ret[] = new static($module_folder); | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
// Module information given > scan only these modules. | |||||
foreach ( $module_info as $module_name => $module_data ) { | |||||
if ( $module_name == 'In-Portal' ) { | |||||
continue; | |||||
} | |||||
$ret[] = new static(FULL_PATH . DIRECTORY_SEPARATOR . rtrim($module_data['Path'], '/')); | |||||
} | |||||
} | |||||
return $ret; | |||||
} | |||||
/** | |||||
* Creates ClassMapBuilder instance. | |||||
* | |||||
* @param string $scan_path Path to scan. | |||||
*/ | |||||
public function __construct($scan_path) | |||||
{ | |||||
$this->scanPath = $scan_path; | |||||
$this->assertPath($this->scanPath); | |||||
$this->cachePath = $this->scanPath . '/install/cache'; | |||||
$this->assertPath($this->cachePath); | |||||
} | |||||
/** | |||||
* Validates that path exists and is directory. | |||||
* | |||||
* @param string $path Path. | |||||
* | |||||
* @return void | |||||
* @throws \InvalidArgumentException When invalid path is given. | |||||
*/ | |||||
protected function assertPath($path) | |||||
{ | |||||
if ( !file_exists($path) || !is_dir($path) ) { | |||||
throw new \InvalidArgumentException('Path "' . $path . '" is not a folder or doesn\'t exist'); | |||||
} | |||||
} | |||||
/** | |||||
* Returns class map, that was build previously. | |||||
* | |||||
* @return array | |||||
*/ | |||||
public function get() | |||||
{ | |||||
$this->load(self::CACHE_FILE_STRUCTURE, false); | |||||
return $this->classToFileMap; | |||||
} | |||||
/** | |||||
* Builds class map. | |||||
* | |||||
* @return void | |||||
*/ | |||||
public function build() | |||||
{ | |||||
$scan_path = preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '...', $this->scanPath, 1); | |||||
echo $this->strPad('path "' . $scan_path . '"', 40); | |||||
$this->load(self::CACHE_FILE_STRUCTURE, true); | |||||
$this->load(self::CACHE_FILE_HASHES, true); | |||||
$start = microtime(true); | |||||
$files = $this->scan(); | |||||
echo $this->strPad('scanned in ' . sprintf('%.4f', microtime(true) - $start) . 's', 25); | |||||
$start = microtime(true); | |||||
$this->createParser(); | |||||
foreach ( $files as $file ) { | |||||
$this->parseFile($file); | |||||
} | |||||
echo $this->strPad('parsed in ' . sprintf('%.4f', microtime(true) - $start) . 's', 25); | |||||
echo PHP_EOL; | |||||
ksort($this->classToFileMap); | |||||
ksort($this->fileHashes); | |||||
$this->store(self::CACHE_FILE_STRUCTURE); | |||||
$this->store(self::CACHE_FILE_HASHES); | |||||
} | |||||
/** | |||||
* Pads text with spaces from right side. | |||||
* | |||||
* @param string $text Text. | |||||
* @param integer $length Pad length. | |||||
* | |||||
* @return string | |||||
*/ | |||||
protected function strPad($text, $length) | |||||
{ | |||||
return str_pad($text, $length, ' ', STR_PAD_RIGHT); | |||||
} | |||||
/** | |||||
* Loads cache from disk. | |||||
* | |||||
* @param string $filename Filename. | |||||
* @param boolean $for_writing Load cache for writing or reading. | |||||
* | |||||
* @return void | |||||
*/ | |||||
protected function load($filename, $for_writing) | |||||
{ | |||||
$file_path = $this->getCacheFilename($filename); | |||||
if ( !file_exists($file_path) ) { | |||||
return; | |||||
} | |||||
$cache = include $file_path; | |||||
if ( $cache['cache_format'] < self::CACHE_FORMAT ) { | |||||
return; | |||||
} | |||||
if ( $filename === self::CACHE_FILE_STRUCTURE ) { | |||||
if ( $for_writing ) { | |||||
foreach ( $cache['classes'] as $class => $file ) { | |||||
if ( !isset($this->fileToClassMap[$file]) ) { | |||||
$this->fileToClassMap[$file] = array(); | |||||
} | |||||
$this->fileToClassMap[$file][] = $class; | |||||
} | |||||
} | |||||
else { | |||||
$this->classToFileMap = $cache['classes']; | |||||
} | |||||
} | |||||
elseif ( $filename === self::CACHE_FILE_HASHES ) { | |||||
$this->fileHashes = $cache['file_hashes']; | |||||
} | |||||
} | |||||
/** | |||||
* Scans path for files. | |||||
* | |||||
* @return array | |||||
*/ | |||||
protected function scan() | |||||
{ | |||||
$files = array(); | |||||
$directory_iterator = new \RecursiveDirectoryIterator($this->scanPath); | |||||
$filter_iterator = new CodeFolderFilterIterator($directory_iterator); | |||||
foreach ( new \RecursiveIteratorIterator($filter_iterator, \RecursiveIteratorIterator::SELF_FIRST) as $file ) { | |||||
/* @var \SplFileInfo $file */ | |||||
if ( $file->isFile() && $file->getExtension() === 'php' ) { | |||||
$relative_path = preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '', $file->getPathname(), 1); | |||||
$files[$relative_path] = true; | |||||
} | |||||
} | |||||
// Don't include cache file itself in cache. | |||||
$exclude_file = preg_replace( | |||||
'/^' . preg_quote(FULL_PATH, '/') . '/', | |||||
'', | |||||
$this->getCacheFilename(self::CACHE_FILE_STRUCTURE), | |||||
1 | |||||
); | |||||
unset($files[$exclude_file]); | |||||
$exclude_file = preg_replace( | |||||
'/^' . preg_quote(FULL_PATH, '/') . '/', | |||||
'', | |||||
$this->getCacheFilename(self::CACHE_FILE_HASHES), | |||||
1 | |||||
); | |||||
unset($files[$exclude_file]); | |||||
return array_keys($files); | |||||
} | |||||
/** | |||||
* Create parser. | |||||
* | |||||
* @return void | |||||
*/ | |||||
protected function createParser() | |||||
{ | |||||
ini_set('xdebug.max_nesting_level', 3000); | |||||
$this->parser = new Parser(new Lexer()); | |||||
$this->traverser = new NodeTraverser(); | |||||
$this->traverser->addVisitor(new NameResolver()); | |||||
$this->traverser->addVisitor(new ClassDetector($this)); | |||||
} | |||||
/** | |||||
* Parses a file. | |||||
* | |||||
* @param string $file Path to file. | |||||
* | |||||
* @return void | |||||
*/ | |||||
protected function parseFile($file) | |||||
{ | |||||
$this->currentFile = $file; | |||||
$code = file_get_contents(FULL_PATH . $file); | |||||
$current_hash = filesize(FULL_PATH . $file); | |||||
$previous_hash = isset($this->fileHashes[$file]) ? $this->fileHashes[$file] : 0; | |||||
if ( $current_hash === $previous_hash ) { | |||||
// File wasn't change since time, when cache was built. | |||||
if ( isset($this->fileToClassMap[$file]) ) { | |||||
foreach ( $this->fileToClassMap[$file] as $class ) { | |||||
$this->addClass($class); | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
// Parse file, because it's content doesn't match the cache. | |||||
$this->fileToClassMap[$file] = array(); | |||||
$this->fileHashes[$file] = $current_hash; | |||||
$statements = $this->parser->parse($code); | |||||
$this->traverser->traverse($statements); | |||||
} | |||||
} | |||||
/** | |||||
* Stores cache to disk. | |||||
* | |||||
* @param string $filename Cache filename. | |||||
* | |||||
* @return void | |||||
* @throws \RuntimeException When cache could not be written. | |||||
*/ | |||||
protected function store($filename) | |||||
{ | |||||
$cache = array('cache_format' => self::CACHE_FORMAT); | |||||
if ( $filename === self::CACHE_FILE_STRUCTURE ) { | |||||
$cache['classes'] = $this->classToFileMap; | |||||
} | |||||
elseif ( $filename === self::CACHE_FILE_HASHES ) { | |||||
$cache['file_hashes'] = $this->fileHashes; | |||||
} | |||||
$cache = $this->prettyVarExport($cache); | |||||
$at = '@'; | |||||
$file_content = <<<EOPHP | |||||
<?php | |||||
// {$at}codingStandardsIgnoreFile | |||||
/** | |||||
* This file is automatically {$at}generated. Use 'php tools/build_class_map.php' to rebuild it. | |||||
*/ | |||||
return {$cache}; | |||||
EOPHP; | |||||
$file_path = $this->getCacheFilename($filename); | |||||
// Don't bother saving, because file wasn't even changed. | |||||
if ( file_exists($file_path) && file_get_contents($file_path) === $file_content ) { | |||||
return; | |||||
} | |||||
if ( file_put_contents($file_path, $file_content) === false ) { | |||||
throw new \RuntimeException('Unable to save cache to "' . $file_path . '" file'); | |||||
} | |||||
} | |||||
/** | |||||
* Prettified var_export. | |||||
* | |||||
* @param mixed $data Data. | |||||
* | |||||
* @return string | |||||
*/ | |||||
protected function prettyVarExport($data) | |||||
{ | |||||
$result = var_export($data, true); | |||||
$result = preg_replace("/=> \n[ ]+array \\(/s", '=> array (', $result); | |||||
$result = str_replace(array('array (', ' '), array('array(', "\t"), $result); | |||||
return $result; | |||||
} | |||||
/** | |||||
* Returns cache filename. | |||||
* | |||||
* @param string $filename Filename. | |||||
* | |||||
* @return string | |||||
*/ | |||||
protected function getCacheFilename($filename) | |||||
{ | |||||
return $this->cachePath . '/' . $filename; | |||||
} | |||||
/** | |||||
* Adds class to the map. | |||||
* | |||||
* @param string $class Class. | |||||
* | |||||
* @return void | |||||
*/ | |||||
public function addClass($class) | |||||
{ | |||||
$this->classToFileMap[$class] = $this->currentFile; | |||||
$this->fileToClassMap[$this->currentFile][] = $class; | |||||
} | |||||
} |