dbPath = $this->determineDatabasePath($dbPath); try { $this->pdo = new PDO('sqlite:' . $this->dbPath); $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); // Enable foreign key constraints + performance pragmas $this->pdo->exec('PRAGMA foreign_keys = ON'); $this->pdo->exec('PRAGMA journal_mode = WAL'); $this->pdo->exec('PRAGMA synchronous = NORMAL'); $this->pdo->exec('PRAGMA cache_size = -8000'); } catch (PDOException $e) { error_log("Database connection failed: " . $e->getMessage()); throw new Exception("Impossible de se connecter à la base de données."); } } /** * Determine database path * Uses centralized config from config.php * Priority: custom path > config.php settings */ private function determineDatabasePath($customPath = null) { // Allow explicit override if ($customPath !== null && file_exists($customPath)) { return $customPath; } // Use centralized configuration return getDatabasePath(); } /** * Get singleton instance (for front-backend) * @return Database */ public static function getInstance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Get PDO connection * @return PDO */ public function getConnection() { return $this->pdo; } /** * Get PDO instance (alias for formulaire compatibility) * @return PDO */ public function getPDO() { return $this->pdo; } // ======================================================================== // TRANSACTION SUPPORT (from formulaire) // ======================================================================== /** * Begin a transaction */ public function beginTransaction() { return $this->pdo->beginTransaction(); } /** * Commit a transaction */ public function commit() { return $this->pdo->commit(); } /** * Rollback a transaction */ public function rollback() { return $this->pdo->rollback(); } // ======================================================================== // PUBLIC SITE METHODS (from front-backend) // ======================================================================== /** * Get all published theses with pagination */ public function getPublishedTheses($limit = 10, $offset = 0) { $sql = "SELECT * FROM v_theses_public ORDER BY year DESC, title ASC LIMIT :limit OFFSET :offset"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } /** * Get theses from the latest published year, in random order (per request). * Used for the default home page view. */ public function getLatestYearTheses(int $limit = 24): array { $sql = "SELECT * FROM v_theses_public WHERE year = (SELECT MAX(year) FROM theses WHERE is_published = 1) ORDER BY RANDOM() LIMIT :limit"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } /** * Get the latest year that has published theses */ public function getLatestPublishedYear(): ?int { $stmt = $this->pdo->query("SELECT MAX(year) FROM theses WHERE is_published = 1"); $val = $stmt->fetchColumn(); return $val ? (int)$val : null; } /** * Count all published theses */ public function countPublishedTheses() { $sql = "SELECT COUNT(*) as count FROM theses WHERE is_published = 1"; $stmt = $this->pdo->query($sql); $result = $stmt->fetch(); return $result['count']; } /** * Get thesis by ID with all related data (for public site) */ public function getThesisById($id) { $sql = "SELECT * FROM v_theses_full WHERE id = :id AND is_published = 1"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); $thesis = $stmt->fetch(); if (!$thesis) { return null; } // Get associated files $thesis['files'] = $this->getThesisFiles($id); return $thesis; } /** * Get thesis by ID (for admin - includes unpublished) * @param int $id Thesis ID * @return array|null Thesis data or null if not found */ public function getThesis($id) { $stmt = $this->pdo->prepare("SELECT * FROM v_theses_full WHERE id = ?"); $stmt->execute([$id]); return $stmt->fetch(); } /** * Get files associated with a thesis */ public function getThesisFiles($thesisId) { $sql = "SELECT * FROM thesis_files WHERE thesis_id = :thesis_id ORDER BY file_type, uploaded_at"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':thesis_id', $thesisId, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } // ======================================================================== // SEARCH FUNCTIONALITY (from front-backend - secure implementation) // ======================================================================== /** * Escape LIKE wildcards in user input to prevent wildcard injection */ private function escapeLikeString($string) { return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $string); } /** * Validate and sanitize search parameters * @throws InvalidArgumentException if validation fails */ private function validateSearchParams($params) { $validated = []; if (!empty($params['query'])) { $query = trim($params['query']); if (strlen($query) > 200) { throw new InvalidArgumentException("Search query too long (max 200 characters)"); } $validated['query'] = $this->escapeLikeString($query); } if (!empty($params['year'])) { $year = intval($params['year']); if ($year < 1900 || $year > 2100) { throw new InvalidArgumentException("Invalid year"); } $validated['year'] = $year; } if (!empty($params['orientation'])) { $orientation = trim($params['orientation']); if (strlen($orientation) > 100) { throw new InvalidArgumentException("Orientation name too long"); } $validated['orientation'] = $this->escapeLikeString($orientation); } if (!empty($params['ap_program'])) { $ap = trim($params['ap_program']); if (strlen($ap) > 100) { throw new InvalidArgumentException("AP program name too long"); } $validated['ap_program'] = $this->escapeLikeString($ap); } if (!empty($params['finality'])) { $finality = trim($params['finality']); if (strlen($finality) > 100) { throw new InvalidArgumentException("Finality name too long"); } $validated['finality'] = $this->escapeLikeString($finality); } if (!empty($params['keyword'])) { $keyword = trim($params['keyword']); if (strlen($keyword) > 100) { throw new InvalidArgumentException("Keyword too long"); } $validated['keyword'] = $this->escapeLikeString($keyword); } if (!empty($params['format'])) { $format = trim($params['format']); if (strlen($format) > 100) { throw new InvalidArgumentException("Format name too long"); } $validated['format'] = $this->escapeLikeString($format); } if (!empty($params['language'])) { $language = trim($params['language']); if (strlen($language) > 50) { throw new InvalidArgumentException("Language name too long"); } $validated['language'] = $this->escapeLikeString($language); } if (isset($params['is_doctoral'])) { $validated['is_doctoral'] = (bool)$params['is_doctoral']; } return $validated; } /** * Build WHERE conditions and named bindings from validated search params. * Always includes the `is_published = 1` guard. * * @param array $params Already-validated params (output of validateSearchParams) * @return array{0: string[], 1: array} [$conditions, $bindings] */ private function buildSearchConditions(array $params): array { $conditions = ["vp.is_published = 1"]; $bindings = []; if (!empty($params['query'])) { $conditions[] = "( vp.title LIKE :query ESCAPE '\\' OR vp.subtitle LIKE :query ESCAPE '\\' OR vp.synopsis LIKE :query ESCAPE '\\' OR vp.authors LIKE :query ESCAPE '\\' OR vp.supervisors LIKE :query ESCAPE '\\' OR EXISTS ( SELECT 1 FROM thesis_tags tt2 JOIN tags tg2 ON tg2.id = tt2.tag_id WHERE tt2.thesis_id = vp.id AND tg2.name LIKE :query ESCAPE '\\' ) )"; $bindings[':query'] = '%' . $params['query'] . '%'; } if (!empty($params['year'])) { $conditions[] = "vp.year = :year"; $bindings[':year'] = $params['year']; } if (!empty($params['orientation'])) { $conditions[] = "vp.orientation LIKE :orientation ESCAPE '\\'"; $bindings[':orientation'] = '%' . $params['orientation'] . '%'; } if (!empty($params['ap_program'])) { $conditions[] = "vp.ap_program LIKE :ap_program ESCAPE '\\'"; $bindings[':ap_program'] = '%' . $params['ap_program'] . '%'; } if (!empty($params['finality'])) { $conditions[] = "vp.finality_type LIKE :finality ESCAPE '\\'"; $bindings[':finality'] = '%' . $params['finality'] . '%'; } if (!empty($params['keyword'])) { $conditions[] = "EXISTS ( SELECT 1 FROM thesis_tags tt_kw JOIN tags tg_kw ON tg_kw.id = tt_kw.tag_id WHERE tt_kw.thesis_id = vp.id AND tg_kw.name LIKE :keyword ESCAPE '\\' )"; $bindings[':keyword'] = '%' . $params['keyword'] . '%'; } if (!empty($params['format'])) { $conditions[] = "vp.formats LIKE :format ESCAPE '\\'"; $bindings[':format'] = '%' . $params['format'] . '%'; } if (!empty($params['language'])) { $conditions[] = "vp.languages LIKE :language ESCAPE '\\'"; $bindings[':language'] = '%' . $params['language'] . '%'; } if (isset($params['is_doctoral'])) { $conditions[] = "vp.is_doctoral = :is_doctoral"; $bindings[':is_doctoral'] = $params['is_doctoral'] ? 1 : 0; } return [$conditions, $bindings]; } /** * Search theses with filters (secure implementation) */ public function searchTheses(array $params = [], $limit = 20, $offset = 0) { $params = $this->validateSearchParams($params); $limit = max(1, min(100, intval($limit))); $offset = max(0, intval($offset)); [$conditions, $bindings] = $this->buildSearchConditions($params); $whereClause = implode(' AND ', $conditions); $sql = "SELECT vp.* FROM v_theses_public vp WHERE $whereClause ORDER BY vp.year DESC, vp.title ASC LIMIT :limit OFFSET :offset"; $stmt = $this->pdo->prepare($sql); foreach ($bindings as $key => $value) { $stmt->bindValue($key, $value); } $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } /** * Count search results */ public function countSearchResults(array $params = []) { $params = $this->validateSearchParams($params); [$conditions, $bindings] = $this->buildSearchConditions($params); $whereClause = implode(' AND ', $conditions); $sql = "SELECT COUNT(*) as count FROM v_theses_public vp WHERE $whereClause"; $stmt = $this->pdo->prepare($sql); foreach ($bindings as $key => $value) { $stmt->bindValue($key, $value); } $stmt->execute(); $result = $stmt->fetch(); return $result['count']; } /** * Get all available years from published theses */ /** * Return ALL published theses (no cap) — for internal use (student index). * Not exposed to user-controlled input. */ public function getAllPublishedTheses(): array { $stmt = $this->pdo->query( "SELECT vp.* FROM v_theses_public vp ORDER BY vp.year DESC, vp.title ASC" ); return $stmt->fetchAll(); } public function getAvailableYears() { $sql = "SELECT DISTINCT year FROM theses WHERE is_published = 1 ORDER BY year DESC"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(PDO::FETCH_COLUMN); } /** * Get all orientations */ public function getOrientations() { $sql = "SELECT * FROM orientations ORDER BY name"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } /** * Alias for formulaire compatibility */ public function getAllOrientations() { return $this->getOrientations(); } /** * Get all AP programs */ public function getApPrograms() { $sql = "SELECT * FROM ap_programs ORDER BY name"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } /** * Alias for formulaire compatibility */ public function getAllAPPrograms() { return $this->getApPrograms(); } /** * Get all finality types */ public function getFinalityTypes() { $sql = "SELECT * FROM finality_types ORDER BY name"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } /** * Alias for formulaire compatibility */ public function getAllFinalityTypes() { return $this->getFinalityTypes(); } /** * Get all keywords used in published theses */ public function getUsedTags(): array { $sql = "SELECT DISTINCT tg.id, tg.name FROM tags tg JOIN thesis_tags tt ON tg.id = tt.tag_id JOIN theses th ON tt.thesis_id = th.id WHERE th.is_published = 1 ORDER BY tg.name"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } /** Backwards-compat alias */ public function getUsedKeywords(): array { return $this->getUsedTags(); } /** * Get all format types */ public function getFormatTypes() { $sql = "SELECT * FROM format_types ORDER BY name"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } /** * Alias for formulaire compatibility */ public function getAllFormatTypes() { return $this->getFormatTypes(); } /** * Get all languages */ public function getLanguages() { $sql = "SELECT * FROM languages ORDER BY name"; $stmt = $this->pdo->query($sql); return $stmt->fetchAll(); } /** * Alias for formulaire compatibility */ public function getAllLanguages() { return $this->getLanguages(); } // ======================================================================== // ADMIN LIST METHOD // ======================================================================== /** * Return theses for the admin list view, with optional filters. * * Filters (all optional): * 'search' string – matches title, subtitle, or author name (LIKE) * 'year' int – exact year match * 'orientation' int – orientation_id exact match * * @param array $filters * @return array */ public function getThesesList(array $filters = []): array { $sql = "SELECT t.id, t.identifier, t.title, t.subtitle, t.year, o.name as orientation, ap.name as ap_program, GROUP_CONCAT(DISTINCT a.name) as authors, t.submitted_at, t.is_published FROM theses t LEFT JOIN orientations o ON t.orientation_id = o.id LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id LEFT JOIN authors a ON ta.author_id = a.id WHERE 1=1"; $params = []; if (!empty($filters['search'])) { $sql .= " AND (t.title LIKE ? OR t.subtitle LIKE ? OR a.name LIKE ?)"; $searchParam = '%' . $filters['search'] . '%'; $params[] = $searchParam; $params[] = $searchParam; $params[] = $searchParam; } if (!empty($filters['year'])) { $sql .= " AND t.year = ?"; $params[] = intval($filters['year']); } if (!empty($filters['orientation'])) { $sql .= " AND t.orientation_id = ?"; $params[] = intval($filters['orientation']); } $sql .= " GROUP BY t.id ORDER BY t.year DESC, t.submitted_at DESC"; $stmt = $this->pdo->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(); } /** * Get distinct years present in the theses table (admin, includes unpublished). */ public function getAllYears(): array { $stmt = $this->pdo->query("SELECT DISTINCT year FROM theses ORDER BY year DESC"); return $stmt->fetchAll(PDO::FETCH_COLUMN); } // ======================================================================== // CRUD METHODS (from formulaire) // ======================================================================== /** * Find or create an author */ public function findOrCreateAuthor($name, $email = null) { $stmt = $this->pdo->prepare("SELECT id FROM authors WHERE name = ?"); $stmt->execute([$name]); $author = $stmt->fetch(); if ($author) { if ($email && $email !== '') { $updateStmt = $this->pdo->prepare("UPDATE authors SET email = ? WHERE id = ?"); $updateStmt->execute([$email, $author['id']]); } return $author['id']; } $stmt = $this->pdo->prepare("INSERT INTO authors (name, email) VALUES (?, ?)"); $stmt->execute([$name, $email]); return $this->pdo->lastInsertId(); } /** * Find or create a supervisor */ public function findOrCreateSupervisor($name) { $stmt = $this->pdo->prepare("SELECT id FROM supervisors WHERE name = ?"); $stmt->execute([$name]); $supervisor = $stmt->fetch(); if ($supervisor) { return $supervisor['id']; } $stmt = $this->pdo->prepare("INSERT INTO supervisors (name) VALUES (?)"); $stmt->execute([$name]); return $this->pdo->lastInsertId(); } /** * Find or create a tag (formerly keyword) */ public function findOrCreateTag(string $name): ?int { $name = trim($name); if ($name === '') { return null; } $stmt = $this->pdo->prepare("SELECT id FROM tags WHERE name = ?"); $stmt->execute([$name]); $row = $stmt->fetch(); if ($row) { return (int)$row['id']; } $stmt = $this->pdo->prepare("INSERT INTO tags (name) VALUES (?)"); $stmt->execute([$name]); return (int)$this->pdo->lastInsertId(); } /** Backwards-compat alias */ public function findOrCreateKeyword($keyword): ?int { return $this->findOrCreateTag((string)$keyword); } // ======================================================================== // TAG MANAGEMENT (admin) // ======================================================================== /** * Return all tags with a count of associated (published) theses. */ public function getAllTagsWithCount(): array { $stmt = $this->pdo->query(" SELECT tg.id, tg.name, COUNT(DISTINCT tt.thesis_id) as thesis_count FROM tags tg LEFT JOIN thesis_tags tt ON tg.id = tt.tag_id GROUP BY tg.id ORDER BY tg.name COLLATE NOCASE "); return $stmt->fetchAll(); } /** * Rename a tag. Throws if the new name already exists. */ public function renameTag(int $id, string $newName): void { $newName = trim($newName); if ($newName === '') throw new Exception("Le nom du tag ne peut pas être vide."); // Check uniqueness $stmt = $this->pdo->prepare("SELECT id FROM tags WHERE name = ? AND id != ?"); $stmt->execute([$newName, $id]); if ($stmt->fetch()) throw new Exception("Un tag avec ce nom existe déjà."); $this->pdo->prepare("UPDATE tags SET name = ? WHERE id = ?")->execute([$newName, $id]); } /** * Merge sourceId into targetId: reassign all thesis_tags rows, then delete source. * Uses INSERT OR IGNORE to avoid PK conflicts. */ public function mergeTag(int $sourceId, int $targetId): void { if ($sourceId === $targetId) throw new Exception("Source et destination identiques."); $this->pdo->beginTransaction(); try { // Re-point thesis_tags rows from source → target (skip conflicts) $this->pdo->prepare(" INSERT OR IGNORE INTO thesis_tags (tag_id, thesis_id) SELECT ?, thesis_id FROM thesis_tags WHERE tag_id = ? ")->execute([$targetId, $sourceId]); // Delete the old source rows $this->pdo->prepare("DELETE FROM thesis_tags WHERE tag_id = ?")->execute([$sourceId]); // Delete the source tag itself $this->pdo->prepare("DELETE FROM tags WHERE id = ?")->execute([$sourceId]); $this->pdo->commit(); } catch (\Throwable $e) { $this->pdo->rollBack(); throw $e; } } /** * Delete a tag and all its thesis_tags rows (cascades via FK). */ public function deleteTag(int $id): void { $this->pdo->prepare("DELETE FROM tags WHERE id = ?")->execute([$id]); } /** * Get orientation ID by name */ public function getOrientationId($name) { $stmt = $this->pdo->prepare("SELECT id FROM orientations WHERE name = ?"); $stmt->execute([$name]); $result = $stmt->fetch(); return $result ? $result['id'] : null; } /** * Get AP program ID by name */ public function getAPProgramId($name) { $stmt = $this->pdo->prepare("SELECT id FROM ap_programs WHERE name = ?"); $stmt->execute([$name]); $result = $stmt->fetch(); return $result ? $result['id'] : null; } /** * Get finality type ID by name */ public function getFinalityId($name) { $stmt = $this->pdo->prepare("SELECT id FROM finality_types WHERE name = ?"); $stmt->execute([$name]); $result = $stmt->fetch(); return $result ? $result['id'] : null; } /** * Get language ID by name */ public function getLanguageId($name) { $stmt = $this->pdo->prepare("SELECT id FROM languages WHERE name = ?"); $stmt->execute([$name]); $result = $stmt->fetch(); return $result ? $result['id'] : null; } /** * Get format type ID by name */ public function getFormatId($name) { $stmt = $this->pdo->prepare("SELECT id FROM format_types WHERE name = ?"); $stmt->execute([$name]); $result = $stmt->fetch(); return $result ? $result['id'] : null; } // ======================================================================== // STATIC PAGES METHODS // ======================================================================== /** * Get a static page by slug * @param string $slug Page slug (e.g. 'about', 'licenses') * @return array|null */ public function getPage(string $slug): ?array { $stmt = $this->pdo->prepare("SELECT * FROM pages WHERE slug = ?"); $stmt->execute([$slug]); $row = $stmt->fetch(); return $row ?: null; } /** * Update content for a static page by slug * @throws Exception if slug not found */ public function savePage(string $slug, string $content): void { $stmt = $this->pdo->prepare("SELECT id FROM pages WHERE slug = ?"); $stmt->execute([$slug]); if (!$stmt->fetch()) { throw new Exception("Page slug not found: $slug"); } $stmt = $this->pdo->prepare( "UPDATE pages SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?" ); $stmt->execute([$content, $slug]); } /** * Get all static pages */ public function getAllPages(): array { $stmt = $this->pdo->query("SELECT * FROM pages ORDER BY slug"); return $stmt->fetchAll(); } // ======================================================================== // LICENSE TYPE METHODS // ======================================================================== /** * Get all license types ordered by name */ public function getLicenseTypes(): array { $stmt = $this->pdo->query("SELECT * FROM license_types ORDER BY name"); return $stmt->fetchAll(); } /** * Alias for form-loading consistency */ public function getAllLicenseTypes(): array { return $this->getLicenseTypes(); } // ======================================================================== // VISIBILITY METHODS // ======================================================================== /** * Set the access_type_id (visibility) for a single thesis. * @param int $thesisId * @param int|null $accessTypeId 1=Libre, 2=Interne, 3=Interdit, null=unset */ public function setVisibility(int $thesisId, ?int $accessTypeId): void { $stmt = $this->pdo->prepare( "UPDATE theses SET access_type_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?" ); $stmt->execute([$accessTypeId, $thesisId]); } /** * Set visibility for multiple theses at once. * @param int[] $thesisIds * @param int|null $accessTypeId */ public function bulkSetVisibility(array $thesisIds, ?int $accessTypeId): void { if (empty($thesisIds)) return; $placeholders = implode(',', array_fill(0, count($thesisIds), '?')); $params = array_merge([$accessTypeId], $thesisIds); $this->pdo->prepare( "UPDATE theses SET access_type_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN ($placeholders)" )->execute($params); } /** * Get all access types (visibility options). */ public function getAccessTypes(): array { $stmt = $this->pdo->query("SELECT * FROM access_types ORDER BY id"); return $stmt->fetchAll(); } // ======================================================================== // JURY METHODS // ======================================================================== /** * Fetch all jury members for a thesis, with role and is_external flag. */ public function getThesisJury(int $thesisId): array { $stmt = $this->pdo->prepare(" SELECT s.id, s.name, ts.role, ts.is_external, ts.supervisor_order FROM thesis_supervisors ts JOIN supervisors s ON s.id = ts.supervisor_id WHERE ts.thesis_id = ? ORDER BY ts.supervisor_order "); $stmt->execute([$thesisId]); return $stmt->fetchAll(); } /** * Replace the entire jury for a thesis in a single transaction. * $juryMembers: array of ['name' => string, 'role' => string, 'is_external' => int] */ public function setThesisJury(int $thesisId, array $juryMembers): void { $this->pdo->prepare("DELETE FROM thesis_supervisors WHERE thesis_id = ?")->execute([$thesisId]); $stmt = $this->pdo->prepare(" INSERT INTO thesis_supervisors (thesis_id, supervisor_id, role, is_external, supervisor_order) VALUES (?, ?, ?, ?, ?) "); foreach ($juryMembers as $order => $member) { $name = trim($member['name'] ?? ''); if ($name === '') continue; $supervisorId = $this->findOrCreateSupervisor($name); $role = in_array($member['role'], ['president', 'promoteur', 'lecteur']) ? $member['role'] : 'promoteur'; $isExternal = isset($member['is_external']) ? (int)$member['is_external'] : 0; $stmt->execute([$thesisId, $supervisorId, $role, $isExternal, $order + 1]); } } // ======================================================================== // BANNER METHODS // ======================================================================== /** * Set (or clear) the banner_path for a thesis. */ public function setBannerPath(int $thesisId, ?string $path): void { $stmt = $this->pdo->prepare( "UPDATE theses SET banner_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?" ); $stmt->execute([$path, $thesisId]); } /** * Insert a thesis file record */ public function insertThesisFile($thesisId, $fileType, $filePath, $fileName, $fileSize, $mimeType) { $stmt = $this->pdo->prepare(" INSERT INTO thesis_files (thesis_id, file_type, file_path, file_name, file_size, mime_type) VALUES (?, ?, ?, ?, ?, ?) "); $stmt->execute([$thesisId, $fileType, $filePath, $fileName, $fileSize, $mimeType]); return $this->pdo->lastInsertId(); } // ======================================================================== // SINGLETON PATTERN ENFORCEMENT // ======================================================================== /** * Prevent cloning */ private function __clone() {} /** * Prevent unserialization */ public function __wakeup() { throw new Exception("Cannot unserialize singleton"); } }