'empty','name'=>'Vide','color'=>'#ffffff']]; } } } if (!class_exists('TemplateFactory')) { class TemplateFactory { private static $cache = null; /** * Scanne le dossier mapModels pour découvrir les modèles disponibles * Retourne un tableau id => ['id'=>..., 'name'=>..., 'description'=>..., 'class'=>...] * @return array */ public static function listAvailable(): array { if (self::$cache !== null) return self::$cache; // calculer le dossier mapModels à partir de la racine du projet $projectRoot = dirname(__DIR__, 3); // app/Models/Templates -> app/Models -> app -> project root $base = $projectRoot . '/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 { $before = get_declared_classes(); require_once $realEntry; $loadedFromModel = true; $after = get_declared_classes(); // ne considérer que les classes nouvellement déclarées par ce fichier $new = array_diff($after, $before); // détecter automatiquement une classe qui implémente MapTemplateInterface foreach ($new as $decl) { if (is_subclass_of($decl, 'MapTemplateInterface')) { $loadedClass = $decl; break; } } // fallback : nom conventionnel défini dans le même fichier if ($loadedClass === null) { $canon = ucfirst($id) . 'Template'; if (in_array($canon, $new, true)) $loadedClass = $canon; } } catch (Throwable $e) { // ne pas interrompre la discovery sur erreur de template utilisateur $loadedClass = null; } } } // Ne pas tenter de charger des classes locales : forcer l'usage exclusif des plugins dans mapModels // si $loadedClass est null, on laisse null (fallback géré par TemplateFactory::get) $results[$id] = [ 'id' => $id, 'name' => $name, 'description' => $desc, 'class' => $loadedClass, 'entry' => $entryPath, 'loadedFromModel' => $loadedFromModel ]; } } } // Always ensure neutral exists as fallback (no local PHP required) if (!isset($results['neutral'])) { $results['neutral'] = [ 'id' => 'neutral', 'name' => 'Neutre', 'description' => 'Template neutre', 'class' => null, 'entry' => null, 'loadedFromModel' => false ]; } 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']; // si neutral a une classe fournie par mapModels, l'utiliser if (!empty($list['neutral']['class'])) return $list['neutral']['class']; // sinon retourner le wrapper neutre global return class_exists('NeutralTemplateWrapper') ? 'NeutralTemplateWrapper' : null; } } }