1
0
Files

450 lines
18 KiB
PHP
Raw Permalink Normal View History

<?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;
feat(console): add db:migrate CLI for idempotent core schema updates Existing systems used to have no automatic mechanism for the db/updates/*.sql idempotent updates after `git pull` — devs had to remember which files were already applied and run new ones manually with `mariadb < ...`. Mirror module:migrate to fix this: - New `bin/console db:migrate` walks db/updates/*.sql in alphabetical order, skipping anything already recorded in the new core_migrations tracking table. Each file runs in its own transaction; failure rolls back so the file is retried on the next run. - New `db:migrate --status` lists applied vs. pending files without running anything. Useful for ops debugging "schema seems stale" symptoms. - module:sync now runs db:migrate as its first step, so the standard post-pull command stays a single invocation. README's 3-step setup is unchanged — module:sync now also covers core schema updates. CoreMigrationRepository owns its own mysqli connection (using the same DB credentials as MintyPHP\DB) and runs raw `$mysqli->query()` instead of going through DB::query's prepared statement path — MariaDB rejects some DDL (e.g. ALTER TABLE … ADD CONSTRAINT CHECK) in the prepared protocol, which the existing 18 db/updates files trip on. ModuleMigrationRepository is left untouched (no module migration uses such DDL today and changing the vendor pattern is out of scope). End-to-end verified: against an existing DB the first run applies all 19 idempotent files as no-ops and records them, second run reports "all updates already applied". Against a freshly init'd DB the same thing happens — no double work, no surprises. All encrypted seed secrets keep decrypting because APP_CRYPTO_KEY is unchanged. Tests: tests/Console/CoreCommandsTest covers success/failure/status output shapes against a FakeModuleRunner (mirror of the existing ModuleCommandsTest pattern). 5 stale baseline entries in phpstan-baseline removed (covered by the new @api annotation on the test fixture class). Docs: CLAUDE.md and docs/reference-cli-commands.md document the new command + --status flag; docs/howto-fehlerbehebung.md gains a "schema seems stale after git pull" section pointing at db:migrate --status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:59:57 +02:00
use MintyPHP\Console\Support\CliAppBootstrap;
use MintyPHP\Console\Support\ModuleCliRuntime;
feat(console): add db:migrate CLI for idempotent core schema updates Existing systems used to have no automatic mechanism for the db/updates/*.sql idempotent updates after `git pull` — devs had to remember which files were already applied and run new ones manually with `mariadb < ...`. Mirror module:migrate to fix this: - New `bin/console db:migrate` walks db/updates/*.sql in alphabetical order, skipping anything already recorded in the new core_migrations tracking table. Each file runs in its own transaction; failure rolls back so the file is retried on the next run. - New `db:migrate --status` lists applied vs. pending files without running anything. Useful for ops debugging "schema seems stale" symptoms. - module:sync now runs db:migrate as its first step, so the standard post-pull command stays a single invocation. README's 3-step setup is unchanged — module:sync now also covers core schema updates. CoreMigrationRepository owns its own mysqli connection (using the same DB credentials as MintyPHP\DB) and runs raw `$mysqli->query()` instead of going through DB::query's prepared statement path — MariaDB rejects some DDL (e.g. ALTER TABLE … ADD CONSTRAINT CHECK) in the prepared protocol, which the existing 18 db/updates files trip on. ModuleMigrationRepository is left untouched (no module migration uses such DDL today and changing the vendor pattern is out of scope). End-to-end verified: against an existing DB the first run applies all 19 idempotent files as no-ops and records them, second run reports "all updates already applied". Against a freshly init'd DB the same thing happens — no double work, no surprises. All encrypted seed secrets keep decrypting because APP_CRYPTO_KEY is unchanged. Tests: tests/Console/CoreCommandsTest covers success/failure/status output shapes against a FakeModuleRunner (mirror of the existing ModuleCommandsTest pattern). 5 stale baseline entries in phpstan-baseline removed (covered by the new @api annotation on the test fixture class). Docs: CLAUDE.md and docs/reference-cli-commands.md document the new command + --status flag; docs/howto-fehlerbehebung.md gains a "schema seems stale after git pull" section pointing at db:migrate --status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:59:57 +02:00
use MintyPHP\Service\Database\CoreMigrationService;
use MintyPHP\Service\Module\ModuleMigrationService;
final class ModuleRunner implements ModuleRunnerInterface
{
feat(console): add db:migrate CLI for idempotent core schema updates Existing systems used to have no automatic mechanism for the db/updates/*.sql idempotent updates after `git pull` — devs had to remember which files were already applied and run new ones manually with `mariadb < ...`. Mirror module:migrate to fix this: - New `bin/console db:migrate` walks db/updates/*.sql in alphabetical order, skipping anything already recorded in the new core_migrations tracking table. Each file runs in its own transaction; failure rolls back so the file is retried on the next run. - New `db:migrate --status` lists applied vs. pending files without running anything. Useful for ops debugging "schema seems stale" symptoms. - module:sync now runs db:migrate as its first step, so the standard post-pull command stays a single invocation. README's 3-step setup is unchanged — module:sync now also covers core schema updates. CoreMigrationRepository owns its own mysqli connection (using the same DB credentials as MintyPHP\DB) and runs raw `$mysqli->query()` instead of going through DB::query's prepared statement path — MariaDB rejects some DDL (e.g. ALTER TABLE … ADD CONSTRAINT CHECK) in the prepared protocol, which the existing 18 db/updates files trip on. ModuleMigrationRepository is left untouched (no module migration uses such DDL today and changing the vendor pattern is out of scope). End-to-end verified: against an existing DB the first run applies all 19 idempotent files as no-ops and records them, second run reports "all updates already applied". Against a freshly init'd DB the same thing happens — no double work, no surprises. All encrypted seed secrets keep decrypting because APP_CRYPTO_KEY is unchanged. Tests: tests/Console/CoreCommandsTest covers success/failure/status output shapes against a FakeModuleRunner (mirror of the existing ModuleCommandsTest pattern). 5 stale baseline entries in phpstan-baseline removed (covered by the new @api annotation on the test fixture class). Docs: CLAUDE.md and docs/reference-cli-commands.md document the new command + --status flag; docs/howto-fehlerbehebung.md gains a "schema seems stale after git pull" section pointing at db:migrate --status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:59:57 +02:00
public function dbMigrate(): array
{
$summary = ['applied' => 0, 'skipped_empty' => 0, 'total_files' => 0, 'applied_filenames' => []];
$message = 'db-migrate: all updates already applied.';
$step = ModuleCliRuntime::runStep('db-migrate', function () use (&$summary, &$message): int {
$lockResult = ModuleCliRuntime::withFileLock(
ModuleCliRuntime::projectRoot() . '/storage/runtime/.db-migrate.lock',
'db-migrate: another migration run is in progress, skipping.',
function () use (&$summary, &$message): int {
/** @var CoreMigrationService $service */
$service = app(CoreMigrationService::class);
$result = $service->applyPendingMigrations();
$summary = $result;
if ($result['total_files'] === 0) {
$message = 'db-migrate: no update files in db/updates/, nothing to do.';
} elseif ($result['applied'] === 0) {
$message = 'db-migrate: all updates already applied.';
} else {
$message = sprintf('db-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' => 'db:migrate',
'status' => $step['status'],
'exit_code' => $step['exit_code'],
'vendor_warnings_ignored' => $step['vendor_warnings_ignored'],
'message' => $step['error'] ?? $message,
];
}
public function dbMigrateStatus(): array
{
CliAppBootstrap::bootstrap('cli', 'bin/console db:migrate --status');
/** @var CoreMigrationService $service */
$service = app(CoreMigrationService::class);
$status = $service->status();
return [
'command' => 'db:migrate --status',
'applied' => $status['applied'],
'pending' => $status['pending'],
];
}
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;
}
$modulesDir = ModuleCliRuntime::projectRoot() . '/modules';
$manifest = ModuleManifestLoader::loadFromDisk($modulesDir, $moduleId);
// Check for reverse dependencies (other modules that require this one)
$dependents = $this->findReverseDependencies($modulesDir, $moduleId);
$warning = '';
if ($dependents !== []) {
$dependentList = implode(', ', $dependents);
$warning = "Warning: module(s) [{$dependentList}] declare '{$moduleId}' in their requires — deactivation may break them.\n";
}
$handlerClass = $manifest->deactivationHandler;
if ($handlerClass === null) {
$message = $warning . "Module '{$moduleId}' has no deactivation_handler declared — nothing to do.";
return 0;
}
if (!class_exists($handlerClass)) {
$message = $warning . "Error: deactivation handler class '{$handlerClass}' not found.";
return 1;
}
$container = $GLOBALS['minty_app_container'] ?? null;
if (!$container instanceof AppContainer) {
$message = $warning . 'Error: app container is not initialized.';
return 1;
}
$handler = $container->has($handlerClass)
? $container->get($handlerClass)
: new $handlerClass();
if (!$handler instanceof ModuleDeactivationHandler) {
$message = $warning . "Error: '{$handlerClass}' does not implement ModuleDeactivationHandler.";
return 1;
}
if ($dryRun) {
$message = $warning . "Dry-run: handler '{$handlerClass}' found and valid for module '{$moduleId}'.";
return 0;
}
$handler->deactivate($container);
$message = $warning . "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 = [
feat(console): add db:migrate CLI for idempotent core schema updates Existing systems used to have no automatic mechanism for the db/updates/*.sql idempotent updates after `git pull` — devs had to remember which files were already applied and run new ones manually with `mariadb < ...`. Mirror module:migrate to fix this: - New `bin/console db:migrate` walks db/updates/*.sql in alphabetical order, skipping anything already recorded in the new core_migrations tracking table. Each file runs in its own transaction; failure rolls back so the file is retried on the next run. - New `db:migrate --status` lists applied vs. pending files without running anything. Useful for ops debugging "schema seems stale" symptoms. - module:sync now runs db:migrate as its first step, so the standard post-pull command stays a single invocation. README's 3-step setup is unchanged — module:sync now also covers core schema updates. CoreMigrationRepository owns its own mysqli connection (using the same DB credentials as MintyPHP\DB) and runs raw `$mysqli->query()` instead of going through DB::query's prepared statement path — MariaDB rejects some DDL (e.g. ALTER TABLE … ADD CONSTRAINT CHECK) in the prepared protocol, which the existing 18 db/updates files trip on. ModuleMigrationRepository is left untouched (no module migration uses such DDL today and changing the vendor pattern is out of scope). End-to-end verified: against an existing DB the first run applies all 19 idempotent files as no-ops and records them, second run reports "all updates already applied". Against a freshly init'd DB the same thing happens — no double work, no surprises. All encrypted seed secrets keep decrypting because APP_CRYPTO_KEY is unchanged. Tests: tests/Console/CoreCommandsTest covers success/failure/status output shapes against a FakeModuleRunner (mirror of the existing ModuleCommandsTest pattern). 5 stale baseline entries in phpstan-baseline removed (covered by the new @api annotation on the test fixture class). Docs: CLAUDE.md and docs/reference-cli-commands.md document the new command + --status flag; docs/howto-fehlerbehebung.md gains a "schema seems stale after git pull" section pointing at db:migrate --status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:59:57 +02:00
'db-migrate' => ['status' => 'pending', 'exit_code' => 0, 'vendor_warnings_ignored' => 0],
'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 = [
feat(console): add db:migrate CLI for idempotent core schema updates Existing systems used to have no automatic mechanism for the db/updates/*.sql idempotent updates after `git pull` — devs had to remember which files were already applied and run new ones manually with `mariadb < ...`. Mirror module:migrate to fix this: - New `bin/console db:migrate` walks db/updates/*.sql in alphabetical order, skipping anything already recorded in the new core_migrations tracking table. Each file runs in its own transaction; failure rolls back so the file is retried on the next run. - New `db:migrate --status` lists applied vs. pending files without running anything. Useful for ops debugging "schema seems stale" symptoms. - module:sync now runs db:migrate as its first step, so the standard post-pull command stays a single invocation. README's 3-step setup is unchanged — module:sync now also covers core schema updates. CoreMigrationRepository owns its own mysqli connection (using the same DB credentials as MintyPHP\DB) and runs raw `$mysqli->query()` instead of going through DB::query's prepared statement path — MariaDB rejects some DDL (e.g. ALTER TABLE … ADD CONSTRAINT CHECK) in the prepared protocol, which the existing 18 db/updates files trip on. ModuleMigrationRepository is left untouched (no module migration uses such DDL today and changing the vendor pattern is out of scope). End-to-end verified: against an existing DB the first run applies all 19 idempotent files as no-ops and records them, second run reports "all updates already applied". Against a freshly init'd DB the same thing happens — no double work, no surprises. All encrypted seed secrets keep decrypting because APP_CRYPTO_KEY is unchanged. Tests: tests/Console/CoreCommandsTest covers success/failure/status output shapes against a FakeModuleRunner (mirror of the existing ModuleCommandsTest pattern). 5 stale baseline entries in phpstan-baseline removed (covered by the new @api annotation on the test fixture class). Docs: CLAUDE.md and docs/reference-cli-commands.md document the new command + --status flag; docs/howto-fehlerbehebung.md gains a "schema seems stale after git pull" section pointing at db:migrate --status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:59:57 +02:00
'db-migrate' => fn (): array => $this->dbMigrate(),
'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(
feat(console): add db:migrate CLI for idempotent core schema updates Existing systems used to have no automatic mechanism for the db/updates/*.sql idempotent updates after `git pull` — devs had to remember which files were already applied and run new ones manually with `mariadb < ...`. Mirror module:migrate to fix this: - New `bin/console db:migrate` walks db/updates/*.sql in alphabetical order, skipping anything already recorded in the new core_migrations tracking table. Each file runs in its own transaction; failure rolls back so the file is retried on the next run. - New `db:migrate --status` lists applied vs. pending files without running anything. Useful for ops debugging "schema seems stale" symptoms. - module:sync now runs db:migrate as its first step, so the standard post-pull command stays a single invocation. README's 3-step setup is unchanged — module:sync now also covers core schema updates. CoreMigrationRepository owns its own mysqli connection (using the same DB credentials as MintyPHP\DB) and runs raw `$mysqli->query()` instead of going through DB::query's prepared statement path — MariaDB rejects some DDL (e.g. ALTER TABLE … ADD CONSTRAINT CHECK) in the prepared protocol, which the existing 18 db/updates files trip on. ModuleMigrationRepository is left untouched (no module migration uses such DDL today and changing the vendor pattern is out of scope). End-to-end verified: against an existing DB the first run applies all 19 idempotent files as no-ops and records them, second run reports "all updates already applied". Against a freshly init'd DB the same thing happens — no double work, no surprises. All encrypted seed secrets keep decrypting because APP_CRYPTO_KEY is unchanged. Tests: tests/Console/CoreCommandsTest covers success/failure/status output shapes against a FakeModuleRunner (mirror of the existing ModuleCommandsTest pattern). 5 stale baseline entries in phpstan-baseline removed (covered by the new @api annotation on the test fixture class). Docs: CLAUDE.md and docs/reference-cli-commands.md document the new command + --status flag; docs/howto-fehlerbehebung.md gains a "schema seems stale after git pull" section pointing at db:migrate --status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:59:57 +02:00
'module-runtime-sync: summary db-migrate=%s migrate=%s permissions-sync=%s build=%s assets-sync=%s vendor_warnings_ignored=%d',
$steps['db-migrate']['status'],
$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,
];
}
/**
* @return list<string> Module IDs that declare $targetId in their requires
*/
private function findReverseDependencies(string $modulesDir, string $targetId): array
{
$dependents = [];
$entries = is_dir($modulesDir) ? (scandir($modulesDir) ?: []) : [];
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..' || $entry === $targetId) {
continue;
}
$manifestFile = $modulesDir . '/' . $entry . '/module.php';
if (!is_file($manifestFile)) {
continue;
}
try {
$manifest = ModuleManifestLoader::loadFromDisk($modulesDir, $entry);
if (in_array($targetId, $manifest->requires, true)) {
$dependents[] = $entry;
}
} catch (\Throwable) {
// Skip unparseable manifests
}
}
sort($dependents);
return $dependents;
}
}