Skip to content

Commit

Permalink
Support custom routes (#6)
Browse files Browse the repository at this point in the history
Co-authored-by: Sergei Predvoditelev <sergei@predvoditelev.ru>
  • Loading branch information
xepozz and vjik committed Jan 6, 2024
1 parent 1246c75 commit 327eb09
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 59 deletions.
133 changes: 84 additions & 49 deletions src/FileRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,73 +47,108 @@ public function withNamespace(string $namespace): self

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
/**
* @psalm-var class-string|null $controllerClass
*/
$controllerClass = $this->parseController($request);
if ($controllerClass === null) {
return $handler->handle($request);
}
/** @psalm-suppress InvalidPropertyFetch */
$actions = $controllerClass::$actions ?? [
'HEAD' => 'head',
'OPTIONS' => 'options',
'GET' => 'index',
'POST' => 'create',
'PUT' => 'update',
'DELETE' => 'delete',
];
$action = $actions[$request->getMethod()] ?? null;
$possibleEntrypoints = $this->parseRequestPath($request);

foreach ($possibleEntrypoints as $possibleEntrypoint) {
/**
* @psalm-var class-string $controllerClass
* @psalm-var string|null $possibleAction
*/
[$controllerClass, $possibleAction] = $possibleEntrypoint;
if (!class_exists($controllerClass)) {
continue;
}

if ($action === null) {
return $handler->handle($request);
}
/** @psalm-suppress InvalidPropertyFetch */
$actions = $controllerClass::$actions ?? [
'HEAD' => 'head',
'OPTIONS' => 'options',
'GET' => 'index',
'POST' => 'create',
'PUT' => 'update',
'DELETE' => 'delete',
];
$action = $possibleAction ?? $actions[$request->getMethod()] ?? null;

if ($action === null) {
continue;
}

if (!method_exists($controllerClass, $action)) {
return $handler->handle($request);
}
if (!method_exists($controllerClass, $action)) {
continue;
}

/** @psalm-suppress InvalidPropertyFetch */
$middlewares = $controllerClass::$middlewares[$action] ?? [];
$middlewares[] = [$controllerClass, $action];
/** @psalm-suppress InvalidPropertyFetch */
$middlewares = $controllerClass::$middlewares[$action] ?? [];
$middlewares[] = [$controllerClass, $action];

$middlewareDispatcher = $this->middlewareDispatcher->withMiddlewares($middlewares);
$middlewareDispatcher = $this->middlewareDispatcher->withMiddlewares($middlewares);

return $middlewareDispatcher->dispatch($request, $handler);
return $middlewareDispatcher->dispatch($request, $handler);
}

return $handler->handle($request);
}

private function parseController(ServerRequestInterface $request): ?string
private function parseRequestPath(ServerRequestInterface $request): iterable
{
$possibleAction = null;
$path = $request->getUri()->getPath();
if ($path === '/') {
$controllerName = 'Index';
$directoryPath = '';
} else {
$controllerName = preg_replace_callback(
'#(/.)#',
fn(array $matches) => strtoupper($matches[1]),
$path,
);

if (!preg_match('#^(.*?)/([^/]+)/?$#', $controllerName, $matches)) {
return null;
}

yield [
$this->cleanClassname(
$this->namespace . '\\' . $this->baseControllerDirectory . '\\' . $controllerName . $this->classPostfix
),
$possibleAction,
];
return;
}

$controllerName = preg_replace_callback(
'#(/.)#',
static fn(array $matches) => strtoupper($matches[1]),
$path,
);

if (!preg_match('#^(.*?)/([^/]+)/?$#', $controllerName, $matches)) {
return;
}

$directoryPath = $matches[1];
$controllerName = $matches[2];

yield [
$this->cleanClassname(
$this->namespace . '\\' . $this->baseControllerDirectory . '\\' . $directoryPath . '\\' . $controllerName . $this->classPostfix
),
$possibleAction,
];

if (preg_match('#^(.*?)/([^/]+)/?$#', $directoryPath, $matches)) {
$possibleAction = strtolower($controllerName);
$directoryPath = $matches[1];
$controllerName = $matches[2];
} else {
$directoryPath = $controllerName;
$controllerName = 'Index';
}

$controller = $controllerName . $this->classPostfix;
yield [
$this->cleanClassname(
$this->namespace . '\\' . $this->baseControllerDirectory . '\\' . $directoryPath . '\\' . $controllerName . $this->classPostfix
),
$possibleAction,
];
}

$className = str_replace(
private function cleanClassname(string $className): string
{
return str_replace(
['\\/\\', '\\/', '\\\\'],
'\\',
$this->namespace . '\\' . $this->baseControllerDirectory . '\\' . $directoryPath . '\\' . $controller
$className,
);

if (class_exists($className)) {
return $className;
}

return null;
}
}
75 changes: 66 additions & 9 deletions tests/FileRouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Yiisoft\FileRouter\FileRouter;
use Yiisoft\FileRouter\Tests\Support\App1\Controller\IndexController;
use Yiisoft\FileRouter\Tests\Support\App1\Controller\User\BlogController;
use Yiisoft\FileRouter\Tests\Support\App1\Controller\UserController;
use Yiisoft\FileRouter\Tests\Support\App2\Action\UserAction;
use Yiisoft\FileRouter\Tests\Support\App1;
use Yiisoft\FileRouter\Tests\Support\App2;
use Yiisoft\FileRouter\Tests\Support\App3;
use Yiisoft\FileRouter\Tests\Support\HeaderMiddleware;
use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher;
use Yiisoft\Middleware\Dispatcher\MiddlewareFactory;
Expand Down Expand Up @@ -212,17 +211,75 @@ public function testUnusualControllerDirectory(): void
$response = $router->process($request, $handler);

$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('Hello, user action!', (string) $response->getBody());
$this->assertEquals('Hello, user/index action!', (string) $response->getBody());
}

public function testCustomRoute(): void
{
$router = $this->createRouter();
$router = $router
->withNamespace('Yiisoft\FileRouter\Tests\Support\App2')
->withBaseControllerDirectory('Action')
->withClassPostfix('Action');

$handler = $this->createExceptionHandler();
$request = new ServerRequest(
method: 'GET',
uri: '/user/hello',
);

$response = $router->process($request, $handler);

$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('Hello, user/hello action!', (string) $response->getBody());
}

#[DataProvider('dataRoutesCollision')]
public function testRoutesCollision(string $method, string $uri, string $expectedResponse): void
{
$router = $this->createRouter();
$router = $router->withNamespace('Yiisoft\FileRouter\Tests\Support\App3');

$handler = $this->createExceptionHandler();
$request = new ServerRequest(
method: $method,
uri: $uri,
);

$response = $router->process($request, $handler);

$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals($expectedResponse, (string) $response->getBody());
}

public static function dataRoutesCollision(): iterable
{
yield 'direct' => [
'GET',
'/user',
'Hello, Controller/UserController!',
];

yield 'indirect' => [
'POST',
'/user',
'Hello, Controller/User/IndexController!',
];
}

private function createRouter(): FileRouter
{
$container = new SimpleContainer([
HeaderMiddleware::class => new HeaderMiddleware(),
BlogController::class => new BlogController(),
UserController::class => new UserController(),
IndexController::class => new IndexController(),
UserAction::class => new UserAction(),

App1\Controller\User\BlogController::class => new App1\Controller\User\BlogController(),
App1\Controller\UserController::class => new App1\Controller\UserController(),
App1\Controller\IndexController::class => new App1\Controller\IndexController(),

App2\Action\UserAction::class => new App2\Action\UserAction(),

App3\Controller\UserController::class => new App3\Controller\UserController(),
App3\Controller\User\IndexController::class => new App3\Controller\User\IndexController(),
]);

return new FileRouter(
Expand Down
7 changes: 6 additions & 1 deletion tests/Support/App2/Action/UserAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ class UserAction

public function index(): ResponseInterface
{
return new TextResponse('Hello, user action!');
return new TextResponse('Hello, user/index action!');
}

public function hello(): ResponseInterface
{
return new TextResponse('Hello, user/hello action!');
}
}
21 changes: 21 additions & 0 deletions tests/Support/App3/Controller/User/IndexController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Yiisoft\FileRouter\Tests\Support\App3\Controller\User;

use HttpSoft\Response\TextResponse;
use Psr\Http\Message\ResponseInterface;

class IndexController
{
public function index(): ResponseInterface
{
return new TextResponse('Hello, Controller/User/IndexController!');
}

public function create(): ResponseInterface
{
return new TextResponse('Hello, Controller/User/IndexController!');
}
}
16 changes: 16 additions & 0 deletions tests/Support/App3/Controller/UserController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Yiisoft\FileRouter\Tests\Support\App3\Controller;

use HttpSoft\Response\TextResponse;
use Psr\Http\Message\ResponseInterface;

class UserController
{
public function index(): ResponseInterface
{
return new TextResponse('Hello, Controller/UserController!');
}
}

0 comments on commit 327eb09

Please sign in to comment.