mbence/opentbs-bundle
A Symfony bundle that integrates OpenTBS, the PHP template engine for merging data into OpenOffice and Microsoft Office documents such as docx, xlsx, pptx, and odt files. Generate dynamic reports and documents from templates using simple tags.
Don't install the bundle — take the library. mbence/opentbs-bundle is a Symfony bundle (DI extension, services.xml, kernel wiring) last released in 2016 and archived. Its composer.json targets Symfony 2/3 and PHP 5.x, so composer require mbence/opentbs-bundle in a Laravel 9+ app will either hard-conflict or drag in an ancient Symfony polyfill stack. The bundle contains almost no logic of its own — it is a thin container wrapper around TinyButStrong's clsTinyButStrong + clsOpenTBS plugin. Pull that instead:
composer require tinybutstrong/tinybutstrong-opentbs # or vendor clsOpenTBS.php + tbs_class.php directly
Where to look first in the repo: Resources/config/services.xml (shows exactly what the bundle instantiates — a clsTinyButStrong with TBS_INSTALL, OPENTBS_PLUGIN applied), the Service/ class that loads templates, and any demo .docx/.odt files under Resources/. Those demos are the most reusable artifact in the package; the PHP glue you can rewrite in 20 lines.
First use case: a certificate / appointment letter / quote. Author storage/app/templates/quote.docx in Word with [client.name], [quote.reference], [quote.total] typed in one go, then:
$tbs = new clsTinyButStrong;
$tbs->Plugin(TBS_INSTALL, OPENTBS_PLUGIN);
$tbs->LoadTemplate(storage_path('app/templates/quote.docx'), OPENTBS_ALREADY_UTF8);
$tbs->MergeField('client', $client->toArray());
$tbs->MergeField('quote', $quote->toArray());
return response()->streamDownload(fn () => $tbs->Show(OPENTBS_STRING, 'quote.docx'), 'quote.docx');
That is the whole package, minus the Symfony ceremony.
Bind a fresh instance per render. OpenTBS keeps merge state on the instance; never register it as a shared singleton (this bites hard under Octane/queue workers). A DocumentGenerator action class that constructs a new clsTinyButStrong per call is the right shape:
final class DocumentGenerator
{
public function render(string $templatePath, array $data, string $outName): StreamedResponse
{
$tbs = app(OpenTbsFactory::class)->make(); // new instance each time
$tbs->LoadTemplate($templatePath, OPENTBS_ALREADY_UTF8);
foreach ($data as $key => $value) {
is_array($value) && array_is_list($value)
? $tbs->MergeBlock($key, $value)
: $tbs->MergeField($key, $value);
}
return response()->streamDownload(fn () => $tbs->Show(OPENTBS_STRING, $outName), $outName);
}
}
Merge patterns you'll actually use:
| Need | Call |
|---|---|
| Scalars / nested arrays | MergeField('quote', [...]) → [quote.total] |
| Repeating table rows | MergeBlock('item', $rows) → [item.name;block=tr] on the row |
| Sub-blocks / conditionals | [item.notes;block=tr], [onshow.item.notes;when [item.notes]!=''] |
| Replace a logo | MergeField('logo', storage_path(...)) → [logo;ope=changepic] |
| Date/number formatting | [quote.date;ope=date(Y-m-d)], [quote.total;ope=numformat] |
| Multi-cell layout tricks | [subtotal;ope=mergecell], [x;ope=clearcell] |
Registry, not hardcoded paths. Store template filename + required keys in a document_templates table or config map so marketing can drop a new .docx in a disk without a deploy; version templates by filename (quote-v3.docx) and keep the placeholder contract in a README next to them.
Output routing. OPENTBS_STRING → response()->streamDownload(); OPENTBS_FILE → Storage::disk('local')->path() then Mail::attach(); OPENTBS_DOWNLOAD is a bad fit for Laravel (it echoes headers itself and fights the response stack).
Testing. Golden-file tests: render deterministic data with OPENTBS_STRING, unzip the result in the test (ZipArchive), and assert word/document.xml contains the expected strings. That catches broken placeholders that a byte-hash comparison would flag on every Word save.
each(), {} string offsets, dynamic properties) throws deprecations or fatals; pin TBS 3.15+/OpenTBS 1.10+ or stay on PHP 7.4. Since the bundle is archived with 0 dependents, you own the code — vendor it and vendor the OpenTBS class rather than trusting Packagist resolution.TBS_TEMP_DIR / sys_temp_dir), or you get "cannot open file" / "not a zip archive" deep inside TbsZip.[client.name] with autocorrect, then editing mid-string leaves it fragmented across <w:r> runs, and TBS silently leaves the tag in the output. Paste as plain text, type in one pass, and never let Word "help" with brackets. Stray [ anywhere in body text is treated as a field.block=tr must sit on the tag in the row that repeats; nested blocks need the inner block declared first and named distinctly. A block that repeats the header row is the #1 "why is my table wrong" bug.\n in a merged value renders as a space in Word. Use separate block rows/paragraphs, or split on \n and merge an array.OPENTBS_ALREADY_UTF8), so pass entities as literals — but you cannot inject rich text/HTML. Anything beyond plain text needs template-side design.NUMPAGES, TOC entries, and cross-references stay stale until opened in Word/Excel. For XLSX, either write values instead of formulas or run LibreOffice headless if the output must be final.Show(OPENTBS_DEBUG_XML_SHOW) (or OPENTBS_DEBUG_XML for the file) to get the merged document.xml as text instead of a document — infinitely faster than opening Word to see whether [quote.total] merged. The bundle modeled this as a debug config flag; in Laravel just gate it on app()->isLocal().opentbs service. In Laravel, bind an OpenTbsFactory interface in a service provider and swap in a subclass (for temp dir, charset, or a custom ope= magnet) via $this->app->bind()..docx mail-merge, phpoffice/phpword's TemplateProcessor is the more mainstream Laravel choice and has a live maintainer. OpenTBS still wins on ODT, XLSX, dynamic image swapping (changepic), and complex repeating/nested table layouts.How can I help you explore Laravel packages today?