I’m working with PHPWord library in my Laravel project to substitute placeholders in DOCX templates. My template has a placeholder called ${content} that needs to be filled with HTML markup like <p>Hi there,<br><br><em>Thanks for joining</em></p>
The issue is that PHPWord treats the HTML as plain text instead of applying the formatting. I need the HTML to display properly with italics, line breaks, and other formatting.
I looked through PHPWord docs but haven’t found a working approach for HTML conversion. Has anyone dealt with this before?
Here’s my current implementation:
<?php
namespace App\Helpers;
use App\Exceptions\DocumentProcessorException;
use App\Traits\Instantiable;
use PhpOffice\PhpWord\Exception\CopyFileException;
use PhpOffice\PhpWord\Exception\CreateTemporaryFileException;
use PhpOffice\PhpWord\Settings;
use PhpOffice\PhpWord\Shared\Html;
use PhpOffice\PhpWord\Shared\XMLWriter;
use PhpOffice\PhpWord\TemplateProcessor;
use PhpOffice\PhpWord\Writer\Word2007\Element\Container;
class DocumentProcessor
{
use Instantiable;
public function __construct(private $templateFile, private $resultFile, private $dataFields = [])
{
if (pathinfo(parse_url($this->templateFile, PHP_URL_PATH), PATHINFO_EXTENSION) !== 'docx') {
throw DocumentProcessorException::invalidFileFormat();
}
}
public function process(): string
{
$processor = $this->createProcessor($this->templateFile);
foreach ($this->dataFields as $fieldName => $fieldData) {
$this->handleField($processor, $fieldName, $fieldData);
}
return $this->saveDocument($processor, $this->resultFile);
}
protected function createProcessor(string $template): TemplateProcessor
{
return new TemplateProcessor($template);
}
protected function handleField(TemplateProcessor $proc, string $name, array $info): void
{
$type = data_get($info, 'type');
$content = data_get($info, 'content');
if ($this->isSignatureType($name, $content)) {
$proc->setImageValue($name, $content);
} elseif ($type === 'html') {
if (empty($content)) {
$proc->setValue($name, $content);
return;
}
$cleanHtml = $this->cleanHtmlContent($content);
$document = new \PhpOffice\PhpWord\PhpWord;
$page = $document->addSection();
Html::addHtml($page, $cleanHtml);
$writer = new XMLWriter;
$elementWriter = new Container($writer, $page, false);
$elementWriter->write();
$proc->replaceXmlBlock($name, $writer->getData());
} elseif ($this->isImageType($type)) {
foreach ($content as $index => $fileData) {
$proc->setValue($name, '${'.$name.$index.'}'.'${'.$name.'}');
$filePath = $this->getFilePath($fileData);
$imgData = $filePath ? @getimagesize($filePath) : null;
if ($imgData && in_array($imgData['mime'], ['image/jpeg', 'image/png', 'image/bmp', 'image/gif'])) {
$proc->setImageValue($name.$index, $fileData);
} else {
Settings::setOutputEscapingEnabled(true);
$proc->setValue($name.$index, $filePath);
}
}
$proc->setValue($name, '');
} else {
Settings::setOutputEscapingEnabled(false);
$content = htmlspecialchars($content);
$content = preg_replace('~\R~u', '</w:t><w:br/><w:t>', $content);
$proc->setValue($name, $content);
}
}
protected function cleanHtmlContent(string $html): string
{
$html = preg_replace('#<br(?![^>]*\/)>#i', '<br />', $html);
$html = preg_replace('#<hr(?![^>]*\/)>#i', '<hr />', $html);
$html = preg_replace('#<img([^>]*)(?<!/)>#i', '<img$1 />', $html);
return $html;
}
protected function isSignatureType(string $name, mixed $content): bool
{
return in_array($name, ['user_signature', 'admin_signature']) && $content;
}
protected function isImageType(string $type): bool
{
return $type === 'image';
}
protected function getFilePath(mixed $data): mixed
{
return is_array($data) ? data_get($data, 'file_path') : $data;
}
protected function saveDocument(TemplateProcessor $proc, string $output): string
{
$proc->saveAs($output);
return $output;
}
}