Refine module manifest discovery handling
This commit is contained in:
parent
c883063543
commit
dd825b4450
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
192
app/Support/Modules/ModuleManifest.php
Normal file
192
app/Support/Modules/ModuleManifest.php
Normal 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));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user