first commit
This commit is contained in:
@@ -0,0 +1,577 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Scheduler
|
||||
* @author Originally based on jqCron by Arnaud Buathier <arnaud@arnapou.net> modified for Grav integration
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Scheduler;
|
||||
|
||||
/*
|
||||
* Usage examples :
|
||||
* ----------------
|
||||
*
|
||||
* $cron = new Cron('10-30/5 12 * * *');
|
||||
*
|
||||
* var_dump($cron->getMinutes());
|
||||
* // array(5) {
|
||||
* // [0]=> int(10)
|
||||
* // [1]=> int(15)
|
||||
* // [2]=> int(20)
|
||||
* // [3]=> int(25)
|
||||
* // [4]=> int(30)
|
||||
* // }
|
||||
*
|
||||
* var_dump($cron->getText('fr'));
|
||||
* // string(32) "Chaque jour à 12:10,15,20,25,30"
|
||||
*
|
||||
* var_dump($cron->getText('en'));
|
||||
* // string(30) "Every day at 12:10,15,20,25,30"
|
||||
*
|
||||
* var_dump($cron->getType());
|
||||
* // string(3) "day"
|
||||
*
|
||||
* var_dump($cron->getCronHours());
|
||||
* // string(2) "12"
|
||||
*
|
||||
* var_dump($cron->matchExact(new \DateTime('2012-07-01 13:25:10')));
|
||||
* // bool(false)
|
||||
*
|
||||
* var_dump($cron->matchExact(new \DateTime('2012-07-01 12:15:20')));
|
||||
* // bool(true)
|
||||
*
|
||||
* var_dump($cron->matchWithMargin(new \DateTime('2012-07-01 12:32:50'), -3, 5));
|
||||
* // bool(true)
|
||||
*/
|
||||
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use RuntimeException;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
class Cron
|
||||
{
|
||||
public const TYPE_UNDEFINED = '';
|
||||
public const TYPE_MINUTE = 'minute';
|
||||
public const TYPE_HOUR = 'hour';
|
||||
public const TYPE_DAY = 'day';
|
||||
public const TYPE_WEEK = 'week';
|
||||
public const TYPE_MONTH = 'month';
|
||||
public const TYPE_YEAR = 'year';
|
||||
/**
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $texts = [
|
||||
'fr' => [
|
||||
'empty' => '-tout-',
|
||||
'name_minute' => 'minute',
|
||||
'name_hour' => 'heure',
|
||||
'name_day' => 'jour',
|
||||
'name_week' => 'semaine',
|
||||
'name_month' => 'mois',
|
||||
'name_year' => 'année',
|
||||
'text_period' => 'Chaque %s',
|
||||
'text_mins' => 'à %s minutes',
|
||||
'text_time' => 'à %02s:%02s',
|
||||
'text_dow' => 'le %s',
|
||||
'text_month' => 'de %s',
|
||||
'text_dom' => 'le %s',
|
||||
'weekdays' => ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'],
|
||||
'months' => ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
|
||||
],
|
||||
'en' => [
|
||||
'empty' => '-all-',
|
||||
'name_minute' => 'minute',
|
||||
'name_hour' => 'hour',
|
||||
'name_day' => 'day',
|
||||
'name_week' => 'week',
|
||||
'name_month' => 'month',
|
||||
'name_year' => 'year',
|
||||
'text_period' => 'Every %s',
|
||||
'text_mins' => 'at %s minutes past the hour',
|
||||
'text_time' => 'at %02s:%02s',
|
||||
'text_dow' => 'on %s',
|
||||
'text_month' => 'of %s',
|
||||
'text_dom' => 'on the %s',
|
||||
'weekdays' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
|
||||
'months' => ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* min hour dom month dow
|
||||
* @var string
|
||||
*/
|
||||
protected $cron = '';
|
||||
/**
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $minutes = [];
|
||||
/**
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hours = [];
|
||||
/**
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $months = [];
|
||||
/**
|
||||
* 0-7 : sunday, monday, ... saturday, sunday
|
||||
* @var array
|
||||
*/
|
||||
protected $dow = [];
|
||||
/**
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dom = [];
|
||||
|
||||
/**
|
||||
* @param string|null $cron
|
||||
*/
|
||||
public function __construct($cron = null)
|
||||
{
|
||||
if (null !== $cron) {
|
||||
$this->setCron($cron);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCron()
|
||||
{
|
||||
return implode(' ', [
|
||||
$this->getCronMinutes(),
|
||||
$this->getCronHours(),
|
||||
$this->getCronDaysOfMonth(),
|
||||
$this->getCronMonths(),
|
||||
$this->getCronDaysOfWeek(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $lang 'fr' or 'en'
|
||||
* @return string
|
||||
*/
|
||||
public function getText($lang)
|
||||
{
|
||||
// check lang
|
||||
if (!isset($this->texts[$lang])) {
|
||||
return $this->getCron();
|
||||
}
|
||||
|
||||
$texts = $this->texts[$lang];
|
||||
// check type
|
||||
|
||||
$type = $this->getType();
|
||||
if ($type === self::TYPE_UNDEFINED) {
|
||||
return $this->getCron();
|
||||
}
|
||||
|
||||
// init
|
||||
$elements = [];
|
||||
$elements[] = sprintf($texts['text_period'], $texts['name_' . $type]);
|
||||
|
||||
// hour
|
||||
if ($type === self::TYPE_HOUR) {
|
||||
$elements[] = sprintf($texts['text_mins'], $this->getCronMinutes());
|
||||
}
|
||||
|
||||
// week
|
||||
if ($type === self::TYPE_WEEK) {
|
||||
$dow = $this->getCronDaysOfWeek();
|
||||
foreach ($texts['weekdays'] as $i => $wd) {
|
||||
$dow = str_replace((string) ($i + 1), $wd, $dow);
|
||||
}
|
||||
$elements[] = sprintf($texts['text_dow'], $dow);
|
||||
}
|
||||
|
||||
// month + year
|
||||
if (in_array($type, [self::TYPE_MONTH, self::TYPE_YEAR], true)) {
|
||||
$elements[] = sprintf($texts['text_dom'], $this->getCronDaysOfMonth());
|
||||
}
|
||||
|
||||
// year
|
||||
if ($type === self::TYPE_YEAR) {
|
||||
$months = $this->getCronMonths();
|
||||
for ($i = count($texts['months']) - 1; $i >= 0; $i--) {
|
||||
$months = str_replace((string) ($i + 1), $texts['months'][$i], $months);
|
||||
}
|
||||
$elements[] = sprintf($texts['text_month'], $months);
|
||||
}
|
||||
|
||||
// day + week + month + year
|
||||
if (in_array($type, [self::TYPE_DAY, self::TYPE_WEEK, self::TYPE_MONTH, self::TYPE_YEAR], true)) {
|
||||
$elements[] = sprintf($texts['text_time'], $this->getCronHours(), $this->getCronMinutes());
|
||||
}
|
||||
|
||||
return str_replace('*', $texts['empty'], implode(' ', $elements));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
$mask = preg_replace('/[^\* ]/', '-', $this->getCron());
|
||||
$mask = preg_replace('/-+/', '-', $mask);
|
||||
$mask = preg_replace('/[^-\*]/', '', $mask);
|
||||
|
||||
if ($mask === '*****') {
|
||||
return self::TYPE_MINUTE;
|
||||
}
|
||||
|
||||
if ($mask === '-****') {
|
||||
return self::TYPE_HOUR;
|
||||
}
|
||||
|
||||
if (substr($mask, -3) === '***') {
|
||||
return self::TYPE_DAY;
|
||||
}
|
||||
|
||||
if (substr($mask, -3) === '-**') {
|
||||
return self::TYPE_MONTH;
|
||||
}
|
||||
|
||||
if (substr($mask, -3) === '**-') {
|
||||
return self::TYPE_WEEK;
|
||||
}
|
||||
|
||||
if (substr($mask, -2) === '-*') {
|
||||
return self::TYPE_YEAR;
|
||||
}
|
||||
|
||||
return self::TYPE_UNDEFINED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $cron
|
||||
* @return $this
|
||||
*/
|
||||
public function setCron($cron)
|
||||
{
|
||||
// sanitize
|
||||
$cron = trim($cron);
|
||||
$cron = preg_replace('/\s+/', ' ', $cron);
|
||||
// explode
|
||||
$elements = explode(' ', $cron);
|
||||
if (count($elements) !== 5) {
|
||||
throw new RuntimeException('Bad number of elements');
|
||||
}
|
||||
|
||||
$this->cron = $cron;
|
||||
$this->setMinutes($elements[0]);
|
||||
$this->setHours($elements[1]);
|
||||
$this->setDaysOfMonth($elements[2]);
|
||||
$this->setMonths($elements[3]);
|
||||
$this->setDaysOfWeek($elements[4]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCronMinutes()
|
||||
{
|
||||
return $this->arrayToCron($this->minutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCronHours()
|
||||
{
|
||||
return $this->arrayToCron($this->hours);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCronDaysOfMonth()
|
||||
{
|
||||
return $this->arrayToCron($this->dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCronMonths()
|
||||
{
|
||||
return $this->arrayToCron($this->months);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCronDaysOfWeek()
|
||||
{
|
||||
return $this->arrayToCron($this->dow);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getMinutes()
|
||||
{
|
||||
return $this->minutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getHours()
|
||||
{
|
||||
return $this->hours;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDaysOfMonth()
|
||||
{
|
||||
return $this->dom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getMonths()
|
||||
{
|
||||
return $this->months;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDaysOfWeek()
|
||||
{
|
||||
return $this->dow;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|string[] $minutes
|
||||
* @return $this
|
||||
*/
|
||||
public function setMinutes($minutes)
|
||||
{
|
||||
$this->minutes = $this->cronToArray($minutes, 0, 59);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|string[] $hours
|
||||
* @return $this
|
||||
*/
|
||||
public function setHours($hours)
|
||||
{
|
||||
$this->hours = $this->cronToArray($hours, 0, 23);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|string[] $months
|
||||
* @return $this
|
||||
*/
|
||||
public function setMonths($months)
|
||||
{
|
||||
$this->months = $this->cronToArray($months, 1, 12);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|string[] $dow
|
||||
* @return $this
|
||||
*/
|
||||
public function setDaysOfWeek($dow)
|
||||
{
|
||||
$this->dow = $this->cronToArray($dow, 0, 7);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|string[] $dom
|
||||
* @return $this
|
||||
*/
|
||||
public function setDaysOfMonth($dom)
|
||||
{
|
||||
$this->dom = $this->cronToArray($dom, 1, 31);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $date
|
||||
* @param int $min
|
||||
* @param int $hour
|
||||
* @param int $day
|
||||
* @param int $month
|
||||
* @param int $weekday
|
||||
* @return DateTime
|
||||
*/
|
||||
protected function parseDate($date, &$min, &$hour, &$day, &$month, &$weekday)
|
||||
{
|
||||
if (is_numeric($date) && (int)$date == $date) {
|
||||
$date = new DateTime('@' . $date);
|
||||
} elseif (is_string($date)) {
|
||||
$date = new DateTime('@' . strtotime($date));
|
||||
}
|
||||
if ($date instanceof DateTime) {
|
||||
$min = (int)$date->format('i');
|
||||
$hour = (int)$date->format('H');
|
||||
$day = (int)$date->format('d');
|
||||
$month = (int)$date->format('m');
|
||||
$weekday = (int)$date->format('w'); // 0-6
|
||||
} else {
|
||||
throw new RuntimeException('Date format not supported');
|
||||
}
|
||||
|
||||
return new DateTime($date->format('Y-m-d H:i:sP'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string|DateTime $date
|
||||
*/
|
||||
public function matchExact($date)
|
||||
{
|
||||
$date = $this->parseDate($date, $min, $hour, $day, $month, $weekday);
|
||||
|
||||
return
|
||||
(empty($this->minutes) || in_array($min, $this->minutes, true)) &&
|
||||
(empty($this->hours) || in_array($hour, $this->hours, true)) &&
|
||||
(empty($this->dom) || in_array($day, $this->dom, true)) &&
|
||||
(empty($this->months) || in_array($month, $this->months, true)) &&
|
||||
(empty($this->dow) || in_array($weekday, $this->dow, true) || ($weekday == 0 && in_array(7, $this->dow, true)) || ($weekday == 7 && in_array(0, $this->dow, true))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string|DateTime $date
|
||||
* @param int $minuteBefore
|
||||
* @param int $minuteAfter
|
||||
*/
|
||||
public function matchWithMargin($date, $minuteBefore = 0, $minuteAfter = 0)
|
||||
{
|
||||
if ($minuteBefore > 0) {
|
||||
throw new RuntimeException('MinuteBefore parameter cannot be positive !');
|
||||
}
|
||||
if ($minuteAfter < 0) {
|
||||
throw new RuntimeException('MinuteAfter parameter cannot be negative !');
|
||||
}
|
||||
|
||||
$date = $this->parseDate($date, $min, $hour, $day, $month, $weekday);
|
||||
$interval = new DateInterval('PT1M'); // 1 min
|
||||
if ($minuteBefore !== 0) {
|
||||
$date->sub(new DateInterval('PT' . abs($minuteBefore) . 'M'));
|
||||
}
|
||||
$n = $minuteAfter - $minuteBefore + 1;
|
||||
for ($i = 0; $i < $n; $i++) {
|
||||
if ($this->matchExact($date)) {
|
||||
return true;
|
||||
}
|
||||
$date->add($interval);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $array
|
||||
* @return string
|
||||
*/
|
||||
protected function arrayToCron($array)
|
||||
{
|
||||
$n = count($array);
|
||||
if (!is_array($array) || $n === 0) {
|
||||
return '*';
|
||||
}
|
||||
|
||||
$cron = [$array[0]];
|
||||
$s = $c = $array[0];
|
||||
for ($i = 1; $i < $n; $i++) {
|
||||
if ($array[$i] == $c + 1) {
|
||||
$c = $array[$i];
|
||||
$cron[count($cron) - 1] = $s . '-' . $c;
|
||||
} else {
|
||||
$s = $c = $array[$i];
|
||||
$cron[] = $c;
|
||||
}
|
||||
}
|
||||
|
||||
return implode(',', $cron);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array|string $string
|
||||
* @param int $min
|
||||
* @param int $max
|
||||
* @return array
|
||||
*/
|
||||
protected function cronToArray($string, $min, $max)
|
||||
{
|
||||
$array = [];
|
||||
if (is_array($string)) {
|
||||
foreach ($string as $val) {
|
||||
if (is_numeric($val) && (int)$val == $val && $val >= $min && $val <= $max) {
|
||||
$array[] = (int)$val;
|
||||
}
|
||||
}
|
||||
} elseif ($string !== '*') {
|
||||
while ($string !== '') {
|
||||
// test "*/n" expression
|
||||
if (preg_match('/^\*\/([0-9]+),?/', $string, $m)) {
|
||||
for ($i = max(0, $min); $i <= min(59, $max); $i += $m[1]) {
|
||||
$array[] = (int)$i;
|
||||
}
|
||||
$string = substr($string, strlen($m[0]));
|
||||
continue;
|
||||
}
|
||||
// test "a-b/n" expression
|
||||
if (preg_match('/^([0-9]+)-([0-9]+)\/([0-9]+),?/', $string, $m)) {
|
||||
for ($i = max($m[1], $min); $i <= min($m[2], $max); $i += $m[3]) {
|
||||
$array[] = (int)$i;
|
||||
}
|
||||
$string = substr($string, strlen($m[0]));
|
||||
continue;
|
||||
}
|
||||
// test "a-b" expression
|
||||
if (preg_match('/^([0-9]+)-([0-9]+),?/', $string, $m)) {
|
||||
for ($i = max($m[1], $min); $i <= min($m[2], $max); $i++) {
|
||||
$array[] = (int)$i;
|
||||
}
|
||||
$string = substr($string, strlen($m[0]));
|
||||
continue;
|
||||
}
|
||||
// test "c" expression
|
||||
if (preg_match('/^([0-9]+),?/', $string, $m)) {
|
||||
if ($m[1] >= $min && $m[1] <= $max) {
|
||||
$array[] = (int)$m[1];
|
||||
}
|
||||
$string = substr($string, strlen($m[0]));
|
||||
continue;
|
||||
}
|
||||
|
||||
// something goes wrong in the expression
|
||||
return [];
|
||||
}
|
||||
}
|
||||
sort($array, SORT_NUMERIC);
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Scheduler
|
||||
* @author Originally based on peppeocchi/php-cron-scheduler modified for Grav integration
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Scheduler;
|
||||
|
||||
use Cron\CronExpression;
|
||||
use InvalidArgumentException;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Trait IntervalTrait
|
||||
* @package Grav\Common\Scheduler
|
||||
*/
|
||||
trait IntervalTrait
|
||||
{
|
||||
/**
|
||||
* Set the Job execution time.
|
||||
*compo
|
||||
* @param string $expression
|
||||
* @return self
|
||||
*/
|
||||
public function at($expression)
|
||||
{
|
||||
$this->at = $expression;
|
||||
$this->executionTime = CronExpression::factory($expression);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every minute.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function everyMinute()
|
||||
{
|
||||
return $this->at('* * * * *');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every hour.
|
||||
*
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function hourly($minute = 0)
|
||||
{
|
||||
$c = $this->validateCronSequence($minute);
|
||||
|
||||
return $this->at("{$c['minute']} * * * *");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to once a day.
|
||||
*
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function daily($hour = 0, $minute = 0)
|
||||
{
|
||||
if (is_string($hour)) {
|
||||
$parts = explode(':', $hour);
|
||||
$hour = $parts[0];
|
||||
$minute = $parts[1] ?? '0';
|
||||
}
|
||||
$c = $this->validateCronSequence($minute, $hour);
|
||||
|
||||
return $this->at("{$c['minute']} {$c['hour']} * * *");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to once a week.
|
||||
*
|
||||
* @param int|string $weekday
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function weekly($weekday = 0, $hour = 0, $minute = 0)
|
||||
{
|
||||
if (is_string($hour)) {
|
||||
$parts = explode(':', $hour);
|
||||
$hour = $parts[0];
|
||||
$minute = $parts[1] ?? '0';
|
||||
}
|
||||
$c = $this->validateCronSequence($minute, $hour, null, null, $weekday);
|
||||
|
||||
return $this->at("{$c['minute']} {$c['hour']} * * {$c['weekday']}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to once a month.
|
||||
*
|
||||
* @param int|string $month
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function monthly($month = '*', $day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
if (is_string($hour)) {
|
||||
$parts = explode(':', $hour);
|
||||
$hour = $parts[0];
|
||||
$minute = $parts[1] ?? '0';
|
||||
}
|
||||
$c = $this->validateCronSequence($minute, $hour, $day, $month);
|
||||
|
||||
return $this->at("{$c['minute']} {$c['hour']} {$c['day']} {$c['month']} *");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every Sunday.
|
||||
*
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function sunday($hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->weekly(0, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every Monday.
|
||||
*
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function monday($hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->weekly(1, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every Tuesday.
|
||||
*
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function tuesday($hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->weekly(2, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every Wednesday.
|
||||
*
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function wednesday($hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->weekly(3, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every Thursday.
|
||||
*
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function thursday($hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->weekly(4, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every Friday.
|
||||
*
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function friday($hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->weekly(5, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every Saturday.
|
||||
*
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function saturday($hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->weekly(6, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every January.
|
||||
*
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function january($day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->monthly(1, $day, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every February.
|
||||
*
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function february($day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->monthly(2, $day, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every March.
|
||||
*
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function march($day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->monthly(3, $day, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every April.
|
||||
*
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function april($day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->monthly(4, $day, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every May.
|
||||
*
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function may($day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->monthly(5, $day, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every June.
|
||||
*
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function june($day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->monthly(6, $day, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every July.
|
||||
*
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function july($day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->monthly(7, $day, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every August.
|
||||
*
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function august($day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->monthly(8, $day, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every September.
|
||||
*
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function september($day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->monthly(9, $day, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every October.
|
||||
*
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function october($day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->monthly(10, $day, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every November.
|
||||
*
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function november($day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->monthly(11, $day, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution time to every December.
|
||||
*
|
||||
* @param int|string $day
|
||||
* @param int|string $hour
|
||||
* @param int|string $minute
|
||||
* @return self
|
||||
*/
|
||||
public function december($day = 1, $hour = 0, $minute = 0)
|
||||
{
|
||||
return $this->monthly(12, $day, $hour, $minute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate sequence of cron expression.
|
||||
*
|
||||
* @param int|string|null $minute
|
||||
* @param int|string|null $hour
|
||||
* @param int|string|null $day
|
||||
* @param int|string|null $month
|
||||
* @param int|string|null $weekday
|
||||
* @return array
|
||||
*/
|
||||
private function validateCronSequence($minute = null, $hour = null, $day = null, $month = null, $weekday = null)
|
||||
{
|
||||
return [
|
||||
'minute' => $this->validateCronRange($minute, 0, 59),
|
||||
'hour' => $this->validateCronRange($hour, 0, 23),
|
||||
'day' => $this->validateCronRange($day, 1, 31),
|
||||
'month' => $this->validateCronRange($month, 1, 12),
|
||||
'weekday' => $this->validateCronRange($weekday, 0, 6),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate sequence of cron expression.
|
||||
*
|
||||
* @param int|string|null $value
|
||||
* @param int $min
|
||||
* @param int $max
|
||||
* @return mixed
|
||||
*/
|
||||
private function validateCronRange($value, $min, $max)
|
||||
{
|
||||
if ($value === null || $value === '*') {
|
||||
return '*';
|
||||
}
|
||||
|
||||
if (! is_numeric($value) ||
|
||||
! ($value >= $min && $value <= $max)
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
"Invalid value: it should be '*' or between {$min} and {$max}."
|
||||
);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,462 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Scheduler
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Scheduler;
|
||||
|
||||
use DateTime;
|
||||
use RocketTheme\Toolbox\File\JsonFile;
|
||||
|
||||
/**
|
||||
* Job History Manager
|
||||
*
|
||||
* Provides comprehensive job execution history, logging, and analytics
|
||||
*
|
||||
* @package Grav\Common\Scheduler
|
||||
*/
|
||||
class JobHistory
|
||||
{
|
||||
/** @var string */
|
||||
protected $historyPath;
|
||||
|
||||
/** @var int */
|
||||
protected $retentionDays = 30;
|
||||
|
||||
/** @var int */
|
||||
protected $maxOutputLength = 5000;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $historyPath
|
||||
* @param int $retentionDays
|
||||
*/
|
||||
public function __construct(string $historyPath, int $retentionDays = 30)
|
||||
{
|
||||
$this->historyPath = $historyPath;
|
||||
$this->retentionDays = $retentionDays;
|
||||
|
||||
// Ensure history directory exists
|
||||
if (!is_dir($this->historyPath)) {
|
||||
mkdir($this->historyPath, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log job execution
|
||||
*
|
||||
* @param Job $job
|
||||
* @param array $metadata Additional metadata to store
|
||||
* @return string Log entry ID
|
||||
*/
|
||||
public function logExecution(Job $job, array $metadata = []): string
|
||||
{
|
||||
$entryId = uniqid($job->getId() . '_', true);
|
||||
$timestamp = new DateTime();
|
||||
|
||||
$entry = [
|
||||
'id' => $entryId,
|
||||
'job_id' => $job->getId(),
|
||||
'command' => is_string($job->getCommand()) ? $job->getCommand() : 'Closure',
|
||||
'arguments' => method_exists($job, 'getRawArguments') ? $job->getRawArguments() : $job->getArguments(),
|
||||
'executed_at' => $timestamp->format('c'),
|
||||
'timestamp' => $timestamp->getTimestamp(),
|
||||
'success' => $job->isSuccessful(),
|
||||
'output' => $this->captureOutput($job),
|
||||
'execution_time' => method_exists($job, 'getExecutionTime') ? $job->getExecutionTime() : null,
|
||||
'retry_count' => method_exists($job, 'getRetryCount') ? $job->getRetryCount() : 0,
|
||||
'priority' => method_exists($job, 'getPriority') ? $job->getPriority() : 'normal',
|
||||
'tags' => method_exists($job, 'getTags') ? $job->getTags() : [],
|
||||
'metadata' => array_merge(
|
||||
method_exists($job, 'getMetadata') ? $job->getMetadata() : [],
|
||||
$metadata
|
||||
),
|
||||
];
|
||||
|
||||
// Store in daily file
|
||||
$this->storeEntry($entry);
|
||||
|
||||
// Also store in job-specific history
|
||||
$this->storeJobHistory($job->getId(), $entry);
|
||||
|
||||
return $entryId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture job output with length limit
|
||||
*
|
||||
* @param Job $job
|
||||
* @return array
|
||||
*/
|
||||
protected function captureOutput(Job $job): array
|
||||
{
|
||||
$output = $job->getOutput();
|
||||
$truncated = false;
|
||||
|
||||
if (strlen($output) > $this->maxOutputLength) {
|
||||
$output = substr($output, 0, $this->maxOutputLength);
|
||||
$truncated = true;
|
||||
}
|
||||
|
||||
return [
|
||||
'content' => $output,
|
||||
'truncated' => $truncated,
|
||||
'length' => strlen($job->getOutput()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Store entry in daily log file
|
||||
*
|
||||
* @param array $entry
|
||||
* @return void
|
||||
*/
|
||||
protected function storeEntry(array $entry): void
|
||||
{
|
||||
$date = date('Y-m-d');
|
||||
$filename = $this->historyPath . '/' . $date . '.json';
|
||||
|
||||
$jsonFile = JsonFile::instance($filename);
|
||||
$entries = $jsonFile->content() ?: [];
|
||||
$entries[] = $entry;
|
||||
$jsonFile->save($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store job-specific history
|
||||
*
|
||||
* @param string $jobId
|
||||
* @param array $entry
|
||||
* @return void
|
||||
*/
|
||||
protected function storeJobHistory(string $jobId, array $entry): void
|
||||
{
|
||||
$jobDir = $this->historyPath . '/jobs';
|
||||
if (!is_dir($jobDir)) {
|
||||
mkdir($jobDir, 0755, true);
|
||||
}
|
||||
|
||||
$filename = $jobDir . '/' . $jobId . '.json';
|
||||
$jsonFile = JsonFile::instance($filename);
|
||||
$history = $jsonFile->content() ?: [];
|
||||
|
||||
// Keep only last 100 executions per job
|
||||
$history[] = $entry;
|
||||
if (count($history) > 100) {
|
||||
$history = array_slice($history, -100);
|
||||
}
|
||||
|
||||
$jsonFile->save($history);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get job history
|
||||
*
|
||||
* @param string $jobId
|
||||
* @param int $limit
|
||||
* @return array
|
||||
*/
|
||||
public function getJobHistory(string $jobId, int $limit = 50): array
|
||||
{
|
||||
$filename = $this->historyPath . '/jobs/' . $jobId . '.json';
|
||||
if (!file_exists($filename)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$jsonFile = JsonFile::instance($filename);
|
||||
$history = $jsonFile->content() ?: [];
|
||||
|
||||
// Return most recent first
|
||||
$history = array_reverse($history);
|
||||
|
||||
if ($limit > 0) {
|
||||
$history = array_slice($history, 0, $limit);
|
||||
}
|
||||
|
||||
return $history;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get history for a date range
|
||||
*
|
||||
* @param DateTime $startDate
|
||||
* @param DateTime $endDate
|
||||
* @param string|null $jobId Filter by job ID
|
||||
* @return array
|
||||
*/
|
||||
public function getHistoryRange(DateTime $startDate, DateTime $endDate, ?string $jobId = null): array
|
||||
{
|
||||
$history = [];
|
||||
$current = clone $startDate;
|
||||
|
||||
while ($current <= $endDate) {
|
||||
$filename = $this->historyPath . '/' . $current->format('Y-m-d') . '.json';
|
||||
if (file_exists($filename)) {
|
||||
$jsonFile = JsonFile::instance($filename);
|
||||
$entries = $jsonFile->content() ?: [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if ($jobId === null || $entry['job_id'] === $jobId) {
|
||||
$history[] = $entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$current->modify('+1 day');
|
||||
}
|
||||
|
||||
return $history;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get job statistics
|
||||
*
|
||||
* @param string $jobId
|
||||
* @param int $days Number of days to analyze
|
||||
* @return array
|
||||
*/
|
||||
public function getJobStatistics(string $jobId, int $days = 7): array
|
||||
{
|
||||
$startDate = new DateTime("-{$days} days");
|
||||
$endDate = new DateTime('now');
|
||||
|
||||
$history = $this->getHistoryRange($startDate, $endDate, $jobId);
|
||||
|
||||
if (empty($history)) {
|
||||
return [
|
||||
'total_runs' => 0,
|
||||
'successful_runs' => 0,
|
||||
'failed_runs' => 0,
|
||||
'success_rate' => 0,
|
||||
'average_execution_time' => 0,
|
||||
'last_run' => null,
|
||||
'last_success' => null,
|
||||
'last_failure' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$totalRuns = count($history);
|
||||
$successfulRuns = 0;
|
||||
$executionTimes = [];
|
||||
$lastRun = null;
|
||||
$lastSuccess = null;
|
||||
$lastFailure = null;
|
||||
|
||||
foreach ($history as $entry) {
|
||||
if ($entry['success']) {
|
||||
$successfulRuns++;
|
||||
if (!$lastSuccess || $entry['timestamp'] > $lastSuccess['timestamp']) {
|
||||
$lastSuccess = $entry;
|
||||
}
|
||||
} else {
|
||||
if (!$lastFailure || $entry['timestamp'] > $lastFailure['timestamp']) {
|
||||
$lastFailure = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$lastRun || $entry['timestamp'] > $lastRun['timestamp']) {
|
||||
$lastRun = $entry;
|
||||
}
|
||||
|
||||
if (isset($entry['execution_time']) && $entry['execution_time'] > 0) {
|
||||
$executionTimes[] = $entry['execution_time'];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_runs' => $totalRuns,
|
||||
'successful_runs' => $successfulRuns,
|
||||
'failed_runs' => $totalRuns - $successfulRuns,
|
||||
'success_rate' => $totalRuns > 0 ? round(($successfulRuns / $totalRuns) * 100, 2) : 0,
|
||||
'average_execution_time' => !empty($executionTimes) ? round(array_sum($executionTimes) / count($executionTimes), 3) : 0,
|
||||
'last_run' => $lastRun,
|
||||
'last_success' => $lastSuccess,
|
||||
'last_failure' => $lastFailure,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global statistics
|
||||
*
|
||||
* @param int $days
|
||||
* @return array
|
||||
*/
|
||||
public function getGlobalStatistics(int $days = 7): array
|
||||
{
|
||||
$startDate = new DateTime("-{$days} days");
|
||||
$endDate = new DateTime('now');
|
||||
|
||||
$history = $this->getHistoryRange($startDate, $endDate);
|
||||
|
||||
$jobStats = [];
|
||||
foreach ($history as $entry) {
|
||||
$jobId = $entry['job_id'];
|
||||
if (!isset($jobStats[$jobId])) {
|
||||
$jobStats[$jobId] = [
|
||||
'runs' => 0,
|
||||
'success' => 0,
|
||||
'failed' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$jobStats[$jobId]['runs']++;
|
||||
if ($entry['success']) {
|
||||
$jobStats[$jobId]['success']++;
|
||||
} else {
|
||||
$jobStats[$jobId]['failed']++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_executions' => count($history),
|
||||
'unique_jobs' => count($jobStats),
|
||||
'job_statistics' => $jobStats,
|
||||
'period_days' => $days,
|
||||
'from_date' => $startDate->format('Y-m-d'),
|
||||
'to_date' => $endDate->format('Y-m-d'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search history
|
||||
*
|
||||
* @param array $criteria
|
||||
* @return array
|
||||
*/
|
||||
public function searchHistory(array $criteria): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
// Determine date range
|
||||
$startDate = isset($criteria['start_date']) ? new DateTime($criteria['start_date']) : new DateTime('-7 days');
|
||||
$endDate = isset($criteria['end_date']) ? new DateTime($criteria['end_date']) : new DateTime('now');
|
||||
|
||||
$history = $this->getHistoryRange($startDate, $endDate, $criteria['job_id'] ?? null);
|
||||
|
||||
foreach ($history as $entry) {
|
||||
$match = true;
|
||||
|
||||
// Filter by success status
|
||||
if (isset($criteria['success']) && $entry['success'] !== $criteria['success']) {
|
||||
$match = false;
|
||||
}
|
||||
|
||||
// Filter by output content
|
||||
if (isset($criteria['output_contains']) &&
|
||||
stripos($entry['output']['content'], $criteria['output_contains']) === false) {
|
||||
$match = false;
|
||||
}
|
||||
|
||||
// Filter by tags
|
||||
if (isset($criteria['tags']) && is_array($criteria['tags'])) {
|
||||
$entryTags = $entry['tags'] ?? [];
|
||||
if (empty(array_intersect($criteria['tags'], $entryTags))) {
|
||||
$match = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($match) {
|
||||
$results[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort results
|
||||
if (isset($criteria['sort_by'])) {
|
||||
usort($results, function($a, $b) use ($criteria) {
|
||||
$field = $criteria['sort_by'];
|
||||
$order = $criteria['sort_order'] ?? 'desc';
|
||||
|
||||
$aVal = $a[$field] ?? 0;
|
||||
$bVal = $b[$field] ?? 0;
|
||||
|
||||
if ($order === 'asc') {
|
||||
return $aVal <=> $bVal;
|
||||
} else {
|
||||
return $bVal <=> $aVal;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Limit results
|
||||
if (isset($criteria['limit'])) {
|
||||
$results = array_slice($results, 0, $criteria['limit']);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean old history files
|
||||
*
|
||||
* @return int Number of files deleted
|
||||
*/
|
||||
public function cleanOldHistory(): int
|
||||
{
|
||||
$deleted = 0;
|
||||
$cutoffDate = new DateTime("-{$this->retentionDays} days");
|
||||
|
||||
$files = glob($this->historyPath . '/*.json');
|
||||
foreach ($files as $file) {
|
||||
$filename = basename($file, '.json');
|
||||
// Check if filename is a date
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $filename)) {
|
||||
$fileDate = new DateTime($filename);
|
||||
if ($fileDate < $cutoffDate) {
|
||||
unlink($file);
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export history to CSV
|
||||
*
|
||||
* @param array $history
|
||||
* @param string $filename
|
||||
* @return bool
|
||||
*/
|
||||
public function exportToCsv(array $history, string $filename): bool
|
||||
{
|
||||
$handle = fopen($filename, 'w');
|
||||
if (!$handle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write headers
|
||||
fputcsv($handle, [
|
||||
'Job ID',
|
||||
'Executed At',
|
||||
'Success',
|
||||
'Execution Time',
|
||||
'Output Length',
|
||||
'Retry Count',
|
||||
'Priority',
|
||||
'Tags',
|
||||
]);
|
||||
|
||||
// Write data
|
||||
foreach ($history as $entry) {
|
||||
fputcsv($handle, [
|
||||
$entry['job_id'],
|
||||
$entry['executed_at'],
|
||||
$entry['success'] ? 'Yes' : 'No',
|
||||
$entry['execution_time'] ?? '',
|
||||
$entry['output']['length'] ?? 0,
|
||||
$entry['retry_count'] ?? 0,
|
||||
$entry['priority'] ?? 'normal',
|
||||
implode(', ', $entry['tags'] ?? []),
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,588 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Scheduler
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Scheduler;
|
||||
|
||||
use RocketTheme\Toolbox\File\JsonFile;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* File-based job queue implementation
|
||||
*
|
||||
* @package Grav\Common\Scheduler
|
||||
*/
|
||||
class JobQueue
|
||||
{
|
||||
/** @var string */
|
||||
protected $queuePath;
|
||||
|
||||
/** @var string */
|
||||
protected $lockFile;
|
||||
|
||||
/** @var array Priority levels */
|
||||
const PRIORITY_HIGH = 'high';
|
||||
const PRIORITY_NORMAL = 'normal';
|
||||
const PRIORITY_LOW = 'low';
|
||||
|
||||
/**
|
||||
* JobQueue constructor
|
||||
*
|
||||
* @param string $queuePath
|
||||
*/
|
||||
public function __construct(string $queuePath)
|
||||
{
|
||||
$this->queuePath = $queuePath;
|
||||
$this->lockFile = $queuePath . '/.lock';
|
||||
|
||||
// Create queue directories
|
||||
$this->initializeDirectories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize queue directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function initializeDirectories(): void
|
||||
{
|
||||
$dirs = [
|
||||
$this->queuePath . '/pending',
|
||||
$this->queuePath . '/processing',
|
||||
$this->queuePath . '/failed',
|
||||
$this->queuePath . '/completed',
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
if (!file_exists($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a job to the queue
|
||||
*
|
||||
* @param Job $job
|
||||
* @param string $priority
|
||||
* @return string Job queue ID
|
||||
*/
|
||||
public function push(Job $job, string $priority = self::PRIORITY_NORMAL): string
|
||||
{
|
||||
$queueId = $this->generateQueueId($job);
|
||||
$timestamp = microtime(true);
|
||||
|
||||
$queueItem = [
|
||||
'id' => $queueId,
|
||||
'job_id' => $job->getId(),
|
||||
'command' => is_string($job->getCommand()) ? $job->getCommand() : 'Closure',
|
||||
'arguments' => method_exists($job, 'getRawArguments') ? $job->getRawArguments() : $job->getArguments(),
|
||||
'priority' => $priority,
|
||||
'timestamp' => $timestamp,
|
||||
'attempts' => 0,
|
||||
'max_attempts' => method_exists($job, 'getMaxAttempts') ? $job->getMaxAttempts() : 1,
|
||||
'created_at' => date('c'),
|
||||
'scheduled_for' => null,
|
||||
'metadata' => [],
|
||||
];
|
||||
|
||||
// Always serialize the job to preserve its full state
|
||||
$queueItem['serialized_job'] = base64_encode(serialize($job));
|
||||
|
||||
$this->writeQueueItem($queueItem, 'pending');
|
||||
|
||||
return $queueId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a job for delayed execution
|
||||
*
|
||||
* @param Job $job
|
||||
* @param \DateTime $scheduledFor
|
||||
* @param string $priority
|
||||
* @return string
|
||||
*/
|
||||
public function pushDelayed(Job $job, \DateTime $scheduledFor, string $priority = self::PRIORITY_NORMAL): string
|
||||
{
|
||||
$queueId = $this->push($job, $priority);
|
||||
|
||||
// Update the scheduled time
|
||||
$item = $this->getQueueItem($queueId, 'pending');
|
||||
if ($item) {
|
||||
$item['scheduled_for'] = $scheduledFor->format('c');
|
||||
$this->writeQueueItem($item, 'pending');
|
||||
}
|
||||
|
||||
return $queueId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop the next job from the queue
|
||||
*
|
||||
* @return Job|null
|
||||
*/
|
||||
public function pop(): ?Job
|
||||
{
|
||||
if (!$this->lock()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all pending items
|
||||
$items = $this->getPendingItems();
|
||||
|
||||
if (empty($items)) {
|
||||
$this->unlock();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sort by priority and timestamp
|
||||
usort($items, function($a, $b) {
|
||||
$priorityOrder = [
|
||||
self::PRIORITY_HIGH => 0,
|
||||
self::PRIORITY_NORMAL => 1,
|
||||
self::PRIORITY_LOW => 2,
|
||||
];
|
||||
|
||||
$aPriority = $priorityOrder[$a['priority']] ?? 1;
|
||||
$bPriority = $priorityOrder[$b['priority']] ?? 1;
|
||||
|
||||
if ($aPriority !== $bPriority) {
|
||||
return $aPriority - $bPriority;
|
||||
}
|
||||
|
||||
return $a['timestamp'] <=> $b['timestamp'];
|
||||
});
|
||||
|
||||
// Get the first item that's ready to run
|
||||
$now = new \DateTime();
|
||||
foreach ($items as $item) {
|
||||
if ($item['scheduled_for']) {
|
||||
$scheduledTime = new \DateTime($item['scheduled_for']);
|
||||
if ($scheduledTime > $now) {
|
||||
continue; // Skip items not yet due
|
||||
}
|
||||
}
|
||||
|
||||
// Move to processing
|
||||
$this->moveQueueItem($item['id'], 'pending', 'processing');
|
||||
|
||||
// Reconstruct the job
|
||||
$job = $this->reconstructJob($item);
|
||||
|
||||
$this->unlock();
|
||||
return $job;
|
||||
}
|
||||
|
||||
$this->unlock();
|
||||
return null;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->unlock();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop a job from the queue with its queue ID
|
||||
*
|
||||
* @return array|null Array with 'job' and 'id' keys
|
||||
*/
|
||||
public function popWithId(): ?array
|
||||
{
|
||||
if (!$this->lock()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all pending items
|
||||
$items = $this->getPendingItems();
|
||||
|
||||
if (empty($items)) {
|
||||
$this->unlock();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sort by priority and timestamp
|
||||
usort($items, function($a, $b) {
|
||||
$priorityOrder = [
|
||||
self::PRIORITY_HIGH => 0,
|
||||
self::PRIORITY_NORMAL => 1,
|
||||
self::PRIORITY_LOW => 2,
|
||||
];
|
||||
|
||||
$aPriority = $priorityOrder[$a['priority']] ?? 1;
|
||||
$bPriority = $priorityOrder[$b['priority']] ?? 1;
|
||||
|
||||
if ($aPriority !== $bPriority) {
|
||||
return $aPriority - $bPriority;
|
||||
}
|
||||
|
||||
return $a['timestamp'] <=> $b['timestamp'];
|
||||
});
|
||||
|
||||
// Get the first item that's ready to run
|
||||
$now = new \DateTime();
|
||||
foreach ($items as $item) {
|
||||
if ($item['scheduled_for']) {
|
||||
$scheduledTime = new \DateTime($item['scheduled_for']);
|
||||
if ($scheduledTime > $now) {
|
||||
continue; // Skip items not yet due
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruct the job first before moving it
|
||||
$job = $this->reconstructJob($item);
|
||||
|
||||
if (!$job) {
|
||||
// Failed to reconstruct, skip this item
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move to processing only if we can reconstruct the job
|
||||
$this->moveQueueItem($item['id'], 'pending', 'processing');
|
||||
|
||||
$this->unlock();
|
||||
return ['job' => $job, 'id' => $item['id']];
|
||||
}
|
||||
|
||||
$this->unlock();
|
||||
return null;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->unlock();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a job as completed
|
||||
*
|
||||
* @param string $queueId
|
||||
* @return void
|
||||
*/
|
||||
public function complete(string $queueId): void
|
||||
{
|
||||
$this->moveQueueItem($queueId, 'processing', 'completed');
|
||||
|
||||
// Clean up old completed items
|
||||
$this->cleanupCompleted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a job as failed
|
||||
*
|
||||
* @param string $queueId
|
||||
* @param string $error
|
||||
* @return void
|
||||
*/
|
||||
public function fail(string $queueId, string $error = ''): void
|
||||
{
|
||||
$item = $this->getQueueItem($queueId, 'processing');
|
||||
|
||||
if ($item) {
|
||||
$item['attempts']++;
|
||||
$item['last_error'] = $error;
|
||||
$item['failed_at'] = date('c');
|
||||
|
||||
if ($item['attempts'] < $item['max_attempts']) {
|
||||
// Move back to pending for retry
|
||||
$item['retry_at'] = $this->calculateRetryTime($item['attempts']);
|
||||
$item['scheduled_for'] = $item['retry_at'];
|
||||
$this->writeQueueItem($item, 'pending');
|
||||
$this->deleteQueueItem($queueId, 'processing');
|
||||
} else {
|
||||
// Move to failed (dead letter queue)
|
||||
$this->writeQueueItem($item, 'failed');
|
||||
$this->deleteQueueItem($queueId, 'processing');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get queue size
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function size(): int
|
||||
{
|
||||
return count($this->getPendingItems());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if queue is empty
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->size() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get queue statistics
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getStatistics(): array
|
||||
{
|
||||
return [
|
||||
'pending' => count($this->getPendingItems()),
|
||||
'processing' => count($this->getItemsInDirectory('processing')),
|
||||
'failed' => count($this->getItemsInDirectory('failed')),
|
||||
'completed_today' => $this->countCompletedToday(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique queue ID
|
||||
*
|
||||
* @param Job $job
|
||||
* @return string
|
||||
*/
|
||||
protected function generateQueueId(Job $job): string
|
||||
{
|
||||
return $job->getId() . '_' . uniqid('', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write queue item to disk
|
||||
*
|
||||
* @param array $item
|
||||
* @param string $directory
|
||||
* @return void
|
||||
*/
|
||||
protected function writeQueueItem(array $item, string $directory): void
|
||||
{
|
||||
$path = $this->queuePath . '/' . $directory . '/' . $item['id'] . '.json';
|
||||
$file = JsonFile::instance($path);
|
||||
$file->save($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read queue item from disk
|
||||
*
|
||||
* @param string $queueId
|
||||
* @param string $directory
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getQueueItem(string $queueId, string $directory): ?array
|
||||
{
|
||||
$path = $this->queuePath . '/' . $directory . '/' . $queueId . '.json';
|
||||
|
||||
if (!file_exists($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$file = JsonFile::instance($path);
|
||||
return $file->content();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete queue item
|
||||
*
|
||||
* @param string $queueId
|
||||
* @param string $directory
|
||||
* @return void
|
||||
*/
|
||||
protected function deleteQueueItem(string $queueId, string $directory): void
|
||||
{
|
||||
$path = $this->queuePath . '/' . $directory . '/' . $queueId . '.json';
|
||||
|
||||
if (file_exists($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move queue item between directories
|
||||
*
|
||||
* @param string $queueId
|
||||
* @param string $fromDir
|
||||
* @param string $toDir
|
||||
* @return void
|
||||
*/
|
||||
protected function moveQueueItem(string $queueId, string $fromDir, string $toDir): void
|
||||
{
|
||||
$fromPath = $this->queuePath . '/' . $fromDir . '/' . $queueId . '.json';
|
||||
$toPath = $this->queuePath . '/' . $toDir . '/' . $queueId . '.json';
|
||||
|
||||
if (file_exists($fromPath)) {
|
||||
rename($fromPath, $toPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pending items
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getPendingItems(): array
|
||||
{
|
||||
return $this->getItemsInDirectory('pending');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items in a specific directory
|
||||
*
|
||||
* @param string $directory
|
||||
* @return array
|
||||
*/
|
||||
protected function getItemsInDirectory(string $directory): array
|
||||
{
|
||||
$items = [];
|
||||
$path = $this->queuePath . '/' . $directory;
|
||||
|
||||
if (!is_dir($path)) {
|
||||
return $items;
|
||||
}
|
||||
|
||||
$files = glob($path . '/*.json');
|
||||
foreach ($files as $file) {
|
||||
$jsonFile = JsonFile::instance($file);
|
||||
$items[] = $jsonFile->content();
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstruct a job from queue item
|
||||
*
|
||||
* @param array $item
|
||||
* @return Job|null
|
||||
*/
|
||||
protected function reconstructJob(array $item): ?Job
|
||||
{
|
||||
if (isset($item['serialized_job'])) {
|
||||
// Unserialize the job
|
||||
try {
|
||||
$job = unserialize(base64_decode($item['serialized_job']));
|
||||
if ($job instanceof Job) {
|
||||
return $job;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Failed to unserialize
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new job from command
|
||||
if (isset($item['command'])) {
|
||||
$args = $item['arguments'] ?? [];
|
||||
$job = new Job($item['command'], $args, $item['job_id']);
|
||||
return $job;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate retry time with exponential backoff
|
||||
*
|
||||
* @param int $attempts
|
||||
* @return string
|
||||
*/
|
||||
protected function calculateRetryTime(int $attempts): string
|
||||
{
|
||||
$backoffSeconds = min(pow(2, $attempts) * 60, 3600); // Max 1 hour
|
||||
$retryTime = new \DateTime();
|
||||
$retryTime->modify("+{$backoffSeconds} seconds");
|
||||
return $retryTime->format('c');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old completed items
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function cleanupCompleted(): void
|
||||
{
|
||||
$items = $this->getItemsInDirectory('completed');
|
||||
$cutoff = new \DateTime('-24 hours');
|
||||
|
||||
foreach ($items as $item) {
|
||||
if (isset($item['created_at'])) {
|
||||
$createdAt = new \DateTime($item['created_at']);
|
||||
if ($createdAt < $cutoff) {
|
||||
$this->deleteQueueItem($item['id'], 'completed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count completed jobs today
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function countCompletedToday(): int
|
||||
{
|
||||
$items = $this->getItemsInDirectory('completed');
|
||||
$today = new \DateTime('today');
|
||||
$count = 0;
|
||||
|
||||
foreach ($items as $item) {
|
||||
if (isset($item['created_at'])) {
|
||||
$createdAt = new \DateTime($item['created_at']);
|
||||
if ($createdAt >= $today) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire lock for queue operations
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function lock(): bool
|
||||
{
|
||||
$attempts = 0;
|
||||
$maxAttempts = 50; // 5 seconds total
|
||||
|
||||
while ($attempts < $maxAttempts) {
|
||||
// Check if lock file exists and is stale (older than 30 seconds)
|
||||
if (file_exists($this->lockFile)) {
|
||||
$lockAge = time() - filemtime($this->lockFile);
|
||||
if ($lockAge > 30) {
|
||||
// Stale lock, remove it
|
||||
@unlink($this->lockFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to acquire lock atomically
|
||||
$handle = @fopen($this->lockFile, 'x');
|
||||
if ($handle !== false) {
|
||||
fclose($handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
$attempts++;
|
||||
usleep(100000); // 100ms
|
||||
}
|
||||
|
||||
// Could not acquire lock
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release queue lock
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function unlock(): void
|
||||
{
|
||||
if (file_exists($this->lockFile)) {
|
||||
unlink($this->lockFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Scheduler
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Common\Scheduler;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Utils;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Scheduler Controller for handling HTTP endpoints
|
||||
*
|
||||
* @package Grav\Common\Scheduler
|
||||
*/
|
||||
class SchedulerController
|
||||
{
|
||||
/** @var Grav */
|
||||
protected $grav;
|
||||
|
||||
/** @var ModernScheduler */
|
||||
protected $scheduler;
|
||||
|
||||
/**
|
||||
* SchedulerController constructor
|
||||
*
|
||||
* @param Grav $grav
|
||||
*/
|
||||
public function __construct(Grav $grav)
|
||||
{
|
||||
$this->grav = $grav;
|
||||
|
||||
// Get scheduler instance
|
||||
$scheduler = $grav['scheduler'];
|
||||
if ($scheduler instanceof ModernScheduler) {
|
||||
$this->scheduler = $scheduler;
|
||||
} else {
|
||||
// Create ModernScheduler instance if not already
|
||||
$this->scheduler = new ModernScheduler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle health check endpoint
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function health(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$config = $this->grav['config']->get('scheduler.modern', []);
|
||||
|
||||
// Check if health endpoint is enabled
|
||||
if (!($config['health']['enabled'] ?? true)) {
|
||||
return $this->jsonResponse(['error' => 'Health check disabled'], 403);
|
||||
}
|
||||
|
||||
// Get health status
|
||||
$health = $this->scheduler->getHealthStatus();
|
||||
|
||||
return $this->jsonResponse($health);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle webhook trigger endpoint
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function webhook(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$config = $this->grav['config']->get('scheduler.modern', []);
|
||||
|
||||
// Check if webhook is enabled
|
||||
if (!($config['webhook']['enabled'] ?? false)) {
|
||||
return $this->jsonResponse(['error' => 'Webhook triggers disabled'], 403);
|
||||
}
|
||||
|
||||
// Get authorization header
|
||||
$authHeader = $request->getHeaderLine('Authorization');
|
||||
$token = null;
|
||||
|
||||
if (preg_match('/Bearer\s+(.+)$/i', $authHeader, $matches)) {
|
||||
$token = $matches[1];
|
||||
}
|
||||
|
||||
// Get query parameters
|
||||
$params = $request->getQueryParams();
|
||||
$jobId = $params['job'] ?? null;
|
||||
|
||||
// Process webhook
|
||||
$result = $this->scheduler->processWebhookTrigger($token, $jobId);
|
||||
|
||||
$statusCode = $result['success'] ? 200 : 400;
|
||||
return $this->jsonResponse($result, $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle statistics endpoint
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function statistics(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Check if user is admin
|
||||
$user = $this->grav['user'] ?? null;
|
||||
if (!$user || !$user->authorize('admin.super')) {
|
||||
return $this->jsonResponse(['error' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$stats = $this->scheduler->getStatistics();
|
||||
|
||||
return $this->jsonResponse($stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle admin AJAX requests for scheduler status
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function adminStatus(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Check if user is admin
|
||||
$user = $this->grav['user'] ?? null;
|
||||
if (!$user || !$user->authorize('admin.scheduler')) {
|
||||
return $this->jsonResponse(['error' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$health = $this->scheduler->getHealthStatus();
|
||||
|
||||
// Format for admin display
|
||||
$response = [
|
||||
'health' => $this->formatHealthStatus($health),
|
||||
'triggers' => $this->formatTriggers($health['trigger_methods'] ?? [])
|
||||
];
|
||||
|
||||
return $this->jsonResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format health status for display
|
||||
*
|
||||
* @param array $health
|
||||
* @return string
|
||||
*/
|
||||
protected function formatHealthStatus(array $health): string
|
||||
{
|
||||
$status = $health['status'] ?? 'unknown';
|
||||
$lastRun = $health['last_run'] ?? null;
|
||||
$queueSize = $health['queue_size'] ?? 0;
|
||||
$failedJobs = $health['failed_jobs_24h'] ?? 0;
|
||||
$jobsDue = $health['jobs_due'] ?? 0;
|
||||
$message = $health['message'] ?? '';
|
||||
|
||||
$statusBadge = match($status) {
|
||||
'healthy' => '<span class="badge badge-success">Healthy</span>',
|
||||
'warning' => '<span class="badge badge-warning">Warning</span>',
|
||||
'critical' => '<span class="badge badge-danger">Critical</span>',
|
||||
default => '<span class="badge badge-secondary">Unknown</span>'
|
||||
};
|
||||
|
||||
$html = '<div class="scheduler-health">';
|
||||
$html .= '<p>Status: ' . $statusBadge;
|
||||
if ($message) {
|
||||
$html .= ' - ' . htmlspecialchars($message);
|
||||
}
|
||||
$html .= '</p>';
|
||||
|
||||
if ($lastRun) {
|
||||
$lastRunTime = new \DateTime($lastRun);
|
||||
$now = new \DateTime();
|
||||
$diff = $now->diff($lastRunTime);
|
||||
|
||||
$timeAgo = '';
|
||||
if ($diff->d > 0) {
|
||||
$timeAgo = $diff->d . ' day' . ($diff->d > 1 ? 's' : '') . ' ago';
|
||||
} elseif ($diff->h > 0) {
|
||||
$timeAgo = $diff->h . ' hour' . ($diff->h > 1 ? 's' : '') . ' ago';
|
||||
} elseif ($diff->i > 0) {
|
||||
$timeAgo = $diff->i . ' minute' . ($diff->i > 1 ? 's' : '') . ' ago';
|
||||
} else {
|
||||
$timeAgo = 'Less than a minute ago';
|
||||
}
|
||||
|
||||
$html .= '<p>Last Run: <strong>' . $timeAgo . '</strong></p>';
|
||||
} else {
|
||||
$html .= '<p>Last Run: <strong>Never</strong></p>';
|
||||
}
|
||||
|
||||
$html .= '<p>Jobs Due: <strong>' . $jobsDue . '</strong></p>';
|
||||
$html .= '<p>Queue Size: <strong>' . $queueSize . '</strong></p>';
|
||||
|
||||
if ($failedJobs > 0) {
|
||||
$html .= '<p class="text-danger">Failed Jobs (24h): <strong>' . $failedJobs . '</strong></p>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format triggers for display
|
||||
*
|
||||
* @param array $triggers
|
||||
* @return string
|
||||
*/
|
||||
protected function formatTriggers(array $triggers): string
|
||||
{
|
||||
if (empty($triggers)) {
|
||||
return '<div class="alert alert-warning">No active triggers detected. Please set up cron, systemd, or webhook triggers.</div>';
|
||||
}
|
||||
|
||||
$html = '<div class="scheduler-triggers">';
|
||||
$html .= '<ul class="list-unstyled">';
|
||||
|
||||
foreach ($triggers as $trigger) {
|
||||
$icon = match($trigger) {
|
||||
'cron' => '⏰',
|
||||
'systemd' => '⚙️',
|
||||
'webhook' => '🔗',
|
||||
'external' => '🌐',
|
||||
default => '•'
|
||||
};
|
||||
|
||||
$label = match($trigger) {
|
||||
'cron' => 'Cron Job',
|
||||
'systemd' => 'Systemd Timer',
|
||||
'webhook' => 'Webhook Triggers',
|
||||
'external' => 'External Triggers',
|
||||
default => ucfirst($trigger)
|
||||
};
|
||||
|
||||
$html .= '<li>' . $icon . ' <strong>' . $label . '</strong> <span class="badge badge-success">Active</span></li>';
|
||||
}
|
||||
|
||||
$html .= '</ul>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create JSON response
|
||||
*
|
||||
* @param array $data
|
||||
* @param int $statusCode
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function jsonResponse(array $data, int $statusCode = 200): ResponseInterface
|
||||
{
|
||||
$response = $this->grav['response'] ?? new \Nyholm\Psr7\Response();
|
||||
|
||||
$response = $response->withStatus($statusCode)
|
||||
->withHeader('Content-Type', 'application/json');
|
||||
|
||||
$body = $response->getBody();
|
||||
$body->write(json_encode($data));
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user