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.
Middlewares
Automatic integration via middleware — every HTTP request is captured without modifying any controller. Click the framework to see the complete code.
Terminable middleware with handle() + terminate(). terminate() runs after the response is sent — naturally fire-and-forget.
public function terminate($request, $response): void
{
$this->emit($request, $response);
// roda DEPOIS da resposta enviada
}View full documentation →
EventSubscriber on kernel.response/kernel.exception + kernel.terminate to send after the response, using Symfony HttpClient.
public static function getSubscribedEvents(): array
{
return [
KernelEvents::TERMINATE => 'onTerminate',
KernelEvents::EXCEPTION => 'onException',
];
}View full documentation →
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
// 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).
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
// 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.
