From 24c432f325971c5abee062fbeedb488d611dae99 Mon Sep 17 00:00:00 2001 From: Erwann PHILIPPE Date: Wed, 16 Sep 2026 11:31:01 +0200 Subject: [PATCH] Support de la recherche par immatriculation --- index.php | 208 ++++++++++++++++++++++++++++++-------------- voituresapi (1).sql | 16 ++-- 2 files changed, 150 insertions(+), 74 deletions(-) diff --git a/index.php b/index.php index 52ab652..17a61fe 100644 --- a/index.php +++ b/index.php @@ -48,40 +48,67 @@ function getBody(): array */ function getPathSegments(): array { - // Si passé en paramètre d'URL direct (ex: ?route=/cars) if (!empty($_GET['route'])) { $path = trim($_GET['route'], '/'); return $path === '' ? [] : explode('/', $path); } - // Si PATH_INFO est fourni (ex: index.php/cars) if (!empty($_SERVER['PATH_INFO'])) { $path = trim($_SERVER['PATH_INFO'], '/'); return $path === '' ? [] : explode('/', $path); } - // Analyse depuis REQUEST_URI $uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH); $scriptDir = dirname($_SERVER['SCRIPT_NAME'] ?? ''); - // Nettoie le préfixe du dossier du script si présent if ($scriptDir !== '/' && $scriptDir !== '\\' && strpos($uri, $scriptDir) === 0) { $uri = substr($uri, strlen($scriptDir)); } - // Supprime "index.php" si présent $uri = preg_replace('#^/?index\.php#i', '', $uri); $path = trim($uri, '/'); return $path === '' ? [] : explode('/', $path); } +/** + * Recherche d'une voiture par sa plaque d'immatriculation (exacte ou normalisée) + */ +function findCarByPlate(PDO $db, string $plaque): void +{ + $clean = strtoupper(trim(urldecode($plaque))); + $norm = str_replace(['-', ' ', '.', '_'], '', $clean); + + $stmt = $db->prepare(' + SELECT c.*, + (SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km + FROM cars c + WHERE UPPER(c.immatriculation) = :raw + OR REPLACE(REPLACE(REPLACE(REPLACE(UPPER(c.immatriculation), "-", ""), " ", ""), ".", ""), "_", "") = :norm + LIMIT 1 + '); + $stmt->execute([':raw' => $clean, ':norm' => $norm]); + $car = $stmt->fetch(); + + if (!$car) { + sendJson(404, [ + 'success' => false, + 'error' => "Aucune voiture trouvée avec l'immatriculation '$clean'" + ]); + } + + sendJson(200, [ + 'success' => true, + 'data' => $car + ]); +} + try { $db = getDB(); $method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET'); $segments = getPathSegments(); - // Route racine: Documentation sommaire des points d'accès + // Route racine: Documentation des points d'accès if (empty($segments)) { sendJson(200, [ 'success' => true, @@ -89,29 +116,30 @@ try { 'database' => DB_NAME, 'endpoints' => [ 'Voitures' => [ - 'GET /cars' => 'Liste toutes les voitures', - 'GET /cars/{id}' => 'Détails d\'une voiture', - 'GET /cars/{id}/etat' => 'État complet (voiture, dernier km, entretiens, notes)', - 'POST /cars' => 'Créer une voiture (marque, modele, annee, dateAchat, VIN)', - 'PUT /cars/{id}' => 'Modifier les paramètres d\'une voiture', - 'DELETE /cars/{id}' => 'Supprimer une voiture (et ses données associées)' + 'GET /cars' => 'Liste toutes les voitures (filtrable par ?immatriculation=...)', + 'GET /cars/{id}' => 'Détails d\'une voiture par ID', + 'GET /cars/immatriculation/{plaque}' => 'Identifier une voiture par sa plaque d\'immatriculation', + 'GET /cars/{id}/etat' => 'État complet (voiture, dernier km, entretiens, notes)', + 'POST /cars' => 'Créer une voiture (marque, modele, annee, dateAchat, VIN, immatriculation)', + 'PUT /cars/{id}' => 'Modifier les paramètres d\'une voiture (y compris immatriculation)', + 'DELETE /cars/{id}' => 'Supprimer une voiture (et ses données associées)' ], 'Kilométrage' => [ - 'GET /cars/{id}/kilometrage' => 'Historique des relevés kilométriques', - 'POST /cars/{id}/kilometrage' => 'Ajouter un relevé kilométrique (valeur, date_releve)', - 'DELETE /kilometrage/{id}' => 'Supprimer un relevé kilométrique' + 'GET /cars/{id}/kilometrage' => 'Historique des relevés kilométriques', + 'POST /cars/{id}/kilometrage' => 'Ajouter un relevé kilométrique (valeur, date_releve)', + 'DELETE /kilometrage/{id}' => 'Supprimer un relevé kilométrique' ], - 'Entretiens (Maintenance)' => [ - 'GET /cars/{id}/maintenance' => 'Historique des entretiens d\'une voiture', - 'POST /cars/{id}/maintenance' => 'Ajouter un entretien (type_entretien, date_evenement, kilometrage, description, prix)', - 'PUT /maintenance/{id}' => 'Modifier un entretien', - 'DELETE /maintenance/{id}' => 'Supprimer un entretien' + 'Entretiens' => [ + 'GET /cars/{id}/maintenance' => 'Historique des entretiens d\'une voiture', + 'POST /cars/{id}/maintenance' => 'Ajouter un entretien (type_entretien, date_evenement, kilometrage, description, prix)', + 'PUT /maintenance/{id}' => 'Modifier un entretien', + 'DELETE /maintenance/{id}' => 'Supprimer un entretien' ], 'Notes' => [ - 'GET /cars/{id}/notes' => 'Notes associées à une voiture', - 'POST /cars/{id}/notes' => 'Ajouter une note (titre, contenu)', - 'PUT /notes/{id}' => 'Modifier une note', - 'DELETE /notes/{id}' => 'Supprimer une note' + 'GET /cars/{id}/notes' => 'Notes associées à une voiture', + 'POST /cars/{id}/notes' => 'Ajouter une note (titre, contenu)', + 'PUT /notes/{id}' => 'Modifier une note', + 'DELETE /notes/{id}' => 'Supprimer une note' ] ] ]); @@ -119,16 +147,34 @@ try { $resource = strtolower($segments[0]); + // ========================================================================= + // ENDPOINT DIRECT: /immatriculation/{plaque} + // ========================================================================= + if ($resource === 'immatriculation' && $method === 'GET') { + $plaque = $segments[1] ?? null; + if (empty($plaque)) { + sendJson(400, ['success' => false, 'error' => 'Numéro d\'immatriculation manquant dans l\'URL']); + } + findCarByPlate($db, $plaque); + } + // ========================================================================= // RESSOURCE: CARS (/cars ...) // ========================================================================= if ($resource === 'cars' || $resource === 'voitures') { + // NOUVEAU ENDPOINT: GET /cars/immatriculation/{plaque} + if (isset($segments[1]) && strtolower($segments[1]) === 'immatriculation' && $method === 'GET') { + $plaque = $segments[2] ?? null; + if (empty($plaque)) { + sendJson(400, ['success' => false, 'error' => 'Numéro d\'immatriculation manquant dans l\'URL']); + } + findCarByPlate($db, $plaque); + } + $carId = isset($segments[1]) && is_numeric($segments[1]) ? (int)$segments[1] : null; $subResource = $segments[2] ?? null; - // --- SOUS-RESSOURCES DE CARS --- - - // 1. /cars/{id}/etat (État complet via la procédure sp_getEtatVoiture) + // 1. GET /cars/{id}/etat (État complet via la procédure sp_getEtatVoiture) if ($carId && $subResource === 'etat' && $method === 'GET') { try { $stmt = $db->prepare('CALL sp_getEtatVoiture(:id)'); @@ -153,13 +199,12 @@ try { sendJson(200, [ 'success' => true, 'data' => [ - 'voiture' => $car, - 'maintenances' => $maintenances, - 'notes' => $notes + 'voiture' => $car, + 'maintenances' => $maintenances, + 'notes' => $notes ] ]); } catch (Exception $e) { - // Fallback si la procédure stockée a un souci $stmtCar = $db->prepare(' SELECT c.*, (SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km @@ -312,10 +357,14 @@ try { sendJson(405, ['success' => false, 'error' => 'Méthode non autorisée pour cette sous-ressource']); } - // --- OPÉRATIONS PRINCIPALES SUR LES VOITURES (/cars ou /cars/{id}) --- + // --- ACTIONS PRINCIPALES SUR LES VOITURES --- + + // GET /cars : Liste toutes les voitures (ou filtre ?immatriculation=...) + if ($carId === null && $subResource === null && $method === 'GET') { + if (!empty($_GET['immatriculation'])) { + findCarByPlate($db, (string)$_GET['immatriculation']); + } - // GET /cars : Liste toutes les voitures - if ($carId === null && $method === 'GET') { $sql = 'SELECT c.*, (SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km FROM cars c @@ -344,7 +393,6 @@ try { if ($carId === null && $method === 'POST') { $body = getBody(); - // Validation des champs obligatoires $required = ['marque', 'modele', 'annee', 'dateAchat']; foreach ($required as $field) { if (empty($body[$field])) { @@ -352,39 +400,56 @@ try { } } - $marque = trim((string)$body['marque']); - $modele = trim((string)$body['modele']); - $annee = (int)$body['annee']; - $dateAchat = (string)$body['dateAchat']; - $vin = !empty($body['VIN']) ? trim((string)$body['VIN']) : null; + $marque = trim((string)$body['marque']); + $modele = trim((string)$body['modele']); + $annee = (int)$body['annee']; + $dateAchat = (string)$body['dateAchat']; + $vin = !empty($body['VIN']) ? trim((string)$body['VIN']) : null; + $immatriculation = !empty($body['immatriculation']) ? strtoupper(trim((string)$body['immatriculation'])) : null; - // Utilisation de la procédure stockée sp_insertVoiture + // Vérifier unicité de l'immatriculation si renseignée + if ($immatriculation !== null) { + $normImmat = str_replace(['-', ' ', '.', '_'], '', $immatriculation); + $stmtCheck = $db->prepare(' + SELECT id FROM cars + WHERE UPPER(immatriculation) = :raw + OR REPLACE(REPLACE(REPLACE(REPLACE(UPPER(immatriculation), "-", ""), " ", ""), ".", ""), "_", "") = :norm + '); + $stmtCheck->execute([':raw' => $immatriculation, ':norm' => $normImmat]); + if ($stmtCheck->fetch()) { + sendJson(409, [ + 'success' => false, + 'error' => "L'immatriculation '$immatriculation' est déjà utilisée pour un autre véhicule." + ]); + } + } + + // Appel de la procédure stockée sp_insertVoiture try { - $stmt = $db->prepare('CALL sp_insertVoiture(:marque, :modele, :annee, :dateAchat, :vin)'); + $stmt = $db->prepare('CALL sp_insertVoiture(:marque, :modele, :annee, :dateAchat, :vin, :immatriculation)'); $stmt->execute([ - ':marque' => $marque, - ':modele' => $modele, - ':annee' => $annee, - ':dateAchat' => $dateAchat, - ':vin' => $vin + ':marque' => $marque, + ':modele' => $modele, + ':annee' => $annee, + ':dateAchat' => $dateAchat, + ':vin' => $vin, + ':immatriculation' => $immatriculation ]); $res = $stmt->fetch(); $stmt->closeCursor(); $newId = isset($res['new_id']) ? (int)$res['new_id'] : (int)$db->lastInsertId(); } catch (Exception $e) { - // Fallback direct INSERT si la procédure a une incompatibilité - $stmt = $db->prepare('INSERT INTO cars (marque, modele, annee, dateAchat, VIN) VALUES (?, ?, ?, ?, ?)'); - $stmt->execute([$marque, $modele, $annee, $dateAchat, $vin]); + $stmt = $db->prepare('INSERT INTO cars (marque, modele, annee, dateAchat, VIN, immatriculation) VALUES (?, ?, ?, ?, ?, ?)'); + $stmt->execute([$marque, $modele, $annee, $dateAchat, $vin, $immatriculation]); $newId = (int)$db->lastInsertId(); } - // Si un kilométrage initial est fourni + // Kilométrage initial optionnel if (!empty($body['kilometrage_initial'])) { $kmStmt = $db->prepare('INSERT INTO kilometrage (car_id, date_releve, valeur) VALUES (?, ?, ?)'); $kmStmt->execute([$newId, $dateAchat, (int)$body['kilometrage_initial']]); } - // Récupère l'enregistrement créé $stmtGet = $db->prepare(' SELECT c.*, (SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km @@ -402,7 +467,6 @@ try { // PUT / PATCH /cars/{id} : Mettre à jour les paramètres d'une voiture if ($carId !== null && $subResource === null && ($method === 'PUT' || $method === 'PATCH')) { - // Vérifier si la voiture existe $check = $db->prepare('SELECT id FROM cars WHERE id = ?'); $check->execute([$carId]); if (!$check->fetch()) { @@ -410,10 +474,30 @@ try { } $body = getBody(); - $allowedFields = ['marque', 'modele', 'annee', 'dateAchat', 'VIN']; + $allowedFields = ['marque', 'modele', 'annee', 'dateAchat', 'VIN', 'immatriculation']; $updates = []; $params = []; + // Si immatriculation modifiée, vérifier unicité + if (!empty($body['immatriculation'])) { + $immat = strtoupper(trim((string)$body['immatriculation'])); + $body['immatriculation'] = $immat; + $normImmat = str_replace(['-', ' ', '.', '_'], '', $immat); + $stmtCheck = $db->prepare(' + SELECT id FROM cars + WHERE (UPPER(immatriculation) = :raw + OR REPLACE(REPLACE(REPLACE(REPLACE(UPPER(immatriculation), "-", ""), " ", ""), ".", ""), "_", "") = :norm) + AND id != :id + '); + $stmtCheck->execute([':raw' => $immat, ':norm' => $normImmat, ':id' => $carId]); + if ($stmtCheck->fetch()) { + sendJson(409, [ + 'success' => false, + 'error' => "L'immatriculation '$immat' est déjà utilisée pour un autre véhicule." + ]); + } + } + foreach ($allowedFields as $field) { if (array_key_exists($field, $body)) { $updates[] = "`$field` = ?"; @@ -430,7 +514,6 @@ try { $stmt = $db->prepare($sql); $stmt->execute($params); - // Récupère la voiture mise à jour $stmtGet = $db->prepare(' SELECT c.*, (SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km @@ -448,14 +531,12 @@ try { // DELETE /cars/{id} : Supprimer une voiture if ($carId !== null && $subResource === null && $method === 'DELETE') { - // Vérifier existence $check = $db->prepare('SELECT id FROM cars WHERE id = ?'); $check->execute([$carId]); if (!$check->fetch()) { sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]); } - // Exécution de la procédure stockée sp_deleteVoiture try { $stmt = $db->prepare('CALL sp_deleteVoiture(:id)'); $stmt->execute([':id' => $carId]); @@ -493,7 +574,7 @@ try { } // ========================================================================= - // RESSOURCE: MAINTENANCE DIRECT (/maintenance/{id}) + // RESSOURCE: ENTRETIENS DIRECT (/maintenance/{id}) // ========================================================================= if ($resource === 'maintenance' || $resource === 'entretiens') { $maintId = isset($segments[1]) && is_numeric($segments[1]) ? (int)$segments[1] : null; @@ -616,20 +697,13 @@ try { sendJson(405, ['success' => false, 'error' => "Méthode non autorisée sur /notes/{id}"]); } - // Ressource inconnue sendJson(404, [ 'success' => false, 'error' => "Ressource '/$resource' non reconnue. Consultez la racine '/' pour la documentation des routes." ]); } catch (PDOException $e) { - sendJson(500, [ - 'success' => false, - 'error' => 'Erreur SQL: ' . $e->getMessage() - ]); + sendJson(500, ['success' => false, 'error' => 'Erreur SQL: ' . $e->getMessage()]); } catch (Throwable $e) { - sendJson(500, [ - 'success' => false, - 'error' => 'Erreur serveur: ' . $e->getMessage() - ]); + sendJson(500, ['success' => false, 'error' => 'Erreur serveur: ' . $e->getMessage()]); } diff --git a/voituresapi (1).sql b/voituresapi (1).sql index 891545f..caf2572 100644 --- a/voituresapi (1).sql +++ b/voituresapi (1).sql @@ -53,9 +53,9 @@ CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_getKilometrages` (IN `p_id` INT, END$$ DROP PROCEDURE IF EXISTS `sp_insertVoiture`$$ -CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_insertVoiture` (IN `p_marque` VARCHAR(255), IN `p_modele` VARCHAR(255), IN `p_annee` INT, IN `p_dateAchat` DATE, IN `p_VIN` VARCHAR(255)) BEGIN - INSERT INTO cars (marque, modele, annee, dateAchat, VIN) - VALUES (p_marque, p_modele, p_annee, p_dateAchat, p_VIN); +CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_insertVoiture` (IN `p_marque` VARCHAR(255), IN `p_modele` VARCHAR(255), IN `p_annee` INT, IN `p_dateAchat` DATE, IN `p_VIN` VARCHAR(255), IN `p_immatriculation` VARCHAR(20)) BEGIN + INSERT INTO cars (marque, modele, annee, dateAchat, VIN, immatriculation) + VALUES (p_marque, p_modele, p_annee, p_dateAchat, p_VIN, p_immatriculation); SELECT LAST_INSERT_ID() AS new_id; END$$ @@ -75,16 +75,18 @@ CREATE TABLE IF NOT EXISTS `cars` ( `annee` int NOT NULL, `dateAchat` date NOT NULL, `VIN` varchar(17) DEFAULT NULL, - PRIMARY KEY (`id`) + `immatriculation` varchar(20) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_immatriculation` (`immatriculation`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- -- Déchargement des données de la table `cars` -- -INSERT INTO `cars` (`id`, `marque`, `modele`, `annee`, `dateAchat`, `VIN`) VALUES -(1, 'Peugeot', '207', 2009, '2025-05-20', 'VF3WA8HZCXXXXXXXX'), -(2, 'Peugeot', '208', 2010, '2025-05-20', 'VF3WA8HZCXXXXXXXX'); +INSERT INTO `cars` (`id`, `marque`, `modele`, `annee`, `dateAchat`, `VIN`, `immatriculation`) VALUES +(1, 'Peugeot', '207', 2009, '2025-05-20', 'VF3WA8HZCXXXXXXXX', 'AB-123-CD'), +(2, 'Peugeot', '208', 2010, '2025-05-20', 'VF3WA8HZCXXXXXXXX', 'EF-456-GH'); -- --------------------------------------------------------