MapModel.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. <?php
  2. /**
  3. * Classe MapModel
  4. *
  5. * Modèle pour gérer les cartes dans la base de données.
  6. * Fournit les opérations CRUD complètes pour les cartes.
  7. */
  8. if (!class_exists('MapModel')) {
  9. class MapModel
  10. {
  11. protected static $table = 'maps';
  12. private static $pdo = null;
  13. /**
  14. * Obtient une connexion PDO à la base de données.
  15. *
  16. * @return PDO
  17. */
  18. private static function getPDO() {
  19. if (self::$pdo === null) {
  20. $config = require __DIR__ . '/../../config/config.php';
  21. // Utiliser le socket Unix pour MAMP
  22. $dsn = sprintf(
  23. 'mysql:dbname=%s;unix_socket=%s;charset=utf8mb4',
  24. $config['db_name'],
  25. $config['db_socket']
  26. );
  27. self::$pdo = new PDO($dsn, $config['db_user'], $config['db_password']);
  28. self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  29. }
  30. return self::$pdo;
  31. }
  32. /**
  33. * Crée la table maps si elle n'existe pas
  34. *
  35. * @return bool True si la table a été créée ou existe déjà
  36. */
  37. public static function createTable(): bool
  38. {
  39. $pdo = self::getPDO();
  40. $sql = "
  41. CREATE TABLE IF NOT EXISTS maps (
  42. id INT AUTO_INCREMENT PRIMARY KEY,
  43. name VARCHAR(255) NOT NULL,
  44. description TEXT,
  45. width INT NOT NULL DEFAULT 10,
  46. height INT NOT NULL DEFAULT 10,
  47. data JSON,
  48. file_path VARCHAR(500),
  49. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  50. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  51. INDEX idx_name (name),
  52. INDEX idx_created_at (created_at)
  53. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
  54. ";
  55. try {
  56. $pdo->exec($sql);
  57. AppDebugger::log('Table maps créée avec succès', 'INFO');
  58. return true;
  59. } catch (Exception $e) {
  60. AppDebugger::log('Erreur lors de la création de la table maps: ' . $e->getMessage(), 'ERROR');
  61. return false;
  62. }
  63. }
  64. /**
  65. * Crée une nouvelle carte dans la base de données
  66. *
  67. * @param Map $map L'objet Map à sauvegarder
  68. * @param string|null $description Description optionnelle
  69. * @return int|false L'ID de la carte créée ou false en cas d'erreur
  70. */
  71. public static function create(Map $map, ?string $description = null, ?string $template = null, ?int $radius = null): int|false
  72. {
  73. $pdo = self::getPDO();
  74. // Générer le chemin du fichier
  75. $fileName = 'map_' . time() . '_' . uniqid() . '.json';
  76. $filePath = 'storage/maps/' . $fileName;
  77. // Sauvegarder le fichier JSON
  78. if (!$map->saveToFile(__DIR__ . '/../../' . $filePath)) {
  79. AppDebugger::log('Erreur lors de la sauvegarde du fichier JSON', 'ERROR');
  80. return false;
  81. }
  82. // Convertir les données pour la base
  83. $data = [
  84. 'name' => $map->getName(),
  85. 'width' => $map->getWidth(),
  86. 'height' => $map->getHeight(),
  87. 'created_at' => $map->getCreatedAt(),
  88. 'updated_at' => $map->getUpdatedAt(),
  89. 'statistics' => $map->getStatistics()
  90. ];
  91. // Ajouter métadonnées template et radius pour traçabilité
  92. if ($template !== null) $data['template'] = $template;
  93. if ($radius !== null) $data['radius'] = (int)$radius;
  94. $sql = "
  95. INSERT INTO maps (name, description, width, height, data, file_path, created_at, updated_at)
  96. VALUES (:name, :description, :width, :height, :data, :file_path, :created_at, :updated_at)
  97. ";
  98. try {
  99. $stmt = $pdo->prepare($sql);
  100. $stmt->execute([
  101. 'name' => $map->getName(),
  102. 'description' => $description,
  103. 'width' => $map->getWidth(),
  104. 'height' => $map->getHeight(),
  105. 'data' => json_encode($data),
  106. 'file_path' => $filePath,
  107. 'created_at' => $map->getCreatedAt(),
  108. 'updated_at' => $map->getUpdatedAt()
  109. ]);
  110. $mapId = $pdo->lastInsertId();
  111. AppDebugger::log('Carte créée avec succès, ID: ' . $mapId, 'INFO');
  112. return (int) $mapId;
  113. } catch (Exception $e) {
  114. AppDebugger::log('Erreur lors de la création de la carte: ' . $e->getMessage(), 'ERROR');
  115. return false;
  116. }
  117. }
  118. /**
  119. * Récupère les données brutes d'une carte par son ID (pour usage interne)
  120. *
  121. * @param int $id L'ID de la carte
  122. * @return array|null Les données de la carte ou null si non trouvée
  123. */
  124. private static function findById(int $id): ?array
  125. {
  126. try {
  127. $pdo = self::getPDO();
  128. $stmt = $pdo->prepare('SELECT * FROM maps WHERE id = :id');
  129. $stmt->execute(['id' => $id]);
  130. $row = $stmt->fetch(PDO::FETCH_ASSOC);
  131. return $row ?: null;
  132. } catch (Exception $e) {
  133. AppDebugger::log('Erreur dans findById: ' . $e->getMessage(), 'ERROR');
  134. return null;
  135. }
  136. }
  137. /**
  138. * Retourne la row brute (publique) pour un id
  139. * @param int $id
  140. * @return array|null
  141. */
  142. public static function getRowById(int $id): ?array
  143. {
  144. return self::findById($id);
  145. }
  146. /**
  147. * Récupère une carte par son ID
  148. *
  149. * @param int $id L'ID de la carte
  150. * @return Map|null La carte chargée ou null si non trouvée
  151. */
  152. public static function findMap(int $id): ?Map
  153. {
  154. $row = self::findById($id);
  155. if (!$row) {
  156. return null;
  157. }
  158. // Charger depuis le fichier JSON
  159. $map = Map::fromFile(__DIR__ . '/../../' . $row['file_path']);
  160. if (!$map) {
  161. AppDebugger::log('Erreur lors du chargement de la carte depuis le fichier: ' . __DIR__ . '/../../' . $row['file_path'], 'ERROR');
  162. return null;
  163. }
  164. return $map;
  165. }
  166. /**
  167. * Met à jour une carte existante
  168. *
  169. * @param int $id L'ID de la carte à mettre à jour
  170. * @param Map $map Les nouvelles données de la carte
  171. * @param string|null $description Nouvelle description optionnelle
  172. * @return bool True si la mise à jour a réussi
  173. */
  174. public static function update(int $id, Map $map, ?string $description = null, ?string $template = null, ?int $radius = null): bool
  175. {
  176. $pdo = self::getPDO();
  177. // Vérifier que la carte existe
  178. $existing = self::findById($id);
  179. if (!$existing) {
  180. AppDebugger::log('Carte non trouvée pour mise à jour, ID: ' . $id, 'ERROR');
  181. return false;
  182. }
  183. // Sauvegarder le fichier JSON mis à jour
  184. $filePath = $existing['file_path'];
  185. if (!$map->saveToFile(__DIR__ . '/../../' . $filePath)) {
  186. AppDebugger::log('Erreur lors de la sauvegarde du fichier JSON mis à jour', 'ERROR');
  187. return false;
  188. }
  189. // Mettre à jour les données
  190. $data = [
  191. 'name' => $map->getName(),
  192. 'width' => $map->getWidth(),
  193. 'height' => $map->getHeight(),
  194. 'created_at' => $existing['created_at'], // Garder la date de création originale
  195. 'updated_at' => $map->getUpdatedAt(),
  196. 'statistics' => $map->getStatistics()
  197. ];
  198. // Ajouter métadonnées template et radius si fournies
  199. if ($template !== null) $data['template'] = $template;
  200. if ($radius !== null) $data['radius'] = (int)$radius;
  201. $sql = "
  202. UPDATE maps
  203. SET name = :name, description = :description, width = :width, height = :height,
  204. data = :data, updated_at = :updated_at
  205. WHERE id = :id
  206. ";
  207. try {
  208. $stmt = $pdo->prepare($sql);
  209. $stmt->execute([
  210. 'id' => $id,
  211. 'name' => $map->getName(),
  212. 'description' => $description,
  213. 'width' => $map->getWidth(),
  214. 'height' => $map->getHeight(),
  215. 'data' => json_encode($data),
  216. 'updated_at' => $map->getUpdatedAt()
  217. ]);
  218. AppDebugger::log('Carte mise à jour avec succès, ID: ' . $id, 'INFO');
  219. return true;
  220. } catch (Exception $e) {
  221. AppDebugger::log('Erreur lors de la mise à jour de la carte: ' . $e->getMessage(), 'ERROR');
  222. return false;
  223. }
  224. }
  225. /**
  226. * Supprime une carte
  227. *
  228. * @param int $id L'ID de la carte à supprimer
  229. * @return bool True si la suppression a réussi
  230. */
  231. public static function delete(int $id): bool
  232. {
  233. $pdo = self::getPDO();
  234. // Récupérer les informations de la carte avant suppression
  235. $existing = self::findById($id);
  236. if (!$existing) {
  237. AppDebugger::log('Carte non trouvée pour suppression, ID: ' . $id, 'ERROR');
  238. return false;
  239. }
  240. // Supprimer le fichier JSON (résoudre chemin absolu depuis la racine du projet)
  241. $projectRootPath = __DIR__ . '/../../';
  242. $absoluteFilePath = realpath($projectRootPath . $existing['file_path']) ?: ($projectRootPath . $existing['file_path']);
  243. if (file_exists($absoluteFilePath)) {
  244. if (!@unlink($absoluteFilePath)) {
  245. AppDebugger::log('Erreur lors de la suppression du fichier: ' . $absoluteFilePath, 'ERROR');
  246. // Ne pas arrêter la suppression pour autant
  247. }
  248. } else {
  249. AppDebugger::log('Fichier à supprimer introuvable: ' . $absoluteFilePath, 'WARNING');
  250. }
  251. // Supprimer de la base de données
  252. $stmt = $pdo->prepare('DELETE FROM maps WHERE id = :id');
  253. try {
  254. $stmt->execute(['id' => $id]);
  255. $affectedRows = $stmt->rowCount();
  256. if ($affectedRows > 0) {
  257. AppDebugger::log('Carte supprimée avec succès, ID: ' . $id, 'INFO');
  258. return true;
  259. } else {
  260. AppDebugger::log('Aucune carte supprimée, ID: ' . $id, 'WARNING');
  261. return false;
  262. }
  263. } catch (Exception $e) {
  264. AppDebugger::log('Erreur lors de la suppression de la carte: ' . $e->getMessage(), 'ERROR');
  265. return false;
  266. }
  267. }
  268. /**
  269. * Récupère toutes les cartes avec pagination
  270. *
  271. * @param int $page Le numéro de page (commence à 1)
  272. * @param int $perPage Nombre d'éléments par page
  273. * @param string $orderBy Colonne de tri
  274. * @param string $orderDirection Direction du tri (ASC ou DESC)
  275. * @return array Tableau avec 'data' (cartes) et 'pagination' (infos de pagination)
  276. */
  277. public static function paginate(int $page = 1, int $perPage = 10, string $orderBy = 'created_at', string $orderDirection = 'DESC'): array
  278. {
  279. $pdo = self::getPDO();
  280. // Calculer l'offset
  281. $offset = ($page - 1) * $perPage;
  282. // Compter le total
  283. $totalStmt = $pdo->query('SELECT COUNT(*) as total FROM maps');
  284. $total = $totalStmt->fetch(PDO::FETCH_ASSOC)['total'];
  285. // Récupérer les cartes
  286. $sql = "SELECT * FROM maps ORDER BY {$orderBy} {$orderDirection} LIMIT :limit OFFSET :offset";
  287. $stmt = $pdo->prepare($sql);
  288. $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
  289. $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
  290. $stmt->execute();
  291. $maps = $stmt->fetchAll(PDO::FETCH_ASSOC);
  292. // Calculer les informations de pagination
  293. $totalPages = ceil($total / $perPage);
  294. $hasNextPage = $page < $totalPages;
  295. $hasPrevPage = $page > 1;
  296. return [
  297. 'data' => $maps,
  298. 'pagination' => [
  299. 'current_page' => $page,
  300. 'per_page' => $perPage,
  301. 'total' => (int) $total,
  302. 'total_pages' => $totalPages,
  303. 'has_next_page' => $hasNextPage,
  304. 'has_prev_page' => $hasPrevPage,
  305. 'next_page' => $hasNextPage ? $page + 1 : null,
  306. 'prev_page' => $hasPrevPage ? $page - 1 : null
  307. ]
  308. ];
  309. }
  310. /**
  311. * Recherche des cartes par nom ou description
  312. *
  313. * @param string $query Le terme de recherche
  314. * @param int $limit Nombre maximum de résultats
  315. * @return array Les cartes trouvées
  316. */
  317. public static function search(string $query, int $limit = 20): array
  318. {
  319. $pdo = self::getPDO();
  320. // S'assurer que limit est un entier positif
  321. $limit = max(1, min(100, (int)$limit));
  322. // Recherche dans le nom et la description
  323. $sql = "SELECT * FROM maps
  324. WHERE name LIKE :query OR description LIKE :query
  325. ORDER BY name
  326. LIMIT $limit";
  327. $stmt = $pdo->prepare($sql);
  328. $searchTerm = '%' . $query . '%';
  329. $stmt->execute(['query' => $searchTerm]);
  330. return $stmt->fetchAll(PDO::FETCH_ASSOC);
  331. }
  332. /**
  333. * Récupère les statistiques globales des cartes
  334. *
  335. * @return array Statistiques générales
  336. */
  337. public static function getGlobalStats(): array
  338. {
  339. $pdo = self::getPDO();
  340. // Compter le nombre total de cartes
  341. $totalStmt = $pdo->query('SELECT COUNT(*) as total FROM maps');
  342. $total = $totalStmt->fetch(PDO::FETCH_ASSOC)['total'];
  343. // Dimensions moyennes
  344. $dimensionsStmt = $pdo->query('SELECT AVG(width) as avg_width, AVG(height) as avg_height FROM maps');
  345. $dimensions = $dimensionsStmt->fetch(PDO::FETCH_ASSOC);
  346. // Dernière carte créée
  347. $latestStmt = $pdo->query('SELECT name, created_at FROM maps ORDER BY created_at DESC LIMIT 1');
  348. $latest = $latestStmt->fetch(PDO::FETCH_ASSOC);
  349. return [
  350. 'total_maps' => (int) $total,
  351. 'average_width' => round((float) $dimensions['avg_width'], 1),
  352. 'average_height' => round((float) $dimensions['avg_height'], 1),
  353. 'latest_map' => $latest ?: null
  354. ];
  355. }
  356. /**
  357. * Vérifie si une carte existe
  358. *
  359. * @param int $id L'ID de la carte
  360. * @return bool True si la carte existe
  361. */
  362. public static function exists(int $id): bool
  363. {
  364. $pdo = self::getPDO();
  365. $stmt = $pdo->prepare('SELECT COUNT(*) as count FROM maps WHERE id = :id');
  366. $stmt->execute(['id' => $id]);
  367. return $stmt->fetch(PDO::FETCH_ASSOC)['count'] > 0;
  368. }
  369. /**
  370. * Duplique une carte existante
  371. *
  372. * @param int $id L'ID de la carte à dupliquer
  373. * @param string $newName Le nom de la nouvelle carte
  374. * @return int|false L'ID de la nouvelle carte ou false en cas d'erreur
  375. */
  376. public static function duplicate(int $id, string $newName): int|false
  377. {
  378. $originalMap = self::findMap($id);
  379. if (!$originalMap) {
  380. AppDebugger::log('Carte originale non trouvée pour duplication, ID: ' . $id, 'ERROR');
  381. return false;
  382. }
  383. // Créer une nouvelle carte avec le même contenu
  384. $newMap = new Map($newName, $originalMap->getWidth(), $originalMap->getHeight());
  385. // Copier toutes les tuiles
  386. for ($q = 0; $q < $originalMap->getWidth(); $q++) {
  387. for ($r = 0; $r < $originalMap->getHeight(); $r++) {
  388. $tile = $originalMap->getTile($q, $r);
  389. if ($tile) {
  390. $newMap->setTile($q, $r, $tile);
  391. }
  392. }
  393. }
  394. // Créer la nouvelle carte en base
  395. $description = "Copie de la carte #" . $id;
  396. return self::create($newMap, $description);
  397. }
  398. /**
  399. * Exporte une carte vers un format spécifique
  400. *
  401. * @param int $id L'ID de la carte
  402. * @param string $format Le format d'export ('json', 'csv', etc.)
  403. * @return string|false Les données exportées ou false en cas d'erreur
  404. */
  405. public static function export(int $id, string $format = 'json'): string|false
  406. {
  407. $map = self::findMap($id);
  408. if (!$map) {
  409. return false;
  410. }
  411. switch ($format) {
  412. case 'json':
  413. return $map->toJson(true);
  414. case 'csv':
  415. return self::exportToCSV($map);
  416. default:
  417. AppDebugger::log('Format d\'export non supporté: ' . $format, 'ERROR');
  418. return false;
  419. }
  420. }
  421. /**
  422. * Exporte une carte au format CSV
  423. *
  424. * @param Map $map La carte à exporter
  425. * @return string Les données CSV
  426. */
  427. private static function exportToCSV(Map $map): string
  428. {
  429. $csv = "q,r,type,resources,temperature,elevation\n";
  430. for ($q = 0; $q < $map->getWidth(); $q++) {
  431. for ($r = 0; $r < $map->getHeight(); $r++) {
  432. $tile = $map->getTile($q, $r);
  433. if ($tile && !$tile->isEmpty()) {
  434. $resources = json_encode($tile->getResources());
  435. $csv .= sprintf(
  436. "%d,%d,%s,%s,%s,%s\n",
  437. $q,
  438. $r,
  439. $tile->getType() ?? '',
  440. $resources,
  441. $tile->getTemperature() ?? '',
  442. $tile->getElevation() ?? ''
  443. );
  444. }
  445. }
  446. }
  447. return $csv;
  448. }
  449. }
  450. }