Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
CronExpressionEvaluator
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
2 / 2
15
100.00% covered (success)
100.00%
1 / 1
 isDue
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 matchPart
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Cron;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11/**
12 * Standard 5-Part Cron Expression Evaluator.
13 *
14 * Evaluates cron schedule expressions (* * * * *) against a given Unix timestamp.
15 *
16 * @package App\Core\Cron
17 */
18final readonly class CronExpressionEvaluator
19{
20    /**
21     * Evaluates whether cron expression matches current timestamp.
22     *
23     * @param string $expression 5-part cron expression string.
24     * @param int|null $timestamp Optional Unix timestamp to test against (defaults to current time).
25     * @return bool True if cron expression matches timestamp.
26     */
27    public function isDue(string $expression, ?int $timestamp = null): bool
28    {
29        $time = $timestamp ?? time();
30        $parts = array_values(array_filter(explode(' ', trim($expression)), fn($p) => $p !== ''));
31
32        if (count($parts) !== 5) {
33            return false;
34        }
35
36        $minute = (int)date('i', $time);
37        $hour = (int)date('G', $time);
38        $dayOfMonth = (int)date('j', $time);
39        $month = (int)date('n', $time);
40        $dayOfWeek = (int)date('w', $time);
41
42        return $this->matchPart($parts[0], $minute, 0, 59)
43            && $this->matchPart($parts[1], $hour, 0, 23)
44            && $this->matchPart($parts[2], $dayOfMonth, 1, 31)
45            && $this->matchPart($parts[3], $month, 1, 12)
46            && $this->matchPart($parts[4], $dayOfWeek, 0, 6);
47    }
48
49    /**
50     * Matches single cron field part.
51     *
52     * @param string $part Cron field value string.
53     * @param int $current Current time unit value.
54     * @param int $min Minimum valid value.
55     * @param int $max Maximum valid value.
56     * @return bool True if field matches current value.
57     */
58    private function matchPart(string $part, int $current, int $min, int $max): bool
59    {
60        if ($part === '*') {
61            return true;
62        }
63
64        $matched = false;
65        if (str_contains($part, ',')) {
66            foreach (explode(',', $part) as $subPart) {
67                if ($this->matchPart($subPart, $current, $min, $max)) {
68                    $matched = true;
69                    break;
70                }
71            }
72        } elseif (str_starts_with($part, '*/')) {
73            $step = (int)substr($part, 2);
74            $matched = $step > 0 && ($current % $step === 0);
75        } elseif (str_contains($part, '-')) {
76            $range = explode('-', $part, 2);
77            $start = (int)$range[0];
78            $end = (int)$range[1];
79            $matched = $current >= $start && $current <= $end;
80        } else {
81            $matched = (int)$part === $current;
82        }
83
84        return $matched;
85    }
86}