MMOframework
Pourquoi l'IA Playground Bibliothèques Documentation Rechercher Agence
FR EN
Index
# mmo.pdf — overview for AI code generation `mmo.pdf` (PHP package `mmo.pdf`, PHP namespace `MMO\pdf`) is Marc Mouret's PDF library. It does two things, which share no code: 1. **`MMO\pdf\SimplePdf`** — **writes a new PDF from code**: an invoice, a receipt, a report. Headings, text, labelled fields, a table whose columns fit their content, totals; UTF-8 texts (accents, `€`); the bytes, a file, or the response a route or a page returns. Built on FPDF 1.86, bundled in `src/vendor/fpdf` with its core fonts and loaded by the package: nothing to install, nothing to include. 2. **`MMO\pdf\PDFTK`** — **fills an existing PDF's form**: a PHP wrapper around the `pdftk` command-line tool. Reads an existing PDF's page/field metadata and fills its AcroForm fields with values, using MMOFramework's own `{path:type}` template-string syntax to bind form field names to arbitrary data. `MMO\pdf\documents\PDFDocument` / `MMO\pdf\PDFPrinter` / `MMO\pdf\helpers\Page` / `MMO\pdf\helpers\Cell` are unfinished scaffolding for a `Document`/`Printer` pair — see "What's unfinished" below. Don't reach for them. Read this whole page before generating code against this package: which of the two real subsystems to use depends entirely on whether you're **filling an existing PDF** or **generating a brand new one**, and they share no code path. ## Decide: write a new PDF, or fill an existing one? - **Writing a new PDF** (an invoice, a quote, a receipt, a report you lay out yourself) → `MMO\pdf\SimplePdf` (see "Writing a new PDF" below). For what it does not do (an image, a line, a logo), `->fpdf()` gives the `FPDF` object underneath, at the current position. - **Filling an existing PDF** (an official form, a contract template, an existing layout you don't control) → `MMO\pdf\PDFTK`. It reads the PDF's own AcroForm field definitions and only needs values, not layout. ## Writing a new PDF — an invoice ```php use MMO\pdf\SimplePdf; $pdf = (new SimplePdf('Invoice ' . $invoice->number)) // the title in the file's properties; A4 portrait ->heading('Invoice ' . $invoice->number) // bold, 16pt ->fields(['Customer' => $invoice->customer, 'Date' => $invoice->date->format('d/m/Y')]) ->table( ['Item', 'Qty', 'Unit price', 'Total'], // the columns' titles array_map(fn($line) => [$line->label, $line->quantity, money($line->price), money($line->total())], $invoice->lines), ['L', 'R', 'R', 'R'] // each column's alignment ) ->totals(['Subtotal' => money($subtotal), 'VAT 20%' => money($vat), 'Total' => money($total)]) // the last one bold ->text('Payment within 30 days.'); return $pdf->response("invoice-{$invoice->number}.pdf"); // from a route: shown in the browser // $pdf->response($name, false) the browser saves it $pdf->output() the bytes (`%PDF-...`) $pdf->save($file) ``` - **From a page** (`src/pages/invoices/pdf.phtml`, served at `invoices/pdf?id=42`): a page answers with a response of its own by stopping its route — `$route->stop($pdf->response('invoice-42.pdf')); return;` (mmo.client's `docs/Pages.md`). From a controller, an `#[APIRoute('GET|invoices/{id}.pdf', ...)]` method returns it, typed `\MMO\core\interfaces\iResponse`. - **Texts are UTF-8**: written in the core fonts' encoding (cp1252), so accents, `€`, `œ` print; a character outside it (Greek, Cyrillic, an emoji) prints as `?`. - **The table** measures its columns on their content and scales them to the page's width; a text too long for its column is cut with `...`; a long table goes on to new pages, its header repeated. Pass `$widths` (mm) to fix them. - **The rest**: `space($mm)`, `text($text, 'B'|'I'|'', 'L'|'C'|'R'|'J')`, `heading($text, $size)`; the public `$font` (`Helvetica`, `Times`, `Courier`), `$size` (points) and `$lineHeight` (mm), set before writing. - **In a test**: `$this->assertStringStartsWith('%PDF-', $pdf->output())`; call `$pdf->fpdf()->SetCompression(false)` first and the texts can be read in the bytes (`"(900,00 \x80)"`: `€` is `\x80` in cp1252). Through the site: `$this->http('GET', 'invoices/pdf?id=42')` (mmo.core's `docs/Testing.md`). ## The things that matter most 1. **`PDFTK` is a singleton module — never `new` it.** It extends `MMO\core\classes\aModule` (which uses `tSingleton`), so access it via `PDFTK::Shared()`, exactly like any other MMOFramework module. 2. **`PDFTK::fill()`'s template strings are MMOFramework template syntax, not raw values.** Its signature is `fill(string $filename, array|\ArrayAccess $data, array $templates, ?callable $coercer = null, array $options = [], bool $flatten = false): string`. `$templates` maps a PDF field name (as `pdftk` reports it) to a template string in the `{path:type}` syntax documented on `\MMO\core\strings\loopTemplate()` (`?` prefix for optional, `:type` suffix passed to `$coercer`). Paths are resolved against `$data` with `\MMO\core\arrays\get()`. A field with no matching template falls back to the PDF's own default value, then to `''`/an exception depending on whether it's required (`$this->throwOnRequired`, `throwOnMaxLength`, `throwOnMinLength`, `throwOnEnum` — all `true` by default except `throwOnMaxLength`; call `PDFTK::Shared()->force()` to relax all four at once). 3. **Call `getInfo()` first to know what fields actually exist.** `PDFTK::getInfo(string $filename): array` shells out to `pdftk ... dump_data_utf8` + `dump_data_fields_utf8` and returns `['pages' => [...], 'fields' => [name => ['type' => 'Text'|'Button'|'Choice'|..., 'required' => bool, 'maxLength' => int, 'default' => ..., 'enum' => [...]]]]` plus top-level PDF metadata (author, page count, etc.). Write your `$templates` array against the real field names this returns — they're whatever the PDF's original author named them, not something `mmo.pdf` controls. 4. **`fill()` returns the filled PDF as a raw string, not a filename.** It shells out to `pdftk fill_form - output -` and returns `$exec->getOutput()` directly — write it to disk yourself (`file_put_contents()`) or stream it in a response (see below). 5. **FPDF is bundled, and loaded with the package.** `src/vendor/fpdf/fpdf.php` (FPDF 1.86, [fpdf.org](http://www.fpdf.org), its licence beside it) and its core fonts (`src/vendor/fpdf/font/`) are part of the package's sources: `\MMO\inc('mmo.pdf')` or a project depending on `mmo.pdf` has the global `FPDF` class. `SimplePdf::fpdf()` gives it for anything `SimplePdf` does not do; its texts are cp1252 (`SimplePdf::encode($utf8)`). The old `fpdf186/` folder at the package root is a local copy, not tracked beyond `fpdf.php`: don't include it. ## Loading the package ```php require __DIR__ . '/../core/src/bootstrap.php'; // mmo.core must be bootstrapped first \MMO\inc('mmo.pdf'); // loads mmo.pdf's own bootstrap.php (PDFTK, FPDF, SimplePdf) ``` ## Working example — filling an existing PDF form ```php use MMO\pdf\PDFTK; $pdf = PDFTK::Shared(); // 1. Inspect the template to see its real field names/types/constraints. $info = $pdf->getInfo(__DIR__ . '/templates/contract.pdf'); // $info['fields'] => ['CustomerName' => ['type' => 'Text', 'required' => true, 'maxLength' => 60], ...] // 2. Map each field name to a template string over your own data shape. $data = ['customer' => ['name' => 'Marc Mouret'], 'total' => 25792]; $templates = [ 'CustomerName' => '{customer/name}', 'Total' => '{total} EUR', ]; // 3. Fill it -- returns the filled PDF's bytes. $bytes = $pdf->fill(__DIR__ . '/templates/contract.pdf', $data, $templates); file_put_contents(__DIR__ . '/out/contract-filled.pdf', $bytes); ``` ## Serving a generated/filled PDF over HTTP `MMO\core\server\Response::FromMime()` does **not** special-case PDF — `application/pdf` resolves to the `'pdf'` extension, which isn't one of `FromMime()`'s matched cases, so it silently falls through to `Response::Text()`, which is wrong for binary PDF content. Don't use it here. The accurate, working pattern is `MMO\core\server\StreamResponse::File()`, which streams a file already on disk and infers `Content-Type: application/pdf` from the `.pdf` extension automatically: ```php use MMO\core\server\StreamResponse; // PDFTK output is bytes, not a file -- write it out first. $bytes = PDFTK::Shared()->fill($templatePath, $data, $templates); $tmp = \MMO\core\files\temporaryFilename('pdf') . '.pdf'; file_put_contents($tmp, $bytes); return StreamResponse::File($tmp, [], 'contract.pdf'); // sets Content-Disposition: attachment; filename="contract.pdf" // SimplePdf answers on its own: return $pdf->response('invoice.pdf'); ``` `StreamResponse::File()` defaults to `Content-Disposition: attachment` (forces a download); override the `Content-Disposition` header yourself if you want the browser to render the PDF inline instead. ## What's unfinished (don't build on these without checking first) - **`MMO\pdf\documents\PDFDocument`** extends `MMO\core\documents\Document` and sets itself up with `MMO\pdf\PDFPrinter` as its printer, plus PDF metadata setters (`setTitle()`, `setAuthor()`, `setSubject()`, `setKeywords()`, `setCreator()`). But **`PDFPrinter` (extends `MMO\core\classes\aExporter`) has no FPDF rendering logic at all** — it only defines value-export helpers (`DateTimeInterface` → JSON string, `Exception` → `['type', 'message']`) and a no-op `hash()`. Nothing turns a `PDFDocument`'s body into PDF bytes yet. - **`MMO\pdf\helpers\Page`** (orientation/unit/page size/margins/zoom/layout, an `iArray` of content) and **`MMO\pdf\helpers\Cell`** (`x`, `y`, `text`, `border`) are plain data holders with no `render()`/output method — nothing consumes them either. - **None of the four classes above are `include()`d by `src/bootstrap.php`** (only `exceptions/*.php`, `helpers/TTFFile.php` and `PDFTK.php` are). They exist in `src/` and are tracked in git, but the framework's autoload fallback cannot reach them either: `src/bootstrap.php` registers no `paths` entry for `MMO\pdf`, so `class_exists()` on any of them is `false` until you `include` its file yourself. - **`MMO\pdf\helpers\TTFFile`** is a working, from-scratch TrueType font file parser (reads `head`/`hhea`/ `maxp`/`hmtx`/`loca`/`glyf`/`cmap`/`name`/`OS/2`/`post` tables, and can `extractSubset()` a glyph subset for a given character list or encoding via `src/fonts/*.map`). It parses correctly on its own, but nothing else in `src/` currently calls it — it reads as groundwork for future custom-font embedding, not a finished feature. - **Stamping and merging PDFs** (mentioned as a goal in old comments in `PDFTK.php`, using `Imagick` to draw checkmarks/images onto form pages and `pdftk multistamp`) is entirely commented out — there is no working `stamp()`/`merge()` method today. The README's Prerequisites section still asks to install `php-imagick`, which is leftover from this: no currently-active code in `src/` calls `Imagick` anywhere. ## Prerequisites - The `pdftk` binary must be installed and on `PATH` (`PDFTK::$bin`, default `'pdftk'`) for anything that uses `PDFTK`. - `mb_convert_encoding` (the `mbstring` extension) is required — `src/bootstrap.php` throws immediately if it's missing. - `SimplePdf` needs nothing more: FPDF and its core fonts are part of the package (`src/vendor/fpdf`).