abc/scheduler
Experimental PHP scheduler library for running jobs based on CRON expressions. Define schedule providers and processors via simple interfaces, bind them in a Scheduler, and execute due schedules with an included Symfony Console command (abc:schedule).
Install the package:
composer require abc/scheduler
Create a custom provider (e.g., app/Providers/SchedulerProvider.php):
use Abc\Scheduler\ProviderInterface;
use Abc\Scheduler\ScheduleInterface;
class SchedulerProvider implements ProviderInterface
{
public function getName(): string { return 'laravel'; }
public function provideSchedules(int $limit = null, int $offset = null): array
{
return [
new class implements ScheduleInterface {
public function getCron(): string { return '* * * * *'; }
public function getName(): string { return 'test-schedule'; }
public function getCallback(): string { return 'App\Jobs\TestJob::dispatch'; }
}
];
}
public function save(ScheduleInterface $schedule): void { /* Implement if needed */ }
}
Create a custom processor (e.g., app/Services/SchedulerProcessor.php):
use Abc\Scheduler\ProcessorInterface;
use Abc\Scheduler\ScheduleInterface;
class SchedulerProcessor implements ProcessorInterface
{
public function process(ScheduleInterface $schedule)
{
$callback = $schedule->getCallback();
if (class_exists($callback)) {
eval("{$callback}"); // Or use Laravel's job dispatcher
}
}
}
Bind in a service provider (e.g., AppServiceProvider):
use Abc\Scheduler\Scheduler;
use Abc\Scheduler\Symfony\ScheduleCommand;
public function register()
{
$scheduler = new Scheduler();
$scheduler->bind(new SchedulerProvider(), new SchedulerProcessor());
$this->app->singleton('scheduler', fn() => $scheduler);
$this->app->singleton(ScheduleCommand::class, fn($app) =>
new ScheduleCommand($app->make('scheduler'))
);
}
Register the command (e.g., in app/Console/Kernel.php):
protected $commands = [
\Abc\Scheduler\Symfony\ScheduleCommand::class,
];
Run the scheduler (via cron or Laravel's task scheduler):
php artisan abc:schedule
Laravel Task Scheduling:
Use Laravel's built-in scheduler (php artisan schedule:run) to trigger abc:schedule:
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->command('abc:schedule')->everyMinute();
}
Dynamic Schedule Management:
Store schedules in a database (e.g., schedules table) and fetch them in provideSchedules():
public function provideSchedules(int $limit = null, int $offset = null): array
{
return DB::table('schedules')
->limit($limit)
->offset($offset)
->get()
->map(fn($record) => new DatabaseSchedule($record));
}
Job Dispatching:
Replace eval in the processor with Laravel's job system:
public function process(ScheduleInterface $schedule)
{
dispatch(new ($schedule->getCallback()));
}
Logging and Retries: Extend the processor to log failures and retry jobs:
public function process(ScheduleInterface $schedule)
{
try {
dispatch(new ($schedule->getCallback()));
} catch (\Throwable $e) {
Log::error("Schedule {$schedule->getName()} failed: " . $e->getMessage());
// Implement retry logic (e.g., via Laravel's queue)
}
}
Environment-Specific Schedules: Load schedules conditionally based on the environment:
public function provideSchedules(int $limit = null, int $offset = null): array
{
$config = config("scheduler.{$this->app->environment()}");
return collect($config)->map(fn($cron, $name) =>
new CronSchedule($name, $cron, 'App\Jobs\\' . $name . 'Job')
)->toArray();
}
Cron Parsing Issues:
@yearly), consider validating input or using a dedicated library like drushin/cron-expression.Race Conditions:
abc:schedule simultaneously, schedules may execute out of order. Use Laravel's queue system to serialize execution:
public function process(ScheduleInterface $schedule)
{
dispatchSync(new ProcessScheduleJob($schedule));
}
Callback Security:
eval in production. Instead, use Laravel's job system or a whitelist of allowed callbacks.Time Zone Handling:
.env:
APP_TIMEZONE=UTC
Database Locking:
public function save(ScheduleInterface $schedule): void
{
DB::table('schedules')->where('name', $schedule->getName())->lockForUpdate();
// Save logic
}
Log Schedule Execution: Add logging to the processor:
public function process(ScheduleInterface $schedule)
{
Log::info("Executing schedule: {$schedule->getName()}");
// Process logic
}
Dry Run Mode: Extend the command to simulate execution without running callbacks:
// In ScheduleCommand
protected function execute(InputInterface $input, OutputInterface $output)
{
if ($input->getOption('dry-run')) {
$this->scheduler->dryRun($output);
return;
}
// Normal execution
}
Test Cron Expressions: Use a cron validator like crontab.guru to verify expressions before implementation.
Custom Schedule Types:
Extend ScheduleInterface to support additional fields (e.g., getMaxRetries()):
interface ScheduleInterface {
// ... existing methods
public function getMaxRetries(): int;
}
Event Dispatching: Trigger Laravel events before/after processing:
public function process(ScheduleInterface $schedule)
{
event(new ScheduleProcessing($schedule));
// Process logic
event(new ScheduleProcessed($schedule));
}
Rate Limiting: Implement a rate limiter in the processor to avoid overloading the system:
use Symfony\Component\RateLimiter\RateLimiterInterface;
public function __construct(private RateLimiterInterface $limiter) {}
public function process(ScheduleInterface $schedule)
{
if (!$this->limiter->consume($schedule->getName())) {
Log::warning("Rate limit exceeded for {$schedule->getName()}");
return;
}
// Process logic
}
Webhook Notifications: Add a webhook endpoint to notify external systems when schedules run:
Route::post('/scheduler/webhook', function (Request $request) {
$schedule = $request->schedule;
// Notify external service
});
How can I help you explore Laravel packages today?