Files
nis2-agile/application/services/DemoSeedService.php
T
DevEnv nis2-agileandClaude Opus 4.8 6900d942a4 [DEMO] resetDataset cablato a DemoSeedService (reset reale dataset demo/sandbox)
- application/services/DemoSeedService.php: logica seed/clone golden #151 -> 996001/996002 (riusabile)
- DemoController::resetDataset: re-seed reale via service (?scope=demo|sandbox|default both), non piu stub
- scripts/seed-demo-dataset.php: thin CLI wrapper sul service
Testato prod: POST /api/demo/reset-dataset (X-Internal-Key) scope=sandbox -> 200, 996002 ri-seedata.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 08:47:09 +02:00

135 lines
6.6 KiB
PHP

<?php
/**
* NIS2 Agile - DemoSeedService
* ----------------------------------------------------------------------------
* Seed/reset del dataset DEMO isolato (Avatar di prodotto / formazione).
* Clona la golden org DataCore (#151) negli org del range demo riservato:
* - 996001 → dataset DEMO (servito read-only via demo_jwt scope=demo:read-only)
* - 996002 → SANDBOX scrivibile isolata per i "prova tu" della formazione
*
* Idempotente: cancella e ricrea i dati del range demo. Usato da:
* - DemoController::resetDataset (reset tra sessioni demo / sandbox)
* - scripts/seed-demo-dataset.php (CLI bootstrap manuale)
*
* Sta sotto application/ (montata in nis2-app) così è raggiungibile sia da
* PHP-FPM (resetDataset) sia da CLI.
*/
class DemoSeedService
{
public const SRC_ORG = 151; // golden DataCore
public const TARGETS = [
996001 => 'DataCore Demo S.r.l.',
996002 => 'NIS2 Sandbox Formazione S.r.l.',
];
private const USERCOLS = ['owner_user_id','responsible_user_id','assigned_to','reported_by','created_by','uploaded_by','relevance_assessed_by','assessed_by','approved_by','reviewed_by','closed_by','user_id'];
/** parent → child (FK figlio) da remappare al nuovo parent */
private const PC = [
['assessments','assessment_responses','assessment_id'],
['risks','risk_treatments','risk_id'],
['incidents','incident_timeline','incident_id'],
['training_courses','training_assignments','course_id'],
['policies','policy_versions','policy_id'],
];
private const SIMPLE = ['compliance_controls','suppliers','assets'];
/**
* Esegue il seed/reset. $targets: mappa orgId=>nome (default: entrambe).
* @return array<int,string> esito per org ("ok uid=N" | "errore: ...")
*/
public static function seed(?array $targets = null): array
{
$targets = $targets ?: self::TARGETS;
$pdo = Database::getInstance();
$out = [];
$pdo->exec('SET FOREIGN_KEY_CHECKS=0');
foreach ($targets as $T => $NAME) {
$T = (int) $T;
$pdo->beginTransaction();
try {
self::purge($pdo, $T);
$org = Database::fetchOne('SELECT * FROM organizations WHERE id = ?', [self::SRC_ORG]);
self::copyRow($pdo, 'organizations', $org, ['id' => $T, 'name' => $NAME], 0);
$uid = (int) Database::insert('users', [
'email' => "demo-$T@nis2-demo.local", 'full_name' => "Demo NIS2 $T", 'role' => 'org_admin',
'password_hash' => password_hash(bin2hex(random_bytes(12)), PASSWORD_DEFAULT),
'is_active' => 1, 'must_change_password' => 0, 'password_version' => 1,
]);
$pdo->prepare('INSERT INTO user_organizations (user_id,organization_id,role) VALUES (?,?,?)')->execute([$uid, $T, 'org_admin']);
foreach (self::PC as [$par, $chi, $fk]) {
if (!self::tableExists($pdo, $par)) continue;
foreach (Database::fetchAll("SELECT * FROM `$par` WHERE organization_id = ? ORDER BY id", [self::SRC_ORG]) as $r) {
$newPar = self::copyRow($pdo, $par, $r, ['organization_id' => $T], $uid);
if (self::tableExists($pdo, $chi)) {
foreach (Database::fetchAll("SELECT * FROM `$chi` WHERE `$fk` = ? ORDER BY id", [$r['id']]) as $cr) {
$ov = [$fk => $newPar];
if (in_array('organization_id', self::cols($pdo, $chi), true)) $ov['organization_id'] = $T;
self::copyRow($pdo, $chi, $cr, $ov, $uid);
}
}
}
}
foreach (self::SIMPLE as $t) {
if (!self::tableExists($pdo, $t)) continue;
foreach (Database::fetchAll("SELECT * FROM `$t` WHERE organization_id = ? ORDER BY id", [self::SRC_ORG]) as $r) {
self::copyRow($pdo, $t, $r, ['organization_id' => $T], $uid);
}
}
$pdo->commit();
$out[$T] = "ok (uid=$uid)";
} catch (Throwable $ex) {
$pdo->rollBack();
$out[$T] = 'errore: ' . $ex->getMessage();
}
}
$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
return $out;
}
private static function purge(PDO $pdo, int $T): void
{
foreach (self::PC as [$par, $chi, $fk]) {
if (self::tableExists($pdo, $chi)) $pdo->exec("DELETE FROM `$chi` WHERE `$fk` IN (SELECT id FROM `$par` WHERE organization_id = $T)");
}
foreach (array_merge(array_column(self::PC, 0), self::SIMPLE) as $t) {
if (self::tableExists($pdo, $t)) $pdo->exec("DELETE FROM `$t` WHERE organization_id = $T");
}
$pdo->exec("DELETE FROM user_organizations WHERE organization_id = $T");
$pdo->exec("DELETE FROM users WHERE email = 'demo-$T@nis2-demo.local'");
$pdo->exec("DELETE FROM organizations WHERE id = $T");
}
private static function cols(PDO $pdo, string $t): array
{
static $cache = [];
if (!isset($cache[$t])) {
$cache[$t] = $pdo->query("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='$t' ORDER BY ORDINAL_POSITION")->fetchAll(PDO::FETCH_COLUMN);
}
return $cache[$t];
}
private static function tableExists(PDO $pdo, string $t): bool
{
return (int) $pdo->query("SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='$t'")->fetchColumn() > 0;
}
/** Copia una riga in $table applicando $overrides e rimappando le colonne utente a $uid. */
private static function copyRow(PDO $pdo, string $table, array $row, array $overrides, int $uid): int
{
$data = [];
foreach (self::cols($pdo, $table) as $c) {
if ($c === 'id') { if (array_key_exists('id', $overrides)) $data['id'] = $overrides['id']; continue; }
if (array_key_exists($c, $overrides)) { $data[$c] = $overrides[$c]; continue; }
$v = $row[$c] ?? null;
if (in_array($c, self::USERCOLS, true) && $v !== null) $v = $uid;
$data[$c] = $v;
}
$cl = implode(',', array_map(fn($x) => "`$x`", array_keys($data)));
$ph = implode(',', array_fill(0, count($data), '?'));
$pdo->prepare("INSERT INTO `$table` ($cl) VALUES ($ph)")->execute(array_values($data));
return (int) $pdo->lastInsertId();
}
}