PHPackages                             particle-academy/last-word - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [PDF &amp; Document Generation](/categories/documents)
4. /
5. particle-academy/last-word

ActiveLibrary[PDF &amp; Document Generation](/categories/documents)

particle-academy/last-word
==========================

Standalone word-processing document read/write tool for agentic docs. Framework-agnostic PHP core that writes .docx (Office Open XML / WordprocessingML) from a JSON document model — headings, styled runs, nested lists, tables, code blocks, quotes, embedded images — reads them back with high fidelity, and bridges to/from GFM markdown so WYSIWYG editors round-trip Word files without converter sandwiches.

v0.3.0(2w ago)0185↑22.2%MITPHPPHP ^8.4CI passing

Since Jul 4Pushed 1mo agoCompare

[ Source](https://github.com/Particle-Academy/last-word)[ Packagist](https://packagist.org/packages/particle-academy/last-word)[ Docs](https://github.com/Particle-Academy/last-word)[ RSS](/packages/particle-academy-last-word/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (2)Versions (5)Used By (0)

LastWord
========

[](#lastword)

[![Fancy UI suite](art/fancy-ui.svg)](https://particle.academy)

PHP package for reading and writing word-processing documents (`.docx`) from a JSON-friendly document model, with markdown bridges. Framework- agnostic, zero runtime dependencies (just `ext-zip` + `ext-dom`). Designed so WYSIWYG editors — react-fancy's `Editor` in particular — round-trip real Word files without a converter sandwich (mammoth → turndown → docx): one model, one engine, both directions.

Mirror package: [`@particle-academy/last-word`](https://github.com/Particle-Academy/last-word-js)(Node/TS) implements the exact same JSON model and Agent API, so a doc emitted by an agent works verbatim on either backend.

Why
---

[](#why)

Sister project to [`holy-sheet`](https://github.com/Particle-Academy/holy-sheet)(XLSX) and [`dark-slide`](https://github.com/Particle-Academy/dark-slide)(PPTX). The three share an "agent emits JSON, PHP writes a real Office document" pattern:

Document typeNode mirrorPHP packageSpreadsheetsholy-sheet-jsholy-sheetPresentationsdark-slide-jsdark-slideDocumentslast-word-js**last-word**Quickstart
----------

[](#quickstart)

```
use LastWord\Agent;

$doc = [
    'title' => 'Quarterly Notes',
    'blocks' => [
        ['type' => 'heading', 'level' => 1, 'runs' => [['text' => 'Summary']]],
        ['type' => 'paragraph', 'runs' => [
            ['text' => 'Revenue was '],
            ['text' => 'up 12%', 'bold' => true],
            ['text' => ' — details in the '],
            ['text' => 'appendix', 'link' => 'https://example.com/appendix'],
            ['text' => '.'],
        ]],
        ['type' => 'list', 'items' => [
            ['runs' => [['text' => 'Ship the docx engine']], 'children' => [
                ['runs' => [['text' => 'Reader + writer']]],
            ]],
            ['runs' => [['text' => 'Wire the Editor bridge']]],
        ]],
    ],
];

// Validate before writing — catches malformed agent output
$errors = Agent::validate($doc);   // [] when valid: [{path, message}, …] otherwise

// Write to disk (synchronous)
$result = Agent::write($doc, storage_path('app/notes.docx'));
// ['path' => …, 'bytes' => 4151, 'blocks' => 3]

// Or keep it in memory
$bytes = Agent::toBytes($doc);

// And read any .docx back into the same model — including Word-authored files
$model = Agent::read($bytes);      // or Agent::read('/path/to/file.docx')
```

The Editor round-trip
---------------------

[](#the-editor-round-trip)

The whole point: an agent (or a human in a WYSIWYG editor) works in markdown or the JSON model, and `.docx` is just a serialization at the edges.

```
use LastWord\Agent;

// Inbound: a Word file arrives → markdown for the Editor
$doc = Agent::read($uploadedBytes);
$markdown = Agent::toMarkdown($doc);

// … the Editor (or an agent) edits the markdown …

// Outbound: markdown → model → a real .docx
$doc = Agent::fromMarkdown($markdown);
$bytes = Agent::toBytes($doc);
```

The bridge is hand-rolled GFM (no external markdown dependency): headings, `**bold**` / `*italic*` / `~~strike~~` / ``code``, links, ordered + unordered nested lists, tables, fenced code blocks, blockquotes, `![alt](src)` images, `---` rules — plus an `\` comment convention so page breaks survive the trip. Underline / colors / alignment have no markdown slot and drop on that path (they round-trip fine through `.docx` itself).

Document model
--------------

[](#document-model)

A `Doc` is a title plus a flat list of blocks; camelCase keys, plain associative arrays, identical in the Node mirror:

```
{ "title": "Optional title", "blocks": [ /* Block[] */ ] }
```

Runs (inline text spans) carry the formatting:

```
{ "text": "Hello", "bold": true, "italic": true, "underline": true,
  "strike": true, "code": true, "link": "https://…",
  "color": "#RRGGBB", "highlight": "#RRGGBB" }
```

Blocks, discriminated by `type`:

TypeShape`heading``{ level: 1-6, runs }``paragraph``{ runs, align?: "left"|"center"|"right"|"justify" }``list``{ ordered?, items: [{ runs, children? }] }` — nesting to 6 levels`table``{ rows: [{ header?, cells: [{ blocks }] }] }``code``{ language?, text }` — multiline, monospace, shaded`quote``{ blocks }``image``{ src: "data:image/png;base64,…", widthPx?, heightPx?, alt? }``pageBreak``{ }``hr``{ }`Image dimensions are optional — the writer sniffs intrinsic size straight from the PNG IHDR / JPEG SOF bytes and caps at 6.5in page width keeping aspect.

Agent API
---------

[](#agent-api)

Static façade, mirrored exactly in the Node package:

MethodPurpose`Agent::validate($doc)`structured `{path, message}[]`, empty = valid`Agent::validateAndRepair($doc)``{ok, schema, errors}` — heuristic repair of near-miss agent output`Agent::toBytes($doc)`DOCX bytes; throws `SchemaException` when invalid`Agent::write($doc, $path)`write to disk → `{path, bytes, blocks}``Agent::read($bytesOrPath)` / `Agent::fromBytes($bytes)`parse a real .docx back into the model`Agent::toMarkdown($doc)` / `Agent::fromMarkdown($md)`the Editor bridge`Agent::describe($doc)`plain-text summary (title, block counts, word count)`Agent::jsonSchema()`JSON Schema for LLM tool registration`Agent::version()`package version`validateAndRepair()` is built for agentic feedback loops: bare strings become runs, `"text"` shorthand becomes runs, heading levels clamp to 1-6, unknown block types drop with the error retained, missing `blocks`defaults to `[]` — hand the errors back to the model if `ok` is false.

Reading Word-authored files
---------------------------

[](#reading-word-authored-files)

`Agent::read()` handles more than its own writer output: headings via `Heading1-9` styles or `outlineLvl`, run formatting including named highlight colors, hyperlinks through the rels part, `numPr` lists with `ilvl` nesting (unknown numbering buckets as unordered), tables, inline images (returned as data URLs), page breaks and border-only paragraphs. Unknown constructs degrade to plain paragraphs — the reader never throws on strange XML.

Determinism
-----------

[](#determinism)

`toBytes()` is reproducible: no timestamps in any XML part, fixed zip entry order, and every entry's mtime pinned. The same document yields the same bytes on every call — diff-able artifacts, cache-friendly outputs.

Cross-language parity
---------------------

[](#cross-language-parity)

As of 0.2.0 the metadata slots match the Node mirror exactly: the title is carried in `docProps/core.xml` (`dc:title`) and the code block `language`in a `lastword:code:{lang}` content-control tag (quotes use `lastword:quote`), so the **same file opens in either engine** — title and code language round-trip PHP ↔ Node in both directions. Files written by 0.1.x (Title-styled paragraph, `LastWordCode_{lang}` bookmark) still read fine; the sibling repo's canonical fixture is frozen into each test suite as a cross-read vector.

Testing
-------

[](#testing)

```
composer install
composer test
```

License
-------

[](#license)

MIT

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance93

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~11 days

Total

4

Last Release

14d ago

PHP version history (2 changes)v0.1.0PHP ^8.2

v0.3.0PHP ^8.4

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/461446?v=4)[Wish Born](/maintainers/wishborn)[@wishborn](https://github.com/wishborn)

---

Top Contributors

[![wishborn](https://avatars.githubusercontent.com/u/461446?v=4)](https://github.com/wishborn "wishborn (2 commits)")

---

Tags

aimarkdownworddocxofficedocumentagenticdocument-creationparticle-academy

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/particle-academy-last-word/health.svg)

```
[![Health](https://phpackages.com/badges/particle-academy-last-word/health.svg)](https://phpackages.com/packages/particle-academy-last-word)
```

###  Alternatives

[gotenberg/gotenberg-php

A PHP client for interacting with Gotenberg, a developer-friendly API for converting numerous document formats into PDF files, and more!

3906.6M34](/packages/gotenberg-gotenberg-php)[vaites/php-apache-tika

Apache Tika bindings for PHP: extracts text from documents and images (with OCR), metadata and more...

1171.5M2](/packages/vaites-php-apache-tika)[aspose-cloud/aspose-words-cloud

Open, generate, edit, split, merge, compare and convert Word documents. Integrate Cloud API into your solutions to manipulate documents. Convert PDF to Word (DOC, DOCX, ODT, RTF and HTML) and in the opposite direction.

33182.7k](/packages/aspose-cloud-aspose-words-cloud)[paperdoc-dev/paperdoc-lib

A zero-dependency PHP library for generating, parsing and converting documents — PDF, DOCX, XLSX, PPTX, HTML, Markdown, CSV and legacy Office formats

1315.4k](/packages/paperdoc-dev-paperdoc-lib)[mnvx/lowrapper

PHP wrapper over LibreOffice converter

127205.2k](/packages/mnvx-lowrapper)[novay/laravel-word-template

Package Laravel untuk melakukan penggantian kata pada file menggunakan template dokumen (.doc atau .docx) yang sudah disediakan.

5617.8k](/packages/novay-laravel-word-template)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
