$id]); * App::flash('error', ErrorHandler::userMessage($e)); */ class ErrorHandler { /** * Map SQLite FK constraint errors to human-readable field names. * * SQLite FK errors reference the *child* table (where the FK column lives), * e.g. "INSERT INTO theses" when orientation_id FK fails. * * We map child table → the form field(s) whose values populate its FK columns. */ private const FK_TABLE_MAP = [ // Main thesis table — FK columns: orientation_id, ap_program_id, // finality_id, access_type_id, license_id 'theses' => 'Orientation, AP, Finalité, Type d\'accès ou Licence', // Junction tables — each maps to one specific field 'thesis_authors' => 'Auteur·ice', 'thesis_supervisors' => 'Composition du jury', 'thesis_languages' => 'Langue(s)', 'thesis_formats' => 'Format(s)', 'thesis_tags' => 'Mots-clés', 'thesis_files' => 'Fichiers', // Admin / system tables 'share_links' => 'Lien étudiant·e', 'file_access_requests' => "Demande d'accès", 'form_help_blocks' => "Bloc d'aide", 'site_settings' => 'Paramètres', 'admin_log' => 'Journal admin', // Parent tables (when referenced directly in "table" pattern) 'orientations' => 'Orientation', 'ap_programs' => 'AP', 'finality_types' => 'Finalité', 'access_types' => "Type d'accès", 'license_types' => 'Licence', 'authors' => 'Auteur·ice', 'supervisors' => 'Composition du jury', 'languages' => 'Langue(s)', 'format_types' => 'Format(s)', 'tags' => 'Mots-clés', ]; /** * Map an exception to a user-facing message. * * Domain exceptions (validation, duplicates) carry their own message; * system exceptions (PDO, generic) get sanitised explanations with * specific field identification where possible. */ public static function userMessage(\Throwable $e): string { // ── Domain exceptions with meaningful messages ────────────────────── if ($e instanceof DuplicateThesisException) { return $e->getMessage(); // Already user-friendly } // ── Database errors ───────────────────────────────────────────────── if ($e instanceof \PDOException) { return self::pdoMessage($e); } // ── Validation errors (RuntimeException, InvalidArgumentException) ── // These are thrown with user-friendly French messages — pass through. if ($e instanceof \RuntimeException) { return $e->getMessage(); } // ── Everything else — generic fallback ────────────────────────────── return 'Une erreur inattendue est survenue.' . ' Veuillez réessayer ou contacter l\'équipe.'; } /** * Build a user-facing message from a PDOException. * * For FK constraint failures, the SQLite error message includes the * table name (e.g. "FOREIGN KEY constraint failed INSERT INTO thesis_formats"). * We extract it and map to a field label. */ private static function pdoMessage(\PDOException $e): string { $msg = $e->getMessage(); if (str_contains($msg, 'FOREIGN KEY constraint failed')) { $field = self::extractFkField($msg); if ($field !== null) { return "Erreur de base de données : la référence pour « {$field} » est invalide." . ' Vérifiez que la valeur existe dans la base de données.'; } return 'Erreur de base de données : une contrainte de référence est invalide.' . ' Vérifiez les champs Orientation, AP, Finalité, Langues, Formats.'; } // ── UNIQUE constraint ─────────────────────────────────────────────── if (str_contains($msg, 'UNIQUE constraint failed')) { return 'Erreur de base de données : une valeur en double a été détectée.' . ' Veuillez réessayer ou contacter l\'équipe.'; } // ── NOT NULL ──────────────────────────────────────────────────────── if (str_contains($msg, 'NOT NULL constraint failed')) { return 'Erreur de base de données : un champ obligatoire est manquant.' . ' Veuillez réessayer ou contacter l\'équipe.'; } // ── Generic SQL error — don't leak raw SQL to users ──────────────── return 'Une erreur de base de données est survenue.' . ' Veuillez réessayer ou contacter l\'équipe.'; } /** * Try to extract the referenced table name from a SQLite FK error message. * * SQLite FK errors typically contain the table name in the message body: * "FOREIGN KEY constraint failed INSERT INTO thesis_formats (thesis_id, format_id) VALUES (?, ?)" * * Also handles quoted table names in newer SQLite messages: * "FOREIGN KEY constraint failed (table \"orientations\")" */ private static function extractFkField(string $msg): ?string { // Pattern 1: "table \"tablename\"" (SQLite 3.37+) if (preg_match('/table\s+"([^"]+)"/i', $msg, $m)) { $table = strtolower($m[1]); return self::FK_TABLE_MAP[$table] ?? null; } // Pattern 2: "INSERT INTO tablename" or "UPDATE tablename" if (preg_match('/(?:INSERT\s+INTO|UPDATE)\s+"?(\w+)"?/i', $msg, $m)) { $table = strtolower($m[1]); return self::FK_TABLE_MAP[$table] ?? null; } // Pattern 3: "REFERENCES tablename" — the constraint itself if (preg_match('/REFERENCES\s+"?(\w+)"?/i', $msg, $m)) { $table = strtolower($m[1]); return self::FK_TABLE_MAP[$table] ?? null; } return null; } /** * Write a structured error log entry. * * @param string $context e.g. 'thesis_edit', 'partage_submit', 'csv_import' * @param \Throwable $e * @param array $extra arbitrary key-value context to include in the log */ public static function log(string $context, \Throwable $e, array $extra = []): void { $parts = [ 'context=' . $context, 'exception=' . get_class($e), 'message=' . $e->getMessage(), ]; foreach ($extra as $k => $v) { if (is_scalar($v) || $v === null) { $parts[] = $k . '=' . json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } elseif (is_array($v)) { $parts[] = $k . '=' . json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } else { $parts[] = $k . '=' . gettype($v); } } $parts[] = 'trace=' . $e->getTraceAsString(); error_log(implode(' | ', $parts)); } }