Core

documents-and-printers

  • mmo.core
  • agent skill
  • mmoframework-skill://mmo.core/mmoframework/references/documents-and-printers.md
# Documents & Printers A **Document** holds structured content (an array/`iArray` body, plus meta/links/logs); a **Printer** turns a document into its final serialized form (string, or bytes for xlsx). `Response::Json(...)`/`Response::Html(...)`/ etc. (see [server-http.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fserver-http.md)) wrap a document + the right printer together for you — most application code builds a `Document` subclass and lets `Response::*` pick the printer, rather than calling printers directly. ## Document subclasses | Class | Printer | Use for | |---|---|---| | `Document` | none by default (`toArray()`/generic) | raw structured data | | `JsonDocument` | `JsonPrinter` | JSON API responses | | `XmlDocument` | `XmlPrinter` | XML output | | `HtmlDocument` | `HtmlPrinter` | full HTML pages | | `TextDocument` | `TextPrinter` (or `ShellPrinter` for ANSI-colored CLI output) | plain text / CLI | | `xlsx\Document` | `XlsxPrinter` | spreadsheets (see below) | Construct with `new JsonDocument(['key' => 'value'])`, or `Response::Json([...])` which does this for you. `Document::Raw($body, $opts)` builds one with the `raw` option: as plain as its format allows — a JsonDocument without its `meta`/`links`/`logs` envelope, an HtmlDocument without the server console, and without the CSS/JS a `DocumentInit` listener (mmo.client's `Theme`) adds unless `isRaw()`: an e-mail's body, for example. Common `Document` API: `addContent($data)` (merge array data in), `addMeta($data)`, `addLink($linkOrArray)`, `getBody()`/`setBody()`, `render($opts = [])`, `setRaw()`/`isRaw()` (skip the configured printer's extra formatting), `createStream(): Stream`. ## Options: typed, or an array A document and a printer take options. Pass them typed, as named arguments: a misspelled one is then an error ("Unknown named parameter $escpae"), where a misspelled array key is silently ignored. - `MMO\core\printers\PrintOptions` — what the printers read: `escape`, `dump`, `maxLevel`, `indent`, `separator`, `precision`, `decimalSeparator`, `thousandSeparator`, `lang`, `basepath`, `ignoreTraces`, `tracesFilter`, `fields`, `store`, `headers`, `eol`. Each printer reads those it knows (see the class's docblock). - `MMO\core\documents\DocumentOptions` — a document's own: `raw`, `charset`, `printer`, `vars`, and `print` (a `PrintOptions` for every value it renders). ```php fragment $page = new HtmlDocument(null, new DocumentOptions(print: new PrintOptions(escape: true))); echo TextPrinter::Print($order, new PrintOptions(indent: 2, maxLevel: 3)); $csv = CsvPrinter::Encode($rows, new PrintOptions(headers: ['Name', 'Age'], separator: ',')); ``` They are accepted by `TextPrinter::Print()` (and `HtmlPrinter::Print()`...), `aExporter::Export()`, `JsonPrinter::Encode()`/`EncodeFile()`, `CsvPrinter::Encode()`, `XlsxPrinter::ExportLines()`, `\MMO\idump()`, a document's constructor, `Document::Raw()` and `renderValue()`. What takes an array only — `render()` (it is `iRenderable`'s), a printer's `__invoke()` — takes `->toArray()`. An option a lib's own printer reads (pdo's `bind`...) is not in `PrintOptions`: pass an array there. ## Templates `$document->import($filename, $vars = [], $key = null)` `require()`s a PHP template file, capturing its output. **`$vars` is NOT `extract()`-ed into local variables.** `Document::import()` only merges `getVars()` defaults into the `$_VARS` array and then `require()`s the file, so the imported template has a `$_VARS` *array* in scope (plus `$this`, bound to the `Document`, since `require()` runs inside `import()`'s own method body) — never individual `$name`-style variables. (Its docblock used to claim otherwise; it has been corrected.) Real call sites agree: `mmo.client`'s `src/templates/category.phtml` reads `$_VARS['content']`, not a bare `$content`. Write templates against `$_VARS['key']`. **`import()` does zero extension magic — it does a literal file lookup.** `addImportPath()` registers a search directory, and `findImportPath()` resolves `$filename` by looking for that *exact literal string* in each registered path (via `\MMO\filename($filename, $path)`). Nothing is appended or stripped, so whatever the real file is named on disk is exactly what you pass — no more, no less. This trips people up in both directions: passing a bare name when the real file has an extension fails just as surely as guessing an extension that isn't there. In `mmo.client` (`libs/client/`), every real template file keeps its `.phtml` extension, and every `import()` call passes it explicitly — confirmed both by reading `src/controllers/PageController.php` (its templates are `'category.phtml'` and `'entity.phtml'`) and by testing it directly: importing a template without its extension fails with "cannot find import", while the real on-disk filename works. ```php $doc = HtmlDocument::Raw(); // Raw() to skip the normal page-chrome init hook $doc->addImportPath('/path/to/templates'); $doc->import('welcome.phtml', ['name' => 'Marc']); // looks for /path/to/templates/welcome.phtml, literally ``` `HtmlDocument::import()` additionally defaults `$key` to `'body'` (appends the rendered output into the page body automatically) — the base `Document::import()` defaults to `null`, which appends it to the document's body too; pass `false` to add it nowhere (`import()` always returns the capture, an `OutputCapture`, for the caller). ## HtmlDocument extras `setTitle()`, `setMeta($name, $content, $key = 'name')` / `getMeta()`, `addStylesheet()`, `addScript()` / `addInlineScript()`, `setScriptVariable($name, $value)` (a `JSVariable` — becomes a real JS variable assignment in the rendered page), `setFavicon()`, `addBreadcrumb()`/`addNavigation()`/`addMenu()` (structured nav link lists, not just arbitrary content). `setTitle()` takes text: it holds it as an `XmlText`, escaped when printed (do not escape it first). The source `addInlineScript()` adds is printed as it is, also by a document that escapes what it prints — see [gotchas.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.core%2Fmmoframework%2Freferences%2Fgotchas.md#htmlprinter-does-not-escape-strings). ## Spreadsheets (`documents/xlsx/Document`) Lazily autoloaded (see conventions.md). `xlsx\Document::File($filename)` opens an existing workbook; `new xlsx\Document()` starts a blank one. Sheets/cells/styles are real, separate classes (`Sheet`, `Cell`, `Column`, `RowPointer`, `styles\{Font,Fill,Border,Alignment,Color,Style}`) — this is a from-scratch OOXML implementation, not a wrapper around another library. Useful statics: `Document::Cell($col, $row): string` (→ `"B3"`-style reference), `Document::Index($cell): array` (reverse), `Document::Range(...)`/`LoopRange(...)` for range strings. Render/save via the normal `Response::Xlsx($document)` or the document's own printer. ## Printers directly (rarely needed) Each printer has a `Shared()` singleton and can be used standalone: `HtmlPrinter::Shared()->dump(...)` (debug printing), `HtmlPrinter::Print($exception)` (render a throwable as HTML — used in error reporting/logs), `HtmlPrinter::Print($value, new PrintOptions(escape: true))`. An entity printed through `TextPrinter` or a printer extending it (`HtmlPrinter`, `XmlPrinter`, `ShellPrinter`: a template's `$this->renderValue($user)`, a node holding it) is its `toArray()`, as `JsonPrinter` exports it: a `@hidden` property is left out, even named in `fields`. A dump (`\MMO\dump($user)`) shows its properties, protected ones included, but a hidden one as `[hidden]`: dump the value itself when you need it (`\MMO\dump($user['passhash'])`). Until 2026-09 an entity was printed property by property -- the password's hash of a user in any HTML or XML page printing it -- and with its collection and repository (50 to 100 KB for one entity); a dump showed the hash too. `JsonPrinter` exports an entity linked to another one in full (its own linked entities too), an entity met again as a `$ref`, and **a list of stored entities inside a linked entity as their links** (`"<uid>title=..."`): until 2026-10 a company's group listed every company of the group, each in full with its own group, and 50 employees with their contract were 1.6 MB (400 KB since). The entity at the top still has its lists in full (`JsonPrinter::IsEntityList()`; `tests/modules/TestEntitySaveFromClient.php`). `CsvPrinter::Encode($rows, ...)` takes the rows themselves (an array, not a document): a plain `headers` list of display titles is matched to the exported fields by position (one title per field, or it throws) — it used to give a correct header row with blank data rows underneath (a real, fixed bug: see `CsvPrinter::Encode()`'s NOTE).