# Guida per creare nuovi plugin in ChronoQuest

Questa guida spiega come aggiungere un nuovo plugin a ChronoQuest partendo da zero. È pensata per programmatori che devono estendere il programma senza mescolare tutta la logica del plugin dentro il codice principale.

Gli esempi citano i plugin già presenti:

- `corse`
- `fight_the_monster`

## 1. Come funziona il sistema plugin

ChronoQuest usa tre livelli:

1. Registrazione del plugin nel database.
2. Attivazione del plugin per una o più classi.
3. Codice applicativo che espone pagine, menu, logica e punti di aggancio.

Le tabelle principali sono:

```sql
ct_plugin
ct_plugin_classe
```

`ct_plugin` contiene il plugin a livello globale.

Campi importanti:

- `id_plugin`: identificativo numerico.
- `nome_plugin`: nome visibile.
- `codice_plugin`: codice stabile usato dal programma, per esempio `fight_the_monster`.
- `versione`: versione del plugin.
- `attivo`: abilita o disabilita globalmente il plugin.
- `descrizione`: testo descrittivo.
- `configurazione_json`: configurazione globale opzionale.

`ct_plugin_classe` indica se un plugin è attivo per una classe.

Campi importanti:

- `fk_plugin`
- `fk_classe`
- `attivo`
- `configurazione_json`: configurazione specifica per la classe.

Il servizio centrale è:

```text
src/Service/PluginManagerService.php
```

Metodi da conoscere:

- `isActive($pluginCode, $classId)`: controlla se il plugin è attivo globalmente e per la classe.
- `getStudentMenuItems($classId)`: restituisce le voci plugin per il menu studenti.
- `getTeacherMenuItems($classId)`: restituisce le voci plugin per il menu docenti.
- `getPluginsForClass($classId)`: usato dalla pagina di gestione plugin.
- `createPlugin(...)`: registra un nuovo plugin dalla pagina amministratore.
- `applyRewardModifiers(...)`: esempio di punto di aggancio per modificare ricompense.

## 2. Cosa fa oggi l'import ZIP

La pagina docente dei plugin consente agli amministratori di caricare uno ZIP quando creano un plugin.

Attenzione: allo stato attuale lo ZIP viene salvato in:

```text
upload/plugin-packages/{codice_plugin}/
```

ma non viene ancora estratto automaticamente né installato nel codice sorgente.

Quindi oggi lo ZIP serve come pacchetto sorgente/versionato del plugin, mentre l'integrazione nel programma va fatta aggiungendo i file nel repository e registrando rotte, menu e punti di aggancio.

## 3. Struttura consigliata di un plugin

Per un plugin con codice:

```text
mio_plugin
```

la struttura consigliata nel progetto è:

```text
src/Plugin/MioPlugin/
  MioPluginController.php
  MioPluginService.php

public/views/plugin/mio_plugin/
  docenti/
    index.php
  studenti/
    index.php

public/css/plugin_mio_plugin/
  mio-plugin.css

public/js/plugin_mio_plugin/
  mio-plugin.js

public/assets/images/plugin_mio_plugin/
  .gitkeep

sql/
  YYYYMMDD_plugin_mio_plugin.sql
```

Regole pratiche:

- il codice plugin nel database deve essere minuscolo e con underscore: `mio_plugin`;
- il namespace PHP può essere in PascalCase: `App\Plugin\MioPlugin`;
- le viste stanno sotto `public/views/plugin/{codice_plugin}`;
- CSS, JS e immagini stanno in cartelle dedicate;
- le tabelle del plugin devono avere un prefisso riconoscibile.

Esempi reali:

```text
src/Plugin/Corse/
src/Plugin/FightTheMonster/
public/views/plugin/corse/
public/views/plugin/fight_the_monster/
public/css/plugin_fight_the_monster/
```

## 4. Struttura consigliata dello ZIP

Lo ZIP importabile dovrebbe contenere il plugin in modo autosufficiente.

Esempio:

```text
mio_plugin.zip
  manifest.json
  sql/
    install.sql
    upgrade_1_1.sql
  src/
    Plugin/
      MioPlugin/
        MioPluginController.php
        MioPluginService.php
  public/
    views/
      plugin/
        mio_plugin/
          docenti/
            index.php
          studenti/
            index.php
    css/
      plugin_mio_plugin/
        mio-plugin.css
    js/
      plugin_mio_plugin/
        mio-plugin.js
    assets/
      images/
        plugin_mio_plugin/
          .gitkeep
  translations/
    it.php.fragment
    en.php.fragment
  README.md
```

`manifest.json` consigliato:

```json
{
  "codice_plugin": "mio_plugin",
  "nome_plugin": "Mio Plugin",
  "versione": "1.0",
  "descrizione": "Descrizione breve del plugin.",
  "menu": {
    "studenti": {
      "label_key": "plugin.mio_plugin.menu",
      "url": "/studenti/plugin/mio-plugin",
      "icon": "fas fa-fw fa-puzzle-piece",
      "order": 70
    },
    "docenti": {
      "label_key": "plugin.mio_plugin.menu",
      "url": "/docenti/plugin/mio-plugin",
      "icon": "fas fa-fw fa-puzzle-piece",
      "order": 70
    }
  },
  "install_sql": "sql/install.sql"
}
```

Nota: il programma attualmente non legge automaticamente questo manifest. È però utile come standard per chi sviluppa e per una futura installazione automatica.

## 5. SQL del plugin

Ogni plugin deve avere una migrazione SQL.

Esempio minimo:

```sql
INSERT INTO ct_plugin (id_plugin, nome_plugin, codice_plugin, versione, attivo, descrizione, configurazione_json)
SELECT next_id,
       'Mio Plugin',
       'mio_plugin',
       '1.0',
       1,
       'Descrizione del plugin.',
       NULL
FROM (SELECT COALESCE(MAX(id_plugin), 0) + 1 AS next_id FROM ct_plugin) ids
WHERE NOT EXISTS (
  SELECT 1 FROM ct_plugin existing_plugin WHERE existing_plugin.codice_plugin = 'mio_plugin'
);

CREATE TABLE IF NOT EXISTS ct_plugin_mio_plugin_eventi (
  id_evento int NOT NULL AUTO_INCREMENT,
  fk_classe int NOT NULL,
  fk_studente int NULL,
  testo text NOT NULL,
  creato_il datetime NOT NULL,
  PRIMARY KEY (id_evento),
  KEY idx_ct_plugin_mio_plugin_eventi_classe (fk_classe)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

Preferire nomi tabella con prefisso:

```text
ct_plugin_{codice_plugin}_...
```

oppure, se il plugin è già nato con un prefisso breve:

```text
ct_ftm_...
```

Per i plugin caricati dalla pagina amministratore, il campo `install_sql` accetta solo statement `CREATE TABLE` con nome che inizia per `ct_plugin_`. Questo è un limite di sicurezza intenzionale.

Per migrazioni più ampie, aggiungere file SQL in `sql/` e farli eseguire manualmente/amministrativamente.

## 6. Controller del plugin

Un plugin con pagine proprie dovrebbe avere un controller dedicato.

Esempio:

```php
<?php

namespace App\Plugin\MioPlugin;

use App\Core\View;
use App\Service\Flash;
use App\Service\PermissionService;

class MioPluginController
{
    public function teacherIndex(): void
    {
        $service = new MioPluginService();
        $pageData = $service->getTeacherPageData();

        if (($pageData['permissionStatus'] ?? null) !== PermissionService::STATUS_OK) {
            Flash::add('danger', 'permission.noteacher');
            header('Location: /');
            exit;
        }

        View::render('plugin/mio_plugin/docenti/index', array_merge($pageData, [
            'title' => 'plugin.mio_plugin.teacher.title',
            'pageStyles' => ['/css/plugin_mio_plugin/mio-plugin.css'],
            'useMathJax' => false,
        ]), 'mainDocLayout');
    }

    public function studentIndex(): void
    {
        $service = new MioPluginService();
        $pageData = $service->getStudentPageData();

        if (($pageData['permissionStatus'] ?? null) !== PermissionService::STATUS_OK) {
            Flash::add('danger', 'permission.nostudent');
            header('Location: /loginStud');
            exit;
        }

        View::render('plugin/mio_plugin/studenti/index', array_merge($pageData, [
            'title' => 'plugin.mio_plugin.student.title',
            'pageStyles' => ['/css/plugin_mio_plugin/mio-plugin.css'],
            'useMathJax' => false,
        ]), 'mainStudLayout');
    }
}
```

## 7. Service del plugin

Il service contiene la logica vera: permessi, query, salvataggi, calcoli.

Esempio:

```php
<?php

namespace App\Plugin\MioPlugin;

use App\Service\Database;
use App\Service\PermissionService;
use App\Service\PluginManagerService;
use PDO;

class MioPluginService
{
    private const PLUGIN_CODE = 'mio_plugin';

    private PermissionService $permissionService;

    public function __construct()
    {
        $this->permissionService = new PermissionService();
    }

    public function getTeacherPageData(): array
    {
        $permissionStatus = $this->permissionService->checkPermissionsTeacher();
        $classId = $this->permissionService->getCurrentClassId();

        $data = [
            'permissionStatus' => $permissionStatus,
            'records' => [],
        ];

        if ($permissionStatus !== PermissionService::STATUS_OK || $classId === null) {
            return $data;
        }

        if (!(new PluginManagerService())->isActive(self::PLUGIN_CODE, $classId)) {
            $data['permissionStatus'] = PermissionService::STATUS_NOT_CLASS_OWNER;
            return $data;
        }

        $stmt = Database::getConnection()->prepare(
            'SELECT * FROM ct_plugin_mio_plugin_eventi WHERE fk_classe = :fk_classe ORDER BY id_evento DESC'
        );
        $stmt->execute(['fk_classe' => $classId]);
        $data['records'] = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];

        return $data;
    }

    public function getStudentPageData(): array
    {
        $permissionStatus = $this->permissionService->checkPermissionsStudent();
        $classId = $this->permissionService->getCurrentClassId();

        $data = [
            'permissionStatus' => $permissionStatus,
            'records' => [],
        ];

        if ($permissionStatus !== PermissionService::STATUS_OK || $classId === null) {
            return $data;
        }

        if (!(new PluginManagerService())->isActive(self::PLUGIN_CODE, $classId)) {
            $data['permissionStatus'] = PermissionService::STATUS_NOT_CLASS_OWNER;
            return $data;
        }

        return $data;
    }
}
```

Regola importante: ogni azione del plugin deve controllare:

```php
(new PluginManagerService())->isActive(self::PLUGIN_CODE, $classId)
```

Questo evita che una pagina o un hook del plugin lavori quando il plugin è disattivato per quella classe.

## 8. Rotte

Le rotte stanno in:

```text
routes/web.php
```

Bisogna aggiungere lo `use`:

```php
use App\Plugin\MioPlugin\MioPluginController;
```

Poi le rotte concrete:

```php
$router->get('/docenti/plugin/mio-plugin', [MioPluginController::class, 'teacherIndex']);
$router->get('/studenti/plugin/mio-plugin', [MioPluginController::class, 'studentIndex']);
```

Se servono azioni:

```php
$router->post('/docenti/plugin/mio-plugin/salva', [MioPluginController::class, 'save']);
$router->post('/studenti/plugin/mio-plugin/azione', [MioPluginController::class, 'studentAction']);
```

Le rotte specifiche devono stare prima delle rotte generiche:

```php
$router->get('/docenti/plugin/{pluginCode}', [PluginPageController::class, 'teacher']);
$router->get('/studenti/plugin/{pluginCode}', [PluginPageController::class, 'student']);
```

Altrimenti il placeholder generico intercetta la richiesta prima del plugin.

## 9. Menu laterale

I menu dinamici dei plugin vengono definiti in:

```text
src/Service/PluginManagerService.php
```

Dentro `BUILTIN_DEFINITIONS` aggiungere:

```php
'mio_plugin' => [
    'student_menu' => [
        'label_key' => 'plugin.mio_plugin.menu',
        'url' => '/studenti/plugin/mio-plugin',
        'icon' => 'fas fa-fw fa-puzzle-piece',
        'order' => 70,
    ],
    'teacher_menu' => [
        'label_key' => 'plugin.mio_plugin.menu',
        'url' => '/docenti/plugin/mio-plugin',
        'icon' => 'fas fa-fw fa-puzzle-piece',
        'order' => 70,
    ],
],
```

Il menu viene mostrato solo se:

- il plugin esiste in `ct_plugin`;
- `ct_plugin.attivo = 1`;
- esiste una riga in `ct_plugin_classe` con `attivo = 1` per la classe selezionata.

## 10. Viste

Le viste del plugin dovrebbero restare isolate:

```text
public/views/plugin/mio_plugin/docenti/index.php
public/views/plugin/mio_plugin/studenti/index.php
```

Esempio base:

```php
<?php

use App\Service\TranslationService;

$translator = new TranslationService();
$records = is_array($records ?? null) ? $records : [];
?>

<div class="container-fluid">
    <h1 class="h3 mb-4 text-gray-800">
        <?= $translator->translate('plugin.mio_plugin.teacher.title') ?>
    </h1>

    <div class="card shadow mb-4">
        <div class="card-body">
            <?php foreach ($records as $record): ?>
                <p><?= htmlspecialchars((string) ($record['testo'] ?? '')) ?></p>
            <?php endforeach; ?>
        </div>
    </div>
</div>
```

Usare sempre:

```php
htmlspecialchars(...)
```

quando si stampano dati provenienti da database o input utente.

## 11. Traduzioni

Le traduzioni stanno in:

```text
translations/it.php
translations/en.php
```

Aggiungere chiavi con prefisso del plugin:

```php
'plugin.mio_plugin.menu' => 'Mio Plugin',
'plugin.mio_plugin.teacher.title' => 'Mio Plugin',
'plugin.mio_plugin.student.title' => 'Mio Plugin',
'plugin.mio_plugin.saved' => 'Salvato correttamente.',
'plugin.mio_plugin.error.permission' => 'Non hai i permessi per usare questo plugin.',
```

E in inglese:

```php
'plugin.mio_plugin.menu' => 'My Plugin',
'plugin.mio_plugin.teacher.title' => 'My Plugin',
'plugin.mio_plugin.student.title' => 'My Plugin',
'plugin.mio_plugin.saved' => 'Saved.',
'plugin.mio_plugin.error.permission' => 'You do not have permission to use this plugin.',
```

## 12. Asset e upload

Se il plugin carica immagini o file propri, creare una cartella dedicata:

```text
public/assets/images/plugin_mio_plugin/
```

Esempio di salvataggio:

```php
$dir = dirname(__DIR__, 3) . '/public/assets/images/plugin_mio_plugin';
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
    throw new \RuntimeException('plugin.mio_plugin.error.upload_folder');
}
```

Controllare sempre:

- errore upload;
- estensione permessa;
- nome file sicuro;
- cartella corretta;
- percorso relativo da salvare nel database.

Estensioni immagini consigliate:

```text
jpg, jpeg, png, gif, webp
```

## 13. Configurazione JSON

Ogni plugin può avere:

- configurazione globale in `ct_plugin.configurazione_json`;
- configurazione per classe in `ct_plugin_classe.configurazione_json`.

Esempio:

```json
{
  "moltiplicatore": 2,
  "limite_giornaliero": 5
}
```

Il metodo `getMergedConfiguration()` di `PluginManagerService` oggi è privato. Se un plugin esterno deve leggere la configurazione unificata, conviene aggiungere un metodo pubblico dedicato, ad esempio:

```php
public function getConfigurationForClass(string $pluginCode, ?int $classId): array
{
    return $this->getMergedConfiguration($pluginCode, $classId);
}
```

## 14. Punti di aggancio principali

Un plugin può interagire con il programma in tre modi.

### 14.1 Pagine proprie

È il caso più semplice.

Esempio:

- pagina docente del plugin;
- pagina studente del plugin;
- gestione tabelle dedicate.

Richiede:

- controller;
- service;
- viste;
- rotte;
- menu in `PluginManagerService`;
- tabelle SQL.

### 14.2 Modifica o ascolto di eventi esistenti

Esempi:

- quando lo studente consegna un esercizio;
- quando il docente valuta una consegna;
- quando lo studente riceve un forziere;
- quando una gara termina.

Esempio reale: Fight the Monster si aggancia alla valutazione docente.

File:

```text
src/Service/TeacherQuestService.php
```

Nel salvataggio della valutazione chiama:

```php
(new FightTheMonsterPluginService())->handleEvaluatedDelivery($classId, $studentId, $deliveryId, $grade);
```

Esempio reale: Fight the Monster modifica i forzieri extra.

File:

```text
src/Service/StudentQuestService.php
```

Quando lo studente matura il forziere ogni 3 consegne chiama:

```php
$bonusChests = (new FightTheMonsterPluginService())->grantBonusChestsForDelivery($classId, $studentId);
```

Quando si aggiunge un nuovo hook, il metodo del plugin deve sempre iniziare controllando:

```php
if (!(new PluginManagerService())->isActive(self::PLUGIN_CODE, $classId)) {
    return;
}
```

### 14.3 Modifica delle ricompense

Il manager ha già un esempio:

```php
PluginManagerService::applyRewardModifiers(...)
```

Oggi gestisce il caso:

```text
event = chest.earned
plugin = forzieri_bonus
```

Un pattern consigliato per il futuro è far passare le ricompense da un contesto standard:

```php
$context = [
    'event' => 'chest.earned',
    'class_id' => $classId,
    'student_id' => $studentId,
    'quantity' => 1,
    'rarity' => 'comune',
];

$context = (new PluginManagerService())->applyRewardModifiers($context);
```

Poi il codice principale assegna la ricompensa usando il contesto modificato.

## 15. Checklist per creare un nuovo plugin

1. Scegliere il codice plugin:

```text
mio_plugin
```

2. Creare la migrazione SQL in `sql/`.

3. Inserire il record in `ct_plugin`.

4. Creare le tabelle del plugin.

5. Creare:

```text
src/Plugin/MioPlugin/MioPluginController.php
src/Plugin/MioPlugin/MioPluginService.php
```

6. Creare le viste:

```text
public/views/plugin/mio_plugin/docenti/index.php
public/views/plugin/mio_plugin/studenti/index.php
```

7. Creare CSS/JS dedicati se servono.

8. Aggiungere rotte in `routes/web.php`.

9. Aggiungere la voce menu in `PluginManagerService::BUILTIN_DEFINITIONS`.

10. Aggiungere traduzioni in `translations/it.php` e `translations/en.php`.

11. Se il plugin deve reagire a eventi esistenti, aggiungere un hook nel service del programma principale.

12. Verificare che ogni metodo del plugin controlli `isActive()`.

13. Attivare il plugin per la classe dalla pagina:

```text
/docenti/plugin
```

14. Testare lato docente e lato studente.

## 16. Esempio di plugin completo minimo

### SQL

```sql
INSERT INTO ct_plugin (id_plugin, nome_plugin, codice_plugin, versione, attivo, descrizione, configurazione_json)
SELECT next_id,
       'Messaggio Segreto',
       'messaggio_segreto',
       '1.0',
       1,
       'Mostra un messaggio speciale agli studenti.',
       NULL
FROM (SELECT COALESCE(MAX(id_plugin), 0) + 1 AS next_id FROM ct_plugin) ids
WHERE NOT EXISTS (
  SELECT 1 FROM ct_plugin existing_plugin WHERE existing_plugin.codice_plugin = 'messaggio_segreto'
);

CREATE TABLE IF NOT EXISTS ct_plugin_messaggio_segreto (
  id_messaggio int NOT NULL AUTO_INCREMENT,
  fk_classe int NOT NULL,
  testo text NOT NULL,
  creato_il datetime NOT NULL,
  PRIMARY KEY (id_messaggio),
  KEY idx_ct_plugin_messaggio_segreto_classe (fk_classe)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### Rotte

```php
use App\Plugin\MessaggioSegreto\MessaggioSegretoPluginController;

$router->get('/docenti/plugin/messaggio-segreto', [MessaggioSegretoPluginController::class, 'teacherIndex']);
$router->post('/docenti/plugin/messaggio-segreto', [MessaggioSegretoPluginController::class, 'save']);
$router->get('/studenti/plugin/messaggio-segreto', [MessaggioSegretoPluginController::class, 'studentIndex']);
```

### Menu

```php
'messaggio_segreto' => [
    'student_menu' => [
        'label_key' => 'plugin.messaggio_segreto.menu',
        'url' => '/studenti/plugin/messaggio-segreto',
        'icon' => 'fas fa-fw fa-scroll',
        'order' => 80,
    ],
    'teacher_menu' => [
        'label_key' => 'plugin.messaggio_segreto.menu',
        'url' => '/docenti/plugin/messaggio-segreto',
        'icon' => 'fas fa-fw fa-scroll',
        'order' => 80,
    ],
],
```

## 17. Convenzioni consigliate

- Non modificare tabelle core se non è necessario.
- Preferire tabelle dedicate del plugin.
- Se si deve modificare un flusso core, farlo con un metodo hook chiaro e piccolo.
- Non mettere logica complessa nelle viste.
- Non salvare HTML non sanificato se non serve.
- Usare transazioni quando il plugin aggiorna più tabelle insieme.
- Usare `PermissionService` per controllare docente/studente/classe.
- Usare `TranslationService` per tutti i testi visibili.
- Usare cartelle asset dedicate.
- Scrivere una migrazione SQL incrementale per ogni modifica successiva.

## 18. Limiti attuali del sistema

Da tenere presenti:

- Lo ZIP viene salvato ma non installato automaticamente.
- Il menu dinamico richiede ancora una voce in `PluginManagerService::BUILTIN_DEFINITIONS`.
- Le rotte devono essere registrate manualmente in `routes/web.php`.
- Gli hook nei flussi principali devono essere aggiunti manualmente.
- L'install SQL da pannello admin accetta solo `CREATE TABLE ct_plugin_...`.

Questi limiti non impediscono di sviluppare plugin, ma indicano dove migliorare in futuro se si vuole arrivare a un marketplace/plugin loader completamente automatico.

## 19. Miglioramenti futuri consigliati

Per rendere lo ZIP davvero installabile automaticamente, si potrebbe aggiungere:

- lettura obbligatoria di `manifest.json`;
- estrazione controllata dello ZIP;
- copia dei file solo in cartelle permesse;
- registrazione automatica di menu e rotte dal manifest;
- esecuzione migrazioni SQL firmate o validate;
- pagina di aggiornamento plugin;
- disinstallazione plugin;
- sistema di eventi centralizzato, ad esempio:

```php
PluginEventBus::dispatch('delivery.evaluated', [
    'class_id' => $classId,
    'student_id' => $studentId,
    'delivery_id' => $deliveryId,
    'grade' => $grade,
]);
```

Con un event bus, i plugin potrebbero ascoltare eventi senza modificare ogni volta i service principali.

## 20. File reali da studiare

Per capire i pattern già usati:

```text
src/Service/PluginManagerService.php
src/Controller/TeacherPluginsController.php
src/Controller/PluginPageController.php
src/Plugin/Corse/CorsePluginController.php
src/Plugin/Corse/CorsePluginService.php
src/Plugin/FightTheMonster/FightTheMonsterPluginController.php
src/Plugin/FightTheMonster/FightTheMonsterPluginService.php
public/views/plugin/corse/
public/views/plugin/fight_the_monster/
sql/20260612_plugin_manager.sql
sql/20260612_plugin_corse.sql
sql/20260626_plugin_fight_the_monster.sql
```

Questi file sono il riferimento più concreto per creare un nuovo plugin coerente con ChronoQuest.
