['id'=>..., 'name'=>..., 'description'=>..., 'class'=>...] * @return array */ public static function listAvailable(): array { if (self::$cache !== null) return self::$cache; $base = __DIR__ . '/../../mapModels'; $results = []; if (is_dir($base)) { $dirs = scandir($base); foreach ($dirs as $d) { if ($d === '.' || $d === '..') continue; $modelJson = $base . '/' . $d . '/model.json'; if (file_exists($modelJson)) { $raw = file_get_contents($modelJson); $meta = json_decode($raw, true) ?: []; $id = $meta['id'] ?? $d; $name = $meta['name'] ?? $id; $desc = $meta['description'] ?? ''; // tenter de charger un fichier PHP depuis le dossier du modèle (sécurisé) $modelDir = realpath($base . '/' . $d); $entry = $meta['entry'] ?? 'template.php'; $entryPath = $modelDir ? $modelDir . DIRECTORY_SEPARATOR . $entry : null; $loadedClass = null; $loadedFromModel = false; if ($entryPath && is_file($entryPath) && pathinfo($entryPath, PATHINFO_EXTENSION) === 'php') { // protection : s'assurer que le fichier est bien dans le dossier mapModels $realEntry = realpath($entryPath); if ($realEntry !== false && strpos($realEntry, realpath($base)) === 0) { // inclure le fichier. utiliser require_once pour éviter double inclusion try { require_once $realEntry; $loadedFromModel = true; // détecter automatiquement une classe qui implémente MapTemplateInterface foreach (get_declared_classes() as $decl) { if (is_subclass_of($decl, 'MapTemplateInterface')) { $loadedClass = $decl; break; } } // fallback : nom conventionnel if ($loadedClass === null) { $canon = ucfirst($id) . 'Template'; if (class_exists($canon)) $loadedClass = $canon; } } catch (Throwable $e) { // ne pas interrompre la discovery sur erreur de template utilisateur $loadedClass = null; } } } // s'il n'y a pas de fichier dans mapModels, tenter la classe interne comme avant if ($loadedClass === null) { $classFile = __DIR__ . '/' . ucfirst($id) . 'Template.php'; $className = ucfirst($id) . 'Template'; if (file_exists($classFile)) { require_once $classFile; if (class_exists($className)) $loadedClass = $className; } } $results[$id] = [ 'id' => $id, 'name' => $name, 'description' => $desc, 'class' => $loadedClass, 'entry' => $entryPath, 'loadedFromModel' => $loadedFromModel ]; } } } // Always ensure neutral exists as fallback if (!isset($results['neutral'])) { // try to include NeutralTemplate $neutralFile = __DIR__ . '/NeutralTemplate.php'; if (file_exists($neutralFile)) require_once $neutralFile; $results['neutral'] = [ 'id' => 'neutral', 'name' => 'Neutre', 'description' => 'Template neutre', 'class' => class_exists('NeutralTemplate') ? 'NeutralTemplate' : null ]; } self::$cache = $results; return $results; } /** * Retourne la classe du template demandé (ou NeutralTemplate) * @param string|null $id * @return string|null */ public static function get(string $id = null): ?string { $list = self::listAvailable(); if ($id && isset($list[$id]) && !empty($list[$id]['class'])) return $list[$id]['class']; return $list['neutral']['class'] ?? null; } } }