Skip to content

Extensions

Extensions provide optional capabilities that enhance Forrst functions with cross-cutting concerns like caching, idempotency, rate limiting, and observability.

ExtensionPurpose
CachingExtensionHTTP-style caching with ETags and conditional requests
DeadlineExtensionRequest timeouts and deadline propagation
DeprecationExtensionMark functions as deprecated with migration guidance
DryRunExtensionValidate requests without side effects
IdempotencyExtensionPrevent duplicate operations
LocaleExtensionLocalization and internationalization
MaintenanceExtensionScheduled maintenance windows
PriorityExtensionRequest prioritization
QueryExtensionRich filtering, sorting, and pagination
QuotaExtensionUsage quotas and limits
RateLimitExtensionThrottle requests
RedactExtensionRedact sensitive data in responses
ReplayExtensionRequest replay for debugging
RetryExtensionAutomatic retry with backoff
SimulationExtensionSandbox mode with simulated responses
StreamExtensionStreaming responses
TracingExtensionDistributed tracing context
config/rpc.php
'servers' => [
[
'extensions' => [
\Cline\Forrst\Extensions\CachingExtension::class,
\Cline\Forrst\Extensions\IdempotencyExtension::class,
],
],
],
use Cline\Forrst\Extensions\CachingExtension;
use Cline\Forrst\Extensions\IdempotencyExtension;
use Cline\Forrst\Extensions\RateLimitExtension;
use Cline\Forrst\Servers\AbstractServer;
class ApiServer extends AbstractServer
{
public function extensions(): array
{
return [
new CachingExtension(cache: cache()->store()),
new IdempotencyExtension(),
new RateLimitExtension(maxAttempts: 60, decayMinutes: 1),
];
}
}

Implements HTTP-style caching with ETags and conditional requests:

use Cline\Forrst\Extensions\CachingExtension;
new CachingExtension(
cache: cache()->store('redis'),
defaultTtl: 300, // 5 minutes
);
{
"call": { "function": "urn:acme:forrst:fn:users:list" },
"extensions": {
"caching": {
"if_none_match": "\"abc123\"",
"if_modified_since": "2025-01-15T10:00:00Z"
}
}
}
{
"result": [...],
"extensions": {
"caching": {
"etag": "\"def456\"",
"last_modified": "2025-01-15T12:30:00Z",
"max_age": 300,
"cache_status": "miss"
}
}
}
StatusDescription
hitClient’s cached copy is valid
missFresh response generated
staleCached copy exists but outdated
bypassCaching intentionally bypassed

Prevents duplicate operations using idempotency keys:

use Cline\Forrst\Extensions\IdempotencyExtension;
new IdempotencyExtension();
{
"call": {
"function": "urn:acme:forrst:fn:payments:charge",
"arguments": { "amount": 99.99, "customer_id": 123 }
},
"extensions": {
"idempotency": {
"key": "charge-123-abc"
}
}
}

If the same key is sent again within the TTL, the cached response is returned without re-executing the function.

Throttles requests to prevent abuse:

use Cline\Forrst\Extensions\RateLimitExtension;
new RateLimitExtension(
maxAttempts: 60, // requests per window
decayMinutes: 1, // window duration
);
{
"extensions": {
"rate_limit": {
"limit": 60,
"remaining": 45,
"reset_at": "2025-01-15T10:01:00Z"
}
}
}
{
"errors": [{
"code": "RATE_LIMITED",
"message": "Too many requests",
"details": {
"retry_after": 45
}
}]
}

Enforces request timeouts:

use Cline\Forrst\Extensions\DeadlineExtension;
new DeadlineExtension(
defaultTimeout: 30, // seconds
);
{
"call": { "function": "urn:acme:forrst:fn:reports:generate" },
"extensions": {
"deadline": {
"timeout": "60s",
"absolute": "2025-01-15T10:05:00Z"
}
}
}

Functions can check remaining time:

class ReportGenerateFunction extends AbstractFunction
{
public function __invoke(): array
{
// Check remaining deadline time
if ($this->getDeadlineRemaining() < 5) {
return $this->partialResult();
}
return $this->generateFullReport();
}
}

Rich filtering, sorting, and pagination for list functions:

use Cline\Forrst\Extensions\QueryExtension;
new QueryExtension();
{
"call": {
"function": "urn:acme:forrst:fn:users:list",
"arguments": {}
},
"extensions": {
"query": {
"filter": {
"status": { "eq": "active" },
"created_at": { "gte": "2025-01-01" }
},
"sort": [
{ "field": "name", "direction": "asc" }
],
"page": {
"size": 25,
"number": 1
}
}
}
}
OperatorDescriptionExample
eqEquals{ "status": { "eq": "active" } }
neqNot equals{ "status": { "neq": "deleted" } }
gtGreater than{ "age": { "gt": 18 } }
gteGreater or equal{ "created_at": { "gte": "2025-01-01" } }
ltLess than{ "price": { "lt": 100 } }
lteLess or equal{ "stock": { "lte": 10 } }
inIn array{ "status": { "in": ["active", "pending"] } }
containsString contains{ "name": { "contains": "john" } }

Marks functions as deprecated with migration guidance:

use Cline\Forrst\Extensions\DeprecationExtension;
new DeprecationExtension();
FunctionDescriptor::make()
->urn('urn:acme:forrst:fn:users:list')
->deprecated(
DeprecatedData::make('2.0.0', 'Use urn:acme:forrst:fn:users:search')
->removedIn('3.0.0')
);
{
"result": [...],
"extensions": {
"deprecation": {
"warning": "This function is deprecated since v2.0.0",
"replacement": "urn:acme:forrst:fn:users:search",
"removed_in": "3.0.0"
}
}
}

Propagates distributed tracing context:

use Cline\Forrst\Extensions\TracingExtension;
new TracingExtension();
{
"call": { "function": "urn:acme:forrst:fn:orders:create" },
"context": {
"trace_id": "abc123",
"span_id": "def456",
"parent_span_id": "ghi789"
}
}
{
"result": {...},
"context": {
"trace_id": "abc123",
"span_id": "jkl012",
"parent_span_id": "def456",
"duration_ms": 45
}
}
<?php
namespace App\Extensions;
use Cline\Forrst\Extensions\AbstractExtension;
use Override;
class AuditExtension extends AbstractExtension
{
#[Override()]
public function getUrn(): string
{
return 'urn:acme:forrst:ext:audit';
}
#[Override()]
public function isGlobal(): bool
{
return true; // Runs on all requests
}
#[Override()]
public function isErrorFatal(): bool
{
return false; // Errors don't fail the request
}
#[Override()]
public function getSubscribedEvents(): array
{
return [
FunctionExecuted::class => [
'priority' => 100,
'method' => 'onFunctionExecuted',
],
];
}
public function onFunctionExecuted(FunctionExecuted $event): void
{
AuditLog::create([
'function' => $event->function->getName(),
'user_id' => auth()->id(),
'arguments' => $event->request->arguments,
'response' => $event->response->result,
]);
}
}
EventWhen Fired
RequestReceivedRequest parsed, before validation
RequestValidatedRequest validated, before function resolution
ExecutingFunctionFunction resolved, before execution
FunctionExecutedFunction completed successfully
SendingResponseResponse prepared, before encoding
RequestFailedError occurred during processing
class TenantExtension extends AbstractExtension
{
public function getSubscribedEvents(): array
{
return [
RequestValidated::class => [
'priority' => 50,
'method' => 'injectTenant',
],
];
}
public function injectTenant(RequestValidated $event): void
{
// Add tenant to all queries automatically
$tenantId = auth()->user()?->tenant_id;
$event->request = $event->request->withArgument(
'tenant_id',
$tenantId,
);
}
}
class TimingExtension extends AbstractExtension
{
private float $startTime;
public function getSubscribedEvents(): array
{
return [
RequestReceived::class => [
'priority' => 0,
'method' => 'startTimer',
],
SendingResponse::class => [
'priority' => 1000,
'method' => 'addTiming',
],
];
}
public function startTimer(RequestReceived $event): void
{
$this->startTime = microtime(true);
}
public function addTiming(SendingResponse $event): void
{
$duration = (microtime(true) - $this->startTime) * 1000;
$event->response = $event->response->withExtension(
'timing',
['duration_ms' => round($duration, 2)],
);
}
}

Lower priority numbers run first. Use these guidelines:

Priority RangePurpose
0-49Early processing (timing, validation)
50-99Request modification (tenant injection)
100-149Core functionality (caching, idempotency)
150-199Response modification
200+Late processing (logging, metrics)

Run on every request automatically:

public function isGlobal(): bool
{
return true; // Tracing, timing, audit logging
}

Only run when client requests them:

public function isGlobal(): bool
{
return false; // Caching, idempotency, dry-run
}

Client requests opt-in extensions:

{
"extensions": {
"caching": { "ttl": 300 },
"idempotency": { "key": "..." }
}
}