Core

server-http

  • mmo.core
  • agent skill
  • mmoframework-skill://mmo.core/mmoframework/references/server-http.md
# Server / HTTP / CLI Layer PSR-7-flavored, but with real (documented, tested) deviations. `Request`/`ShellRequest` model an incoming HTTP or CLI invocation; `Response`/`StreamResponse` model the outgoing one; `Route` drives one request through its matched middleware chain (see [controllers-and-routing.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fcontrollers-and-routing.md) for how routes get matched in the first place). ## Response Build one with a static factory matching the content type — this also sets the right `Content-Type` header and picks the matching document class: ```php fragment Response::Json(['key' => 'value']); // application/json Response::Html($htmlDocumentOrNull); // text/html Response::Xml($xmlDocumentOrNull); Response::Csv($documentOrArray); Response::Xlsx($xlsxDocumentOrNull); Response::Text($textDocumentOrArray); Response::Shell($textDocumentOrArray); // like Text, but via ShellPrinter (ANSI colors) Response::Redirect($url, $headers = [], $post = null, $status = 302); Response::FromMime($mimeType, $document, ...); // picks the factory above matching $mimeType ``` `FromMime()` needs a **real MIME string** (`'application/json'`), not a bare extension (`'json'`) — the extension-to-factory lookup only matches full MIME strings. And for `'html'`/`'xml'`, `$document` must be `null` or an already-built `HtmlDocument`/`XmlDocument` — there's no array→markup mapping, so a plain array throws `eInvalid` there (this used to be a confusing internal `TypeError` instead; fixed to be explicit). ## StreamResponse (files) ```php fragment return StreamResponse::File($path, [], 'Été "2025".pdf'); // also Zip(), JSON(), or $response->withBody($stream) ``` `Application::sendResponse()` sends a `StreamResponse` **as it reads it**, a megabyte at a time (`render(['output' => true])`, which echoes and returns `''`), so a download is not limited by `memory_limit`. `render()` without that option still returns the whole content as a string (what a `Curl` response is read with). A subclass overriding `render()` is still sent through it (mmo.edms's `FileResponse` sends a byte range that way). The name goes through `StreamResponse::ContentDisposition($name, $inline = false)`: an ASCII fallback plus the real name as RFC 6266 `filename*=UTF-8''...`, so a quote, an accent or a line break cannot break the header. Pass that header as `[$value]` if you set it yourself: a string header value is split on `,`. ## Request headers A web server hands php a request's headers in `$_SERVER` under names of its own (`HTTP_RANGE`, `HTTP_X_CSRF_TOKEN`, `CONTENT_TYPE`). A request keeps them under the header's name (`Range`, `X-Csrf-Token`, `Content-Type`: `Request::HeaderName()`), so `$request->getHeaderLine('Range')` reads one on a real request, in any case; the `$_SERVER` names still find them (`getHeader('HTTP_ACCEPT_LANGUAGE')`). But `getHeaders()` is keyed by header names: `getHeaders()['HTTP_RANGE']`, or `$route->get('HTTP_RANGE')`, which looks a header up by its exact key, finds nothing. The rest of `$_SERVER` (`DOCUMENT_ROOT`, `REMOTE_ADDR`...) stays among the headers, as it is. ## Route `Route::get($key, $default = null, $where = -1)` reads a value from the current request, checking (in this fixed order) **attributes → query → body → headers → cookies → files**, and returning the first match. `$where` is a bitmask of `Route::IN_QUERY|IN_BODY|IN_HEADERS|IN_COOKIE|IN_FILES|IN_ATTRIBUTES` to narrow the search. `Route::has($key, $where)` is the boolean version. `$route->extractQuery($query)` populates a `Query` object (pagination/sort/filters/fields) from the request's query/body, per the JSON:API-ish convention documented on `RepositoryController` (`page[offset]`, `page[limit]`, `sort=-created,title`, `filters[...]`, `fields=...`). Route control flow: `$route->addMiddleware($callable, $params = [], $order = 0)` (lower `$order` runs *first* — counter-intuitive, verified directly), `$route->stop($response = null)` (set the final response and abort remaining middleware — use this from inside a middleware/controller method to short-circuit), `$route->redirect(...)`. ## `Stream` A PSR-7 `StreamInterface` implementation. Factories: `Stream::Text($string)` (in-memory), `Stream::File($path, $mode)`, `Stream::TEMP()` (memory, spills to disk past ~2MB), `Stream::STDOUT()`/`STDIN()`/`STDERR()` (shared singletons wrapping the real process streams — don't close one: under CLI the process stream stays open, but the shared instance is detached, so every later write through it throws `stream is non-writable`). ## `Curl` ```php $curl = new Curl(); $curl->setUserAgent('MyApp/1.0'); $curl->addHeaders(['Authorization' => 'Bearer ...']); $response = $curl->exec('https://example.com/api', ['name' => 'value']); // POST if $post given, else GET $response->getStatusCode(); $response->render(); // body as string ``` One `Curl` instance handles one request at a time (throws if you call `exec()` again before the previous one finished — though a fresh call *after* a prior one completed is fine, since `exec()` closes its handle when done). `Curl::FromString($rawCurlCommandText)` parses a browser "Copy as cURL" string into `[$url, $data, $opts, $ignoredArgs]` for replay. ## `Mail` ```php $mail = new Mail('to@example.com', 'Subject', '<p>HTML body</p>', 'from@example.com'); $mail->bodyAlt = 'Plain text alternative'; // makes it multipart/alternative $mail->addAttachment('/path/to/file.pdf'); $mail->send(); // sends via PHP mail(), MIME-built by hand, DKIM-capable ``` `<img src="data:...">` tags in the HTML body are automatically extracted into inline (`cid:`) attachments — this makes the message `multipart/related`. `MailController::Send($template, $email, $subject, $data)` is the higher-level entry point when you want to render a template into the body (see [documents-and-printers.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fdocuments-and-printers.md#templates) — same extension-less-filename rule applies). ## `SSH` / `Websocket` `SSH` wraps the `ssh2` PECL extension (`new SSH($user, $host, $port, $password)`, `$ssh->exec($cmd, $cwd)`). `Websocket` is a hand-rolled RFC 6455 server (`new Websocket($host, $port)`, `->connect()`, `->listen()` [blocks for one client], then `->readMessage()`/`->write()`). Both need a real socket/process to test — not something to unit test casually; if you're modifying either, verify against a real connection. ## `Cron` Wraps the real system `crontab` command — `Cron::Shared()->addJob($interval, $command)`, `->removeJob($command)`, `->getJobs()`. **Every mutating call touches the actual OS crontab immediately** — treat this the same as any other system-configuration change; don't call `addJob()`/`removeJob()`/`save()` speculatively or in a test without meaning to change the real crontab.