Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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-journal
|
||||
.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
|
||||
- Stockage SQLite via PDO
|
||||
- À chaque création / modification / suppression, un snapshot complet de la table est écrit dans `data.json`
|
||||
|
||||
## Prérequis
|
||||
|
||||
@@ -23,7 +22,25 @@ php -m | grep -i sqlite # doit afficher pdo_sqlite et/ou sqlite3
|
||||
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
|
||||
|
||||
@@ -48,16 +65,38 @@ 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.
|
||||
|
||||
## Fichier `data.json`
|
||||
|
||||
Régénéré à chaque POST / PUT / DELETE : contient la liste complète des liens, formatée (pretty print).
|
||||
|
||||
## Tests manuels
|
||||
|
||||
```bash
|
||||
bash tests/curl.sh
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
@@ -65,10 +104,13 @@ public/
|
||||
index.php # point d'entrée unique
|
||||
.htaccess # réécriture vers index.php
|
||||
src/
|
||||
Database.php # connexion PDO SQLite (+ création du schéma)
|
||||
Database.php # connexion PDO SQLite + exécution des migrations
|
||||
LinkRepository.php # CRUD SQL
|
||||
JsonExporter.php # écriture de data.json
|
||||
LinkController.php # routage, validation, orchestration
|
||||
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,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/src/Database.php';
|
||||
|
||||
$applied = Database::runMigrations();
|
||||
|
||||
if ($applied === []) {
|
||||
echo "Nothing to migrate.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
foreach ($applied as $version) {
|
||||
echo "Applied {$version}\n";
|
||||
}
|
||||
File renamed without changes.
@@ -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/LinkRepository.php';
|
||||
require_once __DIR__ . '/../src/JsonExporter.php';
|
||||
require_once __DIR__ . '/../src/LinkController.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
@@ -22,7 +21,6 @@ try {
|
||||
|
||||
$controller = new LinkController(
|
||||
new LinkRepository($pdo),
|
||||
new JsonExporter($pdo, dirname(__DIR__) . '/data.json'),
|
||||
);
|
||||
|
||||
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
|
||||
+57
-8
@@ -8,21 +8,70 @@ final class Database
|
||||
|
||||
public static function connect(): PDO
|
||||
{
|
||||
if (self::$pdo !== null) {
|
||||
if (self::$pdo === null) {
|
||||
self::init();
|
||||
self::runMigrations();
|
||||
}
|
||||
|
||||
return self::$pdo;
|
||||
}
|
||||
|
||||
public static function runMigrations(): array
|
||||
{
|
||||
if (self::$pdo === null) {
|
||||
self::init();
|
||||
}
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
$dsn = 'sqlite:' . $root . '/database/api_tube.sqlite';
|
||||
|
||||
self::$pdo = new PDO($dsn);
|
||||
self::$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS migrations (
|
||||
version TEXT PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)"
|
||||
);
|
||||
|
||||
$applied = array_column(self::$pdo->query('SELECT version FROM migrations')->fetchAll(), 'version');
|
||||
|
||||
$files = glob($root . '/database/migrations/*.sql') ?: [];
|
||||
sort($files);
|
||||
|
||||
$newlyApplied = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$version = basename($file);
|
||||
|
||||
if (in_array($version, $applied, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self::$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
self::$pdo->exec((string) file_get_contents($file));
|
||||
|
||||
$stmt = self::$pdo->prepare('INSERT INTO migrations (version) VALUES (:version)');
|
||||
$stmt->execute(['version' => $version]);
|
||||
|
||||
self::$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
self::$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$newlyApplied[] = $version;
|
||||
}
|
||||
|
||||
return $newlyApplied;
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
$schema = file_get_contents($root . '/database/schema.sql');
|
||||
self::$pdo->exec($schema);
|
||||
|
||||
return self::$pdo;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
private readonly LinkRepository $repository,
|
||||
private readonly JsonExporter $exporter,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -80,7 +79,6 @@ final class LinkController
|
||||
return;
|
||||
}
|
||||
|
||||
$this->exporter->export();
|
||||
$this->json($this->repository->find($id), 201);
|
||||
}
|
||||
|
||||
@@ -115,7 +113,6 @@ final class LinkController
|
||||
return;
|
||||
}
|
||||
|
||||
$this->exporter->export();
|
||||
$this->json($this->repository->find($id));
|
||||
}
|
||||
|
||||
@@ -128,7 +125,6 @@ final class LinkController
|
||||
}
|
||||
|
||||
$this->repository->delete($id);
|
||||
$this->exporter->export();
|
||||
http_response_code(204);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,3 @@ curl -s -w "\n[%{http_code}]\n" "$URL/999"
|
||||
|
||||
echo "== DELETE /links/1 (suppression) =="
|
||||
curl -s -w "\n[%{http_code}]\n" -X DELETE "$URL/1"
|
||||
|
||||
echo "== data.json =="
|
||||
cat data.json
|
||||
Reference in new issue
Block a user