Page MenuHomeIn-Portal Phabricator

ClassMapBuilder.php
No OneTemporary

File Metadata

Created
Thu, Jul 3, 12:18 AM

ClassMapBuilder.php

<?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 = 2;
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();
/**
* Class information used during cache building (file > class > class_info).
*
* @var array
*/
protected $buildingCache = array();
/**
* Class information (type, extends, implements, etc.).
*
* @var array
*/
protected $classInfo = 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 and class information, that was built previously.
*
* @return array
*/
public function get()
{
$this->load(self::CACHE_FILE_STRUCTURE, false);
return array($this->classToFileMap, $this->classInfo);
}
/**
* Builds class map.
*
* @return array
* @throws \RuntimeException When PHP parser not found.
*/
public function build()
{
if ( !class_exists('PhpParser\Parser') ) {
$error_msg = 'PHP Parser not found. Make sure, that Composer dependencies were ';
$error_msg .= 'installed using "php composer.phar install --dev" command.';
throw new \RuntimeException($error_msg);
}
$table_output = array();
$scan_path = preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '...', $this->scanPath, 1);
$table_output[] = $scan_path; // The "Path" column.
$this->load(self::CACHE_FILE_STRUCTURE, true);
$this->load(self::CACHE_FILE_HASHES, true);
$start = microtime(true);
$files = $this->scan();
$table_output[] = sprintf('%.4f', microtime(true) - $start) . 's'; // The "Scanned in" column.
$start = microtime(true);
$this->createParser();
foreach ( $files as $file ) {
$this->parseFile($file);
}
$table_output[] = sprintf('%.4f', microtime(true) - $start) . 's'; // The "Parsed in" column.
ksort($this->classToFileMap);
ksort($this->fileHashes);
ksort($this->classInfo);
$this->store(self::CACHE_FILE_STRUCTURE);
$this->store(self::CACHE_FILE_HASHES);
return $table_output;
}
/**
* 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 ) {
$class_info = $cache['class_info'];
if ( $for_writing ) {
foreach ( $cache['classes'] as $class => $file ) {
if ( !isset($this->buildingCache[$file]) ) {
$this->buildingCache[$file] = array();
}
$this->buildingCache[$file][$class] = $class_info[$class];
}
}
else {
$this->classToFileMap = $cache['classes'];
$this->classInfo = $class_info;
}
}
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()
{
\kUtil::setResourceLimit();
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->buildingCache[$file]) ) {
foreach ( $this->buildingCache[$file] as $class => $class_info ) {
$this->addClass($class, $class_info);
}
}
}
else {
// Parse file, because it's content doesn't match the cache.
$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;
$cache['class_info'] = $this->classInfo;
}
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. Please use 'in-portal classmap:rebuild' command 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.
* @param array $class_info Class info.
*
* @return void
*/
public function addClass($class, array $class_info)
{
$this->classInfo[$class] = $class_info;
$this->classToFileMap[$class] = $this->currentFile;
}
}

Event Timeline