wakeasp-inc/opentbs-bundle
OpenTBS bundle for PHP that generates DOCX, XLSX, PPTX, ODT and other Office documents from templates. Merge data with simple template tags for fast, reliable document generation.
This is a Symfony bundle, not a Laravel package. It wraps TinyButStrong's OpenTBS engine — the actual template-merging library — and its only real job is to register a preconfigured clsOpenTBS-style service plus a temp directory. That framing should drive everything below: the value is in OpenTBS itself, and the bundle is ~50 lines of glue you can read in one sitting.
First steps:
composer require wakeasp-inc/opentbs-bundle (check the actual vendor/namespace — with zero dependents, assume nothing).config/bundles.php (auto-registered by Flex if it ships a recipe; it almost certainly does not, so add it manually).bin/console debug:container | grep -iE 'tbs|opentbs'
bin/console config:dump-reference opentbs # config key may differ
vendor/wakeasp-inc/opentbs-bundle/ and read DependencyInjection/Configuration.php and Resources/config/services.*. The temp-dir setting and service id are the only two things you need from it. Do this first — with 0 stars and a 2022 last release, the bundle's public surface is whatever the code says, not what the README claims.Your first use case should be a single DOCX invoice or contract: a .docx template committed to templates/, [client.name] for scalars, one [line;block=begin] … [line;block=end] row for line items, and a controller that streams the result.
$tbs = $container->get('opentbs'); // verify id via debug:container
$tbs->LoadTemplate($path.'/invoice.docx');
$tbs->MergeField('client', $client);
$tbs->MergeBlock('line', $lines);
$tbs->Show(OPENTBS_DOWNLOAD, 'invoice.docx'); // or OPENTBS_FILE, OPENTBS_STRING
For Laravel: do not install this bundle. It extends Symfony\Component\HttpKernel\Bundle\Bundle and leans on the Symfony DI container; Laravel has neither auto-discovery for it nor a compatible container extension. Install tinybutstrong/opentbs directly and wrap it in a 30-line service — you lose nothing, because the bundle adds nothing you can't write yourself.
Wrap it in your own service, always. Don't inject the bundle service into twenty controllers. One DocumentRenderer with render(string $template, array $data, string $outputPath): string keeps the OpenTBS surface area contained and makes the eventual swap to raw tinybutstrong/opentbs (or away from it entirely) a one-file change.
Template authoring is the real skill. Treat .docx/.xlsx files as code: store them in resources/templates/, version them, and review template diffs by unzipping and diffing word/document.xml or xl/worksheets/sheet1.xml. Name fields to mirror your data structures ([client.address.city]) so MergeField maps without translation layers.
Loops. Put [row;block=begin] and [row;block=end] in the same table row for DOCX; OpenTBS duplicates the row per item. For XLSX use row blocks and [row.cell] field names. Sub-blocks nest inside parent blocks and inherit the parent context — use them for order → line items → tax lines instead of flattening data.
Pictures use ope=changepic: [client.logo;ope=changepic;default=logo.png]. The template must contain a placeholder image of the right aspect ratio; OpenTBS resizes within the existing shape. This is the single most-used feature in invoice/report work and the most common source of "why is my logo squashed" tickets.
Output modes matter for scale. OPENTBS_DOWNLOAD streams, OPENTBS_FILE writes to disk, OPENTBS_STRING holds the whole document in memory. In Laravel, response()->streamDownload(fn () => $tbs->Show(OPENTBS_ECHO), 'x.docx') keeps memory flat; reserve OPENTBS_STRING for small files or unit tests.
Queue anything heavy. A 200-page merged DOCX is CPU- and memory-bound; a report of 5k rows belongs in a ShouldQueue job writing to Storage::disk('local'), with the controller returning a signed URL. Set memory_limit and max_execution_time explicitly in the job.
PDF is not included. OpenTBS outputs Office formats only. The standard pattern is libreoffice --headless --convert-to pdf (or unoconv) on the generated file — a shell-out that needs its own queue, timeout, and sandboxing. Budget for it; it's usually 60% of the effort of a "just generate a PDF invoice" ticket.
Multi-sheet and charts. Select a sheet with $tbs->PlugIn(OPENTBS_SELECT_SHEET, $tbs, 'Sheet2') before merging that sheet's blocks; charts are populated via $tbs->PlugIn(OPENTBS_CHART, $tbs, 'Chart1', 'series_name', $data). Both are documented on the OpenTBS site, not in this bundle.
Testing. Snapshot the generated ZIP: unzip, normalize docProps/core.xml (timestamps differ every run), then compare word/document.xml against a fixture. Field-level assertions (assertStringContainsString('ACME-001', $xml)) catch regressions without brittle full-document diffs.
[client.name] across multiple <w:r> runs if you type it slowly, paste it, or let autocorrect touch it. OpenTBS handles many cases, but not all. Author tags in one keystroke sequence, disable autocorrect for brackets, and when a field silently doesn't merge, unzip the docx and grep document.xml for the tag.$tbs->Plugin(OPENTBS_DEBUG_INFO) shows what OpenTBS parsed; OPENTBS_DEBUG_XML_SHOW / OPENTBS_DEBUG_XML_CURRENT dump the working XML. These are far more useful than var_dump and are the fastest route to "why did my block not repeat."sys_get_temp_dir() is writable and not noexec/read-only; otherwise LoadTemplate fails with an opaque error. Check the bundle's temp-dir config key and set it explicitly to storage_path('app/opentbs-tmp') in Laravel..docx, .xlsx, .pptx, .odt, .ods each pick a different internal handler. Renaming template.docx to template.xlsx produces garbage, not an error.[ character is the escape hatch. Literal brackets in boilerplate text need [ escaping conventions; a stray unmatched bracket can make a whole paragraph vanish from output. Audit templates after edits.OPENTBS_ALREADY_UTF8 to LoadTemplate if your source strings are already UTF-8. Watch XML-illegal control characters in merged data — sanitize before MergeField or the output file won't open.PlugIn directly, or decorate the service. Don't fork the bundle for a one-line config change; a compiler pass in your own app is cheaper.composer require wakeasp-inc/opentbs-bundle:^1.0 with a committed composer.lock, plus a note in the README that the upstream is dormant. Future-you will appreciate the context.How can I help you explore Laravel packages today?