Skip to content

Commit b9d9b59

Browse files
authored
Merge branch 'main' into feat/oauth-installer
2 parents 1f7b0ed + 14a8da8 commit b9d9b59

File tree

62 files changed

+1073
-120
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+1073
-120
lines changed

.github/workflows/create-gh-release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ jobs:
4444
RELEASE_TAG: ${{ github.ref_name }}
4545
run: |
4646
processed_body=$(cat release_notes.txt | perl -0777 -pe 's/\n*### (\S+) (.+?)\n+/\n### `\1` \2\n/g')
47+
processed_body="${processed_body#$'\n'}"
4748
final_content=$(printf "%s\n%s" "-# Read the [GitHub release](<${RELEASE_URL}>)." "$processed_body")
4849
jq -n \
4950
--arg content "$final_content" \

CHANGELOG.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,32 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
## [2.7.0](https://github.com/tempestphp/tempest-framework/compare/v2.6.3..2.7.0) — 2025-11-07
5+
## [2.8.0](https://github.com/tempestphp/tempest-framework/compare/v2.7.2..2.8.0) — 2025-11-10
6+
7+
### 🚨 Breaking changes
8+
9+
- **router**: [**breaking**] add route decorators (#1695) ([c901dfe](https://github.com/tempestphp/tempest-framework/commit/c901dfeec7c01394a4481a08ca8381988a0b03ad))
10+
11+
12+
## [2.7.2](https://github.com/tempestphp/tempest-framework/compare/v2.7.1..v2.7.2) — 2025-11-10
13+
14+
### 🐛 Bug fixes
15+
16+
- **console**: respect default value in confirm when forced (#1698) ([708c8f9](https://github.com/tempestphp/tempest-framework/commit/708c8f9ee5cef6c19d26e8ebcb069633341413ce))
17+
18+
19+
## [2.7.1](https://github.com/tempestphp/tempest-framework/compare/v2.7.0..v2.7.1) — 2025-11-09
20+
21+
### 🚀 Features
22+
23+
- **auth**: mark password property with `#[SensitiveParameter]` (#1693) ([129fdd5](https://github.com/tempestphp/tempest-framework/commit/129fdd54cd989203508b461bbbbddf85448aabb7))
24+
25+
### 🐛 Bug fixes
26+
27+
- **view**: discovery locations for view compiler (#1701) ([8604b86](https://github.com/tempestphp/tempest-framework/commit/8604b86e3118a79f8813df5a6f2315a802368a5c))
28+
29+
30+
## [2.7.0](https://github.com/tempestphp/tempest-framework/compare/v2.6.3..v2.7.0) — 2025-11-07
631

732
### 🚀 Features
833

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/console/src/Actions/RenderConsoleCommand.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,7 @@ private function renderArgument(ConsoleArgumentDefinition $argument): string
6363
return $formattedArgumentName->wrap('<style="fg-gray dim"><</style>', '<style="fg-gray dim">></style>')->toString();
6464
}
6565

66-
$defaultValue = match (true) {
67-
$argument->default === true => 'true',
68-
$argument->default === false => 'false',
69-
is_null($argument->default) => 'null',
70-
is_array($argument->default) => 'array',
71-
default => "{$argument->default}",
72-
};
66+
$defaultValue = $this->getArgumentDefaultValue($argument);
7367

7468
return str()
7569
->append(str('[')->wrap('<style="fg-gray dim">', '</style>'))
@@ -89,11 +83,24 @@ private function renderEnumArgument(ConsoleArgumentDefinition $argument): string
8983

9084
$partsAsString = ' {<style="fg-blue">' . implode('|', $parts) . '</style>}';
9185
$line = "<style=\"fg-blue\">{$argument->name}</style>";
86+
$defaultValue = $this->getArgumentDefaultValue($argument);
9287

9388
if ($argument->hasDefault) {
94-
return "[{$line}={$argument->default->value}{$partsAsString}]";
89+
return "[{$line}={$defaultValue}{$partsAsString}]";
9590
}
9691

9792
return "<{$line}{$partsAsString}>";
9893
}
94+
95+
private function getArgumentDefaultValue(ConsoleArgumentDefinition $argument): string
96+
{
97+
return match (true) {
98+
$argument->default === true => 'true',
99+
$argument->default === false => 'false',
100+
is_null($argument->default) => 'null',
101+
is_array($argument->default) => 'array',
102+
$argument->default instanceof BackedEnum => $argument->default->value,
103+
default => "{$argument->default}",
104+
};
105+
}
99106
}

packages/console/src/GenericConsole.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ public function ask(
262262
public function confirm(string $question, bool $default = false, ?string $yes = null, ?string $no = null): bool
263263
{
264264
if ($this->isForced) {
265-
return true;
265+
return $default;
266266
}
267267

268268
return $this->component(new ConfirmComponent($question, $default, $yes, $no));

packages/core/src/Kernel.php

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

99
interface Kernel
1010
{
11-
public const string VERSION = '2.7.0';
11+
public const string VERSION = '2.8.0';
1212

1313
public string $root {
1414
get;

packages/event-bus/src/Testing/EventBusTester.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function assertDispatched(string|object $event, ?Closure $callback = null
3838
{
3939
$this->assertFaked();
4040

41-
Assert::assertNotNull(
41+
Assert::assertNotEmpty(
4242
actual: $dispatches = $this->findDispatches($event),
4343
message: 'The event was not dispatched.',
4444
);

packages/mail/src/Testing/TestingMailer.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
use Tempest\Mail\EmailWasSent;
88
use Tempest\Mail\Mailer;
99

10+
use function Tempest\get;
11+
1012
final class TestingMailer implements Mailer
1113
{
12-
public function __construct(
13-
private readonly ?EventBus $eventBus = null,
14-
) {}
14+
private ?EventBus $eventBus {
15+
get => get(className: EventBus::class);
16+
}
1517

1618
/**
1719
* List of emails that would have been sent.

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

0 commit comments

Comments
 (0)