Skip to main content
ImmutableLog logo
BackPHP

PHP

Complete integration guide for ImmutableLog in PHP. Automatic middleware for Laravel and Symfony — or a direct Guzzle HTTP client for workers, commands, and queue jobs. Use terminable middleware (Laravel) or kernel.terminate (Symfony) to send after the response.

Direct HTTP client (Guzzle)

Use the ImmutableLog class to send events directly without depending on a web framework. Ideal for artisan/console commands, queue workers, and any PHP script that needs to record events.

php
<?php
// src/ImmutableLog.php
// composer require guzzlehttp/guzzle ramsey/uuid

use GuzzleHttp\Client;
use Ramsey\Uuid\Uuid;

final class ImmutableLog
{
    private Client $http;

    public function __construct(
        private string $apiKey,
        private string $service = 'php-service',
        private string $env = 'production',
        ?string $apiUrl = null,
    ) {
        $this->http = new Client([
            'base_uri' => $apiUrl ?? (getenv('IMTBL_URL') ?: 'https://api.immutablelog.com'),
            'timeout'  => 5,
        ]);
    }

    public function sendEvent(
        string $eventName,
        string $kind,                 // "success" | "info" | "error"
        array $payload,
        ?string $immutableTrail = null,
    ): void {
        $requestId = Uuid::uuid4()->toString();
        $trail = self::sanitizeTrail($immutableTrail);

        // Todos os valores de meta precisam ser strings.
        $meta = [
            'type'       => $kind,
            'event_name' => $eventName,
            'service'    => $this->service,
            'request_id' => $requestId,
            'env'        => $this->env,
        ];
        if ($trail !== null) {
            $meta['immutable_trail'] = $trail;
        }

        $event = [
            'payload' => json_encode($payload + ['timestamp' => gmdate('c')]),
            'meta'    => $meta,
        ];

        $headers = [
            'Authorization' => "Bearer {$this->apiKey}",
            'Content-Type'  => 'application/json',
            // Idempotency-Key e OBRIGATORIO (sem ele a API responde 400).
            'Idempotency-Key'         => "{$eventName}-{$requestId}",
            'Request-Id'              => $requestId,
            'X-Client-TZ'             => 'America/Sao_Paulo',
            'X-Client-Offset-Minutes' => '-180',
        ];
        if ($trail !== null) {
            $headers['X-Imtbl-Trail'] = $trail;
        }

        $this->http->post('/v1/events', [
            'headers' => $headers,
            'json'    => $event,
        ]);
    }

    // Normaliza o immutable_trail (trim, nao-vazio, max 256, sem ':').
    // O servidor responde 400 invalid_immutable_trail se a regra for violada.
    public static function sanitizeTrail(?string $value): ?string
    {
        if ($value === null) {
            return null;
        }
        $v = trim($value);
        if ($v === '') {
            return null;
        }
        $v = str_replace(':', '-', $v);
        if (mb_strlen($v) > 256) {
            $v = mb_substr($v, 0, 256);
        }
        return $v;
    }
}

// Uso / Usage
$log = new ImmutableLog(getenv('IMTBL_API_KEY'), 'payments-service', 'production');
$log->sendEvent('payment.approved', 'success', [
    'payment_id' => 'pay_abc123',
    'amount'     => 299.90,
], immutableTrail: 'order-7782');

PHP is synchronous per request. To avoid blocking the user, send events from a queue job (Laravel Queue / Symfony Messenger) or from a terminable middleware.

Retry with exponential backoff

For high-availability production, add retry with exponential backoff. ImmutableLog returns 202 on success and 200 on an idempotent duplicate — never retry on 429 (monthly limit reached).

php
public function sendWithRetry(
    string $eventName,
    string $kind,
    array $payload,
    ?string $trail = null,
    int $maxAttempts = 3,
): void {
    $delayMs = 500;

    for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
        try {
            $this->sendEvent($eventName, $kind, $payload, $trail);
            return;
        } catch (\GuzzleHttp\Exception\RequestException $e) {
            $status = $e->getResponse()?->getStatusCode();

            // 429 = limite mensal — nao fazer retry.
            if ($status === 429) {
                return;
            }
            if ($attempt + 1 < $maxAttempts) {
                usleep($delayMs * 1000);
                $delayMs *= 2; // 500ms -> 1s -> 2s
            }
        }
    }
}

Sensitive data hashing

Never send raw personal data (email, CPF, IP) to ImmutableLog. Use SHA-256 to generate a deterministic digest — traceable without exposing the original data.

php
<?php
// Faz o hash de dados sensiveis antes de enviar — nunca logue PII bruto.
function sha256_hex(string $data): string
{
    return hash('sha256', $data);
}

// Uso no payload do evento:
$payload = [
    'user_id'    => 'usr_123',
    'email_hash' => sha256_hex('user@example.com'),
    'ip_hash'    => sha256_hex('192.168.1.1'),
];

This documentation reflects the current API behavior. For questions or advanced integrations, contact the support team.