From 1b4a69d423d64212846928802b00732fd2321373 Mon Sep 17 00:00:00 2001 From: bfoujols Date: Mon, 5 Oct 2026 16:31:08 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20mettre=20=C3=A0=20jour=20la=20v?= =?UTF-8?q?=C3=A9rification=20de=20version=20PHP=20=C3=A0=208.4=20et=20aju?= =?UTF-8?q?ster=20les=20messages=20d'erreur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/index.php b/public/index.php index 5b02f72..aa3ae83 100644 --- a/public/index.php +++ b/public/index.php @@ -20,10 +20,10 @@ // Démarrage de la session PHP pour la gestion des variables de session session_start(); -// Masquer les deprecations de PHP 8.1 +// Masquer les deprecations de PHP 8.4 error_reporting(E_ALL & ~E_DEPRECATED); -if (version_compare(PHP_VERSION, '8.1', '<') === false) { +if (version_compare(PHP_VERSION, '8.4', '<') === false) { // Autoloader => chargement automatique des classes depuis le dossier vendor/ require_once __DIR__ . '/../vendor/autoload.php'; @@ -42,6 +42,6 @@ // Chargement des classes utilisées par l'application (new LoadCouchCore())->run(); } else { - printf("Cet app nécessite au moins PHP8.1."); + printf("Cet app nécessite au moins PHP8.4."); printf(" Veuillez mettre à jour votre version de PHP.\n"); } From 1eca6b8d80c3f227025deefdeb61a634a9cb13ea Mon Sep 17 00:00:00 2001 From: bfoujols Date: Mon, 5 Oct 2026 21:07:08 +0200 Subject: [PATCH 2/2] Add documentation for building APIs and CRUD operations - Updated index.md to include sections for CRUD and API construction. - Added new documentation file controller-edu-api.md detailing how to create a JSON API using the make:api command. - Enhanced command-edu.md with a reference to the new API documentation. --- docs/_summary.md | 3 +- docs/build/controller-edu-api.md | 652 +++++++++++++++++++++++++++++++ docs/build/dataservice-select.md | 601 +++++++++++++++++++++++++++- docs/build/index.md | 14 +- docs/installation/command-edu.md | 4 + 5 files changed, 1261 insertions(+), 13 deletions(-) create mode 100644 docs/build/controller-edu-api.md diff --git a/docs/_summary.md b/docs/_summary.md index fbbf065..bba50ed 100644 --- a/docs/_summary.md +++ b/docs/_summary.md @@ -12,7 +12,8 @@ * [Arborescence](build/arborescence.md) * [Construire une page](build/controller-edu.md) * [Construire un formulaire](build/controller-edu-post.md) - * [Gestion des données](build/dataservice-select.md) + * [Construire une API](build/controller-edu-api.md) + * [Construire le CRUD](build/dataservice-select.md) * [Construire une commande CLI](build/new-command-edu.md) * [Consolider](boost/index.md) * [Request - La gestion des requêtes HTTP](boost/resquet.md) diff --git a/docs/build/controller-edu-api.md b/docs/build/controller-edu-api.md new file mode 100644 index 0000000..8772d48 --- /dev/null +++ b/docs/build/controller-edu-api.md @@ -0,0 +1,652 @@ +# Comment construire une API ? + +!!! warning "Attention" + + Attention : cette feature est disponible à partir de v2.0 + +Une [API](../annexes/glossaire.md#api){:target="_blank"} permet à d'autres logiciels (application mobile, site web tiers, script...) de consommer les données de votre application. +Là où un controller classique renvoie une page HTML, un controller API renvoie des données au format [JSON](../annexes/glossaire.md#json){:target="_blank"}. + +La commande `make:api` reprend le principe de [`make:controller`](controller-edu.md) et génère tout le nécessaire pour exposer une API : + +| | `make:controller` | `make:api` | +|-----------------------|---------------------------------------|-----------------------------------------| +| Réponse renvoyée | HTML (vue Twig) | JSON | +| Dossier du controller | `app/Controller/` | `app/Controller/api/` | +| URI générée | `/` | `/api/` | +| Vue générée | `app/Template//.html.twig` | Aucune (pas de vue) | +| Documentation | Aucune | Attributs [OpenAPI](https://swagger.io/specification/){:target="_blank"} générés | + +## La commande make:api + +Vous pouvez l'utiliser à la racine de votre projet en tapant la commande suivante : + +```Shell +php bin/edu make:api NOM_DU_CONTROLLER +``` + +N'oubliez pas de remplacer **"NOM_DU_CONTROLLER"** par le nom de votre controller. L'argument `controller-name` est obligatoire. + +La commande génère **trois éléments** : + +| Élément généré | Emplacement | Détail | +|-----------------------|-----------------------------------------------|--------------------------------------------------------------------------------------------| +| Le controller API | `app/Controller/api/Controller.php` | Renvoie une réponse au format JSON | +| La route | `app/Config/routes.yaml` | Nom préfixé par `api_`, URI préfixée par `/api/`, méthode HTTP `GET` | +| Le fichier OpenAPI | `app/Controller/api/openapi.php` | Généré **uniquement lors du premier** `make:api` (non écrasé ensuite) | + +## Les règles de nommage + +Le nom passé en argument détermine le nom de la classe, le nom de la route et l'URI : + +| Argument saisi | Classe générée | Nom de la route | URI générée | +|-----------------|------------------------|----------------------|--------------------------| +| `hello` | `HelloController` | `api_hello` | `/api/hello` | +| `ville` | `VilleController` | `api_ville` | `/api/ville` | +| `MyUserTest` | `MyUserTestController` | `api_my-user-test` | `/api/my-user-test` | + +Les règles appliquées par la commande : + +- Le nom de la **classe** : première lettre du nom en majuscule + le suffixe `Controller` +- L'**URI** : nom en minuscules, chaque majuscule devient un tiret (format *kebab-case*), avec le préfixe `/api/` +- Le nom de la **route** : `api_` + l'URI sans le `/` + +!!! warning "Attention" + + Évitez le camelCase qui commence par une minuscule. La commande `php bin/edu make:api myUserTest` génère bien la classe `MyUserTestController`, mais l'URI produite sera `/api/user-test` (le premier mot est ignoré dans l'URI). + Préférez un nom tout en minuscules (`myusertest`) ou en PascalCase (`MyUserTest`). + +# Exemple de création d'une API + +Nous allons créer une API "hello" avec la commande suivante : + +```Shell +php bin/edu make:api Hello +``` + +Voici le résultat de la commande : + +``` + ============================================== + + Class : ./app/Controller/api/HelloController + URI : /api/hello + + ============================================== + + [OK] Controller and route successfully generated +``` + +Voici l'arborescence des fichiers générés ou modifiés : + +``` hl_lines="3 5 6 7" +├── app +│ ├── Config +│ │ └── routes.yaml +│ ├── Controller +│ │ └── api +│ │ ├── HelloController.php +│ │ └── openapi.php +``` + +### Le fichier HelloController.php + +Cette commande va créer un fichier "HelloController.php" dans le dossier "app/Controller/api". + +```php + ["nom" => "Yohaio", "prenom" => "Benoit"], + 1 => ["nom" => "Toma", "prenom" => "Yann"] + ]; + + return json_encode($listTest); + } +} +``` + +Plusieurs points sont à observer dans ce controller : + +- Le **namespace** est `Controller\api` : les controllers d'API sont séparés des controllers "web" (namespace `Controller`). +- Comme tout controller, il **implémente l'interface `ControllerInterface`** et sa méthode `execute()` prend en paramètre un objet [Request](../boost/resquet.md) et retourne une chaîne de caractères (string) ou null. +- La fonction native `header('Content-Type: application/json')` indique au client que la réponse est au format JSON. +- La fonction native `json_encode()` transforme un tableau PHP en chaîne JSON. +- Il n'y a **pas de vue Twig** : la chaîne JSON renvoyée par `json_encode()` est directement la réponse. +- Les **attributs OpenAPI** (`#[Attributes\Get(...)]` et `#[Attributes\Response(...)]`) décrivent l'API pour sa documentation. Ils sont détaillés dans la section [Documenter son API avec OpenAPI](#documenter-son-api-avec-openapi). + +### Le fichier openapi.php + +Le fichier "openapi.php" est créé dans le dossier "app/Controller/api" **seulement lors du premier** `make:api`. +Si le fichier existe déjà, la commande ne le régénère pas. + +```php + ["nom" => "Yohaio", "prenom" => "Benoit"], +- 1 => ["nom" => "Toma", "prenom" => "Yann"] +- ]; +- +- return json_encode($listTest); ++ $comBase = DatabaseService::getConnect(); ++ $statementPDO = $comBase->query("SELECT * FROM ville"); ++ $villes = $statementPDO->fetchAll(PDO::FETCH_ASSOC); ++ ++ return json_encode($villes); + } +} +``` + +Testez votre API : + +```Shell +curl -i http://localhost:8042/api/ville +``` + +Résultat : + +``` +HTTP/1.1 200 OK +Content-Type: application/json + +[{"id":1,"nom":"Paris","code_postal":"75000","nombre_habitant":2200000},{"id":2,"nom":"Marseille","code_postal":"13000","nombre_habitant":800000},{"id":3,"nom":"Lyon","code_postal":"69000","nombre_habitant":500000}] +``` + +!!! info "Pourquoi `PDO::FETCH_ASSOC` ?" + + Par défaut, la méthode `fetchAll()` renvoie chaque ligne avec **à la fois** les clés numériques (`0`, `1`...) et les clés associatives (`id`, `nom`...) : le JSON serait dupliqué. + `PDO::FETCH_ASSOC` ne garde que les clés associatives, ce qui produit un JSON propre et lisible. + +!!! warning "L'import `use PDO;` est obligatoire" + + Le controller est dans le namespace `Controller\api`. Contrairement aux fonctions, les classes PHP **ne retombent pas** automatiquement dans l'espace global : sans l'import `use PDO;`, l'utilisation de `PDO::FETCH_ASSOC` produirait l'erreur `Class "Controller\api\PDO" not found`. + +## Exemple à réaliser n°2 : un paramètre dans l'URI + +!!! info "Objectif" + + - Créer une API qui renvoie **une seule ville** à partir de son identifiant : `GET /api/ville/{id}` + - Récupérer un paramètre de route avec la classe [Request](../boost/resquet.md) + - Documenter le paramètre avec l'attribut OpenAPI `Parameter` + +En REST, il est d'usage d'exposer une ressource unique sous la forme `/api/ville/{id}`. +Nous allons créer un second controller API dédié à ce besoin : + +```Shell +php bin/edu make:api VilleDetail +``` + +La commande génère l'URI `/api/ville-detail`. Nous allons la modifier pour respecter la convention REST. + +Modifiez l'URI de la route dans le fichier "app/Config/routes.yaml" : + +```diff +api_ville-detail: +- uri: /api/ville-detail ++ uri: /api/ville/{id} + controller: Controller\api\VilleDetailController + httpMethod: [GET] +``` + +Puis modifiez le fichier "app/Controller/api/VilleDetailController.php" : + +```diff +get("id"); ++ ++ if ($id === null) { ++ http_response_code(404); ++ return json_encode(["erreur" => "Ville introuvable"]); ++ } ++ ++ $comBase = DatabaseService::getConnect(); ++ $statementPDO = $comBase->prepare("SELECT * FROM ville WHERE id = :id"); ++ $statementPDO->execute(["id" => (int) $id]); ++ $ville = $statementPDO->fetch(PDO::FETCH_ASSOC); ++ ++ if ($ville === false) { ++ http_response_code(404); ++ return json_encode(["erreur" => "Ville introuvable"]); ++ } ++ ++ return json_encode($ville); + } +} +``` + +Explications : + +- Le paramètre dynamique `{id}` de la route est récupéré avec la méthode `get()` de la classe [Request](../boost/resquet.md), comme dans le [use case VILLE](use-case-ville.md). +- La requête utilise une requête préparée avec le paramètre `:id` pour éviter l'injection SQL. +- La fonction native `http_response_code(404)` permet de renvoyer un code HTTP d'erreur avec un corps JSON explicite. +- L'attribut `#[Attributes\Parameter(...)]` documente le paramètre de chemin pour OpenAPI. + +Testez votre API : + +```Shell +curl -i http://localhost:8042/api/ville/1 # HTTP 200 : la ville dont l'id est 1 +curl -i http://localhost:8042/api/ville/999 # HTTP 404 : la ville 999 n'existe pas +``` + +Résultat : + +``` +HTTP/1.1 200 OK +Content-Type: application/json + +{"id":1,"nom":"Paris","code_postal":"75000","nombre_habitant":2200000} +``` + +!!! warning "Attention" + + N'oubliez pas de modifier **aussi** le chemin dans l'attribut `#[Attributes\Get(path: '...')]` du controller (dans le diff ci-dessus) : la documentation OpenAPI doit rester cohérente avec la route réelle déclarée dans "app/Config/routes.yaml". + +## Exemple à réaliser n°3 : créer une ville en POST + +!!! info "Objectif" + + - Accepter la méthode `POST` sur la route `api_ville` + - Créer une ville via l'API : `POST /api/ville` + - Renvoyer le code HTTP `201 Created` + +En REST, une même URI peut servir à plusieurs opérations : `GET /api/ville` renvoie la liste, `POST /api/ville` crée une nouvelle ressource. +C'est ce que nous allons mettre en place sur le controller de l'[exemple n°1](#exemple-a-realiser-n1-renvoyer-des-villes-stockees-en-base). + +### Modifier la route + +Ajoutez la méthode `POST` dans le fichier "app/Config/routes.yaml" : + +```diff +api_ville: + uri: /api/ville + controller: Controller\api\VilleController +- httpMethod: [GET] ++ httpMethod: [GET,POST] +``` + +!!! warning "Attention" + + Dans le cas d'une erreur "HTTP 405 Method Not Allowed", vérifiez que la méthode `POST` est bien ajoutée dans le fichier de configuration des routes. + +### Modifier le controller + +```diff +getHttpMethod() === "POST") { ++ $comBase = DatabaseService::getConnect(); ++ $statementPDO = $comBase->prepare( ++ "INSERT INTO ville (nom, code_postal, nombre_habitant) VALUES (:nom, :code_postal, :nb_habitant)" ++ ); ++ $statementPDO->execute([ ++ 'nom' => $request->get('nom'), ++ 'code_postal' => $request->get('code_postal'), ++ 'nb_habitant' => (int) $request->get('nombre_habitant') ++ ]); ++ ++ http_response_code(201); ++ return json_encode([ ++ "id" => (int) $comBase->lastInsertId(), ++ "nom" => $request->get('nom'), ++ "code_postal" => $request->get('code_postal'), ++ "nombre_habitant" => (int) $request->get('nombre_habitant') ++ ]); ++ } + + $comBase = DatabaseService::getConnect(); + $statementPDO = $comBase->query("SELECT * FROM ville"); + $villes = $statementPDO->fetchAll(PDO::FETCH_ASSOC); + + return json_encode($villes); + } +} +``` + +Explications : + +- La méthode `getHttpMethod()` de la classe [Request](../boost/resquet.md#gethttpmethod) permet de distinguer le traitement du `GET` (liste) et du `POST` (création). +- Les données envoyées par le client sont récupérées avec la méthode `get()` de la classe [Request](../boost/resquet.md#getkey). +- La fonction native `http_response_code(201)` renvoie le code HTTP `201 Created`, convention REST pour une création réussie. +- L'attribut `#[Attributes\Post(path: '...')]` documente la nouvelle opération pour OpenAPI. + +Testez votre API : + +```Shell +curl -i -X POST http://localhost:8042/api/ville \ + -d "nom=Bordeaux" \ + -d "code_postal=33000" \ + -d "nombre_habitant=257000" +``` + +Résultat : + +``` +HTTP/1.1 201 Created +Content-Type: application/json + +{"id":4,"nom":"Bordeaux","code_postal":"33000","nombre_habitant":257000} +``` + +!!! info "Corps de requête au format JSON" + + La classe [Request](../boost/resquet.md) lit les données de formulaire (type `application/x-www-form-urlencoded`, comme celles envoyées par curl ou un formulaire HTML). + Si votre client envoie directement un corps JSON (type `application/json`, fréquent avec Postman ou un frontend JavaScript), les données se lisent ainsi : + + ```php + $data = json_decode(file_get_contents('php://input'), true); + $nom = $data['nom'] ?? null; + ``` + +# Documenter son API avec OpenAPI + +[OpenAPI](https://swagger.io/specification/){:target="_blank"} est un format de description des API (anciennement Swagger). +La commande `make:api` génère déjà les attributs nécessaires, répartis dans deux fichiers : + +- **"app/Controller/api/openapi.php"** : les informations globales de l'API (attribut `Info` : titre, version) +- **Le controller généré** : les informations de chaque opération (attributs `Get`, `Post`, `Response`, `Parameter`...) + +Vous pouvez enrichir la documentation au fil de l'eau, par exemple : + +```php +#[Attributes\Get(path: '/api/ville/{id}')] +#[Attributes\Parameter(name: 'id', in: 'path', required: true, schema: new Attributes\Schema(type: 'integer'))] +#[Attributes\Response(response: '200', description: 'La ville demandee')] +#[Attributes\Response(response: '404', description: 'Ville introuvable')] +public function execute(Request $request): string|null +``` + +Les attributs les plus utiles : + +| Attribut | Rôle | +|-------------------------------------------|----------------------------------------------------------| +| `#[Attributes\Get(path: '...')]` | Documente une opération `GET` sur le chemin indiqué | +| `#[Attributes\Post(path: '...')]` | Documente une opération `POST` sur le chemin indiqué | +| `#[Attributes\Response(response: '...')]` | Documente un code de réponse HTTP et sa description | +| `#[Attributes\Parameter(...)]` | Documente un paramètre (de chemin, de requête...) | +| `#[Attributes\Info(title: '...', version: '...')]` | Informations globales de l'API (dans "openapi.php") | + +### Générer la spécification OpenAPI + +La librairie [swagger-php](https://github.com/zircote/swagger-php){:target="_blank"} (installée avec Edu Framework) analyse ces attributs et produit un fichier de spécification : + +```Shell +./vendor/bin/openapi app/Controller/api -o public/openapi.yaml +``` + +Exemple de résultat : + +```yaml +openapi: 3.0.0 +info: + title: 'My First API' + version: '0.1' +paths: + /api/hello: + get: + responses: + '200': + description: 'Mettre une description' + /api/ville: + get: + responses: + '200': + description: 'La liste des villes' +``` + +Ce fichier peut ensuite être visualisé dans des outils comme [Swagger Editor](https://editor-next.swagger.io/){:target="_blank"}, ou servi à des outils de génération de client. + +!!! bug "Incompatibilité connue avec PHP 8.4+" + + La version de swagger-php actuellement embarquée par Edu Framework (4.x-dev) n'est pas compatible avec PHP 8.4 et supérieur : la commande `vendor/bin/openapi` affiche l'erreur `Constant E_STRICT is deprecated` et produit un fichier incomplet. + + En attendant la mise à jour de cette dépendance, vous pouvez générer la spécification via un petit script PHP : + + ```php + generate([__DIR__ . '/app/Controller/api']); + file_put_contents(__DIR__ . '/public/openapi.yaml', $spec->toYaml()); + ``` + + ```Shell + php generate-openapi.php + ``` + + Des messages d'avertissement (deprecated) peuvent s'afficher : ils n'empêchent pas la génération du fichier "public/openapi.yaml". + +# Les erreurs possibles de la commande + +| Message affiché | Cause | Solution | +|--------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------| +| `Route already exists.` | Une route `api_` existe déjà dans "app/Config/routes.yaml" (la commande a déjà été exécutée avec ce nom) | Choisir un autre nom, ou supprimer / renommer la route dans "app/Config/routes.yaml" | +| `Controller already exists.` | Le fichier "app/Controller/api/\Controller.php" existe déjà (même si la route a été renommée) | Choisir un autre nom, ou supprimer le fichier controller | +| `Not enough arguments (missing: "controller-name")` | L'argument obligatoire n'a pas été passé à la commande | Relancer la commande : `php bin/edu make:api ` | + +!!! info "Ordre des vérifications" + + La commande vérifie d'abord l'existence de la **route**, puis celle du **controller**. + Si vous exécutez deux fois `make:api` avec le même nom, c'est donc `Route already exists.` qui s'affiche. + +# Pour aller plus loin + +- [Request - La gestion des requêtes HTTP](../boost/resquet.md) +- [Route - Générer une URL par le nom](../boost/route.md) +- [DatabaseService - La gestion des données](../boost/dataservice.md) +- [Le use case "VILLE" complet (CRUD)](use-case-ville.md) +- [La documentation de swagger-php](https://github.com/zircote/swagger-php){:target="_blank"} +- [La spécification OpenAPI](https://swagger.io/specification/){:target="_blank"} diff --git a/docs/build/dataservice-select.md b/docs/build/dataservice-select.md index 11f6578..ea6147b 100644 --- a/docs/build/dataservice-select.md +++ b/docs/build/dataservice-select.md @@ -1,9 +1,9 @@ -# Gestion des données +# Construire le CRUD ## Introduction Nous avons vu dans la documentation précédente comment gérer une page avec un controller et un formulaire en méthode POST. -La suite logique est de comprendre comment gérer les données dans une base de données. +La suite logique est de construire le [CRUD](../annexes/glossaire.md#crud){:target="_blank"} (Create, Read, Update, Delete) d'une entité : créer, lire, modifier et supprimer des enregistrements dans une base de données. !!! Prérequis "PHP Data Objects (PDO)" @@ -11,6 +11,14 @@ La suite logique est de comprendre comment gérer les données dans une base de Pour plus d'informations sur l'objet PDO, vous pouvez consulter la [documentation officielle](https://www.php.net/manual/fr/book.pdo.php){:target="_blank"}. +Voici les quatre opérations que nous allons construire pour l'entité `ville` : + +| Lettre | Opération | Requête SQL | Ce que nous allons construire | +|--------|-----------------------|-------------|----------------------------------------------| +| **C** | Create (créer) | `INSERT` | Le formulaire de création (`POST /ville`) | +| **R** | Read (lire) | `SELECT` | La liste des villes (`GET /ville`) | +| **U** | Update (modifier) | `UPDATE` | Le formulaire de modification (`/ville/{id}/update`) | +| **D** | Delete (supprimer) | `DELETE` | Le lien de suppression (`/ville/{id}/delete`) | ## Prérequis @@ -19,8 +27,8 @@ Vous devez activer le service DatabaseService pour utiliser une base de données !!! info "Lecture obligatoire" Vous devez avoir lu la documentation sur la [DatabaseService : La gestion des données](../boost/dataservice.md) - - L'activation du service DatabaseService s'oppère dans le fichier .env de votre projet. + + L'activation du service DatabaseService s'opère dans le fichier .env de votre projet. ```diff -- DB_HOST_STATUS=false ++ DB_HOST_STATUS=true @@ -63,7 +71,7 @@ INSERT INTO ville (nom, code_postal, nombre_habitant) VALUES ('Marseille', '1300 INSERT INTO ville (nom, code_postal, nombre_habitant) VALUES ('Lyon', '69000', 500000); ``` -## Création de l'entité Ville et de la couche modèle (Repository) +## Étape 1 : le Read avec l'entité et le repository Nous allons reprendre la page sur [Construire un formulaire](controller-edu-post.md) @@ -84,7 +92,7 @@ Voici l'arborecence actuelle de notre projet : Nous allons ajouter deux dossiers dans le dossier `app` pour gérer les données : - `Entity` : pour les entités (classes qui représentent les données de la base de données) -- `Repository` : pour les classes qui permettent de récupérer les données de la base de données +- `Repository` : pour les classes qui permettent de récupérer les données de la base de données ``` hl_lines="6 7 8 9" ├── app @@ -204,8 +212,8 @@ class VilleController implements ControllerInterface return TwigCore::getEnvironment()->render('ville/ville.html.twig', [ "titre" => 'VilleController', - "request" => $request, - "add_ville" => $request->get('nom_ville'), + "request" => $request, + "add_ville" => $request->get('nom_ville'), + "villes" => (new VilleRepository())->getVilles() ] ); @@ -244,4 +252,579 @@ Nous allons modifier la vue `ville.html.twig` pour afficher les données de la b + {% endfor %} + {% endblock %} -``` \ No newline at end of file +``` + +Le **Read** (la liste des villes) est en place. Pour l'instant, le formulaire n'enregistre rien en base de données : c'est l'objet de l'étape suivante. + +## Étape 2 : le Create avec le formulaire de création + +!!! info "Objectif" + + - Compléter le formulaire pour saisir toutes les informations d'une ville + - Ajouter la méthode `createVille()` au repository + - Enregistrer la ville en base de données lors de l'envoi du formulaire en POST + +### Compléter le formulaire dans la vue ville.html.twig + +Le formulaire actuel ne contient que le nom de la ville. Nous allons le compléter avec le code postal et le nombre d'habitants : + +```diff +{% block content %} +

{{ titre }}

+- {% if add_ville is not null %} +- +- {% endif %} +- +-

Créer une nouvelle ville

++

Créer une nouvelle ville

+
+- +- ++ ++ ++ ++ ++ ++ ++ ++ ++ + +
+ +

Liste des villes

+
    + {% for ville in villes %} +
  • {{ ville.getNom() }} ({{ ville.getCodePostal() }}) - {{ ville.getNombreHabitant() }} habitants
  • + {% endfor %} +
+{% endblock %} +``` + +L'affichage du message de confirmation (`add_ville`) est supprimé : après l'enregistrement, nous allons rediriger l'utilisateur vers la liste des villes (voir le pattern PRG ci-dessous). + +### Ajouter la méthode createVille() au repository + +Nous allons ajouter la méthode `createVille()` dans le fichier "app/Repository/VilleRepository.php". + +```diff + class VilleRepository + { + public function getVilles(): array + { + $villes = []; + + $DataService = DatabaseService::getConnect(); + $stmt = $DataService->query('SELECT * FROM ville'); + + while ($row = $stmt->fetch()) { + $villes[] = new Ville($row['id'], $row['nom'], $row['code_postal'], $row['nombre_habitant']); + } + + return $villes; + } ++ ++ public function createVille(Ville $ville): int ++ { ++ $comBase = DatabaseService::getConnect(); ++ $statementPDO = $comBase->prepare( ++ "INSERT INTO ville (nom, code_postal, nombre_habitant) VALUES (:nom, :code_postal, :nombre_habitant)" ++ ); ++ $statementPDO->execute([ ++ 'nom' => $ville->getNom(), ++ 'code_postal' => $ville->getCodePostal(), ++ 'nombre_habitant' => $ville->getNombreHabitant() ++ ]); ++ ++ return (int) $comBase->lastInsertId(); ++ } + } +``` + +La méthode utilise une **requête préparée** avec les paramètres `:nom`, `:code_postal` et `:nombre_habitant`. + +!!! warning "Sujet cybersécurité" + + Pourquoi utiliser une requête préparée ? + + - Pour éviter les attaques par injection SQL + - Pour améliorer les performances des requêtes SQL + - Pour faciliter la gestion des paramètres de la requête + + La méthode `lastInsertId()` renvoie l'identifiant de la ville créée : utile si vous souhaitez rediriger l'utilisateur vers la fiche de la ville. + +### Traiter le formulaire dans le controller + +Nous allons modifier le controller `VilleController` pour enregistrer la ville lorsque la requête est en POST. + +```diff +getHttpMethod() === "POST") { ++ $ville = new Ville( ++ 0, ++ $request->get('nom'), ++ $request->get('code_postal'), ++ (int) $request->get('nombre_habitant') ++ ); ++ ++ (new VilleRepository())->createVille($ville); ++ ++ header('Location: ' . (new Route())->getNameToPath('ville')); ++ return null; ++ } ++ + return TwigCore::getEnvironment()->render('ville/ville.html.twig', + [ + "titre" => 'VilleController', + "request" => $request, +- "add_ville" => $request->get('nom_ville'), + "villes" => (new VilleRepository())->getVilles() + ] + ); + } +} +``` + +Explications : + +- La méthode `getHttpMethod()` de la classe [Request](../boost/resquet.md) permet de distinguer l'affichage de la page (`GET`) de l'envoi du formulaire (`POST`). +- Les données du formulaire sont récupérées avec la méthode `get()` de la classe [Request](../boost/resquet.md). +- L'identifiant est initialisé à `0` : c'est la base de données qui attribuera le vrai identifiant lors de l'`INSERT`. +- La classe [Route](../boost/route.md) génère l'URL de la route `ville` par son nom : si vous changez l'URI de la route, la redirection suivra automatiquement. + +!!! info "Le pattern PRG (Post/Redirect/Get)" + + Après le traitement du formulaire, le controller **redirige** l'utilisateur vers la liste des villes avec `header('Location: ...')` puis `return null`. + + - `header('Location: ...')` envoie une redirection HTTP (code `302`) + - `return null` : le controller n'affiche aucune vue, la réponse est uniquement la redirection + + Ce pattern évite que le formulaire soit **renvoyé** si l'utilisateur actualise la page (touche F5) après une création. Sans redirection, la ville serait enregistrée deux fois ! + +!!! warning "Attention" + + Vérifiez que la route `ville` accepte bien la méthode `POST` dans le fichier "app/Config/routes.yaml" (`httpMethod: [GET,POST]`), sinon une erreur `HTTP 405 Method Not Allowed` sera renvoyée. + +## Étape 3 : le Update avec le formulaire de modification + +!!! info "Objectif" + + - Générer un controller dédié à la modification d'une ville + - Récupérer une ville par son identifiant pour pré-remplir le formulaire + - Enregistrer les modifications en base de données + +### Générer le controller de mise à jour + +Créez le controller avec la commande suivante : + +```Shell +php bin/edu make:controller villeUpdate +``` + +Cette commande génère le controller "VilleUpdateController.php", la vue "villeupdate/villeupdate.html.twig" et la route `villeupdate` dans le fichier "app/Config/routes.yaml". + +### Modifier la route + +La route générée est `/villeupdate`. Pour modifier une ville précise, l'URI doit contenir son identifiant. Modifiez la route dans le fichier "app/Config/routes.yaml" : + +```diff +villeupdate: +- uri: /villeupdate ++ uri: /ville/{id}/update + controller: Controller\VilleUpdateController +- httpMethod: [GET] ++ httpMethod: [GET,POST] +``` + +- Le paramètre dynamique `{id}` permet de cibler une ville, comme dans le [use case VILLE](use-case-ville.md) +- La méthode `POST` est nécessaire pour l'enregistrement du formulaire de modification + +### Ajouter les méthodes getVille() et updateVille() au repository + +Ajoutez deux méthodes dans le fichier "app/Repository/VilleRepository.php" : + +```diff + public function createVille(Ville $ville): int + { + $comBase = DatabaseService::getConnect(); + $statementPDO = $comBase->prepare( + "INSERT INTO ville (nom, code_postal, nombre_habitant) VALUES (:nom, :code_postal, :nombre_habitant)" + ); + $statementPDO->execute([ + 'nom' => $ville->getNom(), + 'code_postal' => $ville->getCodePostal(), + 'nombre_habitant' => $ville->getNombreHabitant() + ]); + + return (int) $comBase->lastInsertId(); + } ++ ++ public function getVille(int $id): Ville|false ++ { ++ $comBase = DatabaseService::getConnect(); ++ $statementPDO = $comBase->prepare("SELECT * FROM ville WHERE id = :id"); ++ $statementPDO->execute(['id' => $id]); ++ ++ $row = $statementPDO->fetch(); ++ ++ if ($row === false) { ++ return false; ++ } ++ ++ return new Ville($row['id'], $row['nom'], $row['code_postal'], $row['nombre_habitant']); ++ } ++ ++ public function updateVille(Ville $ville): bool ++ { ++ $comBase = DatabaseService::getConnect(); ++ $statementPDO = $comBase->prepare( ++ "UPDATE ville SET nom = :nom, code_postal = :code_postal, nombre_habitant = :nombre_habitant WHERE id = :id" ++ ); ++ return $statementPDO->execute([ ++ 'nom' => $ville->getNom(), ++ 'code_postal' => $ville->getCodePostal(), ++ 'nombre_habitant' => $ville->getNombreHabitant(), ++ 'id' => $ville->getId() ++ ]); ++ } + } +``` + +- `getVille()` complète le **Read** : elle renvoie l'entité `Ville` correspondant à l'identifiant, ou `false` si la ville n'existe pas +- `updateVille()` met à jour la ligne correspondant à l'identifiant de l'entité + +### Modifier le controller VilleUpdateController + +Modifiez le fichier "app/Controller/VilleUpdateController.php" généré par la commande : + +```diff +get("id"); ++ ++ if ($request->getHttpMethod() === "POST") { ++ $ville = new Ville( ++ $id, ++ $request->get('nom'), ++ $request->get('code_postal'), ++ (int) $request->get('nombre_habitant') ++ ); ++ ++ $villeRepository->updateVille($ville); ++ ++ header('Location: ' . (new Route())->getNameToPath('ville')); ++ return null; ++ } ++ ++ $ville = $villeRepository->getVille($id); ++ ++ if ($ville === false) { ++ header('Location: ' . (new Route())->getNameToPath('ville')); ++ return null; ++ } ++ + return TwigCore::getEnvironment()->render('villeupdate/villeupdate.html.twig', + [ + "titre" => 'VilleUpdateController', +- "request" => $request ++ "request" => $request, ++ "ville" => $ville + ] + ); + } +} +``` + +Explications : + +- L'identifiant est récupéré depuis l'URI avec `$request->get("id")` : le paramètre `{id}` de la route est accessible comme un paramètre de requête +- En `POST` : la ville est mise à jour puis l'utilisateur est redirigé vers la liste (pattern PRG) +- En `GET` : la ville est récupérée en base de données pour pré-remplir le formulaire ; si elle n'existe pas (`false`), l'utilisateur est redirigé vers la liste + +### Modifier la vue villeupdate.html.twig + +Le formulaire de modification est pré-rempli avec les valeurs de la ville. Modifiez le fichier "app/Template/villeupdate/villeupdate.html.twig" : + +```diff +{% extends "base.html.twig" %} + +{% block title %}{{ titre }}{% endblock %} + +{% block content %} +

{{ titre }}

++ ++
++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++
+{% endblock %} +``` + +La fonction Twig `getNameToPath()` accepte les paramètres de route en second argument : `getNameToPath('villeupdate', {'id': ville.getId()})` génère l'URL `/ville/42/update` pour la ville n°42. Vous pouvez consulter la documentation de la classe [Route](../boost/route.md) pour plus de détails. + +### Ajouter le lien "Modifier" dans la liste + +Transformez la liste à puces de la vue "ville.html.twig" en tableau pour ajouter une colonne d'actions : + +```diff +-

Liste des villes

+-
    +- {% for ville in villes %} +-
  • {{ ville.getNom() }} ({{ ville.getCodePostal() }}) - {{ ville.getNombreHabitant() }} habitants
  • +- {% endfor %} +-
++

Liste des villes

++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ {% for ville in villes %} ++ ++ ++ ++ ++ ++ ++ ++ {% endfor %} ++ ++
IDNomCode postalNombre d'habitantsActions
{{ ville.getId() }}{{ ville.getNom() }}{{ ville.getCodePostal() }}{{ ville.getNombreHabitant() }} ++ Modifier ++
+``` + +Le **Update** est en place : la liste des villes propose un lien "Modifier" pour chaque ville. + +## Étape 4 : le Delete avec le lien de suppression + +!!! info "Objectif" + + - Générer un controller dédié à la suppression d'une ville + - Supprimer la ville en base de données et rediriger vers la liste + +### Générer le controller de suppression + +Créez le controller avec la commande suivante : + +```Shell +php bin/edu make:controller villeDelete +``` + +Cette commande génère le controller "VilleDeleteController.php", la vue "villedelete/villedelete.html.twig" et la route `villedelete` dans le fichier "app/Config/routes.yaml". + +### Modifier la route + +Comme pour la modification, l'URI doit contenir l'identifiant de la ville à supprimer : + +```diff +villedelete: +- uri: /villedelete ++ uri: /ville/{id}/delete + controller: Controller\VilleDeleteController + httpMethod: [GET] +``` + +### Ajouter la méthode deleteVille() au repository + +Ajoutez la méthode dans le fichier "app/Repository/VilleRepository.php" : + +```diff + public function updateVille(Ville $ville): bool + { + $comBase = DatabaseService::getConnect(); + $statementPDO = $comBase->prepare( + "UPDATE ville SET nom = :nom, code_postal = :code_postal, nombre_habitant = :nombre_habitant WHERE id = :id" + ); + return $statementPDO->execute([ + 'nom' => $ville->getNom(), + 'code_postal' => $ville->getCodePostal(), + 'nombre_habitant' => $ville->getNombreHabitant(), + 'id' => $ville->getId() + ]); + } ++ ++ public function deleteVille(int $id): bool ++ { ++ $comBase = DatabaseService::getConnect(); ++ $statementPDO = $comBase->prepare("DELETE FROM ville WHERE id = :id"); ++ return $statementPDO->execute(['id' => $id]); ++ } + } +``` + +### Modifier le controller VilleDeleteController + +Modifiez le fichier "app/Controller/VilleDeleteController.php" généré par la commande : + +```diff +render('villedelete/villedelete.html.twig', +- [ +- "titre" => 'VilleDeleteController', +- "request" => $request +- ] +- ); ++ if ($request->get("id") !== null) { ++ (new VilleRepository())->deleteVille((int) $request->get("id")); ++ } ++ ++ header('Location: ' . (new Route())->getNameToPath('ville')); ++ return null; + } +} +``` + +La suppression ne nécessite pas de vue : le controller supprime la ville puis redirige vers la liste (pattern PRG). +La vue "villedelete.html.twig" générée par la commande n'est pas utilisée, vous pouvez la supprimer. + +### Ajouter le lien "Supprimer" dans la liste + +Ajoutez le lien de suppression dans la colonne des actions de la vue "ville.html.twig" : + +```diff + + Modifier ++ Supprimer + +``` + +!!! tip "Confirmer la suppression" + + Une suppression par simple clic peut être dangereuse. Vous pouvez demander une confirmation à l'utilisateur avec JavaScript : + + ```html + Supprimer + ``` + + Pour aller plus loin en sécurité, la bonne pratique est d'effectuer la suppression par une requête `POST` (avec un formulaire) plutôt qu'un simple lien `GET`. + +## Récapitulatif du CRUD + +### Le fichier de configuration des routes + +Voici les trois routes du CRUD dans le fichier "app/Config/routes.yaml" : + +```yaml +ville: + uri: /ville + controller: Controller\VilleController + httpMethod: [GET,POST] +villeupdate: + uri: /ville/{id}/update + controller: Controller\VilleUpdateController + httpMethod: [GET,POST] +villedelete: + uri: /ville/{id}/delete + controller: Controller\VilleDeleteController + httpMethod: [GET] +``` + +### L'arborescence finale du projet + +``` hl_lines="6 7 8 9 11 12 13 14" +├── app +│ ├── Config +│ │ └── routes.yaml +│ ├── Controller +│ │ ├── VilleController.php +│ │ ├── VilleDeleteController.php +│ │ └── VilleUpdateController.php +│ ├── Entity +│ │ └── Ville.php +│ ├── Repository +│ │ └── VilleRepository.php +│ └── Template +│ ├── base.html.twig +│ ├── ville +│ │ └── ville.html.twig +│ └── villeupdate +│ └── villeupdate.html.twig +``` + +### Le mapping du CRUD + +| Opération | Route | Méthode HTTP | Requête SQL | Vue | +|-----------|-----------------------|--------------|-------------|----------------------| +| Create | `/ville` | `POST` | `INSERT` | Formulaire de création | +| Read | `/ville` | `GET` | `SELECT` | Liste des villes | +| Update | `/ville/{id}/update` | `GET`, `POST`| `SELECT` + `UPDATE` | Formulaire de modification | +| Delete | `/ville/{id}/delete` | `GET` | `DELETE` | Aucune (redirection) | + +Le CRUD de l'entité `ville` est terminé : vous pouvez créer, lire, modifier et supprimer des villes depuis votre application. :smile: + +## Pour aller plus loin + +- [Le use case "VILLE" sans repository](use-case-ville.md) : la version directe du CRUD (requêtes SQL dans les controllers) +- [Refactoring du use case "VILLE"](use-case-ville-refactor.md) : harmoniser les routes et regrouper les controllers +- [Construire une API](controller-edu-api.md) : exposer le CRUD de ville en API REST (JSON) +- [DatabaseService - La gestion des données](../boost/dataservice.md) +- [Route - Générer une URL par le nom](../boost/route.md) +- [Request - La gestion des requêtes HTTP](../boost/resquet.md) diff --git a/docs/build/index.md b/docs/build/index.md index 0e9c177..d3db391 100644 --- a/docs/build/index.md +++ b/docs/build/index.md @@ -18,12 +18,20 @@ Pour créer un formulaire dans une page, vous devez suivre les instructions suiv - [Comment construire un formulaire en methode POST avec un controller](controller-edu-post.md) -## 3. Gestion des données avec un service +## 3. Construire le CRUD -- Coming soon +Pour construire le CRUD complet d'une entité (Create, Read, Update, Delete), vous devez suivre les instructions suivantes : + +- [Comment construire le CRUD de l'entité ville](dataservice-select.md) ## 4. Construire une commande CLI Pour créer une nouvelle commande, vous devez suivre les instructions suivantes : -- [Comment construire une commande CLI](new-command-edu.md) \ No newline at end of file +- [Comment construire une commande CLI](new-command-edu.md) + +## 5. Construire une API + +Pour créer une API JSON (controller, route et documentation OpenAPI), vous devez suivre les instructions suivantes : + +- [Comment construire une API](controller-edu-api.md) \ No newline at end of file diff --git a/docs/installation/command-edu.md b/docs/installation/command-edu.md index 37e0249..7445d58 100644 --- a/docs/installation/command-edu.md +++ b/docs/installation/command-edu.md @@ -38,6 +38,10 @@ Voici les commandes disponibles par version : | make:api | génération d'un controller type json API | v2.0 | | make:command | génération d'un commande line | v2.0 | +!!! info "Pour aller plus loin" + + Vous pouvez consulter la page [Construire une API](../build/controller-edu-api.md) pour plus de détails sur la commande `make:api`. + ### Les options de la commande start La commande `start` accepte des options pour adapter le démarrage à vos besoins (à partir de la version v2.3.0) :