mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Error tests, FK violations fix
- ErrorHandler tests: 77 assertions covering FK extraction, normalization, dedup, edge cases. Fix FK table map for child tables. - Fix FK violation: (int)null → 0 in createThesis for orientation/ap/finality/license FK columns. Add FK value logging to updateThesis. - Add CURRENT_ISSUES.md with summary of FK violation, dev debugging, and tag dedup status for next conversation
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/Parsedown.php';
|
||||
require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
|
||||
class AboutController
|
||||
{
|
||||
@@ -24,7 +25,7 @@ class AboutController
|
||||
$contacts = $db->getAproposContent('contacts');
|
||||
$contacts = is_array($contacts) && !empty($contacts) ? $contacts : null;
|
||||
} catch (Exception $e) {
|
||||
error_log('Error loading about page: ' . $e->getMessage());
|
||||
ErrorHandler::log('about_page', $e);
|
||||
$rawContent = $this->defaultContent;
|
||||
$contacts = null;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ class HomeController
|
||||
public static function create(): self
|
||||
{
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
|
||||
return new self(Database::getInstance());
|
||||
}
|
||||
@@ -98,7 +99,7 @@ class HomeController
|
||||
);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log('HomeController: ' . $e->getMessage());
|
||||
ErrorHandler::log('home', $e);
|
||||
// Return safe empty state; view will show "Aucun mémoire trouvé"
|
||||
$isDefaultView = false;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/Parsedown.php';
|
||||
require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
|
||||
class LicenceController
|
||||
{
|
||||
@@ -18,7 +19,7 @@ class LicenceController
|
||||
$content = $dbPage ? $dbPage['content'] : '';
|
||||
$pageTitle = $dbPage ? $dbPage['title'] : 'Licences';
|
||||
} catch (Exception $e) {
|
||||
error_log('Error loading licence page: ' . $e->getMessage());
|
||||
ErrorHandler::log('licence_page', $e);
|
||||
$content = '';
|
||||
$pageTitle = 'Licences';
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class MediaController
|
||||
// 3. Visibility gate for thesis files
|
||||
if (preg_match('#^theses/#', $requestedPath)) {
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
try {
|
||||
$mediaDb = Database::getInstance();
|
||||
$accessTypeId = $mediaDb->getFileVisibility($requestedPath);
|
||||
@@ -61,7 +62,7 @@ class MediaController
|
||||
exit;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log('MediaController visibility check error: ' . $e->getMessage());
|
||||
ErrorHandler::log('media_visibility', $e, ['path' => $path]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ class SearchController
|
||||
{
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/RateLimit.php';
|
||||
require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
|
||||
$rateLimit = new RateLimit(
|
||||
self::RATE_LIMIT_MAX,
|
||||
@@ -105,7 +106,7 @@ class SearchController
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$validationError = $e->getMessage();
|
||||
} catch (Exception $e) {
|
||||
error_log('SearchController: ' . $e->getMessage());
|
||||
ErrorHandler::log('search', $e);
|
||||
$validationError = 'Une erreur est survenue.';
|
||||
}
|
||||
|
||||
@@ -169,7 +170,7 @@ class SearchController
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$validationError = $e->getMessage();
|
||||
} catch (Exception $e) {
|
||||
error_log('SearchController: ' . $e->getMessage());
|
||||
ErrorHandler::log('repertoire', $e);
|
||||
$validationError = 'Une erreur est survenue.';
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ class TfeController
|
||||
public static function create(): self
|
||||
{
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
|
||||
return new self(Database::getInstance());
|
||||
}
|
||||
@@ -64,7 +65,7 @@ class TfeController
|
||||
try {
|
||||
$data = $this->db->getThesisById($thesisId);
|
||||
} catch (Exception $e) {
|
||||
error_log('TfeController: ' . $e->getMessage());
|
||||
ErrorHandler::log('tfe_view', $e, ['thesis_id' => $thesisId]);
|
||||
$this->redirectHome();
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ class ThesisCreateController
|
||||
public static function make(): self
|
||||
{
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
|
||||
return new self(new Database());
|
||||
}
|
||||
@@ -198,17 +199,28 @@ class ThesisCreateController
|
||||
]);
|
||||
|
||||
$identifier = $this->db->getThesisIdentifier($thesisId);
|
||||
error_log("ThesisCreateController: created thesis #$thesisId ($identifier) with " . count($authorEntries) . ' author(s)');
|
||||
error_log("[ThesisCreate] Step 1 OK — thesis_id=$thesisId ($identifier) | authors=" . count($authorEntries));
|
||||
|
||||
$this->db->setThesisAuthors($thesisId, $authorEntries);
|
||||
error_log("[ThesisCreate] Step 2 OK — authors=" . json_encode($data['authorNames']));
|
||||
|
||||
$this->db->setThesisJury($thesisId, $data['juryMembers']);
|
||||
error_log("[ThesisCreate] Step 3 OK — jury=" . count($data['juryMembers']));
|
||||
|
||||
$this->db->setThesisLanguages($thesisId, $data['languageIds']);
|
||||
error_log("[ThesisCreate] Step 4 OK — languages=" . json_encode($data['languageIds']));
|
||||
|
||||
$this->db->setThesisFormats($thesisId, $data['formatIds']);
|
||||
error_log("[ThesisCreate] Step 5 OK — formats=" . json_encode($data['formatIds']));
|
||||
|
||||
$this->db->setThesisTags($thesisId, $data['keywords']);
|
||||
error_log("[ThesisCreate] Step 6 OK — tags=" . json_encode($data['keywords']));
|
||||
|
||||
$this->db->commit();
|
||||
error_log("[ThesisCreate] COMMIT OK — thesis_id=$thesisId");
|
||||
|
||||
} catch (Exception $e) {
|
||||
ErrorHandler::log('thesis_create_tx', $e, ['thesis_id' => $thesisId ?? null]);
|
||||
$this->db->rollback();
|
||||
throw $e;
|
||||
}
|
||||
@@ -420,12 +432,31 @@ class ThesisCreateController
|
||||
throw new Exception('Veuillez indiquer au moins un·e lecteur·ice externe.');
|
||||
}
|
||||
|
||||
// Keywords (max 10)
|
||||
$tagRaw = $this->sanitiseString($post['tag'] ?? '');
|
||||
$keywords = $tagRaw !== '' ? array_map('trim', explode(',', $tagRaw)) : [];
|
||||
// Keywords (max 10, min 3) — lowercased, spaces collapsed, deduplicated
|
||||
$keywords = [];
|
||||
$normalizeTag = fn(string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
|
||||
if (isset($post['tag']) && is_array($post['tag'])) {
|
||||
$keywords = array_values(array_unique(array_map(
|
||||
$normalizeTag,
|
||||
array_map(fn($t) => (string)$t, $post['tag'])
|
||||
)));
|
||||
$keywords = array_filter($keywords, fn($t) => $t !== '');
|
||||
$keywords = array_slice($keywords, 0, 10);
|
||||
} else {
|
||||
$tagRaw = $this->sanitiseString($post['tag'] ?? '');
|
||||
if ($tagRaw !== '') {
|
||||
$keywords = array_map($normalizeTag, explode(',', $tagRaw));
|
||||
}
|
||||
}
|
||||
$keywords = array_values(array_unique($keywords));
|
||||
$keywords = array_filter($keywords, fn($t) => $t !== '');
|
||||
$keywords = array_slice($keywords, 0, 10);
|
||||
if (count($keywords) > 10) {
|
||||
throw new Exception('Maximum 10 mots-clés autorisés.');
|
||||
}
|
||||
if (count($keywords) < 3) {
|
||||
throw new Exception('Veuillez indiquer au moins 3 mots-clés.');
|
||||
}
|
||||
|
||||
// Languages (at least one required)
|
||||
$languageIds = isset($post['languages']) && is_array($post['languages'])
|
||||
|
||||
@@ -36,6 +36,7 @@ class ThesisEditController
|
||||
public static function create(?Database $db = null): self
|
||||
{
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/ErrorHandler.php';
|
||||
|
||||
return new self($db ?? Database::getInstance());
|
||||
}
|
||||
@@ -192,7 +193,7 @@ class ThesisEditController
|
||||
|
||||
try {
|
||||
// ── 1. Thesis metadata ────────────────────────────────────────────
|
||||
$this->db->updateThesis($thesisId, [
|
||||
$meta = [
|
||||
'title' => trim($post['titre'] ?? ''),
|
||||
'subtitle' => trim($post['subtitle'] ?? ''),
|
||||
'year' => intval($post['année'] ?? 0),
|
||||
@@ -211,7 +212,9 @@ class ThesisEditController
|
||||
'exemplaire_erg' => !empty($post['exemplaire_erg']),
|
||||
'cc2r' => !empty($post['cc2r']),
|
||||
'license_custom' => trim($post['license_custom'] ?? ''),
|
||||
]);
|
||||
];
|
||||
$this->db->updateThesis($thesisId, $meta);
|
||||
error_log('[ThesisEdit] Step 1 OK — thesis_id=' . $thesisId);
|
||||
|
||||
// ── 2. Authors (alphabetically sorted) ─────────────────────────────
|
||||
$authorsRaw = trim($post['auteurice'] ?? '');
|
||||
@@ -230,10 +233,12 @@ class ThesisEditController
|
||||
];
|
||||
}
|
||||
$this->db->setThesisAuthors($thesisId, $authorEntries);
|
||||
error_log('[ThesisEdit] Step 2 OK — authors=' . json_encode($authorNames));
|
||||
|
||||
// ── 3. Jury ───────────────────────────────────────────────────────
|
||||
$juryMembers = $this->collectJuryMembers($post);
|
||||
$this->db->setThesisJury($thesisId, $juryMembers);
|
||||
error_log('[ThesisEdit] Step 3 OK — jury=' . count($juryMembers));
|
||||
|
||||
// ── 4. Languages ──────────────────────────────────────────────────
|
||||
$langIds = isset($post['languages']) && is_array($post['languages'])
|
||||
@@ -248,25 +253,43 @@ class ThesisEditController
|
||||
}
|
||||
}
|
||||
$this->db->setThesisLanguages($thesisId, $langIds);
|
||||
error_log('[ThesisEdit] Step 4 OK — languages=' . json_encode($langIds));
|
||||
|
||||
// ── 5. Formats ────────────────────────────────────────────────────
|
||||
$this->db->setThesisFormats(
|
||||
$thesisId,
|
||||
isset($post['formats']) && is_array($post['formats'])
|
||||
? $post['formats']
|
||||
: []
|
||||
);
|
||||
$formatIds = isset($post['formats']) && is_array($post['formats'])
|
||||
? $post['formats']
|
||||
: [];
|
||||
$this->db->setThesisFormats($thesisId, $formatIds);
|
||||
error_log('[ThesisEdit] Step 5 OK — formats=' . json_encode($formatIds));
|
||||
|
||||
// ── 6. Tags ───────────────────────────────────────────────────────
|
||||
$keywordsRaw = trim($post['tag'] ?? '');
|
||||
$keywords = $keywordsRaw !== ''
|
||||
? array_map('trim', explode(',', $keywordsRaw))
|
||||
: [];
|
||||
$normalizeTag = fn(string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
|
||||
$keywords = [];
|
||||
if (isset($post['tag']) && is_array($post['tag'])) {
|
||||
$keywords = array_values(array_unique(array_map(
|
||||
$normalizeTag,
|
||||
array_map(fn($t) => (string)$t, $post['tag'])
|
||||
)));
|
||||
} else {
|
||||
$keywordsRaw = trim($post['tag'] ?? '');
|
||||
if ($keywordsRaw !== '') {
|
||||
$keywords = array_map($normalizeTag, explode(',', $keywordsRaw));
|
||||
}
|
||||
}
|
||||
$keywords = array_values(array_unique($keywords));
|
||||
$keywords = array_filter($keywords, fn($t) => $t !== '');
|
||||
$keywords = array_slice($keywords, 0, 10);
|
||||
if (count($keywords) < 3) {
|
||||
throw new Exception('Veuillez indiquer au moins 3 mots-clés.');
|
||||
}
|
||||
$this->db->setThesisTags($thesisId, $keywords);
|
||||
error_log('[ThesisEdit] Step 6 OK — tags=' . json_encode($keywords));
|
||||
|
||||
$this->db->commit();
|
||||
error_log('[ThesisEdit] COMMIT OK — thesis_id=' . $thesisId);
|
||||
|
||||
} catch (Exception $e) {
|
||||
ErrorHandler::log('thesis_edit_tx', $e, ['thesis_id' => $thesisId]);
|
||||
$this->db->rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
@@ -1134,6 +1134,39 @@ class Database
|
||||
// TAG MANAGEMENT (admin)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Search tags by name prefix. Returns up to 10 matching tags.
|
||||
* If $query is empty, returns the most-used tags (up to 10).
|
||||
*/
|
||||
public function searchTags(string $query = ''): array
|
||||
{
|
||||
$query = trim($query);
|
||||
if ($query === '') {
|
||||
$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 thesis_count DESC, tg.name COLLATE NOCASE
|
||||
LIMIT 10
|
||||
');
|
||||
} else {
|
||||
$stmt = $this->pdo->prepare('
|
||||
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
|
||||
WHERE tg.name LIKE ?
|
||||
GROUP BY tg.id
|
||||
ORDER BY tg.name = ? DESC, thesis_count DESC, tg.name COLLATE NOCASE
|
||||
LIMIT 10
|
||||
');
|
||||
$stmt->execute([$query . '%', $query]);
|
||||
}
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all tags with a count of associated (published) theses.
|
||||
*/
|
||||
@@ -1740,19 +1773,27 @@ class Database
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
');
|
||||
$orientation = ($data['orientation_id'] ?? null) ? (int)$data['orientation_id'] : null;
|
||||
$ap = ($data['ap_program_id'] ?? null) ? (int)$data['ap_program_id'] : null;
|
||||
$finality = ($data['finality_id'] ?? null) ? (int)$data['finality_id'] : null;
|
||||
$license = $data['license_id'] ?? null;
|
||||
$access = $data['access_type_id'] ?? null;
|
||||
|
||||
error_log("[DB:updateThesis] thesis_id=$thesisId orientation=$orientation ap=$ap finality=$finality license=$license access=$access");
|
||||
|
||||
$stmt->execute([
|
||||
$data['title'],
|
||||
!empty($data['subtitle']) ? $data['subtitle'] : null,
|
||||
(int)$data['year'],
|
||||
($data['orientation_id'] ?? null) ? (int)$data['orientation_id'] : null,
|
||||
($data['ap_program_id'] ?? null) ? (int)$data['ap_program_id'] : null,
|
||||
($data['finality_id'] ?? null) ? (int)$data['finality_id'] : null,
|
||||
$orientation,
|
||||
$ap,
|
||||
$finality,
|
||||
$data['synopsis'],
|
||||
!empty($data['context_note']) ? $data['context_note'] : null,
|
||||
!empty($data['baiu_link']) ? $data['baiu_link'] : null,
|
||||
$data['license_id'] ?? null,
|
||||
$license,
|
||||
!empty($data['license_custom']) ? $data['license_custom'] : null,
|
||||
$data['access_type_id'] ?? null,
|
||||
$access,
|
||||
$data['is_published'] ? 1 : 0,
|
||||
!empty($data['remarks']) ? $data['remarks'] : null,
|
||||
isset($data['jury_points']) && $data['jury_points'] !== '' ? (float)$data['jury_points'] : null,
|
||||
@@ -1808,20 +1849,26 @@ class Database
|
||||
$validObjet = ['tfe', 'thèse', 'frart'];
|
||||
$objet = in_array($data['objet'] ?? '', $validObjet, true) ? $data['objet'] : 'tfe';
|
||||
|
||||
$orientation = $data['orientation_id'] ?? null;
|
||||
$ap = $data['ap_program_id'] ?? null;
|
||||
$finality = $data['finality_id'] ?? null;
|
||||
$license = $data['license_id'] ?? null;
|
||||
$access = $data['access_type_id'] ?? 2;
|
||||
|
||||
$stmt->execute([
|
||||
$identifier,
|
||||
$data['title'],
|
||||
!empty($data['subtitle']) ? $data['subtitle'] : null,
|
||||
(int)$data['year'],
|
||||
(int)$data['orientation_id'],
|
||||
(int)$data['ap_program_id'],
|
||||
(int)$data['finality_id'],
|
||||
$orientation ? (int)$orientation : null,
|
||||
$ap ? (int)$ap : null,
|
||||
$finality ? (int)$finality : null,
|
||||
$data['synopsis'],
|
||||
!empty($data['context_note']) ? $data['context_note'] : null,
|
||||
!empty($data['baiu_link']) ? $data['baiu_link'] : null,
|
||||
$data['license_id'] ?? null,
|
||||
$license ? (int)$license : null,
|
||||
!empty($data['license_custom']) ? $data['license_custom'] : null,
|
||||
isset($data['access_type_id']) ? (int)$data['access_type_id'] : 2, // default: Interne
|
||||
$access ? (int)$access : 2, // default: Interne
|
||||
$objet,
|
||||
!empty($data['remarks']) ? $data['remarks'] : null,
|
||||
isset($data['jury_points']) && $data['jury_points'] !== '' ? (float)$data['jury_points'] : null,
|
||||
|
||||
183
app/src/ErrorHandler.php
Normal file
183
app/src/ErrorHandler.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ErrorHandler — shared error-normalisation and logging utilities.
|
||||
*
|
||||
* Provides:
|
||||
* - userMessage($e) — turns raw exceptions into user-friendly French messages
|
||||
* - log($context, $e, array $extra) — structured error_log with trace
|
||||
*
|
||||
* Usage from any action/controller:
|
||||
* ErrorHandler::log('thesis_edit', $e, ['thesis_id' => $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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user