- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
221 lines
6.0 KiB
PHP
221 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Scheduler\ValueObjects;
|
|
|
|
/**
|
|
* Value Object for cron expressions with validation
|
|
*/
|
|
final readonly class CronExpression
|
|
{
|
|
public function __construct(
|
|
public string $expression
|
|
) {
|
|
$this->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
|
|
}
|
|
}
|
|
} |