validate(); } public static function fromString(string $expression): self { return new self(trim($expression)); } public static function everyMinute(): self { return new self('* * * * *'); } public static function hourly(): self { return new self('0 * * * *'); } public static function daily(): self { return new self('0 0 * * *'); } public static function weekly(): self { return new self('0 0 * * 0'); } public static function monthly(): self { return new self('0 0 1 * *'); } public static function yearly(): self { return new self('0 0 1 1 *'); } public static function at(int $hour, int $minute = 0): self { if ($hour < 0 || $hour > 23) { throw new \InvalidArgumentException('Hour must be between 0 and 23'); } if ($minute < 0 || $minute > 59) { throw new \InvalidArgumentException('Minute must be between 0 and 59'); } return new self("{$minute} {$hour} * * *"); } public static function weekdaysAt(int $hour, int $minute = 0): self { if ($hour < 0 || $hour > 23) { throw new \InvalidArgumentException('Hour must be between 0 and 23'); } if ($minute < 0 || $minute > 59) { throw new \InvalidArgumentException('Minute must be between 0 and 59'); } return new self("{$minute} {$hour} * * 1-5"); } public function toString(): string { return $this->expression; } public function equals(self $other): bool { return $this->expression === $other->expression; } public function getParts(): array { return explode(' ', $this->expression); } public function getMinute(): string { return $this->getParts()[0]; } public function getHour(): string { return $this->getParts()[1]; } public function getDayOfMonth(): string { return $this->getParts()[2]; } public function getMonth(): string { return $this->getParts()[3]; } public function getDayOfWeek(): string { return $this->getParts()[4]; } public function getDescription(): string { return match ($this->expression) { '* * * * *' => 'Every minute', '0 * * * *' => 'Every hour', '0 0 * * *' => 'Daily at midnight', '0 0 * * 0' => 'Weekly on Sunday at midnight', '0 0 1 * *' => 'Monthly on the 1st at midnight', '0 0 1 1 *' => 'Yearly on January 1st at midnight', default => "Cron: {$this->expression}" }; } private function validate(): void { if (empty($this->expression)) { throw new \InvalidArgumentException('Cron expression cannot be empty'); } $parts = explode(' ', $this->expression); if (count($parts) !== 5) { throw new \InvalidArgumentException( 'Cron expression must have exactly 5 parts (minute hour day month weekday)' ); } [$minute, $hour, $day, $month, $weekday] = $parts; $this->validateField($minute, 0, 59, 'minute'); $this->validateField($hour, 0, 23, 'hour'); $this->validateField($day, 1, 31, 'day'); $this->validateField($month, 1, 12, 'month'); $this->validateField($weekday, 0, 7, 'weekday'); // 0 and 7 both represent Sunday } private function validateField(string $field, int $min, int $max, string $fieldName): void { // Allow wildcards if ($field === '*') { return; } // Handle ranges (e.g., 1-5) if (str_contains($field, '-')) { $range = explode('-', $field); if (count($range) !== 2) { throw new \InvalidArgumentException("Invalid range in {$fieldName}: {$field}"); } $start = (int) $range[0]; $end = (int) $range[1]; if ($start < $min || $start > $max || $end < $min || $end > $max || $start > $end) { throw new \InvalidArgumentException("Invalid range in {$fieldName}: {$field}"); } return; } // Handle step values (e.g., */5) if (str_contains($field, '/')) { $step = explode('/', $field); if (count($step) !== 2) { throw new \InvalidArgumentException("Invalid step in {$fieldName}: {$field}"); } $stepValue = (int) $step[1]; if ($stepValue <= 0) { throw new \InvalidArgumentException("Step value must be positive in {$fieldName}: {$field}"); } // Validate base (e.g., the '*' in '*/5') if ($step[0] !== '*') { $this->validateField($step[0], $min, $max, $fieldName); } return; } // Handle comma-separated values (e.g., 1,3,5) if (str_contains($field, ',')) { $values = explode(',', $field); foreach ($values as $value) { $this->validateField(trim($value), $min, $max, $fieldName); } return; } // Single numeric value if (!is_numeric($field)) { throw new \InvalidArgumentException("Invalid {$fieldName} value: {$field}"); } $value = (int) $field; if ($value < $min || $value > $max) { throw new \InvalidArgumentException( "{$fieldName} must be between {$min} and {$max}, got {$value}" ); } // Special case for weekday: both 0 and 7 represent Sunday if ($fieldName === 'weekday' && $value === 7) { return; // 7 is valid for Sunday } } }