Files
donut-gestprojet/src/bootstrap.php
T
Arnaud 3a32e93ab1 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>
2026-05-28 19:55:28 +02:00

100 lines
3.8 KiB
PHP

<?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);
});