cronitorex / php
Official PHP SDK for Cronitorex: cron job, heartbeat and uptime monitoring. Ping API and Management API clients with typed errors, on any PSR-18 HTTP client.
Package info
pkg:composer/cronitorex/php
Requires
- php: ^8.1
- ext-json: *
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1|^2.0
- psr/log: ^2.0|^3.0
Requires (Dev)
- dragonmantank/cron-expression: ^3.3
- friendsofphp/php-cs-fixer: ^3.64
- guzzlehttp/guzzle: ^7.8
- guzzlehttp/psr7: ^2.6
- php-http/mock-client: ^1.6
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.0|^11.0
- symfony/yaml: ^6.4|^7.0
Suggests
- dragonmantank/cron-expression: Required to derive intervals from cron expressions in ensure(); pass intervalSeconds without it
- guzzlehttp/guzzle: Default PSR-18 client; the SDK configures sane timeouts for it automatically
- symfony/yaml: Required for YAML bundle files in applyFromFile()/exportToFile(); JSON works without it
This package is auto-updated.
Last update: 2026-08-05 17:27:12 UTC
README
Official PHP SDK for Cronitorex: cron job, heartbeat and uptime monitoring. Works with any framework or none - the only transport requirement is a PSR-18 HTTP client (Guzzle works out of the box).
Using Laravel? The cronitorex/laravel
package instruments the Scheduler automatically; this SDK is the universal
building block for everything else.
Installation
composer require cronitorex/php
If your project has no PSR-18 client yet, add one (Guzzle is the usual choice):
composer require guzzlehttp/guzzle
Wrap a job
An ingest key (ck_, from the panel) is all you need. Monitors are created
automatically on first ping. The fastest way to monitor a job is to wrap it:
use Cronitorex\Cronitorex; $cx = new Cronitorex(ingestKey: 'ck_your_key'); $cx->job('db-backup', function () { // ... do the work ... });
job() sends run before your callable, complete with the measured duration
after it, and fail with the exception message if it throws. All three pings
share a generated series id, so overlapping runs never pair up wrong. The
callable's return value is passed through, its exceptions are rethrown - only
the pings themselves are fail-safe.
Pings (dead man's switch)
For full control, send the pings yourself:
use Cronitorex\Cronitorex; use Cronitorex\Enum\PingStatus; $cx = new Cronitorex(ingestKey: 'ck_your_key'); $series = uniqid(); $cx->ping('db-backup', PingStatus::Run, series: $series); // ... do the work ... $cx->ping('db-backup', PingStatus::Complete, series: $series, duration: 47.3, exitCode: 0);
ping() never throws and never blocks for long: monitoring must not be able to
take down the job it monitors. It returns false when the ping could not be
sent. If you want the exception instead (in a monitoring-critical path), use
pingOrFail(). Stream and custom events go through event(), which is
fail-safe the same way (with eventOrFail() as the throwing variant):
$cx->event('deploys', state: 'started', host: gethostname());
Management API
Add a management key (mk_, scopes read + write) to manage monitors as code.
Both keys are optional and independent - pass only what you use.
use Cronitorex\Enum\MonitorKind; $cx = new Cronitorex(managementKey: 'mk_your_key'); $account = $cx->account(); // plan, monitors used/limit $list = $cx->monitors()->list(MonitorKind::Ping); $cx->monitors()->create([ 'manifest_version' => 1, 'kind' => 'ping', 'name' => 'db-backup', 'enabled' => true, 'expected_interval_seconds' => 86400, 'grace_seconds' => 3600, 'tags' => ['production'], ]); $bundle = $cx->monitors()->export(); // whole account as a bundle $report = $cx->monitors()->apply($bundle, dryRun: true); $cx->monitors()->delete($uuid, confirm: true); // without confirm: refused locally
For the common case - "make sure this cron job has a monitor with sane
settings" - use ensure(), an idempotent upsert by name:
// derives expected_interval_seconds from the schedule and a grace of // interval/2 clamped to [2 min, 1 h]; needs dragonmantank/cron-expression $cx->monitors()->ensure('db-backup', cronExpression: '0 3 * * *'); // or state the interval yourself - no extra package needed $cx->monitors()->ensure('db-backup', intervalSeconds: 86400); // computed fields can be overridden $cx->monitors()->ensure('db-backup', intervalSeconds: 86400, extra: ['tags' => ['production']]);
Manifests are plain arrays shaped exactly like the API contract - the server is the single source of validation truth, so the SDK never gets stale on manifest rules.
Config as code
Keep your monitors in a version-controlled file and apply it on deploy. YAML
and JSON are supported by extension; YAML needs composer require symfony/yaml:
// validate the file in CI (no changes are made) $report = $cx->monitors()->applyFromFile('monitors.yaml', dryRun: true); // apply it on deploy $report = $cx->monitors()->applyFromFile('monitors.yaml'); // snapshot the current account state back into a file $cx->monitors()->exportToFile('monitors.yaml');
# monitors.yaml manifest_version: 1 monitors: - kind: ping name: db-backup expected_interval_seconds: 86400 grace_seconds: 3600
Errors
Management calls throw typed exceptions with stable, machine-readable codes (never match on message text):
use Cronitorex\Enum\ErrorCode; use Cronitorex\Exception\{ValidationException, AuthException, RateLimitException, ConfirmRequiredException, ConfigAsCodeDisabledException, TransportException}; try { $cx->monitors()->create($manifest); } catch (ValidationException $e) { // $e->errorCodes() => ['schedule' => [ErrorCode::ScheduleInvalid]] // $e->errors() => human-readable messages per field } catch (AuthException $e) { // bad key, or a scoped key missing the needed scope } catch (RateLimitException $e) { // 60 req/min per management key }
Unknown error codes map to ErrorCode::Unknown instead of failing, so new
server-side codes never break existing SDK versions.
Configuration
Everything is a constructor argument; nothing is global:
| Argument | Default | Purpose |
|---|---|---|
ingestKey |
null |
ck_ key; without it ping() returns false |
managementKey |
null |
mk_ key; without it monitors()/account() throw locally |
ingestUrl |
https://api.cronitorex.com |
Ping API base |
apiUrl |
https://app.cronitorex.com/api/v1 |
Management API base |
httpClient |
auto-discovered | Any PSR-18 client; bring your own to control timeouts |
requestFactory / streamFactory |
auto-discovered | Any PSR-17 factories |
logger |
null |
Any PSR-3 logger; fail-safe methods (ping(), event(), health()) log swallowed failures as warnings |
Every exception the SDK throws implements
Cronitorex\Exception\CronitorexExceptionInterface, so one catch block covers
them all.
Timeouts
PSR-18 has no timeout API, so timeouts belong to the HTTP client. When the SDK
constructs the default Guzzle client itself, it sets a 2 s connect / 5 s
request timeout - short on purpose, because ping() runs synchronously
inside the job it monitors, and a down monitoring endpoint must cost seconds,
not tens. If you inject your own client, its configuration wins and the
timeouts become your responsibility:
$cx = new Cronitorex( ingestKey: 'ck_your_key', httpClient: new \GuzzleHttp\Client(['connect_timeout' => 1, 'timeout' => 2]), );
Migrating from cronitor-php
The ping vocabulary is intentionally compatible (run / complete / fail /
skip, duration, exit_code, host, series), so most call sites map
one to one:
| cronitor-php | cronitorex/php |
|---|---|
new Cronitor\Client($apiKey) |
new Cronitorex(ingestKey: 'ck_...', managementKey: 'mk_...') |
$client->ping('key', ['state' => 'run']) |
$cx->ping('name', PingStatus::Run) |
$client->job('key', $fn) |
$cx->job('name', $fn) |
$client->applyConfig() |
$cx->monitors()->applyFromFile('monitors.yaml') |
$client->validateConfig() |
$cx->monitors()->applyFromFile('monitors.yaml', dryRun: true) |
$client->generateConfig() |
$cx->monitors()->exportToFile('monitors.yaml') |
Differences to know about: monitors are addressed by name (created
automatically on first ping), errors are typed exceptions with stable
ErrorCode enums instead of raw responses, and the transport is any PSR-18
client rather than bundled cURL. See the
migration guide
for the platform-level differences.
Requirements
PHP 8.1+, ext-json, any PSR-18 HTTP client.
Links
License
MIT