+{% endblock %}
diff --git a/app/Template/medecin/new.html.twig b/app/Template/medecin/new.html.twig
new file mode 100644
index 0000000..de02b24
--- /dev/null
+++ b/app/Template/medecin/new.html.twig
@@ -0,0 +1,7 @@
+{% extends "base.html.twig" %}
+
+{% block title %}{{ titre }}{% endblock %}
+
+{% block content %}
+
{{ titre }}
+{% endblock %}
diff --git a/compose.yaml b/compose.yaml
index 6ddec08..630f5a6 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,18 +1,18 @@
services:
database:
container_name: edu-web-database
- image: mysql:8.0
- command: mysqld --sql_mode="STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"
- platform: linux/amd64
+ image: mariadb:latest
ports:
- "3306:3306"
restart: always
environment:
MYSQL_DATABASE: app_db
- MYSQL_ROOT_PASSWORD: studoo # Si tu changes le mot de passe root, change aussi le mot de passe dans le fichier .env et dans la config du service dbgate
- #MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
- MYSQL_USER: app_db_user
- MYSQL_PASSWORD: app_db_password
+ MYSQL_ROOT_PASSWORD: studoo
+ MYSQL_USER: stud
+ MYSQL_PASSWORD: studoo
+ # Si tu changes le mot de passe root ou stud, change aussi le mot de passe dans le fichier .env et dans la config du service dbgate
+# MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
+ TZ: Europe/Paris
volumes:
- ./var/dbdata:/var/lib/mysql
mailer:
@@ -32,7 +32,7 @@ services:
- 8082:3000
volumes:
- ./var/dbgate:/root/.dbgate
- - ./var/sqlite:/home/jan
+ - ./var/logs:/home/jan
environment:
CONNECTIONS: con1,con2
@@ -44,7 +44,7 @@ services:
ENGINE_con1: mysql@dbgate-plugin-mysql
LABEL_con2: SQLite
- FILE_con2: /home/jan/app_db.sqlite
+ FILE_con2: /home/jan/core_logs.sqlite
ENGINE_con2: sqlite@dbgate-plugin-sqlite
phpmyadmin:
diff --git a/docs/boost/resquet.md b/docs/boost/resquet.md
index 588094b..ea7a924 100644
--- a/docs/boost/resquet.md
+++ b/docs/boost/resquet.md
@@ -183,3 +183,139 @@ Résultat :
```
/inscription
```
+
+## Gestion des fichiers (upload)
+
+Request permet de gérer les fichiers envoyés via un formulaire HTML avec l'attribut `enctype="multipart/form-data"`.
+
+!!! warning "Formulaire obligatoire"
+
+ Pour envoyer des fichiers, le formulaire HTML **doit** avoir l'attribut `enctype="multipart/form-data"` :
+
+ ```html
+
+ ```
+
+### hasFile()
+Permet de savoir si le formulaire a envoyé un fichier pour ce champ.
+
+```php
+if ($request->hasFile('fichier')) {
+ // Un fichier a été envoyé pour le champ "fichier"
+}
+```
+
+!!! warning "Champ laissé vide"
+
+ Un champ file laissé vide génère quand même une entrée dans `$_FILES` avec `error = 4` (`UPLOAD_ERR_NO_FILE`).
+ `hasFile()` retourne `true` mais `isValid()` retourne `false`.
+
+### getFile()
+Renvoie la **liste** des fichiers téléversés pour un champ du formulaire, ou `null` si le champ n'existe pas.
+
+```php
+$fichiers = $request->getFile('fichier');
+```
+
+Résultat :
+
+```php
+array(1) {
+ [0]=>
+ array(5) {
+ ["name"]=> string(12) "medecins.csv"
+ ["type"]=> string(9) "text/csv"
+ ["size"]=> int(123)
+ ["tmp_name"]=> string(14) "/tmp/phpXYZ"
+ ["error"]=> int(0)
+ }
+}
+```
+
+#### Pourquoi une liste ? La normalisation de $_FILES
+
+En PHP natif, la structure de `$_FILES` change complètement de forme selon le champ HTML :
+
+| Champ HTML | Structure native de `$_FILES` |
+|---|---|
+| `` | `$_FILES['fichier']['name']` est une **chaine** |
+| `` | `$_FILES['fichiers']['name']` est un **tableau** (`type`, `size`, `tmp_name`, `error` aussi) |
+
+Le framework **normalise** cette structure à l'injection : chaque champ pointe toujours vers une **liste** de fichiers, avec les mêmes clés (`name`, `type`, `size`, `tmp_name`, `error`). Vous n'avez plus à gérer les deux formes.
+
+### getFiles()
+Renvoie l'ensemble des fichiers téléversés de la requête (structure normalisée : chaque champ → une liste de fichiers).
+
+```php
+$tousLesFichiers = $request->getFiles();
+```
+
+### isValid()
+Vérifie que le(s) fichier(s) du champ sont valides, c'est-à-dire que le téléversement s'est terminé sans erreur (`error = 0`, constante `UPLOAD_ERR_OK`).
+
+```php
+// Vérifie tous les fichiers du champ
+$request->isValid('fichier');
+
+// Vérifie uniquement le fichier à l'index 1 (champ multi-fichiers)
+$request->isValid('fichiers', 1);
+```
+
+### getExtension()
+Renvoie l'extension du fichier téléversé (en minuscules), ou `null` si le fichier est absent ou invalide.
+
+```php
+$extension = $request->getExtension('fichier'); // "csv"
+```
+
+### move()
+Déplace le fichier téléversé depuis son emplacement temporaire (`tmp_name`) vers sa destination finale.
+Cette méthode encapsule la fonction PHP `move_uploaded_file()` en vérifiant au préalable que le téléversement est valide. Elle retourne `false` en cas de problème.
+
+```php
+$destination = __DIR__ . '/../../public/upload/mon-fichier.csv';
+if ($request->move('fichier', $destination) === true) {
+ // Le fichier a été déplacé
+}
+```
+
+## Exemple complet
+
+Un exemple complet et fonctionnel est disponible dans le framework :
+la route [/medecin/import](http://localhost:8042/medecin/import) permet de téléverser un fichier CSV ou TXT.
+
+```php
+public function import(Request $request): string|null
+{
+ $message = null;
+ $erreur = null;
+
+ if ($request->getHttpMethod() === "POST") {
+ if ($request->hasFile('fichier') === false) {
+ $erreur = "Aucun fichier n'a été envoyé";
+ } elseif ($request->isValid('fichier') === false) {
+ $erreur = "Le fichier n'est pas valide (taille trop grande ou téléversement interrompu)";
+ } elseif (in_array($request->getExtension('fichier'), ['csv', 'txt'], true) === false) {
+ $erreur = "Extension non autorisée";
+ } else {
+ $destination = $dossierUpload . '/import-' . uniqid() . '.' . $request->getExtension('fichier');
+ if ($request->move('fichier', $destination) === true) {
+ $message = "Le fichier a bien été téléversé";
+ } else {
+ $erreur = "Problème lors de l'enregistrement du fichier";
+ }
+ }
+ }
+
+ return TwigCore::getEnvironment()->render('medecin/import.html.twig', [...]);
+}
+```
+
+!!! warning "Sécurité"
+
+ - Vérifiez toujours `isValid()` **et** l'extension côté serveur avec une **liste blanche** (jamais une liste noire)
+ - Ne faites **jamais** confiance au nom de fichier fourni par le client : générez le nom final côté serveur (Exemple: `uniqid()`)
+ - Utilisez toujours `move_uploaded_file()` (via `move()`) et non `copy()` : c'est la seule façon sûre de manipuler un fichier téléversé
diff --git a/docs/boost/route.md b/docs/boost/route.md
index 9039871..a1cdd03 100644
--- a/docs/boost/route.md
+++ b/docs/boost/route.md
@@ -21,6 +21,12 @@ Cette fonctionnalité est disponible à partir de la version **v2.1.0**.
Si vous changez l'`uri` d'une route, tous les liens générés par `getNameToPath()` suivent automatiquement.
+!!! info "Plusieurs méthodes dans un même controller"
+
+ Depuis la version **v2.4.0**, la clé `controller` du fichier `app/Config/routes.yaml` peut désigner une méthode précise du controller avec la syntaxe `Controller\MedecinController::index` (voir [Un controller, plusieurs routes](../build/controller-edu.md#un-controller-plusieurs-routes-plusieurs-methodes)).
+
+ Cela ne change rien pour `getNameToPath()` : la génération d'URL se base uniquement sur le **nom** et l'`uri` de la route.
+
## Utilisation dans un template Twig
`getNameToPath` est une fonction Twig disponible dans tous vos templates :
diff --git a/docs/build/controller-edu.md b/docs/build/controller-edu.md
index 504e84d..3bea75d 100644
--- a/docs/build/controller-edu.md
+++ b/docs/build/controller-edu.md
@@ -128,6 +128,79 @@ Si nous allons de nouveau sur l'url [http://localhost:8042/hello](http://localho
Pour rappel, l'erreur `HTTP 404 signifie que la ressource demandée (route) n'existe pas ou plus.
+## Un controller, plusieurs routes (plusieurs méthodes)
+
+Jusqu'à présent, une route était associée à un controller possédant une seule méthode `execute()`.
+Il est possible de regrouper plusieurs routes dans un même controller, chacune pointant vers une méthode différente.
+Ce fonctionnement est disponible à partir de la version **v2.4.0**.
+
+Pour cela, on ajoute le nom de la méthode à appeler après le nom du controller, séparé par `::` dans le fichier "app/Config/routes.yaml" :
+
+```yaml
+medecin:
+ uri: /medecin
+ controller: Controller\MedecinController::index
+ httpMethod: [GET]
+medecin_new:
+ uri: /medecin/new
+ controller: Controller\MedecinController::new
+ httpMethod: [GET, POST]
+```
+
+La route `/medecin` appelle la méthode `index()` et la route `/medecin/new` appelle la méthode `new()` du controller `MedecinController` :
+
+```php
+render('medecin/index.html.twig',
+ [
+ "titre" => 'Liste des médecins',
+ "request" => $request
+ ]
+ );
+ }
+
+ public function new(Request $request): string|null
+ {
+ return TwigCore::getEnvironment()->render('medecin/new.html.twig',
+ [
+ "titre" => 'Créer un médecin',
+ "request" => $request
+ ]
+ );
+ }
+}
+```
+
+!!! info "La méthode new() est-elle valide ?"
+
+ `new` est un mot réservé du langage PHP, mais il est autorisé comme nom de méthode depuis PHP 7.
+
+### Les règles à respecter
+
+Une méthode appelée par une route doit :
+
+- être **publique** et **non statique**,
+- avoir un paramètre de type [Request](../boost/resquet.md),
+- retourner une chaine de caractères (`string`) ou `null`.
+
+Si une de ces règles n'est pas respectée, une exception `ErrorControllerException` est levée avec un message explicite.
+
+!!! info "Et l'interface ControllerInterface ?"
+
+ - Sans méthode explicite (`Controller\MedecinController`), le controller doit toujours implémenter l'interface `ControllerInterface` et c'est sa méthode `execute()` qui est appelée. Ce comportement reste inchangé : toutes vos routes existantes continuent de fonctionner.
+ - Avec une méthode explicite (`Controller\MedecinController::index`), l'interface n'est plus requise : le framework vérifie directement la méthode appelée.
+
+
## Schema pour mieux comprendre le fonctionnement
diff --git a/docs/installation/command-edu.md b/docs/installation/command-edu.md
index 7445d58..0ceabda 100644
--- a/docs/installation/command-edu.md
+++ b/docs/installation/command-edu.md
@@ -37,6 +37,7 @@ Voici les commandes disponibles par version :
| make:controller | génération d'un controller | v1.0 |
| make:api | génération d'un controller type json API | v2.0 |
| make:command | génération d'un commande line | v2.0 |
+| cache:clear | suppression du cache de l'application | v2.4.0 |
!!! info "Pour aller plus loin"
@@ -59,6 +60,28 @@ php bin/edu start --no-start
Vous pouvez consulter la page [Start/Stop l'application](start-application.md) pour plus de détails sur la commande `start`.
+### La commande cache:clear
+
+Les templates Twig sont compilés puis mis en cache dans le dossier `var/cache/twig` (à partir de la version v2.4.0).
+Pour supprimer le cache de l'application, vous pouvez taper la commande suivante :
+
+```bash
+php bin/edu cache:clear
+```
+
+| Élément | Détail |
+|------------------------|-----------------------------------------------------|
+| Dossier concerné | `var/cache` (configurable via la clé `cache_path`) |
+| Contenu supprimé | tout le contenu du dossier (Exemple: le cache TWIG) |
+| Le dossier lui-même | conservé |
+
+Si le dossier `var/cache` n'existe pas, un message vous indique qu'il n'y a aucun cache à supprimer.
+
+!!! info "Recompilation automatique"
+
+ Les templates modifiés sont automatiquement recompilés (option `auto_reload` de Twig).
+ La commande `cache:clear` reste utile pour repartir d'un cache vierge, par exemple après un déploiement.
+
___
## Si vous rencontrez une erreur
diff --git a/docs/installation/installation.md b/docs/installation/installation.md
index 10a75c6..2e0c383 100644
--- a/docs/installation/installation.md
+++ b/docs/installation/installation.md
@@ -139,6 +139,27 @@ Fatal error: Uncaught Dotenv\Exception\InvalidPathException:
Unable to read any of the environment file(s) at [...]
````
+### Erreur sur le fichier des routes
+
+Si le fichier des routes **"app/Config/routes.yaml"** est introuvable, le framework lève une exception `ErrorRouteConfigNotExistException` :
+
+````Bash
+Fatal error: Uncaught Studoo\EduFramework\Core\Exception\ErrorRouteConfigNotExistException:
+Le fichier de configuration des routes <.../app/config/routes.yaml> n'existe pas.
+Vérifiez la configuration 'route_config_path' et la casse du dossier
+(Exemple: app/Config et non app/config, les systèmes de fichiers Linux sont sensibles à la casse)
+````
+
+Les causes possibles :
+
+- la configuration `route_config_path` dans "public/index.php" ne pointe pas vers le bon dossier,
+- **la casse du dossier est incorrecte** : le dossier doit s'appeler `app/Config` (C majuscule) et non `app/config`.
+
+!!! warning "Sensibilité à la casse"
+
+ Sur macOS et Windows, la casse des noms de fichiers et de dossiers est ignorée : un projet avec `app/config` fonctionne en local.
+ En revanche, sur Linux (GitHub Codespaces, devcontainer, serveur de production), le système de fichiers est **sensible à la casse** : `app/config` et `app/Config` sont deux dossiers différents.
+
### Erreur d'installation des dépendances
Au moment de la commande "composer create-project studoo/edu-framework-skeleton ...", vous pouvez rencontrer des erreurs d'installation des dépendances.
diff --git a/public/index.php b/public/index.php
index aa3ae83..1822d36 100644
--- a/public/index.php
+++ b/public/index.php
@@ -33,6 +33,7 @@
'base_path' => __DIR__ . '/../',
'twig_path' => __DIR__ . '/../app/Template',
'route_config_path' => __DIR__ . '/../app/Config/',
+ 'cache_path' => __DIR__ . '/../var/cache/',
'sqlite_path' => __DIR__ . '/../var/sqlite/',
'sqlite_logs_path' => __DIR__ . '/../var/logs/'
]
diff --git a/public/upload/.gitkeep b/public/upload/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/EduFramework/Commands/ClearCacheCommand.php b/src/EduFramework/Commands/ClearCacheCommand.php
new file mode 100644
index 0000000..ec3b991
--- /dev/null
+++ b/src/EduFramework/Commands/ClearCacheCommand.php
@@ -0,0 +1,67 @@
+writeln([
+ CommandBanner::getBanner(),
+ 'Suppression du cache',
+ ''
+ ]);
+
+ // Le chemin du cache est défini dans la configuration (clé cache_path)
+ $cachePath = ConfigCore::getConfig('cache_path');
+
+ if (is_dir($cachePath) === false) {
+ self::$stdOutput->info("Aucun cache à supprimer. Le dossier <$cachePath> n'existe pas");
+ return Command::SUCCESS;
+ }
+
+ // Suppression du contenu du dossier de cache Exemple: le cache TWIG (var/cache/twig)
+ // Le dossier de cache lui-même est conservé
+ $fileSystem = new Filesystem();
+ foreach (scandir($cachePath) as $item) {
+ if ($item !== '.' && $item !== '..') {
+ $fileSystem->remove($cachePath . DIRECTORY_SEPARATOR . $item);
+ }
+ }
+
+ self::$stdOutput->success("Le cache a été supprimé : " . realpath($cachePath));
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/src/EduFramework/Commands/Extends/AppCommand.php b/src/EduFramework/Commands/Extends/AppCommand.php
index 1d36432..1574c5d 100644
--- a/src/EduFramework/Commands/Extends/AppCommand.php
+++ b/src/EduFramework/Commands/Extends/AppCommand.php
@@ -47,6 +47,7 @@ public function __construct()
$this->add(new \Studoo\EduFramework\Commands\StartCommand());
$this->add(new \Studoo\EduFramework\Commands\CreateCliCommand());
$this->add(new \Studoo\EduFramework\Commands\CreateApiCommand());
+ $this->add(new \Studoo\EduFramework\Commands\ClearCacheCommand());
if (file_exists(ConfigCore::getConfig('command_config_path') . 'commands.yaml') === true) {
$commandList = Yaml::parseFile(ConfigCore::getConfig('command_config_path') . 'commands.yaml');
diff --git a/src/EduFramework/Core/ConfigCore.php b/src/EduFramework/Core/ConfigCore.php
index 990b607..797e7f7 100644
--- a/src/EduFramework/Core/ConfigCore.php
+++ b/src/EduFramework/Core/ConfigCore.php
@@ -37,13 +37,14 @@ public function __construct(array $config)
self::$config = array_merge(
[
'name' => 'EduFramework',
- 'version' => 'v2.3.2@stable',
- 'date_version' => '2026-10-05', // Date de la livraison de la version
+ 'version' => 'v2.4.0@stable',
+ 'date_version' => '2026-10-08', // Date de la livraison de la version
'php_version' => '8.4', // Warning : bin/edu require PHP 8.4 or higher
'base_path' => '/',
'twig_path' => '/app/Template',
'route_config_path' => '/app/Config/',
'command_config_path' => 'app/Config/',
+ 'cache_path' => './var/cache/',
'sqlite_path' => './var/sqlite/',
'sqlite_logs_path' => './var/logs/',
],
diff --git a/src/EduFramework/Core/Controller/BuildControllerTrait.php b/src/EduFramework/Core/Controller/BuildControllerTrait.php
index cc85e88..156a9ec 100644
--- a/src/EduFramework/Core/Controller/BuildControllerTrait.php
+++ b/src/EduFramework/Core/Controller/BuildControllerTrait.php
@@ -32,4 +32,87 @@ public function buildController(string $controller): ControllerInterface
// Création de l'objet du controller en respectant le contrat de l'interface "ControllerInterface"
return new $controller();
}
+
+ /**
+ * Sépare le nom de la classe du controller et le nom de la méthode à appeler
+ * Le handler peut être de deux formes :
+ * - "Controller\MedecinController" : la méthode execute() sera appelée par défaut
+ * - "Controller\MedecinController::index" : la méthode index() sera appelée
+ * @param string $handler Nom du controller suivi ou non de la méthode à appeler
+ * @return array Tableau contenant le nom de la classe et le nom de la méthode [0 => classe, 1 => méthode|null]
+ */
+ public function resolveHandler(string $handler): array
+ {
+ // Pas de méthode explicite dans le handler, la méthode par défaut est execute()
+ if (strpos($handler, '::') === false) {
+ return [$handler, null];
+ }
+
+ // Séparation du nom de la classe et du nom de la méthode
+ [$controller, $method] = explode('::', $handler, 2);
+
+ // Si aucune méthode n'est renseignée, on retombe sur la méthode par défaut execute()
+ if ($method === '') {
+ return [$controller, null];
+ }
+
+ return [$controller, $method];
+ }
+
+ /**
+ * Appelle la méthode du controller associée à la requête HTTP
+ * Cette méthode est utilisée quand la route définit une méthode explicite Exemple: Controller\MedecinController::index
+ * La méthode du controller doit être publique, non statique,
+ * avoir un paramètre de type Request et retourner une chaine de caractères (string) ou null
+ * @param Request $request La requête HTTP contenant le controller (getHander) et la méthode (getAction)
+ * @return string|null
+ * @throws ErrorControllerException
+ */
+ public function callAction(Request $request): string|null
+ {
+ $controller = $request->getHander();
+ $action = $request->getAction();
+
+ // On vérifie que le controller existe
+ if (class_exists($controller) === false) {
+ throw new ErrorControllerException("Le controller <$controller> n'existe pas");
+ }
+
+ // On vérifie que la méthode existe dans le controller
+ if (method_exists($controller, $action) === false) {
+ throw new ErrorControllerException("La méthode <$action> n'existe pas dans le controller <$controller>");
+ }
+
+ $reflection = new \ReflectionMethod($controller, $action);
+
+ // On vérifie que la méthode est publique et non statique
+ if ($reflection->isPublic() === false || $reflection->isStatic() === true) {
+ throw new ErrorControllerException(
+ "La méthode <$action> du controller <$controller> doit être publique et non statique"
+ );
+ }
+
+ // On vérifie que la méthode a un paramètre de type Request
+ $parameters = $reflection->getParameters();
+ if (count($parameters) === 0
+ || $parameters[0]->getType() === null
+ || $parameters[0]->getType()->getName() !== Request::class) {
+ throw new ErrorControllerException(
+ "La méthode <$action> du controller <$controller> doit avoir un paramètre de type Request"
+ );
+ }
+
+ // On vérifie que le type de retour est compatible avec le framework : string ou null
+ $returnType = $reflection->getReturnType();
+ if ($returnType !== null
+ && ($returnType instanceof \ReflectionNamedType === false
+ || in_array($returnType->getName(), ['string', 'mixed'], true) === false)) {
+ throw new ErrorControllerException(
+ "La méthode <$action> du controller <$controller> doit retourner une chaine de caractères (string) ou null"
+ );
+ }
+
+ // Appel de la méthode du controller avec la requête HTTP
+ return (new $controller())->$action($request);
+ }
}
diff --git a/src/EduFramework/Core/Controller/FastRouteCore.php b/src/EduFramework/Core/Controller/FastRouteCore.php
index c201537..0243c6c 100644
--- a/src/EduFramework/Core/Controller/FastRouteCore.php
+++ b/src/EduFramework/Core/Controller/FastRouteCore.php
@@ -16,6 +16,7 @@
use Studoo\EduFramework\Core\Controller\Error\HttpError404Controller;
use Studoo\EduFramework\Core\Controller\Error\HttpError405Controller;
use Studoo\EduFramework\Core\Controller\Error\HttpErrorDefaultController;
+use Studoo\EduFramework\Core\Exception\ErrorRouteConfigNotExistException;
use Symfony\Component\Yaml\Yaml;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
@@ -51,9 +52,20 @@ public function __construct()
* Methode pour charger les routes depuis un fichier de configuration (Config/route.yaml)
* @param string $pathConfigFile Chemin vers le fichier de configuration
* @return $this
+ * @throws ErrorRouteConfigNotExistException
*/
public function loadRouteConfig(string $pathConfigFile): self
{
+ // On vérifie que le fichier de configuration des routes existe
+ // pour renvoyer une erreur explicite plutôt qu'une exception du composant Yaml
+ if (is_file($pathConfigFile . 'routes.yaml') === false) {
+ throw new ErrorRouteConfigNotExistException(
+ "Le fichier de configuration des routes <" . $pathConfigFile . "routes.yaml> n'existe pas. "
+ . "Vérifiez la configuration 'route_config_path' et la casse du dossier "
+ . "(Exemple: app/Config et non app/config, les systèmes de fichiers Linux sont sensibles à la casse)"
+ );
+ }
+
$fileData = Yaml::parseFile($pathConfigFile . 'routes.yaml');
if (is_array($fileData)) {
foreach ($fileData as $routeConfig) {
@@ -70,7 +82,10 @@ public function loadRouteConfig(string $pathConfigFile): self
* Cette route peut avoir des méthodes HTTP associées (GET, POST, PUT, DELETE, ...)
* @param string|array $httpMethod (GET, POST, PUT, DELETE, ...)
* @param string $uri La route à appeler
- * @param string $controller Nom du controller à appeler
+ * @param string $controller Nom du controller à appeler.
+ * Deux formes possibles :
+ * - "Controller\MedecinController" : appel de la méthode execute() par défaut
+ * - "Controller\MedecinController::index" : appel de la méthode index()
* @return $this
*/
public function addRoute(string|array $httpMethod, string $uri, string $controller): self
@@ -92,6 +107,7 @@ public function getDispatcher(): Dispatcher
/**
* Methode pour récupérer la classe controller à appeler
* Elle retourne le résultat de la méthode execute() du controller
+ * ou de la méthode définie dans la route Exemple: Controller\MedecinController::index
* @return string|null
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
@@ -146,15 +162,32 @@ public function getRoute(): string|null
break;
// Si la route est trouvée alors j'appelle le controller correspondant
case Dispatcher::FOUND:
+ // Le handler peut être de deux formes :
+ // - "Controller\MedecinController" : la méthode execute() sera appelée par défaut
+ // - "Controller\MedecinController::index" : la méthode index() sera appelée
+ [$controller, $action] = $this->resolveHandler($routeInfo[1]);
+
// J'ajoute le nom de la classe controller à appeler
// et les paramètres de la route à l'objet requête HTTP
- $request->setHander($routeInfo[1])->setVars($_GET);
- $request->setHander($routeInfo[1])->setVars($_POST);
- $request->setHander($routeInfo[1])->setVars($routeInfo[2]);
-
- // J'appelle la méthode execute() du controller
- // et je récupère la vue à afficher
- $returnView = $this->buildController($request->getHander())->execute($request);
+ $request->setHander($controller);
+ $request->setVars($_GET);
+ $request->setVars($_POST);
+ $request->setVars($routeInfo[2]);
+ // Les fichiers téléversés via un formulaire multipart/form-data
+ $request->setFiles($_FILES);
+
+ if ($action === null) {
+ // Sans méthode explicite dans la route, le controller doit implémenter l'interface ControllerInterface
+ // J'appelle la méthode execute() du controller
+ // et je récupère la vue à afficher
+ $returnView = $this->buildController($request->getHander())->execute($request);
+ } else {
+ // Une méthode explicite est définie dans la route Exemple: Controller\MedecinController::index
+ // La validation de la méthode est faite dans la méthode callAction()
+ // J'appelle la méthode du controller et je récupère la vue à afficher
+ $request->setAction($action);
+ $returnView = $this->callAction($request);
+ }
break;
default:
$returnView = (new HttpErrorDefaultController())->execute($request);
diff --git a/src/EduFramework/Core/Controller/Request.php b/src/EduFramework/Core/Controller/Request.php
index d30feff..175c884 100644
--- a/src/EduFramework/Core/Controller/Request.php
+++ b/src/EduFramework/Core/Controller/Request.php
@@ -38,12 +38,27 @@ class Request
*/
private string $hander = '';
+ /**
+ * La méthode du controller à appeler
+ * Par défaut, c'est la méthode execute() du controller
+ * @var string $action
+ */
+ private string $action = 'execute';
+
/**
* Les variables de la requête HTTP
* @var array $vars
*/
private array $vars = [];
+ /**
+ * Les fichiers téléversés de la requête HTTP (structure normalisée)
+ * Chaque clé du formulaire pointe vers une liste de fichiers
+ * Exemple: $files['fichier'][0]['name']
+ * @var array $files
+ */
+ private array $files = [];
+
/**
* Une requete HTTP a obligatoirement une route et une méthode HTTP (GET, POST ...)
* Cet objet est présent dans la méthode execute() d'un controller
@@ -107,6 +122,29 @@ public function setHander(string $hander): Request
return $this;
}
+ /**
+ * Renvoi la méthode du controller qui est associée à la requête HTTP
+ * Par défaut, c'est la méthode execute() du controller
+ * @return string
+ */
+ public function getAction(): string
+ {
+ return $this->action;
+ }
+
+ /**
+ * Permet de définir la méthode du controller à appeler
+ * La méthode est définie dans le fichier de configuration des routes (Config/routes.yaml)
+ * Exemple: Controller\MedecinController::index
+ * @param string $action Le nom de la méthode du controller
+ * @return Request
+ */
+ public function setAction(string $action): Request
+ {
+ $this->action = $action;
+ return $this;
+ }
+
/**
* Permet de récupérer une variable de la requête HTTP
* @param string $key Le nom de la variable
@@ -137,6 +175,150 @@ public function setVars(array $vars): Request
return $this;
}
+ /**
+ * Permet de définir les fichiers téléversés de la requête HTTP ($_FILES)
+ * La structure native de $_FILES est normalisée :
+ * chaque clé du formulaire devient une liste de fichiers,
+ * que le champ HTML soit simple ()
+ * ou multiple ()
+ * @param array $files Les fichiers téléversés (Exemple: $_FILES)
+ * @return Request
+ */
+ public function setFiles(array $files): Request
+ {
+ foreach ($files as $key => $file) {
+ if (is_array($file['name'] ?? null) === true) {
+ // Champ multi-fichiers Exemple:
+ $listeFichiers = [];
+ foreach (array_keys($file['name']) as $index) {
+ $listeFichiers[] = [
+ 'name' => $file['name'][$index],
+ 'type' => $file['type'][$index],
+ 'size' => $file['size'][$index],
+ 'tmp_name' => $file['tmp_name'][$index],
+ 'error' => $file['error'][$index],
+ ];
+ }
+ $this->files[$key] = $listeFichiers;
+ } else {
+ // Champ simple
+ $this->files[$key] = [
+ [
+ 'name' => $file['name'],
+ 'type' => $file['type'],
+ 'size' => $file['size'],
+ 'tmp_name' => $file['tmp_name'],
+ 'error' => $file['error'],
+ ],
+ ];
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Renvoi l'ensemble des fichiers téléversés de la requête HTTP (structure normalisée)
+ * @return array
+ */
+ public function getFiles(): array
+ {
+ return $this->files;
+ }
+
+ /**
+ * Renvoi la liste des fichiers téléversés pour un champ du formulaire
+ * La structure native de $_FILES est normalisée :
+ * la méthode retourne toujours une liste de fichiers, même pour un champ simple
+ * @param string $key Le nom du champ du formulaire
+ * @return array|null La liste des fichiers ou null si le champ n'existe pas
+ */
+ public function getFile(string $key): array|null
+ {
+ return $this->files[$key] ?? null;
+ }
+
+ /**
+ * Indique si le formulaire a envoyé un fichier pour ce champ
+ * Attention : un champ laissé vide génère une entrée avec error = UPLOAD_ERR_NO_FILE (4)
+ * hasFile() retourne true mais isValid() retourne false
+ * @param string $key Le nom du champ du formulaire
+ * @return bool
+ */
+ public function hasFile(string $key): bool
+ {
+ return array_key_exists($key, $this->files);
+ }
+
+ /**
+ * Vérifie que le(s) fichier(s) téléversé(s) du champ sont valides (error = UPLOAD_ERR_OK)
+ * @param string $key Le nom du champ du formulaire
+ * @param int|null $index L'index du fichier à vérifier (champ multi-fichiers),
+ * null pour vérifier tous les fichiers du champ
+ * @return bool
+ */
+ public function isValid(string $key, int|null $index = null): bool
+ {
+ $fichiers = $this->getFile($key);
+
+ if ($fichiers === null) {
+ return false;
+ }
+
+ if ($index === null) {
+ foreach ($fichiers as $fichier) {
+ if ($fichier['error'] !== UPLOAD_ERR_OK) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ return ($fichiers[$index]['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK;
+ }
+
+ /**
+ * Renvoi l'extension du fichier téléversé (en minuscules)
+ * @param string $key Le nom du champ du formulaire
+ * @param int $index L'index du fichier (champ multi-fichiers)
+ * @return string|null L'extension (Exemple: csv) ou null
+ */
+ public function getExtension(string $key, int $index = 0): string|null
+ {
+ $fichier = $this->getFile($key)[$index] ?? null;
+
+ if ($fichier === null || $fichier['error'] !== UPLOAD_ERR_OK) {
+ return null;
+ }
+
+ $extension = pathinfo($fichier['name'], PATHINFO_EXTENSION);
+
+ return ($extension !== '' ? strtolower($extension) : null);
+ }
+
+ /**
+ * Déplace un fichier téléversé vers sa destination finale
+ * Cette méthode encapsule la fonction PHP move_uploaded_file()
+ * en vérifiant au préalable que le téléversement est valide
+ * @param string $key Le nom du champ du formulaire
+ * @param string $destination Le chemin complet du fichier de destination
+ * @param int $index L'index du fichier (champ multi-fichiers)
+ * @return bool true si le fichier a été déplacé, false sinon
+ */
+ public function move(string $key, string $destination, int $index = 0): bool
+ {
+ $fichier = $this->getFile($key)[$index] ?? null;
+
+ if ($fichier === null || $fichier['error'] !== UPLOAD_ERR_OK) {
+ return false;
+ }
+
+ if (is_uploaded_file($fichier['tmp_name']) === false) {
+ return false;
+ }
+
+ return move_uploaded_file($fichier['tmp_name'], $destination);
+ }
+
/**
* Renvoi la méthode HTTP de la requête
* @return string
diff --git a/src/EduFramework/Core/Controller/Route.php b/src/EduFramework/Core/Controller/Route.php
index 320e29a..0ae8f1c 100644
--- a/src/EduFramework/Core/Controller/Route.php
+++ b/src/EduFramework/Core/Controller/Route.php
@@ -14,6 +14,7 @@
use FastRoute\RouteParser\Std;
use Studoo\EduFramework\Core\ConfigCore;
use Studoo\EduFramework\Core\Exception\BadRouteException;
+use Studoo\EduFramework\Core\Exception\ErrorRouteConfigNotExistException;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
@@ -76,11 +77,24 @@ public function getRouteInfo(): array
/**
* Récupération du fichier config/routes.yaml
- * @return array|bool Tableau des routes ou false si le fichier n'existe pas
+ * @return array|bool Tableau des routes ou false si le fichier ne contient pas de routes
+ * @throws ErrorRouteConfigNotExistException
*/
public function loadRoute(): array|bool
{
- $fileData = Yaml::parseFile(ConfigCore::getConfig("route_config_path") . 'routes.yaml');
+ $pathFile = ConfigCore::getConfig("route_config_path") . 'routes.yaml';
+
+ // On vérifie que le fichier de configuration des routes existe
+ // pour renvoyer une erreur explicite plutôt qu'une exception du composant Yaml
+ if (is_file($pathFile) === false) {
+ throw new ErrorRouteConfigNotExistException(
+ "Le fichier de configuration des routes <" . $pathFile . "> n'existe pas. "
+ . "Vérifiez la configuration 'route_config_path' et la casse du dossier "
+ . "(Exemple: app/Config et non app/config, les systèmes de fichiers Linux sont sensibles à la casse)"
+ );
+ }
+
+ $fileData = Yaml::parseFile($pathFile);
if (is_array($fileData)) {
return $fileData;
}
diff --git a/src/EduFramework/Core/Exception/ErrorRouteConfigNotExistException.php b/src/EduFramework/Core/Exception/ErrorRouteConfigNotExistException.php
new file mode 100644
index 0000000..44a2b07
--- /dev/null
+++ b/src/EduFramework/Core/Exception/ErrorRouteConfigNotExistException.php
@@ -0,0 +1,27 @@
+ false, 'debug' => true]
+ [
+ 'cache' => ConfigCore::getConfig('cache_path') . 'twig',
+ 'auto_reload' => true,
+ 'debug' => true,
+ ]
);
// Ajoutez l'extension Debug
self::$twig->addExtension(new DebugExtension());
diff --git a/tests/Command/ClearCacheCommandTest.php b/tests/Command/ClearCacheCommandTest.php
new file mode 100644
index 0000000..5b63a2e
--- /dev/null
+++ b/tests/Command/ClearCacheCommandTest.php
@@ -0,0 +1,76 @@
+cachePath = sys_get_temp_dir() . '/edu-framework-test-cache-' . uniqid() . '/';
+
+ // Simulation du contenu d'un dossier de cache (Exemple: le cache TWIG)
+ mkdir($this->cachePath . 'twig', 0777, true);
+ file_put_contents($this->cachePath . 'twig/template.html.php', 'cache compile');
+ file_put_contents($this->cachePath . 'fichier-test.txt', 'cache');
+
+ (new ConfigCore(['cache_path' => $this->cachePath]));
+ $application = new Application(ConfigCore::getConfig('name'), ConfigCore::getConfig('version'));
+ $application->add(new ClearCacheCommand());
+ $command = $application->find('cache:clear');
+ $this->commandeTester = new CommandTester($command);
+ }
+
+ protected function tearDown(): void
+ {
+ // Nettoyage du dossier de cache temporaire
+ (new Filesystem())->remove($this->cachePath);
+ $this->commandeTester = null;
+ }
+
+ public function testCommandClearCache(): void
+ {
+ $this->commandeTester->execute([]);
+ $output = $this->commandeTester->getDisplay();
+
+ $this->assertStringContainsString('Suppression du cache', $output);
+ $this->assertStringContainsString('Le cache a été supprimé', $output);
+
+ // Le contenu du dossier de cache est supprimé
+ $this->assertFileDoesNotExist($this->cachePath . 'fichier-test.txt');
+ $this->assertFileDoesNotExist($this->cachePath . 'twig/template.html.php');
+
+ // Le dossier de cache lui-même est conservé
+ $this->assertDirectoryExists($this->cachePath);
+ }
+
+ public function testCommandClearCacheWithDirNotExist(): void
+ {
+ // Le dossier de cache n'existe pas
+ (new ConfigCore(['cache_path' => sys_get_temp_dir() . '/edu-framework-test-cache-inexistant-' . uniqid() . '/']));
+
+ $application = new Application(ConfigCore::getConfig('name'), ConfigCore::getConfig('version'));
+ $application->add(new ClearCacheCommand());
+ $command = $application->find('cache:clear');
+ $commandeTester = new CommandTester($command);
+
+ $commandeTester->execute([]);
+ $output = $commandeTester->getDisplay();
+
+ $this->assertStringContainsString('Aucun cache à supprimer', $output);
+ }
+}
diff --git a/tests/Config/routes.yaml b/tests/Config/routes.yaml
index 6714cb9..bae223a 100644
--- a/tests/Config/routes.yaml
+++ b/tests/Config/routes.yaml
@@ -21,4 +21,16 @@ userUpdate:
userName:
uri: /user/{id:\d+}[/{name}]
controller: Controller\UserController
+ httpMethod: [GET,POST]
+medecin:
+ uri: /medecin
+ controller: Controller\MedecinController::index
+ httpMethod: [GET]
+medecin_new:
+ uri: /medecin/new
+ controller: Controller\MedecinController::new
+ httpMethod: [GET,POST]
+medecin_import:
+ uri: /medecin/import
+ controller: Controller\MedecinController::import
httpMethod: [GET,POST]
\ No newline at end of file
diff --git a/tests/Controller/FastRouteCoreTest.php b/tests/Controller/FastRouteCoreTest.php
index 16df16b..d426b8f 100644
--- a/tests/Controller/FastRouteCoreTest.php
+++ b/tests/Controller/FastRouteCoreTest.php
@@ -6,6 +6,8 @@
use PHPUnit\Framework\TestCase;
use Studoo\EduFramework\Core\ConfigCore;
use Studoo\EduFramework\Core\Controller\FastRouteCore;
+use Studoo\EduFramework\Core\Exception\ErrorControllerException;
+use Studoo\EduFramework\Core\Exception\ErrorRouteConfigNotExistException;
use Studoo\EduFramework\Core\View\TwigCore;
class FastRouteCoreTest extends TestCase
@@ -18,6 +20,9 @@ public function setUp(): void
// de la barre de debug dans le rendu et casserait les hashs ci-dessous.
unset($_ENV['APP_ENV'], $_SERVER['APP_ENV']);
+ // Neutralise les fichiers téléversés pour isoler chaque test
+ $_FILES = [];
+
(new ConfigCore([
'twig_path' => __DIR__ . '/../../app/Template',
'route_config_path' => __DIR__ . "/../Config/"
@@ -77,6 +82,14 @@ public function testLoadRoutesMatchSuccess()
$this->assertEquals('a2bd992a139c4ec5c9a3cac42e646dfc4a8e026c719d37924f193e7ff06fe520', hash('sha256', $route->getRoute()));
}
+ public function testLoadRouteConfigWithFileNotExist()
+ {
+ $route = new FastRouteCore();
+
+ $this->expectException(ErrorRouteConfigNotExistException::class);
+ $route->loadRouteConfig(__DIR__ . '/../ConfigNotExist/');
+ }
+
public function testLoadRoutesMatchSuccessWithExceptionNotFound()
{
$route = new FastRouteCore();
@@ -118,4 +131,85 @@ public function testLinkToGetnametopathInTwigUserid()
$this->assertEquals('ab2550a7609a2997e9aaef4b49a12388d8978d5507d970cb45fa96fec99e768b', hash('sha256', $route->getRoute()));
}
+
+ public function testRouteWithMethodExplicitIndex()
+ {
+ $route = new FastRouteCore();
+ $route->loadRouteConfig(__DIR__ . '/../Config/');
+
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $_SERVER['REQUEST_URI'] = '/medecin';
+
+ $this->assertEquals('ed13c9c6c5ed3833bd1af6bea040089eff6e48baefcc219a9a04f382f2532931', hash('sha256', $route->getRoute()));
+ }
+
+ public function testRouteWithMethodExplicitNew()
+ {
+ $route = new FastRouteCore();
+ $route->loadRouteConfig(__DIR__ . '/../Config/');
+
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $_SERVER['REQUEST_URI'] = '/medecin/new';
+
+ $this->assertEquals('e2f7b9a3c530c751e475fca16a4e46e95e249ceca802a947adc17e50bfb96a91', hash('sha256', $route->getRoute()));
+ }
+
+ public function testRouteWithMethodNotExist()
+ {
+ $route = new FastRouteCore();
+ $route->addRoute('GET', '/medecin/error', "Controller\MedecinController::inconnue");
+
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $_SERVER['REQUEST_URI'] = '/medecin/error';
+
+ $this->expectException(ErrorControllerException::class);
+ $this->expectExceptionMessage("La méthode n'existe pas dans le controller ");
+ $route->getRoute();
+ }
+
+ public function testRouteWithMethodNotPublic()
+ {
+ $route = new FastRouteCore();
+ $route->addRoute('GET', '/medecin/error', "Controller\MedecinController::testPrivate");
+
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $_SERVER['REQUEST_URI'] = '/medecin/error';
+
+ $this->expectException(ErrorControllerException::class);
+ $route->getRoute();
+ }
+
+ public function testRouteImportWithFileInvalid()
+ {
+ $route = new FastRouteCore();
+ $route->loadRouteConfig(__DIR__ . '/../Config/');
+
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $_SERVER['REQUEST_URI'] = '/medecin/import';
+ $_FILES = [
+ 'fichier' => [
+ 'name' => 'medecins.csv',
+ 'type' => 'text/csv',
+ 'size' => 123,
+ 'tmp_name' => '/tmp/phpXYZ',
+ 'error' => UPLOAD_ERR_INI_SIZE,
+ ],
+ ];
+
+ // Le fichier téléversé est en erreur : la vue affiche la branche "fichier invalide"
+ $this->assertEquals('c3a41ed55dbc9d7f5798e52b5140f32adb704b37859824845ad92b82c54d9794', hash('sha256', $route->getRoute()));
+ }
+
+ public function testRouteImportWithoutFile()
+ {
+ $route = new FastRouteCore();
+ $route->loadRouteConfig(__DIR__ . '/../Config/');
+
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $_SERVER['REQUEST_URI'] = '/medecin/import';
+ $_FILES = [];
+
+ // Aucun fichier envoyé : la vue affiche la branche "aucun fichier"
+ $this->assertEquals('b619f84de911fb72a6459e15946fbe934a00a63fcf3a7d6d23c9bd0506c3137a', hash('sha256', $route->getRoute()));
+ }
}
diff --git a/tests/Controller/RequestTest.php b/tests/Controller/RequestTest.php
index e2d6562..b65fdfd 100644
--- a/tests/Controller/RequestTest.php
+++ b/tests/Controller/RequestTest.php
@@ -66,4 +66,166 @@ public function testGetHander()
$request->setHander("Controller\HomeController");
$this->assertEquals("Controller\HomeController", $request->getHander());
}
+
+ public function testGetActionDefault()
+ {
+ $request = new Request("/test", "GET");
+ $this->assertEquals("execute", $request->getAction());
+ }
+
+ public function testSetAction()
+ {
+ $request = new Request("/test", "GET");
+ $request->setAction("index");
+ $this->assertEquals("index", $request->getAction());
+ }
+
+ public function testSetActionAndHander()
+ {
+ $request = new Request("/test", "GET");
+ $request->setHander("Controller\MedecinController")->setAction("index");
+ $this->assertEquals("Controller\MedecinController", $request->getHander());
+ $this->assertEquals("index", $request->getAction());
+ }
+
+ public function testSetFilesSimpleField()
+ {
+ $request = new Request("/test", "POST");
+ $request->setFiles([
+ 'fichier' => [
+ 'name' => 'medecins.csv',
+ 'type' => 'text/csv',
+ 'size' => 123,
+ 'tmp_name' => '/tmp/phpXYZ',
+ 'error' => UPLOAD_ERR_OK,
+ ],
+ ]);
+
+ // La structure est normalisée : une liste de fichiers même pour un champ simple
+ $this->assertEquals(
+ [[
+ 'name' => 'medecins.csv',
+ 'type' => 'text/csv',
+ 'size' => 123,
+ 'tmp_name' => '/tmp/phpXYZ',
+ 'error' => UPLOAD_ERR_OK,
+ ]],
+ $request->getFile('fichier')
+ );
+ $this->assertNull($request->getFile('inconnu'));
+ $this->assertTrue($request->hasFile('fichier'));
+ $this->assertFalse($request->hasFile('inconnu'));
+ $this->assertEquals(1, count($request->getFiles()));
+ }
+
+ public function testSetFilesMultipleField()
+ {
+ $request = new Request("/test", "POST");
+ $request->setFiles([
+ 'galerie' => [
+ 'name' => ['a.png', 'b.png'],
+ 'type' => ['image/png', 'image/png'],
+ 'size' => [123, 456],
+ 'tmp_name' => ['/tmp/phpA', '/tmp/phpB'],
+ 'error' => [UPLOAD_ERR_OK, UPLOAD_ERR_OK],
+ ],
+ ]);
+
+ // La structure native de $_FILES en multi-upload est normalisée en liste de fichiers
+ $this->assertEquals(
+ [
+ ['name' => 'a.png', 'type' => 'image/png', 'size' => 123, 'tmp_name' => '/tmp/phpA', 'error' => UPLOAD_ERR_OK],
+ ['name' => 'b.png', 'type' => 'image/png', 'size' => 456, 'tmp_name' => '/tmp/phpB', 'error' => UPLOAD_ERR_OK],
+ ],
+ $request->getFile('galerie')
+ );
+ }
+
+ public function testIsValid()
+ {
+ $request = new Request("/test", "POST");
+ $request->setFiles([
+ 'fichier' => [
+ 'name' => 'medecins.csv',
+ 'type' => 'text/csv',
+ 'size' => 123,
+ 'tmp_name' => '/tmp/phpXYZ',
+ 'error' => UPLOAD_ERR_OK,
+ ],
+ ]);
+ $this->assertTrue($request->isValid('fichier'));
+ $this->assertTrue($request->isValid('fichier', 0));
+ $this->assertFalse($request->isValid('inconnu'));
+ $this->assertFalse($request->isValid('fichier', 10));
+ }
+
+ public function testIsValidWithError()
+ {
+ $request = new Request("/test", "POST");
+ $request->setFiles([
+ 'fichier' => [
+ 'name' => '',
+ 'type' => '',
+ 'size' => 0,
+ 'tmp_name' => '',
+ 'error' => UPLOAD_ERR_NO_FILE,
+ ],
+ ]);
+ // Champ laissé vide : hasFile() vrai mais isValid() faux
+ $this->assertTrue($request->hasFile('fichier'));
+ $this->assertFalse($request->isValid('fichier'));
+ }
+
+ public function testGetExtension()
+ {
+ $request = new Request("/test", "POST");
+ $request->setFiles([
+ 'fichier' => [
+ 'name' => 'medecins.CSV',
+ 'type' => 'text/csv',
+ 'size' => 123,
+ 'tmp_name' => '/tmp/phpXYZ',
+ 'error' => UPLOAD_ERR_OK,
+ ],
+ ]);
+ $this->assertEquals('csv', $request->getExtension('fichier'));
+ $this->assertNull($request->getExtension('inconnu'));
+ }
+
+ public function testMoveWithFieldNotExist()
+ {
+ $request = new Request("/test", "POST");
+ $this->assertFalse($request->move('inconnu', '/tmp/destination.csv'));
+ }
+
+ public function testMoveWithError()
+ {
+ $request = new Request("/test", "POST");
+ $request->setFiles([
+ 'fichier' => [
+ 'name' => 'medecins.csv',
+ 'type' => 'text/csv',
+ 'size' => 123,
+ 'tmp_name' => '/tmp/phpXYZ',
+ 'error' => UPLOAD_ERR_INI_SIZE,
+ ],
+ ]);
+ $this->assertFalse($request->move('fichier', '/tmp/destination.csv'));
+ }
+
+ public function testMoveWithNotUploadedFile()
+ {
+ $request = new Request("/test", "POST");
+ $request->setFiles([
+ 'fichier' => [
+ 'name' => 'medecins.csv',
+ 'type' => 'text/csv',
+ 'size' => 123,
+ 'tmp_name' => '/tmp/fichier-factice.csv',
+ 'error' => UPLOAD_ERR_OK,
+ ],
+ ]);
+ // Sans vrai téléversement, is_uploaded_file() échoue et move() retourne false
+ $this->assertFalse($request->move('fichier', '/tmp/destination.csv'));
+ }
}
diff --git a/tests/Controller/RouteTest.php b/tests/Controller/RouteTest.php
index adcd775..ac306df 100644
--- a/tests/Controller/RouteTest.php
+++ b/tests/Controller/RouteTest.php
@@ -3,6 +3,7 @@
namespace Controller;
use Studoo\EduFramework\Core\Exception\BadRouteException;
+use Studoo\EduFramework\Core\Exception\ErrorRouteConfigNotExistException;
use PHPUnit\Framework\TestCase;
use Studoo\EduFramework\Core\ConfigCore;
use Studoo\EduFramework\Core\Controller\Route;
@@ -89,4 +90,17 @@ public function testGetListRouteNameForException()
$this->expectException(BadRouteException::class);
$this->assertEquals('/user/1', $route->getNameToPath('userNameError', ['id' => 2]));
}
+
+ public function testLoadRouteWithFileNotExist()
+ {
+ (new ConfigCore([
+ 'twig_path' => __DIR__ . '/../../app/Template',
+ 'route_config_path' => __DIR__ . "/../ConfigNotExist/"
+ ]));
+
+ $route = new Route();
+
+ $this->expectException(ErrorRouteConfigNotExistException::class);
+ $route->getRouteInfo();
+ }
}
\ No newline at end of file