ggggino/wordbundle
A PHP bundle for working with Microsoft Word (.docx) documents, wrapping PHPWord to generate, template and export Word files from your application. Simplifies document creation with reusable templates and straightforward configuration.
ggggino/wordbundle is a thin Symfony2-era DI wrapper around PHPWord: it registers a PHPWord service and a small configuration tree so you don't have to hand-instantiate \PhpOffice\PhpWord\PhpWord in every controller. With zero stars and zero dependents, treat it as a convenience layer, not a maintained abstraction — the bundle source is small, and reading it is the real onboarding step.
composer require ggggino/wordbundle
app/AppKernel.php:public function registerBundles()
{
$bundles = [
// ...
new Ggggino\WordBundle\GggginoWordBundle(),
];
}
app/config/config.yml:ggggino_word: ~
Read two files in this order before writing code:
DependencyInjection/Configuration.php — the actual knobs (default font/size, temp directory, document defaults).DependencyInjection/GggginoWordExtension.php and Resources/config/services.yml — the service id you'll inject and whether the service is shared.First use case — generate a .docx from a controller:
public function exportAction()
{
/** @var \PhpOffice\PhpWord\PhpWord $word */
$word = $this->get('ggggino_word'); // confirm id in services.yml
$section = $word->addSection();
$section->addTitle('Quarterly Report', 1);
$section->addText('Generated at ' . date('c'));
$writer = \PhpOffice\PhpWord\IOFactory::createWriter($word, 'Word2007');
$tmp = tempnam(sys_get_temp_dir(), 'docx');
$writer->save($tmp);
return new \Symfony\Component\HttpFoundation\BinaryFileResponse($tmp, 200, [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Content-Disposition' => 'attachment; filename="report.docx"',
]);
}
That's the smallest end-to-end loop: service → content → writer → response. Once it works, everything else is PHPWord API plus wiring.
Prefer constructor injection over $this->get(). The bundle is Symfony2-style, so you'll need to define your own service definition to autowire:
services:
app.report_exporter:
class: AppBundle\Service\ReportExporter
arguments: ['@ggggino_word'] # actual id from services.yml
Template filling is the dominant day-to-day pattern. Most real work is "open a .docx the business team designed, replace placeholders, send it back." Use PHPWord's TemplateProcessor directly with a path, rather than asking the bundle to do it — the bundle rarely wraps this:
$tpl = new \PhpOffice\PhpWord\TemplateProcessor($pathToUploadedDocx);
$tpl->setValue('customer_name', $customer->getName());
$tpl->setValue('invoice_total', number_format($total, 2));
$tpl->saveAs($outPath);
Build vs. fill. Use TemplateProcessor when the layout matters to a human; use programmatic addSection/addTable/addTextRun when the document is data-shaped (statements, exports, batch generation). Mixing the two in one class usually causes style drift — keep two services.
Read/parse uploaded documents through IOFactory::load($path) and walk sections/elements only when you need metadata (headings, plain text extraction). Don't attempt round-trip editing of arbitrary user .docx files; PHPWord's reader is lossy for complex layouts.
Stream instead of temp-file when you can. For small documents, use php://output and StreamedResponse:
return new StreamedResponse(function () use ($writer) {
$writer->save('php://output');
}, 200, [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Content-Disposition' => 'attachment; filename="report.docx"',
]);
Decouple with your own factory. Wrap the bundle service in AppBundle\Document\WordFactory that returns PhpWord instances. When the bundle dies or is replaced, you change one class instead of every controller. This is the single highest-value integration tip for a package with no dependents.
Async/batch generation. Generate to a storage path in a console command or message handler, return the path, and let the web request just download it. Long-running workers must reset or recreate the PhpWord instance per document — see gotchas.
Mail attachments. Write the file to a temp path, attach it via your mailer (SwiftMailer or Symfony\Component\Mime\Email::attachFromPath), and delete in a finally. Don't hold the BinaryFileResponse pattern for email.
Abandonment is the number-one risk. No stars, no dependents, no activity means no Symfony 5/6 support and no security patches. Vendor the two or three files you actually use, or skip the bundle and require phpoffice/phpword directly — the wrapper adds very little. If you keep it, isolate it behind your own service as above.
Namespace/version drift in PHPWord. Older releases use PhpOffice\PhpWord and renamed classes across 0.13–0.14 (PHPWord → PhpWord, IOFactory behavior changes). The bundle pins a range; check composer.lock and confirm the writer name strings ('Word2007') match the installed version. Mismatched versions produce "class not found" or silent empty documents.
Shared service = state leak. Symfony services are shared by default. If the bundle registers PhpWord as a singleton, sections and styles accumulate across calls — in a worker processing 100 invoices you'll get one giant 100-section document. Either request a fresh instance from a factory or clone/reset between uses.
BinaryFileResponse cleanup. It does not delete the temp file unless you call deleteFileAfterSend(true). Conversely, calling it on a file you also want to keep will surprise you. Either way, orphaned files in sys_get_temp_dir() accumulate silently.
Missing Content-Type. PHPWord writes a valid .docx, but if you omit the MIME header browsers may save it as .zip (a .docx is a zip). Always set both Content-Type and Content-Disposition.
TemplateProcessor escaping. setValue HTML-escapes by default in recent PHPWord versions — placeholders with &, <, or quotes get mangled. Use setValue($key, $val, true) where supported, or pre-sanitize. Also: placeholders split across runs in Word (${name} typed in pieces) won't match; retype them in one go.
Config quirks. Symfony2 bundles often fail silently on unknown config keys under older Configuration implementations. Start with ggggino_word: ~, verify the bundle boots, then add keys one at a time from Configuration.php. Use bin/console config:dump-reference ggggino_word if the bundle implements it (many Symfony2 bundles don't).
Debugging. When output is blank or corrupt, dump the element tree instead of guessing: $word->getSections() and iterate. Wrap writer calls in try/catch — PHPWord throws generic \Exception on bad template paths rather than typed exceptions, so log $e->getMessage() plus the source path.
Extension points are limited. Expect no events, no compiler passes worth using, and no documented hooks. The realistic override is a compiler pass or bundle config that replaces the ggggino_word service definition with your own class as long as it satisfies the same interface (often just PhpWord itself). Test that override in a kernel-boot test.
Testing. Booting the Symfony2 kernel for document tests is slow. Test your ReportExporter/WordFactory against real PHPWord directly, and keep one thin integration test that asserts the bundle service exists and is the expected class. Assert output by unzipping the .docx and grepping word/document.xml for expected strings — cheaper and more reliable than binary comparison.
How can I help you explore Laravel packages today?