refactor: rename lib/ to core/ for clearer core-module separation
Rename the top-level lib/ directory to core/ so the project root immediately communicates which code is core platform and which lives in modules/. PHP namespaces (MintyPHP\*) are unchanged — only the Composer PSR-4 path mapping moves from lib/ to core/. Module-internal lib/ directories (modules/*/lib/) are untouched. Updated across the full stack: - composer.json PSR-4 mapping - bootstrap entry points (web/index.php, bin/*, tests/bootstrap.php) - tooling configs (phpstan.neon, phpunit.xml, php-cs-fixer) - 26 architecture contract tests - enforcement-policy, guard-catalog, quality-gates - all documentation (CLAUDE.md, README, 25 docs/, .agents/skills/) - bin/qa-extended.sh search paths - .claude/settings.local.json permission paths Workflow: .agents/runs/CORE-LIB-RENAME-001/ (Analyst → Planner → Executor → Code Review (4 findings fixed) → Security Review → Acceptance Test → Finalizer) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
229
core/Service/Scheduler/ScheduledJobRegistry.php
Normal file
229
core/Service/Scheduler/ScheduledJobRegistry.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?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' => array_values($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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user