根目录标识 => 绝对路径 */ protected array $roots = []; /** @var array 根目录标识 => 命名空间前缀 */ protected array $namespaces = []; public function __construct(App $app) { $this->app = $app; $this->roots = $app->config->get('app.app_roots', [ 'app' => $app->getBasePath(), 'addon' => $app->getRootPath() . 'addon' . DIRECTORY_SEPARATOR, ]); // $this->namespaces = $app->config->get('app.app_namespaces', [ // 'app' => 'app', // 'addon' => 'addon', // ]); $this->namespaces = [ 'app' => 'app', 'addon' => 'addon', ]; } public function handle($request, Closure $next) { if (!$this->parseMultiApp()) { return $next($request); } return $this->app->middleware ->pipeline('app') ->send($request) ->then(function ($request) use ($next) { return $next($request); }); } protected function getRoutePath(): string { return $this->app->getAppPath() . 'route' . DIRECTORY_SEPARATOR; } /** * 核心解析 */ protected function parseMultiApp(): bool { $scriptName = $this->getScriptName(); $defaultApp = $this->app->config->get('app.default_app') ?: 'index'; $appName = $this->app->http->getName(); // ==================== 阶段 1:独立入口 / 显式绑定 ==================== if ($appName || ($scriptName && !in_array($scriptName, ['index', 'router', 'think']))) { $appName = $appName ?: $scriptName; $this->app->http->setBind(); // 独立入口可不走 domain_bind,直接解析 $resolved = $this->resolve($appName); if (!$resolved) { throw new HttpException(404, 'app not exists:' . $appName); } return $this->applyResolved($resolved, $appName); } // ==================== 阶段 2: 自动识别 ==================== $this->app->http->setBind(false); $appName = null; // -------- 2.1 域名绑定 -------- $bind = $this->app->config->get('app.domain_bind', []); if (!empty($bind)) { $subDomain = $this->app->request->subDomain(); $domain = $this->app->request->host(true); if (isset($bind[$domain])) { $appName = $bind[$domain]; $this->app->http->setBind(); } elseif (isset($bind[$subDomain])) { $appName = $bind[$subDomain]; $this->app->http->setBind(); } elseif (isset($bind['*'])) { $appName = $bind['*']; $this->app->http->setBind(); } } if ($this->app->http->isBind()) { $resolved = $this->resolve($appName); if (!$resolved) { throw new HttpException(404, 'app not exists:' . $appName); } return $this->applyResolved($resolved, $appName); } // -------- 2.2 URL 路径识别 -------- $path = $this->app->request->pathinfo(); $map = $this->app->config->get('app.app_map', []); $deny = $this->app->config->get('app.deny_app_list', []); $fullName = current(explode('/', $path)); if (strpos($fullName, '.')) { $fullName = strstr($fullName, '.', true); } // 分支1: 命中 app_map if (isset($map[$fullName])) { if ($map[$fullName] instanceof Closure) { $mapped = call_user_func_array($map[$fullName], [$this->app]) ?: $fullName; } else { $mapped = $map[$fullName]; } $resolved = $this->resolve($mapped); } // 分支 2: 黑名单 / map 显式禁用 elseif ($fullName !== '' && (false !== array_search($fullName, $map) || in_array($fullName, $deny))) { throw new HttpException(404, 'app not exists:' . $fullName); } // 分支 3: map 通配 * elseif ($fullName !== '' && isset($map['*'])) { $resolved = $this->resolve($map['*']); } // 分支 4: URL 段本身带前缀(强制指定根) elseif (str_contains($fullName, ':')) { $resolved = $this->resolve($fullName); } // 分支 5: 默认行为 —— 多根顺序查找 else { $name = $fullName !== '' ? $fullName : null; // URL 没给应用名 → 用 default_app $name ??= $defaultApp; $resolved = $this->resolve($name); // ← prefix='',走多根查找 } // ===== 查找失败处理 ===== if (!$resolved) { // app_express=true 且不是显式前缀时 → 兜底 default_app $express = $this->app->config->get('app.app_express', false); if ($express && !str_contains($fullName, ':')) { $resolved = $this->resolve($defaultApp); } if (!$resolved) { throw new HttpException(404, 'app not exists:' . ($fullName ?: $defaultApp)); } } // 重写 pathinfo: 把 URL 第一段剥掉 if ($fullName) { $this->app->request->setRoot('/' . $fullName); $this->app->request->setPathinfo( strpos($path, '/') ? ltrim(strstr($path, '/'), '/') : '' ); } return $this->applyResolved($resolved, $fullName); } /** * 解析应用名为最终 [prefix, name, appPath] * 支持: * "app2:blog" → ['app2', 'blog', '/www/app2/blog/'] * "blog" → ['', 'blog', null] ← 调用方拿到 null 时按 roots 顺序兜底 */ protected function resolve(string $appName): ?array { [$prefix, $name] = $this->splitAppName($appName); if ($prefix !== '') { // 显式前缀: 必须严格匹配,失败立即返回 null(不再降级) if (!isset($this->roots[$prefix])) { return null; } $path = $this->roots[$prefix] . $name . DIRECTORY_SEPARATOR; return is_dir($path) ? [$prefix, $name, $path] : null; } // 多根顺序查找 foreach ($this->roots as $key => $root) { $path = $root . $name . DIRECTORY_SEPARATOR; if (is_dir($path)) { return [$key, $name, $path]; } } return null; } /** * 拆分应用名 * "app2:blog" → ["app2", "blog"] * "blog" → ["", "blog"] */ protected function splitAppName(string $fullName): array { if ($fullName !== '' && str_contains($fullName, ':')) { [$prefix, $name] = explode(':', $fullName, 2); return [$prefix ?: '', $name]; } return ['', $fullName]; } protected function getScriptName(): string { if (isset($_SERVER['SCRIPT_FILENAME'])) { $file = $_SERVER['SCRIPT_FILENAME']; } elseif (isset($_SERVER['argv'][0])) { $file = realpath($_SERVER['argv'][0]); } return isset($file) ? pathinfo($file, PATHINFO_FILENAME) : ''; } /** * 把 resolve() 结果落地到 App上下文 */ protected function applyResolved(array $resolved, string $urlName): bool { [$prefix, $name, $appPath] = $resolved; $this->app->http->name($name); // 应用目录 $this->app->setAppPath($appPath); // 命名空间 $nsPrefix = $this->namespaces[$prefix] ?? $prefix; $this->app->setNamespace($nsPrefix . '\\' . $name); // 运行时: 按根 + 应用双层隔离 $this->app->setRuntimePath( $this->app->getRuntimePath() . $prefix . DIRECTORY_SEPARATOR . $name . DIRECTORY_SEPARATOR ); // 路由目录 $this->app->http->setRoutePath($this->getRoutePath()); // 应用专属配置/中间件/provider/语言包 $this->loadApp($name, $appPath); return true; } protected function loadApp(string $appName, string $appPath): void { if (is_file($appPath . 'common.php')) { include_once $appPath . 'common.php'; } $files = glob($appPath . 'config' . DIRECTORY_SEPARATOR . '*' . $this->app->getConfigExt()); foreach ($files as $file) { $this->app->config->load($file, pathinfo($file, PATHINFO_FILENAME)); } if (is_file($appPath . 'event.php')) { $this->app->loadEvent(include $appPath . 'event.php'); } if (is_file($appPath . 'middleware.php')) { $this->app->middleware->import(include $appPath . 'middleware.php', 'app'); } if (is_file($appPath . 'provider.php')) { $this->app->bind(include $appPath . 'provider.php'); } $this->app->loadLangPack($this->app->lang->defaultLangSet()); } }