Skip to content

Commit c901dfe

Browse files
authored
feat(router)!: add route decorators (#1695)
1 parent 16a6c9d commit c901dfe

35 files changed

+450
-75
lines changed

docs/1-essentials/01-routing.md

Lines changed: 111 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -440,26 +440,120 @@ final readonly class ReceiveInteractionController
440440
}
441441
```
442442

443-
### Group middleware
443+
## Route decorators (route groups)
444444

445-
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.
445+
Route decorators are Tempest's way to manage routes in bulk; it's a feature similar to route groups in other frameworks. Route decorators are attributes that implement the {b`\Tempest\Router\RouteDecorator`} interface. A route decorator's task is to make changes or add functionality to whether route it's associated with. Tempest comes with a few built-in route decorators, and you can make your own as well.
446446

447-
```php Api.php
448-
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
449-
final readonly class Api implements Route
447+
In most cases, you'll want to add route decorators to a controller class, so that they are applied to all actions of that class:
448+
449+
```php
450+
use Tempest\Router\Prefix;
451+
use Tempest\Router\Get;
452+
453+
#[Prefix('/api')]
454+
final readonly class ApiController
450455
{
451-
public function __construct(
452-
public Method $method,
453-
public string $uri,
454-
public array $middleware = [],
455-
public array $without = [],
456-
) {
457-
$this->uri = "/api/{$uri}";
458-
$this->without[] = [
459-
...$without,
460-
VerifyCsrfMiddleware::class,
461-
SetCookieMiddleware::class
462-
];
456+
#[Get('/books')]
457+
public function books(): Response { /* … */ }
458+
459+
#[Get('/authors')]
460+
public function authors(): Response { /* … */ }
461+
}
462+
```
463+
464+
However, route decorators may also be applied to individual controller actions:
465+
466+
```php
467+
use Tempest\Router\Stateless;
468+
use Tempest\Router\Get;
469+
470+
final readonly class BlogPostController
471+
{
472+
#[Stateless]
473+
#[Get('/rss')]
474+
public function rss(): Response { /* … */ }
475+
}
476+
```
477+
478+
### Built-in route decorators
479+
480+
These route decorators are provided by Tempest:
481+
482+
#### `#[Stateless]`
483+
484+
When you're building API endpoints, RSS feeds, or any other kind of page that does not require any cookie or session data, you may use the {b`#[Tempest\Router\Stateless]`} attribute, which will remove all state-related logic:
485+
486+
```php
487+
use Tempest\Router\Stateless;
488+
use Tempest\Router\Get;
489+
490+
final readonly class BlogPostController
491+
{
492+
#[Stateless]
493+
#[Get('/rss')]
494+
public function rss(): Response { /* … */ }
495+
}
496+
```
497+
498+
#### `#[Prefix]`
499+
500+
Adds a prefix to all associated routes.
501+
502+
```php
503+
use Tempest\Router\Prefix;
504+
use Tempest\Router\Get;
505+
506+
#[Prefix('/api')]
507+
final readonly class ApiController
508+
{
509+
#[Get('/books')]
510+
public function books(): Response { /* … */ }
511+
512+
#[Get('/authors')]
513+
public function authors(): Response { /* … */ }
514+
}
515+
```
516+
517+
#### `#[WithMiddleware]`
518+
519+
Adds middleware to all associated routes.
520+
521+
```php
522+
use Tempest\Router\WithMiddleware;
523+
use Tempest\Router\Get;
524+
525+
#[Middleware(AuthMiddleware::class, AdminMiddleware::class)]
526+
final readonly class AdminController { /* … */ }
527+
```
528+
529+
#### `#[WithoutMiddleware]`
530+
531+
Explicitly removes middleware to all associated routes.
532+
533+
```php
534+
use Tempest\Router\WithoutMiddleware;
535+
use Tempest\Router\Get;
536+
537+
#[WithoutMiddleware(VerifyCsrfMiddleware::class, SetCookieMiddleware::class)]
538+
final readonly class StatelessController { /* … */ }
539+
```
540+
541+
### Custom route decorators
542+
543+
Building your own route decorators is done by implementing the {b`\Tempest\Router\RouteDecorator`} interface and marking your decorator as an attribute.
544+
545+
```php
546+
use Attribute;
547+
use Tempest\Router\RouteDecorator;
548+
549+
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
550+
final readonly class Auth implements RouteDecorator
551+
{
552+
public function decorate(Route $route): Route
553+
{
554+
$route->middleare[] = AuthMiddleware::class;
555+
556+
return $route;
463557
}
464558
}
465559
```
@@ -631,23 +725,6 @@ final class ErrorResponseProcessor implements ResponseProcessor
631725
}
632726
```
633727

634-
## Stateless routes
635-
636-
When you're building API endpoints, RSS pages, or any other kind of page that does not require any cookie or session data, you may use the `{#[Tempest\Router\Stateless]}` attribute, which will remove all state-related logic:
637-
638-
```php
639-
use Tempest\Router\Stateless;
640-
use Tempest\Router\Get;
641-
642-
final readonly class JsonController
643-
{
644-
#[Stateless]
645-
#[Get('/json')]
646-
public function json(string $path): Response
647-
{ /* … */ }
648-
}
649-
```
650-
651728
## Custom route attributes
652729

653730
It is often a requirement to have a bunch of routes following the same specifications—for instance, using the same middleware, or the same URI prefix.

packages/router/src/Connect.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Tempest\Http\Method;
99

1010
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
11-
final readonly class Connect implements Route
11+
final class Connect implements Route
1212
{
1313
public Method $method;
1414

packages/router/src/Delete.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Tempest\Http\Method;
99

1010
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
11-
final readonly class Delete implements Route
11+
final class Delete implements Route
1212
{
1313
public Method $method;
1414

packages/router/src/GenericRouter.php

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use Tempest\Http\Request;
1212
use Tempest\Http\Response;
1313
use Tempest\Http\Responses\Ok;
14-
use Tempest\Http\Session\VerifyCsrfMiddleware;
1514
use Tempest\Router\Exceptions\ControllerActionHadNoReturn;
1615
use Tempest\Router\Exceptions\MatchedRouteCouldNotBeResolved;
1716
use Tempest\Router\Routing\Matching\RouteMatcher;
@@ -83,22 +82,6 @@ private function getCallable(): HttpMiddlewareCallable
8382
)) {
8483
return $callable($request);
8584
}
86-
87-
// Skip middleware that sets cookies or session values when the route is stateless
88-
if (
89-
$matchedRoute->route->handler->hasAttribute(Stateless::class)
90-
&& in_array(
91-
needle: $middlewareClass->getName(),
92-
haystack: [
93-
VerifyCsrfMiddleware::class,
94-
SetCurrentUrlMiddleware::class,
95-
SetCookieMiddleware::class,
96-
],
97-
strict: true,
98-
)
99-
) {
100-
return $callable($request);
101-
}
10285
}
10386

10487
/** @var HttpMiddleware $middleware */

packages/router/src/Get.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Tempest\Http\Method;
99

1010
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
11-
final readonly class Get implements Route
11+
final class Get implements Route
1212
{
1313
public Method $method;
1414

packages/router/src/Head.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Tempest\Http\Method;
99

1010
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
11-
final readonly class Head implements Route
11+
final class Head implements Route
1212
{
1313
public Method $method;
1414

packages/router/src/MatchedRoute.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
use Tempest\Router\Routing\Construction\DiscoveredRoute;
88

9-
final readonly class MatchedRoute
9+
final class MatchedRoute
1010
{
1111
public function __construct(
1212
public DiscoveredRoute $route,

packages/router/src/Options.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Tempest\Http\Method;
99

1010
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
11-
final readonly class Options implements Route
11+
final class Options implements Route
1212
{
1313
public Method $method;
1414

packages/router/src/Patch.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Tempest\Http\Method;
99

1010
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
11-
final readonly class Patch implements Route
11+
final class Patch implements Route
1212
{
1313
public Method $method;
1414

packages/router/src/Post.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Tempest\Http\Method;
99

1010
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
11-
final readonly class Post implements Route
11+
final class Post implements Route
1212
{
1313
public Method $method;
1414

0 commit comments

Comments
 (0)