Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
/vendor/
/var/
# Composer
/composer.phar
composer.phar

# IDE configuration
/.idea/**
/.vscode/**
.idea/**
.vscode/**
.DS_Store

# Testing @ QA
Expand All @@ -16,12 +16,12 @@
.codacy/logs/

# Environment
/.env
/clover.xml
/.codacy
.env
clover.xml
.codacy

# Documentation
venv/**
/venv/**
.cache/**
site/**
.DS_Store
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

**Présentation des versions du framework Edu Framework**

## v2.4.0 - 08/10/2026

**new features**

- Un controller peut maintenant gérer plusieurs routes via plusieurs méthodes : la clé `controller` du fichier "app/Config/routes.yaml" accepte la syntaxe `Controller\VilleController::index`. Sans méthode explicite, la méthode `execute()` est appelée comme avant (100 % rétro-compatible)
- La méthode appelée est validée par le framework (publique, non statique, paramètre de type Request, retour string|null) et lève une `ErrorControllerException` avec un message explicite en cas d'erreur
- [#75](https://github.com/studoo-app/edu-framework/issues/75) Gestion des fichiers téléversés (`$_FILES`) dans `Request` : normalisation automatique de la structure (champ simple et multi-fichiers), méthodes `hasFile()`, `getFile()`, `getFiles()`, `isValid()`, `getExtension()` et `move()`, démo applicative `/medecin/import` et documentation complète
- [#64](https://github.com/studoo-app/edu-framework/issues/64) Activation du cache TWIG : les templates compilés sont stockés dans `var/cache/twig` (nouvelle clé de configuration `cache_path`), recompilation automatique des templates modifiés (`auto_reload`)
- [#65](https://github.com/studoo-app/edu-framework/issues/65) Nouvelle commande `php bin/edu cache:clear` pour supprimer le cache de l'application (dossier `var/cache`)
- Documentation : nouvelle section "Un controller, plusieurs routes" dans [docs/build/controller-edu.md](docs/build/controller-edu.md)

**bug Fixes**

- Le fichier de configuration des routes est vérifié avant lecture : une exception `ErrorRouteConfigNotExistException` avec un message explicite (chemin attendu, configuration `route_config_path`, sensibilité à la casse sur Linux) remplace le fatal error du composant Yaml quand "routes.yaml" est introuvable

## v2.3.2 - 05/10/2026

**new features**
Expand Down
12 changes: 12 additions & 0 deletions app/Config/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
86 changes: 86 additions & 0 deletions app/Controller/MedecinController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace Controller;

use Studoo\EduFramework\Core\ConfigCore;
use Studoo\EduFramework\Core\Controller\Request;
use Studoo\EduFramework\Core\View\TwigCore;

class MedecinController
{
public function index(Request $request): string|null
{
return TwigCore::getEnvironment()->render('medecin/index.html.twig',
[
"titre" => 'MedecinController - index',
"request" => $request
]
);
}

public function new(Request $request): string|null
{
return TwigCore::getEnvironment()->render('medecin/new.html.twig',
[
"titre" => 'MedecinController - new',
"request" => $request
]
);
}

/**
* Démo de gestion d'un téléversement de fichier (upload)
* GET : affiche le formulaire d'import
* POST : contrôle le fichier téléversé puis le déplace dans le dossier public/upload/
*/
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) {
// Sécurité : liste blanche des extensions autorisées
$erreur = "Extension non autorisée (seuls les fichiers csv et txt sont acceptés)";
} else {
// Le dossier d'upload est créé s'il n'existe pas
$dossierUpload = ConfigCore::getConfig('base_path') . 'public/upload';
if (is_dir($dossierUpload) === false) {
mkdir($dossierUpload, 0777, true);
}

// Sécurité : le nom du fichier est maîtrisé côté serveur
$destination = $dossierUpload . '/medecin-import-' . uniqid() . '.'
. $request->getExtension('fichier');

if ($request->move('fichier', $destination) === true) {
$message = "Le fichier a bien été téléversé : " . basename($destination);
} else {
$erreur = "Problème lors de l'enregistrement du fichier";
}
}
}

return TwigCore::getEnvironment()->render('medecin/import.html.twig',
[
"titre" => 'MedecinController - import',
"request" => $request,
"message" => $message,
"erreur" => $erreur
]
);
}

/**
* Méthode privée pour les tests de validation du framework (méthode non publique)
* @return string|null
*/
private function testPrivate(Request $request): string|null
{
return null;
}
}
19 changes: 19 additions & 0 deletions app/Template/medecin/import.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "base.html.twig" %}

{% block title %}{{ titre }}{% endblock %}

{% block content %}
<h1>{{ titre }}</h1>

{% if message %}
<p style="color: green;">{{ message }}</p>
{% endif %}
{% if erreur %}
<p style="color: red;">{{ erreur }}</p>
{% endif %}

<form action="{{ getNameToPath('medecin_import') }}" method="post" enctype="multipart/form-data">
<input type="file" name="fichier">
<input type="submit" value="Importer">
</form>
{% endblock %}
7 changes: 7 additions & 0 deletions app/Template/medecin/index.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% extends "base.html.twig" %}

{% block title %}{{ titre }}{% endblock %}

{% block content %}
<h1>{{ titre }}</h1>
{% endblock %}
7 changes: 7 additions & 0 deletions app/Template/medecin/new.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% extends "base.html.twig" %}

{% block title %}{{ titre }}{% endblock %}

{% block content %}
<h1>{{ titre }}</h1>
{% endblock %}
18 changes: 9 additions & 9 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -32,7 +32,7 @@ services:
- 8082:3000
volumes:
- ./var/dbgate:/root/.dbgate
- ./var/sqlite:/home/jan
- ./var/logs:/home/jan
environment:
CONNECTIONS: con1,con2

Expand All @@ -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:
Expand Down
136 changes: 136 additions & 0 deletions docs/boost/resquet.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<form action="/medecin/import" method="post" enctype="multipart/form-data">
<input type="file" name="fichier">
<input type="submit" value="Importer">
</form>
```

### 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` |
|---|---|
| `<input type="file" name="fichier">` | `$_FILES['fichier']['name']` est une **chaine** |
| `<input type="file" name="fichiers[]">` | `$_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é
6 changes: 6 additions & 0 deletions docs/boost/route.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand Down
Loading
Loading