refactor(cli)!: hard-cut legacy scripts and standardize console runtime
- remove legacy bin/module-*.php and bin/doctor.php entry scripts - move module/doctor execution into class-based runners under lib/Console - add stable JSON output for doctor and module:sync - introduce bin/dev for init/up/down/logs/console/qa workflow - update DI wiring, phpstan scan config, tests, and docs to new CLI contract
This commit is contained in:
317
lib/Console/Runner/Doctor/DoctorRunner.php
Normal file
317
lib/Console/Runner/Doctor/DoctorRunner.php
Normal file
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MintyPHP\Console\Runner\Doctor;
|
||||
|
||||
use MintyPHP\App\AppContainer;
|
||||
use MintyPHP\App\Bootstrap\EnvValidator;
|
||||
use MintyPHP\Console\Support\CliAppBootstrap;
|
||||
use MintyPHP\DB;
|
||||
use MintyPHP\Module\Audit\Service\SystemAuditService;
|
||||
use MintyPHP\Service\Access\AuthorizationService;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Access\UiAccessService;
|
||||
use MintyPHP\Service\Auth\AuthService;
|
||||
use Throwable;
|
||||
|
||||
final class DoctorRunner implements DoctorRunnerInterface
|
||||
{
|
||||
public function run(): array
|
||||
{
|
||||
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
|
||||
ini_set('display_errors', '0');
|
||||
|
||||
/** @var AppContainer $container */
|
||||
$container = CliAppBootstrap::bootstrap('cli', 'bin/console doctor');
|
||||
$startedAt = microtime(true);
|
||||
|
||||
$results = [];
|
||||
$failCount = 0;
|
||||
$warnCount = 0;
|
||||
|
||||
$runCheck = static function (string $name, callable $check) use (&$results, &$failCount, &$warnCount): void {
|
||||
try {
|
||||
$result = $check();
|
||||
} catch (Throwable $throwable) {
|
||||
$result = [
|
||||
'status' => 'fail',
|
||||
'message' => $throwable->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
$status = strtolower(trim((string) ($result['status'] ?? 'fail')));
|
||||
if (!in_array($status, ['ok', 'warn', 'fail'], true)) {
|
||||
$status = 'fail';
|
||||
}
|
||||
|
||||
$message = trim((string) ($result['message'] ?? ''));
|
||||
if ($message === '') {
|
||||
$message = 'no details';
|
||||
}
|
||||
|
||||
if ($status === 'fail') {
|
||||
$failCount++;
|
||||
} elseif ($status === 'warn') {
|
||||
$warnCount++;
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'status' => $status,
|
||||
'name' => $name,
|
||||
'message' => $message,
|
||||
];
|
||||
};
|
||||
|
||||
$runCheck('Environment validation', static function (): array {
|
||||
EnvValidator::validate();
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'all required env keys and formats are valid',
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('App container bootstrap', static function () use ($container): array {
|
||||
if (!$container instanceof AppContainer) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => 'registerContainer.php did not return AppContainer',
|
||||
];
|
||||
}
|
||||
|
||||
app(AuthService::class);
|
||||
app(AuthorizationService::class);
|
||||
app(UiAccessService::class);
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'core services resolved successfully',
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('Database connectivity', static function (): array {
|
||||
$pong = DB::selectValue('select 1');
|
||||
if ((int) $pong !== 1) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => 'select 1 did not return expected value',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'connection established',
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('Database schema basics', static function (): array {
|
||||
$requiredTables = [
|
||||
'users',
|
||||
'roles',
|
||||
'permissions',
|
||||
'user_roles',
|
||||
'role_permissions',
|
||||
'tenants',
|
||||
'departments',
|
||||
'settings',
|
||||
'scheduler_runtime_status',
|
||||
];
|
||||
|
||||
$rows = DB::select(
|
||||
'select table_name from information_schema.tables where table_schema = database() and table_name in (???)',
|
||||
$requiredTables
|
||||
);
|
||||
|
||||
$present = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$table = (string) ($row['tables']['table_name'] ?? $row['table_name'] ?? '');
|
||||
if ($table !== '') {
|
||||
$present[] = $table;
|
||||
}
|
||||
}
|
||||
$present = array_values(array_unique($present));
|
||||
sort($present, SORT_STRING);
|
||||
|
||||
$missing = array_values(array_diff($requiredTables, $present));
|
||||
if ($missing !== []) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => 'missing tables: ' . implode(', ', $missing),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => sprintf('%d core tables present', count($requiredTables)),
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('Storage path writeability', static function (): array {
|
||||
$storagePath = defined('APP_STORAGE_PATH') && APP_STORAGE_PATH
|
||||
? rtrim((string) APP_STORAGE_PATH, '/')
|
||||
: rtrim(CliAppBootstrap::projectRoot() . '/storage', '/');
|
||||
|
||||
if (!is_dir($storagePath)) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => "storage directory not found: {$storagePath}",
|
||||
];
|
||||
}
|
||||
if (!is_writable($storagePath)) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => "storage directory not writable: {$storagePath}",
|
||||
];
|
||||
}
|
||||
|
||||
$probeFile = $storagePath . '/.doctor-write-probe-' . uniqid('', true);
|
||||
$written = @file_put_contents($probeFile, 'ok');
|
||||
if ($written === false) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => "write probe failed in: {$storagePath}",
|
||||
];
|
||||
}
|
||||
@unlink($probeFile);
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => "storage path is writable ({$storagePath})",
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('RBAC baseline permissions', static function (): array {
|
||||
$requiredPermissions = [
|
||||
PermissionService::USERS_VIEW,
|
||||
PermissionService::TENANTS_VIEW,
|
||||
PermissionService::DEPARTMENTS_VIEW,
|
||||
PermissionService::ROLES_VIEW,
|
||||
PermissionService::PERMISSIONS_VIEW,
|
||||
PermissionService::SETTINGS_VIEW,
|
||||
];
|
||||
|
||||
$rows = DB::select(
|
||||
'select `key` from permissions where active = 1 and `key` in (???)',
|
||||
$requiredPermissions
|
||||
);
|
||||
|
||||
$present = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$key = (string) ($row['permissions']['key'] ?? $row['key'] ?? '');
|
||||
if ($key !== '') {
|
||||
$present[] = $key;
|
||||
}
|
||||
}
|
||||
$present = array_values(array_unique($present));
|
||||
sort($present, SORT_STRING);
|
||||
|
||||
$missing = array_values(array_diff($requiredPermissions, $present));
|
||||
if ($missing !== []) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => 'missing active permissions: ' . implode(', ', $missing),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => sprintf('%d baseline permissions active', count($requiredPermissions)),
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('Admin role assignment', static function (): array {
|
||||
$count = (int) (DB::selectValue(
|
||||
'select count(distinct ur.user_id) from user_roles ur join roles r on r.id = ur.role_id and r.active = 1 where r.description in (?, ?) or r.id = 1',
|
||||
'Admin',
|
||||
'Administrator'
|
||||
) ?? 0);
|
||||
|
||||
if ($count <= 0) {
|
||||
return [
|
||||
'status' => 'fail',
|
||||
'message' => 'no active user assigned to Admin/Administrator role',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => sprintf('%d admin user(s) assigned', $count),
|
||||
];
|
||||
});
|
||||
|
||||
$runCheck('Scheduler heartbeat', static function (): array {
|
||||
$row = DB::selectOne('select last_heartbeat_at, last_result, last_error_code from scheduler_runtime_status where id = 1 limit 1');
|
||||
$status = is_array($row) ? ($row['scheduler_runtime_status'] ?? $row) : null;
|
||||
if (!is_array($status)) {
|
||||
return [
|
||||
'status' => 'warn',
|
||||
'message' => 'no scheduler runtime status row found yet',
|
||||
];
|
||||
}
|
||||
|
||||
$heartbeat = trim((string) ($status['last_heartbeat_at'] ?? ''));
|
||||
$result = trim((string) ($status['last_result'] ?? 'unknown'));
|
||||
$errorCode = trim((string) ($status['last_error_code'] ?? ''));
|
||||
if ($heartbeat === '') {
|
||||
return [
|
||||
'status' => 'warn',
|
||||
'message' => 'scheduler heartbeat is empty',
|
||||
];
|
||||
}
|
||||
|
||||
$seconds = time() - strtotime($heartbeat . ' UTC');
|
||||
if ($seconds < 0) {
|
||||
$seconds = 0;
|
||||
}
|
||||
|
||||
if ($seconds > 300) {
|
||||
return [
|
||||
'status' => 'warn',
|
||||
'message' => "last heartbeat {$seconds}s ago (result={$result}" . ($errorCode !== '' ? ", error={$errorCode}" : '') . ')',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => "last heartbeat {$seconds}s ago (result={$result}" . ($errorCode !== '' ? ", error={$errorCode}" : '') . ')',
|
||||
];
|
||||
});
|
||||
|
||||
$okCount = count($results) - $warnCount - $failCount;
|
||||
$exitCode = $failCount > 0 ? 1 : 0;
|
||||
$status = $failCount > 0 ? 'fail' : ($warnCount > 0 ? 'warn' : 'ok');
|
||||
$durationMs = max(0, (int) round((microtime(true) - $startedAt) * 1000));
|
||||
|
||||
try {
|
||||
app(SystemAuditService::class)->record(
|
||||
'cli.command',
|
||||
$exitCode === 0 ? 'success' : 'failed',
|
||||
[
|
||||
'metadata' => [
|
||||
'command' => 'bin/console doctor',
|
||||
'exit_code' => $exitCode,
|
||||
'duration_ms' => $durationMs,
|
||||
'result' => $status,
|
||||
'warn_count' => $warnCount,
|
||||
'fail_count' => $failCount,
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (Throwable) {
|
||||
// fail-open
|
||||
} finally {
|
||||
DB::close();
|
||||
}
|
||||
|
||||
return [
|
||||
'command' => 'doctor',
|
||||
'status' => $status,
|
||||
'checks' => $results,
|
||||
'ok_count' => $okCount,
|
||||
'warn_count' => $warnCount,
|
||||
'fail_count' => $failCount,
|
||||
'exit_code' => $exitCode,
|
||||
'duration_ms' => $durationMs,
|
||||
];
|
||||
}
|
||||
}
|
||||
22
lib/Console/Runner/Doctor/DoctorRunnerInterface.php
Normal file
22
lib/Console/Runner/Doctor/DoctorRunnerInterface.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MintyPHP\Console\Runner\Doctor;
|
||||
|
||||
interface DoctorRunnerInterface
|
||||
{
|
||||
/**
|
||||
* @return array{
|
||||
* command: string,
|
||||
* status: string,
|
||||
* checks: list<array{status: string, name: string, message: string}>,
|
||||
* ok_count: int,
|
||||
* warn_count: int,
|
||||
* fail_count: int,
|
||||
* exit_code: int,
|
||||
* duration_ms: int
|
||||
* }
|
||||
*/
|
||||
public function run(): array;
|
||||
}
|
||||
348
lib/Console/Runner/Module/ModuleRunner.php
Normal file
348
lib/Console/Runner/Module/ModuleRunner.php
Normal file
@@ -0,0 +1,348 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MintyPHP\Console\Runner\Module;
|
||||
|
||||
use MintyPHP\App\AppContainer;
|
||||
use MintyPHP\App\Module\Contracts\ModuleDeactivationHandler;
|
||||
use MintyPHP\App\Module\ModuleManifestLoader;
|
||||
use MintyPHP\App\Module\ModulePermissionSynchronizer;
|
||||
use MintyPHP\App\Module\ModuleRegistry;
|
||||
use MintyPHP\App\Module\ModuleRuntimeAssetPublisher;
|
||||
use MintyPHP\App\Module\ModuleRuntimePageBuilder;
|
||||
use MintyPHP\Console\Support\ModuleCliRuntime;
|
||||
use MintyPHP\Service\Module\ModuleMigrationService;
|
||||
|
||||
final class ModuleRunner implements ModuleRunnerInterface
|
||||
{
|
||||
public function migrate(): array
|
||||
{
|
||||
$summary = ['applied' => 0, 'modules_enabled' => 0, 'skipped_empty' => 0];
|
||||
$message = 'module-migrate: all modules up to date, 0 new migrations.';
|
||||
$step = ModuleCliRuntime::runStep('module-migrate', function () use (&$summary, &$message): int {
|
||||
$lockResult = ModuleCliRuntime::withFileLock(
|
||||
ModuleCliRuntime::projectRoot() . '/storage/runtime/.module-migrate.lock',
|
||||
'module-migrate: another migration run is in progress, skipping.',
|
||||
function () use (&$summary, &$message): int {
|
||||
/** @var ModuleMigrationService $service */
|
||||
$service = app(ModuleMigrationService::class);
|
||||
$result = $service->applyPendingMigrations();
|
||||
$summary = $result;
|
||||
|
||||
if ($result['modules_enabled'] === 0) {
|
||||
$message = 'module-migrate: no modules enabled, nothing to do.';
|
||||
} elseif ($result['applied'] === 0) {
|
||||
$message = 'module-migrate: all modules up to date, 0 new migrations.';
|
||||
} else {
|
||||
$message = sprintf('module-migrate: applied %d migration(s).', $result['applied']);
|
||||
}
|
||||
|
||||
if ($result['skipped_empty'] > 0) {
|
||||
$message .= sprintf(' (%d empty file(s) skipped)', $result['skipped_empty']);
|
||||
}
|
||||
|
||||
return $result['ok'] ? 0 : 1;
|
||||
}
|
||||
);
|
||||
|
||||
if ($lockResult['busy']) {
|
||||
$message = (string) $lockResult['message'];
|
||||
}
|
||||
|
||||
return $lockResult['exit_code'];
|
||||
});
|
||||
|
||||
return [
|
||||
'command' => 'module:migrate',
|
||||
'status' => $step['status'],
|
||||
'exit_code' => $step['exit_code'],
|
||||
'vendor_warnings_ignored' => $step['vendor_warnings_ignored'],
|
||||
'message' => $step['error'] ?? $message,
|
||||
];
|
||||
}
|
||||
|
||||
public function permissionsSync(): array
|
||||
{
|
||||
$sync = [
|
||||
'created' => 0,
|
||||
'updated' => 0,
|
||||
'unchanged' => 0,
|
||||
'deactivated' => 0,
|
||||
'total' => 0,
|
||||
];
|
||||
$message = 'module-permissions-sync: ok';
|
||||
|
||||
$step = ModuleCliRuntime::runStep('module-permissions-sync', function () use (&$sync, &$message): int {
|
||||
$lockResult = ModuleCliRuntime::withFileLock(
|
||||
ModuleCliRuntime::projectRoot() . '/storage/runtime/.module-permissions-sync.lock',
|
||||
'module-permissions-sync: another sync is in progress, skipping.',
|
||||
function () use (&$sync): int {
|
||||
/** @var ModulePermissionSynchronizer $synchronizer */
|
||||
$synchronizer = app(ModulePermissionSynchronizer::class);
|
||||
$sync = $synchronizer->sync();
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
|
||||
if ($lockResult['busy']) {
|
||||
$message = (string) $lockResult['message'];
|
||||
}
|
||||
|
||||
return $lockResult['exit_code'];
|
||||
});
|
||||
|
||||
return [
|
||||
'command' => 'module:permissions-sync',
|
||||
'status' => $step['status'],
|
||||
'exit_code' => $step['exit_code'],
|
||||
'vendor_warnings_ignored' => $step['vendor_warnings_ignored'],
|
||||
'created' => (int) $sync['created'],
|
||||
'updated' => (int) $sync['updated'],
|
||||
'unchanged' => (int) $sync['unchanged'],
|
||||
'deactivated' => (int) $sync['deactivated'],
|
||||
'total' => (int) $sync['total'],
|
||||
'message' => $step['error'] ?? $message,
|
||||
];
|
||||
}
|
||||
|
||||
public function build(): array
|
||||
{
|
||||
$buildSummary = [
|
||||
'core_entries' => 0,
|
||||
'module_entries' => 0,
|
||||
];
|
||||
$message = 'module-build: ok';
|
||||
|
||||
$step = ModuleCliRuntime::runStep('module-build', function () use (&$buildSummary, &$message): int {
|
||||
$runtimeDir = ModuleCliRuntime::projectRoot() . '/storage/runtime/pages';
|
||||
$runtimeParent = dirname($runtimeDir);
|
||||
if (!is_dir($runtimeParent) && !mkdir($runtimeParent, 0775, true) && !is_dir($runtimeParent)) {
|
||||
$message = "Failed to create runtime directory: {$runtimeParent}";
|
||||
return 1;
|
||||
}
|
||||
|
||||
$lockResult = ModuleCliRuntime::withFileLock(
|
||||
ModuleCliRuntime::projectRoot() . '/storage/runtime/.module-build.lock',
|
||||
'module-build: another build is in progress, skipping.',
|
||||
function () use (&$buildSummary, $runtimeDir): int {
|
||||
/** @var ModuleRegistry $registry */
|
||||
$registry = app(ModuleRegistry::class);
|
||||
/** @var ModuleRuntimePageBuilder $builder */
|
||||
$builder = app(ModuleRuntimePageBuilder::class);
|
||||
$modules = $registry->getModules();
|
||||
$buildSummary = $builder->build(
|
||||
$runtimeDir,
|
||||
ModuleCliRuntime::projectRoot() . '/pages',
|
||||
$modules
|
||||
);
|
||||
ModuleRuntimePageBuilder::writeFingerprint($runtimeDir, $modules);
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
|
||||
if ($lockResult['busy']) {
|
||||
$message = (string) $lockResult['message'];
|
||||
}
|
||||
|
||||
return $lockResult['exit_code'];
|
||||
});
|
||||
|
||||
return [
|
||||
'command' => 'module:build',
|
||||
'status' => $step['status'],
|
||||
'exit_code' => $step['exit_code'],
|
||||
'vendor_warnings_ignored' => $step['vendor_warnings_ignored'],
|
||||
'core_entries' => (int) $buildSummary['core_entries'],
|
||||
'module_entries' => (int) $buildSummary['module_entries'],
|
||||
'message' => $step['error'] ?? $message,
|
||||
];
|
||||
}
|
||||
|
||||
public function assetsSync(): array
|
||||
{
|
||||
$summary = [
|
||||
'created' => 0,
|
||||
'updated' => 0,
|
||||
'removed' => 0,
|
||||
'unchanged' => 0,
|
||||
];
|
||||
$mode = trim((string) getenv('APP_MODULE_ASSET_MODE'));
|
||||
if ($mode === '') {
|
||||
$mode = 'symlink';
|
||||
}
|
||||
$message = 'module-assets-sync: ok';
|
||||
|
||||
$step = ModuleCliRuntime::runStep('module-assets-sync', function () use (&$summary, &$message, $mode): int {
|
||||
$lockResult = ModuleCliRuntime::withFileLock(
|
||||
ModuleCliRuntime::projectRoot() . '/storage/runtime/.module-assets-sync.lock',
|
||||
'module-assets-sync: another sync is in progress, skipping.',
|
||||
function () use (&$summary, $mode): int {
|
||||
/** @var ModuleRegistry $registry */
|
||||
$registry = app(ModuleRegistry::class);
|
||||
/** @var ModuleRuntimeAssetPublisher $publisher */
|
||||
$publisher = app(ModuleRuntimeAssetPublisher::class);
|
||||
$summary = $publisher->publish(
|
||||
ModuleCliRuntime::projectRoot() . '/web/modules',
|
||||
$registry->getModules(),
|
||||
$mode
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
|
||||
if ($lockResult['busy']) {
|
||||
$message = (string) $lockResult['message'];
|
||||
}
|
||||
|
||||
return $lockResult['exit_code'];
|
||||
});
|
||||
|
||||
return [
|
||||
'command' => 'module:assets-sync',
|
||||
'status' => $step['status'],
|
||||
'exit_code' => $step['exit_code'],
|
||||
'vendor_warnings_ignored' => $step['vendor_warnings_ignored'],
|
||||
'mode' => $mode,
|
||||
'created' => (int) $summary['created'],
|
||||
'updated' => (int) $summary['updated'],
|
||||
'removed' => (int) $summary['removed'],
|
||||
'unchanged' => (int) $summary['unchanged'],
|
||||
'message' => $step['error'] ?? $message,
|
||||
];
|
||||
}
|
||||
|
||||
public function deactivate(string $moduleId, bool $confirm, bool $dryRun): array
|
||||
{
|
||||
$message = '';
|
||||
|
||||
$step = ModuleCliRuntime::runStep('module-deactivate', function () use ($moduleId, $confirm, $dryRun, &$message): int {
|
||||
if ($moduleId === '') {
|
||||
$message = 'Error: module id is required.';
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!$confirm && !$dryRun) {
|
||||
$message = 'Error: must pass --confirm to execute or --dry-run to validate.';
|
||||
return 1;
|
||||
}
|
||||
|
||||
$manifest = ModuleManifestLoader::loadFromDisk(
|
||||
ModuleCliRuntime::projectRoot() . '/modules',
|
||||
$moduleId
|
||||
);
|
||||
|
||||
$handlerClass = $manifest->deactivationHandler;
|
||||
if ($handlerClass === null) {
|
||||
$message = "Module '{$moduleId}' has no deactivation_handler declared — nothing to do.";
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!class_exists($handlerClass)) {
|
||||
$message = "Error: deactivation handler class '{$handlerClass}' not found.";
|
||||
return 1;
|
||||
}
|
||||
|
||||
$container = $GLOBALS['minty_app_container'] ?? null;
|
||||
if (!$container instanceof AppContainer) {
|
||||
$message = 'Error: app container is not initialized.';
|
||||
return 1;
|
||||
}
|
||||
$handler = $container->has($handlerClass)
|
||||
? $container->get($handlerClass)
|
||||
: new $handlerClass();
|
||||
|
||||
if (!$handler instanceof ModuleDeactivationHandler) {
|
||||
$message = "Error: '{$handlerClass}' does not implement ModuleDeactivationHandler.";
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$message = "Dry-run: handler '{$handlerClass}' found and valid for module '{$moduleId}'.";
|
||||
return 0;
|
||||
}
|
||||
|
||||
$handler->deactivate($container);
|
||||
$message = "Deactivation handler for module '{$moduleId}' completed.";
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return [
|
||||
'command' => 'module:deactivate',
|
||||
'status' => $step['status'],
|
||||
'exit_code' => $step['exit_code'],
|
||||
'vendor_warnings_ignored' => $step['vendor_warnings_ignored'],
|
||||
'message' => $step['error'] ?? $message,
|
||||
];
|
||||
}
|
||||
|
||||
public function runtimeSync(): array
|
||||
{
|
||||
$startedAt = microtime(true);
|
||||
$steps = [
|
||||
'migrate' => ['status' => 'pending', 'exit_code' => 0, 'vendor_warnings_ignored' => 0],
|
||||
'permissions-sync' => ['status' => 'pending', 'exit_code' => 0, 'vendor_warnings_ignored' => 0],
|
||||
'build' => ['status' => 'pending', 'exit_code' => 0, 'vendor_warnings_ignored' => 0],
|
||||
'assets-sync' => ['status' => 'pending', 'exit_code' => 0, 'vendor_warnings_ignored' => 0],
|
||||
];
|
||||
$message = '';
|
||||
|
||||
$step = ModuleCliRuntime::runStep('module-runtime-sync', function () use (&$steps, &$message): int {
|
||||
$lockResult = ModuleCliRuntime::withFileLock(
|
||||
ModuleCliRuntime::projectRoot() . '/storage/runtime/.module-runtime-sync.lock',
|
||||
'module-runtime-sync: another runtime sync is in progress, skipping.',
|
||||
function () use (&$steps): int {
|
||||
$orderedSteps = [
|
||||
'migrate' => fn (): array => $this->migrate(),
|
||||
'permissions-sync' => fn (): array => $this->permissionsSync(),
|
||||
'build' => fn (): array => $this->build(),
|
||||
'assets-sync' => fn (): array => $this->assetsSync(),
|
||||
];
|
||||
|
||||
foreach ($orderedSteps as $stepName => $runner) {
|
||||
$result = $runner();
|
||||
$steps[$stepName] = [
|
||||
'status' => $result['status'],
|
||||
'exit_code' => $result['exit_code'],
|
||||
'vendor_warnings_ignored' => $result['vendor_warnings_ignored'],
|
||||
];
|
||||
|
||||
if ($result['exit_code'] !== 0) {
|
||||
return $result['exit_code'];
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
|
||||
if ($lockResult['busy']) {
|
||||
$message = (string) $lockResult['message'];
|
||||
}
|
||||
|
||||
return $lockResult['exit_code'];
|
||||
});
|
||||
|
||||
if ($message === '') {
|
||||
$message = sprintf(
|
||||
'module-runtime-sync: summary migrate=%s permissions-sync=%s build=%s assets-sync=%s vendor_warnings_ignored=%d',
|
||||
$steps['migrate']['status'],
|
||||
$steps['permissions-sync']['status'],
|
||||
$steps['build']['status'],
|
||||
$steps['assets-sync']['status'],
|
||||
$step['vendor_warnings_ignored']
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'command' => 'module:sync',
|
||||
'status' => $step['status'],
|
||||
'exit_code' => $step['exit_code'],
|
||||
'vendor_warnings_ignored_total' => $step['vendor_warnings_ignored'],
|
||||
'duration_ms' => max(0, (int) round((microtime(true) - $startedAt) * 1000)),
|
||||
'steps' => $steps,
|
||||
'message' => $step['error'] ?? $message,
|
||||
];
|
||||
}
|
||||
}
|
||||
82
lib/Console/Runner/Module/ModuleRunnerInterface.php
Normal file
82
lib/Console/Runner/Module/ModuleRunnerInterface.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MintyPHP\Console\Runner\Module;
|
||||
|
||||
interface ModuleRunnerInterface
|
||||
{
|
||||
/**
|
||||
* @return array{command: string, status: string, exit_code: int, vendor_warnings_ignored: int, message: string}
|
||||
*/
|
||||
public function migrate(): array;
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* command: string,
|
||||
* status: string,
|
||||
* exit_code: int,
|
||||
* vendor_warnings_ignored: int,
|
||||
* created: int,
|
||||
* updated: int,
|
||||
* unchanged: int,
|
||||
* deactivated: int,
|
||||
* total: int,
|
||||
* message: string
|
||||
* }
|
||||
*/
|
||||
public function permissionsSync(): array;
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* command: string,
|
||||
* status: string,
|
||||
* exit_code: int,
|
||||
* vendor_warnings_ignored: int,
|
||||
* core_entries: int,
|
||||
* module_entries: int,
|
||||
* message: string
|
||||
* }
|
||||
*/
|
||||
public function build(): array;
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* command: string,
|
||||
* status: string,
|
||||
* exit_code: int,
|
||||
* vendor_warnings_ignored: int,
|
||||
* mode: string,
|
||||
* created: int,
|
||||
* updated: int,
|
||||
* removed: int,
|
||||
* unchanged: int,
|
||||
* message: string
|
||||
* }
|
||||
*/
|
||||
public function assetsSync(): array;
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* command: string,
|
||||
* status: string,
|
||||
* exit_code: int,
|
||||
* vendor_warnings_ignored: int,
|
||||
* message: string
|
||||
* }
|
||||
*/
|
||||
public function deactivate(string $moduleId, bool $confirm, bool $dryRun): array;
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* command: string,
|
||||
* status: string,
|
||||
* exit_code: int,
|
||||
* vendor_warnings_ignored_total: int,
|
||||
* duration_ms: int,
|
||||
* steps: array<string, array{status: string, exit_code: int, vendor_warnings_ignored: int}>,
|
||||
* message: string
|
||||
* }
|
||||
*/
|
||||
public function runtimeSync(): array;
|
||||
}
|
||||
Reference in New Issue
Block a user