add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace MintyPHP\Service\Scheduler;
|
|
|
|
|
|
|
|
|
|
|
|
class ScheduleCalculator
|
|
|
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
|
public function normalizeTimezone(?string $timezone): string
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
{
|
|
|
|
|
|
$timezone = trim((string) $timezone);
|
|
|
|
|
|
if ($timezone === '') {
|
|
|
|
|
|
$timezone = defined('APP_TIMEZONE') ? (string) APP_TIMEZONE : 'UTC';
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
new \DateTimeZone($timezone);
|
|
|
|
|
|
return $timezone;
|
|
|
|
|
|
} catch (\Throwable $exception) {
|
|
|
|
|
|
return defined('APP_TIMEZONE') ? (string) APP_TIMEZONE : 'UTC';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
|
public function normalizeScheduleType(?string $type): string
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
{
|
|
|
|
|
|
$type = strtolower(trim((string) $type));
|
|
|
|
|
|
return in_array($type, ['hourly', 'daily', 'weekly'], true) ? $type : 'daily';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
|
public function normalizeInterval(string $type, int $value): int
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
|
$type = $this->normalizeScheduleType($type);
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
if ($type === 'hourly') {
|
|
|
|
|
|
return max(1, min(24, $value));
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($type === 'weekly') {
|
|
|
|
|
|
return max(1, min(52, $value));
|
|
|
|
|
|
}
|
|
|
|
|
|
return max(1, min(365, $value));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
|
public function normalizeTime(?string $time): ?string
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
{
|
|
|
|
|
|
$time = trim((string) $time);
|
|
|
|
|
|
if ($time === '') {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!preg_match('/^(\d{1,2}):(\d{2})$/', $time, $matches)) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
$hour = (int) $matches[1];
|
|
|
|
|
|
$minute = (int) $matches[2];
|
|
|
|
|
|
if ($hour < 0 || $hour > 23 || $minute < 0 || $minute > 59) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return sprintf('%02d:%02d', $hour, $minute);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
|
public function normalizeWeekdaysCsv(mixed $value): ?string
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
{
|
2026-02-23 12:58:19 +01:00
|
|
|
|
$list = $this->parseWeekdays($value);
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
if (!$list) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return implode(',', $list);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @return array<int>
|
|
|
|
|
|
*/
|
2026-02-23 12:58:19 +01:00
|
|
|
|
public function parseWeekdays(mixed $value): array
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
{
|
|
|
|
|
|
$raw = [];
|
|
|
|
|
|
if (is_array($value)) {
|
|
|
|
|
|
$raw = $value;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$raw = explode(',', (string) $value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$weekdays = [];
|
|
|
|
|
|
foreach ($raw as $item) {
|
|
|
|
|
|
$day = (int) trim((string) $item);
|
|
|
|
|
|
if ($day >= 1 && $day <= 7) {
|
|
|
|
|
|
$weekdays[$day] = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$list = array_keys($weekdays);
|
|
|
|
|
|
sort($list, SORT_NUMERIC);
|
|
|
|
|
|
return $list;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Calculates the next UTC execution time for a scheduled job based on its schedule
|
|
|
|
|
|
* configuration and a reference point in time.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Expected keys in $job:
|
|
|
|
|
|
* - schedule_type (string) 'hourly'|'daily'|'weekly'
|
|
|
|
|
|
* - schedule_interval (int) 1..24 for hourly, 1..365 for daily, 1..52 for weekly
|
|
|
|
|
|
* - timezone (string) IANA timezone name (e.g. 'Europe/Berlin')
|
|
|
|
|
|
* - schedule_time (string) 'HH:MM', required for daily and weekly
|
|
|
|
|
|
* - schedule_weekdays_csv (string) CSV of ISO weekday numbers 1 (Mon)..7 (Sun), required for weekly
|
|
|
|
|
|
*
|
|
|
|
|
|
* All schedule calculations are performed in the job's local timezone to correctly
|
|
|
|
|
|
* handle DST transitions. The returned value is always UTC.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Returns null if the schedule is misconfigured (e.g. missing schedule_time for daily/weekly)
|
|
|
|
|
|
* or if no valid next run could be found within the search window.
|
|
|
|
|
|
*/
|
2026-02-23 12:58:19 +01:00
|
|
|
|
public function calculateNextRunUtc(array $job, ?\DateTimeImmutable $referenceUtc = null): ?\DateTimeImmutable
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
{
|
|
|
|
|
|
$referenceUtc = $referenceUtc ?? new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
2026-02-23 12:58:19 +01:00
|
|
|
|
$type = $this->normalizeScheduleType((string) ($job['schedule_type'] ?? 'daily'));
|
|
|
|
|
|
$interval = $this->normalizeInterval($type, (int) ($job['schedule_interval'] ?? 1));
|
|
|
|
|
|
$timezone = new \DateTimeZone($this->normalizeTimezone((string) ($job['timezone'] ?? '')));
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
$localReference = $referenceUtc->setTimezone($timezone);
|
|
|
|
|
|
|
|
|
|
|
|
if ($type === 'hourly') {
|
|
|
|
|
|
// Snap to the top of the current hour, then advance by the interval.
|
|
|
|
|
|
$candidate = $localReference
|
|
|
|
|
|
->setTime((int) $localReference->format('H'), 0, 0)
|
|
|
|
|
|
->modify('+' . $interval . ' hour');
|
|
|
|
|
|
return $candidate->setTimezone(new \DateTimeZone('UTC'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 12:58:19 +01:00
|
|
|
|
$time = $this->normalizeTime((string) ($job['schedule_time'] ?? ''));
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
if ($time === null) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
[$hours, $minutes] = array_map('intval', explode(':', $time));
|
|
|
|
|
|
|
|
|
|
|
|
if ($type === 'daily') {
|
|
|
|
|
|
$candidate = $localReference->setTime($hours, $minutes, 0);
|
|
|
|
|
|
if ($candidate <= $localReference) {
|
|
|
|
|
|
$candidate = $candidate->modify('+' . $interval . ' day');
|
|
|
|
|
|
}
|
|
|
|
|
|
return $candidate->setTimezone(new \DateTimeZone('UTC'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Weekly: iterate forward day by day until we find a matching weekday in the correct
|
|
|
|
|
|
// week interval. The search window is 1110 days (~3 years), which comfortably covers
|
|
|
|
|
|
// the maximum weekly interval of 52 weeks × 7 days/week = 364 days, with margin for
|
|
|
|
|
|
// weekday alignment and DST edge cases.
|
2026-02-23 12:58:19 +01:00
|
|
|
|
$weekdays = $this->parseWeekdays((string) ($job['schedule_weekdays_csv'] ?? ''));
|
add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00
|
|
|
|
if (!$weekdays) {
|
|
|
|
|
|
$weekdays = [1];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$startOfDay = $localReference->setTime(0, 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 1970-01-05 is a Monday in any timezone offset that does not cross the date line,
|
|
|
|
|
|
// used as a stable epoch to count week indices for "every N weeks" interval logic.
|
|
|
|
|
|
$epochMonday = new \DateTimeImmutable('1970-01-05 00:00:00', $timezone);
|
|
|
|
|
|
for ($i = 0; $i < 370 * 3; $i++) {
|
|
|
|
|
|
$day = $startOfDay->modify('+' . $i . ' day');
|
|
|
|
|
|
$isoDay = (int) $day->format('N');
|
|
|
|
|
|
if (!in_array($isoDay, $weekdays, true)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$daysFromEpoch = (int) $epochMonday->diff($day)->format('%r%a');
|
|
|
|
|
|
$weekIndex = (int) floor($daysFromEpoch / 7);
|
|
|
|
|
|
if ($interval > 1 && ($weekIndex % $interval) !== 0) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$candidate = $day->setTime($hours, $minutes, 0);
|
|
|
|
|
|
if ($candidate <= $localReference) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
return $candidate->setTimezone(new \DateTimeZone('UTC'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|