daily() * ->register(); * * Call ::flushIntervals() from a cron_schedules filter is handled automatically. * Unschedule with Scheduler::forget('softminal_cache_daily_cleanup'). * * PHP 7.4 compatible. */ class Scheduler { private string $hook; /** @var callable|null */ private $callback; private string $recurrence = 'daily'; private ?int $firstRun = null; /** @var array */ private array $args = []; /** Custom intervals to inject into cron_schedules, keyed by name. */ private static array $customIntervals = []; private static bool $filterBound = false; private function __construct(string $hook, ?callable $callback = null) { $this->hook = $hook; $this->callback = $callback; self::bindIntervalFilter(); } public static function event(string $hook, ?callable $callback = null): self { return new self($hook, $callback); } /* ----- Recurrence helpers ----- */ public function hourly(): self { return $this->recurrence('hourly'); } public function twiceDaily(): self { return $this->recurrence('twicedaily'); } public function daily(): self { return $this->recurrence('daily'); } public function weekly(): self { self::registerInterval('weekly', WEEK_IN_SECONDS, 'Once Weekly'); return $this->recurrence('weekly'); } /** * A custom recurrence in seconds. Registers the interval on the fly. */ public function everySeconds(int $seconds, ?string $name = null): self { $name = $name ?? 'softminal_cache_every_' . $seconds . 's'; self::registerInterval($name, $seconds, 'Every ' . $seconds . ' seconds'); return $this->recurrence($name); } public function everyMinutes(int $minutes): self { return $this->everySeconds($minutes * MINUTE_IN_SECONDS, 'softminal_cache_every_' . $minutes . 'min'); } public function recurrence(string $recurrence): self { $this->recurrence = $recurrence; return $this; } public function startingAt(int $timestamp): self { $this->firstRun = $timestamp; return $this; } /** * @param array $args */ public function withArgs(array $args): self { $this->args = $args; return $this; } /** * Bind the callback and ensure the recurring event is scheduled. */ public function register(): self { if ($this->callback !== null) { add_action($this->hook, $this->callback); } if (!wp_next_scheduled($this->hook, $this->args)) { $result = wp_schedule_event( $this->firstRun ?? time(), $this->recurrence, $this->hook, $this->args ); if ($result === false || is_wp_error($result)) { do_action('softminal_cache_scheduler_error', $this->hook, $this->recurrence); } } return $this; } /** * Unschedule all occurrences of a hook. */ public static function forget(string $hook, array $args = []): void { $timestamp = wp_next_scheduled($hook, $args); while ($timestamp) { wp_unschedule_event($timestamp, $hook, $args); $timestamp = wp_next_scheduled($hook, $args); } if (function_exists('wp_clear_scheduled_hook')) { wp_clear_scheduled_hook($hook, $args); } } /** * Register a one-off event to run once at (or after) a timestamp. */ public static function once(string $hook, int $timestamp, array $args = []): void { if (!wp_next_scheduled($hook, $args)) { wp_schedule_single_event($timestamp, $hook, $args); } } /** * Register a custom cron interval (added via the cron_schedules filter). */ public static function registerInterval(string $name, int $intervalSeconds, string $display): void { self::$customIntervals[$name] = [ 'interval' => $intervalSeconds, 'display' => $display, ]; self::bindIntervalFilter(); } private static function bindIntervalFilter(): void { if (self::$filterBound) { return; } self::$filterBound = true; add_filter('cron_schedules', function ($schedules) { foreach (self::$customIntervals as $name => $config) { $schedules[$name] = $config; } return $schedules; }); } }