MAX_BODY_BYTES) { Response::error('Request body too large', 413); } $store = new Store(dirname(__DIR__) . '/data/projects'); $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $route = trim((string) ($_GET['r'] ?? ''), '/'); $parts = $route === '' ? [] : explode('/', $route); if (($parts[0] ?? '') !== 'projects' || count($parts) > 2) { Response::error('Not found', 404); } // Collection: /projects if (count($parts) === 1) { if ($method === 'GET') { Response::json($store->list()); } if ($method === 'POST') { $body = Response::body(); $name = trim((string) ($body['name'] ?? '')); if ($name === '' || mb_strlen($name) > 120) { Response::error('Project name must be 1–120 characters', 422); } Response::json($store->create($name), 201); } Response::error('Method not allowed', 405); } // Item: /projects/{id} $id = $parts[1]; if (!Store::isValidId($id)) { Response::error('Invalid project id', 400); } if ($method === 'GET') { $doc = $store->load($id); if ($doc === null) { Response::error('Project not found', 404); } Response::json($doc); } if ($method === 'PUT') { $body = Response::body(); if (!isset($body['revision']) || !is_int($body['revision']) || !is_array($body['project'] ?? null)) { Response::error('Body must contain integer "revision" and object "project"', 422); } try { $result = $store->save($id, $body['revision'], $body['project']); } catch (StaleRevisionException $e) { Response::error('Project was modified elsewhere', 409, ['revision' => $e->currentRevision]); } if ($result === null) { Response::error('Project not found', 404); } Response::json(['revision' => $result['revision'], 'updatedAt' => $result['updatedAt']]); } if ($method === 'DELETE') { if (!$store->delete($id)) { Response::error('Project not found', 404); } Response::noContent(); } Response::error('Method not allowed', 405); } catch (Throwable $e) { error_log('[game-maker api] ' . $e->getMessage()); Response::error('Internal server error', 500); }