indatus/dispatcher
Indatus Dispatcher is a Laravel package for scheduling Artisan commands and queued jobs with a fluent API. Define cron-style schedules, manage overlaps, and log or email command output, offering a flexible alternative to Laravel's built-in scheduler.
Install the package and wire it into the framework:
composer require indatus/dispatcher:~1.0
Laravel 4 — add to app/config/app.php:
'Indatus\Dispatcher\ServiceProvider',
'Dispatcher' => 'Indatus\Dispatcher\Facades\Dispatcher',
Laravel 5 — add to config/app.php the same provider and alias, then publish and run:
php artisan config:publish indatus/dispatcher # L4
php artisan vendor:publish --provider="Indatus\Dispatcher\ServiceProvider"
php artisan migrate --package=indatus/dispatcher # or --path for published migrations
The one thing that makes Dispatcher "work" is a single crontab entry, because the package evaluates schedules but does not tick itself:
* * * * * php /var/www/myapp/artisan scheduled:run >> /dev/null 2>&1
First use case: scaffold a scheduled command and confirm it is registered.
php artisan scheduled:make BackupDatabase
php artisan scheduled:list
php artisan scheduled:run # run once manually to verify evaluation
scheduled:make drops a class into app/Console/Scheduled/ (path configurable in config/dispatcher.php). That directory is the primary place to look first — everything else (cron, DB registry, drivers) exists to serve the classes living there.
Fluent schedule definition inside a command class. Each scheduled class extends the package's base and returns a Schedulable:
use Indatus\Dispatcher\Scheduling\ScheduledCommand;
use Indatus\Dispatcher\Scheduling\Schedulable;
class BackupDatabase extends ScheduledCommand
{
protected $name = 'db:backup';
protected $description = 'Nightly database dump';
public function schedule(Schedulable $scheduler)
{
return $scheduler
->daily()
->at(2, 30)
->args(['--compress'])
->runInBackground();
}
}
The fluent vocabulary mirrors cron semantics — hourly(), daily(), weekly(), monthly(), at($hour, $minute), and raw cron('*/15 * * * *') when the helpers don't express what you need. Prefer cron() for anything non-obvious (business-hours-only, "every 5 minutes but not at :00") rather than nesting helpers.
Ad-hoc scheduling from a service provider. For one-off invocations that don't deserve a class, register the artisan command directly:
Dispatcher::schedule('cache:warm')
->daily()
->at(5, 0);
This is the fastest path when migrating an existing crontab line-by-line: one provider method, one Dispatcher::schedule() call per line, and you can delete the crontab entries as you go.
Two drivers, one mental model. config/dispatcher.php selects local or remote. Local runs commands in-process on the web/app host; remote shells out over SSH to another box. The remote driver is the reason teams picked this over raw cron: you can keep all schedule logic in version control on the app server while the actual command executes on a database or worker host. Treat the remote config block as production-critical and diff it per environment.
Background vs foreground. runInBackground() is what you want for anything longer than a few seconds, otherwise scheduled:run (ticking every minute) blocks and subsequent due commands slip. Reserve foreground execution for quick, deterministic commands where you actually want the output captured.
Overlap protection. Long-running jobs need a guard so a slow run doesn't stack on the next tick. Use the package's mutex/locking facility (backed by the published migration) or wrap the inner command in your own lock. This is not optional for anything touching backups, billing, or external APIs with rate limits.
Environment scoping. Wrap registration in an environment check in the provider (if (App::environment('production'))) rather than relying on the schedule itself. It reads better and keeps dev machines from firing production side-effects when someone runs scheduled:run locally.
scheduled:run is not a daemon. It is a one-shot evaluator designed to be invoked every minute by cron. If you run it in a while true loop you will get drift and duplicate launches.php binary in cron. The classic failure is php: command not found under cron's minimal PATH. Use /usr/bin/php or your PHP-FPM's binary explicitly.$schedule->command() in app/Console/Kernel.php. Keep Dispatcher only for legacy Laravel 4.x / early 5.x apps where rewriting the schedule is not worth the risk.composer.json, and expect to patch it yourself if a PHP or framework upgrade breaks reflection or the SSH driver.scheduled:list reads from the package's table, not from disk. Rename or delete a scheduled class without running scheduled:remove and you get phantom entries that error every minute in the logs. After any refactor, run scheduled:list and prune.php artisan config:cache snapshots dispatcher.php. Changing the driver or remote host requires a re-cache — otherwise you'll swear the config change "didn't apply."/dev/null, so a failing command produces nothing. Always send output somewhere (>> /var/log/dispatcher.log 2>&1) and have the underlying command log its own start/finish/exit code. Debugging a schedule that "didn't run" without logs is guesswork.config/app.php says UTC but you're reasoning in local time, ->at(2, 30) fires at the wrong hour. Verify with scheduled:list which surfaces the resolved next-run time.runInBackground() gives you parallelism you probably don't want for the same command. Pair it with the mutex, or your nightly job will eventually collide with itself after a slow run.Schedulable. If you need custom logic (e.g. "only on the first business day"), subclass or compose around the scheduler rather than fighting the fluent API — that's the seam the package expects you to use.How can I help you explore Laravel packages today?