# 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.