$parts */ function routeProjects(array $parts, string $method): never { $store = new Store(dirname(__DIR__) . '/data/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); } $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); } /** @param list $parts */ function routeTextures(array $parts, string $method): never { $store = new TextureStore(dirname(__DIR__) . '/data/textures'); if (count($parts) === 1) { if ($method === 'GET') { Response::json($store->list()); } if ($method === 'POST') { $bytes = Response::rawBody(); if (strlen($bytes) > MAX_TEXTURE_BYTES) { Response::error('Image must be at most ' . (MAX_TEXTURE_BYTES / 1024 / 1024) . ' MB', 413); } try { Response::json($store->store($bytes, (string) ($_GET['name'] ?? '')), 201); } catch (InvalidImageException $e) { Response::error($e->getMessage(), 422); } } Response::error('Method not allowed', 405); } if ($method === 'DELETE') { if (!$store->delete($parts[1])) { Response::error('Texture not found', 404); } Response::noContent(); } Response::error('Method not allowed', 405); } /** @param list $parts */ function routeFonts(array $parts, string $method): never { $store = new FontStore(dirname(__DIR__) . '/data/fonts'); if (count($parts) === 1) { if ($method === 'GET') { Response::json($store->list()); } if ($method === 'POST') { $bytes = Response::rawBody(); if (strlen($bytes) > MAX_FONT_BYTES) { Response::error('Font must be at most ' . (MAX_FONT_BYTES / 1024 / 1024) . ' MB', 413); } try { Response::json($store->store($bytes, (string) ($_GET['name'] ?? '')), 201); } catch (InvalidFontException $e) { Response::error($e->getMessage(), 422); } } Response::error('Method not allowed', 405); } if ($method === 'DELETE') { if (!$store->delete($parts[1])) { Response::error('Font not found', 404); } Response::noContent(); } Response::error('Method not allowed', 405); } try { if ((int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > MAX_BODY_BYTES) { Response::error('Request body too large', 413); } $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $route = trim((string) ($_GET['r'] ?? ''), '/'); $parts = $route === '' ? [] : explode('/', $route); if (count($parts) < 1 || count($parts) > 2) { Response::error('Not found', 404); } match ($parts[0]) { 'projects' => routeProjects($parts, $method), 'textures' => routeTextures($parts, $method), 'fonts' => routeFonts($parts, $method), default => Response::error('Not found', 404), }; } catch (Throwable $e) { error_log('[game-maker api] ' . $e->getMessage()); Response::error('Internal server error', 500); }