Skip to content

Commit

Permalink
major refactoring of tests and code + remove some features + add bett…
Browse files Browse the repository at this point in the history
…er features
  • Loading branch information
IngeniozIT committed Feb 27, 2024
1 parent d9d7a5f commit 593c4bb
Show file tree
Hide file tree
Showing 20 changed files with 696 additions and 701 deletions.
241 changes: 25 additions & 216 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Router

A PHP router.
A PHP Router.

## Disclaimer

In order to ensure that this package is easy to integrate into your app, it is built around the **PHP Standard Recommendations** : it takes in a [PSR-7 Server Request](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) and returns a [PSR-7 Response](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface). It also uses a [PSR-11 Container](https://www.php-fig.org/psr/psr-11/) (such as [EDICT](https://github.com/IngeniozIT/psr-container-edict)) to resolve the route handlers.

It is inspired by routers from well-known frameworks *(did anyone say Laravel ?)* aswell as some home-made routers used internally by some major companies.

It is build with quality in mind : readability, immutability, no global states, 100% code coverage, 100% mutation testing score, and validation from various static analysis tools at the highest level.

## About

Expand All @@ -22,227 +30,28 @@ composer require ingenioz-it/router

## Documentation

### Configuring the Router

#### Overview

Here is a quick reference sample of how to configure the routes:

```php
$routes = new RouteGroup(
routes: [
Route::get(path: '/hello', callback: () => 'Hello, world!', name: 'hello'),
// Users
Route::get(path: '/users', callback: ListUsersHandler::class, name: 'users.list'),
Route::post(path: '/users/{id:[0-9]+}', callback: new CreateUserHandler(), name: 'user.create'),
// Admin
new RouteGroup(
routes: [
Route::get(path: '/admin', callback: PreviewAllPostsHandler::class, name: 'admin.index'),
Route::get(path: '/admin/logout', callback: AdminLogoutHandler::class, name: 'admin.logout'),
],
conditions: ['IsAdmin'],
),
// Web
Route::get(path: '{page}', callback: PageHandler::class, name: 'page'),
Route::get(path: '{page}', callback: PageNotFoundHandler::class, name: 'page.not_found'),
],
middlewares: [
ExceptionHandler::class,
RedirectionHandler::class,
],
patterns: ['page' => '.*'],
);

$router = new Router($routes, $container);

$response = $router->handle($request);
```

#### Path

The path can contain parameters, which are enclosed in curly braces:

```php
new RouteGroup([
Route::get(path: '/users/{id}', callback: /* handler */),
]);
```

By default, the parameters match any character except `/`.

To match a parameter with a different pattern, use a regular expression:

```php
new RouteGroup([
Route::get(path: '/users/{id:[0-9]+}', callback: /* handler */),
]);
```

Alternatively, you can define the pattern in the `patterns` parameter:

```php
new RouteGroup([
Route::get(path: '/users/{id}', callback: /* handler */, patterns: ['id' => '[0-9]+']),
]);
```

If you have a parameter that is used in multiple routes, you can define it inside the `RouteGroup`. It will be used in all the routes of the group:

```php
new RouteGroup(
routes: [
Route::get(path: '/users/{id}/posts/{postId}', callback: /* handler */),
Route::get(path: '/users/{id}/comments/{commentId}', callback: /* handler */),
],
patterns: ['id' => '[0-9]+'],
);
```

#### HTTP Method

The `Route` class provides static methods to create routes to match each HTTP method:

```php
new RouteGroup([
Route::get(/* ... */),
Route::post(/* ... */),
Route::put(/* ... */),
Route::patch(/* ... */),
Route::delete(/* ... */),
Route::head(/* ... */),
Route::options(/* ... */),
Route::any(/* ... */), // mathes all HTTP methods
Route::some(['GET', 'POST'], /* ... */), // matches only GET and POST
]);
```

#### Handlers

The handler can be a callable, a PSR-15 `RequestHandlerInterface`, a PSR-15 `MiddlewareInterface`, or a string.

```php
new RouteGroup([
Route::get(path: '/baz', callback: () => 'Hello, world!'),
Route::get(path: '/bar', callback: new Handler()),
Route::get(path: '/foo', callback: Handler::class),
]);
```

If the handler is a string, the container will be used to resolve it.
### Overview

If the handler is a middleware, calling the next handler will continue the routing:
Create your routes, instantiate the router and handle the request:

```php
new RouteGroup([
Route::get(path: '/', callback: ($request, $handler) => $handler->handle($request)), // Will delegate to the next route
Route::get(path: '/', callback: () => 'Hello, world!'),
]);
```


#### Name

You can name a route:

```php
new RouteGroup([
Route::get(path: '/', callback: /* handler */, name: 'home'),
Route::get(path: '/users', callback: /* handler */, name: 'users'),
]);
```

#### Middlewares

You can add middlewares to a route group:

```php
new RouteGroup(
route: [
Route::get(path: '/', callback: /* handler */),
],
middlewares: [
new MyMiddleware(),
MyMiddleware::class,
($request, $handler) => $handler->handle($request),
],
);
```

A middleware can be a PSR-15 `MiddlewareInterface`, a string, or a callable.

If the middleware is a string, the container will be used to resolve it.

If the middleware is a callable, it will be called with the request and the next handler as arguments.

#### Subgroups

You can nest route groups:

```php
new RouteGroup(
routes: [
Route::get(path: '/foo', callback: /* handler */),
new RouteGroup(
routes: [
Route::get(path: '/bar', callback: /* handler */),
Route::get(path: '/baz', callback: /* handler */),
],
),
],
);
```

#### Conditions

You can add conditions to a route group. The conditions are checked before the route group is parsed.

Conditions take the request as argument. They can either return `false` if the request does not match the conditions, or an array of parameters to inject into the request.

```php
new RouteGroup(
routes: [
new RouteGroup(
conditions: [
// The request must have the header 'X-Is-Admin'
fn ($request) => $request->hasHeader('X-Is-Admin') ? ['IsAdmin' => true] : false,
],
routes: [
Route::get(path: '/admin-stuff', callback: /* handler */),
],
),
Route::get(path: '/foo', callback: /* handler */),
],
);
```

If the request does not match the condition, the route group will be skipped.

If a condition is a string, the container will be used to resolve it.

### Using the Router

#### Creating the router

The `Router` uses a `RouteGroup` to store the routes and a PSR-11 `ContainerInterface` to inject dependencies into the route handlers.

```php
use IngeniozIT\Router\Router;
use IngeniozIT\Router\RouteGroup;
use IngeniozIT\Router\Route;
use IngeniozIT\Router\Router;

$container = /* PSR ContainerInterface */;
$routeGroup = new RouteGroup([/* routes */]);

$router = new Router($routeGroup, $container);
```

#### Routing a request

The `Router` uses a PSR-7 `ServerRequestInterface` to route the request.
// Create your routes
$routes = new RouteGroup([
Route::get('/hello', fn() => new Response('Hello, world!')),
Route::get('/bye', fn() => new Response('Goodbye, world!')),
]);

It returns a PSR-7 `ResponseInterface`.
// Instantiate the router
$container = new Container();
$router = new Router($routes, $container);

```php
$request = /* PSR ServerRequestInterface */;
// Handle the request
$request = new ServerRequest();
$response = $router->handle($request);
```

@todo continue working on the documentation (create a wiki ?)
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"quality:psalm": "vendor/bin/psalm --no-cache --config ./quality/psalm.xml.dist",
"quality:phan": "vendor/bin/phan --config-file ./quality/phan.php",
"quality:phan-silent": "vendor/bin/phan --no-progress-bar --config-file ./quality/phan.php",
"quality:infection": "vendor/bin/infection --configuration=./quality/infection.json.dist",
"quality:infection": "vendor/bin/infection -j$(nproc) --configuration=./quality/infection.json.dist",
"quality:phpmd": "vendor/bin/phpmd src/,tests/ text quality/phpmd.xml.dist",
"fulltest": [
"@test",
Expand Down
64 changes: 64 additions & 0 deletions index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

require 'vendor/autoload.php';

use IngeniozIT\Router\Route;
use IngeniozIT\Router\RouteGroup;

$routes = new RouteGroup(
path: '/api/distributor/v1',
middlewares: [
SetCorsHeadersMiddleware::class,
DistributorErrorMiddleware::class,
MaintenanceMiddleware::class,
ApiCheckConfigurationEnabledMiddleware::class,
],
routes: [
new RouteGroup(
path: '/oauth2',
middlewares: [
CheckApiKeyAuthenticationModeMiddleware::class,
DistributorScopesMiddleware::class,
],
routes: [
Route::post('/token', DistributorOAuth2Controller::class),
Route::get(
'/public-keys/latest',
PublicKeyController::class,
name: 'bar',
where: ['foo' => 'bar'],
with: ['foo' => 'bar'],
),
],
name: 'foo',
where: ['bar' => 'baz'],
with: ['bar' => 'baz'],
conditions: [
IsLoggedAsAdmin::class,
],
),
],
);


class AdminIndexController
{

}

class AdminUserController
{

}

class IsLoggedAsAdmin
{

}

class AdminMiddleware
{

}

print_r($routes);
4 changes: 3 additions & 1 deletion quality/phpmd.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
All default rulesets from PHPMD.
</description>
<rule ref="rulesets/codesize.xml" />
<rule ref="rulesets/cleancode.xml" />
<rule ref="rulesets/cleancode.xml">
<exclude name="StaticAccess" />
</rule>
<rule ref="rulesets/controversial.xml" />
<rule ref="rulesets/design.xml" />
<rule ref="rulesets/naming.xml" />
Expand Down
Loading

0 comments on commit 593c4bb

Please sign in to comment.