Files
michaelschiemer/src/Framework/Scheduler/ValueObjects/CronExpression.php
Michael Schiemer 5050c7d73a docs: consolidate documentation into organized structure
- 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
2025-10-05 11:05:04 +02:00

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
}
}
}