Skip to content
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
44 changes: 43 additions & 1 deletion docs/1-essentials/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ Note that priority is defined using an integer. You can however use one of the b

### Middleware discovery

Global middleware classes are discovered and sorted based on their priority. You can make a middleware class non-global by adding the `#[SkipDiscovery]` attribute:
Global middleware classes are discovered and sorted based on their priority. You can make a middleware class non-global by adding the {b`#[Tempest\Discovery\SkipDiscovery]`} attribute:

```php
use Tempest\Discovery\SkipDiscovery;
Expand All @@ -384,6 +384,48 @@ final readonly class ValidateWebhook implements HttpMiddleware
{ /* … */ }
```

### Excluding route middleware

Some routes may not require specific global middleware to be applied. For instance, API routes do not need CSRF protection. You may skip specific middleware by using the `without` argument of the route attribute.

```php app/Slack/ReceiveInteractionController.php
use Tempest\Router\Post;
use Tempest\Http\Response;

final readonly class ReceiveInteractionController
{
#[Post('/slack/interaction', without: [VerifyCsrfMiddleware::class, SetCookieMiddleware::class])]
public function __invoke(): Response
{
// …
}
}
```

### Group middleware

While Tempest does not provide a way to group middleware, you can easily create your own route attribute that applies or excludes a set of middleware to a route.

```php Api.php
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
final readonly class Api implements Route
{
public function __construct(
public Method $method,
public string $uri,
public array $middleware = [],
public array $without = [],
) {
$this->uri = "/api/{$uri}";
$this->without[] = [
...$without,
VerifyCsrfMiddleware::class,
SetCookieMiddleware::class
];
}
}
```

## Responses

All requests to a controller action expect a response to be returned to the client. This is done by returning a `{php}View` or a `{php}Response` object.
Expand Down
4 changes: 3 additions & 1 deletion packages/router/src/Connect.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
public Method $method;

/**
* @param class-string<HttpMiddleware>[] $middleware
* @param class-string<HttpMiddleware>[] $middleware Middleware specific to this route.
* @param class-string<HttpMiddleware>[] $without Middleware to remove from this route.
*/
public function __construct(
public string $uri,
public array $middleware = [],
public array $without = [],
) {
$this->method = Method::CONNECT;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/router/src/Delete.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
public Method $method;

/**
* @param class-string<HttpMiddleware>[] $middleware
* @param class-string<HttpMiddleware>[] $middleware Middleware specific to this route.
* @param class-string<HttpMiddleware>[] $without Middleware to remove from this route.
*/
public function __construct(
public string $uri,
public array $middleware = [],
public array $without = [],
) {
$this->method = Method::DELETE;
}
Expand Down
9 changes: 9 additions & 0 deletions packages/router/src/GenericRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ private function getCallable(): HttpMiddlewareCallable

foreach ($middlewareStack->unwrap() as $middlewareClass) {
$callable = new HttpMiddlewareCallable(function (Request $request) use ($middlewareClass, $callable) {
// We skip this middleware if it's ignored by the route
if ($this->container->has(MatchedRoute::class)) {
$matchedRoute = $this->container->get(MatchedRoute::class);

if (in_array($middlewareClass->getName(), $matchedRoute->route->without, strict: true)) {
return $callable($request);
}
}

/** @var HttpMiddleware $middleware */
$middleware = $this->container->get($middlewareClass->getName());

Expand Down
4 changes: 3 additions & 1 deletion packages/router/src/Get.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
public Method $method;

/**
* @param class-string<HttpMiddleware>[] $middleware
* @param class-string<HttpMiddleware>[] $middleware Middleware specific to this route.
* @param class-string<HttpMiddleware>[] $without Middleware to remove from this route.
*/
public function __construct(
public string $uri,
public array $middleware = [],
public array $without = [],
) {
$this->method = Method::GET;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/router/src/Head.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
public Method $method;

/**
* @param class-string<HttpMiddleware>[] $middleware
* @param class-string<HttpMiddleware>[] $middleware Middleware specific to this route.
* @param class-string<HttpMiddleware>[] $without Middleware to remove from this route.
*/
public function __construct(
public string $uri,
public array $middleware = [],
public array $without = [],
) {
$this->method = Method::HEAD;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/router/src/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
public Method $method;

/**
* @param class-string<HttpMiddleware>[] $middleware
* @param class-string<HttpMiddleware>[] $middleware Middleware specific to this route.
* @param class-string<HttpMiddleware>[] $without Middleware to remove from this route.
*/
public function __construct(
public string $uri,
public array $middleware = [],
public array $without = [],
) {
$this->method = Method::OPTIONS;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/router/src/Patch.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
public Method $method;

/**
* @param class-string<HttpMiddleware>[] $middleware
* @param class-string<HttpMiddleware>[] $middleware Middleware specific to this route.
* @param class-string<HttpMiddleware>[] $without Middleware to remove from this route.
*/
public function __construct(
public string $uri,
public array $middleware = [],
public array $without = [],
) {
$this->method = Method::PATCH;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/router/src/Post.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
public Method $method;

/**
* @param class-string<HttpMiddleware>[] $middleware
* @param class-string<HttpMiddleware>[] $middleware Middleware specific to this route.
* @param class-string<HttpMiddleware>[] $without Middleware to remove from this route.
*/
public function __construct(
public string $uri,
public array $middleware = [],
public array $without = [],
) {
$this->method = Method::POST;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/router/src/Put.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
public Method $method;

/**
* @param class-string<HttpMiddleware>[] $middleware
* @param class-string<HttpMiddleware>[] $middleware Middleware specific to this route.
* @param class-string<HttpMiddleware>[] $without Middleware to remove from this route.
*/
public function __construct(
public string $uri,
public array $middleware = [],
public array $without = [],
) {
$this->method = Method::PUT;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/router/src/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ interface Route
public array $middleware {
get;
}

/** @var class-string<HttpMiddleware>[] */
public array $without {
get;
}
}
2 changes: 2 additions & 0 deletions packages/router/src/Routing/Construction/DiscoveredRoute.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static function fromRoute(Route $route, MethodReflector $methodReflector)
self::getRouteParams($route->uri),
$route->middleware,
$methodReflector,
$route->without,
);
}

Expand All @@ -36,6 +37,7 @@ private function __construct(
/** @var class-string<\Tempest\Router\HttpMiddleware>[] */
public array $middleware,
public MethodReflector $handler,
public array $without = [],
) {
$this->isDynamic = $parameters !== [];
}
Expand Down
4 changes: 3 additions & 1 deletion packages/router/src/Trace.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
public Method $method;

/**
* @param class-string<HttpMiddleware>[] $middleware
* @param class-string<HttpMiddleware>[] $middleware Middleware specific to this route.
* @param class-string<HttpMiddleware>[] $without Middleware to remove from this route.
*/
public function __construct(
public string $uri,
public array $middleware = [],
public array $without = [],
) {
$this->method = Method::TRACE;
}
Expand Down
1 change: 1 addition & 0 deletions packages/router/tests/FakeRouteBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function __construct(
public string $uri = '/',
/** @var class-string<HttpMiddleware>[] */
public array $middleware = [],
public array $without = [],
) {
$this->handler = new MethodReflector(new ReflectionMethod($this, 'handler'));
}
Expand Down
11 changes: 11 additions & 0 deletions src/Tempest/Framework/Testing/Http/TestResponseHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ public function assertHasHeader(string $name): self
return $this;
}

public function assertDoesNotHaveHeader(string $name): self
{
Assert::assertArrayNotHasKey(
$name,
$this->response->headers,
sprintf('Failed to assert that response does not contain header [%s].', $name),
);

return $this;
}

/**
* Asserts that the given header contains the given value.
*/
Expand Down
6 changes: 6 additions & 0 deletions tests/Fixtures/Controllers/TestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ public function withMiddleware(): Response
return new Ok();
}

#[Get(uri: '/without-middleware', without: [TestMiddleware::class])]
public function withoutMiddleware(): Response
{
return new Ok();
}

#[Get('/view-model-with-response-data')]
public function viewWithResponseData(): Response
{
Expand Down
12 changes: 12 additions & 0 deletions tests/Integration/Route/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Tests\Tempest\Fixtures\Controllers\EnumForController;
use Tests\Tempest\Fixtures\Controllers\TestController;
use Tests\Tempest\Fixtures\Controllers\TestGlobalMiddleware;
use Tests\Tempest\Fixtures\Controllers\TestMiddleware;
use Tests\Tempest\Fixtures\Controllers\UriGeneratorController;
use Tests\Tempest\Fixtures\Migrations\CreateAuthorTable;
use Tests\Tempest\Fixtures\Migrations\CreateBookTable;
Expand Down Expand Up @@ -161,6 +162,17 @@ public function test_middleware(): void
$this->assertEquals(['yes'], $response->getHeader('global-middleware')->values);
}

public function test_skip_middleware(): void
{
$this
->container->get(RouteConfig::class)
->middleware->add(TestMiddleware::class);

$this->http
->get('/without-middleware')
->assertDoesNotHaveHeader('middleware');
}

public function test_trailing_slash(): void
{
$this->http
Expand Down
Loading