Compatibilité MySQL + suppression de Redis comme dépendance requise
DbCompat (app/Support/DbCompat.php) : - like() → ilike (pgsql) ou like (mysql) - jsonRegexRaw() → data::text ~* ? (pgsql) ou CAST(data AS CHAR) REGEXP ? (mysql) - ftsRaw() → to_tsvector/plainto_tsquery (pgsql) ou null/fallback LIKE (mysql) - generatedJsonCol() → syntaxe colonne générée JSON selon le SGBD - generatedJsonNestedCol() → idem pour champs imbriqués Migrations : - create_releves_table : JSON/JSONB selon SGBD, colonnes générées adaptées, index GIN uniquement pour PostgreSQL Controllers : - LieuController (search + index) : ilike → DbCompat::like() - Admin\UserController (index) : ilike → DbCompat::like() - RechercheController : FTS + regex → DbCompat, fallback LIKE MySQL - ExportController : regex → DbCompat::jsonRegexRaw() UpdateService : - backupDatabase() : pg_dump (pgsql) ou mysqldump (mysql) - restoreBackup() : psql (pgsql) ou mysql (mysql) Docker : - docker-compose.yml : suppression Redis (plus requis) - docker-compose.mysql.yml : nouveau fichier pour dev MySQL - docker-compose.prod.yml : suppression Redis, DB_IMAGE configurable, CACHE_STORE/SESSION_DRIVER/QUEUE_CONNECTION → database par défaut .env.example : - DB_PORT commenté avec les deux valeurs (5432/3306) - CACHE_STORE et QUEUE_CONNECTION commentés (database par défaut) - Redis marqué optionnel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
|
||||
use App\Enums\UserRole;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Support\DbCompat;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rules\Enum;
|
||||
@@ -22,9 +23,10 @@ class UserController extends Controller
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$q = trim($request->get('q'));
|
||||
$like = DbCompat::like();
|
||||
$query->where(fn ($wq) => $wq
|
||||
->where('name', 'ilike', "%{$q}%")
|
||||
->orWhere('email', 'ilike', "%{$q}%")
|
||||
->where('name', $like, "%{$q}%")
|
||||
->orWhere('email', $like, "%{$q}%")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Enums\SourceStatus;
|
||||
use App\Models\Releve;
|
||||
use App\Models\Source;
|
||||
use App\Services\GedcomExportService;
|
||||
use App\Support\DbCompat;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -50,15 +51,17 @@ class ExportController extends Controller
|
||||
}); // $request déjà dans le use()
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$q = trim($request->get('q'));
|
||||
$query->where(function ($wq) use ($q) {
|
||||
$wq->where('nom', 'ilike', "%{$q}%")
|
||||
->orWhere('prenom','ilike', "%{$q}%")
|
||||
->orWhere('date_evenement', 'ilike', "%{$q}%")
|
||||
->orWhereRaw(
|
||||
"to_tsvector('french', data::text) @@ plainto_tsquery('french', ?)",
|
||||
[$q]
|
||||
);
|
||||
$q = trim($request->get('q'));
|
||||
$like = DbCompat::like();
|
||||
$fts = DbCompat::ftsRaw();
|
||||
|
||||
$query->where(function ($wq) use ($q, $like, $fts) {
|
||||
$wq->where('nom', $like, "%{$q}%")
|
||||
->orWhere('prenom', $like, "%{$q}%")
|
||||
->orWhere('date_evenement', $like, "%{$q}%");
|
||||
if ($fts) {
|
||||
$wq->orWhereRaw($fts, [$q]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,7 +79,7 @@ class ExportController extends Controller
|
||||
$noms = collect($rows)->pluck('nom')->filter();
|
||||
if ($noms->isNotEmpty()) {
|
||||
$pattern = $noms->map(fn ($n) => preg_quote($n, '/'))->join('|');
|
||||
$query->whereRaw("data::text ~* ?", [$pattern]);
|
||||
$query->whereRaw(DbCompat::jsonRegexRaw('data'), [$pattern]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Http\Requests\StoreLieuRequest;
|
||||
use App\Http\Requests\UpdateLieuRequest;
|
||||
use App\Models\Lieu;
|
||||
use App\Models\LieuType;
|
||||
use App\Support\DbCompat;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -20,9 +21,9 @@ class LieuController extends Controller
|
||||
|
||||
$lieux = Lieu::with('lieuType')
|
||||
->where(function ($query) use ($q) {
|
||||
$query->where('nom_long', 'ilike', "%{$q}%")
|
||||
->orWhere('nom', 'ilike', "%{$q}%")
|
||||
->orWhere('code', 'ilike', "%{$q}%");
|
||||
$query->where('nom_long', DbCompat::like(), "%{$q}%")
|
||||
->orWhere('nom', DbCompat::like(), "%{$q}%")
|
||||
->orWhere('code', DbCompat::like(), "%{$q}%");
|
||||
})
|
||||
->orderBy('nom_long')
|
||||
->limit(25)
|
||||
@@ -49,9 +50,9 @@ class LieuController extends Controller
|
||||
if ($request->filled('q')) {
|
||||
$q = trim($request->get('q'));
|
||||
$query->where(function ($wq) use ($q) {
|
||||
$wq->where('nom_long', 'ilike', "%{$q}%")
|
||||
->orWhere('nom', 'ilike', "%{$q}%")
|
||||
->orWhere('code', 'ilike', "%{$q}%");
|
||||
$wq->where('nom_long', DbCompat::like(), "%{$q}%")
|
||||
->orWhere('nom', DbCompat::like(), "%{$q}%")
|
||||
->orWhere('code', DbCompat::like(), "%{$q}%");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Enums\SourceStatus;
|
||||
use App\Models\Lieu;
|
||||
use App\Models\Releve;
|
||||
use App\Models\SourceType;
|
||||
use App\Support\DbCompat;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
@@ -50,15 +51,18 @@ class RechercheController extends Controller
|
||||
|
||||
// ── Recherche textuelle ──────────────────────────────────────────────
|
||||
if ($request->filled('q')) {
|
||||
$q = trim($request->get('q'));
|
||||
$query->where(function ($wq) use ($q) {
|
||||
$wq->where('nom', 'ilike', "%{$q}%")
|
||||
->orWhere('prenom','ilike', "%{$q}%")
|
||||
->orWhere('date_evenement', 'ilike', "%{$q}%")
|
||||
->orWhereRaw(
|
||||
"to_tsvector('french', data::text) @@ plainto_tsquery('french', ?)",
|
||||
[$q]
|
||||
);
|
||||
$q = trim($request->get('q'));
|
||||
$like = DbCompat::like();
|
||||
$fts = DbCompat::ftsRaw();
|
||||
|
||||
$query->where(function ($wq) use ($q, $like, $fts) {
|
||||
$wq->where('nom', $like, "%{$q}%")
|
||||
->orWhere('prenom', $like, "%{$q}%")
|
||||
->orWhere('date_evenement', $like, "%{$q}%");
|
||||
|
||||
if ($fts) {
|
||||
$wq->orWhereRaw($fts, [$q]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,11 +70,10 @@ class RechercheController extends Controller
|
||||
if ($request->filled('lieu_id')) {
|
||||
$lieuNoms = $this->getLieuNoms($request->integer('lieu_id'));
|
||||
if ($lieuNoms->isNotEmpty()) {
|
||||
// Recherche regex case-insensitive dans le JSONB text
|
||||
$pattern = $lieuNoms
|
||||
->map(fn ($n) => preg_quote($n, '/'))
|
||||
->join('|');
|
||||
$query->whereRaw("data::text ~* ?", [$pattern]);
|
||||
$query->whereRaw(DbCompat::jsonRegexRaw('data'), [$pattern]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -206,21 +206,30 @@ class UpdateService
|
||||
throw new RuntimeException("Fichier introuvable : {$path}");
|
||||
}
|
||||
|
||||
$host = config('database.connections.pgsql.host');
|
||||
$port = config('database.connections.pgsql.port', 5432);
|
||||
$db = config('database.connections.pgsql.database');
|
||||
$user = config('database.connections.pgsql.username');
|
||||
$password = config('database.connections.pgsql.password');
|
||||
$driver = config('database.default');
|
||||
$conn = config("database.connections.{$driver}");
|
||||
|
||||
$cmd = sprintf(
|
||||
'PGPASSWORD=%s psql -h %s -p %s -U %s %s < %s',
|
||||
escapeshellarg($password),
|
||||
escapeshellarg($host),
|
||||
(int) $port,
|
||||
escapeshellarg($user),
|
||||
escapeshellarg($db),
|
||||
escapeshellarg($path),
|
||||
);
|
||||
if ($driver === 'pgsql') {
|
||||
$cmd = sprintf(
|
||||
'PGPASSWORD=%s psql -h %s -p %s -U %s %s < %s',
|
||||
escapeshellarg($conn['password']),
|
||||
escapeshellarg($conn['host']),
|
||||
(int) ($conn['port'] ?? 5432),
|
||||
escapeshellarg($conn['username']),
|
||||
escapeshellarg($conn['database']),
|
||||
escapeshellarg($path),
|
||||
);
|
||||
} else {
|
||||
$cmd = sprintf(
|
||||
'mysql -h %s -P %s -u %s %s %s < %s',
|
||||
escapeshellarg($conn['host']),
|
||||
(int) ($conn['port'] ?? 3306),
|
||||
escapeshellarg($conn['username']),
|
||||
$conn['password'] ? '-p' . escapeshellarg($conn['password']) : '',
|
||||
escapeshellarg($conn['database']),
|
||||
escapeshellarg($path),
|
||||
);
|
||||
}
|
||||
|
||||
Artisan::call('down');
|
||||
$this->exec($cmd);
|
||||
@@ -238,27 +247,35 @@ class UpdateService
|
||||
|
||||
$filename = "db-before-{$version}-" . date('Ymd-His') . ".sql";
|
||||
$path = "{$dir}/{$filename}";
|
||||
$driver = config('database.default');
|
||||
$conn = config("database.connections.{$driver}");
|
||||
|
||||
$host = config('database.connections.pgsql.host');
|
||||
$port = config('database.connections.pgsql.port', 5432);
|
||||
$db = config('database.connections.pgsql.database');
|
||||
$user = config('database.connections.pgsql.username');
|
||||
$password = config('database.connections.pgsql.password');
|
||||
|
||||
$cmd = sprintf(
|
||||
'PGPASSWORD=%s pg_dump -h %s -p %s -U %s %s > %s',
|
||||
escapeshellarg($password),
|
||||
escapeshellarg($host),
|
||||
(int) $port,
|
||||
escapeshellarg($user),
|
||||
escapeshellarg($db),
|
||||
escapeshellarg($path),
|
||||
);
|
||||
if ($driver === 'pgsql') {
|
||||
$cmd = sprintf(
|
||||
'PGPASSWORD=%s pg_dump -h %s -p %s -U %s %s > %s',
|
||||
escapeshellarg($conn['password']),
|
||||
escapeshellarg($conn['host']),
|
||||
(int) ($conn['port'] ?? 5432),
|
||||
escapeshellarg($conn['username']),
|
||||
escapeshellarg($conn['database']),
|
||||
escapeshellarg($path),
|
||||
);
|
||||
} else {
|
||||
$cmd = sprintf(
|
||||
'mysqldump -h %s -P %s -u %s %s %s > %s',
|
||||
escapeshellarg($conn['host']),
|
||||
(int) ($conn['port'] ?? 3306),
|
||||
escapeshellarg($conn['username']),
|
||||
$conn['password'] ? '-p' . escapeshellarg($conn['password']) : '',
|
||||
escapeshellarg($conn['database']),
|
||||
escapeshellarg($path),
|
||||
);
|
||||
}
|
||||
|
||||
$this->exec($cmd);
|
||||
|
||||
if (! file_exists($path) || filesize($path) === 0) {
|
||||
throw new RuntimeException("La sauvegarde PostgreSQL a échoué (pg_dump disponible dans le container ?)");
|
||||
throw new RuntimeException("La sauvegarde de la base de données a échoué ({$driver}).");
|
||||
}
|
||||
|
||||
return $path;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Helpers pour les différences de syntaxe SQL entre PostgreSQL et MySQL.
|
||||
*/
|
||||
class DbCompat
|
||||
{
|
||||
public static function isPgsql(): bool
|
||||
{
|
||||
return DB::connection()->getDriverName() === 'pgsql';
|
||||
}
|
||||
|
||||
/** Opérateur LIKE insensible à la casse */
|
||||
public static function like(): string
|
||||
{
|
||||
return self::isPgsql() ? 'ilike' : 'like';
|
||||
}
|
||||
|
||||
/** Caste une colonne JSON en texte brut */
|
||||
public static function jsonToText(string $column): string
|
||||
{
|
||||
return self::isPgsql()
|
||||
? "{$column}::text"
|
||||
: "CAST({$column} AS CHAR)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition de correspondance regex (insensible à la casse) sur une colonne JSON.
|
||||
* Retourne la chaîne à passer à whereRaw() avec un seul binding `?`.
|
||||
*/
|
||||
public static function jsonRegexRaw(string $column = 'data'): string
|
||||
{
|
||||
return self::isPgsql()
|
||||
? "{$column}::text ~* ?"
|
||||
: "CAST({$column} AS CHAR) REGEXP ?";
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition de recherche plein-texte.
|
||||
* Retourne null pour MySQL (le caller doit prévoir un fallback LIKE).
|
||||
*/
|
||||
public static function ftsRaw(): ?string
|
||||
{
|
||||
return self::isPgsql()
|
||||
? "to_tsvector('french', data::text) @@ plainto_tsquery('french', ?)"
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Syntaxe de la colonne générée stockée pour extraire un champ JSON de premier niveau */
|
||||
public static function generatedJsonCol(string $jsonKey): string
|
||||
{
|
||||
return self::isPgsql()
|
||||
? "data->>'$jsonKey'"
|
||||
: "JSON_UNQUOTE(JSON_EXTRACT(data, '$.$jsonKey'))";
|
||||
}
|
||||
|
||||
/** Syntaxe de la colonne générée stockée pour un champ JSON imbriqué (ex: date_evenement.valeur) */
|
||||
public static function generatedJsonNestedCol(string $jsonPath): string
|
||||
{
|
||||
if (self::isPgsql()) {
|
||||
// data->'date_evenement'->>'valeur'
|
||||
$parts = explode('.', $jsonPath);
|
||||
$last = array_pop($parts);
|
||||
$chain = implode('', array_map(fn ($p) => "->'{$p}'", $parts));
|
||||
return "data{$chain}->>'{$last}'";
|
||||
}
|
||||
// MySQL: $.date_evenement.valeur
|
||||
return "JSON_UNQUOTE(JSON_EXTRACT(data, '$.{$jsonPath}'))";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user