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:
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