Guida Sistema Moduli
Ultimo aggiornamento: 2026-04-26
Scopo
Questa guida descrive come Logeon gestisce i moduli:
- rilevamento su filesystem;
- stato persistito su database;
- lifecycle completo (attivazione, disattivazione, disinstallazione);
- integrazione UI additiva senza toccare il core;
- isolamento completo del codice modulo dalla cartella
/app/.
Principio fondamentale: isolamento totale
Un modulo non tocca mai /app/ né /core/.
Il codice del modulo (controller, service, model, rotte, asset, migrazioni) vive esclusivamente
nella propria cartella modules/<vendor.modulo>/.
La comunicazione tra core e modulo avviene esclusivamente tramite il sistema hook (Core\Hooks).
Il core non importa classi del modulo. Il modulo non modifica file del core.
Questa regola garantisce che disinstallare un modulo significhi:
- eseguire
uninstall.sqlper ripulire DB; - eliminare la cartella
modules/<vendor.modulo>/.
Zero codice residuo in /app/ o /core/.
Confine Core vs Moduli
- Il core gestisce solo l'orchestrazione moduli (
/admin/modules/*) e il sistema hook. - Le API funzionali di un modulo vanno documentate nella guida del modulo stesso.
docs/contratti-api-backend.mdinclude solo i contratti core; le API dei moduli sono escluse.- Il core non conosce le classi concrete del modulo. Usa i risultati degli hook senza sapere chi li produce.
Comunicazione core ↔ modulo via hook
Il sistema hook (Core\Hooks) è l'unico canale di comunicazione bidirezionale.
Il core emette un hook (filter o action) e lavora con il risultato come dato grezzo.
Il modulo registra un handler sull'hook nel proprio bootstrap.php.
Esempio — il core chiede il provider archetipi:
// core (già presente, non va modificato)
$provider = \Core\Hooks::filter('character.archetype.provider', null);
if ($provider !== null) {
$list = $provider->list();
}
Esempio — il modulo risponde:
// modules/logeon.archetypes/bootstrap.php
\Core\Hooks::addFilter('character.archetype.provider', function () {
return new \Modules\Logeon\Archetypes\Provider\ArchetypesModuleProvider();
});
Il core non importa ArchetypesModuleProvider. Il modulo non aggiunge nulla a /app/Contracts/.
Autoloading classi del modulo
Il modulo registra il proprio autoloader PSR-4 nel bootstrap.php.
Non va modificato composer.json del core.
Schema standard:
// modules/<vendor.modulo>/bootstrap.php
spl_autoload_register(function (string $class): void {
$prefix = 'Modules\\Vendor\\NomeModulo\\';
if (!str_starts_with($class, $prefix)) {
return;
}
$relative = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, strlen($prefix)));
$file = __DIR__ . '/src/' . $relative . '.php';
if (is_file($file)) {
require_once $file;
}
});
Il namespace radice consigliato segue la convenzione Modules\<Vendor>\<NomeModulo>\.
Struttura completa modulo
modules/<vendor.modulo>/
├── module.json ← manifest obbligatorio
├── bootstrap.php ← autoloader + registrazione hook
├── routes.php ← definizione rotte modulo (caricate solo se attivo)
├── src/
│ ├── Controllers/ ← controller HTTP del modulo
│ ├── Services/ ← logica di business del modulo
│ ├── Models/ ← modelli dati (se presenti)
│ └── Provider/ ← implementazioni provider per gli hook core
├── migrations/
│ ├── install.sql ← schema aggiunto all'attivazione
│ └── uninstall.sql ← schema rimosso alla disinstallazione (purge)
├── assets/ ← JS/CSS del modulo (opzionale)
├── views/ ← template Twig del modulo (opzionale)
└── docs/
└── README.md ← documentazione del modulo
Tutto il codice PHP del modulo sta in src/. Nulla va in /app/.
Manifest module.json
Nota GDPR: oltre ai campi tecnici, il manifest dovrebbe includere anche un blocco privacy per dichiarare il trattamento dati del modulo. L'audit moduli verifica presenza e coerenza di questa sezione.
Blocco privacy consigliato
{
"privacy": {
"personal_data": true,
"data_categories": ["email", "profilo_utente"],
"purposes": ["erogazione_servizio"],
"retention": "fino a cancellazione account o purge modulo",
"requires_consent": false,
"exports_user_data": true,
"supports_purge": true
}
}
Regole pratiche:
- Se
personal_data=true, valorizzare sempredata_categories,purposeseretention. - Per moduli Classe B, dichiarare
supports_purge=truequando il modulo e realmente disinstallabile con pulizia dati. - L'audit moduli segnala manifest senza blocco privacy o con dichiarazione incompleta/incoerente.
Campi principali:
id,name,version,vendordescriptionclass— tassonomia modulo:"bundled"(Classe A) oppure omesso /"optional"(Classe B, default). Vedi sezione Tassonomia moduli.dependencies(dipendenze richieste/opzionali)compat— range versione core:{"min": "0.8.0", "max": ""}menus— iniezione menu UI negli slot core
Tassonomia moduli
Classe A — Bundled Standard
Moduli distribuiti con Logeon, estratti dal core. Hanno colonne FK nelle tabelle core preesistenti (es. characters.socialstatus_id, characters.faction_id). Non possono essere rimossi senza ALTER TABLE sulle tabelle core.
- Ciclo di vita supportato: activate / deactivate.
- Uninstall e purge: non supportati.
ModuleManager::uninstall()restituisceerror_code: module_bundled_no_purge. - Identificazione:
"class": "bundled"inmodule.json. - Moduli Classe A correnti:
logeon.archetypes,logeon.attributes,logeon.factions,logeon.multi-currency,logeon.novelty,logeon.quests,logeon.social-status,logeon.weather.
Classe B — Optional Third-party
Moduli aggiuntivi con schema completamente additivo: nessuna colonna nelle tabelle core, solo tabelle proprie.
- Ciclo di vita supportato: install / activate / deactivate / uninstall / purge.
- Identificazione: nessun campo
classinmodule.json(defaultoptional).
La guida docs/guida-creazione-moduli.md e valida per i moduli Classe B. I moduli Classe A usano lo stesso sistema di rilevamento e attivazione, ma non supportano uninstall/purge.
Stati modulo
| Stato | Significato |
|---|---|
detected | Presente su filesystem, non installato |
installed | Installato, non attivo |
active | Attivo e caricato a runtime |
inactive | Installato ma disattivato |
error | Errore rilevato da runtime o audit |
La transizione da detected a installed avviene alla prima attivazione.
La disattivazione porta da active a inactive senza perdita dati.
La disinstallazione rimuove lo stato DB; con purge=1 esegue anche uninstall.sql.
Esempio concreto di lifecycle
Supponiamo di avere il modulo acme.bestiary nella cartella modules/acme.bestiary/.
- Copi la cartella nel progetto: il modulo compare come
detected. - Attivi il modulo da
/admin/modules: il sistema applicainstall.sqle lo stato passa aactive. - Crei una creatura dalla pagina
/admin/bestiary-creatures, per esempioLupo delle Nebbie. - Disattivi il modulo: il menu scompare e le rotte non vengono piu caricate, ma i record di
bestiary_entriesrestano nel database. - Esegui uninstall con
purge=1(solo Classe B): il sistema rimuove stato e schema del modulo, riportando il database allo stato precedente.
Runtime
- Il core monta sempre le proprie rotte.
ModuleRuntimecaricabootstrap.phperoutes.phpsolo per i moduli con statoactive.- Lo stato modulo è sempre verificato e persistito su DB, non solo dai file presenti.
- Se un modulo è
activema i file sono stati rimossi,auditlo segnala comeerror.
API admin moduli (core)
Permesso richiesto: settings.manage.
POST /admin/modules/list
Uso: elenco moduli con stato, versione, compatibilita, dipendenze.
POST /admin/modules/activate
Request: module_id
Effetti:
- valida compatibilita core (
compat.min/compat.max); - verifica dipendenze soddisfatte;
- applica
install.sql(idempotente); - imposta stato
active.
POST /admin/modules/deactivate
Request: module_id, cascade (0|1, opzionale)
Effetti:
- imposta stato
inactive; - con
cascade=1disattiva anche i moduli dipendenti attivi.
POST /admin/modules/uninstall
Request: module_id, purge (0|1, opzionale)
Precondizioni: modulo deve essere inactive.
Nota: i moduli Classe A (bundled) non supportano questa operazione. La chiamata restituisce error_code: module_bundled_no_purge. Per i moduli Classe A usare solo deactivate.
Effetti (solo moduli Classe B — optional):
- rimuove metadati runtime e stato DB;
- con
purge=0: mantiene eventuali dati applicativi; - con
purge=1: esegueuninstall.sql(rimozione tabelle, colonne, dati); - l'eliminazione della cartella
modules/<vendor.modulo>/è manuale dopo l'uninstall.
POST /admin/modules/audit
Uso: verifica coerenza runtime (stati inconsistenti, file mancanti, orfani).
Controlli inclusi:
- moduli installati/orfani/artifact runtime;
- moduli attivi senza artifact;
- stato dichiarazione privacy (
privacy_missing_declaration,privacy_incomplete_declaration).
Error code modulo (core)
module_not_foundmodule_not_installedmodule_dependency_missingmodule_incompatible_coremodule_activation_failedmodule_deactivation_failedmodule_deactivation_requires_confirmationmodule_uninstall_requires_inactivemodule_bundled_no_purge— tentata disinstallazione di un modulo Classe A (bundled)module_uninstall_failedmodule_audit_failed
Iniezione menu e asset
Twig helpers disponibili nel core:
module_assets(channel)module_active(moduleId)module_menu_entries(channel, slot, context)module_menu_sections(channel, slot, context)
Slot attualmente supportati:
game.profile_dropdowngame.profile_offcanvasadmin.aside
Sezioni sidebar admin
Quando un modulo registra una voce in menus.admin.aside, il campo section determina
il gruppo visuale nella sidebar (app/views/admin/layouts/aside.twig).
Il template distingue due comportamenti in base al nome della sezione:
- Sezione nota — la voce viene iniettata in coda al gruppo hardcoded corrispondente.
Il nome deve corrispondere esattamente (case-sensitive) a uno di questi valori:Utenti e personaggi, Richieste e segnalazioni, Oggetti, Parametri ed entità,Commercio, Mondo e navigazione, Narrativa, Economia, Gruppi e fazioni,Comunicazione, Documentazione, Logs.
- Sezione standalone — qualsiasi altro nome crea un nuovo gruppo in fondo alla sidebar,
separato dalle sezioni core. Usare questa modalità solo per sezioni concettualmente distinte
(es. Meteo).
Se si aggiunge una nuova sezione hardcoded ad aside.twig, il suo nome deve essere
aggiunto alla lista _known_sec_keys nel template e a questa guida.
Pagine admin riservate
Il campo page in menus.admin.aside e il nome dello slot twig.slot.admin.dashboard.<page>
determinano quale contenuto viene mostrato nell'area principale quando si naviga su /admin/<page>.
Il template app/views/admin/dashboard.twig gestisce le pagine seguenti con branch {% elseif %}
dedicati. Un modulo non deve usare nessuno di questi valori come page:
dashboard, users, characters, blacklist,
maps, currencies, shops, conflicts,
narrative-events, narrative-states, system-events, character-lifecycle,
character-requests, locations, inventory-shop,
jobs, jobs-tasks, jobs-levels,
guilds, guild-alignments, guilds-reqs, guilds-locations, guild-locations, guilds-events,
forums, forums-types,
storyboards, rules, how-to-play,
items, items-categories, items-rarities, equipment-slots, item-equipment-rules,
settings,
narrative-tags, narrative-delegation, narrative-delegation-grants, narrative-npcs,
message-reports,
logs-conflicts, logs-currency, logs-experience, logs-fame,
logs-guild, logs-job, logs-location-access, logs-sys, logs-narrative,
themes, modules
Tutti gli altri valori di page vanno nel ramo {% else %} del template, che risolve
il contenuto tramite lo slot hook twig.slot.admin.dashboard.<page> oppure tramitemodule_slot('admin.dashboard.<page>'). È qui che i moduli devono registrare i propri
slot per mostrare contenuti nell'area principale.
Convenzione consigliata: prefissare il page col nome del modulo per garantire unicità
(es. weather-overview, social-status, archetypes).
> Nota tecnica — rendering dello slot: il contenuto restituito dagli slot
> (slot() e module_slot()) viene assegnato a una variabile Twig prima di essere
> stampato. Le variabili Twig perdono il flag is_safe, quindi l'output verrebbe
> auto-escaped se non si usa il filtro |raw. Il template core gestisce già questo
> nel ramo {% else %} di dashboard.twig. Se si crea un template custom che usa
> {% set x = slot(...) %} seguito da {{ x }}, aggiungere sempre {{ x|raw }}.
Flusso operativo
- Copia la cartella modulo in
modules/<vendor.modulo>/. - Verifica in
/admin/modulesche compaia comedetected. - Attiva il modulo; il sistema valida compatibilita e dipendenze, poi esegue
install.sql. - Verifica menu, rotte e feature del modulo.
- Per disattivare:
/admin/modules/deactivate— i dati restano, il modulo smette di caricarsi. - Per disinstallare (solo moduli Classe B — optional): il modulo deve essere
inactive; esegui/admin/modules/uninstall?purge=1per pulizia completa DB, poi elimina la cartella. - I moduli Classe A (bundled) non supportano il passo 6: usare solo
deactivate.
Checklist pre-rilascio modulo
- Nessun file PHP del modulo si trova in
/app/o/core/. - Autoloader registrato in
bootstrap.php. - Tutte le rotte definite in
routes.phpdel modulo. install.sqlidempotente (usaCREATE TABLE IF NOT EXISTS,ALTER TABLE ... ADD COLUMN IF NOT EXISTS).uninstall.sqlripulisce tutto il DB aggiunto dal modulo (Classe B — optional only; i moduli Classe A non hannouninstall.sqlsignificativo).- Comunicazione col core solo via
Core\Hooks. - Nessun file aggiunto a
/app/Contracts/o/core/. - UI additiva: menu via slot, nessuna modifica a template core.
- Test attivazione → feature operative → disattivazione senza regressioni core → reinstallazione.
- Per moduli Classe B: test uninstall purge + verifica DB pulito.
- Documentazione modulo in
docs/README.mdseparata dai doc core. - Blocco
privacynel manifest coerente con i dati trattati dal modulo.