TemplateFactory.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. <?php
  2. // Wrapper neutre global (fallback safe) — déclaré au niveau fichier
  3. if (!class_exists('NeutralTemplateWrapper')) {
  4. class NeutralTemplateWrapper
  5. {
  6. public static function id(): string { return 'neutral'; }
  7. public static function applyTemplate($map): void { /* no-op */ }
  8. public static function tileDefinitions(): array { return [['id'=>'empty','name'=>'Vide','color'=>'#ffffff']]; }
  9. }
  10. }
  11. if (!class_exists('TemplateFactory')) {
  12. class TemplateFactory
  13. {
  14. private static $cache = null;
  15. /**
  16. * Scanne le dossier mapModels pour découvrir les modèles disponibles
  17. * Retourne un tableau id => ['id'=>..., 'name'=>..., 'description'=>..., 'class'=>...]
  18. * @return array
  19. */
  20. public static function listAvailable(): array
  21. {
  22. if (self::$cache !== null) return self::$cache;
  23. // calculer le dossier mapModels à partir de la racine du projet
  24. $projectRoot = dirname(__DIR__, 3); // app/Models/Templates -> app/Models -> app -> project root
  25. $base = $projectRoot . '/mapModels';
  26. $results = [];
  27. if (is_dir($base)) {
  28. $dirs = scandir($base);
  29. foreach ($dirs as $d) {
  30. if ($d === '.' || $d === '..') continue;
  31. $modelJson = $base . '/' . $d . '/model.json';
  32. if (file_exists($modelJson)) {
  33. $raw = file_get_contents($modelJson);
  34. $meta = json_decode($raw, true) ?: [];
  35. $id = $meta['id'] ?? $d;
  36. $name = $meta['name'] ?? $id;
  37. $desc = $meta['description'] ?? '';
  38. // tenter de charger un fichier PHP depuis le dossier du modèle (sécurisé)
  39. $modelDir = realpath($base . '/' . $d);
  40. $entry = $meta['entry'] ?? 'template.php';
  41. $entryPath = $modelDir ? $modelDir . DIRECTORY_SEPARATOR . $entry : null;
  42. $loadedClass = null;
  43. $loadedFromModel = false;
  44. if ($entryPath && is_file($entryPath) && pathinfo($entryPath, PATHINFO_EXTENSION) === 'php') {
  45. // protection : s'assurer que le fichier est bien dans le dossier mapModels
  46. $realEntry = realpath($entryPath);
  47. if ($realEntry !== false && strpos($realEntry, realpath($base)) === 0) {
  48. // inclure le fichier. utiliser require_once pour éviter double inclusion
  49. try {
  50. $before = get_declared_classes();
  51. require_once $realEntry;
  52. $loadedFromModel = true;
  53. $after = get_declared_classes();
  54. // ne considérer que les classes nouvellement déclarées par ce fichier
  55. $new = array_diff($after, $before);
  56. // détecter automatiquement une classe qui implémente MapTemplateInterface
  57. foreach ($new as $decl) {
  58. if (is_subclass_of($decl, 'MapTemplateInterface')) {
  59. $loadedClass = $decl;
  60. break;
  61. }
  62. }
  63. // fallback : nom conventionnel défini dans le même fichier
  64. if ($loadedClass === null) {
  65. $canon = ucfirst($id) . 'Template';
  66. if (in_array($canon, $new, true)) $loadedClass = $canon;
  67. }
  68. } catch (Throwable $e) {
  69. // ne pas interrompre la discovery sur erreur de template utilisateur
  70. $loadedClass = null;
  71. }
  72. }
  73. }
  74. // Ne pas tenter de charger des classes locales : forcer l'usage exclusif des plugins dans mapModels
  75. // si $loadedClass est null, on laisse null (fallback géré par TemplateFactory::get)
  76. $results[$id] = [
  77. 'id' => $id,
  78. 'name' => $name,
  79. 'description' => $desc,
  80. 'class' => $loadedClass,
  81. 'entry' => $entryPath,
  82. 'loadedFromModel' => $loadedFromModel
  83. ];
  84. }
  85. }
  86. }
  87. // Always ensure neutral exists as fallback (no local PHP required)
  88. if (!isset($results['neutral'])) {
  89. $results['neutral'] = [
  90. 'id' => 'neutral',
  91. 'name' => 'Neutre',
  92. 'description' => 'Template neutre',
  93. 'class' => null,
  94. 'entry' => null,
  95. 'loadedFromModel' => false
  96. ];
  97. }
  98. self::$cache = $results;
  99. return $results;
  100. }
  101. /**
  102. * Retourne la classe du template demandé (ou NeutralTemplate)
  103. * @param string|null $id
  104. * @return string|null
  105. */
  106. public static function get(?string $id = null): ?string
  107. {
  108. $list = self::listAvailable();
  109. if ($id && isset($list[$id]) && !empty($list[$id]['class'])) return $list[$id]['class'];
  110. // si neutral a une classe fournie par mapModels, l'utiliser
  111. if (!empty($list['neutral']['class'])) return $list['neutral']['class'];
  112. // sinon retourner le wrapper neutre global
  113. return class_exists('NeutralTemplateWrapper') ? 'NeutralTemplateWrapper' : null;
  114. }
  115. }
  116. }