À la fin de cette séance, vous serez capables de :
-
Expliquer les principes d’une API REST (ressource, routes, JSON, verbes HTTP, statuts…).
-
Installer et utiliser un framework PHP minimaliste : Slim (structure MVC simplifiée, routes, contrôleurs).
-
Utiliser Medoo, un micro-ORM simple pour interagir avec une base MySQL.
-
Créer une API REST fonctionnelle capable de :
- Envoyer un fichier (upload)
- Lister tous les fichiers
- Afficher les informations d’un fichier
- Télécharger un fichier
- Supprimer un fichier
- Consulter des statistiques (taille totale / quota)
-
Tester correctement une API avec Postman.
-
Comprendre comment cette API pourra être utilisée plus tard → par un client JavaFX ou une interface web.
Une API est une interface permettant à deux programmes de communiquer entre eux. Ici : un serveur PHP communiquera avec un client JavaFX ou une appli web.
REST repose sur 4 idées principales :
C’est un type d’objet que l’on manipule.
Dans ce TP, notre ressource s’appelle File (un fichier).
Exemples :
| Action | Méthode | URI |
|---|---|---|
| Lister les fichiers | GET | /files |
| Voir un fichier | GET | /files/{id} |
| Uploader un fichier | POST | /files |
| Télécharger | GET | /files/{id}/download |
| Supprimer | DELETE | /files/{id} |
GET→ lirePOST→ créerPUT/PATCH→ modifierDELETE→ supprimer
Exemple :
{
"id": 1,
"filename": "test.pdf",
"size": 23456
}200 OK→ tout va bien201 Created→ ressource créée400 Bad Request→ mauvaise requête404 Not Found→ ressource inexistante500 Internal Server Error→ erreur serveur
Appelez-le :
file-vault-api
Dans le dossier :
composer init
(Pressez ENTER pour toutes les questions.)
composer require slim/slim:"^4.12" slim/psr7:"^1.8" catfan/medoo:"^2.2"
Créez les dossiers suivants :
file-vault-api/
│── public/
│ └── index.php
│── src/
│ ├── Controller/
│ │ └── FileController.php
│ └── Model/
│ └── FileRepository.php
└── storage/
└── uploads/
Le dossier uploads servira à stocker les fichiers envoyés.
Dans phpMyAdmin / MySQL :
CREATE DATABASE file_vault;
USE file_vault;
CREATE TABLE files (
id INT AUTO_INCREMENT PRIMARY KEY,
filename VARCHAR(255) NOT NULL,
stored_name VARCHAR(255) NOT NULL,
size BIGINT NOT NULL,
mime_type VARCHAR(100) NOT NULL,
uploaded_at DATETIME NOT NULL
);
CREATE TABLE settings (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
value VARCHAR(255) NOT NULL
);
INSERT INTO settings (name, value) VALUES ('quota_bytes', '52428800'); -- 50 MoCréez le fichier :
Collez :
<?php
use Slim\Factory\AppFactory;
use Medoo\Medoo;
use App\Controller\FileController;
require __DIR__ . '/../vendor/autoload.php';
$database = new Medoo([
'type' => 'mysql',
'host' => '127.0.0.1',
'database' => 'file_vault',
'username' => 'root',
'password' => '',
]);
$app = AppFactory::create();
$app->addBodyParsingMiddleware();
$app->addRoutingMiddleware();
// Auto‑détection du base path quand l'app est servie depuis un sous‑dossier
// (ex.: /file-vault-api ou /file-vault-api/public)
$scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
$basePath = rtrim(str_ireplace('index.php', '', $scriptName), '/');
if ($basePath !== '') {
$app->setBasePath($basePath);
}
$fileController = new FileController($database);
$app->get('/files', [$fileController, 'list']);
$app->get('/files/{id}', [$fileController, 'show']);
$app->get('/files/{id}/download', [$fileController, 'download']);
$app->post('/files', [$fileController, 'upload']);
$app->delete('/files/{id}', [$fileController, 'delete']);
$app->get('/stats', [$fileController, 'stats']);
// Route d'accueil (GET /)
$app->get('/', function ($request, $response) {
$response->getBody()->write(json_encode([
'message' => 'File Vault API',
'endpoints' => [
'GET /files',
'GET /files/{id}',
'GET /files/{id}/download',
'POST /files',
'DELETE /files/{id}',
'GET /stats',
]
], JSON_PRETTY_PRINT));
return $response->withHeader('Content-Type', 'application/json');
});
$app->run();Sous Apache (Laragon, XAMPP, WAMP…), la racine web doit pointer vers le dossier public/.
Si votre VirtualHost pointe vers la racine du projet, utilisez le fichier public/.htaccess pour rediriger toutes les requêtes vers index.php.
Contenu recommandé de public/.htaccess (présent dans ce projet) :
<IfModule mod_rewrite.c>
RewriteEngine On
# Si le chemin correspond à un fichier ou dossier physique, ne pas réécrire
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Tout le reste -> index.php (Slim gère le routing)
RewriteRule ^ index.php [QSA,L]
</IfModule>
# Facultatif: assurer l’index par défaut
DirectoryIndex index.phpNotes importantes :
- Activez le module Apache
mod_rewrite. - DocumentRoot doit être
.../file-vault-api/publicou, à défaut, conservez ce.htaccess. - L’application autodétecte son base path grâce au code ajouté dans
index.php.- Exemple d’URL de base si servi depuis un sous-dossier:
http://localhost/file-vault-api/public - Exemple d’URL de base si VirtualHost pointe sur
public/:http://file-vault.local/
- Exemple d’URL de base si servi depuis un sous-dossier:
Créez le fichier :
<?php
namespace App\Model;
use Medoo\Medoo;
class FileRepository
{
private Medoo $db;
public function __construct(Medoo $db)
{
$this->db = $db;
}
public function listFiles(): array
{
return $this->db->select('files', '*');
}
public function find(int $id): ?array
{
return $this->db->get('files', '*', ['id' => $id]) ?: null;
}
public function create(array $data): int
{
$this->db->insert('files', $data);
return (int)$this->db->id();
}
public function delete(int $id): void
{
$this->db->delete('files', ['id' => $id]);
}
public function totalSize(): int
{
return (int)$this->db->sum('files', 'size') ?: 0;
}
public function quotaBytes(): int
{
return (int)$this->db->get('settings', 'value', ['name' => 'quota_bytes']);
}
}Ajouter une méthode :
public function countFiles(): intCréez :
<?php
// src/Controller/FileController.php
namespace App\Controller;
use App\Model\FileRepository;
use Medoo\Medoo;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class FileController
{
private FileRepository $files;
private string $uploadDir;
public function __construct(Medoo $db)
{
$this->files = new FileRepository($db);
$this->uploadDir = __DIR__ . '/../../storage/uploads';
}
// GET /files
public function list(Request $request, Response $response): Response
{
$data = $this->files->listFiles();
$payload = json_encode($data, JSON_PRETTY_PRINT);
$response->getBody()->write($payload);
return $response
->withHeader('Content-Type', 'application/json')
->withStatus(200);
}
// GET /files/{id}
public function show(Request $request, Response $response, array $args): Response
{
$id = (int)$args['id'];
$file = $this->files->find($id);
if (!$file) {
$response->getBody()->write(json_encode(['error' => 'File not found']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
}
$response->getBody()->write(json_encode($file, JSON_PRETTY_PRINT));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
// POST /files (upload via form-data)
public function upload(Request $request, Response $response): Response
{
$uploadedFiles = $request->getUploadedFiles();
if (!isset($uploadedFiles['file'])) {
$response->getBody()->write(json_encode(['error' => 'No file uploaded']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
$file = $uploadedFiles['file'];
if ($file->getError() !== UPLOAD_ERR_OK) {
$response->getBody()->write(json_encode(['error' => 'Upload error']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
$size = $file->getSize();
$totalSize = $this->files->totalSize();
$quota = $this->files->quotaBytes();
if ($quota > 0 && ($totalSize + $size) > $quota) {
$response->getBody()->write(json_encode(['error' => 'Quota exceeded']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(413);
}
$originalName = $file->getClientFilename();
$mimeType = $file->getClientMediaType();
$storedName = uniqid('f_', true) . '_' . $originalName;
$file->moveTo($this->uploadDir . DIRECTORY_SEPARATOR . $storedName);
$id = $this->files->create([
'filename' => $originalName,
'stored_name' => $storedName,
'size' => $size,
'mime_type' => $mimeType,
'uploaded_at' => date('Y-m-d H:i:s')
]);
$response->getBody()->write(json_encode([
'message' => 'File uploaded',
'id' => $id
], JSON_PRETTY_PRINT));
return $response->withHeader('Content-Type', 'application/json')->withStatus(201);
}
// GET /files/{id}/download
public function download(Request $request, Response $response, array $args): Response
{
$id = (int)$args['id'];
$file = $this->files->find($id);
if (!$file) {
$response->getBody()->write('File not found');
return $response->withStatus(404);
}
$path = $this->uploadDir . DIRECTORY_SEPARATOR . $file['stored_name'];
if (!file_exists($path)) {
$response->getBody()->write('File missing on disk');
return $response->withStatus(500);
}
$stream = fopen($path, 'rb');
$response->getBody()->write(stream_get_contents($stream));
fclose($stream);
return $response
->withHeader('Content-Type', $file['mime_type'])
->withHeader('Content-Disposition', 'attachment; filename="' . $file['filename'] . '"')
->withStatus(200);
}
// DELETE /files/{id}
public function delete(Request $request, Response $response, array $args): Response
{
$id = (int)$args['id'];
$file = $this->files->find($id);
if (!$file) {
$response->getBody()->write(json_encode(['error' => 'File not found']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
}
// Supprimer le fichier sur le disque
$path = $this->uploadDir . DIRECTORY_SEPARATOR . $file['stored_name'];
if (file_exists($path)) {
unlink($path);
}
// Supprimer en base
$this->files->delete($id);
$response->getBody()->write(json_encode(['message' => 'File deleted']));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
// GET /stats
public function stats(Request $request, Response $response): Response
{
$totalSize = $this->files->totalSize();
$quota = $this->files->quotaBytes();
// Exercice 1: utiliser countFiles() ici si l'étudiant l’a codée
// $count = $this->files->countFiles();
$data = [
'total_size_bytes' => $totalSize,
'quota_bytes' => $quota,
// 'file_count' => $count,
];
$response->getBody()->write(json_encode($data, JSON_PRETTY_PRINT));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
}➡️ Ce fichier contient toutes les routes REST :
- Upload
- Listing
- Affichage
- Suppression
- Statistiques
- Téléchargement
http://localhost/file-vault-api/public
- Méthode : GET
- URL :
/files - Résultat attendu : un tableau JSON.
-
Méthode : POST
-
URL :
/files -
Onglet Body :
- Type : form-data
- Clé :
file(typeFile) - Envoyez un PDF / PNG / TXT
Résultat attendu :
{
"message": "File uploaded",
"id": 1
}- Méthode : GET
- Exemple :
/files/1
Télécharge le fichier.
Supprime définitivement :
{ "message": "File deleted" }Affiche la taille totale + quota :
{
"total_size_bytes": 125000,
"quota_bytes": 52428800
}Ajouter dans /stats :
- le nombre total de fichiers →
countFiles() - le pourcentage d’utilisation du quota
Vous devez avoir :
avec les routes :
| Route | Action |
|---|---|
| GET /files | liste |
| GET /files/{id} | détail |
| POST /files | upload |
| GET /files/{id}/download | téléchargement |
| DELETE /files/{id} | suppression |
| GET /stats | statistiques |
→ capture d’écran attendue pour chaque opération.
- Ajouter une route PUT
/quotapour modifier le quota global. - Empêcher l’upload de fichiers dont certaines extensions ne sont pas autorisées.
- Ajouter une pagination dans
GET /files.
-
Slim = mini framework très simple → parfait pour comprendre MVC.
-
Medoo = micro-ORM → syntaxe ultra simple, idéal pour débuter.
-
REST = approche moderne pour exposer des services.
-
Une API REST peut être consommée par :
- une appli JavaFX (client lourd),
- une interface web (client léger),
- Postman,
- d’autres services.