<?php
namespace think;
use think\Config;
use think\Env;
use think\Exception;
use think\exception\HttpException;
use think\exception\HttpResponseException;
use think\Hook;
use think\Lang;
use think\Loader;
use think\Log;
use think\Request;
use think\Response;
use think\Route;
class App
{
protected static $init = false;
public static $modulePath;
public static $debug = true;
public static $namespace = 'app';
public static $suffix = false;
protected static $routeCheck;
protected static $routeMust;
protected static $dispatch;
protected static $file = [];
public static function run(Request $request = null)
{
is_null($request) && $request = Request::instance();
$config = self::initCommon();
if (defined('BIND_MODULE')) {
BIND_MODULE && Route::bind(BIND_MODULE);
} elseif ($config['auto_bind_module']) {
$name = pathinfo($request->baseFile(), PATHINFO_FILENAME);
if ($name && 'index' != $name && is_dir(APP_PATH . $name)) {
Route::bind($name);
}
}
$request->filter($config['default_filter']);
try {
if ($config['lang_switch_on']) {
$request->langset(Lang::detect());
Lang::load(THINK_PATH . 'lang' . DS . $request->langset() . EXT);
if (!$config['app_multi_module']) {
Lang::load(APP_PATH . 'lang' . DS . $request->langset() . EXT);
}
}
$dispatch = self::$dispatch;
if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config);
}
$request->dispatch($dispatch);
if (self::$debug) {
Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
}
Hook::listen('app_begin', $dispatch);
switch ($dispatch['type']) {
case 'redirect':
$data = Response::create($dispatch['url'], 'redirect')->code($dispatch['status']);
break;
case 'module':
$data = self::module($dispatch['module'], $config, isset($dispatch['convert']) ? $dispatch['convert'] : null);
break;
case 'controller':
$data = Loader::action($dispatch['controller']);
break;
case 'method':
$data = self::invokeMethod($dispatch['method']);
break;
case 'function':
$data = self::invokeFunction($dispatch['function']);
break;
case 'response':
$data = $dispatch['response'];
break;
default:
throw new \InvalidArgumentException('dispatch type not support');
}
} catch (HttpResponseException $exception) {
$data = $exception->getResponse();
}
Loader::clearInstance();
if ($data instanceof Response) {
$response = $data;
} elseif (!is_null($data)) {
$isAjax = $request->isAjax();
$type = $isAjax ? Config::get('default_ajax_return') : Config::get('default_return_type');
$response = Response::create($data, $type);
} else {
$response = Response::create();
}
Hook::listen('app_end', $response);
return $response;
}
public static function dispatch($dispatch, $type = 'module')
{
self::$dispatch = ['type' => $type, $type => $dispatch];
}
public static function invokeFunction($function, $vars = [])
{
$reflect = new \ReflectionFunction($function);
$args = self::bindParams($reflect, $vars);
self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info');
return $reflect->invokeArgs($args);
}
public static function invokeMethod($method, $vars = [])
{
if (is_array($method)) {
$class = is_object($method[0]) ? $method[0] : new $method[0];
$reflect = new \ReflectionMethod($class, $method[1]);
} else {
$reflect = new \ReflectionMethod($method);
}
$args = self::bindParams($reflect, $vars);
self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info');
return $reflect->invokeArgs(isset($class) ? $class : null, $args);
}
private static function bindParams($reflect, $vars = [])
{
if (empty($vars)) {
if (Config::get('url_param_type')) {
$vars = Request::instance()->route();
} else {
$vars = Request::instance()->param();
}
}
$args = [];
reset($vars);
$type = key($vars) === 0 ? 1 : 0;
if ($reflect->getNumberOfParameters() > 0) {
$params = $reflect->getParameters();
foreach ($params as $param) {
$name = $param->getName();
$class = $param->getClass();
if ($class) {
$className = $class->getName();
if (isset($vars[$name]) && $vars[$name] instanceof $className) {
$args[] = $vars[$name];
unset($vars[$name]);
} else {
$args[] = method_exists($className, 'instance') ? $className::instance() : new $className();
}
} elseif (1 == $type && !empty($vars)) {
$args[] = array_shift($vars);
} elseif (0 == $type && isset($vars[$name])) {
$args[] = $vars[$name];
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue();
} else {
throw new \InvalidArgumentException('method param miss:' . $name);
}
}
array_walk_recursive($args, [Request::instance(), 'filterExp']);
}
return $args;
}
public static function module($result, $config, $convert = null)
{
if (is_string($result)) {
$result = explode('/', $result);
}
$request = Request::instance();
if ($config['app_multi_module']) {
$module = strip_tags(strtolower($result[0] ?: $config['default_module']));
$bind = Route::getBind('module');
$available = false;
if ($bind) {
list($bindModule) = explode('/', $bind);
if (empty($result[0])) {
$module = $bindModule;
$available = true;
} elseif ($module == $bindModule) {
$available = true;
}
} elseif (!in_array($module, $config['deny_module_list']) && is_dir(APP_PATH . $module)) {
$available = true;
}
if ($module && $available) {
$request->module($module);
$config = self::init($module);
} else {
throw new HttpException(404, 'module not exists:' . $module);
}
} else {
$module = '';
$request->module($module);
}
App::$modulePath = APP_PATH . ($module ? $module . DS : '');
$convert = is_bool($convert) ? $convert : $config['url_convert'];
$controller = strip_tags($result[1] ?: $config['default_controller']);
$controller = $convert ? strtolower($controller) : $controller;
$actionName = strip_tags($result[2] ?: $config['default_action']);
$actionName = $convert ? strtolower($actionName) : $actionName;
$request->controller(Loader::parseName($controller, 1))->action($actionName);
Hook::listen('module_init', $request);
try {
$instance = Loader::controller($controller, $config['url_controller_layer'], $config['controller_suffix'], $config['empty_controller']);
if (is_null($instance)) {
throw new HttpException(404, 'controller not exists:' . Loader::parseName($controller, 1));
}
$action = $actionName . $config['action_suffix'];
if (!preg_match('/^[A-Za-z](\w)*$/', $action)) {
throw new \ReflectionException('illegal action name:' . $actionName);
}
$call = [$instance, $action];
Hook::listen('action_begin', $call);
$data = self::invokeMethod($call);
} catch (\ReflectionException $e) {
if (method_exists($instance, '_empty')) {
$reflect = new \ReflectionMethod($instance, '_empty');
$data = $reflect->invokeArgs($instance, [$action]);
self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info');
} else {
throw new HttpException(404, 'method not exists:' . (new \ReflectionClass($instance))->getName() . '->' . $action);
}
}
return $data;
}
public static function initCommon()
{
if (empty(self::$init)) {
$config = self::init();
self::$suffix = $config['class_suffix'];
self::$debug = Env::get('app_debug', Config::get('app_debug'));
if (!self::$debug) {
ini_set('display_errors', 'Off');
} elseif (!IS_CLI) {
if (ob_get_level() > 0) {
$output = ob_get_clean();
}
ob_start();
if (!empty($output)) {
echo $output;
}
}
self::$namespace = $config['app_namespace'];
Loader::addNamespace($config['app_namespace'], APP_PATH);
if (!empty($config['root_namespace'])) {
Loader::addNamespace($config['root_namespace']);
}
if (!empty($config['extra_file_list'])) {
foreach ($config['extra_file_list'] as $file) {
$file = strpos($file, '.') ? $file : APP_PATH . $file . EXT;
if (is_file($file) && !isset(self::$file[$file])) {
include $file;
self::$file[$file] = true;
}
}
}
date_default_timezone_set($config['default_timezone']);
Hook::listen('app_init');
self::$init = $config;
}
return self::$init;
}
private static function init($module = '')
{
$module = $module ? $module . DS : '';
if (is_file(APP_PATH . $module . 'init' . EXT)) {
include APP_PATH . $module . 'init' . EXT;
} elseif (is_file(RUNTIME_PATH . $module . 'init' . EXT)) {
include RUNTIME_PATH . $module . 'init' . EXT;
} else {
$path = APP_PATH . $module;
$config = Config::load(CONF_PATH . $module . 'config' . CONF_EXT);
if ($config['extra_config_list']) {
foreach ($config['extra_config_list'] as $name => $file) {
$filename = CONF_PATH . $module . $file . CONF_EXT;
Config::load($filename, is_string($name) ? $name : pathinfo($filename, PATHINFO_FILENAME));
}
}
if ($config['app_status']) {
$config = Config::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT);
}
if (is_file(CONF_PATH . $module . 'tags' . EXT)) {
Hook::import(include CONF_PATH . $module . 'tags' . EXT);
}
if (is_file($path . 'common' . EXT)) {
include $path . 'common' . EXT;
}
if ($config['lang_switch_on'] && $module) {
Lang::load($path . 'lang' . DS . Request::instance()->langset() . EXT);
}
}
return Config::get();
}
public static function routeCheck($request, array $config)
{
$path = $request->path();
$depr = $config['pathinfo_depr'];
$result = false;
$check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
if ($check) {
if (is_file(RUNTIME_PATH . 'route.php')) {
$rules = include RUNTIME_PATH . 'route.php';
if (is_array($rules)) {
Route::rules($rules);
}
} else {
$files = $config['route_config_file'];
foreach ($files as $file) {
if (is_file(CONF_PATH . $file . CONF_EXT)) {
$rules = include CONF_PATH . $file . CONF_EXT;
if (is_array($rules)) {
Route::import($rules);
}
}
}
}
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
$must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];
if ($must && false === $result) {
throw new HttpException(404, 'Route Not Found');
}
}
if (false === $result) {
$result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
}
return $result;
}
public static function route($route, $must = false)
{
self::$routeCheck = $route;
self::$routeMust = $must;
}
}