Skip to content

Commit

Permalink
Merge pull request #10 from yiisoft/docs
Browse files Browse the repository at this point in the history
Documentation + module prefix
  • Loading branch information
xepozz authored Jan 11, 2024
2 parents 11bb979 + 06da861 commit 1fa398a
Show file tree
Hide file tree
Showing 6 changed files with 331 additions and 2 deletions.
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`

Sets the directory where controllers are located.

By default, it is set to `Controller`.

### `withClassPostfix(string $postfix): self`

Sets the postfix for controller class names.

By default, it is set to `Controller`.

### `withNamespace(string $namespace): self`

Sets the namespace for controller classes.

By default, it is set to `App`.

### `withDefaultControllerName(string $name): self`

Sets the default controller name.

By default, it is set to `Index`.

### `withRoutePrefix(string $prefix): self`

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

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

0 comments on commit 1fa398a

Please sign in to comment.