Resolve all non-false-positive PHPStan 2 findings, reducing the baseline from 486 to 382 entries (only unused-public false positives remain). Categories fixed: - Remove 39 redundant type checks (is_string, is_array, is_object, method_exists) - Remove 12 no-op array_values() on already-sequential lists - Simplify 13 null-coalesce on guaranteed offsets - Remove 8 always-true instanceof checks - Replace 12 redundant test assertions (assertTrue(true) → addToAssertionCount) - Simplify 6 unnecessary nullsafe operators (?-> → ->) - Fix 6 always-true !== '' comparisons - Fix remaining: unset.offset, return.unusedType, staticMethod narrowing 53 files changed across core/, modules/, pages/, and tests/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
230 lines
7.9 KiB
PHP
230 lines
7.9 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Service\Scheduler;
|
|
|
|
use MintyPHP\App\AppContainer;
|
|
use MintyPHP\App\Module\ModuleRegistry;
|
|
use MintyPHP\Service\Scheduler\Handler\ScheduledJobHandlerInterface;
|
|
use MintyPHP\Service\Scheduler\Handler\UserLifecycleJobHandler;
|
|
use MintyPHP\Service\User\UserLifecycleService;
|
|
use RuntimeException;
|
|
|
|
class ScheduledJobRegistry
|
|
{
|
|
public const USER_LIFECYCLE_RUN = 'user_lifecycle_run';
|
|
|
|
/** @var array<string, ScheduledJobHandlerInterface> */
|
|
private array $handlers;
|
|
|
|
/**
|
|
* @var array<string, array{
|
|
* handler: string,
|
|
* label: string,
|
|
* description: string,
|
|
* default_enabled: int,
|
|
* default_timezone: string,
|
|
* default_schedule_type: string,
|
|
* default_schedule_interval: int,
|
|
* default_schedule_time: ?string,
|
|
* default_schedule_weekdays_csv: ?string,
|
|
* default_catchup_once: int,
|
|
* allowed_schedule_types: list<string>
|
|
* }>
|
|
*/
|
|
private array $moduleJobs = [];
|
|
|
|
public function __construct(
|
|
UserLifecycleService $userLifecycleService,
|
|
private readonly AppContainer $container
|
|
) {
|
|
$this->handlers = [
|
|
self::USER_LIFECYCLE_RUN => new UserLifecycleJobHandler($userLifecycleService),
|
|
];
|
|
|
|
$this->loadModuleJobs();
|
|
}
|
|
|
|
/**
|
|
* Returns all job definitions keyed by job_key.
|
|
* Consumed by ScheduledJobService->ensureSystemJobs() to sync registry with the database.
|
|
*/
|
|
public function definitions(): array
|
|
{
|
|
$result = [];
|
|
foreach ($this->handlers as $jobKey => $handler) {
|
|
$result[$jobKey] = $handler->definition();
|
|
}
|
|
foreach ($this->moduleJobs as $jobKey => $definition) {
|
|
$result[$jobKey] = $this->stripHandlerFromDefinition($definition);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Returns the definition for a single job key, or null if not registered.
|
|
*/
|
|
public function get(string $jobKey): ?array
|
|
{
|
|
$jobKey = trim($jobKey);
|
|
if ($jobKey === '') {
|
|
return null;
|
|
}
|
|
if (isset($this->handlers[$jobKey])) {
|
|
return $this->handlers[$jobKey]->definition();
|
|
}
|
|
if (isset($this->moduleJobs[$jobKey])) {
|
|
return $this->stripHandlerFromDefinition($this->moduleJobs[$jobKey]);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Dispatches execution to the registered handler for the given job key and returns
|
|
* a normalized result envelope. Unknown job keys return status 'failed' with
|
|
* error_code 'job_not_supported' rather than throwing an exception.
|
|
*
|
|
* @param string $jobKey Registered job identifier (see class constants).
|
|
* @param int|null $actorUserId Null for scheduler-triggered runs; user ID for manual runs.
|
|
* @return array{status:string,error_code:?string,error_message:?string,result:array}
|
|
*/
|
|
public function execute(string $jobKey, ?int $actorUserId): array
|
|
{
|
|
if (isset($this->handlers[$jobKey])) {
|
|
return $this->handlers[$jobKey]->execute($actorUserId);
|
|
}
|
|
|
|
$moduleDefinition = $this->moduleJobs[$jobKey] ?? null;
|
|
if (!is_array($moduleDefinition)) {
|
|
return [
|
|
'status' => 'failed',
|
|
'error_code' => 'job_not_supported',
|
|
'error_message' => null,
|
|
'result' => [],
|
|
];
|
|
}
|
|
|
|
$handlerClass = trim((string) $moduleDefinition['handler']);
|
|
if ($handlerClass === '') {
|
|
return [
|
|
'status' => 'failed',
|
|
'error_code' => 'job_handler_missing',
|
|
'error_message' => 'Scheduler job handler is not configured.',
|
|
'result' => [],
|
|
];
|
|
}
|
|
|
|
try {
|
|
$handler = $this->resolveHandler($handlerClass);
|
|
return $handler->execute($actorUserId);
|
|
} catch (\Throwable $exception) {
|
|
return [
|
|
'status' => 'failed',
|
|
'error_code' => 'job_handler_resolution_failed',
|
|
'error_message' => $exception->getMessage(),
|
|
'result' => [],
|
|
];
|
|
}
|
|
}
|
|
|
|
private function loadModuleJobs(): void
|
|
{
|
|
$registry = $this->resolveModuleRegistry();
|
|
if (!$registry instanceof ModuleRegistry) {
|
|
return;
|
|
}
|
|
|
|
foreach ($registry->getSchedulerJobs() as $jobDefinition) {
|
|
$jobKey = trim((string) $jobDefinition['job_key']);
|
|
if ($jobKey === '') {
|
|
continue;
|
|
}
|
|
|
|
if (isset($this->handlers[$jobKey])) {
|
|
throw new RuntimeException(
|
|
"Module scheduler job '{$jobKey}' collides with core scheduler job key."
|
|
);
|
|
}
|
|
if (isset($this->moduleJobs[$jobKey])) {
|
|
throw new RuntimeException(
|
|
"Duplicate module scheduler job key '{$jobKey}'."
|
|
);
|
|
}
|
|
|
|
$this->moduleJobs[$jobKey] = [
|
|
'handler' => trim((string) $jobDefinition['handler']),
|
|
'label' => trim((string) $jobDefinition['label']),
|
|
'description' => trim((string) $jobDefinition['description']),
|
|
'default_enabled' => (int) $jobDefinition['default_enabled'],
|
|
'default_timezone' => trim((string) $jobDefinition['default_timezone']),
|
|
'default_schedule_type' => trim((string) $jobDefinition['default_schedule_type']),
|
|
'default_schedule_interval' => (int) $jobDefinition['default_schedule_interval'],
|
|
'default_schedule_time' => $jobDefinition['default_schedule_time'],
|
|
'default_schedule_weekdays_csv' => $jobDefinition['default_schedule_weekdays_csv'],
|
|
'default_catchup_once' => (int) $jobDefinition['default_catchup_once'],
|
|
'allowed_schedule_types' => $jobDefinition['allowed_schedule_types'],
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array{
|
|
* handler: string,
|
|
* label: string,
|
|
* description: string,
|
|
* default_enabled: int,
|
|
* default_timezone: string,
|
|
* default_schedule_type: string,
|
|
* default_schedule_interval: int,
|
|
* default_schedule_time: ?string,
|
|
* default_schedule_weekdays_csv: ?string,
|
|
* default_catchup_once: int,
|
|
* allowed_schedule_types: list<string>
|
|
* } $definition
|
|
* @return array{
|
|
* label: string,
|
|
* description: string,
|
|
* default_enabled: int,
|
|
* default_timezone: string,
|
|
* default_schedule_type: string,
|
|
* default_schedule_interval: int,
|
|
* default_schedule_time: ?string,
|
|
* default_schedule_weekdays_csv: ?string,
|
|
* default_catchup_once: int,
|
|
* allowed_schedule_types: list<string>
|
|
* }
|
|
*/
|
|
private function stripHandlerFromDefinition(array $definition): array
|
|
{
|
|
unset($definition['handler']);
|
|
return $definition;
|
|
}
|
|
|
|
private function resolveModuleRegistry(): ?ModuleRegistry
|
|
{
|
|
try {
|
|
$registry = $this->container->get(ModuleRegistry::class);
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
return $registry instanceof ModuleRegistry ? $registry : null;
|
|
}
|
|
|
|
private function resolveHandler(string $handlerClass): ScheduledJobHandlerInterface
|
|
{
|
|
$handler = null;
|
|
if ($this->container->has($handlerClass)) {
|
|
$handler = $this->container->get($handlerClass);
|
|
} elseif (class_exists($handlerClass)) {
|
|
$handler = new $handlerClass();
|
|
}
|
|
|
|
if (!$handler instanceof ScheduledJobHandlerInterface) {
|
|
throw new RuntimeException(
|
|
"Scheduler job handler '{$handlerClass}' must implement " . ScheduledJobHandlerInterface::class . '.'
|
|
);
|
|
}
|
|
|
|
return $handler;
|
|
}
|
|
}
|