243 lines
8.6 KiB
PHP
243 lines
8.6 KiB
PHP
<?php
|
|
|
|
namespace App\Seo;
|
|
|
|
use App\Contracts\Extension\ModuleManagerInterface;
|
|
use App\Contracts\Extension\PluginManagerInterface;
|
|
use App\Contracts\Extension\TemplateManagerInterface;
|
|
use App\Services\TemplateService;
|
|
|
|
class TemplateRouteResolver
|
|
{
|
|
public function __construct(
|
|
private readonly TemplateService $templateService,
|
|
private readonly TemplateManagerInterface $templateManager,
|
|
private readonly ModuleManagerInterface $moduleManager,
|
|
private readonly PluginManagerInterface $pluginManager,
|
|
) {}
|
|
|
|
/**
|
|
* URL을 템플릿 레이아웃으로 매핑합니다.
|
|
*
|
|
* @param string $url 요청 URL 경로
|
|
* @return array|null { templateIdentifier, layoutName, routeParams, moduleIdentifier? } 또는 null
|
|
*/
|
|
public function resolve(string $url): ?array
|
|
{
|
|
$loaded = $this->loadActiveRoutes('user');
|
|
if ($loaded === null) {
|
|
return null;
|
|
}
|
|
|
|
[$templateIdentifier, $routes] = $loaded;
|
|
|
|
foreach ($routes as $route) {
|
|
// auth_required 라우트 제외
|
|
if ($route['auth_required'] ?? false) {
|
|
continue;
|
|
}
|
|
|
|
// guest_only 라우트 제외
|
|
if ($route['guest_only'] ?? false) {
|
|
continue;
|
|
}
|
|
|
|
$routePath = $route['path'] ?? '';
|
|
$routePath = $this->resolveRoutePath($routePath);
|
|
|
|
$params = $this->matchRoute($routePath, $url);
|
|
if ($params !== null) {
|
|
$layoutName = $route['layout'] ?? '';
|
|
|
|
// 확장 식별자 추출 (레이아웃명에 '.' 포함 시)
|
|
$moduleIdentifier = null;
|
|
$pluginIdentifier = null;
|
|
if (str_contains($layoutName, '.')) {
|
|
[$extensionId, $layoutName] = explode('.', $layoutName, 2);
|
|
|
|
// 모듈/플러그인 판별
|
|
if ($this->moduleManager->getModule($extensionId) !== null) {
|
|
$moduleIdentifier = $extensionId;
|
|
} elseif ($this->pluginManager->getPlugin($extensionId) !== null) {
|
|
$pluginIdentifier = $extensionId;
|
|
} else {
|
|
// fallback: 기존 동작 유지 (모듈로 간주)
|
|
$moduleIdentifier = $extensionId;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'templateIdentifier' => $templateIdentifier,
|
|
'layoutName' => $layoutName,
|
|
'routeParams' => $params,
|
|
'moduleIdentifier' => $moduleIdentifier,
|
|
'pluginIdentifier' => $pluginIdentifier,
|
|
'routeMeta' => $route['meta'] ?? [],
|
|
];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* URL이 활성 템플릿(+모듈/플러그인 병합)의 등록된 라우트에 존재하는지 판별합니다.
|
|
*
|
|
* resolve()와 달리 auth_required/guest_only 라우트도 "존재"로 인정합니다.
|
|
* 미등록 경로 catch-all 의 404 판별에 사용합니다 (공개#47).
|
|
*
|
|
* @param string $url 요청 URL 경로 (선행 '/' 포함)
|
|
* @param string $type 템플릿 타입 ('user' | 'admin')
|
|
* @return bool 등록 라우트면 true, 미등록이면 false. 템플릿 미설치/로드 실패 시 true(404 억제, 안전측).
|
|
*/
|
|
public function routeExists(string $url, string $type = 'user'): bool
|
|
{
|
|
$loaded = $this->loadActiveRoutes($type);
|
|
// 템플릿 미설치/로드 실패 시 전 경로 404 차단 — 안전측 폴백 (D-47-3).
|
|
if ($loaded === null) {
|
|
return true;
|
|
}
|
|
|
|
[, $routes] = $loaded;
|
|
|
|
foreach ($routes as $route) {
|
|
// auth_required/guest_only 검사 없이 path 만 매칭 (resolve()와 차이 — 핵심).
|
|
$routePath = $this->resolveRoutePath($route['path'] ?? '');
|
|
|
|
if ($this->matchRoute($routePath, $url) !== null) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 활성 템플릿($type)의 병합 routes 배열을 로드합니다.
|
|
*
|
|
* @param string $type 템플릿 타입 ('user' | 'admin')
|
|
* @return array{0: string, 1: array}|null [templateIdentifier, routes] 또는 실패 시 null
|
|
*/
|
|
private function loadActiveRoutes(string $type): ?array
|
|
{
|
|
$activeTemplate = $this->templateManager->getActiveTemplate($type);
|
|
if (! $activeTemplate) {
|
|
return null;
|
|
}
|
|
|
|
$templateIdentifier = $activeTemplate['identifier'] ?? null;
|
|
if (! $templateIdentifier) {
|
|
return null;
|
|
}
|
|
|
|
$routesResult = $this->templateService->getRoutesDataWithModules($templateIdentifier);
|
|
if (! ($routesResult['success'] ?? false) || empty($routesResult['data']['routes'])) {
|
|
return null;
|
|
}
|
|
|
|
return [$templateIdentifier, $routesResult['data']['routes']];
|
|
}
|
|
|
|
/**
|
|
* 라우트 경로의 동적 표현식을 해석합니다.
|
|
*
|
|
* @param string $routePath 라우트 경로
|
|
* @return string 해석된 경로
|
|
*/
|
|
private function resolveRoutePath(string $routePath): string
|
|
{
|
|
// 와일드카드 '*' 제거 (prefix 표시용)
|
|
$routePath = ltrim($routePath, '*/');
|
|
if (! str_starts_with($routePath, '/')) {
|
|
$routePath = '/'.$routePath;
|
|
}
|
|
|
|
// 템플릿 표현식 해석: {{_global.modules?.['sirsoft-ecommerce']?.basic_info?.route_path ?? 'shop'}}
|
|
$routePath = (string) preg_replace_callback('/\{\{(.+?)\}\}/', function ($matches) {
|
|
$expr = trim($matches[1]);
|
|
|
|
return $this->resolveRouteExpression($expr);
|
|
}, $routePath);
|
|
|
|
// 이중 슬래시 정규화 (빈 표현식 결과 시 발생)
|
|
$routePath = preg_replace('#/+#', '/', $routePath);
|
|
|
|
return $routePath;
|
|
}
|
|
|
|
/**
|
|
* 라우트 경로 내 표현식을 해석합니다.
|
|
*
|
|
* @param string $expr 표현식
|
|
* @return string 해석된 값
|
|
*/
|
|
private function resolveRouteExpression(string $expr): string
|
|
{
|
|
// 삼항 연산자 패턴: condition ? '' : (modules?.['id']?.key ?? 'default')
|
|
// no_route 플래그가 true면 빈 문자열, 아니면 route_path 또는 기본값 반환
|
|
if (preg_match("/\.no_route\s*\?/", $expr)) {
|
|
// 삼항 else 절(: 이후)에서 모듈 설정 추출
|
|
$elseClause = preg_match("/:\s*(.+)$/", $expr, $elseMatch) ? $elseMatch[1] : '';
|
|
if ($elseClause !== '' && preg_match("/modules\?\.\['([^']+)'\]\?\.([\\w?.]+)\s*\?\?\s*'(.+?)'/", $elseClause, $matches)) {
|
|
$moduleId = $matches[1];
|
|
// optional chaining 제거: basic_info?.route_path → basic_info.route_path
|
|
$settingKey = str_replace('?.', '.', $matches[2]);
|
|
$default = $matches[3];
|
|
|
|
// no_route 설정 확인
|
|
$noRoute = g7_module_settings($moduleId, 'basic_info.no_route');
|
|
if ($noRoute) {
|
|
return '';
|
|
}
|
|
|
|
return g7_module_settings($moduleId, $settingKey) ?? $default;
|
|
}
|
|
}
|
|
|
|
// _global.modules?.['module-id']?.key ?? 'default' 패턴 처리 (optional chaining 포함)
|
|
if (preg_match("/modules\?\.\['([^']+)'\]\?\.([\\w?.]+)\s*\?\?\s*'(.+?)'/", $expr, $matches)) {
|
|
$moduleId = $matches[1];
|
|
$settingKey = str_replace('?.', '.', $matches[2]);
|
|
$default = $matches[3];
|
|
|
|
return g7_module_settings($moduleId, $settingKey) ?? $default;
|
|
}
|
|
|
|
// 단순 null coalescing
|
|
if (str_contains($expr, '??')) {
|
|
$parts = array_map('trim', explode('??', $expr, 2));
|
|
|
|
return trim($parts[1], " '\"()");
|
|
}
|
|
|
|
return $expr;
|
|
}
|
|
|
|
/**
|
|
* URL이 라우트 패턴에 매칭되는지 확인합니다.
|
|
*
|
|
* @param string $routePath 라우트 패턴 (예: /shop/products/:id)
|
|
* @param string $url 실제 URL (예: /shop/products/123)
|
|
* @return array|null 매칭된 파라미터 또는 null
|
|
*/
|
|
private function matchRoute(string $routePath, string $url): ?array
|
|
{
|
|
// 정규식으로 변환
|
|
$pattern = preg_replace('/:(\w+)/', '(?P<$1>[^/]+)', $routePath);
|
|
$pattern = '#^'.$pattern.'$#';
|
|
|
|
if (preg_match($pattern, $url, $matches)) {
|
|
$params = [];
|
|
foreach ($matches as $key => $value) {
|
|
if (is_string($key)) {
|
|
$params[$key] = $value;
|
|
}
|
|
}
|
|
|
|
return $params;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|