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
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
<?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:
$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:
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():
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 callsexit(1)before the failure ping has returned will lose the alert. Always send the failure ping inside thetry/catch, not inside a shutdown handler.- Fatal errors (segfaults, OOM). Drumbeats catches these via
max_duration_secondson the monitor — pair thestartping with a max-duration config so a process death surfaces as a hung run. - Symfony Messenger. Wrap the handler's
__invokemethod body inDrumbeats::with; do not wrap the entire message bus.
Production patterns
- Time-bound pings. The default Guzzle
timeoutof 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
Clientinstance 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.
Related guides
- 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.