MapModel.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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): 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($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. $sql = "
  92. INSERT INTO maps (name, description, width, height, data, file_path, created_at, updated_at)
  93. VALUES (:name, :description, :width, :height, :data, :file_path, :created_at, :updated_at)
  94. ";
  95. try {
  96. $stmt = $pdo->prepare($sql);
  97. $stmt->execute([
  98. 'name' => $map->getName(),
  99. 'description' => $description,
  100. 'width' => $map->getWidth(),
  101. 'height' => $map->getHeight(),
  102. 'data' => json_encode($data),
  103. 'file_path' => $filePath,
  104. 'created_at' => $map->getCreatedAt(),
  105. 'updated_at' => $map->getUpdatedAt()
  106. ]);
  107. $mapId = $pdo->lastInsertId();
  108. AppDebugger::log('Carte créée avec succès, ID: ' . $mapId, 'INFO');
  109. return (int) $mapId;
  110. } catch (Exception $e) {
  111. AppDebugger::log('Erreur lors de la création de la carte: ' . $e->getMessage(), 'ERROR');
  112. return false;
  113. }
  114. }
  115. /**
  116. * Récupère les données brutes d'une carte par son ID (pour usage interne)
  117. *
  118. * @param int $id L'ID de la carte
  119. * @return array|null Les données de la carte ou null si non trouvée
  120. */
  121. private static function findById(int $id): ?array
  122. {
  123. try {
  124. $pdo = self::getPDO();
  125. $stmt = $pdo->prepare('SELECT * FROM maps WHERE id = :id');
  126. $stmt->execute(['id' => $id]);
  127. $row = $stmt->fetch(PDO::FETCH_ASSOC);
  128. return $row ?: null;
  129. } catch (Exception $e) {
  130. AppDebugger::log('Erreur dans findById: ' . $e->getMessage(), 'ERROR');
  131. return null;
  132. }
  133. }
  134. /**
  135. * Récupère une carte par son ID
  136. *
  137. * @param int $id L'ID de la carte
  138. * @return Map|null La carte chargée ou null si non trouvée
  139. */
  140. public static function findMap(int $id): ?Map
  141. {
  142. $row = self::findById($id);
  143. if (!$row) {
  144. return null;
  145. }
  146. // Charger depuis le fichier JSON
  147. $map = Map::fromFile(__DIR__ . '/../../' . $row['file_path']);
  148. if (!$map) {
  149. AppDebugger::log('Erreur lors du chargement de la carte depuis le fichier: ' . __DIR__ . '/../../' . $row['file_path'], 'ERROR');
  150. return null;
  151. }
  152. return $map;
  153. }
  154. /**
  155. * Met à jour une carte existante
  156. *
  157. * @param int $id L'ID de la carte à mettre à jour
  158. * @param Map $map Les nouvelles données de la carte
  159. * @param string|null $description Nouvelle description optionnelle
  160. * @return bool True si la mise à jour a réussi
  161. */
  162. public static function update(int $id, Map $map, ?string $description = null): bool
  163. {
  164. $pdo = self::getPDO();
  165. // Vérifier que la carte existe
  166. $existing = self::findById($id);
  167. if (!$existing) {
  168. AppDebugger::log('Carte non trouvée pour mise à jour, ID: ' . $id, 'ERROR');
  169. return false;
  170. }
  171. // Sauvegarder le fichier JSON mis à jour
  172. $filePath = $existing['file_path'];
  173. if (!$map->saveToFile($filePath)) {
  174. AppDebugger::log('Erreur lors de la sauvegarde du fichier JSON mis à jour', 'ERROR');
  175. return false;
  176. }
  177. // Mettre à jour les données
  178. $data = [
  179. 'name' => $map->getName(),
  180. 'width' => $map->getWidth(),
  181. 'height' => $map->getHeight(),
  182. 'created_at' => $existing['created_at'], // Garder la date de création originale
  183. 'updated_at' => $map->getUpdatedAt(),
  184. 'statistics' => $map->getStatistics()
  185. ];
  186. $sql = "
  187. UPDATE maps
  188. SET name = :name, description = :description, width = :width, height = :height,
  189. data = :data, updated_at = :updated_at
  190. WHERE id = :id
  191. ";
  192. try {
  193. $stmt = $pdo->prepare($sql);
  194. $stmt->execute([
  195. 'id' => $id,
  196. 'name' => $map->getName(),
  197. 'description' => $description,
  198. 'width' => $map->getWidth(),
  199. 'height' => $map->getHeight(),
  200. 'data' => json_encode($data),
  201. 'updated_at' => $map->getUpdatedAt()
  202. ]);
  203. AppDebugger::log('Carte mise à jour avec succès, ID: ' . $id, 'INFO');
  204. return true;
  205. } catch (Exception $e) {
  206. AppDebugger::log('Erreur lors de la mise à jour de la carte: ' . $e->getMessage(), 'ERROR');
  207. return false;
  208. }
  209. }
  210. /**
  211. * Supprime une carte
  212. *
  213. * @param int $id L'ID de la carte à supprimer
  214. * @return bool True si la suppression a réussi
  215. */
  216. public static function delete(int $id): bool
  217. {
  218. $pdo = self::getPDO();
  219. // Récupérer les informations de la carte avant suppression
  220. $existing = self::findById($id);
  221. if (!$existing) {
  222. AppDebugger::log('Carte non trouvée pour suppression, ID: ' . $id, 'ERROR');
  223. return false;
  224. }
  225. // Supprimer le fichier JSON
  226. if (file_exists($existing['file_path'])) {
  227. if (!unlink($existing['file_path'])) {
  228. AppDebugger::log('Erreur lors de la suppression du fichier: ' . $existing['file_path'], 'ERROR');
  229. // Ne pas arrêter la suppression pour autant
  230. }
  231. }
  232. // Supprimer de la base de données
  233. $stmt = $pdo->prepare('DELETE FROM maps WHERE id = :id');
  234. try {
  235. $stmt->execute(['id' => $id]);
  236. $affectedRows = $stmt->rowCount();
  237. if ($affectedRows > 0) {
  238. AppDebugger::log('Carte supprimée avec succès, ID: ' . $id, 'INFO');
  239. return true;
  240. } else {
  241. AppDebugger::log('Aucune carte supprimée, ID: ' . $id, 'WARNING');
  242. return false;
  243. }
  244. } catch (Exception $e) {
  245. AppDebugger::log('Erreur lors de la suppression de la carte: ' . $e->getMessage(), 'ERROR');
  246. return false;
  247. }
  248. }
  249. /**
  250. * Récupère toutes les cartes avec pagination
  251. *
  252. * @param int $page Le numéro de page (commence à 1)
  253. * @param int $perPage Nombre d'éléments par page
  254. * @param string $orderBy Colonne de tri
  255. * @param string $orderDirection Direction du tri (ASC ou DESC)
  256. * @return array Tableau avec 'data' (cartes) et 'pagination' (infos de pagination)
  257. */
  258. public static function paginate(int $page = 1, int $perPage = 10, string $orderBy = 'created_at', string $orderDirection = 'DESC'): array
  259. {
  260. $pdo = self::getPDO();
  261. // Calculer l'offset
  262. $offset = ($page - 1) * $perPage;
  263. // Compter le total
  264. $totalStmt = $pdo->query('SELECT COUNT(*) as total FROM maps');
  265. $total = $totalStmt->fetch(PDO::FETCH_ASSOC)['total'];
  266. // Récupérer les cartes
  267. $sql = "SELECT * FROM maps ORDER BY {$orderBy} {$orderDirection} LIMIT :limit OFFSET :offset";
  268. $stmt = $pdo->prepare($sql);
  269. $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
  270. $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
  271. $stmt->execute();
  272. $maps = $stmt->fetchAll(PDO::FETCH_ASSOC);
  273. // Calculer les informations de pagination
  274. $totalPages = ceil($total / $perPage);
  275. $hasNextPage = $page < $totalPages;
  276. $hasPrevPage = $page > 1;
  277. return [
  278. 'data' => $maps,
  279. 'pagination' => [
  280. 'current_page' => $page,
  281. 'per_page' => $perPage,
  282. 'total' => (int) $total,
  283. 'total_pages' => $totalPages,
  284. 'has_next_page' => $hasNextPage,
  285. 'has_prev_page' => $hasPrevPage,
  286. 'next_page' => $hasNextPage ? $page + 1 : null,
  287. 'prev_page' => $hasPrevPage ? $page - 1 : null
  288. ]
  289. ];
  290. }
  291. /**
  292. * Recherche des cartes par nom ou description
  293. *
  294. * @param string $query Le terme de recherche
  295. * @param int $limit Nombre maximum de résultats
  296. * @return array Les cartes trouvées
  297. */
  298. public static function search(string $query, int $limit = 20): array
  299. {
  300. $pdo = self::getPDO();
  301. // S'assurer que limit est un entier positif
  302. $limit = max(1, min(100, (int)$limit));
  303. // Recherche dans le nom et la description
  304. $sql = "SELECT * FROM maps
  305. WHERE name LIKE :query OR description LIKE :query
  306. ORDER BY name
  307. LIMIT $limit";
  308. $stmt = $pdo->prepare($sql);
  309. $searchTerm = '%' . $query . '%';
  310. $stmt->execute(['query' => $searchTerm]);
  311. return $stmt->fetchAll(PDO::FETCH_ASSOC);
  312. }
  313. /**
  314. * Récupère les statistiques globales des cartes
  315. *
  316. * @return array Statistiques générales
  317. */
  318. public static function getGlobalStats(): array
  319. {
  320. $pdo = self::getPDO();
  321. // Compter le nombre total de cartes
  322. $totalStmt = $pdo->query('SELECT COUNT(*) as total FROM maps');
  323. $total = $totalStmt->fetch(PDO::FETCH_ASSOC)['total'];
  324. // Dimensions moyennes
  325. $dimensionsStmt = $pdo->query('SELECT AVG(width) as avg_width, AVG(height) as avg_height FROM maps');
  326. $dimensions = $dimensionsStmt->fetch(PDO::FETCH_ASSOC);
  327. // Dernière carte créée
  328. $latestStmt = $pdo->query('SELECT name, created_at FROM maps ORDER BY created_at DESC LIMIT 1');
  329. $latest = $latestStmt->fetch(PDO::FETCH_ASSOC);
  330. return [
  331. 'total_maps' => (int) $total,
  332. 'average_width' => round((float) $dimensions['avg_width'], 1),
  333. 'average_height' => round((float) $dimensions['avg_height'], 1),
  334. 'latest_map' => $latest ?: null
  335. ];
  336. }
  337. /**
  338. * Vérifie si une carte existe
  339. *
  340. * @param int $id L'ID de la carte
  341. * @return bool True si la carte existe
  342. */
  343. public static function exists(int $id): bool
  344. {
  345. $pdo = self::getPDO();
  346. $stmt = $pdo->prepare('SELECT COUNT(*) as count FROM maps WHERE id = :id');
  347. $stmt->execute(['id' => $id]);
  348. return $stmt->fetch(PDO::FETCH_ASSOC)['count'] > 0;
  349. }
  350. /**
  351. * Duplique une carte existante
  352. *
  353. * @param int $id L'ID de la carte à dupliquer
  354. * @param string $newName Le nom de la nouvelle carte
  355. * @return int|false L'ID de la nouvelle carte ou false en cas d'erreur
  356. */
  357. public static function duplicate(int $id, string $newName): int|false
  358. {
  359. $originalMap = self::findMap($id);
  360. if (!$originalMap) {
  361. AppDebugger::log('Carte originale non trouvée pour duplication, ID: ' . $id, 'ERROR');
  362. return false;
  363. }
  364. // Créer une nouvelle carte avec le même contenu
  365. $newMap = new Map($newName, $originalMap->getWidth(), $originalMap->getHeight());
  366. // Copier toutes les tuiles
  367. for ($q = 0; $q < $originalMap->getWidth(); $q++) {
  368. for ($r = 0; $r < $originalMap->getHeight(); $r++) {
  369. $tile = $originalMap->getTile($q, $r);
  370. if ($tile) {
  371. $newMap->setTile($q, $r, $tile);
  372. }
  373. }
  374. }
  375. // Créer la nouvelle carte en base
  376. $description = "Copie de la carte #" . $id;
  377. return self::create($newMap, $description);
  378. }
  379. /**
  380. * Exporte une carte vers un format spécifique
  381. *
  382. * @param int $id L'ID de la carte
  383. * @param string $format Le format d'export ('json', 'csv', etc.)
  384. * @return string|false Les données exportées ou false en cas d'erreur
  385. */
  386. public static function export(int $id, string $format = 'json'): string|false
  387. {
  388. $map = self::findMap($id);
  389. if (!$map) {
  390. return false;
  391. }
  392. switch ($format) {
  393. case 'json':
  394. return $map->toJson(true);
  395. case 'csv':
  396. return self::exportToCSV($map);
  397. default:
  398. AppDebugger::log('Format d\'export non supporté: ' . $format, 'ERROR');
  399. return false;
  400. }
  401. }
  402. /**
  403. * Exporte une carte au format CSV
  404. *
  405. * @param Map $map La carte à exporter
  406. * @return string Les données CSV
  407. */
  408. private static function exportToCSV(Map $map): string
  409. {
  410. $csv = "q,r,type,resources,temperature,elevation\n";
  411. for ($q = 0; $q < $map->getWidth(); $q++) {
  412. for ($r = 0; $r < $map->getHeight(); $r++) {
  413. $tile = $map->getTile($q, $r);
  414. if ($tile && !$tile->isEmpty()) {
  415. $resources = json_encode($tile->getResources());
  416. $csv .= sprintf(
  417. "%d,%d,%s,%s,%s,%s\n",
  418. $q,
  419. $r,
  420. $tile->getType() ?? '',
  421. $resources,
  422. $tile->getTemperature() ?? '',
  423. $tile->getElevation() ?? ''
  424. );
  425. }
  426. }
  427. }
  428. return $csv;
  429. }
  430. }
  431. }