Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance router #4

Merged
merged 5 commits into from
Dec 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"require": {
"php": "^8.1",
"psr/http-message": "^2.0",
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"yiisoft/middleware-dispatcher": "^5.2"
},
Expand All @@ -46,7 +47,6 @@
"roave/infection-static-analysis-plugin": "^1.34",
"spatie/phpunit-watcher": "^1.23",
"vimeo/psalm": "^5.16",
"yiisoft/strings": "^2.4",
"yiisoft/test-support": "^3.0"
},
"autoload": {
Expand Down
45 changes: 27 additions & 18 deletions src/FileRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher;
use Yiisoft\Strings\StringHelper;

final class FileRouter implements MiddlewareInterface
{
Expand Down Expand Up @@ -48,16 +47,33 @@ 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);
}
$action = $this->parseAction($request);
/** @psalm-suppress InvalidPropertyFetch */
$actions = $controllerClass::$actions ?? [
'HEAD' => 'head',
'OPTIONS' => 'options',
'GET' => 'index',
'POST' => 'create',
'PUT' => 'update',
'DELETE' => 'delete',
];
$action = $actions[$request->getMethod()] ?? null;

if ($action === null) {
return $handler->handle($request);
}

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

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

Expand All @@ -66,18 +82,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
return $middlewareDispatcher->dispatch($request, $handler);
}

private function parseAction(ServerRequestInterface $request): ?string
{
return match ($request->getMethod()) {
'HEAD', 'GET' => 'index',
'POST' => 'create',
'PUT' => 'update',
'DELETE' => 'delete',
default => throw new \Exception('Not implemented.'),
};
}

private function parseController(ServerRequestInterface $request): mixed
private function parseController(ServerRequestInterface $request): ?string
{
$path = $request->getUri()->getPath();
if ($path === '/') {
Expand All @@ -89,15 +94,19 @@ private function parseController(ServerRequestInterface $request): mixed
fn(array $matches) => strtoupper($matches[1]),
$path,
);
$directoryPath = StringHelper::directoryName($controllerName);

$controllerName = StringHelper::basename($controllerName);
if (!preg_match('#^(.*?)/([^/]+)/?$#', $controllerName, $matches)) {
return null;
}
$directoryPath = $matches[1];
$controllerName = $matches[2];
}

$controller = $controllerName . $this->classPostfix;

$className = str_replace(
['/', '\\\\'],
['\\', '\\'],
['\\/\\', '\\/', '\\\\'],
'\\',
$this->namespace . '\\' . $this->baseControllerDirectory . '\\' . $directoryPath . '\\' . $controller
);

Expand Down
72 changes: 72 additions & 0 deletions tests/FileRouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ public function testMiddleware(): void
$this->assertEquals('x-header-value', $response->getHeaderLine('X-Header-Name'));
}

public function testTrailingSlash(): void
{
/**
* @var FileRouter $router
*/
$router = $this->createRouter();
$router = $router
->withNamespace('Yiisoft\FileRouter\Tests\Support\App1');

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

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

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

#[DataProvider('dataRouter')]
public function testRouter(string $method, string $uri, string $expectedResponse): void
{
Expand Down Expand Up @@ -89,6 +110,23 @@ public static function dataRouter(): iterable
}

public function testUnsupportedMethod(): void
{
$router = $this->createRouter();
$router = $router
->withNamespace('Yiisoft\FileRouter\Tests\Support\App1');

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

$this->expectException(\Exception::class);
$this->expectExceptionMessage('Not implemented from tests.');
$router->process($request, $handler);
}

public function testNotImplementedAction(): void
{
$router = $this->createRouter();
$router = $router
Expand All @@ -105,6 +143,40 @@ public function testUnsupportedMethod(): void
$router->process($request, $handler);
}

public function testUnknownController(): void
{
$router = $this->createRouter();
$router = $router
->withNamespace('Yiisoft\FileRouter\Tests\Support\App1');

$handler = $this->createExceptionHandler();
$request = new ServerRequest(
method: 'DELETE',
uri: '/test/123',
);

$this->expectException(\Exception::class);
$this->expectExceptionMessage('Not implemented from tests.');
$router->process($request, $handler);
}

public function testIncorrectUrl(): void
{
$router = $this->createRouter();
$router = $router
->withNamespace('Yiisoft\FileRouter\Tests\Support\App1');

$handler = $this->createExceptionHandler();
$request = new ServerRequest(
method: 'DELETE',
uri: '/test//123///',
);

$this->expectException(\Exception::class);
$this->expectExceptionMessage('Not implemented from tests.');
$router->process($request, $handler);
}

public function testBaseController(): void
{
$router = $this->createRouter();
Expand Down
4 changes: 4 additions & 0 deletions tests/Support/App1/Controller/IndexController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

class IndexController
{
public static array $actions = [
'GET' => 'index',
'DELETE' => 'delete',
];
public function index(): ResponseInterface
{
return new TextResponse('Hello, index!', 200, ['X-Header-Name' => 'X-Header-Value']);
Expand Down