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

Documentation + module prefix #10

Merged
merged 5 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ The package could be installed with composer:
composer require yiisoft/file-router
```

## General usage
## Documentation

- [English](docs/en/README.md)

## Testing

Expand Down
196 changes: 196 additions & 0 deletions docs/en/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# Documentation

> Note: The router can be used along with the [`yiisoft/router`](https://github.com/yiisoft/router) package.

> Note: Once the router found a matching route, it will interrupt the middleware queue and execute the controller
> action.

## General usage

1. Add `\Yiisoft\FileRouter\FileRouter` to the list of middlewares in your application configuration:

`web/params.php`

```php
return [
'middlewares' => [
// ...
\Yiisoft\FileRouter\FileRouter::class,
// or
[
'class' => FileRouter::class,
'withNamespace()' => ['MyApp\\Package1'],
'withDefaultControllerName()' => ['Default'],
],
// ...
]
];
```

2. [Configure the router](#configuration) for your needs.

## Configuration

`\Yiisoft\FileRouter\FileRouter` supports the following configuration options:

#### `withBaseControllerDirectory(string $directory): self`
xepozz marked this conversation as resolved.
Show resolved Hide resolved

Sets the directory where controllers are located.

By default, it is set to `Controller`.

#### `withClassPostfix(string $postfix): self`
xepozz marked this conversation as resolved.
Show resolved Hide resolved

Sets the postfix for controller class names.

By default, it is set to `Controller`.

#### `withNamespace(string $namespace): self`
xepozz marked this conversation as resolved.
Show resolved Hide resolved

Sets the namespace for controller classes.

By default, it is set to `App`.

#### `withDefaultControllerName(string $name): self`
xepozz marked this conversation as resolved.
Show resolved Hide resolved

Sets the default controller name.

By default, it is set to `Index`.

#### `withRoutePrefix(string $prefix): self`
xepozz marked this conversation as resolved.
Show resolved Hide resolved

Sets the route prefix.

By default, it is empty.

It could be useful if you want to add a prefix to all routes or to separate routes from different [modules](#modularity).

## Middlewares

`\Yiisoft\FileRouter\FileRouter` supports adding middlewares to the routes.

To add a middleware, add the static property `$middlewares` to the controller class:

```php
class UserController
{
public static array $middlewares = [
'index' => [
HeaderMiddleware::class,
],
];

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

Where `index` is the method name and the value is an array of middleware class names, or middleware definitions.

Look at all supported middleware definitions formats in
the [Middleware Dispatcher](https://github.com/yiisoft/middleware-dispatcher#general-usage) package.

## Matching

### HTTP methods matching

The router maps HTTP methods to controller action methods as follows:

| Method | Action |
|-----------|-------------|
| `HEAD` | `head()` |
| `OPTIONS` | `options()` |
| `GET` | `index()` |
| `POST` | `create()` |
| `PUT` | `update()` |
| `PATCH` | `patch()` |
| `DELETE` | `delete()` |

> Note: If the controller does not have a method that matches the HTTP method, the router **will not** throw an exception.

#### Custom routes
xepozz marked this conversation as resolved.
Show resolved Hide resolved

To add a custom route, add the static property `$actions` to the controller class:

```php
class UserController
{
public static array $actions = [
'GET' => 'main',
];

public function main(): ResponseInterface
{
return new TextResponse('Hello, user/main!', 200);
}
}
```

> Note: Once you override the actions map, the router will only look for the actions specified in the map.
> In the example above, the router will fail to find the `index` / `delete`, etc. actions


### Route collision

Let's imagine that you have a request `GET /user/index`.

It has two possible controller and action variations:

- `src/Controller/User/IndexController::index()`
- `User/IndexController` class has matched by the full route path (`user/index`)
- `index()` method has matched by the [HTTP methods matching](#http-methods-matching)
- `src/Controller/UserController::index()`
- `UserController` class has matched by the first part of the route path (`user`)
- `index()` method has matched by the second part of the route path (`index`)

For example, if you have a `UserController` and a `User/IndexController`, a `GET` request to `/user` will be handled
by the `UserController`, if it has an `index()` method.

Otherwise, the `User/IndexController` will be used along with the [HTTP methods matching](#http-methods-matching).

## Unicode routes

The router also supports Unicode in routes, controller names, and action names.

You can use Unicode characters in your URIs, controller class names, and action method names.

## Modularity

You can add more than one router to the application. It can help to build a modular application.

For example, you have two modules with the same controller name:

```text
- src
- Module1/Controller/
- UserController.php
- Module2/Controller/
- UserController.php
```

To add the router for each module, add the following code to the application configuration:

`web/params.php`

```php
return [
'middlewares' => [
// ...
[
'class' => FileRouter::class,
'withNamespace()' => ['App\\Module1\\'],
'withRoutePrefix()' => ['module1'],
],
[
'class' => FileRouter::class,
'withNamespace()' => ['App\\Module2\\'],
'withRoutePrefix()' => ['module2'],
],
// ...
]
];
```

As a usual middleware, each router will be executed one by one. The first router that matches the route will be used.
37 changes: 37 additions & 0 deletions src/FileRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@ final class FileRouter implements MiddlewareInterface
private string $classPostfix = 'Controller';
private string $namespace = 'App';
private string $defaultControllerName = 'Index';
private string $routePrefix = '';

public function __construct(
private readonly MiddlewareDispatcher $middlewareDispatcher,
) {
}

/**
* Sets the directory where controllers are located.
*
* @see withNamespace() if you want to set the namespace for controller classes.
*/
public function withBaseControllerDirectory(string $directory): self
{
$new = clone $this;
Expand All @@ -30,6 +36,9 @@ public function withBaseControllerDirectory(string $directory): self
return $new;
}

/**
* Sets the postfix for controller class names.
*/
public function withClassPostfix(string $postfix): self
{
$new = clone $this;
Expand All @@ -38,6 +47,11 @@ public function withClassPostfix(string $postfix): self
return $new;
}

/**
* Sets the namespace for controller classes.
*
* @see withBaseControllerDirectory() if you want to set the directory where controllers are located.
*/
public function withNamespace(string $namespace): self
{
$new = clone $this;
Expand All @@ -46,6 +60,9 @@ public function withNamespace(string $namespace): self
return $new;
}

/**
* Sets the default controller name.
*/
public function withDefaultControllerName(string $name): self
{
$new = clone $this;
Expand All @@ -54,6 +71,17 @@ public function withDefaultControllerName(string $name): self
return $new;
}

/**
* Sets the route prefix.
*/
public function withRoutePrefix(string $prefix): self
{
$new = clone $this;
$new->routePrefix = $prefix;

return $new;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$possibleEntrypoints = $this->parseRequestPath($request);
Expand All @@ -75,6 +103,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
'GET' => 'index',
'POST' => 'create',
'PUT' => 'update',
'PATCH' => 'patch',
'DELETE' => 'delete',
])[$request->getMethod()] ?? null;

Expand Down Expand Up @@ -102,6 +131,14 @@ private function parseRequestPath(ServerRequestInterface $request): iterable
{
$possibleAction = null;
$path = urldecode($request->getUri()->getPath());

if ($this->routePrefix !== '' && str_starts_with($path, $this->routePrefix)) {
$path = mb_substr($path, strlen($this->routePrefix));
if ($path === '') {
$path = '/';
}
}

if ($path === '/') {
yield [
$this->cleanClassname(
Expand Down
64 changes: 63 additions & 1 deletion tests/FileRouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Yiisoft\FileRouter\Tests\Support\App2;
use Yiisoft\FileRouter\Tests\Support\App3;
use Yiisoft\FileRouter\Tests\Support\App4;
use Yiisoft\FileRouter\Tests\Support\App5;
use Yiisoft\FileRouter\Tests\Support\HeaderMiddleware;
use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher;
use Yiisoft\Middleware\Dispatcher\MiddlewareFactory;
Expand Down Expand Up @@ -319,6 +320,63 @@ public static function dataUnicodeRoutes(): iterable
];
}

#[DataProvider('dataModularity')]
public function testModularity(
string $namespace,
string $routePrefix,
string $method,
string $uri,
string $expectedResult,
): void {
$router = $this->createRouter();
$router = $router
->withNamespace($namespace)
->withRoutePrefix($routePrefix);

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

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

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

public static function dataModularity(): iterable
{
yield 'module1 /' => [
'Yiisoft\\FileRouter\\Tests\\Support\\App5\\Module1',
'/module1',
'GET',
'/module1',
'Hello, module1!',
];
yield 'module1 /index' => [
'Yiisoft\\FileRouter\\Tests\\Support\\App5\\Module1',
'/module1',
'GET',
'/module1/',
'Hello, module1!',
];
yield 'module2 /index' => [
'Yiisoft\\FileRouter\\Tests\\Support\\App5\\Module2',
'/module2',
'GET',
'/module2/index',
'Hello, module2!',
];
yield 'm/o/d/u/l/e /index' => [
'Yiisoft\\FileRouter\\Tests\\Support\\App5\\Module2',
'/m/o/d/u/l/e',
'GET',
'/m/o/d/u/l/e/index',
'Hello, module2!',
];
}

private function createRouter(): FileRouter
{
$container = new SimpleContainer([
Expand All @@ -333,7 +391,11 @@ private function createRouter(): FileRouter
App3\Controller\UserController::class => new App3\Controller\UserController(),
App3\Controller\User\IndexController::class => new App3\Controller\User\IndexController(),

App4\Контроллеры\Пользователь\ГлавныйКонтроллер::class => new App4\Контроллеры\Пользователь\ГлавныйКонтроллер(),
App4\Контроллеры\Пользователь\ГлавныйКонтроллер::class => new App4\Контроллеры\Пользователь\ГлавныйКонтроллер(
),

App5\Module1\Controller\IndexController::class => new App5\Module1\Controller\IndexController(),
App5\Module2\Controller\IndexController::class => new App5\Module2\Controller\IndexController(),
]);

return new FileRouter(
Expand Down
Loading
Loading