|
|
<?php |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace think\console\command\optimize; |
|
|
|
|
|
use think\App; |
|
|
use think\Config; |
|
|
use think\console\Command; |
|
|
use think\console\Input; |
|
|
use think\console\Output; |
|
|
|
|
|
class Autoload extends Command |
|
|
{ |
|
|
|
|
|
protected function configure() |
|
|
{ |
|
|
$this->setName('optimize:autoload') |
|
|
->setDescription('Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.'); |
|
|
} |
|
|
|
|
|
protected function execute(Input $input, Output $output) |
|
|
{ |
|
|
|
|
|
$classmapFile = <<<EOF |
|
|
<?php |
|
|
/** |
|
|
* 类库映射 |
|
|
*/ |
|
|
|
|
|
return [ |
|
|
|
|
|
EOF; |
|
|
|
|
|
$namespacesToScan = [ |
|
|
App::$namespace . '\\' => realpath(rtrim(APP_PATH)), |
|
|
'think\\' => LIB_PATH . 'think', |
|
|
'behavior\\' => LIB_PATH . 'behavior', |
|
|
'traits\\' => LIB_PATH . 'traits', |
|
|
'' => realpath(rtrim(EXTEND_PATH)), |
|
|
]; |
|
|
|
|
|
$root_namespace = Config::get('root_namespace'); |
|
|
foreach ($root_namespace as $namespace => $dir) { |
|
|
$namespacesToScan[$namespace . '\\'] = realpath($dir); |
|
|
} |
|
|
|
|
|
krsort($namespacesToScan); |
|
|
$classMap = []; |
|
|
foreach ($namespacesToScan as $namespace => $dir) { |
|
|
|
|
|
if (!is_dir($dir)) { |
|
|
continue; |
|
|
} |
|
|
|
|
|
$namespaceFilter = $namespace === '' ? null : $namespace; |
|
|
$classMap = $this->addClassMapCode($dir, $namespaceFilter, $classMap); |
|
|
} |
|
|
|
|
|
ksort($classMap); |
|
|
foreach ($classMap as $class => $code) { |
|
|
$classmapFile .= ' ' . var_export($class, true) . ' => ' . $code; |
|
|
} |
|
|
$classmapFile .= "];\n"; |
|
|
|
|
|
if (!is_dir(RUNTIME_PATH)) { |
|
|
@mkdir(RUNTIME_PATH, 0755, true); |
|
|
} |
|
|
|
|
|
file_put_contents(RUNTIME_PATH . 'classmap' . EXT, $classmapFile); |
|
|
|
|
|
$output->writeln('<info>Succeed!</info>'); |
|
|
} |
|
|
|
|
|
protected function addClassMapCode($dir, $namespace, $classMap) |
|
|
{ |
|
|
foreach ($this->createMap($dir, $namespace) as $class => $path) { |
|
|
|
|
|
$pathCode = $this->getPathCode($path) . ",\n"; |
|
|
|
|
|
if (!isset($classMap[$class])) { |
|
|
$classMap[$class] = $pathCode; |
|
|
} elseif ($classMap[$class] !== $pathCode && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($classMap[$class] . ' ' . $path, '\\', '/'))) { |
|
|
$this->output->writeln( |
|
|
'<warning>Warning: Ambiguous class resolution, "' . $class . '"' . |
|
|
' was found in both "' . str_replace(["',\n"], [ |
|
|
'', |
|
|
], $classMap[$class]) . '" and "' . $path . '", the first will be used.</warning>' |
|
|
); |
|
|
} |
|
|
} |
|
|
return $classMap; |
|
|
} |
|
|
|
|
|
protected function getPathCode($path) |
|
|
{ |
|
|
|
|
|
$baseDir = ''; |
|
|
$libPath = $this->normalizePath(realpath(LIB_PATH)); |
|
|
$appPath = $this->normalizePath(realpath(APP_PATH)); |
|
|
$extendPath = $this->normalizePath(realpath(EXTEND_PATH)); |
|
|
$rootPath = $this->normalizePath(realpath(ROOT_PATH)); |
|
|
$path = $this->normalizePath($path); |
|
|
|
|
|
if ($libPath !== null && strpos($path, $libPath . '/') === 0) { |
|
|
$path = substr($path, strlen(LIB_PATH)); |
|
|
$baseDir = 'LIB_PATH'; |
|
|
} elseif ($appPath !== null && strpos($path, $appPath . '/') === 0) { |
|
|
$path = substr($path, strlen($appPath) + 1); |
|
|
$baseDir = 'APP_PATH'; |
|
|
} elseif ($extendPath !== null && strpos($path, $extendPath . '/') === 0) { |
|
|
$path = substr($path, strlen($extendPath) + 1); |
|
|
$baseDir = 'EXTEND_PATH'; |
|
|
} elseif ($rootPath !== null && strpos($path, $rootPath . '/') === 0) { |
|
|
$path = substr($path, strlen($rootPath) + 1); |
|
|
$baseDir = 'ROOT_PATH'; |
|
|
} |
|
|
|
|
|
if ($path !== false) { |
|
|
$baseDir .= " . "; |
|
|
} |
|
|
|
|
|
return $baseDir . (($path !== false) ? var_export($path, true) : ""); |
|
|
} |
|
|
|
|
|
protected function normalizePath($path) |
|
|
{ |
|
|
if ($path === false) { |
|
|
return; |
|
|
} |
|
|
$parts = []; |
|
|
$path = strtr($path, '\\', '/'); |
|
|
$prefix = ''; |
|
|
$absolute = false; |
|
|
|
|
|
if (preg_match('{^([0-9a-z]+:(?://(?:[a-z]:)?)?)}i', $path, $match)) { |
|
|
$prefix = $match[1]; |
|
|
$path = substr($path, strlen($prefix)); |
|
|
} |
|
|
|
|
|
if (substr($path, 0, 1) === '/') { |
|
|
$absolute = true; |
|
|
$path = substr($path, 1); |
|
|
} |
|
|
|
|
|
$up = false; |
|
|
foreach (explode('/', $path) as $chunk) { |
|
|
if ('..' === $chunk && ($absolute || $up)) { |
|
|
array_pop($parts); |
|
|
$up = !(empty($parts) || '..' === end($parts)); |
|
|
} elseif ('.' !== $chunk && '' !== $chunk) { |
|
|
$parts[] = $chunk; |
|
|
$up = '..' !== $chunk; |
|
|
} |
|
|
} |
|
|
|
|
|
return $prefix . ($absolute ? '/' : '') . implode('/', $parts); |
|
|
} |
|
|
|
|
|
protected function createMap($path, $namespace = null) |
|
|
{ |
|
|
if (is_string($path)) { |
|
|
if (is_file($path)) { |
|
|
$path = [new \SplFileInfo($path)]; |
|
|
} elseif (is_dir($path)) { |
|
|
|
|
|
$objects = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::SELF_FIRST); |
|
|
|
|
|
$path = []; |
|
|
|
|
|
|
|
|
foreach ($objects as $object) { |
|
|
if ($object->isFile() && $object->getExtension() == 'php') { |
|
|
$path[] = $object; |
|
|
} |
|
|
} |
|
|
} else { |
|
|
throw new \RuntimeException( |
|
|
'Could not scan for classes inside "' . $path . |
|
|
'" which does not appear to be a file nor a folder' |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
$map = []; |
|
|
|
|
|
|
|
|
foreach ($path as $file) { |
|
|
$filePath = $file->getRealPath(); |
|
|
|
|
|
if (pathinfo($filePath, PATHINFO_EXTENSION) != 'php') { |
|
|
continue; |
|
|
} |
|
|
|
|
|
$classes = $this->findClasses($filePath); |
|
|
|
|
|
foreach ($classes as $class) { |
|
|
if (null !== $namespace && 0 !== strpos($class, $namespace)) { |
|
|
continue; |
|
|
} |
|
|
|
|
|
if (!isset($map[$class])) { |
|
|
$map[$class] = $filePath; |
|
|
} elseif ($map[$class] !== $filePath && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($map[$class] . ' ' . $filePath, '\\', '/'))) { |
|
|
$this->output->writeln( |
|
|
'<warning>Warning: Ambiguous class resolution, "' . $class . '"' . |
|
|
' was found in both "' . $map[$class] . '" and "' . $filePath . '", the first will be used.</warning>' |
|
|
); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return $map; |
|
|
} |
|
|
|
|
|
protected function findClasses($path) |
|
|
{ |
|
|
$extraTypes = '|trait'; |
|
|
|
|
|
$contents = @php_strip_whitespace($path); |
|
|
if (!$contents) { |
|
|
if (!file_exists($path)) { |
|
|
$message = 'File at "%s" does not exist, check your classmap definitions'; |
|
|
} elseif (!is_readable($path)) { |
|
|
$message = 'File at "%s" is not readable, check its permissions'; |
|
|
} elseif ('' === trim(file_get_contents($path))) { |
|
|
return []; |
|
|
} else { |
|
|
$message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted'; |
|
|
} |
|
|
$error = error_get_last(); |
|
|
if (isset($error['message'])) { |
|
|
$message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message']; |
|
|
} |
|
|
throw new \RuntimeException(sprintf($message, $path)); |
|
|
} |
|
|
|
|
|
if (!preg_match('{\b(?:class|interface' . $extraTypes . ')\s}i', $contents)) { |
|
|
return []; |
|
|
} |
|
|
|
|
|
|
|
|
$contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents); |
|
|
|
|
|
$contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); |
|
|
|
|
|
if (substr($contents, 0, 2) !== '<?') { |
|
|
$contents = preg_replace('{^.+?<\?}s', '<?', $contents, 1, $replacements); |
|
|
if ($replacements === 0) { |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
$contents = preg_replace('{\?>.+<\?}s', '?><?', $contents); |
|
|
|
|
|
$pos = strrpos($contents, '?>'); |
|
|
if (false !== $pos && false === strpos(substr($contents, $pos), '<?')) { |
|
|
$contents = substr($contents, 0, $pos); |
|
|
} |
|
|
|
|
|
preg_match_all('{ |
|
|
(?: |
|
|
\b(?<![\$:>])(?P<type>class|interface' . $extraTypes . ') \s++ (?P<name>[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) |
|
|
| \b(?<![\$:>])(?P<ns>namespace) (?P<nsname>\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] |
|
|
) |
|
|
}ix', $contents, $matches); |
|
|
|
|
|
$classes = []; |
|
|
$namespace = ''; |
|
|
|
|
|
for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { |
|
|
if (!empty($matches['ns'][$i])) { |
|
|
$namespace = str_replace([' ', "\t", "\r", "\n"], '', $matches['nsname'][$i]) . '\\'; |
|
|
} else { |
|
|
$name = $matches['name'][$i]; |
|
|
if ($name[0] === ':') { |
|
|
$name = 'xhp' . substr(str_replace(['-', ':'], ['_', '__'], $name), 1); |
|
|
} elseif ($matches['type'][$i] === 'enum') { |
|
|
$name = rtrim($name, ':'); |
|
|
} |
|
|
$classes[] = ltrim($namespace . $name, '\\'); |
|
|
} |
|
|
} |
|
|
|
|
|
return $classes; |
|
|
} |
|
|
|
|
|
} |
|
|
|