feat: initial API skeleton — routes, controllers, migrations, deploy docs
Implements the JSON API for Le Donut Infolab per INSTRUCTIONS.md.
Schema (migrations/001-002):
- 8 tables: categories, members, projects, project_members, roadmap_steps,
discussions, resources, activity_log
- 6 categories seeded with colors / dimensions (social, ecologique, transversal)
- FK contraintes + soft delete sur projects, CHECK sur progress_percent
Bare PHP framework (src/):
- Front controller public/index.php + router :params + auth flag par route
- PSR-4 autoload sans Composer (bootstrap.php)
- Database PDO singleton + helpers fetchOne/All/insert/update/delete
- Validator fluent -> 422 {error: validation_failed, fields: {...}}
- Bearer token admin (hash_equals) sur POST/PATCH/DELETE
- Slugger avec unicité (suffixe -2, -3...) via intl Transliterator
- ActivityLogger best-effort sur activity_log
- Erreurs uniformes : 400/401/404/405/409/422/500 + CORS configurable
10 contrôleurs (Health, Categories, Members, Projects, ProjectMembers,
Roadmap, Discussions, Resources, Activity, Stats). Transitions de statut
loggées (created_project, completed_step, joined, etc.).
Cible prod :
- Dockerfile (php:8.3-apache + pdo_mysql + intl + opcache)
- docker-compose.yml sur réseau donut-net, bind 127.0.0.1:8003
- caddy.conf pour infolab.ledonut-marseille.org
- migrations/apply.sh idempotent (table schema_migrations)
- DEPLOY.md avec commandes SSH exactes
Tests :
- tests/smoke.sh exerce 10 endpoints clés, body envoyé via stdin
(--data-binary @-) pour gérer l UTF-8 sous Git Bash Windows.
Vérifié localement sous Laragon Apache + MySQL 8.4 : 10/10 OK + cas
négatifs (401, 404, 405, 409, 422) conformes au contrat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// bootstrap.php
|
||||
// Initialise l'environnement PHP : timezone UTC, exception applicative,
|
||||
// chargement .env (dev local), autoload PSR-4 pour le namespace Infolab\,
|
||||
// et conversion des warnings PHP en exceptions.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Tout en UTC en interne, les clients gèrent leur fuseau côté front.
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
/**
|
||||
* Exception applicative : porte un code HTTP et le payload JSON à renvoyer.
|
||||
* Levée par les contrôleurs et helpers, attrapée par le handler global de index.php.
|
||||
* Définie dans le namespace racine pour être accessible depuis tout le code.
|
||||
*/
|
||||
final class ApiException extends \RuntimeException
|
||||
{
|
||||
/** @param array<string,mixed> $payload */
|
||||
public function __construct(public readonly int $statusCode, public readonly array $payload)
|
||||
{
|
||||
parent::__construct($payload['error'] ?? 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Raccourci pour interrompre rapidement le traitement avec une erreur typée.
|
||||
*
|
||||
* @param array<string,mixed> $extra champs additionnels à inclure dans le payload JSON
|
||||
*/
|
||||
function api_abort(int $status, string $error, ?string $message = null, array $extra = []): never
|
||||
{
|
||||
$payload = ['error' => $error];
|
||||
if ($message !== null) {
|
||||
$payload['message'] = $message;
|
||||
}
|
||||
if ($extra !== []) {
|
||||
$payload += $extra;
|
||||
}
|
||||
throw new ApiException($status, $payload);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Chargement minimal d'un fichier .env (pour le dev local sous Laragon).
|
||||
// En prod docker, c'est `env_file:` qui pousse les variables — on ne fait rien.
|
||||
// -----------------------------------------------------------------------------
|
||||
$envFile = dirname(__DIR__) . '/.env';
|
||||
if (is_file($envFile)) {
|
||||
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#' || !str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
[$k, $v] = explode('=', $line, 2);
|
||||
$k = trim($k);
|
||||
$v = trim($v);
|
||||
// Strip guillemets simples ou doubles s'ils encadrent la valeur.
|
||||
if (strlen($v) >= 2 && (
|
||||
($v[0] === '"' && substr($v, -1) === '"') ||
|
||||
($v[0] === "'" && substr($v, -1) === "'")
|
||||
)) {
|
||||
$v = substr($v, 1, -1);
|
||||
}
|
||||
// Ne pas écraser un env déjà défini (ex: par docker, par le shell de CI...)
|
||||
if (getenv($k) === false) {
|
||||
putenv("$k=$v");
|
||||
$_ENV[$k] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Autoload PSR-4 pour le namespace Infolab\ -> src/
|
||||
// On évite Composer : pas de dépendance externe, pas de `composer install`.
|
||||
// -----------------------------------------------------------------------------
|
||||
spl_autoload_register(static function (string $class): void {
|
||||
$prefix = 'Infolab\\';
|
||||
if (!str_starts_with($class, $prefix)) {
|
||||
return;
|
||||
}
|
||||
$relative = substr($class, strlen($prefix));
|
||||
$path = __DIR__ . '/' . str_replace('\\', '/', $relative) . '.php';
|
||||
if (is_file($path)) {
|
||||
require $path;
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Convertit les warnings/notices PHP en exceptions (sauf ceux supprimés par @).
|
||||
// Centralise tout le tracking d'erreurs dans le handler de index.php.
|
||||
// -----------------------------------------------------------------------------
|
||||
set_error_handler(static function (int $severity, string $message, string $file, int $line): bool {
|
||||
if (!(error_reporting() & $severity)) {
|
||||
return false;
|
||||
}
|
||||
throw new \ErrorException($message, 0, $severity, $file, $line);
|
||||
});
|
||||
Reference in New Issue
Block a user