You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: src/Web/Blog/articles/2025-03-30-about-route-attributes.md
+17-39Lines changed: 17 additions & 39 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -57,6 +57,10 @@ The second reason this argument fails is that in real project, route files becom
57
57
58
58
## Route Grouping
59
59
60
+
:::info
61
+
Since writing this blog post, route grouping in Tempest has gotten a serious update. Read all about it [here](/blog/route-decorators)
62
+
:::
63
+
60
64
The second-biggest argument against route attributes is the "route grouping" argument. A single route configuration file like for example in Laravel, allows you to reuse route configuration by grouping them together:
Laravel's approach is really useful because you can configure several routes as a single group, so that you don't have to repeat middleware configuration, prefixes, etc. for _every individual route_. With route attributes, you cannot do that — or can you?
75
79
76
-
Tempest's route attributes are designed so that you can make your own. Even better: you're encouraged to do so! Let's say we have a route group specifically for admins: they need to be prefixed, and they need an extra middleware for checking authentication. Here's what that looks like in Tempest. First you make a `AdminRoute` class representing the route group:
80
+
Tempest has a concept called [route decorators](/2.x/essentials/routing#route-decorators-route-groups) which are a super convenient way to model route groups and share behavior. They look like this:
public function delete(Book $book): Response { /* … */ }
98
+
#[Delete('/books/{book}/delete')]
99
+
public function delete(): View { /* … */ }
120
100
}
121
101
```
122
102
123
-
Of course, you could make variations like `AdminGet`, `AdminPost`, and `AdminDelete` as well, whatever fits your case. You could even make a `BookRoute` specifically for books.
124
-
125
-
So, the second argument against route attributes also falls short: you can in fact create route groups. I'd even say I prefer modelling route groups as classes instead of relying on configuration. That might be preference, of course, but I definitely don't feel like one approach is better than the other; which means the second argument fails as well.
103
+
You can read more about its design in [this blog post](/blog/route-decorators).
Copy file name to clipboardExpand all lines: src/Web/Blog/articles/2025-11-10-route-decorators.md
+134-8Lines changed: 134 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,7 +5,7 @@ author: brent
5
5
tag: release
6
6
---
7
7
8
-
When I began working on Tempest, the very first things I added were a container and a router. I already had a clear vision on what I wanted routing to look like: to embrace attributes to keep routes and controller actions close together. Coming from Laravel, this is quite a different approach, and so I wrote about [my vision on the router's design](/blog/about-route-attributes) to make sure everyone understood.
8
+
When I began working on Tempest, the very first features were a container and a router. I already had a clear vision on what I wanted routing to look like: to embrace attributes to keep routes and controller actions close together. Coming from Laravel, this is quite a different approach, and so I wrote about [my vision on the router's design](/blog/about-route-attributes) to make sure everyone understood.
9
9
10
10
> If you decide that route attributes aren't your thing then, well, Tempest won't be your thing. That's ok. I do hope that I was able to present a couple of good arguments in favor of route attributes; and that they might have challenged your opinion if you were absolutely against them.
11
11
@@ -65,12 +65,12 @@ final class BookAdminController
65
65
}
66
66
```
67
67
68
-
While I really like attribute-based routing, grouping route bevaiour does feel… suboptimal with this approach. A couple of nitpicks:
68
+
While I really like attribute-based routing, grouping route behavior does feel… suboptimal because of attributes. A couple of nitpicks:
69
69
70
-
- Tempest's default route attributes are represented by HTTP verbs: `#[Get]`, `#[Post]`, etc. Making admin variants for each verb might be tedious, so in my previous example I decided to use one `#[AdminRoute]`, where the verb would be specified manually. There's nothing stopping me from adding `#[AdminGet]`, `#[AdminPost]`, etc; but it doesn't feel super clean either.
71
-
- When you prefer to namespace admin-specific route attributes like `#[Admin\Get]`, and `#[Admin\Post]`, you end up with naming collisions between normal-and admin versions. I've always found those types of ambiguities to increase cognitive load while coding.
72
-
- This approach doesn't really scale: say there are two types of route groups that require a specific middleware (`AuthMiddleware`, for example), then you end up making two or more route attributes, duplicating adding the`AuthMiddleware` to both.
73
-
- Say you want nested route groups: one for admin routes and then one for book routes (with a `/admin/books` prefix), you end up with yet another `#[AdminBookRoute]` attribute, not ideal.
70
+
- Tempest's default route attributes are represented by HTTP verbs: `#[Get]`, `#[Post]`, etc. Making admin variants for each verb might be tedious, so in my previous example I decided to use one `#[AdminRoute]`, where the verb would be specified manually. There's nothing stopping me from adding `#[AdminGet]`, `#[AdminPost]`, etc; but it doesn't feel super clean.
71
+
- When you prefer to namespace admin-specific route attributes like `#[Admin\Get]`, and `#[Admin\Post]`, you end up with naming collisions between normal-and admin versions. I've always found those types of ambiguities to increase cognitive load while coding.
72
+
- This approach doesn't really scale: say there are two types of route groups that require a specific middleware (`AuthMiddleware`, for example), then you end up making two or more route attributes, duplicating that logic of adding`AuthMiddleware` to both.
73
+
- Say you want nested route groups: one for admin routes and then one for book routes (with a `/admin/books` prefix), you end up with yet another variant called `#[AdminBookRoute]` attribute, not ideal.
74
74
75
75
So… what's the solution? I first looked at Symfony, which also uses attributes for routing:
76
76
@@ -95,10 +95,136 @@ class BookAdminController extends AbstractController
95
95
}
96
96
```
97
97
98
-
I think Symfony's approach gets us halfway there: it has the benefit of being able to define "shared route behavior" on the controller level, but not across controllers. You could create abstract controllers like `AdminController` and `AdminBookController`, but I find that a very cumbersome approach. It also doesn't scale when you want to combine multiple route behaviors horizontally because PHP doesn't have multi-inheritance. On top of that, I also like Tempest's design of using HTTP verbs to model route attributes like `#[Get]` and `#[Post]`, which is missing with Symfony. All of that to say, I like Symfony's approach, but I feel like there's even more room for improvement.
98
+
I think Symfony's approach gets us halfway there: it has the benefit of being able to define "shared route behavior" on the controller level, but not across controllers. You could create abstract controllers like `AdminController` and `AdminBookController`, which doesn't scale horizontally when you want to combine multiple route groups, because PHP doesn't have multi-inheritance. On top of that, I also like Tempest's design of using HTTP verbs to model route attributes like `#[Get]` and `#[Post]`, which is missing with Symfony. All of that to say, I like Symfony's approach, but I feel like there's room for improvement.
99
99
100
100
With the scene now being set, let's see the design we ended up with in Tempest.
101
101
102
102
## A Tempesty solution
103
103
104
-
A week ago, my production server suddenly died. After some debugging, I realized the problem had to do with my recent refactor from [my blog](https://stitcher.io) to Tempest. The RSS- and meta-image routes apparently started a session (which were stored as files), which eventually led to the server being overflooded with unused files after two weeks. I had forgotten to exclude these routes from the default web flow.
104
+
A week ago, my production server suddenly died. After some debugging, I realized the problem had to do with the recent refactor of [my blog](https://stitcher.io) to Tempest. The RSS and meta-image routes apparently started a session, which eventually led to the server being overflooded with hundreds of RSS reader- and social media requests per minute, each of them starting a new session. The solution was to remove all session-related middleware (CSRF protection, and "back URL" support) from these routes. While trying to come up with a proper solution, I had a realization: instead of making a "stateless route" class, why not add an attribute that worked _alongside_ the existing route attributes? That's what led to a new `#[Stateless]` attribute:
105
+
106
+
```php
107
+
#[Stateless, {:hl-type:Get:}('/rss')]
108
+
public function rss(): Response {}
109
+
```
110
+
111
+
This felt like a really nice solution: I didn't have to make my own route attributes anymore, but could instead "decorate" them with additional functionality. The first iteration of the `#[Stateless]` attribute was rather hard-coded in Tempest's router (I was on the clock, trying to revive my server), it looked something like this:
112
+
113
+
```php
114
+
// Skip middleware that sets cookies or session values when the route is stateless
I knew, however, that it would be trivial to make this into a reusable pattern. A couple of days later and that's exactly what I did: route decorators are Tempest's new way of modeling grouped route behavior, and I absolutely love them. Here's a quick overview.
132
+
133
+
First, route decorators work _alongside_ route attributes, not as a _replacement_. This means that they can be combined in any way you'd like, and they should all work together seeminglessly:
public function show(Book $book): View { /* … */ }
140
+
141
+
// …
142
+
}
143
+
```
144
+
145
+
Furthermore, route decorators can also be defined on the controller level, which means they'll be applied to all its actions:
146
+
147
+
```php
148
+
#[{:hl-type:Admin:}, {:hl-type:Books:}]
149
+
final class BookAdminController
150
+
{
151
+
#[Get('/books')]
152
+
public function index(): View { /* … */ }
153
+
154
+
#[Get('/books/{book}/show')]
155
+
public function show(Book $book): View { /* … */ }
156
+
157
+
#[Post('/books/new')]
158
+
public function new(): View { /* … */ }
159
+
160
+
#[Post('/books/{book}/update')]
161
+
public function update(): View { /* … */ }
162
+
163
+
#[Delete('/books/{book}/delete')]
164
+
public function delete(): View { /* … */ }
165
+
}
166
+
```
167
+
168
+
Finally, you're encouraged to make your custom route attributes as well (you might have already guessed that because of `#[Admin]` and `#[Books]`). Here's what both of these attributes would look like:
You can probably guess what a route decorator's job is: it is passed the current route, it can do some changes to it, and then return it. You can add and combine as many route decorators as you'd like, and Tempest's router will stitch them all together. Under the hood, that looks like this:
204
+
205
+
```php
206
+
// Get the route attribute
207
+
$route = $method->getAttributes(Route::class);
208
+
209
+
// Get all decorators from the method and its controller class
// Loop over each decorator and apply it one by one
216
+
foreach ($decorators as $decorator) {
217
+
$route = $decorator->decorate($route);
218
+
}
219
+
```
220
+
221
+
As an added benefit: all of this route decorating is done during [Tempest's discovery phase](/2.x/internals/discovery), which means the decorated route will be cached, and decorators themselves won't be run in production.
222
+
223
+
On top of adding the {b`Tempest\Router\RouteDecorator`} interface, I've also added a couple of built-in route decorators that come with the framework:
224
+
225
+
- {b`Tempest\Router\Prefix`}: which adds a prefix to all decorated routes.
226
+
- {b`Tempest\Router\WithMiddleware`}: which adds one or more middleware classes to all decorated routes.
227
+
- {b`Tempest\Router\WithoutMiddleware`}: which explicitely removes one or more middleware classes from the default middleware stack to all decorated routes.
228
+
- {b`Tempest\Router\Stateless`}: which will remove all session and cookie related middleware from the decorated routes.
229
+
230
+
I really like the solution we ended up with. I think it combines the best of both worlds. Maybe you have some thoughts about it as well? [Join the Tempest Discord](/discord) to let us know! You can also read all the details of route decorators [in the docs](/2.x/essentials/routing#route-decorators-route-groups).
0 commit comments