diff --git a/assets/components/minishop3/api.php b/assets/components/minishop3/api.php index 3ce8f978..567d531f 100644 --- a/assets/components/minishop3/api.php +++ b/assets/components/minishop3/api.php @@ -65,27 +65,9 @@ $ms3 = $modx->services->get('ms3'); $ms3->initialize('web'); - // Создаём роутер + // Создаём роутер — только Web API (фронтенд); manager connector не грузит эти пути (#384) $router = new \MiniShop3\Router\Router($modx); - - // Загружаем ТОЛЬКО Web API роуты (фронтенд, публичные) - $webRoutesFile = $componentPath . 'config/routes/web.php'; - - if (!file_exists($webRoutesFile)) { - throw new \Exception('Web routes not found: ' . $webRoutesFile); - } - - $router->loadRoutes($webRoutesFile); - - // Загружаем пользовательские роуты (опционально) - $customRoutesFile = MODX_CORE_PATH . 'config/ms3_routes_web.custom.php'; - if (file_exists($customRoutesFile)) { - $router->loadRoutes($customRoutesFile); - } - - $router->loadRoutesFromDirectory(\MiniShop3\Router\Router::coreAddonRoutesDirectory('web')); - - // Строим dispatcher + $router->loadWebRoutes($componentPath, MODX_CORE_PATH); $router->build(); // Обрабатываем запрос diff --git a/core/components/minishop3/config/ms3.routes.d/manager/example-addon.php.dist b/core/components/minishop3/config/ms3.routes.d/manager/example-addon.php.dist index 46711eb5..3b288794 100644 --- a/core/components/minishop3/config/ms3.routes.d/manager/example-addon.php.dist +++ b/core/components/minishop3/config/ms3.routes.d/manager/example-addon.php.dist @@ -4,10 +4,9 @@ * * Скопируйте в: core/config/ms3.routes.d/manager/50-myaddon.php * - * Порядок (Processors\Api\Router — встроенная админка MODX): системные manager.php → - * ms3_routes_manager.custom.php → ms3.routes.d/manager/*.php (web.php и ms3.routes.d/web/ не грузятся). - * - * Processors\Api\Index (кастомные фронты / Vue-менеджер): дополнительно web.php и ms3.routes.d/web/*.php. + * Порядок (Index и Router на connector): системные manager.php → + * ms3_routes_manager.custom.php → ms3.routes.d/manager/*.php. + * Storefront (web.php, ms3.routes.d/web/) — только через api.php (#384). * * @var \MiniShop3\Router\Router $router * @var \MODX\Revolution\modX $modx diff --git a/core/components/minishop3/config/ms3.routes.d/web/example-addon.php.dist b/core/components/minishop3/config/ms3.routes.d/web/example-addon.php.dist index 64b7ef3b..3c5ad967 100644 --- a/core/components/minishop3/config/ms3.routes.d/web/example-addon.php.dist +++ b/core/components/minishop3/config/ms3.routes.d/web/example-addon.php.dist @@ -8,11 +8,10 @@ * core/config/ms3.routes.d/web/50-myaddon.php * (имя с префиксом для порядка загрузки; файлы сортируются по алфавиту.) * - * ВАЖНО — ДВЕ ТОЧКИ ВХОДА: - * ======================== - * Файлы из ms3.routes.d/web/ подключаются и в публичном Web API (api.php), и в connector через - * Processors\Api\Index (кастомный фронт / Vue-менеджер). Они НЕ подключаются в Processors\Api\Router - * (встроенная админка MODX). Учитывайте это при проектировании путей и прав доступа. + * ВАЖНО — ТОЧКА ВХОДА: + * ==================== + * Файлы из ms3.routes.d/web/ подключаются только в публичном Web API (api.php). + * Connector Index/Router manager-роуты не грузят (#384). * * ПОРЯДОК ЗАГРУЗКИ (Web API, api.php): * ==================================== diff --git a/core/components/minishop3/config/routes/manager.php b/core/components/minishop3/config/routes/manager.php index 71aa8978..012b269c 100644 --- a/core/components/minishop3/config/routes/manager.php +++ b/core/components/minishop3/config/routes/manager.php @@ -9,8 +9,8 @@ * * DO NOT edit this file directly! * For custom routes use: core/config/ms3_routes_manager.custom.php - * For addon fragments: core/config/ms3.routes.d/manager/*.php (connector). Web fragments (ms3.routes.d/web/) - * load via api.php and Processors\Api\Index only, not via Processors\Api\Router (built-in manager). + * For addon fragments: core/config/ms3.routes.d/manager/*.php (connector Index/Router). + * Web fragments (ms3.routes.d/web/) load via api.php only — not via connector processors (#384). * * Custom and addon routes are loaded AFTER system routes and can override them. * diff --git a/core/components/minishop3/config/routes/web.php b/core/components/minishop3/config/routes/web.php index a3381528..cf2584a1 100644 --- a/core/components/minishop3/config/routes/web.php +++ b/core/components/minishop3/config/routes/web.php @@ -16,7 +16,7 @@ * DO NOT edit this file directly! * For custom routes use: core/config/ms3_routes_web.custom.php * For addon fragments (one file per addon): core/config/ms3.routes.d/web/*.php - * (same fragments load in api.php and in Processors\Api\Index on connector, not in Processors\Api\Router) + * Loaded only via api.php — not via connector Index/Router (#384). * * Custom and addon routes are loaded AFTER system routes and can override them. * diff --git a/core/components/minishop3/src/Processors/Api/Index.php b/core/components/minishop3/src/Processors/Api/Index.php index 799b8c1e..2afa96fa 100644 --- a/core/components/minishop3/src/Processors/Api/Index.php +++ b/core/components/minishop3/src/Processors/Api/Index.php @@ -2,109 +2,29 @@ namespace MiniShop3\Processors\Api; -use MiniShop3\Router\Router as ApiRouter; use MODX\Revolution\Processors\Processor; /** - * Processor for handling API requests through connector.php (кастомные фронты / Vue-менеджер). + * Processor for handling API requests through connector.php (Vue Manager / custom mgr fronts). * - * Полная цепочка: manager.php, web.php, custom-файлы, ms3.routes.d/manager и ms3.routes.d/web. - * Встроенная админка MODX использует Processors\Api\Router — без web-роутов и без web-фрагментов. + * Manager routes only: manager.php, ms3_routes_manager.custom.php, ms3.routes.d/manager/. + * Storefront /api/v1 lives on api.php — not loaded here (#384). * * Usage: - * connector.php?action=api&route=/api/mgr/test/success + * connector.php?action=MiniShop3\Processors\Api\Index&route=/api/mgr/... */ class Index extends Processor { + use ProcessesManagerConnectorRouteTrait; + /** @var string $permission Empty string = public access without permission check */ public $permission = ''; /** - * Check access permissions - * Router will check permissions via middleware, so allow everything here - * - * @return bool + * Router middleware enforces mgr Auth + permissions per route. */ public function checkPermissions() { return true; } - - /** - * {@inheritdoc} - */ - public function process() - { - try { - $route = $this->getProperty('route', ''); - - if (empty($route)) { - return $this->failure('Route parameter is required', ['code' => 400]); - } - - $componentPath = MODX_CORE_PATH . 'components/minishop3/'; - $autoloader = $componentPath . 'vendor/autoload.php'; - - if (!file_exists($autoloader)) { - return $this->failure('Component not properly installed. Run: composer install', ['code' => 500]); - } - - if (!class_exists('FastRoute\\simpleDispatcher')) { - require_once $autoloader; - } - - $router = new ApiRouter($this->modx); - - $systemRoutesFile = $componentPath . 'config/routes/manager.php'; - - if (!file_exists($systemRoutesFile)) { - return $this->failure('System routes not found: ' . $systemRoutesFile, ['code' => 500]); - } - - $router->loadRoutes($systemRoutesFile); - - $webRoutesFile = $componentPath . 'config/routes/web.php'; - - if (file_exists($webRoutesFile)) { - $router->loadRoutes($webRoutesFile); - } - - $customRoutesFile = MODX_CORE_PATH . 'config/ms3_routes_manager.custom.php'; - - if (file_exists($customRoutesFile)) { - $router->loadRoutes($customRoutesFile); - } - - $router->loadRoutesFromDirectory(ApiRouter::coreAddonRoutesDirectory('manager')); - $router->loadRoutesFromDirectory(ApiRouter::coreAddonRoutesDirectory('web')); - - $router->build(); - - $response = $router->dispatch($route, $_SERVER['REQUEST_METHOD']); - - $responseData = $response->getData(); - $statusCode = $response->getStatusCode(); - - http_response_code($statusCode); - - if ($statusCode >= 200 && $statusCode < 300) { - return $this->success('', $responseData['data'] ?? $responseData); - } - - return $this->failure( - $responseData['message'] ?? 'API request failed', - $responseData - ); - - } catch (\Exception $e) { - $this->modx->log(\MODX\Revolution\modX::LOG_LEVEL_ERROR, '[MiniShop3 API] ' . $e->getMessage()); - - return $this->failure( - $this->modx->getOption('ms3_api_debug', null, false) - ? $e->getMessage() - : 'Internal server error', - ['code' => 500] - ); - } - } } diff --git a/core/components/minishop3/src/Processors/Api/ProcessesManagerConnectorRouteTrait.php b/core/components/minishop3/src/Processors/Api/ProcessesManagerConnectorRouteTrait.php new file mode 100644 index 00000000..8b464da6 --- /dev/null +++ b/core/components/minishop3/src/Processors/Api/ProcessesManagerConnectorRouteTrait.php @@ -0,0 +1,88 @@ +getProperty('route', '')); + + if ($route === '') { + http_response_code(400); + + return $this->failure('Route parameter is required', ['code' => 400]); + } + + if (ApiRouter::isStorefrontRoute($route)) { + http_response_code(404); + + return $this->failure( + 'Storefront API is not available via manager connector. Use api.php.', + ['code' => 404] + ); + } + + $componentPath = MODX_CORE_PATH . 'components/minishop3/'; + $autoloader = $componentPath . 'vendor/autoload.php'; + + if (!file_exists($autoloader)) { + http_response_code(500); + + return $this->failure('Component not properly installed. Run: composer install', ['code' => 500]); + } + + $managerRoutes = $componentPath . 'config/routes/manager.php'; + if (!file_exists($managerRoutes)) { + http_response_code(500); + + return $this->failure('System routes not found: ' . $managerRoutes, ['code' => 500]); + } + + if (!class_exists('FastRoute\\simpleDispatcher')) { + require_once $autoloader; + } + + $router = new ApiRouter($this->modx); + $router->loadManagerRoutes($componentPath, MODX_CORE_PATH); + $router->build(); + + $response = $router->dispatch($route, $_SERVER['REQUEST_METHOD']); + + $responseData = $response->getData(); + $statusCode = $response->getStatusCode(); + + http_response_code($statusCode); + + if ($statusCode >= 200 && $statusCode < 300) { + return $this->success('', $responseData['data'] ?? $responseData); + } + + return $this->failure( + $responseData['message'] ?? 'API request failed', + $responseData + ); + } catch (\Exception $e) { + $this->modx->log(\MODX\Revolution\modX::LOG_LEVEL_ERROR, '[MiniShop3 API] ' . $e->getMessage()); + http_response_code(500); + + return $this->failure( + $this->modx->getOption('ms3_api_debug', null, false) + ? $e->getMessage() + : 'Internal server error', + ['code' => 500] + ); + } + } +} diff --git a/core/components/minishop3/src/Processors/Api/Router.php b/core/components/minishop3/src/Processors/Api/Router.php index 4f7b6b36..8d441059 100644 --- a/core/components/minishop3/src/Processors/Api/Router.php +++ b/core/components/minishop3/src/Processors/Api/Router.php @@ -2,88 +2,18 @@ namespace MiniShop3\Processors\Api; -use MiniShop3\Router\Router as ApiRouter; use MODX\Revolution\Processors\Processor; /** - * Processor for handling API requests through connector.php (встроенная админка MODX, сессия менеджера). + * Processor for handling API requests through connector.php (built-in MODX manager UI). * - * Загружаются только manager-роуты: системный manager.php, ms3_routes_manager.custom.php, ms3.routes.d/manager/. - * Web-роуты (web.php, ms3.routes.d/web/) сюда не подключаются — для них отдельные точки входа: api.php и Processors\Api\Index. + * Manager routes only: manager.php, ms3_routes_manager.custom.php, ms3.routes.d/manager/. + * Storefront /api/v1 lives on api.php (#384). * * Usage: - * connector.php?action=api&route=/api/mgr/test/success + * connector.php?action=MiniShop3\Processors\Api\Router&route=/api/mgr/... */ class Router extends Processor { - /** - * {@inheritdoc} - */ - public function process() - { - try { - $route = $this->getProperty('route', ''); - - if (empty($route)) { - return $this->failure('Route parameter is required', ['code' => 400]); - } - - $componentPath = MODX_CORE_PATH . 'components/minishop3/'; - $autoloader = $componentPath . 'vendor/autoload.php'; - - if (!file_exists($autoloader)) { - return $this->failure('Component not properly installed. Run: composer install', ['code' => 500]); - } - - if (!class_exists('FastRoute\\simpleDispatcher')) { - require_once $autoloader; - } - - $router = new ApiRouter($this->modx); - - $systemRoutesFile = $componentPath . 'config/routes/manager.php'; - - if (!file_exists($systemRoutesFile)) { - return $this->failure('System routes not found: ' . $systemRoutesFile, ['code' => 500]); - } - - $router->loadRoutes($systemRoutesFile); - - $customRoutesFile = MODX_CORE_PATH . 'config/ms3_routes_manager.custom.php'; - - if (file_exists($customRoutesFile)) { - $router->loadRoutes($customRoutesFile); - } - - $router->loadRoutesFromDirectory(ApiRouter::coreAddonRoutesDirectory('manager')); - - $router->build(); - - $response = $router->dispatch($route, $_SERVER['REQUEST_METHOD']); - - $responseData = $response->getData(); - $statusCode = $response->getStatusCode(); - - http_response_code($statusCode); - - if ($statusCode >= 200 && $statusCode < 300) { - return $this->success('', $responseData['data'] ?? $responseData); - } - - return $this->failure( - $responseData['message'] ?? 'API request failed', - $responseData - ); - - } catch (\Exception $e) { - $this->modx->log(\MODX\Revolution\modX::LOG_LEVEL_ERROR, '[MiniShop3 API] ' . $e->getMessage()); - - return $this->failure( - $this->modx->getOption('ms3_api_debug', null, false) - ? $e->getMessage() - : 'Internal server error', - ['code' => 500] - ); - } - } + use ProcessesManagerConnectorRouteTrait; } diff --git a/core/components/minishop3/src/Router/Router.php b/core/components/minishop3/src/Router/Router.php index 03812542..34cfcc1e 100644 --- a/core/components/minishop3/src/Router/Router.php +++ b/core/components/minishop3/src/Router/Router.php @@ -50,6 +50,126 @@ public static function coreAddonRoutesDirectory(string $segment): string return MODX_CORE_PATH . 'config/ms3.routes.d/' . $segment; } + /** + * Storefront API routes served by api.php — not via manager connector (#384). + */ + public static function isStorefrontRoute(string $route): bool + { + return str_starts_with($route, '/api/v1'); + } + + /** + * Planned manager-route sources (connector Index/Router). Never includes web (#384). + * + * @return list + */ + public static function managerRoutePlan(string $componentPath, string $corePath): array + { + $componentPath = rtrim($componentPath, '/\\') . DIRECTORY_SEPARATOR; + $corePath = rtrim($corePath, '/\\') . DIRECTORY_SEPARATOR; + + return [ + [ + 'kind' => 'file', + 'path' => $componentPath . 'config' . DIRECTORY_SEPARATOR . 'routes' . DIRECTORY_SEPARATOR . 'manager.php', + 'required' => true, + ], + [ + 'kind' => 'file', + 'path' => $corePath . 'config' . DIRECTORY_SEPARATOR . 'ms3_routes_manager.custom.php', + 'required' => false, + ], + [ + 'kind' => 'dir', + 'path' => $corePath . 'config' . DIRECTORY_SEPARATOR . 'ms3.routes.d' . DIRECTORY_SEPARATOR . 'manager', + 'required' => false, + ], + ]; + } + + /** + * Planned web-route sources (api.php). Never includes manager (#384). + * + * @return list + */ + public static function webRoutePlan(string $componentPath, string $corePath): array + { + $componentPath = rtrim($componentPath, '/\\') . DIRECTORY_SEPARATOR; + $corePath = rtrim($corePath, '/\\') . DIRECTORY_SEPARATOR; + + return [ + [ + 'kind' => 'file', + 'path' => $componentPath . 'config' . DIRECTORY_SEPARATOR . 'routes' . DIRECTORY_SEPARATOR . 'web.php', + 'required' => true, + ], + [ + 'kind' => 'file', + 'path' => $corePath . 'config' . DIRECTORY_SEPARATOR . 'ms3_routes_web.custom.php', + 'required' => false, + ], + [ + 'kind' => 'dir', + 'path' => $corePath . 'config' . DIRECTORY_SEPARATOR . 'ms3.routes.d' . DIRECTORY_SEPARATOR . 'web', + 'required' => false, + ], + ]; + } + + /** + * Load manager-only routes for connector.php processors. + * + * @throws \RuntimeException when required manager.php is missing + */ + public function loadManagerRoutes( + ?string $componentPath = null, + ?string $corePath = null + ): self { + $componentPath ??= MODX_CORE_PATH . 'components/minishop3'; + $corePath ??= MODX_CORE_PATH; + + return $this->loadRoutePlan(self::managerRoutePlan($componentPath, $corePath)); + } + + /** + * Load storefront routes for api.php. + * + * @throws \RuntimeException when required web.php is missing + */ + public function loadWebRoutes( + ?string $componentPath = null, + ?string $corePath = null + ): self { + $componentPath ??= MODX_CORE_PATH . 'components/minishop3'; + $corePath ??= MODX_CORE_PATH; + + return $this->loadRoutePlan(self::webRoutePlan($componentPath, $corePath)); + } + + /** + * @param list $plan + */ + protected function loadRoutePlan(array $plan): self + { + foreach ($plan as $item) { + if ($item['kind'] === 'dir') { + $this->loadRoutesFromDirectory($item['path']); + continue; + } + + if (!file_exists($item['path'])) { + if (!empty($item['required'])) { + throw new \RuntimeException('System routes not found: ' . $item['path']); + } + continue; + } + + $this->loadRoutes($item['path']); + } + + return $this; + } + /** * Load routes from configuration file * diff --git a/core/components/minishop3/tests/RouterRoutePlansTest.php b/core/components/minishop3/tests/RouterRoutePlansTest.php new file mode 100644 index 00000000..58bafbee --- /dev/null +++ b/core/components/minishop3/tests/RouterRoutePlansTest.php @@ -0,0 +1,126 @@ + $item) { + $path = $item['path']; + foreach (['web.php', 'ms3_routes_web', 'ms3.routes.d' . DIRECTORY_SEPARATOR . 'web'] as $forbidden) { + if (str_contains($path, $forbidden)) { + $fail("manager plan[{$i}] contains forbidden fragment {$forbidden}: {$path}"); + } + } +} + +$webPlan = Router::webRoutePlan($component, $core); +$assertSame(3, count($webPlan), 'web plan size'); +$assertSame( + $component . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'routes' . DIRECTORY_SEPARATOR . 'web.php', + $webPlan[0]['path'], + 'web.php path' +); +$assertSame( + $core . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'ms3.routes.d' . DIRECTORY_SEPARATOR . 'web', + $webPlan[2]['path'], + 'web addon dir' +); + +foreach ($webPlan as $i => $item) { + $path = $item['path']; + foreach (['manager.php', 'ms3_routes_manager', 'ms3.routes.d' . DIRECTORY_SEPARATOR . 'manager'] as $forbidden) { + if (str_contains($path, $forbidden)) { + $fail("web plan[{$i}] contains forbidden fragment {$forbidden}: {$path}"); + } + } +} + +// Plans must be disjoint (no shared path) +$managerPaths = array_column($managerPlan, 'path'); +$webPaths = array_column($webPlan, 'path'); +$overlap = array_intersect($managerPaths, $webPaths); +if ($overlap !== []) { + $fail('manager and web plans overlap: ' . implode(', ', $overlap)); +} + +// Connector trait must use loadManagerRoutes + isStorefrontRoute (wiring check) +$traitSrc = file_get_contents(__DIR__ . '/../src/Processors/Api/ProcessesManagerConnectorRouteTrait.php'); +if ($traitSrc === false) { + $fail('cannot read ProcessesManagerConnectorRouteTrait.php'); +} +if (!str_contains($traitSrc, 'loadManagerRoutes')) { + $fail('trait must call loadManagerRoutes()'); +} +if (!str_contains($traitSrc, 'isStorefrontRoute')) { + $fail('trait must reject storefront via isStorefrontRoute()'); +} +if (!str_contains($traitSrc, 'System routes not found:')) { + $fail('trait must return user-visible message when manager.php is missing'); +} +// Early rejects set HTTP status (storefront 404), not only JSON code +if (!str_contains($traitSrc, 'http_response_code(404)')) { + $fail('trait must call http_response_code(404) when rejecting storefront routes'); +} +if (str_contains($traitSrc, 'loadWebRoutes') || str_contains($traitSrc, 'ManagerConnectorRouteLoader')) { + $fail('trait must not load web routes or use deleted ManagerConnectorRouteLoader'); +} + +if (file_exists(__DIR__ . '/../src/Processors/Api/ManagerConnectorRouteLoader.php')) { + $fail('ManagerConnectorRouteLoader.php should be deleted'); +} + +$apiPhpPath = dirname(__DIR__, 4) . '/assets/components/minishop3/api.php'; +$apiPhp = file_get_contents($apiPhpPath); +if ($apiPhp === false || !str_contains($apiPhp, 'loadWebRoutes')) { + $fail('api.php must use loadWebRoutes() (' . $apiPhpPath . ')'); +} + +fwrite(STDOUT, "OK: RouterRoutePlansTest\n");