diff --git a/README.md b/README.md index 6a641b2..ed506df 100644 --- a/README.md +++ b/README.md @@ -344,6 +344,8 @@ Schedule::driver('custom', CustomSchedule::class); Each driver exposes a static `tap` method to configure the underlying library instance before it's used: +Callbacks are scoped to the concrete driver class. Configuring or clearing one driver's callback does not affect other drivers or subclasses. + ```php use Cron\CronExpression; use DirectoryTree\Cadence\Drivers\CronSchedule; diff --git a/src/Drivers/CronSchedule.php b/src/Drivers/CronSchedule.php index 070a1dc..b65124d 100644 --- a/src/Drivers/CronSchedule.php +++ b/src/Drivers/CronSchedule.php @@ -15,8 +15,8 @@ protected function resolveNextOccurrence(CarbonInterface $after): ?CarbonInterfa { $cron = new CronExpression($this->expression); - if (static::$tapUsing) { - (static::$tapUsing)($cron, $this); + if ($callback = static::$tapCallbacks[static::class] ?? null) { + $callback($cron, $this); } return Carbon::instance($cron->getNextRunDate($after)); diff --git a/src/Drivers/RecurrSchedule.php b/src/Drivers/RecurrSchedule.php index 3ba168e..e63171e 100644 --- a/src/Drivers/RecurrSchedule.php +++ b/src/Drivers/RecurrSchedule.php @@ -36,8 +36,8 @@ protected function resolveNextOccurrence(CarbonInterface $after): ?CarbonInterfa $transformer = $this->newTransformer(); - if (static::$tapUsing) { - (static::$tapUsing)($rule, $transformer, $this); + if ($callback = static::$tapCallbacks[static::class] ?? null) { + $callback($rule, $transformer, $this); } $recurrences = $transformer->transform($rule, $this->newConstraint($after, $rule), false); diff --git a/src/Drivers/RruleSchedule.php b/src/Drivers/RruleSchedule.php index 6be6f49..75db91f 100644 --- a/src/Drivers/RruleSchedule.php +++ b/src/Drivers/RruleSchedule.php @@ -30,8 +30,8 @@ protected function resolveNextOccurrence(CarbonInterface $after): ?CarbonInterfa { $rrule = new RRule($this->toRfcString()); - if (static::$tapUsing) { - (static::$tapUsing)($rrule); + if ($callback = static::$tapCallbacks[static::class] ?? null) { + $callback($rrule); } $occurrences = $rrule->getOccurrencesAfter($after, false, 1); diff --git a/src/Drivers/Schedule.php b/src/Drivers/Schedule.php index 2bf0937..d11b55a 100644 --- a/src/Drivers/Schedule.php +++ b/src/Drivers/Schedule.php @@ -18,9 +18,11 @@ abstract class Schedule implements ScheduleDriver protected ?string $timezone = null; /** - * The tap callback for configuring the underlying implementation. + * The tap callbacks for configuring each driver's underlying implementation. + * + * @var array */ - protected static ?Closure $tapUsing = null; + protected static array $tapCallbacks = []; /** * Create a new schedule instance. @@ -36,7 +38,11 @@ public function __construct(string $expression, ?string $timezone = null) */ public static function tap(?Closure $callback): void { - static::$tapUsing = $callback; + if ($callback === null) { + unset(static::$tapCallbacks[static::class]); + } else { + static::$tapCallbacks[static::class] = $callback; + } } /** diff --git a/tests/Drivers/DriverTapTest.php b/tests/Drivers/DriverTapTest.php new file mode 100644 index 0000000..a5ff7ab --- /dev/null +++ b/tests/Drivers/DriverTapTest.php @@ -0,0 +1,104 @@ +getNextOccurrence(now()); + (new RruleSchedule('FREQ=DAILY;DTSTART=20260501T090000'))->getNextOccurrence(now()); + (new RecurrSchedule('FREQ=DAILY;DTSTART=20260501T090000'))->getNextOccurrence(now()); + + expect($called)->toBe([CronSchedule::class, RruleSchedule::class, RecurrSchedule::class]); +}); + +it('does not invoke a callback registered for another driver', function () { + Carbon::setTestNow('2026-05-02 08:00:00'); + $called = false; + + RecurrSchedule::tap(function (Rule $rule, ArrayTransformer $transformer) use (&$called) { + $called = true; + }); + + (new CronSchedule('0 9 * * *'))->getNextOccurrence(now()); + (new RruleSchedule('FREQ=DAILY;DTSTART=20260501T090000'))->getNextOccurrence(now()); + + expect($called)->toBeFalse(); +}); + +it('clears only the callback for the requested driver', function ($cleared) { + Carbon::setTestNow('2026-05-02 08:00:00'); + $called = []; + $drivers = [ + new CronSchedule('0 9 * * *'), + new RruleSchedule('FREQ=DAILY;DTSTART=20260501T090000'), + new RecurrSchedule('FREQ=DAILY;DTSTART=20260501T090000'), + ]; + + foreach ($drivers as $driver) { + $driver::tap(function () use (&$called, $driver) { + $called[] = $driver::class; + }); + } + + $cleared::tap(null); + + foreach ($drivers as $driver) { + $driver->getNextOccurrence(now()); + } + + expect($called)->toBe(array_values(array_diff([ + CronSchedule::class, RruleSchedule::class, RecurrSchedule::class, + ], [$cleared]))); +})->with([CronSchedule::class, RruleSchedule::class, RecurrSchedule::class]); + +it('keeps subclass callbacks separate from their parent driver', function () { + Carbon::setTestNow('2026-05-02 08:00:00'); + $called = []; + $custom = new class('0 9 * * *') extends CronSchedule {}; + + CronSchedule::tap(function (CronExpression $cron, CronSchedule $driver) use (&$called) { + $called[] = $driver::class; + }); + + $custom::tap(function (CronExpression $cron, CronSchedule $driver) use (&$called) { + $called[] = $driver::class; + }); + + try { + (new CronSchedule('0 9 * * *'))->getNextOccurrence(now()); + $custom->getNextOccurrence(now()); + $custom::tap(null); + (new CronSchedule('0 9 * * *'))->getNextOccurrence(now()); + + expect($called)->toBe([CronSchedule::class, $custom::class, CronSchedule::class]); + } finally { + $custom::tap(null); + } +});