// +---------------------------------------------------------------------- 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 = << 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('Succeed!'); } 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: Ambiguous class resolution, "' . $class . '"' . ' was found in both "' . str_replace(["',\n"], [ '', ], $classMap[$class]) . '" and "' . $path . '", the first will be used.' ); } } 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 = []; /** @var \SplFileInfo $object */ 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 = []; /** @var \SplFileInfo $file */ 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: Ambiguous class resolution, "' . $class . '"' . ' was found in both "' . $map[$class] . '" and "' . $filePath . '", the first will be used.' ); } } } 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 []; } // strip heredocs/nowdocs $contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents); // strip strings $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); // strip leading non-php code if needed if (substr($contents, 0, 2) !== '.+<\?}s', '?>'); if (false !== $pos && false === strpos(substr($contents, $pos), '])(?Pclass|interface' . $extraTypes . ') \s++ (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) | \b(?])(?Pnamespace) (?P\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; } }