src/Compressor.php
<?php
namespace samsonphp\compressor;
use samson\core\Core;
use samson\core\iModule;
use samsonframework\localfilemanager\LocalFileManager;
use samsonframework\resource\ResourceMap;
use samsonos\compressor\Module;
use samsonphp\compressor\resource\JavaScript;
use samsonphp\event\Event;
use samsonphp\resource\Router;
/**
* Module for automatic code optimization|compression
*
* @package samsonos\compressor
* @author Vitaly Iegorov <egorov@samsonos.com>
*/
class Compressor
{
/** Identifier of global namespace */
const NS_GLOBAL = '';
const E_CREATE_MODULE_LIST = 'compressor.create.module.list';
const E_CREATE_RESOURCE_LIST = 'compressor.create.resource.list';
const E_RESOURCE_COMPRESS = 'compressor.resource.compress';
/** Array key for storing last generated data */
const VIEWS = 'views';
/** Output path for compressed web application */
public $output = 'out/';
/** Collection of requires to insert in compressed file */
public $require = array();
/** Ignored resource extensions */
public $ignored_extensions = array('php', 'js', 'css', 'md', 'map', 'dbs', 'vphp', 'less', 'gz', 'lock', 'json', 'sql', 'xml', 'yml');
/** Ignored resource files */
public $ignored_resources = array('.project', '.buildpath', '.gitignore', '.travis.yml', 'phpunit.xml', 'thumbs.db', 'Thumbs.db');
/** @var array Collection of folders to be ignored by compressor */
public $ignoredFolders = array('vendor', 'var');
/** @var array Collection of file paths to be ignored by compressor */
public $ignoredFiles = array();
/** Папка где размещается исходное веб-приложение */
public $input = __SAMSON_CWD__;
/** View rendering mode */
protected $view_mode = Core::RENDER_VARIABLE;
/** Указатель на текущий сворачиваемый модуль */
protected $current;
/** Коллекция уже обработанных файлов */
protected $files = array();
/** Collection for storing all php code by namespace */
private $php = array(self::NS_GLOBAL => array());
/** @var string Web-application environment identifier */
protected $environment = 'prod';
/** @var bool Debug flag */
protected $debug = false;
/** @var string Supported php version */
protected $phpVersion = PHP_VERSION;
protected $resourceManager;
protected $classConst = array();
protected $resourceUrlsList = [];
/** @var FileManagerInterface File system manager */
protected $fileManager;
/**
* Compress web-application
* @param string $output Path for creating compressed version
* @param boolean $debug Disable errors output
* @param string $environment Configuration environment
* @param string $phpVersion PHP version to support
* @param array $configuration Configuration
*/
public function __construct($output = 'out/', $debug = false, $environment = 'prod', $phpVersion = PHP_VERSION, $configuration = array())
{
$this->resourceManager = new resource\Generic($this);
$this->fileManager = new LocalFileManager();
$this->output = $output;
$this->debug = $debug;
$this->environment = $environment;
$this->phpVersion = $phpVersion;
foreach ($configuration as $key => $value) {
// If object has configured property defined
if (property_exists($this, $key)) {
// Set object variable value
$this->$key = $value;
}
}
}
/**
* Свернуть файл представления
*
* @param string $view_file Полный путь к файлу представления
* @param iModule $module Указатель на модуль которому принадлежит это представление
*
* @return bool
*/
public function compress_view($view_file, iModule & $module)
{
// Build relative path to module view
$rel_path = ($module->id() == 'local' ? '' : $module->id() . '/') . str_replace($module->path(), '', $view_file);
$this->log(' -- Preparing view[##] relative path [##]', $view_file, $rel_path);
// Прочитаем файл представления
$view_html = file_get_contents($view_file);
if (!isset($view_file{0})) return e('View: ##(##) is empty', E_SAMSON_SNAPSHOT_ERROR, array($view_file, $rel_path));
// TODO: should be done via events in resourcer module
// Найдем обращения к роутеру ресурсов
$view_html = preg_replace_callback(
'/(<\?php)*\s*src\s*\(\s*(\'|\")?(?<path>[^\'\"\?\;\)]+)(\'|\")?(\s*,\s*(\'|\")(?<module>[^\'\"\)]+)(\'|\"))?\s*\)\;?(\s*\?>)?/uis',
array($this, 'src_replace_callback'),
$view_html
);
// Replace old inline php tags
$view_html = str_ireplace('<? ', '<?php ', $view_html);
// Сожмем HTML
$view_html = Minify_HTML::minify($view_html);
// Fire event to render view correctly
Event::fire('core.render', array(&$view_html, array(), &$module));
// Template re-rendering
// TODO: We must split regular view and template file to handle differently, for now nothing will change but in future....
$template = !isset($this->resourceUrlsList[$view_file])?Router::I_MAIN_PROJECT_TEMPLATE:$view_file;
Event::fire('core.rendered', array(&$view_html, $this->resourceUrlsList[$template]));
$view_php = "<<<'EOT'" . "\n" . $view_html . "\n" . "EOT;";
// Add view code to final global namespace
$this->php[self::NS_GLOBAL][self::VIEWS] .= "\n" . '$GLOBALS["__compressor_files"]["' . $rel_path . '"] = ' . $view_php;
}
/**
* Свернуть модуль
*
* @param iModule $module Указатель на модуль для сворачивания
* @param ResourceMap $data
*/
public function compress_module(iModule &$module, ResourceMap &$data)
{
// Идентификатор модуля
$id = $module->id();
// Сохраним указатель на текущий модуль
$this->current = &$module;
// Build output module path
$module_output_path = $id == 'local' ? '' : basename($module->path()) . '/';
// Build resource source path
$module_path = $id == 'local' ? $module->path() . __SAMSON_PUBLIC_PATH : $module->path();
$this->log(' - Compressing module[##] from [##]', $id, $module_path);
// Call special method enabling module personal resource pre-management on compressing
if ($module->beforeCompress($this, $this->php) !== false) {
// Copy all module resources
$this->copy_path_resources($data->resources, $module_path, $module_output_path);
// Internal collection of module php code, not views
$module_php = array();
foreach ($data->classes as $key => $php) {
$this->compress_php($key, $module, $module_php);
}
// Iterate module plain php code
foreach ($data->php as $php) {
$this->compress_php($php, $module, $module_php);
}
foreach ($data->globals as $php) {
$this->compress_php($php, $module, $module_php);
}
// Iterate module controllers php code
foreach ($data->controllers as $php) {
$this->compress_php($php, $module, $module_php);
}
// Iterate module controllers php code
foreach ($data->modules as $php) {
$this->compress_php($php[1], $module, $module_php);
}
// Iterate module model php code
foreach ($data->models as $php) {
$this->compress_php($php, $module, $module_php);
}
// Iterate module views
foreach ($data->views as $php) {
$this->compress_view($php, $module);
}
}
// Call special method enabling module personal resource post-management on compressing
$module->afterCompress($this, $module_php);
// Gather all code in to global code collection with namespaces
$this->code_array_combine($module_php, $this->php);
// Change module path
$module->path($id . '/');
}
public function rewriteResourceRouter(array $matches)
{
//trace($matches,1);
//die;
return '"' . $this->current->id() . '/' . $matches['path'] . '";';
}
/**
* Обработчик замены роутера ресурсов
* @param array $matches Найденые совпадения по шаблону
* @return string Обработанный вариант пути к ресурсу
*/
public function src_replace_callback($matches)
{
// Получим относительный путь к ресурсу
$path = trim($matches['path']);
// Путь к модуля после сжимания
$module_path = (isset($matches['module']) && strlen($matches['module']) > 0) ? $matches['module'] . '/' : $this->current->id() . '/';
// Если передана переменная мы не можем гарантировать её значение
if (strpos($path, '$') !== false) $path = '<?php echo \'' . $module_path . '\'.' . $path . '; ?>';
// Просто строка
else $path = $module_path . $path;
return $path;
//e('Файл представления ## - Обращение к роутеру ресурсов через переменную ##', E_SAMSON_SNAPSHOT_ERROR, array($view_path, $path));
}
/** Generic log function for further modification */
public function log($message)
{
// Get passed vars
$vars = func_get_args();
// Remove first message var
array_shift($vars);
// Render debug message
return trace(debug_parse_markers($message, $vars));
}
/**
* Compress web-application
* @param boolean $debug Disable errors output
* @param string $php_version PHP version to support
*/
public function compress($debug = false, $environment = 'prod', $php_version = PHP_VERSION)
{
// Set compressed project environment
$this->environment = $environment;
elapsed('Started web-application compression[' . $this->environment . ']');
s()->async(true);
ini_set('memory_limit', '256M');
// Check output path
if (!isset($this->output{0})) {
return $this->log('Cannot compress web-application from [##] - No output path is specified', $this->input);
}
// Define rendering model depending on PHP version
$php_version = isset($php_version{0}) ? $php_version : PHP_VERSION;
if (version_compare($php_version, '5.3.0', '<')) {
$this->view_mode = Core::RENDER_ARRAY;
}
// Add url base to path
$this->output .= url()->base();
// Creating output project folder
$result = \samson\core\File::mkdir($this->output);
if ($result) {
$this->log('Created output project folder [##]', $this->output);
} else if ($result == -1) {
return $this->log('Compression failed! Cannot create output project folder [##]', $this->output);
}
// Remove all trailing slashes
$this->output = realpath($this->output) . '/';
$this->log('[##]## Compressing web-application[##] from [##] to [##]',
$environment,
$debug ? '[DEBUG]' : '',
$php_version,
$this->input,
$this->output
);
// Add generic composer auto loader require
$this->php['__before_all']['composer'] = "\n" . 'if(file_exists("vendor/autoload.php")) require "vendor/autoload.php";';
// Define global views collection
$this->php[self::NS_GLOBAL][self::VIEWS] = "\n" . '$GLOBALS["__compressor_files"] = array();';
// If resourcer is loaded - copy css and js
// Link
$rr = &s()->module_stack['resourcer'];
// Iterate all css and js resources
$ignoreFolders = array();
foreach ($this->ignoredFolders as $folder) {
$ignoreFolders[] = $this->output . $folder;
}
// Remove all old javascript and css
\samson\core\File::clear($this->output, array('js', 'css'), $ignoreFolders);
$moduleListArray = [];
//$moduleListArray[Router::I_MAIN_PROJECT_TEMPLATE] = $this->system->module_stack;
Event::fire(self::E_CREATE_MODULE_LIST, array(& $moduleListArray));
$resource = new Resource($this->fileManager);
foreach ($moduleListArray as $template => $moduleList)
{
$resourceUrls = [];
Event::fire(self::E_CREATE_RESOURCE_LIST, array(& $resourceUrls, $moduleList));
foreach ($resourceUrls as $type => $urls) {
$file = $resource->compress($urls, $type, $this->output);
$this->resourceUrlsList[$template][$type] = [DIRECTORY_SEPARATOR.$file];
}
}
// Iterate core ns resources collection
foreach (s()->module_stack as $id => &$module) {
// Work only with compressable modules
if (is_a($module, ns_classname('CompressInterface', 'samsonframework\core')) ||
(isset($this->composerParameters['samsonphp_package_compressable']) &&
($this->composerParameters['samsonphp_package_compressable'] = 1))
) {
$this->compress_module($module, $module->resourceMap);
}
// Change path to local modules
if (is_a($module, '\samson\core\VirtualModule')) {
$module->path('');
}
}
/*foreach ($rr->cached['js'] as $jsCachedFile) {
// Manage javascript resource
$javascriptManager = new resource\JavaScript($this);
$javascriptManager->compress(__SAMSON_CWD__ . $jsCachedFile, $this->output . basename($jsCachedFile));
}
foreach ($rr->cached['css'] as $cssCachedFile) {
// Manage CSS resource
$cssManager = new resource\CSS($this, $rr);
$cssManager->compress(__SAMSON_CWD__ . $cssCachedFile, $this->output . basename($cssCachedFile));
}*/
//}
// Copy main project composer.json
$composerPath = __SAMSON_CWD__ . 'composer.json';
if (file_exists($composerPath)) {
// Read json file
$composerJSON = (array)json_decode(file_get_contents($composerPath));
// Remove development dependencies
unset($composerJSON['require-dev']);
// Remove autoload section
unset($composerJSON['autoload']);
// Remove install/update scripts
unset($composerJSON['scripts']);
// Write modified composer.json
file_put_contents($this->output . 'composer.json', json_encode($composerJSON));
}
// Set errors output
$this->php[self::NS_GLOBAL][self::VIEWS] .= "\n" . '\samson\core\Error::$OUTPUT = ' . (!$debug ? 'false' : 'true') . ';';
// Create SamsonPHP core compressor
$core = new \samsonphp\compressor\Core(s(), $environment, $this);
// Add global base64 serialized core string
$this->php[self::NS_GLOBAL][self::VIEWS] .= "\n" . '$GLOBALS["__CORE_SNAPSHOT"] = \'' . $core->compress() . '\';';
// Add all specified requires
foreach ($this->require as $require) $this->php[self::NS_GLOBAL][self::VIEWS] .= "\n" . 'require("' . $require . '");';
// Add localization data
$locale_str = array();
foreach (\samson\core\SamsonLocale::$locales as $locale) {
if ($locale != '') {
$locale_str[] = '\'' . $locale . '\'';
}
}
// Add [setlocales] code
$this->php[self::NS_GLOBAL][self::VIEWS] .= "\n" . 'setlocales( ' . implode(',', $locale_str) . ');';
// TODO: add generic handlers to modules to provide compressing logic for each module
// TODO: add generic constants namespace to put all constants definition there - and put only defined constrat and redeclare them
// TODO: WTF???? Thi must be local module logic
// If this is remote web-app - collect local resources
if (__SAMSON_REMOTE_APP) {
// Gather all resources
$path = __SAMSON_CWD__;
$ls = array();
s()->resources($path, $ls);
// If we have any resources
if (isset($ls['resources'])) {
$this->copy_path_resources($ls['resources'], __SAMSON_CWD__, '');
}
}
// If default locale is defined
if (!defined('DEFAULT_LOCALE')) {
define('DEFAULT_LOCALE', 'ru');
}
// Add default system locale to them end of core definition
$this->php['samson\core'][self::VIEWS] = "\n" . 'define("DEFAULT_LOCALE", "' . DEFAULT_LOCALE . '");';
// Pointer to entry script code
$entryScriptPath = __SAMSON_CWD__ . __SAMSON_PUBLIC_PATH . 'index.php';
$entryScript = &$this->php[self::NS_GLOBAL][$entryScriptPath];
// Collect all event system data
$eventCompressor = new EventCompressor();
$eventCompressor->collect($entryScript);
// Remove standard framework entry point from index.php - just preserve default controller
if (preg_match('/start\(\s*(\'|\")(?<default>[^\'\"]+)/i', $entryScript, $matches)) {
/*
* Temporary solution to support compressed version, because other way localization does not work,
* as chain is broken, first time URL object is created and URL is parsed only after start, so
* CMS::afterCompress does not knows what is current locale and does not inject it to all material
* queries.
*/
$this->php[self::NS_GLOBAL][self::VIEWS] .= "\n" . 'url();';
$this->php[self::NS_GLOBAL][self::VIEWS] .= "\n" . 's()->start(\'' . $matches['default'] . '\');';
} else e('Default module definition not found - possible errors at compressed version');
// Clear default entry point
unset($this->php[self::NS_GLOBAL][$entryScriptPath]);
// Set global namespace as last
$global_ns = $this->php[self::NS_GLOBAL];
unset($this->php[self::NS_GLOBAL]);
$this->php[self::NS_GLOBAL] = $global_ns;
// Set view data to the end of global namespace
$s = $this->php[self::NS_GLOBAL][self::VIEWS];
unset($this->php[self::NS_GLOBAL][self::VIEWS]);
$this->php[self::NS_GLOBAL][self::VIEWS] = $s;
// Load all OOP entities
$classes = array();
// Соберем коллекцию загруженных интерфейсов их файлов по пространствам имен
$this->classes_to_ns_files(get_declared_interfaces(), $classes);
// Соберем коллекцию загруженных классов их файлов по пространствам имен
$this->classes_to_ns_files(get_declared_classes(), $classes);
// Fix OOP entities
foreach ($this->php as $ns => & $files) {
// If this namespace has been loaded
if (isset($classes[$ns])) {
// Fill namespace entities, make OOP entities correct order
$files = array_merge($classes[$ns], $files);
}
}
// Соберем весь PHP код в один файл
$index_php = $this->code_array_to_str($this->php, ($this->view_mode == 2));
// Collect all event system data
$eventCompressor->collect($index_php);
// Transform event system in all project code
if ($eventCompressor->transform($index_php, $index_php)) {
//trace($eventCompressor->subscriptions, true);
}
// Remove url_base parsing and put current url base
if (preg_match('/define\(\'__SAMSON_BASE__\',\s*([^;]+)/i', $index_php, $matches)) {
$index_php = str_replace($matches[0], 'define(\'__SAMSON_BASE__\',\'' . __SAMSON_BASE__ . '\');', $index_php);
}
// Set global constant to specify supported PHP version
if (preg_match('/define\s*\(\'__SAMSON_PHP_OLD[^;]+/', $index_php, $matches)) {
$index_php = str_replace($matches[0], 'define(\'__SAMSON_PHP_OLD\',\'' . ($this->view_mode == 2) . '\');', $index_php);
}
$index_php = $this->removeBlankLines($index_php);
$index_php = preg_replace('/(declare *\( *strict_types *= *1 *\) *;)/i', ' ', $index_php);
// Запишем пусковой файл
file_put_contents($this->output . 'index.php', '<?php ' . $index_php . "\n" . '?>');
// Minify PHP code if no debug is needed
if (!$debug) {
file_put_contents($this->output . 'index.php', php_strip_whitespace($this->output . 'index.php'));
}
elapsed('Site has been successfully compressed to ' . $this->output);
}
/**
* Преобразовать коллекцию полученного кода в виде NS/Files в строку
* с правильными NS
*
* @param array $code Коллекция кода полученная функцией @see compress_php()
* @param boolean $no_ns Флаг убирания NS из кода
* @return string Правильно собранный код в виде строки
*/
public function code_array_to_str(array $code, $no_ns = false)
{
// Соберем весь PHP код модуля
$php_code = '';
foreach ($code as $ns => $files) {
// If we support namespaces
if (!$no_ns) $php_code .= "\n" . 'namespace ' . $ns . '{';
// Insert files code
foreach ($files as $file => $php) {
// Ignore uses array
if ($file == 'uses') continue;
// TODO: Add uses support class name changing
// If we does not support namespaces
if ($no_ns) {
// Find all static class usage
if (preg_match_all('/[\!\.\,\(\s\n\=\:]+\s*(?:self|parent|static|(?<classname>[\\\a-z_0-9]+))::/i', $php, $matches)) {
$php = $this->changeClassName($matches, $php, $ns);
}
// Find all class definition
if (preg_match_all('/(\n|\s)\s*class\s+(?<classname>[^\s]+)/i', $php, $matches)) {
$php = $this->changeClassName($matches, $php, $ns);
}
// Find all instanceof definition
if (preg_match_all('/\s+instanceof\s+(?<classname>[\\\a-z_0-9]+)/i', $php, $matches)) {
$php = $this->changeClassName($matches, $php, $ns);
}
// Find all interface definition
if (preg_match_all('/(\n|\s)\s*interface\s+(?<classname>[^\s]+)/i', $php, $matches)) {
$php = $this->changeClassName($matches, $php, $ns);
}
// Find all class implements, class can implement many interfaces
if (preg_match_all('/\s+implements\s+(?<classes>.*)/i', $php, $matches)) {
$replace = $matches[0][0];
foreach (explode(',', $matches['classes'][0]) as $classname) {
$replace = $this->transformClassName($classname, $classname, $replace, $ns);
}
$php = str_replace($matches[0][0], $replace, $php);
}
// Find all class extends
if (preg_match_all('/\s+extends\s+(?<classname>[^\s]+)/i', $php, $matches)) {
$php = $this->changeClassName($matches, $php, $ns);
}
// Find all class creation
if (preg_match_all('/[\.\,\(\s\n=:]+\s*new\s+(?<classname>[^\(]+)\s*\(/i', $php, $matches)) {
$php = $this->changeClassName($matches, $php, $ns);
}
// Find all class hints
if (preg_match_all('/(\(|\,)\s*(?:array|(?<classname>[\\\a-z_0-9]+))\s*(\&|\$)/i', $php, $matches)) {
$php = $this->changeClassName($matches, $php, $ns);
}
// Replace special word with its value
$php = str_replace('__NAMESPACE__', '\'' . $ns . '\'', $php);
}
// Just concatenate file code
$php_code .= $php;
}
// Close namespace if we support
if (!$no_ns) $php_code .= "\n" . '}';
}
return $php_code;
}
public function code_array_combine(array & $source, array & $target)
{
foreach ($source as $ns => $files) {
// Если в целевом массиве нет нужного NS - создадим
if (!isset($target[$ns])) $target[$ns] = array();
// Запишем содержание NS/Files
foreach ($files as $file => $php) {
if (isset($target[$ns][$file]) && is_string($php)) $target[$ns][$file] .= $php;
else if (isset($target[$ns][$file]) && is_array($php)) {
$target[$ns][$file] = array_unique(array_merge($target[$ns][$file], $php));
} else $target[$ns][$file] = $php;
}
}
}
/**
* Выполнить рекурсивное "собирание" файла
*
* @param string $path Абсолютный путь к файлу сайта
*
* @param null $module
* @param array $code
* @param string $namespace
*
* @return string
*/
public function compress_php($path, $module = NULL, & $code = array(), $namespace = self::NS_GLOBAL)
{
// TODO: Довести до ума разпознование require - убрать точку с зяпятоц которая остается
// TODO: Убрать пустые линии
// TODO: Анализатор использования функция и переменных??
//trace(' + Вошли в функцию:'.$path.'('.$namespace.')');
$_path = $path;
$path = normalizepath(realpath($path));
// Если мы уже подключили данный файл или он не существует
if (isset($this->files[$path])) return $this->log(' ! Файл: [##], already compressed', $path);
else if (!is_file($path)) return $this->log(' ! Файл: [##], не существует', $_path);
else if (strpos($path, 'vendor/autoload.php') !== false) return $this->log(' Ignoring composer autoloader [##]', $path);
else if (in_array(basename($path), $this->ignoredFiles)) {
return $this->log(' Ignoring file[##] by configuration', $path);
}
$this->log(' -- Compressing file [##]', $path);
// Load file
require_once($path);
//trace('Чтение файла: '.$path );
// Сохраним файл
$this->files[$path] = $path;
// Относительный путь к файлу
if (isset($rel_path)) $this->files[$rel_path] = $path;
// Прочитаем php файл
$fileStr = file_get_contents($path);
// Если в файле нет namespace - считаем его глобальным
if (strpos($fileStr, 'namespace') === false)
//$file_dir = '';
// Вырежим путь к файлу
//$file_dir = (pathinfo( $path, PATHINFO_DIRNAME ) == '.' ? '' : pathinfo( $path, PATHINFO_DIRNAME ).'/');
// Сюда соберем код программы
$main_code = '';
$main_code = "\n" . '// Модуль: ' . m($module)->id() . ', файл: ' . $path . "\n";
// Создадим уникальную коллекцию алиасов для NS
if (!isset($code[$namespace]['uses'])) $code[$namespace]['uses'] = array();
// Установим ссылку на коллекцию алиасов
$uses = &$code[$namespace]['uses'];
// Local file uses collection
$file_uses = array();
// Получим константы документа
$consts = get_defined_constants();
// Маркеры для отрезания специальных блоков которые не нужны в PRODUCTION
$rmarker_st = '\/\/\[PHPCOMPRESSOR\(remove\,start\)\]';
$rmarker_en = '\/\/\[PHPCOMPRESSOR\(remove\,end\)\]';
// Найдем все "ненужные" блоки кода и уберем их
$fileStr = preg_replace('/' . $rmarker_st . '.*?' . $rmarker_en . '/uis', '', $fileStr);
$className = '';
$classConstList = array();
//TODO: Fix to normal external dependency with ResourceRouter
$fileStr = preg_replace_callback('/(\\\\samson\\\\resourcer\\\\)?ResourceRouter::url\((\'|\")(?<path>[^,)]+)(\'|\")(,(?<module>[^)]+))?\);/i', array($this, 'rewriteResourceRouter'), $fileStr);
/** @var bool $classStared Flag for matching trait uses */
$classStared = false;
// Разберем код программы
$tokens = token_get_all($fileStr);
for ($i = 0; $i < sizeof($tokens); $i++) {
// Получим следующий жетон из кода программы
$token = $tokens[$i];
// Если просто строка
if (is_string($token)) $main_code .= $token;
// Если это специальный жетон
else {
// token array
list($id, $text) = $token;
// Перебирем тип комманды
switch ($id) {
case T_COMMENT: // Пропускаем все комментарии
case T_DOC_COMMENT:
case T_CLOSE_TAG: // Начало,конец файла
case T_OPEN_TAG:
break;
case T_WHITESPACE:
$main_code .= $text; /*$main_code .= ' ';*/
break;
// Обработаем алиасы
case T_USE:
$_use = '';
// Переберем все что иде после комманды алиаса
for ($j = $i + 1; $j < sizeof($tokens); $j++) {
// Получим идентификатор метки и текстовое представление
$id = isset($tokens[$j][0]) ? $tokens[$j][0] : '';
$text = isset($tokens[$j][1]) ? $tokens[$j][1] : '';
//trace('"'.$id.'" - "'.$text.'"');
// Если use используется в функции
if ($id == '(') {
$j--;
break;
}
// Если это закрывающая скобка - прекратим собирание пути к файлу
if ($id == ';') break;
// Все пробелы игнорирую
if ($id == T_WHITESPACE) continue;
// Если у метки есть текстовое представление
if (isset($text)) {
// Если єто константа
if (isset($consts[$text])) $_use .= $consts[$text];
// Если это путь
else $_use .= $text;
}
}
// Если это не use в inline функции - добавим алиас в коллекцию
// для данного ns с проверкой на уникальность
if ($id !== '(') {
// If this tait use
if ($classStared) {
// Consider rewriting trait usage fully qualified name
//TODO: Not fully qualified trait name adds slash before
$_use = strpos($_use, '\\') === false
? '\\' . $namespace . '\\' . $_use
: $_use;
// TODO: Import trait code
if (!trait_exists($_use)) {
throw new \Exception('Trait "' . $_use . '" does not exists in "' . $path . '"');
} else {
$main_code .= ' use ' . $_use . ';';
}
} else {
// Преведем все use к одному виду
if ($_use{0} !== '\\') {
$_use = '\\' . $_use;
}
// Add local file uses
$file_uses[] = $_use;
// TODO: Вывести замечание что бы код везде был одинаковый
if (!in_array($_use, $uses)) {
$uses[] = $_use;
}
}
} else {
$main_code .= ' use ';
}
// Сместим указатель чтения файла
$i = $j;
break;
case T_NAMESPACE:
// Определим временное пространство имен
$_namespace = '';
// Переберем все что иде после комманды подключения файла
for ($j = $i + 1; $j < sizeof($tokens); $j++) {
// Получим идентификатор метки и текстовое представление
$id = isset($tokens[$j][0]) ? $tokens[$j][0] : '';
$text = isset($tokens[$j][1]) ? $tokens[$j][1] : '';
//trace('"'.$id.'" - "'.$text.'"');
// Если это закрывающая скобка - прекратим собирание пути к файлу
if ($id == ')' || $id == ';' || $id == '{') break;
// Все пробелы игнорирую
if ($id == T_WHITESPACE) continue;
// Если у метки есть текстовое представление
if (isset($text)) {
// Если єто константа
if (isset($consts[$text])) $_namespace .= $consts[$text];
// Если это путь
else $_namespace .= $text;
}
}
// Если найденный NS отличается от текущего - установим переход к новому NS
if ($namespace !== $_namespace) {
// Сохраним новый как текущий
$namespace = strtolower($_namespace);
//trace(' #'.$i.' -> Изменили NS с '.$namespace.' на '.$_namespace);
// Если мы еще не создали данный NS
if (!isset($code[$namespace])) $code[$namespace] = array();
// Создадим уникальную коллекцию алиасов для NS
if (!isset($code[$namespace]['uses'])) $code[$namespace]['uses'] = array();
// Установим ссылку на коллекцию алиасов
$uses = &$code[$namespace]['uses'];
}
// Сместим указатель чтения файла
$i = $j;
break;
// Выделяем код подключаемых файлов
case T_REQUIRE :
case T_REQUIRE_ONCE :
//case T_INCLUDE :
case T_INCLUDE_ONCE: {
// Получим путь к подключаемому файлу
$file_path = '';
// Переберем все что иде после комманды подключения файла
for ($j = $i + 1; $j < sizeof($tokens); $j++) {
// Получим идентификатор метки и текстовое представление
$id = isset($tokens[$j][0]) ? $tokens[$j][0] : '';
$text = isset($tokens[$j][1]) ? $tokens[$j][1] : '';
//trace('"'.$id.'" - "'.$text.'"');
// Если это закрывающая скобка - прекратим собирание пути к файлу
if ($id == ';') break;
// Все пробелы игнорирую
if ($id == T_WHITESPACE) continue;
// Если у метки есть текстовое представление
if (isset($text)) {
// Если єто константа
if (isset($consts[$text])) $file_path .= $consts[$text];
// Если это путь
else $file_path .= $text;
}
}
// Если указан путь к файлу
if (isset($file_path{1})) {
// Уберем ковычки
$file_path = str_replace(array("'", '"'), array('', ''), $file_path);
// Если это не абсолютный путь - попробуем относительный
if (!file_exists($file_path)) $file_path = pathname($path) . $file_path;
// Если файл найден - получим его содержимое
if (file_exists($file_path)) {
//trace('Углубляемся в файл:'.$file_path.'('.$namespace.')');
// Углубимся в рекурсию
$this->compress_php($file_path, $module, $code, $namespace);
// Измением позицию маркера чтения файла
$i = $j + 1;
}
} else {
$main_code .= $text;
}
}
break;
case T_INTERFACE:
case T_CLASS:
$classStared = true;
$main_code .= $text;
for ($j = $i + 1; $j < sizeof($tokens); $j++) {
// Get id and text of token
$id = isset($tokens[$j][0]) ? $tokens[$j][0] : '';
$text = isset($tokens[$j][1]) ? $tokens[$j][1] : '';
// Ignore all whitespace
if ($id == T_WHITESPACE) continue;
if (isset($text)) {
$className = $text;
break;
}
}
break;
case T_CONST:
$main_code .= $text;
$classConst = array();
$nameFlag = 'name';
for ($j = $i + 1; $j < sizeof($tokens); $j++) {
// Get id and text of token
$id = isset($tokens[$j][0]) ? $tokens[$j][0] : '';
$text = isset($tokens[$j][1]) ? $tokens[$j][1] : '';
if ($id == ';') break;
// Ignore all whitespace
if ($id == T_WHITESPACE) continue;
if ($id == '=') {
$nameFlag = 'value';
continue;
}
if (isset($text)) {
// Is it defined constant
if (isset($consts[$text])) $classConst[$nameFlag] = $consts[$text];
else $classConst[$nameFlag] = $text;
}
}
$classConstList[$classConst['name']] = $classConst['value'];
break;
// Собираем основной код программы
default:
$main_code .= $text;
break;
}
}
}
// Replace all class shortcut usage with full name
if (count($file_uses)) {
$main_code = $this->removeUSEStatement($main_code, $file_uses);
}
$matches = array();
if ($className == 'Module') {
$temp = '';
}
if (preg_match_all('/(?<start>[(=+-\/*%., \n\t])(?<class>[\\\\a-zA-Z_]+)::(?<name>[a-zA-Z_]+)(?<end>[):;=+-\/*%., \n\t])/i', $main_code, $matches)) {
for ($i = 0; $i < sizeof($matches['name']); $i++) {
$matchClass = $matches['class'][$i];
// If this is self - use current file class
if ($matches['class'][$i] === 'self') {
$constantName = $namespace . '\\' . $className;
} elseif ($matches['class'][$i] == $className) {
// If this is current class add namespace
$constantName = $namespace . '\\' . $className;
} elseif ($matches['class'][$i] === 'parent') {
continue;
} elseif ($matches['class'][$i] === 'static') {
continue;
} else {
$constantName = $matches['class'][$i];
}
// If constant has no namespace - use current
if (strpos($constantName, '\\') === false) {
$constantName = $namespace . '\\' . $constantName;
}
// Add constant name
$constantName .= '::' . $matches['name'][$i];
$replaceName = $matches['start'][$i] . $matchClass . '::' . $matches['name'][$i] . $matches['end'][$i];
// Check if we have this constant defined
if (defined($constantName)) {
// Get constant value
$value = constant($constantName);
// Fix slashes, add quotes for string
$value = is_string($value) ? str_replace('\\', '\\\\\\\\', "'" . $value . "'") : $value;
$replacer = str_replace('\\', '\\\\', $replaceName);
$replacer = str_replace(array(')','('), array('\)', '\('), $replacer);
// Replace constant call in the code
$main_code = preg_replace(
'/' . $replacer . '/i', //([;=+-\/*%., ])
$matches['start'][$i] . $value . $matches['end'][$i],
$main_code
);
}
}
}
// Запишем в коллекцию кода полученный код
$code[$namespace][$path] = $main_code;
return $main_code;
}
/**
* Transform class name with namespace to PHP 5.2 format
* @param $source
* @param $className
* @param $php
* @param $ns
*
* @return mixed
*/
private function transformClassName($source, $className, $php, $ns)
{
// Create copy
$nClassName = trim($className);
// If this class uses other namespace or in global namespace
if (strrpos($nClassName, '\\') > 0) {
// If this is full class name
if ($nClassName{0} == '\\') {
// Remove global name space character from beginning
$nClassName = substr($nClassName, 1);
}
// Transform namespace
$nClassName = str_replace('\\', '_', $nClassName);
} else if ($nClassName{0} == '\\') { // This is global namespace class
// Remove first character "\"
$nClassName = substr($nClassName, 1);
} else { // No name space in class name
// Create old-styled namespace format
$nClassName = str_replace('\\', '_', $ns) . '_' . $nClassName;
}
// Replace class name in source
$replace = str_replace($className, $nClassName, $source);
if (strpos($source, 'm(')) {
//trace($source, true);
}
// Replace code
$php = str_ireplace($source, $replace, $php);
//trace('Changing class name('.$ns.')"'.htmlentities(trim($className)).'" with "'.htmlentities(trim($nClassName)).'"');
//trace('Replacing "'.htmlentities(trim($source)).'" with "'.htmlentities(trim($replace)).'"');
return $php;
}
/**
* Remove blank lines from code
* http://stackoverflow.com/questions/709669/how-do-i-remove-blank-lines-from-text-in-php
* @param string $code Code for removing blank lines
* @return string Modified code
*/
protected function removeBlankLines($code)
{
// New line is required to split non-blank lines
return preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $code);
}
/** Change class name to old format without namespace */
private function changeClassName($matches, $php, $ns, $uses = array())
{
// Iterate all class name usage matches
for ($i = 0; $i < sizeof($matches[0]); $i++) {
// Get source matching string
$source = $matches[0][$i];
// Get found classname
$className = &$matches['classname'][$i];
// If class name found or this is variable
if (!isset($className) || !isset($className{0}) || strpos($className, '$') !== false) {
continue;
}
// Transform class name
$php = $this->transformClassName($source, $className, $php, $ns, $uses);
}
return $php;
}
/**
* Copy resources
*/
private function copy_path_resources($path_resources, $module_path, $module_output_path)
{
$this->log(' -> Copying resources from [##] to [##]', $module_path, $module_output_path);
// Iterate module resources
foreach ($path_resources as $extension => $resources) {
foreach ($resources as $resource) {
// Build relative module resource path
$relative_path = str_replace($module_path, '', $resource);
$this->resourceManager->compress($resource, $this->output . $module_output_path . $relative_path);
}
}
}
/**
* Remove all USE statements and replace class shortcuts to full class names
*
* @param string $code Code to work with
* @param array $classes Array of class names to replace
*
* @return bool|mixed|string
*/
private function removeUSEStatement($code, array $classes)
{
// Iterate found use statements
foreach (array_unique($classes) as $full_class) {
// Ignore trait uses
if (trait_exists($full_class)) {
continue;
}
// Get class shortcut
$class_name = \samson\core\AutoLoader::getOnlyClass($full_class);
// Check class existance
if (!class_exists($full_class) && !interface_exists($full_class)) {
//return e('Found USE statement for undeclared class ##', E_SAMSON_FATAL_ERROR, $full_class);
continue;
}
// Replace class static call
$code = preg_replace('/([^\\\a-z])' . $class_name . '::/i', '$1' . $full_class . '::', $code);
// Replace class implements calls
$code = preg_replace('/\s+implements(.*\W)' . $class_name . '([^\\\])/i', ' implements $1' . $full_class . '$2 ', $code);
// Handle instanceof operator
$code = preg_replace('/instanceof\s+' . $class_name . '/i', 'instanceof ' . $full_class . '', $code);
// Replace class extends calls
$code = preg_replace('/extends\s+' . $class_name . '/i', 'extends ' . $full_class . '', $code);
// Replace multiple class extends calls
$code = preg_replace('/\s+extends(.*\W),?\s' . $class_name . '([^\\\])/i', ' extends $1' . $full_class . '$2 ', $code);
// Replace class hint calls
$code = preg_replace('/(\(|\s|\,)\s*' . $class_name . '\s*(&|$)/i', '$1' . $full_class . ' $2', $code);
// Replace class creation call
$code = preg_replace('/new\s+' . $class_name . '\s*\(/i', 'new ' . $full_class . '(', $code);
// Replace annotations
$code = preg_replace('/([, (])' . $class_name . '\s\$/i', '$1 $2' . $full_class . ' $', $code);
}
return $code;
}
/**
* Преобразовать коллекцию имен классов в коллекцию
* [Namespace][ ClassFileName ]
*
* @param array $collection Коллекция имен классов
* @param array $classes Коллекция для возврата результатов
*/
private function classes_to_ns_files($collection, & $classes = array())
{
// Соберем коллекцию загруженных интерфейсов их файлов по пространствам имен
foreach ($collection as $class) {
$ac = new \ReflectionClass($class);
$ns = $ac->getNamespaceName();
if ($ns != '') {
$ns = strtolower($ns);
if (!isset($classes[$ns])) {
$classes[$ns] = array();
}
$classes[$ns][normalizepath($ac->getFileName())] = '';
}
}
}
}