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 * @throws \RuntimeException When PHP parser not found. */ public function build() { if ( !class_exists('PhpParser\Parser') ) { $error_msg = 'PHP Parser not found. '; $error_msg .= 'Make sure, that Composer dependencies were installed using "php composer.phar install --dev" command.'; throw new \RuntimeException($error_msg); } $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 = <<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; } }