Add a module kernel (ModuleManifest, ModuleRegistry) that allows modules to contribute routes, UI slots, search providers, layout context, session lifecycle, and permissions. Modules are activated via config/modules.php or APP_ENABLED_MODULES env variable. Conflicts (duplicate routes, permissions, slot keys) cause a fail-fast with a clear error message. Extract the address book from hardcoded Core integration points into the first module (modules/addressbook/). The module provides: - Aside icon-bar tab + People panel via UI slot system - Global search resource via AddressBookSearchProvider - Layout context data via AddressBookLayoutProvider - Session lifecycle via AddressBookSessionProvider Core cleanup removes address-book hardcodings from SearchSqlResourceProvider, SearchUiMetaProvider, SearchItemMapperProvider, appBuildLayoutNavContext(), and the aside templates. Permissions (ADDRESS_BOOK_VIEW) and business logic (AddressBookService) remain in Core as they gate general user visibility. Includes 38 new tests (894 total), PHPStan level 5 clean, and architecture tests verifying zero hardcoded address-book references in search/templates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
119 lines
3.7 KiB
PHP
119 lines
3.7 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/**
|
|
* Applies pending module migrations for all enabled modules.
|
|
*
|
|
* Usage: php bin/module-migrate.php
|
|
*
|
|
* Each module declares a migrations_path in its manifest. SQL files in that
|
|
* directory are applied in alphabetical order. Applied files are tracked in the
|
|
* module_migrations table to ensure idempotency.
|
|
*
|
|
* Exit codes:
|
|
* 0 — all migrations applied (or nothing to do)
|
|
* 1 — error during migration
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
chdir(__DIR__ . '/..');
|
|
|
|
require 'vendor/autoload.php';
|
|
require 'config/config.php';
|
|
require 'lib/Support/helpers.php';
|
|
$container = require 'lib/App/registerContainer.php';
|
|
setAppContainer($container);
|
|
|
|
use MintyPHP\App\Module\ModuleRegistry;
|
|
use MintyPHP\DB;
|
|
|
|
$exitCode = 0;
|
|
$totalApplied = 0;
|
|
|
|
try {
|
|
$registry = app(ModuleRegistry::class);
|
|
$modules = $registry->getModules();
|
|
|
|
if (count($modules) === 0) {
|
|
fwrite(STDOUT, "module-migrate: no modules enabled, nothing to do.\n");
|
|
exit(0);
|
|
}
|
|
|
|
// Ensure tracking table exists (safe if already created by db/updates/ script)
|
|
DB::execute(
|
|
'CREATE TABLE IF NOT EXISTS `module_migrations` (
|
|
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
`module_id` VARCHAR(64) NOT NULL,
|
|
`filename` VARCHAR(255) NOT NULL,
|
|
`applied_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (`id`),
|
|
UNIQUE KEY `uq_module_migration` (`module_id`, `filename`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
|
);
|
|
|
|
foreach ($modules as $manifest) {
|
|
$migrationsPath = $manifest->migrationsPath;
|
|
if ($migrationsPath === null || !is_dir($migrationsPath)) {
|
|
continue;
|
|
}
|
|
|
|
// Discover SQL files
|
|
$files = glob($migrationsPath . '/*.sql');
|
|
if ($files === false || count($files) === 0) {
|
|
continue;
|
|
}
|
|
sort($files); // alphabetical order
|
|
|
|
// Get already-applied filenames
|
|
$applied = [];
|
|
$rows = DB::select('SELECT filename FROM module_migrations WHERE module_id = ?', [$manifest->id]);
|
|
foreach ($rows as $row) {
|
|
$applied[$row['filename']] = true;
|
|
}
|
|
|
|
foreach ($files as $file) {
|
|
$filename = basename($file);
|
|
if (isset($applied[$filename])) {
|
|
continue;
|
|
}
|
|
|
|
$sql = file_get_contents($file);
|
|
if ($sql === false || trim($sql) === '') {
|
|
fwrite(STDERR, sprintf("module-migrate: WARNING skipping empty file %s/%s\n", $manifest->id, $filename));
|
|
continue;
|
|
}
|
|
|
|
fwrite(STDOUT, sprintf("module-migrate: applying %s/%s ... ", $manifest->id, $filename));
|
|
|
|
// Execute migration (may contain multiple statements)
|
|
$statements = array_filter(array_map('trim', explode(';', $sql)), static fn (string $s): bool => $s !== '');
|
|
foreach ($statements as $statement) {
|
|
DB::execute($statement);
|
|
}
|
|
|
|
// Record as applied
|
|
DB::execute(
|
|
'INSERT INTO module_migrations (module_id, filename) VALUES (?, ?)',
|
|
[$manifest->id, $filename]
|
|
);
|
|
|
|
fwrite(STDOUT, "OK\n");
|
|
$totalApplied++;
|
|
}
|
|
}
|
|
|
|
if ($totalApplied === 0) {
|
|
fwrite(STDOUT, "module-migrate: all modules up to date, 0 new migrations.\n");
|
|
} else {
|
|
fwrite(STDOUT, sprintf("module-migrate: applied %d migration(s).\n", $totalApplied));
|
|
}
|
|
} catch (Throwable $exception) {
|
|
fwrite(STDERR, sprintf("module-migrate: FAILED: %s\n", $exception->getMessage()));
|
|
$exitCode = 1;
|
|
} finally {
|
|
DB::close();
|
|
}
|
|
|
|
exit($exitCode);
|