cron-monitor / php-sdk
PHP SDK for cronheart.com (cron-monitor) with first-class Symfony Scheduler and Laravel Scheduler support.
Requires
- php: >=8.2
- nyholm/psr7: ^1.8
- psr/clock: ^1.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/log: ^2.0 || ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.50
- guzzlehttp/guzzle: ^7.8
- guzzlehttp/psr7: ^2.6
- illuminate/console: ^10.0 || ^11.0
- illuminate/contracts: ^10.0 || ^11.0
- illuminate/support: ^10.0 || ^11.0
- phpstan/phpstan: ^1.11
- phpunit/phpunit: ^10.5
- symfony/console: ^6.4 || ^7.0
- symfony/dependency-injection: ^6.4 || ^7.0
- symfony/framework-bundle: ^6.4 || ^7.0
- symfony/http-kernel: ^6.4 || ^7.0
- symfony/messenger: ^6.4 || ^7.0
- symfony/scheduler: ^6.4 || ^7.0
Suggests
- guzzlehttp/guzzle: Recommended PSR-18 HTTP client. Any PSR-18 client works.
- illuminate/console: Required to use the Laravel scheduler macros and sync command.
- symfony/framework-bundle: Required to use the Symfony bundle bridge (^6.4 || ^7.0).
- symfony/scheduler: Required to auto-register Symfony Scheduler runs as monitors.
This package is auto-updated.
Last update: 2026-05-14 12:59:25 UTC
README
Composer SDK for cronheart.com — a Healthchecks-style service that pings you when a scheduled job stops running. First-class support for Symfony Scheduler and the Laravel scheduler.
Why
Uptime monitors don't catch the silent failure mode: a backup that stopped running a month ago, an invoice job that didn't fire on the 1st, an ETL pipeline whose systemd timer was renamed. cronheart's per-job dead-man switch does. This SDK takes the boilerplate out of wiring it up.
Install
composer require cron-monitor/php-sdk
PHP ≥ 8.2.
- Symfony bundle path (
composer require cron-monitor/php-sdkin a Symfony 7 project): drop-in. The bundle binds PSR-17 factories (via the bundlednyholm/psr7) and the PSR-18 client (viasymfony/http-client'sPsr18Client, present in virtually all modern Symfony projects) out of the box. Already wired your own PSR-17 / PSR-18 (Flex recipes, custom factories)? Your bindings override the bundle defaults automatically. - Framework-agnostic path (plain PHP / Laravel / Slim, etc.): bring
your own PSR-18 client + PSR-17 factory and pass them to the
CronMonitorClientconstructor (see Quick start below). The Laravel bridge auto-discovers Guzzle'sHttpFactory+ Guzzle's HTTP client when bound, and falls back to them if you haven't bound aClientInterface/RequestFactoryInterfacein the container.
Quick start (framework-agnostic)
use CronMonitor\Client\Configuration; use CronMonitor\Client\CronMonitorClient; use GuzzleHttp\Client as Guzzle; use GuzzleHttp\Psr7\HttpFactory; $config = Configuration::withDefaultEndpoint(); $factory = new HttpFactory(); $client = new CronMonitorClient( $config, new Guzzle(['timeout' => $config->timeoutSeconds]), $factory, $factory, ); $client->start('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'); try { runMyImportantJob(); $client->success('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'); } catch (\Throwable $e) { $client->fail('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx', $e->getMessage()); throw $e; }
The client never throws on network or HTTP errors — a broken cron-monitor backend will not break your job.
Symfony Scheduler integration
Register the bundle (Flex usually does this for you):
// config/bundles.php return [ // ... CronMonitor\Bridge\Symfony\CronMonitorBundle::class => ['all' => true], ];
Configure:
# config/packages/cron_monitor.yaml cron_monitor: endpoint: '%env(CRON_MONITOR_ENDPOINT)%' # optional, defaults to SaaS timeout_seconds: 5.0 retries: 1 api_key: '%env(CRON_MONITOR_API_KEY)%' # optional messages: # FQCN of any message your Scheduler dispatches via Messenger. # The middleware ships start/success/fail pings for each one. App\Scheduler\Message\NightlyReportRun: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
Discover the FQCNs the bundle can see:
php bin/console cron-monitor:sync
It prints every RecurringMessage from every tagged
scheduler.schedule_provider, plus a YAML snippet you can paste into the
config above.
Plain bin/console commands
If your cron entry is a console command rather than a Scheduler run — e.g.
* * * * * php bin/console app:reports:nightly straight out of crontab —
map the command name and the bundle wraps every invocation in
start/success/fail pings via a kernel event subscriber:
cron_monitor: commands: 'app:reports:nightly': 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
No code changes inside the command. A non-zero exit fires fail; an
uncaught throwable fires fail with the exception class, message, and
file:line in the body so the cron-monitor dashboard shows the immediate
cause without you tailing logs.
Laravel scheduler integration
The service provider is auto-discovered. Publish the config:
php artisan vendor:publish --tag=cron-monitor-config
Then in routes/console.php:
use Illuminate\Support\Facades\Schedule; Schedule::command('reports:nightly') ->dailyAt('02:00') ->monitor('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx');
The ->monitor(...) macro hooks before / onSuccess / onFailure so
you get start/success/fail pings on the same boundary as the job execution.
php artisan cron-monitor:sync lists every scheduled command and emits a
config/cron-monitor.php snippet.
Queued jobs
For ShouldQueue jobs dispatched outside the scheduler, attach the bundled
job middleware:
use CronMonitor\Bridge\Laravel\Queue\MonitorQueueJob; class GenerateNightlyReport implements ShouldQueue { public function middleware(): array { return [MonitorQueueJob::withUuid('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')]; } }
Each invocation pings start, then success on completion or fail on a
thrown exception (with the class, message, and file:line in the body). The
underlying SDK swallows its own failures, so a flaky cron-monitor backend
never breaks the queued job.
Standalone CLI
For plain cron / systemd timer users, the vendor/bin/cron-monitor
binary doesn't require any framework:
# Heartbeat from a one-liner cron entry * * * * * /opt/scripts/run.sh && cron-monitor heartbeat $UUID # Wrap a job with start/success/fail cron-monitor start $UUID if /opt/scripts/run.sh 2> /tmp/err; then cron-monitor success $UUID else cron-monitor fail $UUID --body="$(cat /tmp/err)" fi
Set CRON_MONITOR_ENDPOINT and CRON_MONITOR_API_KEY in the environment
to avoid repeating the flags.
Configuration knobs
| Setting | Default | Notes |
|---|---|---|
endpoint |
https://cronheart.com |
Self-hosted: point at your install. |
timeout_seconds |
5.0 |
Per-request, low by design. |
retries |
1 |
Pings are idempotent server-side. |
api_key |
null |
Reserved for future authenticated routes. |
allow_insecure_endpoint |
false |
Required for http:// endpoints. |
Security
- HTTPS is required by default. The SDK refuses to send pings to plain
HTTP unless
allow_insecure_endpoint: trueis explicitly set. - The per-monitor UUID is treated as a write credential and is validated against the canonical UUID v4 shape before being concatenated into a URL — no path traversal via the action segment.
- The
Authorization: Bearer <api_key>header is attached only when an API key is configured; nothing is sent for anonymous installs. failpings include exception text and a host file path. When a monitored handler throws, the SDK sends the exception class name,getMessage(), andfile:lineof the throw site to the cron-monitor endpoint (capped at 10 KB). Exception messages routinely embed attacker-controlled input (e.g.PDOExceptionSQL fragments, validation errors echoing user data); the file path discloses the host deployment layout. If your threat model treats either as sensitive, wrap the host job in atry/catchthat throws a sanitised exception, or callCronMonitorClient::fail($uuid, $body)directly with a curated body.
Development
composer install composer test # PHPUnit composer stan # PHPStan level 8 composer cs-check # php-cs-fixer dry-run
License
MIT — see LICENSE.