ibexa/cron
Exposes cron/cron for Ibexa and Symfony through the simple ibexa:cron:run command. Tag existing Symfony commands as cron jobs with schedules, optional categories, and custom options, then run them from system cron. A lightweight alternative to cron-bundle.
Install and verify the wire-up first.
composer require ibexa/cron
bin/console list ibexa:cron # confirm the command is registered
bin/console ibexa:cron:run --help # confirm the actual options on your version
Look here first, in this order:
src/Command/ (or src/) — the package is small enough that the entrypoint command is the documentation. Read the execute() method: it tells you exactly which tagged commands are collected, how they are filtered by category, and whether failures abort the loop.ibexa.cron.job tag is consumed and the tag attributes (name, schedule, category) are read. If a tag looks ignored, the bug is almost always here.cron/cron dependency itself — Cron\CronExpression::factory($schedule)->isDue() is the whole matching engine. Everything surprising about timing comes from that library, not from Ibexa.First use case — the one that pays for the install: an existing Symfony command that already works from the CLI, run on a schedule without writing a crontab entry per command.
#[AsCommand(name: 'app:sync-prices', description: 'Sync prices from ERP')]
class SyncPricesCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
// ...
return Command::SUCCESS;
}
}
Tag it in config/services.yaml:
services:
App\Command\SyncPricesCommand:
tags:
- {
name: 'ibexa.cron.job',
schedule: '*/15 * * * *',
category: 'default',
}
Then a single system crontab line, on every machine that should run jobs:
* * * * * /usr/bin/php /var/www/app/bin/console ibexa:cron:run --category=default >> /var/log/app-cron.log 2>&1
Note the deviation from the README example: log to a file instead of /dev/null. You will spend hours debugging without it.
Treat the package as a dispatcher, not a scheduler. Its job is "at this minute, which tagged commands are due, and run them." Anything heavier belongs in Messenger or a Symfony Scheduler.
Thin-command pattern (recommended). Keep the scheduled command trivial — parse input, dispatch a message, exit. Do the work in a handler.
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->bus->dispatch(new SyncPricesMessage());
return Command::SUCCESS;
}
This keeps the cron tick short, gives you retries and transport-level concurrency control, and makes it painless to move the job off the cron path later.
Category as environment/role boundary. The category attribute is the package's only real knob, so use it deliberately:
default — lightweight, per-node, idempotent jobs (cache warmers, health pings).batch — heavy or stateful work; registered on exactly one node's crontab.infrequent — hourly/daily jobs, on a separate crontab line so a runaway batch job can't starve them.Because each category requires its own system crontab entry, keep the number small. Two or three categories is plenty; ten is an operational mess.
Expression style. Use cron/cron's supported syntax and stick to expressions a Linux admin can read: * * * * *, */5 * * * *, 0 3 * * *, 0 3 * * 1-5. Alias forms like @daily and hash-based H are the kind of thing you should verify against your pinned version rather than assume.
Integration tip — one config block per job, next to the command. Rather than a central crons.yaml, tag each command in the file where it's declared (or in a dedicated config/services/cron.yaml that imports nothing else). When you delete a command, its schedule dies with it. Centralized schedules rot.
Prefer idempotent jobs. Since there's no lock and no state tracking in this package, a job that can safely run twice is worth more than a job that needs exactly-once semantics.
Verify registration in CI. A wrong attribute name or a typo'd category fails silently at runtime — the job just never runs. Add a smoke check:
bin/console debug:container --tag=ibexa.cron.job
and assert the expected command IDs are present. This is the single highest-value test you can write around this package.
No locking — this is the big one. The README is explicit that cluster support and a lock system are future work. Consequences:
symfony/lock, a DB advisory lock, or a flock wrapper in the crontab line).ibexa:cron:run on multiple nodes means every node runs every default job. Don't scale horizontally into this without external coordination.Silent no-ops cost the most time. A job doesn't run when:
name isn't byte-for-byte ibexa.cron.job;expression/cron instead of schedule;category in the tag doesn't match the --category you pass (both default to default, so a typo on one side means never);services.yaml — always bin/console cache:clear (in dev, the compiled container is cached).Debug in this order: debug:container --tag=ibexa.cron.job → run the command directly (bin/console app:sync-prices) → run bin/console ibexa:cron:run --category=... -vvv at a minute where the expression should match.
Timezone mismatch. cron/cron evaluates against PHP's default timezone; system cron uses the system's. On a container with TZ=UTC and an app configured to Europe/Warsaw, a 0 3 * * * job fires at the wrong local hour. Pin date.timezone in php.ini or set TZ on the container.
One-minute granularity is a hard floor. Your crontab runs every minute; the package matches the current minute. Sub-minute schedules don't exist here, and a tick that takes >60s silently swallows the next minute's jobs.
Don't point /dev/null at it. The README example discards output. Redirect to a log, and consider --no-interaction (or -n) plus set -e-style discipline in the wrapper script so a PHP fatal doesn't go unnoticed.
Exit codes and failure isolation. Check the source for whether a failing job stops the remaining jobs in the batch. Don't assume other jobs still run — if isolation isn't there, order your tags so the critical jobs are least likely to be starved, or run critical jobs in their own category.
Not a cron-bundle replacement — don't expect the features. No admin UI, no job history, no retries, no per-job output capture, no "last run" state. If you need visibility, wrap the command with your own logging listener or emit metrics from inside the job.
Don't double-schedule. If you already use symfony/scheduler or Messenger's Schedule attributes, pick one mechanism per job. Two schedulers firing the same command is the classic post-migration outage.
Extension points. The runner is a normal Symfony command and the collection is built from tagged services, so:
ibexa:cron:run command to add timing/logging around invocation;License and maintenance risk. GitHub reports NOASSERTION, which means the license wasn't auto-detected — open the LICENSE file and confirm it's acceptable to your legal team before shipping (Ibexa packages are typically GPL-family, which matters for proprietary distributions). Combined with 3 stars and zero known dependents, treat this as thin, unproven infrastructure: pin an exact version in composer.json, read the source of the version you pin, and keep a documented fallback (plain crontab lines, or symfony/scheduler) so you can migrate in an afternoon if it's abandoned.
How can I help you explore Laravel packages today?