diff --git a/composer.json b/composer.json index ee1ab84e..64fc9759 100644 --- a/composer.json +++ b/composer.json @@ -58,6 +58,7 @@ "dotkernel/dot-errorhandler": "^4.0.0", "dotkernel/dot-mail": "^5.3.0", "dotkernel/dot-response-header": "^3.4.1", + "dotkernel/dot-router": "^1.0.4", "laminas/laminas-component-installer": "^3.5.0", "laminas/laminas-config-aggregator": "^1.18.0", "laminas/laminas-hydrator": "^4.16.0", diff --git a/config/config.php b/config/config.php index ec61fbd1..b0d7731a 100644 --- a/config/config.php +++ b/config/config.php @@ -46,6 +46,7 @@ class_exists(Mezzio\Tooling\ConfigProvider::class) Dot\Mail\ConfigProvider::class, Dot\DataFixtures\ConfigProvider::class, Dot\Cache\ConfigProvider::class, + Dot\Router\ConfigProvider::class, // Default App module config Core\Admin\ConfigProvider::class, diff --git a/documentation/command/route-list.md b/documentation/command/route-list.md index 5529a49b..efae2dc9 100644 --- a/documentation/command/route-list.md +++ b/documentation/command/route-list.md @@ -11,45 +11,49 @@ php ./bin/cli.php route:list The command runs through all routes and extracts endpoint information in realtime. The output should be similar to the following: -| Request method | Route name | Route path | -|-----------------|---------------------------------|--------------------------------| -| DELETE | admin.delete | /admin/{uuid} | -| DELETE | user.my-account.delete | /user/my-account | -| DELETE | user.my-avatar.delete | /user/my-avatar | -| DELETE | user.delete | /user/{uuid} | -| DELETE | user.avatar.delete | /user/{uuid}/avatar | -| GET | home | / | -| GET | account.reset-password.validate | /account/reset-password/{hash} | -| GET | admin.list | /admin | -| GET | admin.my-account.view | /admin/my-account | -| GET | admin.role.list | /admin/role | -| GET | admin.role.view | /admin/role/{uuid} | -| GET | admin.view | /admin/{uuid} | -| GET | user.list | /user | -| GET | user.my-account.view | /user/my-account | -| GET | user.my-avatar.view | /user/my-avatar | -| GET | user.role.list | /user/role | -| GET | user.role.view | /user/role/{uuid} | -| GET | user.view | /user/{uuid} | -| GET | user.avatar.view | /user/{uuid}/avatar | -| PATCH | account.activate | /account/activate/{hash} | -| PATCH | account.modify-password | /account/reset-password/{hash} | -| PATCH | admin.my-account.update | /admin/my-account | -| PATCH | admin.update | /admin/{uuid} | -| PATCH | user.my-account.update | /user/my-account | -| PATCH | user.update | /user/{uuid} | -| POST | account.activate.request | /account/activate | -| POST | account.recover-identity | /account/recover-identity | -| POST | account.register | /account/register | -| POST | account.reset-password.request | /account/reset-password | -| POST | admin.create | /admin | -| POST | error.report | /error-report | -| POST | security.generate-token | /security/generate-token | -| POST | security.refresh-token | /security/refresh-token | -| POST | user.create | /user | -| POST | user.my-avatar.create | /user/my-avatar | -| POST | user.activate | /user/{uuid}/activate | -| POST | user.avatar.create | /user/{uuid}/avatar | +```text ++------+----------------+-------------------- 37 Routes ------+-------------------------------------+ +| # | Request method | Route name | Route path | ++------+----------------+-------------------------------------+-------------------------------------+ +| 1 | GET | app::view-index | / | +| 2 | GET | admin::list-admin | /admin | +| 3 | POST | admin::create-admin | /admin | +| 4 | GET | admin::view-account | /admin/account | +| 5 | PATCH | admin::update-account | /admin/account | +| 6 | GET | admin::list-role | /admin/role | +| 7 | GET | admin::view-role | /admin/role/{uuid} | +| 8 | DELETE | admin::delete-admin | /admin/{uuid} | +| 9 | GET | admin::view-admin | /admin/{uuid} | +| 10 | PATCH | admin::update-admin | /admin/{uuid} | +| 11 | POST | app::create-error-report | /error-report | +| 12 | POST | security::token | /security/token | +| 13 | GET | user::list-user | /user | +| 14 | POST | user::create-user | /user | +| 15 | DELETE | user::delete-account | /user/account | +| 16 | GET | user::view-account | /user/account | +| 17 | PATCH | user::update-account | /user/account | +| 18 | POST | user::create-account | /user/account | +| 19 | POST | user::request-activate-account | /user/account/activate | +| 20 | PATCH | user::activate-account | /user/account/activate/{hash} | +| 21 | DELETE | user::delete-account-avatar | /user/account/avatar | +| 22 | GET | user::view-account-avatar | /user/account/avatar | +| 23 | POST | user::create-account-avatar | /user/account/avatar | +| 24 | POST | user::recover-account | /user/account/recover | +| 25 | POST | user::create-account-reset-password | /user/account/reset-password | +| 26 | GET | user::check-account-reset-password | /user/account/reset-password/{hash} | +| 27 | PATCH | user::update-account-reset-password | /user/account/reset-password/{hash} | +| 28 | GET | user::list-role | /user/role | +| 29 | GET | user::view-role | /user/role/{uuid} | +| 30 | DELETE | user::delete-user | /user/{uuid} | +| 31 | GET | user::view-user | /user/{uuid} | +| 32 | PATCH | user::update-user | /user/{uuid} | +| 33 | PATCH | user::activate-user | /user/{uuid}/activate | +| 34 | DELETE | user::delete-user-avatar | /user/{uuid}/avatar | +| 35 | GET | user::view-user-avatar | /user/{uuid}/avatar | +| 36 | POST | user::create-user-avatar | /user/{uuid}/avatar | +| 37 | PATCH | user::deactivate-user | /user/{uuid}/deactivate | ++------+----------------+-------------------------------------+-------------------------------------+ +``` ## Filtering results diff --git a/src/Admin/src/RoutesDelegator.php b/src/Admin/src/RoutesDelegator.php index 2c315423..8e8103fd 100644 --- a/src/Admin/src/RoutesDelegator.php +++ b/src/Admin/src/RoutesDelegator.php @@ -13,33 +13,42 @@ use Api\Admin\Handler\Admin\PostAdminResourceHandler; use Api\Admin\Handler\Admin\Role\GetAdminRoleCollectionHandler; use Api\Admin\Handler\Admin\Role\GetAdminRoleResourceHandler; +use Dot\Router\RouteCollectorInterface; use Mezzio\Application; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; - -use function assert; +use Psr\Container\NotFoundExceptionInterface; class RoutesDelegator { + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { - $app = $callback(); - assert($app instanceof Application); - $uuid = \Api\App\RoutesDelegator::REGEXP_UUID; - $app->get('/admin', GetAdminCollectionHandler::class, 'admin::list-admin'); - $app->post('/admin', PostAdminResourceHandler::class, 'admin::create-admin'); + /** @var RouteCollectorInterface $routeCollector */ + $routeCollector = $container->get(RouteCollectorInterface::class); + + $routeCollector->group('/admin') + ->get('', GetAdminCollectionHandler::class, 'admin::list-admin') + ->post('', PostAdminResourceHandler::class, 'admin::create-admin'); - $app->delete('/admin/' . $uuid, DeleteAdminResourceHandler::class, 'admin::delete-admin'); - $app->get('/admin/' . $uuid, GetAdminResourceHandler::class, 'admin::view-admin'); - $app->patch('/admin/' . $uuid, PatchAdminResourceHandler::class, 'admin::update-admin'); + $routeCollector->group('/admin/' . $uuid) + ->delete('', DeleteAdminResourceHandler::class, 'admin::delete-admin') + ->get('', GetAdminResourceHandler::class, 'admin::view-admin') + ->patch('', PatchAdminResourceHandler::class, 'admin::update-admin'); - $app->get('/admin/role', GetAdminRoleCollectionHandler::class, 'admin::list-role'); - $app->get('/admin/role/' . $uuid, GetAdminRoleResourceHandler::class, 'admin::view-role'); + $routeCollector->group('/admin/role') + ->get('', GetAdminRoleCollectionHandler::class, 'admin::list-role') + ->get('/' . $uuid, GetAdminRoleResourceHandler::class, 'admin::view-role'); - $app->get('/admin/account', GetAdminAccountResourceHandler::class, 'admin::view-account'); - $app->patch('/admin/account', PatchAdminAccountResourceHandler::class, 'admin::update-account'); + $routeCollector->group('/admin/account') + ->get('', GetAdminAccountResourceHandler::class, 'admin::view-account') + ->patch('', PatchAdminAccountResourceHandler::class, 'admin::update-account'); - return $app; + return $callback(); } } diff --git a/src/App/src/Command/RouteListCommand.php b/src/App/src/Command/RouteListCommand.php index c034986e..f61e7f74 100644 --- a/src/App/src/Command/RouteListCommand.php +++ b/src/App/src/Command/RouteListCommand.php @@ -5,6 +5,7 @@ namespace Api\App\Command; use Api\App\RoutesDelegator; +use Fig\Http\Message\RequestMethodInterface; use Mezzio\Application; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -56,7 +57,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int $routes = []; foreach ($this->application->getRoutes() as $route) { - foreach ($route->getAllowedMethods() as $method) { + $methods = $route->getAllowedMethods(); + if (empty($methods)) { + $methods = [ + RequestMethodInterface::METHOD_DELETE, + RequestMethodInterface::METHOD_GET, + RequestMethodInterface::METHOD_PATCH, + RequestMethodInterface::METHOD_POST, + RequestMethodInterface::METHOD_PUT, + ]; + } + + foreach ($methods as $method) { if (! str_contains($route->getName(), $nameFilter)) { continue; } diff --git a/src/App/src/ConfigProvider.php b/src/App/src/ConfigProvider.php index 79bf2cd9..8850fc46 100644 --- a/src/App/src/ConfigProvider.php +++ b/src/App/src/ConfigProvider.php @@ -10,6 +10,7 @@ use Api\App\Factory\HandlerDelegatorFactory; use Api\App\Factory\RouteListCommandFactory; use Api\App\Factory\TokenGenerateCommandFactory; +use Api\App\Handler\GetIndexResourceHandler; use Api\App\Handler\PostErrorReportResourceHandler; use Api\App\Middleware\AuthenticationMiddleware; use Api\App\Middleware\AuthorizationMiddleware; @@ -52,6 +53,7 @@ public function getDependencies(): array return [ 'delegators' => [ Application::class => [RoutesDelegator::class], + GetIndexResourceHandler::class => [HandlerDelegatorFactory::class], PostErrorReportResourceHandler::class => [HandlerDelegatorFactory::class], ], 'factories' => [ @@ -63,6 +65,7 @@ public function getDependencies(): array DeprecationMiddleware::class => AttributedServiceFactory::class, Environment::class => TwigEnvironmentFactory::class, ErrorReportPermissionMiddleware::class => AttributedServiceFactory::class, + GetIndexResourceHandler::class => AttributedServiceFactory::class, PostErrorReportResourceHandler::class => AttributedServiceFactory::class, ErrorReportService::class => AttributedServiceFactory::class, ResponseMiddleware::class => AttributedServiceFactory::class, diff --git a/src/App/src/Middleware/DeprecationMiddleware.php b/src/App/src/Middleware/DeprecationMiddleware.php index f7939edb..ac41a6a8 100644 --- a/src/App/src/Middleware/DeprecationMiddleware.php +++ b/src/App/src/Middleware/DeprecationMiddleware.php @@ -9,8 +9,9 @@ use Api\App\Exception\DeprecationConflictException; use Core\App\Message; use Dot\DependencyInjection\Attribute\Inject; +use Dot\Router\Middleware\LazyLoadingMiddleware; use Laminas\Stratigility\MiddlewarePipe; -use Mezzio\Middleware\LazyLoadingMiddleware; +use Mezzio\Middleware\LazyLoadingMiddleware as MezzioLazyLoadingMiddleware; use Mezzio\Router\RouteResult; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -141,7 +142,10 @@ private function getReflectionAttributes(ReflectionClass $reflectionObject): arr private function getHandler(MiddlewareInterface $routeMiddleware): ?ReflectionClass { $reflectionHandler = null; - if ($routeMiddleware instanceof LazyLoadingMiddleware) { + if ( + $routeMiddleware instanceof MezzioLazyLoadingMiddleware + || $routeMiddleware instanceof LazyLoadingMiddleware + ) { /** @var class-string $routeMiddlewareName */ $routeMiddlewareName = $routeMiddleware->middlewareName; $reflectionMiddlewareClass = new ReflectionClass($routeMiddlewareName); diff --git a/src/App/src/RoutesDelegator.php b/src/App/src/RoutesDelegator.php index d4ab78a3..d08f7af7 100644 --- a/src/App/src/RoutesDelegator.php +++ b/src/App/src/RoutesDelegator.php @@ -7,30 +7,33 @@ use Api\App\Handler\GetIndexResourceHandler; use Api\App\Handler\PostErrorReportResourceHandler; use Api\App\Middleware\ErrorReportPermissionMiddleware; +use Dot\Router\RouteCollectorInterface; use Mezzio\Application; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; - -use function assert; +use Psr\Container\NotFoundExceptionInterface; class RoutesDelegator { public const REGEXP_UUID = '{uuid:[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}}'; + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { - $app = $callback(); - assert($app instanceof Application); - - // Home page - $app->get('/', GetIndexResourceHandler::class, 'app::view-index'); - - // Other application reports an error - $app->post( - '/error-report', - [ErrorReportPermissionMiddleware::class, PostErrorReportResourceHandler::class], - 'app::create-error-report' - ); - - return $app; + /** @var RouteCollectorInterface $routeCollector */ + $routeCollector = $container->get(RouteCollectorInterface::class); + + $routeCollector + ->get('/', GetIndexResourceHandler::class, 'app::view-index') + ->post( + '/error-report', + [ErrorReportPermissionMiddleware::class, PostErrorReportResourceHandler::class], + 'app::create-error-report' + ); + + return $callback(); } } diff --git a/src/Security/src/RoutesDelegator.php b/src/Security/src/RoutesDelegator.php index dbbc2823..fb7e6c08 100644 --- a/src/Security/src/RoutesDelegator.php +++ b/src/Security/src/RoutesDelegator.php @@ -5,25 +5,30 @@ namespace Api\Security; use Api\Security\Middleware\ErrorResponseMiddleware; +use Dot\Router\RouteCollectorInterface; use Mezzio\Application; use Mezzio\Authentication\OAuth2\TokenEndpointHandler; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; - -use function assert; +use Psr\Container\NotFoundExceptionInterface; class RoutesDelegator { + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { - $app = $callback(); - assert($app instanceof Application); + /** @var RouteCollectorInterface $routeCollector */ + $routeCollector = $container->get(RouteCollectorInterface::class); - $app->post( + $routeCollector->post( '/security/token', [ErrorResponseMiddleware::class, TokenEndpointHandler::class], 'security::token' ); - return $app; + return $callback(); } } diff --git a/src/User/src/RoutesDelegator.php b/src/User/src/RoutesDelegator.php index fa700c6c..60a876c8 100644 --- a/src/User/src/RoutesDelegator.php +++ b/src/User/src/RoutesDelegator.php @@ -29,73 +29,69 @@ use Api\User\Handler\User\PostUserResourceHandler; use Api\User\Handler\User\Role\GetUserRoleCollectionHandler; use Api\User\Handler\User\Role\GetUserRoleResourceHandler; +use Dot\Router\RouteCollectorInterface; use Mezzio\Application; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; - -use function assert; +use Psr\Container\NotFoundExceptionInterface; class RoutesDelegator { + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application { - $app = $callback(); - assert($app instanceof Application); - $uuid = \Api\App\RoutesDelegator::REGEXP_UUID; - // Accounts having higher than user permissions manage user accounts - - $app->get('/user', GetUserCollectionHandler::class, 'user::list-user'); - $app->post('/user', PostUserResourceHandler::class, 'user::create-user'); - - $app->delete('/user/' . $uuid, DeleteUserResourceHandler::class, 'user::delete-user'); - $app->get('/user/' . $uuid, GetUserResourceHandler::class, 'user::view-user'); - $app->patch('/user/' . $uuid, PatchUserResourceHandler::class, 'user::update-user'); + /** @var RouteCollectorInterface $routeCollector */ + $routeCollector = $container->get(RouteCollectorInterface::class); - $app->delete('/user/' . $uuid . '/avatar', DeleteUserAvatarResourceHandler::class, 'user::delete-user-avatar'); - $app->get('/user/' . $uuid . '/avatar', GetUserAvatarResourceHandler::class, 'user::view-user-avatar'); - $app->post('/user/' . $uuid . '/avatar', PostUserAvatarResourceHandler::class, 'user::create-user-avatar'); + $routeCollector->group('/user') + ->get('', GetUserCollectionHandler::class, 'user::list-user') + ->post('', PostUserResourceHandler::class, 'user::create-user'); - $app->get('/user/role', GetUserRoleCollectionHandler::class, 'user::list-role'); - $app->get('/user/role/' . $uuid, GetUserRoleResourceHandler::class, 'user::view-role'); + $routeCollector + ->patch('/user/' . $uuid . '/activate', PatchUserActivateHandler::class, 'user::activate-user') + ->patch('/user/' . $uuid . '/deactivate', PatchUserDeactivateHandler::class, 'user::deactivate-user'); - $app->patch('/user/' . $uuid . '/activate', PatchUserActivateHandler::class, 'user::activate-user'); - $app->patch('/user/' . $uuid . '/deactivate', PatchUserDeactivateHandler::class, 'user::deactivate-user'); + $routeCollector->group('/user/' . $uuid) + ->delete('', DeleteUserResourceHandler::class, 'user::delete-user') + ->get('', GetUserResourceHandler::class, 'user::view-user') + ->patch('', PatchUserResourceHandler::class, 'user::update-user'); - // Users manage their user accounts + $routeCollector->group('/user/' . $uuid . '/avatar') + ->delete('', DeleteUserAvatarResourceHandler::class, 'user::delete-user-avatar') + ->get('', GetUserAvatarResourceHandler::class, 'user::view-user-avatar') + ->post('', PostUserAvatarResourceHandler::class, 'user::create-user-avatar'); - $app->delete('/user/account', DeleteUserAccountResourceHandler::class, 'user::delete-account'); - $app->get('/user/account', GetUserAccountResourceHandler::class, 'user::view-account'); - $app->patch('/user/account', PatchUserAccountResourceHandler::class, 'user::update-account'); - $app->post('/user/account', PostUserAccountResourceHandler::class, 'user::create-account'); + $routeCollector->group('/user/role') + ->get('', GetUserRoleCollectionHandler::class, 'user::list-role') + ->get('/' . $uuid, GetUserRoleResourceHandler::class, 'user::view-role'); - $app->delete('/user/account/avatar', DeleteUserAccountAvatarHandler::class, 'user::delete-account-avatar'); - $app->get('/user/account/avatar', GetUserAccountAvatarHandler::class, 'user::view-account-avatar'); - $app->post('/user/account/avatar', PostUserAccountAvatarHandler::class, 'user::create-account-avatar'); + $routeCollector->group('/user/account') + ->delete('', DeleteUserAccountResourceHandler::class, 'user::delete-account') + ->get('', GetUserAccountResourceHandler::class, 'user::view-account') + ->patch('', PatchUserAccountResourceHandler::class, 'user::update-account') + ->post('', PostUserAccountResourceHandler::class, 'user::create-account'); - // Unauthenticated users manage their user accounts + $routeCollector->group('/user/account/activate') + ->patch('/{hash}', PatchUserAccountActivateHandler::class, 'user::activate-account') + ->post('', PostUserAccountActivateHandler::class, 'user::request-activate-account'); - $app->patch('/user/account/activate/{hash}', PatchUserAccountActivateHandler::class, 'user::activate-account'); - $app->post('/user/account/activate', PostUserAccountActivateHandler::class, 'user::request-activate-account'); + $routeCollector->post('/user/account/recover', PostUserAccountRecoverHandler::class, 'user::recover-account'); - $app->post('/user/account/recover', PostUserAccountRecoverHandler::class, 'user::recover-account'); + $routeCollector->group('/user/account/avatar') + ->delete('', DeleteUserAccountAvatarHandler::class, 'user::delete-account-avatar') + ->get('', GetUserAccountAvatarHandler::class, 'user::view-account-avatar') + ->post('', PostUserAccountAvatarHandler::class, 'user::create-account-avatar'); - $app->get( - '/user/account/reset-password/{hash}', - GetUserAccountResetPasswordHandler::class, - 'user::check-account-reset-password' - ); - $app->patch( - '/user/account/reset-password/{hash}', - PatchUserAccountResetPasswordHandler::class, - 'user::update-account-reset-password' - ); - $app->post( - '/user/account/reset-password', - PostUserAccountResetPasswordHandler::class, - 'user::create-account-reset-password' - ); + $routeCollector->group('/user/account/reset-password') + ->get('/{hash}', GetUserAccountResetPasswordHandler::class, 'user::check-account-reset-password') + ->patch('/{hash}', PatchUserAccountResetPasswordHandler::class, 'user::update-account-reset-password') + ->post('', PostUserAccountResetPasswordHandler::class, 'user::create-account-reset-password'); - return $app; + return $callback(); } }