PHPackages                             acat/render-engine - 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. acat/render-engine

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

acat/render-engine
==================

renders documents

059[1 PRs](https://github.com/HobbyFrosch/acat-render-engine/pulls)PHP

Since Jul 28Pushed 3w agoCompare

[ Source](https://github.com/HobbyFrosch/acat-render-engine)[ Packagist](https://packagist.org/packages/acat/render-engine)[ RSS](/packages/acat-render-engine/feed)WikiDiscussions main Synced yesterday

READMEChangelogDependenciesVersions (2)Used By (0)

acat-render-engine
==================

[](#acat-render-engine)

Renders `.docx` Word documents as templates: fields, repeating blocks and conditional logic can be defined directly inside the document by an author in Word, and are then filled in and evaluated against PHP data at render time.

Requirements
------------

[](#requirements)

- PHP &gt;= 8.5
- Extensions: `dom`, `json`, `mbstring`, `openssl`, `xml`, `zip`, `libxml`

Installation
------------

[](#installation)

```
composer require acat/render-engine
```

How it works
------------

[](#how-it-works)

A `.docx` file is a zip archive of XML parts (`word/document.xml`, `word/settings.xml`, headers, footers, ...). The engine:

1. Opens the archive and reads its content parts (`ACAT\Document\Word\WordDocument`).
2. **Normalizes** each part: Word frequently splits a placeholder across several `` runs whenever the text has mixed formatting (e.g. `${F:101}` typed with a different font on the digits). The `Normalizer` finds these runs and merges them back into a single text node before parsing continues (`ACAT\Parser\Normalizer\Normalizer`).
3. **Generates tags**: every `${...}` marker found in the merged text is turned into a small custom XML element (`acat:field`, `acat:text`, `acat:view`, `acat:block`, `acat:condition`) inserted right into the Word XML, next to the surrounding `w:t`/`w:r`/`w:p` nodes (`ACAT\Parser\Tag\TagGenerator`).
4. **Collects elements**: `ACAT\Parser\Element\ElementGenerator` walks the tagged document and groups everything into fields, text, views, conditions and blocks (with their child elements).
5. **Renders**: `ACAT\Render\RenderEngine` replaces each field/text/view element with the corresponding value, evaluates conditions and repeats blocks, then `WordDocument::save()` writes the modified parts back into the `.docx` archive.

Placeholder syntax
------------------

[](#placeholder-syntax)

Markers are written directly into the Word document text as `${...}`.

MarkerMeaning`${F:id}`**Field.** Replaced with the plain-text value for `id`.`${T:text}`**Static text.** Replaced with `text` itself; mainly useful together with a condition, e.g. to print a fixed label only when a condition is met.`${V:id}`**View.** Like a field, but its value may contain the literal substring `` to insert line breaks — useful for multi-line values (e.g. an address) that a plain field can't represent.`${B:0}` / `${B:1}`**Block start / block end.** Marks the boundaries of a repeating region (see below).`${C:id:expression:action}`**Condition.** Evaluates `expression` against the value of field `id` and, if true, performs `action` (see below).### Fields, text and views

[](#fields-text-and-views)

```
Customer number: ${F:101}

```

If the rendered data provides a value for field `101`, the marker is replaced with it; if no value is provided, the marker is simply removed (a missing value renders as nothing, not as an error).

### Conditions

[](#conditions)

```
${C:101::0}

```

Supported operators: `=`, ``, ``, `>=`, ` [
        'fields' => [
            '101' => 'Ada Lovelace',
        ],
        'views' => [
            '102' => "Erste ZeileZweite Zeile",
        ],
        'blocks' => [
            // block 0 = the first ${B:0}...${B:1} pair found in the document
            0 => [
                'fields' => [
                    0 => ['101' => 'Row one'],
                    1 => ['101' => 'Row two'],
                ],
            ],
        ],
    ],
];

$engine = new RenderEngine($logger); // $logger is optional, any PSR-3 LoggerInterface
$engine->render($document, $values);
```

Values are keyed by the content part they belong to (`word/document.xml`, or a header/footer part), then by `fields`, `views` and `blocks`. Blocks are addressed by their numeric position in the document (`0` for the first block found, `1` for the second, and so on), and each block's `fields`/ `views` is itself a list of one value-set per repeated row.

Inspecting a template
---------------------

[](#inspecting-a-template)

`ACAT\Render\RecordStructure` parses a template without rendering it and reports which field, view, condition and block IDs it references — useful for validating a template or building a form for the data it expects:

```
use ACAT\Document\Word\WordDocument;
use ACAT\Render\RecordStructure;

$structure = new RecordStructure(new WordDocument('/path/to/template.docx'));
$recordStructure = $structure->getRecordStructure();
// ['word/document.xml' => ['views' => [...], 'fields' => [...], 'blocks' => [...], 'conditions' => [...]]]
```

`validate()` checks the same document for `${B:0}`/`${B:1}` markers that could not be paired into a block — e.g. one of them was accidentally deleted or duplicated while editing the template. An unpaired marker is never rendered or cleaned up; it silently stays in the output as a raw XML element, so this is meant to be checked before rendering:

```
$problems = $structure->validate();
// [] if the template is fine, otherwise e.g. ['word/document.xml' => ['1 block marker(s) could not be paired into a start/end block']]
```

Password-protecting a document
------------------------------

[](#password-protecting-a-document)

`WordDocument::protect(?string $password = null)` marks the document read-only using Word's own document-protection mechanism (a random password is generated if none is given).

**This is not encryption and not access control.** It is the same "Restrict Editing" feature Word itself offers — a UI-level restriction that Word enforces while editing, not a way to keep the document's contents confidential. Anyone with the file can read it normally, and the restriction is trivially removed by deleting the `w:documentProtection` element from `word/settings.xml` or by using any of the widely available password-removal tools. Use it to discourage accidental edits, not to protect sensitive content.

Testing
-------

[](#testing)

```
composer install
vendor/bin/phpunit
```

License
-------

[](#license)

MIT, see [LICENSE](LICENSE) — free to use, modify and distribute, provided the copyright notice (Ronny Krämer, Akademie für Weiterbildung, Universität Bremen) stays in every copy.

###  Health Score

23

—

LowBetter than 25% of packages

Maintenance62

Regular maintenance activity

Popularity8

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity13

Early-stage or recently created project

 Bus Factor1

Top contributor holds 71.4% 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.

### Community

Maintainers

![](https://www.gravatar.com/avatar/487e70334c06c6f9f93bbf424132fc7813099459cb92b61b518d39a938dc4106?d=identicon)[rkraemer](/maintainers/rkraemer)

---

Top Contributors

[![f-3-l-i-x](https://avatars.githubusercontent.com/u/65679730?v=4)](https://github.com/f-3-l-i-x "f-3-l-i-x (15 commits)")[![HobbyFrosch](https://avatars.githubusercontent.com/u/65679730?v=4)](https://github.com/HobbyFrosch "HobbyFrosch (6 commits)")

### Embed Badge

![Health badge](/badges/acat-render-engine/health.svg)

```
[![Health](https://phpackages.com/badges/acat-render-engine/health.svg)](https://phpackages.com/packages/acat-render-engine)
```

###  Alternatives

[rodenastyle/stream-parser

PHP Multiformat Streaming Parser

442206.4k2](/packages/rodenastyle-stream-parser)[cybermonde/odtphp

ODT document generator

49123.4k1](/packages/cybermonde-odtphp)[akeneo-labs/excel-connector-bundle

Akeneo PIM Excel connector bundle

166.4k](/packages/akeneo-labs-excel-connector-bundle)

PHPackages © 2026

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