Refine module manifest discovery handling

This commit is contained in:
michele 2026-04-20 10:40:59 +00:00
parent c883063543
commit dd825b4450
2 changed files with 282 additions and 53 deletions

View File

@ -1,11 +1,10 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\DB;
use App\Support\Modules\ModuleManifest;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use ZipArchive;
@ -31,7 +30,7 @@ public function __construct()
*/
protected function loadActiveModules()
{
if (!$this->modulesTableExists()) {
if (! $this->modulesTableExists()) {
$this->activeModules = [];
return;
}
@ -61,7 +60,7 @@ public function installModule($zipPath, $validateLicense = true)
$config = $this->validateModuleConfig($extractPath);
// 2. Verifica licenza (se richiesta)
if ($validateLicense && !$this->validateLicense($config)) {
if ($validateLicense && ! $this->validateLicense($config)) {
throw new \Exception("Licenza non valida per il modulo {$config['name']}");
}
@ -97,7 +96,7 @@ public function installModule($zipPath, $validateLicense = true)
return [
'success' => true,
'message' => "Modulo {$config['display_name']} installato con successo",
'module' => $config
'module' => $config,
];
} catch (\Exception $e) {
Log::error("Errore installazione modulo: " . $e->getMessage());
@ -109,7 +108,7 @@ public function installModule($zipPath, $validateLicense = true)
return [
'success' => false,
'message' => $e->getMessage()
'message' => $e->getMessage(),
];
}
}
@ -126,12 +125,12 @@ public function uninstallModule($moduleName, $keepData = false)
// 1. Controlla dipendenze inverse
$dependentModules = $this->getDependentModules($moduleName);
if (!empty($dependentModules)) {
if (! empty($dependentModules)) {
throw new \Exception("Impossibile disinstallare: moduli dipendenti attivi: " . implode(', ', $dependentModules));
}
// 2. Backup prima della disinstallazione
if (!$keepData) {
if (! $keepData) {
$this->createBackup($moduleName);
}
@ -148,7 +147,7 @@ public function uninstallModule($moduleName, $keepData = false)
$this->removeMenuItems($config);
// 7. Esegui migrazioni rollback (se non si tengono i dati)
if (!$keepData) {
if (! $keepData) {
$this->rollbackMigrations($moduleName);
}
@ -168,14 +167,14 @@ public function uninstallModule($moduleName, $keepData = false)
return [
'success' => true,
'message' => "Modulo {$moduleName} disinstallato con successo"
'message' => "Modulo {$moduleName} disinstallato con successo",
];
} catch (\Exception $e) {
Log::error("Errore disinstallazione modulo: " . $e->getMessage());
return [
'success' => false,
'message' => $e->getMessage()
'message' => $e->getMessage(),
];
}
}
@ -198,7 +197,7 @@ public function updateModule($moduleName, $zipPath)
$newConfig = $this->validateModuleConfig($extractPath);
// 3. Verifica compatibilità versioni
if (!$this->isCompatibleVersion($oldConfig, $newConfig)) {
if (! $this->isCompatibleVersion($oldConfig, $newConfig)) {
throw new \Exception("Versione non compatibile");
}
@ -226,7 +225,7 @@ public function updateModule($moduleName, $zipPath)
'success' => true,
'message' => "Modulo {$moduleName} aggiornato alla versione {$newConfig['version']}",
'old_version' => $oldConfig['version'],
'new_version' => $newConfig['version']
'new_version' => $newConfig['version'],
];
} catch (\Exception $e) {
Log::error("Errore aggiornamento modulo: " . $e->getMessage());
@ -238,7 +237,7 @@ public function updateModule($moduleName, $zipPath)
return [
'success' => false,
'message' => $e->getMessage()
'message' => $e->getMessage(),
];
}
}
@ -258,16 +257,45 @@ public function getActiveModules()
*/
public function getAllModules()
{
if (!$this->modulesTableExists()) {
if (! $this->modulesTableExists()) {
return collect();
}
return DB::table('modules')->get()->map(function ($module) {
$module->config = json_decode($module->config, true);
$config = json_decode($module->config, true);
$module->config = is_array($config) ? ModuleManifest::normalize($config) : [];
return $module;
});
}
/**
* Ottieni il catalogo locale dei moduli con manifest normalizzato.
*/
public function discoverLocalModules()
{
if (! File::isDirectory($this->modulesPath)) {
return collect();
}
return collect(File::directories($this->modulesPath))
->map(function (string $modulePath) {
$moduleName = basename($modulePath);
try {
return (object) $this->getModuleConfig($moduleName);
} catch (\Throwable $e) {
Log::warning('ModuleManager: manifest locale non leggibile', [
'module' => $moduleName,
'error' => $e->getMessage(),
]);
return null;
}
})
->filter()
->values();
}
/**
* Verifica licenza modulo (connessione server licenze)
*/
@ -293,13 +321,13 @@ public function validateLicense($config)
*/
public function toggleModule($moduleName)
{
if (!$this->modulesTableExists()) {
if (! $this->modulesTableExists()) {
throw new \Exception('Tabella modules non disponibile');
}
$module = DB::table('modules')->where('name', $moduleName)->first();
if (!$module) {
if (! $module) {
throw new \Exception("Modulo {$moduleName} non trovato");
}
@ -307,7 +335,7 @@ public function toggleModule($moduleName)
DB::table('modules')->where('name', $moduleName)->update([
'status' => $newStatus,
'updated_at' => now()
'updated_at' => now(),
]);
$this->clearModuleCache();
@ -324,7 +352,7 @@ private function extractModule($zipPath)
$zip = new ZipArchive;
$extractPath = storage_path('app/modules/temp/' . uniqid());
if ($zip->open($zipPath) === TRUE) {
if ($zip->open($zipPath) === true) {
$zip->extractTo($extractPath);
$zip->close();
return $extractPath;
@ -337,27 +365,27 @@ private function validateModuleConfig($extractPath)
{
$configPath = $extractPath . '/module.json';
if (!File::exists($configPath)) {
if (! File::exists($configPath)) {
throw new \Exception("File module.json non trovato");
}
$config = json_decode(File::get($configPath), true);
if (!$config || !isset($config['name'], $config['version'])) {
if (! $config || ! isset($config['name'], $config['version'])) {
throw new \Exception("Configurazione modulo non valida");
}
return $config;
return ModuleManifest::normalize($config);
}
private function checkDependencies($config)
{
if (!isset($config['dependencies'])) {
if (! isset($config['dependencies'])) {
return true;
}
foreach ($config['dependencies'] as $dependency) {
if (!in_array($dependency, $this->activeModules)) {
if (! in_array($dependency, $this->activeModules)) {
throw new \Exception("Dipendenza mancante: {$dependency}");
}
}
@ -367,20 +395,22 @@ private function checkDependencies($config)
private function registerModule($config)
{
if (!$this->modulesTableExists()) {
if (! $this->modulesTableExists()) {
throw new \Exception('Tabella modules non disponibile');
}
$config = ModuleManifest::normalize(is_array($config) ? $config : []);
DB::table('modules')->updateOrInsert(
['name' => $config['name']],
[
'name' => $config['name'],
'display_name' => $config['display_name'],
'display_name' => $config['display_name'] ?? $config['name'],
'version' => $config['version'],
'status' => 'active',
'config' => json_encode($config),
'installed_at' => now(),
'updated_at' => now()
'updated_at' => now(),
]
);
}
@ -399,11 +429,16 @@ private function getModuleConfig($moduleName)
{
$configPath = $this->modulesPath . "/{$moduleName}/module.json";
if (!File::exists($configPath)) {
if (! File::exists($configPath)) {
throw new \Exception("Configurazione modulo {$moduleName} non trovata");
}
return json_decode(File::get($configPath), true);
$config = json_decode(File::get($configPath), true);
if (! is_array($config)) {
throw new \Exception("Configurazione modulo {$moduleName} non valida");
}
return ModuleManifest::normalize($config);
}
private function getModuleInfo($moduleName)
@ -425,7 +460,7 @@ private function getModuleInfo($moduleName)
return (object) array_merge($config, [
'status' => $dbInfo->status ?? 'unknown',
'installed_at' => $dbInfo->installed_at ?? null,
'updated_at' => $dbInfo->updated_at ?? null
'updated_at' => $dbInfo->updated_at ?? null,
]);
}
@ -451,7 +486,9 @@ private function registerRoutes($moduleName)
}
private function installPermissions($config)
{
if (!isset($config['permissions']) || empty($config['permissions'])) {
$config = ModuleManifest::normalize(is_array($config) ? $config : []);
if (! isset($config['permissions']) || empty($config['permissions'])) {
return;
}
@ -463,7 +500,7 @@ private function installPermissions($config)
// Crea il permesso se non esiste
$existingPermission = \Spatie\Permission\Models\Permission::where('name', $permission['name'])->first();
if (!$existingPermission) {
if (! $existingPermission) {
$newPermission = \Spatie\Permission\Models\Permission::create([
'name' => $permission['name'],
'guard_name' => $permission['guard_name'] ?? 'web',
@ -502,7 +539,7 @@ private function assignPermissionsToRoles($permissions)
if ($role) {
// Assegna il permesso al ruolo se non ce l'ha già
if (!$role->hasPermissionTo($permName)) {
if (! $role->hasPermissionTo($permName)) {
$role->givePermissionTo($permName);
Log::info("Permesso {$permName} assegnato al ruolo {$roleName}");
}
@ -513,7 +550,7 @@ private function assignPermissionsToRoles($permissions)
private function uninstallPermissions($config)
{
if (!isset($config['permissions']) || empty($config['permissions'])) {
if (! isset($config['permissions']) || empty($config['permissions'])) {
return;
}

View File

@ -0,0 +1,192 @@
<?php
namespace App\Support\Modules;
final class ModuleManifest
{
/**
* @param array<string, mixed> $manifest
* @return array<string, mixed>
*/
public static function normalize(array $manifest): array
{
$normalized = $manifest;
$name = trim((string) ($manifest['name'] ?? ''));
$normalized['name'] = $name;
$normalized['display_name'] = self::normalizeNullableString($manifest['display_name'] ?? null) ?? self::normalizeNullableString($manifest['title'] ?? null) ?? $name;
$normalized['description'] = self::normalizeNullableString($manifest['description'] ?? null);
$normalized['version'] = self::normalizeNullableString($manifest['version'] ?? null) ?? '0.0.0';
$normalized['author'] = self::normalizeNullableString($manifest['author'] ?? null);
$normalized['license'] = self::normalizeNullableString($manifest['license'] ?? null) ?? 'free';
$normalized['category'] = self::normalizeNullableString($manifest['category'] ?? null);
$normalized['dependencies'] = self::normalizeStringList($manifest['dependencies'] ?? []);
$normalized['optional_dependencies'] = self::normalizeStringList($manifest['optional_dependencies'] ?? []);
$normalized['hooks'] = self::normalizeStringList($manifest['hooks'] ?? []);
$normalized['features'] = self::normalizeStringList($manifest['features'] ?? []);
$normalized['permissions'] = self::normalizePermissions($manifest['permissions'] ?? []);
$normalized['menu_items'] = self::normalizeMenuItems($manifest['menu_items'] ?? []);
$normalized['requirements'] = is_array($manifest['requirements'] ?? null) ? $manifest['requirements'] : [];
$normalized['config'] = is_array($manifest['config'] ?? null) ? $manifest['config'] : [];
$normalized['manifest_version'] = (int) ($manifest['manifest_version'] ?? 1);
return $normalized;
}
/**
* @param mixed $value
*/
private static function normalizeNullableString(mixed $value): ?string
{
if (! is_scalar($value)) {
return null;
}
$normalized = trim((string) $value);
return $normalized !== '' ? $normalized : null;
}
/**
* @param mixed $value
* @return array<int, string>
*/
private static function normalizeStringList(mixed $value): array
{
if (! is_array($value)) {
return [];
}
$normalized = [];
foreach ($value as $item) {
$item = self::normalizeNullableString($item);
if ($item !== null) {
$normalized[] = $item;
}
}
return array_values(array_unique($normalized));
}
/**
* @param mixed $value
* @return array<int, array{name:string,display_name:string,description:?string,guard_name:string}>
*/
private static function normalizePermissions(mixed $value): array
{
if (! is_array($value)) {
return [];
}
$permissions = [];
if (self::isAssociative($value)) {
foreach ($value as $name => $description) {
$permissionName = self::normalizeNullableString($name);
if ($permissionName === null) {
continue;
}
$display = self::normalizeNullableString($description) ?? self::humanizePermissionName($permissionName);
$permissions[] = [
'name' => $permissionName,
'display_name' => $display,
'description' => self::normalizeNullableString($description) ?? $display,
'guard_name' => 'web',
];
}
return $permissions;
}
foreach ($value as $permission) {
if (is_string($permission)) {
$permissionName = self::normalizeNullableString($permission);
if ($permissionName === null) {
continue;
}
$permissions[] = [
'name' => $permissionName,
'display_name' => self::humanizePermissionName($permissionName),
'description' => null,
'guard_name' => 'web',
];
continue;
}
if (! is_array($permission)) {
continue;
}
$permissionName = self::normalizeNullableString($permission['name'] ?? null);
if ($permissionName === null) {
continue;
}
$permissions[] = [
'name' => $permissionName,
'display_name' => self::normalizeNullableString($permission['display_name'] ?? null) ?? self::humanizePermissionName($permissionName),
'description' => self::normalizeNullableString($permission['description'] ?? null),
'guard_name' => self::normalizeNullableString($permission['guard_name'] ?? null) ?? 'web',
];
}
return $permissions;
}
/**
* @param mixed $value
* @return array<int, array<string, mixed>>
*/
private static function normalizeMenuItems(mixed $value): array
{
if (! is_array($value)) {
return [];
}
$menuItems = [];
foreach ($value as $item) {
if (! is_array($item)) {
continue;
}
$title = self::normalizeNullableString($item['title'] ?? null);
$route = self::normalizeNullableString($item['route'] ?? null);
if ($title === null || $route === null) {
continue;
}
$menuItems[] = [
'title' => $title,
'route' => $route,
'icon' => self::normalizeNullableString($item['icon'] ?? null),
'permission' => self::normalizeNullableString($item['permission'] ?? null),
'parent' => self::normalizeNullableString($item['parent'] ?? null),
'description' => self::normalizeNullableString($item['description'] ?? null),
'order' => isset($item['order']) && is_numeric($item['order']) ? (int) $item['order'] : 0,
'badge' => $item['badge'] ?? null,
];
}
usort($menuItems, static fn(array $left, array $right): int => ($left['order'] ?? 0) <=> ($right['order'] ?? 0));
return $menuItems;
}
/**
* @param array<mixed> $value
*/
private static function isAssociative(array $value): bool
{
return array_keys($value) !== range(0, count($value) - 1);
}
private static function humanizePermissionName(string $permissionName): string
{
$label = str_replace(['.', '_', '-'], ' ', $permissionName);
return ucfirst(trim(preg_replace('/\s+/', ' ', $label) ?? $label));
}
}