PHP code example of spiral / idempotency

1. Go to this page and download the library: Download spiral/idempotency library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

spiral / idempotency example snippets


// app/src/Application/Kernel.php
public function defineBootloaders(): array
{
    return [
        // ...
        \Spiral\Idempotency\Bootloader\IdempotencyBootloader::class,
        // opt-in: HTTP wiring for the #[Idempotent] attribute
        \Spiral\Idempotency\Bootloader\HttpIdempotencyBootloader::class,
        // opt-in: idempotency tables in the ORM schema (cycle:sync / cycle:migrate)
        \Spiral\Idempotency\Bootloader\CycleSchemaBootloader::class,
    ];
}

// app/config/idempotency.php
use Spiral\Idempotency\Driver\Cycle\CycleInboxConfig;
use Spiral\Idempotency\Driver\Cycle\CycleLeaseConfig;
use Spiral\Idempotency\Http\HttpKeyMiddleware;
use Spiral\Idempotency\Http\HttpOutcomeMiddleware;

return [
    'default' => 'payments',

    // Resolution middleware, outer → inner. The outcome middleware sits OUTERMOST: it marshals
    // the response (replay headers, Locked → 409, missing key → 400); the key middleware inside
    // it extracts and normalizes the key.
    'transports' => [
        'http' => [
            HttpOutcomeMiddleware::class,
            HttpKeyMiddleware::class,
        ],
    ],

    // Semantic aliases: the handler code names an alias, the driver and the declared guarantee
    // live here. The declared guarantee is verified against the driver capability at bootstrap.
    'storages' => [
        'payments' => new CycleLeaseConfig(
            table: 'idempotency_lease',
            lockTtl: 30,        // seconds the PROCESSING lock is held
            retentionTtl: 3600, // seconds the cached result is kept
        ),
        'orders' => new CycleInboxConfig(
            table: 'idempotency_inbox',
        ),
    ],
];

use Spiral\Idempotency\Interceptor\IdempotencyInterceptor;

final class AppBootloader extends DomainBootloader
{
    protected const INTERCEPTORS = [
        // ...your existing interceptors (Cycle, Guard, ...) — innermost after auth:
        IdempotencyInterceptor::class,
    ];
}

use Spiral\Idempotency\Attribute\Idempotent;

final class PaymentController
{
    #[Route(route: '/payments/charge', methods: 'POST')]
    #[Idempotent(storage: 'payments')]
    public function charge(): ResponseInterface
    {
        // charge the gateway, return the response — it will be cached and replayed
    }
}

// Kernel::defineBootloaders()
\Spiral\Idempotency\Bootloader\QueueIdempotencyBootloader::class,

// app/config/idempotency.php
use Spiral\Idempotency\Queue\QueueKeyMiddleware;
use Spiral\Idempotency\Queue\QueueRetryMiddleware;

'transports' => [
    'queue' => [
        QueueRetryMiddleware::class, // outer: maps failure modes back to the transport
        QueueKeyMiddleware::class,   // inner: extracts the key from the job header
    ],
],

use Spiral\Idempotency\Interceptor\IdempotencyInterceptor;

$queue->addConsumeInterceptor(IdempotencyInterceptor::class);

#[Idempotent(storage: 'orders', key: 'orderId')] // dot-path over the job payload
public function invoke(string $orderId, array $payload): void
{
    // runs once per orderId; a redelivered job replays / is skipped
}

// a bootloader of the app — the config stack lists class names, so bind the configured instance
QueueKeyMiddleware::class => static fn(KeyResolver $resolver): QueueKeyMiddleware
    => new QueueKeyMiddleware($resolver, fallbackToJobId: true),

#[Idempotent(storage: 'orders')] // key from the job header
final class DispatchEvent extends JobHandler {} // handle() is inherited from JobHandler

// Kernel::defineBootloaders()
\Spiral\Idempotency\Bootloader\GrpcIdempotencyBootloader::class,

// app/config/idempotency.php
use Spiral\Idempotency\Grpc\GrpcKeyMiddleware;
use Spiral\Idempotency\Grpc\GrpcOutcomeMiddleware;

'transports' => [
    'grpc' => [
        GrpcOutcomeMiddleware::class, // outer: response/status snapshot, Locked → ABORTED
        GrpcKeyMiddleware::class,     // inner: key from the `idempotency-key` metadata entry
    ],
],

// Kernel::defineBootloaders()
\Spiral\Idempotency\Bootloader\EventsIdempotencyBootloader::class,

use Spiral\Idempotency\Events\HasIdempotencyKey;

final class OrderPlaced implements HasIdempotencyKey
{
    public function __construct(public readonly string $id) {} // outbox message_id, not a fresh uuid

    public function idempotencyKey(): string
    {
        return $this->id;
    }
}

final class PaymentService
{
    #[Listener]
    #[Idempotent(storage: 'events')]
    public function charge(OrderPlaced $event): void {}

    #[Listener]
    #[Idempotent(storage: 'events')]
    public function notify(OrderPlaced $event): void {}
}

interface DomainEvent extends HasIdempotencyKey {}

trait WithEventId
{
    public readonly Uid $id; // the outbox message_id, preserved across re-publications

    public function idempotencyKey(): string
    {
        return $this->id->rawValue();
    }
}

#[Idempotent(storage: 'payments', key: 'command.orderId', lockTtl: 60, ttl: 86400, scope: null)]

use Spiral\Idempotency\Driver\Cycle\CycleContext;
use Spiral\Idempotency\IdempotencyContext;

#[Idempotent(storage: 'orders', key: 'command.orderId')]
public function place(PlaceOrder $command, IdempotencyContext $ctx): array
{
    \assert($ctx instanceof CycleContext);

    // ORM entities: a scoped Unit of Work flushed inside the transaction
    $ctx->entityManager()->persist(new Order(...));
    // ...or raw DBAL through the transactional connection
    $ctx->database()->insert('order_events')->values([...])->run();

    return ['status' => 'placed'];
}

use Spiral\Idempotency\ExecuteOptions;
use Spiral\Idempotency\FailurePolicy;
use Spiral\Idempotency\IdempotencyContext;
use Spiral\Idempotency\IdempotencyRegistry;

public function __construct(
    private readonly IdempotencyRegistry $registry,
) {}

public function handle(string $transactionId): Receipt
{
    return $this->registry->get('payments')->execute(
        $transactionId,
        static fn(IdempotencyContext $ctx): Receipt => /* the operation */,
        new ExecuteOptions(lockTtl: 60, ttl: 86400, failurePolicy: FailurePolicy::Cache),
    );
}

final class PaymentDeclined extends \DomainException implements ReplayableFailure
{
    public function toReplayPayload(): array
    {
        return ['code' => $this->code, 'reason' => $this->reason];
    }

    public static function fromReplayPayload(array $payload): static
    {
        return new self($payload['code'], $payload['reason']);
    }
}

use Spiral\Idempotency\FailurePolicy;

#[Idempotent(storage: 'jobs', key: 'event.id', failurePolicy: FailurePolicy::Release)]

   final class DeclineRenderer implements DomainFailureRenderer
   {
       public function __construct(private ResponseFactoryInterface $responses) {}

       public function render(\Throwable $failure): ?ResponseInterface
       {
           // Return null for failures this renderer does not own — they are rethrown untouched.
           return $failure instanceof PaymentDeclined
               ? $this->responses->createResponse(402)
               : null;
       }
   }