Recherche insensible aux accents sur toutes les barres de recherche (publique + backoffice).

Ajoute une fonction SQLite UDF accfold() (app/src/AccentFolding.php) qui plie les caractères accentués latin vers leur base ASCII (é→e, ç→c, …), et l'applique à chaque condition de recherche côté colonne ET côté terme (accfold(column) LIKE accfold(:term)). Comme le runtime n'a ni intl (Normalizer) ni iconv, le repli passe par une map de translitération manuelle (Western Latin-1 + Latin Extended-A, NULL-safe).

Couvert :
- recherche publique (searchTheses / countSearchResults) : titre, sous-titre, synopsis, auteurs, promoteurs, tags ;
- popover étudiant (getThesesByAuthorName / getThesesForAuthors) : a.name ;
- recherche backoffice (getThesesList / getThesesListCount) : titre, sous-titre, identifiant, auteur.

L'UDF est enregistré dans Database::registerSqliteFunctions() (appelé par le constructeur et par le harnais de test TestDatabaseInstance). Évite aussi la dépréciation PHP 8.5 de PDO::sqliteCreateFunction() : la connexion est créée via \Pdo\Sqlite quand disponible (createFunction()) avec repli sur PDO (sqliteCreateFunction()) sur les anciens runtimes ; les tests passent de @dataProvider docblock à l'attribut #[DataProvider] (fin de la dépréciation PHPUnit). Détache .php-cs-fixer.cache (gitignoré) pour ne plus polluer le working copy.

294 tests PHPUnit verts sans dépréciation, phpstan niveau 5 OK.
This commit is contained in:
Pontoporeia committed 2026-09-18 16:26:36 +02:00
1 parent 0896c4b8c8
commit a31e4ab307
7 files changed
+341 -32

No files matched your search

+91 -28
View File
@@ -20,9 +20,10 @@ class Database
$this->dbPath = $this->determineDatabasePath($dbPath);
try {
$this->pdo = new PDO('sqlite:' . $this->dbPath);
$this->pdo = self::newConnection($this->dbPath);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$this->registerSqliteFunctions($this->pdo);
// Enable foreign key constraints + performance pragmas
$this->pdo->exec('PRAGMA foreign_keys = ON');
@@ -36,6 +37,50 @@ class Database
}
}
/**
* Create a new SQLite PDO connection.
*
* Since PHP 8.5, `PDO::sqliteCreateFunction()` (used to register the
* `accfold` UDF) is deprecated in favour of the `Pdo\Sqlite::createFunction()`
* method. `Pdo\Sqlite` extends `PDO` and keeps the full query API, so we use
* it directly when available and fall back to a plain `PDO` otherwise
* (older runtimes rely on the named argument there).
*/
private static function newConnection(string $dbPath): PDO
{
// `Pdo\Sqlite` exists since PHP 8.5; on older versions class_exists() is false.
if (class_exists('Pdo\Sqlite')) {
return new Pdo\Sqlite('sqlite:' . $dbPath);
}
return new PDO('sqlite:' . $dbPath);
}
/**
* Register user-defined SQLite functions used by search queries.
*
* Currently exposes `accfold()` — accent folding — so SQL can match
* diacritic-insensitively (accfold(column) LIKE accfold(term)).
*
* Also called for the in-memory test connection (see TestDatabase).
*/
public static function registerSqliteFunctions(
PDO $pdo
): void {
require_once __DIR__ . '/AccentFolding.php';
$callback = static fn (?string $value): ?string => AccentFolding::fold($value);
// Since PHP 8.5, `PDO::sqliteCreateFunction()` is deprecated: use the
// `Pdo\Sqlite::createFunction()` method instead. `PDO::sqliteCreateFunction()`
// remains the non-deprecated API on older versions where Pdo\Sqlite doesn't exist.
if ($pdo instanceof \Pdo\Sqlite) {
$pdo->createFunction('accfold', $callback, 1);
} else {
$pdo->sqliteCreateFunction('accfold', $callback, 1);
}
}
/**
* Run one-off schema migrations.
*/
@@ -336,23 +381,31 @@ class Database
*/
private function buildSearchConditions(array $params): array
{
require_once __DIR__ . '/AccentFolding.php';
$conditions = ['vp.is_published = 1'];
$bindings = [];
// Fold diacritics off a validated (already LIKE-escaped) term so both
// sides of the comparison are accent-insensitive. Escape characters
// (\, %, _) introduced by escapeLikeString are untouched by folding.
$fold = static fn (string $term): string => AccentFolding::fold($term) ?? $term;
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
accfold(vp.title) LIKE :query ESCAPE '\\' OR
accfold(vp.subtitle) LIKE :query ESCAPE '\\' OR
accfold(vp.synopsis) LIKE :query ESCAPE '\\' OR
accfold(vp.authors) LIKE :query ESCAPE '\\' OR
accfold(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 '\\'
WHERE tt2.thesis_id = vp.id
AND accfold(tg2.name) LIKE :query ESCAPE '\\'
)
)";
$bindings[':query'] = '%' . $params['query'] . '%';
$bindings[':query'] = '%' . $fold($params['query']) . '%';
}
if (!empty($params['year'])) {
@@ -361,37 +414,38 @@ class Database
}
if (!empty($params['orientation'])) {
$conditions[] = "vp.orientation LIKE :orientation ESCAPE '\\'";
$bindings[':orientation'] = '%' . $params['orientation'] . '%';
$conditions[] = "accfold(vp.orientation) LIKE :orientation ESCAPE '\\'";
$bindings[':orientation'] = '%' . $fold($params['orientation']) . '%';
}
if (!empty($params['ap_program'])) {
$conditions[] = "vp.ap_program LIKE :ap_program ESCAPE '\\'";
$bindings[':ap_program'] = '%' . $params['ap_program'] . '%';
$conditions[] = "accfold(vp.ap_program) LIKE :ap_program ESCAPE '\\'";
$bindings[':ap_program'] = '%' . $fold($params['ap_program']) . '%';
}
if (!empty($params['finality'])) {
$conditions[] = "vp.finality_type LIKE :finality ESCAPE '\\'";
$bindings[':finality'] = '%' . $params['finality'] . '%';
$conditions[] = "accfold(vp.finality_type) LIKE :finality ESCAPE '\\'";
$bindings[':finality'] = '%' . $fold($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 '\\'
WHERE tt_kw.thesis_id = vp.id
AND accfold(tg_kw.name) LIKE :keyword ESCAPE '\\'
)";
$bindings[':keyword'] = '%' . $params['keyword'] . '%';
$bindings[':keyword'] = '%' . $fold($params['keyword']) . '%';
}
if (!empty($params['format'])) {
$conditions[] = "vp.formats LIKE :format ESCAPE '\\'";
$bindings[':format'] = '%' . $params['format'] . '%';
$conditions[] = "accfold(vp.formats) LIKE :format ESCAPE '\\'";
$bindings[':format'] = '%' . $fold($params['format']) . '%';
}
if (!empty($params['language'])) {
$conditions[] = "vp.languages LIKE :language ESCAPE '\\'";
$bindings[':language'] = '%' . $params['language'] . '%';
$conditions[] = "accfold(vp.languages) LIKE :language ESCAPE '\\'";
$bindings[':language'] = '%' . $fold($params['language']) . '%';
}
if (isset($params['is_doctoral'])) {
@@ -498,16 +552,18 @@ class Database
*/
public function getThesesByAuthorName(string $name): array
{
require_once __DIR__ . '/AccentFolding.php';
$stmt = $this->pdo->prepare(
'SELECT vp.id, vp.title, vp.subtitle, vp.year, vp.synopsis,
vp.orientation, vp.finality_type, vp.authors
FROM v_theses_public vp
JOIN thesis_authors ta ON ta.thesis_id = vp.id
JOIN authors a ON a.id = ta.author_id
WHERE a.name = ?
WHERE accfold(a.name) = accfold(?)
ORDER BY vp.year DESC, vp.title ASC'
);
$stmt->execute([$name]);
$stmt->execute([AccentFolding::fold($name)]);
return $stmt->fetchAll();
}
@@ -524,6 +580,9 @@ class Database
return [];
}
require_once __DIR__ . '/AccentFolding.php';
$folded = array_map(static fn (string $n) => AccentFolding::fold($n) ?? $n, $names);
$placeholders = implode(',', array_fill(0, count($names), '?'));
$stmt = $this->pdo->prepare(
"SELECT a.name AS author_name,
@@ -532,10 +591,10 @@ class Database
FROM v_theses_public vp
JOIN thesis_authors ta ON ta.thesis_id = vp.id
JOIN authors a ON a.id = ta.author_id
WHERE a.name IN ($placeholders)
WHERE accfold(a.name) IN ($placeholders)
ORDER BY a.name ASC, vp.year DESC, vp.title ASC"
);
$stmt->execute($names);
$stmt->execute($folded);
$rows = $stmt->fetchAll();
$grouped = [];
@@ -856,6 +915,8 @@ class Database
*/
public function getThesesListCount(array $filters = []): int
{
require_once __DIR__ . '/AccentFolding.php';
$sql = 'SELECT COUNT(DISTINCT t.id)
FROM theses t
LEFT JOIN orientations o ON t.orientation_id = o.id
@@ -867,8 +928,8 @@ class Database
$params = [];
if (!empty($filters['search'])) {
$sql .= ' AND (t.title LIKE ? OR t.subtitle LIKE ? OR t.identifier LIKE ? OR a.name LIKE ?)';
$searchParam = '%' . $filters['search'] . '%';
$sql .= ' AND (accfold(t.title) LIKE ? ESCAPE \'\\\' OR accfold(t.subtitle) LIKE ? ESCAPE \'\\\' OR accfold(t.identifier) LIKE ? ESCAPE \'\\\' OR accfold(a.name) LIKE ? ESCAPE \'\\\')';
$searchParam = '%' . AccentFolding::fold($filters['search']) . '%';
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
@@ -897,6 +958,8 @@ class Database
public function getThesesList(array $filters = [], int $limit = 0, int $offset = 0): array
{
require_once __DIR__ . '/AccentFolding.php';
$sql = 'SELECT
t.id, t.identifier, t.title, t.subtitle, t.year,
o.name as orientation,
@@ -916,8 +979,8 @@ class Database
$params = [];
if (!empty($filters['search'])) {
$sql .= ' AND (t.title LIKE ? OR t.subtitle LIKE ? OR t.identifier LIKE ? OR a.name LIKE ?)';
$searchParam = '%' . $filters['search'] . '%';
$sql .= ' AND (accfold(t.title) LIKE ? ESCAPE \'\\\' OR accfold(t.subtitle) LIKE ? ESCAPE \'\\\' OR accfold(t.identifier) LIKE ? ESCAPE \'\\\' OR accfold(a.name) LIKE ? ESCAPE \'\\\')';
$searchParam = '%' . AccentFolding::fold($filters['search']) . '%';
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;