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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
/packages/http/ @brendt @aidan-casey
/packages/http-client/ @aidan-casey
/packages/icon/ @innocenzi
/packages/idempotency/ @xHeaven
/packages/kv-store/ @innocenzi
/packages/log/ @brendt
/packages/mail/ @innocenzi
Expand Down
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"tempest/http": "self.version",
"tempest/http-client": "self.version",
"tempest/icon": "self.version",
"tempest/idempotency": "self.version",
"tempest/intl": "self.version",
"tempest/kv-store": "self.version",
"tempest/log": "self.version",
Expand Down Expand Up @@ -151,6 +152,7 @@
"Tempest\\HttpClient\\": "packages/http-client/src",
"Tempest\\Http\\": "packages/http/src",
"Tempest\\Icon\\": "packages/icon/src",
"Tempest\\Idempotency\\": "packages/idempotency/src",
"Tempest\\Intl\\": "packages/intl/src",
"Tempest\\KeyValue\\": "packages/kv-store/src",
"Tempest\\Log\\": "packages/log/src",
Expand Down Expand Up @@ -220,6 +222,7 @@
"Tempest\\HttpClient\\Tests\\": "packages/http-client/tests",
"Tempest\\Http\\Tests\\": "packages/http/tests",
"Tempest\\Icon\\Tests\\": "packages/icon/tests",
"Tempest\\Idempotency\\Tests\\": "packages/idempotency/tests",
"Tempest\\Intl\\Tests\\": "packages/intl/tests",
"Tempest\\KeyValue\\Tests\\": "packages/kv-store/tests",
"Tempest\\Log\\Tests\\": "packages/log/tests",
Expand Down
33 changes: 33 additions & 0 deletions docs/1-essentials/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,39 @@ final readonly class Auth implements RouteDecorator
}
```

## Idempotent routes

For operations like payment processing or order creation, retrying the same request should not produce duplicate side effects. Tempest provides the {b`Tempest\Idempotency\Attributes\Idempotent`} route decorator to handle this. Clients send an `Idempotency-Key` header; the first request executes normally and caches the response, while subsequent requests with the same key replay the cached response.

```php app/OrderController.php
use Tempest\Router\Post;
use Tempest\Http\Response;
use Tempest\Http\GenericResponse;
use Tempest\Http\Status;
use Tempest\Idempotency\Attributes\Idempotent;

final readonly class OrderController
{
#[Post('/orders')]
#[Idempotent]
public function create(CreateOrderRequest $request): Response
{
$order = $this->orderService->create($request);

return new GenericResponse(
status: Status::CREATED,
body: ['id' => $order->id],
);
}
}
```

Idempotency is only supported for `POST` and `PATCH` routes. The attribute can be applied at the class level to make all routes in a controller idempotent, and accepts optional TTL parameters. Route-specific settings like key requirement and header name can be configured with the `#[IdempotentRoute]` attribute.

:::info
Read the full [idempotency documentation](../2-features/19-idempotency.md) for details on scope resolvers, configuration, response behavior, and command bus idempotency.
:::

## Responses

All requests to a controller action expect a response to be returned to the client. This is done by returning a {b`Tempest\View\View`} or a {b`Tempest\Http\Response`} object.
Expand Down
64 changes: 64 additions & 0 deletions docs/2-features/10-command-bus.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,70 @@ In order to _run_ an asynchronous command, you'll have to run the `tempest comma

Note that async command handling is still an early feature, and will receive many improvements over time.

## Idempotent commands

Commands that should not be processed more than once—such as payment processing or invoice imports—can be marked with {b`Tempest\Idempotency\Attributes\Idempotent`}. The attribute can be placed on the command class or on the handler method. Duplicate dispatches with the same payload are silently skipped.

```php
// app/ImportInvoicesCommand.php

use Tempest\Idempotency\Attributes\Idempotent;

#[Idempotent]
final readonly class ImportInvoicesCommand
{
public function __construct(
public string $vendorId,
public string $month,
) {}
}
```

Alternatively, the attribute can be placed on the handler method instead:

```php
// app/ImportInvoicesHandler.php

use Tempest\CommandBus\CommandHandler;
use Tempest\Idempotency\Attributes\Idempotent;

final class ImportInvoicesHandler
{
#[Idempotent]
#[CommandHandler]
public function handle(ImportInvoicesCommand $command): void { /* … */ }
}
```

By default, the deduplication key is derived from the command's properties. Two commands with identical property values are considered duplicates. For explicit control over the key, implement the {b`Tempest\Idempotency\HasIdempotencyKey`} interface:

```php
// app/ProcessPaymentCommand.php

use Tempest\Idempotency\Attributes\Idempotent;
use Tempest\Idempotency\HasIdempotencyKey;

#[Idempotent]
final readonly class ProcessPaymentCommand implements HasIdempotencyKey
{
public function __construct(
public string $paymentId,
public int $amount,
) {}

public function getIdempotencyKey(): string
{
return $this->paymentId;
}
}
```

When using explicit keys, the payload fingerprint is still verified. Dispatching the same key with a different payload throws {b`Tempest\Idempotency\Exceptions\IdempotencyKeyWasAlreadyUsed`}.

:::info
Read the full [idempotency documentation](./19-idempotency.md) for details on configuration, TTL overrides, custom stores, and HTTP route idempotency.
:::

## Command bus middleware

Whenever commands are dispatched, they are passed to the command bus, which will pass the command along to each of its handlers. Similar to web requests and console commands, this command bus supports middleware. Command bus middleware can be used to, for example, do logging for specific commands, add metadata to commands, or anything else. Command bus middleware are classes that implement the `CommandBusMiddleware` interface, and look like this:
Expand Down
Loading
Loading