Compare commits
13
Commits
12751c3859
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bac3cfeb4b | ||
|
|
6bf96beaed | ||
|
|
e2aaa5dcd4 | ||
|
|
7de8e4782b | ||
|
|
56244d2e95 | ||
|
|
a033f415b5 | ||
|
|
88800dde63 | ||
|
|
b5d1c9cf90 | ||
|
|
8f1576aa6c | ||
|
|
ea460db104 | ||
|
|
2069b66f9b | ||
|
|
8118f97b8f | ||
|
|
c68d49a48a |
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
.git
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-journal
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
data.json
|
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite-journal
|
*.sqlite-journal
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
FROM php:8.4-apache
|
||||||
|
|
||||||
|
RUN a2enmod rewrite
|
||||||
|
|
||||||
|
RUN printf '<VirtualHost *:80>\n\
|
||||||
|
DocumentRoot /var/www/html/public\n\
|
||||||
|
<Directory /var/www/html/public>\n\
|
||||||
|
AllowOverride All\n\
|
||||||
|
Require all granted\n\
|
||||||
|
</Directory>\n\
|
||||||
|
</VirtualHost>\n' > /etc/apache2/sites-available/000-default.conf
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN mkdir -p database && chown -R www-data:www-data /var/www/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
@@ -4,7 +4,6 @@ API REST en PHP natif (POO) pour gérer des liens de vidéos YouTube.
|
|||||||
|
|
||||||
- PHP natif uniquement : aucun framework, aucune dépendance, aucun Composer
|
- PHP natif uniquement : aucun framework, aucune dépendance, aucun Composer
|
||||||
- Stockage SQLite via PDO
|
- Stockage SQLite via PDO
|
||||||
- À chaque création / modification / suppression, un snapshot complet de la table est écrit dans `data.json`
|
|
||||||
|
|
||||||
## Prérequis
|
## Prérequis
|
||||||
|
|
||||||
@@ -23,7 +22,25 @@ php -m | grep -i sqlite # doit afficher pdo_sqlite et/ou sqlite3
|
|||||||
php -S localhost:8000 -t public
|
php -S localhost:8000 -t public
|
||||||
```
|
```
|
||||||
|
|
||||||
La base `database/api_tube.sqlite` et son schéma sont créés automatiquement au premier appel.
|
La base `database/api_tube.sqlite` et son schéma sont créés automatiquement au premier appel (via les migrations).
|
||||||
|
|
||||||
|
## Migrations
|
||||||
|
|
||||||
|
Les migrations sont des fichiers SQL numérotés dans `database/migrations/`, appliqués une seule fois et tracés dans la table `migrations`.
|
||||||
|
|
||||||
|
Elles sont exécutées automatiquement à la connexion à la base. Pour les appliquer manuellement (hors serveur web) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php database/migrate.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajouter une migration :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# fichier database/migrations/002_ajout_colonne.sql
|
||||||
|
echo "ALTER TABLE links ADD COLUMN visible INTEGER DEFAULT 1;" > database/migrations/002_ajout_colonne.sql
|
||||||
|
php database/migrate.php
|
||||||
|
```
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
@@ -48,16 +65,50 @@ La base `database/api_tube.sqlite` et son schéma sont créés automatiquement a
|
|||||||
|
|
||||||
`title` et `link_url` sont obligatoires ; `description` et `thumbnail` sont optionnels. `link_url` doit être une URL valide et unique.
|
`title` et `link_url` sont obligatoires ; `description` et `thumbnail` sont optionnels. `link_url` doit être une URL valide et unique.
|
||||||
|
|
||||||
## Fichier `data.json`
|
|
||||||
|
|
||||||
Régénéré à chaque POST / PUT / DELETE : contient la liste complète des liens, formatée (pretty print).
|
|
||||||
|
|
||||||
## Tests manuels
|
## Tests manuels
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash tests/curl.sh
|
bash tests/curl.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
Éléments de sécurité implémentés dans l'API :
|
||||||
|
|
||||||
|
- **Injection SQL** : toutes les requêtes avec paramètres utilisent des requêtes préparées PDO (`prepare()` + `execute()`) dans `src/LinkRepository.php`, jamais de concaténation de variables dans le SQL.
|
||||||
|
- **Validation des entrées** : champs obligatoires vérifiés, `trim()` systématique, et validation de `link_url` via `FILTER_VALIDATE_URL` (`src/LinkController.php`).
|
||||||
|
- **Pas de fuite d'informations** : toute erreur (base, rendu) renvoie une réponse 500 générique `Erreur interne du serveur.` sans stack trace ni détail (`public/index.php`). `display_errors` n'est pas activé.
|
||||||
|
- **Exposition minimale au web** : le DocumentRoot Apache (Docker) pointe vers `public/` uniquement ; `src/`, `database/` et la base SQLite ne sont pas servis publiquement. Le `.htaccess` réécrit toute requête vers `index.php`.
|
||||||
|
- **CORS restreint** : méthodes limitées à `GET, POST, PUT, DELETE, OPTIONS` et en-têtes à `Content-Type` (`public/index.php`).
|
||||||
|
- **Base hors de l'image Docker** : `*.sqlite` est exclu via `.dockerignore` et stocké dans le volume `db-data`, pas dans les layers de l'image.
|
||||||
|
- **Typage strict** : `declare(strict_types=1)` dans tous les fichiers PHP, paramètres et retours typés.
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
### Lancement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
# API accessible sur http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arrêter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down # conserve la base (volume)
|
||||||
|
docker compose down -v # supprime la base (reset complet)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Persistance
|
||||||
|
|
||||||
|
La base SQLite est stockée dans le Docker volume `db-data` : elle survit aux `down`/`up` successifs. Un `down -v` la supprime ; le prochain démarrage recrée la base via les migrations.
|
||||||
|
|
||||||
|
### Rebuild après modification du code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
## Arborescence
|
## Arborescence
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -65,10 +116,13 @@ public/
|
|||||||
index.php # point d'entrée unique
|
index.php # point d'entrée unique
|
||||||
.htaccess # réécriture vers index.php
|
.htaccess # réécriture vers index.php
|
||||||
src/
|
src/
|
||||||
Database.php # connexion PDO SQLite (+ création du schéma)
|
Database.php # connexion PDO SQLite + exécution des migrations
|
||||||
LinkRepository.php # CRUD SQL
|
LinkRepository.php # CRUD SQL
|
||||||
JsonExporter.php # écriture de data.json
|
|
||||||
LinkController.php # routage, validation, orchestration
|
LinkController.php # routage, validation, orchestration
|
||||||
database/
|
database/
|
||||||
schema.sql # schéma de la table links
|
migrate.php # runner de migrations (CLI)
|
||||||
|
migrations/ # fichiers SQL versionnés
|
||||||
|
Dockerfile # image PHP 8.4 Apache + mod_rewrite
|
||||||
|
docker-compose.yml # service web + volume db-data
|
||||||
|
.dockerignore
|
||||||
```
|
```
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
name: api-yt-links
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8000:80"
|
||||||
|
volumes:
|
||||||
|
- db-data:/var/www/html/database
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db-data:
|
||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once __DIR__ . '/../src/Database.php';
|
require_once __DIR__ . '/../src/Database.php';
|
||||||
require_once __DIR__ . '/../src/LinkRepository.php';
|
require_once __DIR__ . '/../src/LinkRepository.php';
|
||||||
require_once __DIR__ . '/../src/JsonExporter.php';
|
|
||||||
require_once __DIR__ . '/../src/LinkController.php';
|
require_once __DIR__ . '/../src/LinkController.php';
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
@@ -22,7 +21,6 @@ try {
|
|||||||
|
|
||||||
$controller = new LinkController(
|
$controller = new LinkController(
|
||||||
new LinkRepository($pdo),
|
new LinkRepository($pdo),
|
||||||
new JsonExporter($pdo, dirname(__DIR__) . '/data.json'),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||||
|
|||||||
+14
-13
@@ -8,21 +8,22 @@ final class Database
|
|||||||
|
|
||||||
public static function connect(): PDO
|
public static function connect(): PDO
|
||||||
{
|
{
|
||||||
if (self::$pdo !== null) {
|
if (self::$pdo === null) {
|
||||||
return self::$pdo;
|
self::init();
|
||||||
|
assert(self::$pdo instanceof PDO);
|
||||||
|
self::$pdo->exec((string) file_get_contents(dirname(__DIR__) . '/database/schema.sql'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$root = dirname(__DIR__);
|
|
||||||
$dsn = 'sqlite:' . $root . '/database/api_tube.sqlite';
|
|
||||||
|
|
||||||
self::$pdo = new PDO($dsn);
|
|
||||||
self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
||||||
self::$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
|
||||||
self::$pdo->exec('PRAGMA foreign_keys = ON');
|
|
||||||
|
|
||||||
$schema = file_get_contents($root . '/database/schema.sql');
|
|
||||||
self::$pdo->exec($schema);
|
|
||||||
|
|
||||||
return self::$pdo;
|
return self::$pdo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function init(): void
|
||||||
|
{
|
||||||
|
$root = dirname(__DIR__);
|
||||||
|
|
||||||
|
self::$pdo = new PDO('sqlite:' . $root . '/database/api_tube.sqlite');
|
||||||
|
self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
self::$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||||
|
self::$pdo->exec('PRAGMA foreign_keys = ON');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
final class JsonExporter
|
|
||||||
{
|
|
||||||
private PDO $pdo;
|
|
||||||
private string $filePath;
|
|
||||||
|
|
||||||
public function __construct(PDO $pdo, string $filePath)
|
|
||||||
{
|
|
||||||
$this->pdo = $pdo;
|
|
||||||
$this->filePath = $filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function export(): void
|
|
||||||
{
|
|
||||||
$links = $this->pdo->query('SELECT * FROM links ORDER BY id DESC')->fetchAll();
|
|
||||||
|
|
||||||
$json = json_encode($links, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
||||||
|
|
||||||
file_put_contents($this->filePath, $json . PHP_EOL, LOCK_EX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ final class LinkController
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly LinkRepository $repository,
|
private readonly LinkRepository $repository,
|
||||||
private readonly JsonExporter $exporter,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +79,6 @@ final class LinkController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->exporter->export();
|
|
||||||
$this->json($this->repository->find($id), 201);
|
$this->json($this->repository->find($id), 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +113,6 @@ final class LinkController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->exporter->export();
|
|
||||||
$this->json($this->repository->find($id));
|
$this->json($this->repository->find($id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +125,6 @@ final class LinkController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->repository->delete($id);
|
$this->repository->delete($id);
|
||||||
$this->exporter->export();
|
|
||||||
http_response_code(204);
|
http_response_code(204);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,3 @@ curl -s -w "\n[%{http_code}]\n" "$URL/999"
|
|||||||
|
|
||||||
echo "== DELETE /links/1 (suppression) =="
|
echo "== DELETE /links/1 (suppression) =="
|
||||||
curl -s -w "\n[%{http_code}]\n" -X DELETE "$URL/1"
|
curl -s -w "\n[%{http_code}]\n" -X DELETE "$URL/1"
|
||||||
|
|
||||||
echo "== data.json =="
|
|
||||||
cat data.json
|
|
||||||
Reference in new issue
Block a user