PHP integration

Wire Drumbeats into a PHP codebase. Quick example, centralized setup with Guzzle, Laravel scheduler hooks, and production patterns.

This guide shows how to wire Drumbeats into a PHP codebase. The patterns work with plain PHP scripts, Laravel scheduled tasks, Symfony Messenger consumers, and any framework that runs background work.

The examples use Guzzle for the HTTP layer because it ships timeouts, JSON helpers, and a sensible default config out of the box. If you're constrained to curl_* functions or file_get_contents, the shape is the same — only the transport changes.

Quick example

php
<?php

use GuzzleHttp\Client;

$api = 'https://api.drumbeats.io/v1/ping/<monitor-id>';
$client = new Client(['timeout' => 3]);

$client->get("$api/start");
try {
    runMyJob();
    $client->get("$api/success");
} catch (\Throwable $e) {
    $client->post("$api/failure", [
        'json' => ['payload' => (string) $e],
    ]);
    throw $e;
}
<?php

use GuzzleHttp\Client;

$api = 'https://api.drumbeats.io/v1/ping/<monitor-id>';
$client = new Client(['timeout' => 3]);

$client->get("$api/start");
try {
    runMyJob();
    $client->get("$api/success");
} catch (\Throwable $e) {
    $client->post("$api/failure", [
        'json' => ['payload' => (string) $e],
    ]);
    throw $e;
}

Centralized setup

src/Monitoring/Drumbeats.php
<?php

namespace App\Monitoring;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Throwable;

class Drumbeats
{
    private string $baseUrl;
    private Client $client;

    /** @var array<string, string> */
    public const MONITORS = [
        'daily_backup'     => '11111111-2222-3333-4444-555555555555',
        'hourly_sync'      => '66666666-7777-8888-9999-aaaaaaaaaaaa',
        'newsletter_send'  => 'bbbbbbbb-cccc-dddd-eeee-ffffffffffff',
    ];

    public function __construct(?Client $client = null, ?string $baseUrl = null)
    {
        $this->baseUrl = $baseUrl ?? getenv('DRUMBEATS_BASE_URL') ?: 'https://api.drumbeats.io/v1';
        $this->client = $client ?? new Client(['timeout' => 3]);
    }

    /**
     * @template T
     * @param callable():T $fn
     * @return T
     */
    public function with(string $key, callable $fn, ?string $runId = null)
    {
        $monitorId = self::MONITORS[$key] ?? throw new \InvalidArgumentException("Unknown monitor: $key");
        $runId = $runId ?? "$key-" . bin2hex(random_bytes(8));

        $this->ping($monitorId, 'start', $runId);
        try {
            $result = $fn();
            $this->ping($monitorId, 'success', $runId);
            return $result;
        } catch (Throwable $e) {
            $this->ping($monitorId, 'failure', $runId, (string) $e);
            throw $e;
        }
    }

    private function ping(string $monitorId, string $event, string $runId, ?string $payload = null): void
    {
        $url = "{$this->baseUrl}/ping/$monitorId/$event?run_id=" . urlencode($runId);
        try {
            if ($payload === null) {
                $this->client->get($url);
            } else {
                $this->client->post($url, [
                    'json' => ['payload' => mb_substr($payload, 0, 20000)],
                ]);
            }
        } catch (GuzzleException) {
            // Side-channel; do not propagate ping failures.
        }
    }
}
<?php

namespace App\Monitoring;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Throwable;

class Drumbeats
{
    private string $baseUrl;
    private Client $client;

    /** @var array<string, string> */
    public const MONITORS = [
        'daily_backup'     => '11111111-2222-3333-4444-555555555555',
        'hourly_sync'      => '66666666-7777-8888-9999-aaaaaaaaaaaa',
        'newsletter_send'  => 'bbbbbbbb-cccc-dddd-eeee-ffffffffffff',
    ];

    public function __construct(?Client $client = null, ?string $baseUrl = null)
    {
        $this->baseUrl = $baseUrl ?? getenv('DRUMBEATS_BASE_URL') ?: 'https://api.drumbeats.io/v1';
        $this->client = $client ?? new Client(['timeout' => 3]);
    }

    /**
     * @template T
     * @param callable():T $fn
     * @return T
     */
    public function with(string $key, callable $fn, ?string $runId = null)
    {
        $monitorId = self::MONITORS[$key] ?? throw new \InvalidArgumentException("Unknown monitor: $key");
        $runId = $runId ?? "$key-" . bin2hex(random_bytes(8));

        $this->ping($monitorId, 'start', $runId);
        try {
            $result = $fn();
            $this->ping($monitorId, 'success', $runId);
            return $result;
        } catch (Throwable $e) {
            $this->ping($monitorId, 'failure', $runId, (string) $e);
            throw $e;
        }
    }

    private function ping(string $monitorId, string $event, string $runId, ?string $payload = null): void
    {
        $url = "{$this->baseUrl}/ping/$monitorId/$event?run_id=" . urlencode($runId);
        try {
            if ($payload === null) {
                $this->client->get($url);
            } else {
                $this->client->post($url, [
                    'json' => ['payload' => mb_substr($payload, 0, 20000)],
                ]);
            }
        } catch (GuzzleException) {
            // Side-channel; do not propagate ping failures.
        }
    }
}

Apply it from any caller:

php
$drumbeats = new \App\Monitoring\Drumbeats();
$drumbeats->with('daily_backup', function () {
    dumpDatabase();
    uploadToS3();
});
$drumbeats = new \App\Monitoring\Drumbeats();
$drumbeats->with('daily_backup', function () {
    dumpDatabase();
    uploadToS3();
});

Laravel integration

Laravel ships a scheduler — the cleanest wiring sends pings around each scheduled task automatically:

app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
    $schedule->command('backup:run')
        ->dailyAt('02:00')
        ->before(function () {
            app(Drumbeats::class)->ping('daily_backup', 'start');
        })
        ->onSuccess(function () {
            app(Drumbeats::class)->ping('daily_backup', 'success');
        })
        ->onFailure(function () {
            app(Drumbeats::class)->ping('daily_backup', 'failure');
        });
}
protected function schedule(Schedule $schedule): void
{
    $schedule->command('backup:run')
        ->dailyAt('02:00')
        ->before(function () {
            app(Drumbeats::class)->ping('daily_backup', 'start');
        })
        ->onSuccess(function () {
            app(Drumbeats::class)->ping('daily_backup', 'success');
        })
        ->onFailure(function () {
            app(Drumbeats::class)->ping('daily_backup', 'failure');
        });
}

(Expose a public ping() method on Drumbeats for this — the example class hides it as private. Same shape, different visibility.)

For queue workers, wrap the handler with with():

php
class ProcessOrder implements ShouldQueue
{
    public function handle(Drumbeats $drumbeats): void
    {
        $drumbeats->with('order_processor', fn () => $this->process(), runId: "order-{$this->order->id}");
    }
}
class ProcessOrder implements ShouldQueue
{
    public function handle(Drumbeats $drumbeats): void
    {
        $drumbeats->with('order_processor', fn () => $this->process(), runId: "order-{$this->order->id}");
    }
}

Error handling

  • exit(...) calls. A worker that calls exit(1) before the failure ping has returned will lose the alert. Always send the failure ping inside the try/catch, not inside a shutdown handler.
  • Fatal errors (segfaults, OOM). Drumbeats catches these via max_duration_seconds on the monitor — pair the start ping with a max-duration config so a process death surfaces as a hung run.
  • Symfony Messenger. Wrap the handler's __invoke method body in Drumbeats::with; do not wrap the entire message bus.

Production patterns

  • Time-bound pings. The default Guzzle timeout of 3 seconds keeps a slow Drumbeats request from blocking your workload.
  • Cap payload size. The class above truncates payloads at 20KB — Drumbeats stores 25KB at no extra beat cost per Beats and usage.
  • Connection reuse. The Client instance is reusable. In long-lived workers, keep it on the service container so you reuse the underlying TCP connection.
  • Multi-environment via slug. Use /v1/s-ping/<project>/<slug>/<event> to route the same code in staging and prod to different monitors — see Ping API: scheduled pings.
  • Monitor types — pick Cron, Heartbeat, or Event-driven first.
  • Ping API — endpoint and parameter reference.
  • Production hardening — retries, timeouts, observability for any language.
  • Alternatives — how Drumbeats compares to Cronitor and Healthchecks.io for PHP stacks.