WordPress Core

kaiseki/wp-cron

Register and manage WordPress cron jobs declaratively through a single config key.

Describe a cron job as a small class — its hook, its recurrence and the work it does — and register it in the cron.jobs config key. A single kaiseki/wp-hook provider (CronScheduler) wires the action, schedules the event, reschedules it when you change the recurrence, registers custom intervals and clears events for jobs you remove — so you never hand-write the wp_next_scheduled() / wp_schedule_event() dance again.

Installation

composer require kaiseki/wp-cron

Requires PHP 8.2 or newer.

Usage

Register ConfigProvider with your laminas-style config aggregator. That alone activates the scheduler (it adds CronScheduler to hook.provider for you); you only supply the jobs.

1. Write a cron job

Implement CronJobInterface. Return a Recurrence case for the built-in WordPress schedules:

use Kaiseki\WordPress\Cron\CronJobInterface;
use Kaiseki\WordPress\Cron\Recurrence;
use Kaiseki\WordPress\Cron\ScheduleInterface;

final class PublishScheduledPosts implements CronJobInterface
{
    public function getHook(): string
    {
        return 'acme_publish_scheduled_posts';
    }

    public function getRecurrence(): ScheduleInterface
    {
        return Recurrence::Hourly;
    }

    public function run(): void
    {
        // … the work that runs on every tick.
    }
}

Recurrence is a backed enum of the WordPress defaults — Hourly, TwiceDaily, Daily, Weekly — so you do not hard-code the magic strings.

2. Register it

use Kaiseki\WordPress\Cron\ConfigProvider;

return [
    'cron' => [
        'jobs' => [
            PublishScheduledPosts::class,
        ],
    ],
];

That is the whole contract. On the next request the scheduler:

  • adds add_action('acme_publish_scheduled_posts', [$job, 'run']),
  • schedules the recurring event if it is not already scheduled,
  • reschedules it if you later change getRecurrence() (WordPress keeps the old interval otherwise), and
  • unschedules the event if you remove the class from cron.jobs.

Jobs are resolved from your PSR-11 container, so constructor dependencies (loggers, repositories, the environment) are injected as usual — register each job class with the container as you would any other service.

Custom intervals

WordPress only ships hourly, twicedaily, daily and weekly. For anything else, return a CustomScheduleInterface from the job. It carries its own interval and label, and the scheduler registers it with WordPress (via the cron_schedules filter) before scheduling the event — there is no separate registration step and no recurrence name to keep in sync.

The quickest way is the ready-made IntervalSchedule value object:

use Kaiseki\WordPress\Cron\CronJobInterface;
use Kaiseki\WordPress\Cron\IntervalSchedule;
use Kaiseki\WordPress\Cron\ScheduleInterface;

final class HealthCheck implements CronJobInterface
{
    public function getHook(): string
    {
        return 'acme_health_check';
    }

    public function getRecurrence(): ScheduleInterface
    {
        return new IntervalSchedule('acme_every_five_minutes', 5 * 60, 'Every five minutes');
    }

    public function run(): void
    {
        // …
    }
}

Registering the job in cron.jobs is all that is needed — the schedule comes with it:

return [
    'cron' => [
        'jobs' => [
            HealthCheck::class,
        ],
    ],
];

If a schedule is shared across several jobs, implement CustomScheduleInterface on a dedicated class (or expose a shared IntervalSchedule instance) instead of repeating the literal — any job that returns it registers it, and the scheduler de-duplicates by name.

What this replaces

The hand-written pattern repeated in every theme’s cron class:

public function addHooks(): void
{
    add_action(self::HOOK, [$this, 'run'], 99);
    if (wp_next_scheduled(self::HOOK) !== false) {
        return;
    }
    wp_schedule_event(time(), 'hourly', self::HOOK);
}

collapses to a getHook() / getRecurrence() / run() triple — and you additionally get reschedule-on-change and cleanup-on-removal, which the snippet above silently gets wrong (changing 'hourly' to 'daily' there leaves the old hourly event running forever).

Development

composer install
composer check   # check-deps, cs-check, phpstan, phpunit

Tests use Brain Monkey to mock the WordPress functions, so no WordPress install is needed — run them on their own with composer phpunit.

License

MIT — see LICENSE.

Previous
kaiseki/wp-context