kibatic / cronitoring-bundle
Ping a healthchecks.io-compatible instance and sync check schedules for Symfony Scheduler cron tasks.
Package info
github.com/kibatic/cronitoring-bundle
Type:symfony-bundle
pkg:composer/kibatic/cronitoring-bundle
Requires
- php: >=8.5
- dragonmantank/cron-expression: ^3.6
- psr/log: ^3.0
- symfony/config: ^8.0
- symfony/console: ^8.0
- symfony/dependency-injection: ^8.0
- symfony/event-dispatcher: ^8.0
- symfony/framework-bundle: ^8.0
- symfony/http-client: ^8.0
- symfony/http-client-contracts: ^3.0
- symfony/http-kernel: ^8.0
- symfony/messenger: ^8.0
- symfony/scheduler: ^8.0
- symfony/string: ^8.0
- symfony/translation-contracts: ^3.0
Requires (Dev)
- phpunit/phpunit: ^12.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-15 08:43:42 UTC
README
Lire en français : README.fr.md.
Integrates a healthchecks.io-compatible cron monitoring instance
(whether the official service or a self-hosted instance, designated via base_url) into a Symfony
application using the Scheduler component: pings the
start/success/failure/log/exit-code of every scheduled run, and automatically syncs the corresponding
check's schedule (schedule, timezone, grace, tags).
Requirements
- PHP >= 8.5
- Symfony 8.x
symfony/http-client, with an HTTP client enabled (framework.http_client)
Installation
composer require kibatic/cronitoring-bundle
Configuration
# config/packages/cronitoring.yaml cronitoring: base_url: 'https://cron.example.com' # required — healthchecks.io-compatible instance # # (self-hosted or not) to use ping_key: '%env(CRONITORING_PING_KEY)%' # ping-key of the project on the target instance api_key: '%env(CRONITORING_API_KEY)%' # (read-write) API key for the same project slug_prefix: '%env(CRONITORING_SLUG_PREFIX)%' # slug prefix, to distinguish installations
base_url is required: this bundle points to no instance by default, each consuming project must
explicitly declare which one it wants to use. ping_key, api_key and slug_prefix remain optional.
As long as ping_key and api_key are not set, the bundle makes no network call at all; slug_prefix
defaults to an empty string, which is a perfectly valid configuration (it never gates any network call).
Usage
use Kibatic\CronitoringBundle\Attribute\AsMonitoredCronTask; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; #[AsCommand(name: 'app:my-task')] #[AsMonitoredCronTask('*/5 * * * *', slug: 'my-task')] class MyTaskCommand extends Command { // ... }
The slug is optional: when omitted, it is derived from the command's name and its arguments.
#[AsMonitoredCronTask] is only honored on a Console command class (#[AsCommand]): placed on an
invokable service scheduled via #[AsPeriodicTask] or a service method call, it silently has no effect.
For any of this to actually run, a messenger:consume <schedule-transport-name> worker (e.g.
scheduler_default) must be running: this bundle only wires the listener, but it's Symfony's Scheduler
itself that requires an active consumer.
Making the cron expression configurable via an environment variable
#[AsMonitoredCronTask] extends the Scheduler component's #[AsCronTask]: its arguments are evaluated
by PHP itself, at compile time, as a constant expression (literals, new, class constants...). The PHP
engine has no notion of Symfony container parameters, so #[AsMonitoredCronTask('%env(MY_CRON)%')] does
not work: %env(...)% is only interpreted in config files (YAML/XML/PHP) when the container is built,
never in an attribute. Written as-is, it would just be a literal string sent to Cron\CronExpression,
which would fail.
To make a schedule configurable, it therefore has to be built by hand in the application's
ScheduleProviderInterface (#[AsSchedule]), injecting the env var through regular autowiring — a
service constructor, unlike an attribute, does resolve %env(...)%:
use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Messenger\Message\RunCommandMessage; use Symfony\Component\Scheduler\Attribute\AsSchedule; use Symfony\Component\Scheduler\RecurringMessage; use Symfony\Component\Scheduler\Schedule as SymfonySchedule; use Symfony\Component\Scheduler\ScheduleProviderInterface; #[AsSchedule] class Schedule implements ScheduleProviderInterface { public function __construct( #[Autowire('%env(MY_TASK_CRON)%')] private string $myTaskCron, ) { } public function getSchedule(): SymfonySchedule { return (new SymfonySchedule()) ->add(RecurringMessage::cron( $this->myTaskCron, new RunCommandMessage('app:my-task'), )); } }
In that case, remove #[AsMonitoredCronTask(...)] from the command class: otherwise it would be
scheduled twice (once via the attribute, once via the manual addition). Cronitoring monitoring (ping +
sync) stays active: this bundle's listener reacts to any RunCommandMessage processed by the Scheduler,
whether it was declared via attribute or added programmatically — only slug resolution differs (derived
from the command name in both cases, since there is no attribute to read).
Tasks declared via #[AsMonitoredCronTask] elsewhere in the code keep showing up automatically in the
final schedule, whatever getSchedule() does otherwise: it's AddScheduleMessengerPass, the Scheduler
component's compiler pass, that adds them there.