I'm using the PHPWord package in Laravel to replace placeholders in a DOCX template.
I have a placeholder ${html} in my DOCX file, and I'm trying to replace it with HTML content like:
<p>Hello,<br><br><strong>Welcome</strong></p>
Currently, PHPWord is inserting the raw HTML tags as text, but I want the content to be rendered with proper formatting (bold text, line breaks, etc.).
I've checked the PHPWord documentation but couldn't find a clear solution for HTML parsing. Any suggestions or examples would be appreciated.
So far, I've referred this solution. But it's creating corrupted docx file.
I've done this cdode:
<?php
namespace App\Services;
use App\Exceptions\DocxKeyReplacerException;
use App\Traits\Makeable;
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 DocxKeyReplacer
{
use Makeable;
/**
* @throws DocxKeyReplacerException
*/
public function __construct(private $inputFile, private $outputFile, private $properties = [])
{
if (pathinfo(parse_url($this->inputFile, PHP_URL_PATH), PATHINFO_EXTENSION) !== 'docx') {
throw DocxKeyReplacerException::formatIsInvalid();
}
}
/**
* Executes the process of filling a document template with data and images.
*
* @return string Path to the output file.
*
* @throws CopyFileException
* @throws CreateTemporaryFileException
*/
public function execute(): string
{
$templateProcessor = $this->initializeTemplateProcessor($this->inputFile);
foreach ($this->properties as $property => $data) {
$this->processTemplateProperty($templateProcessor, $property, $data);
}
return $this->finalizeTemplateProcessing($templateProcessor, $this->outputFile);
}
/**
* Initialize the template processor with the input file.
*
* @throws CopyFileException
* @throws CreateTemporaryFileException
*/
protected function initializeTemplateProcessor(string $inputFile): TemplateProcessor
{
return new TemplateProcessor($inputFile);
}
/**
* Process a single property and set it in the template processor.
*/
protected function processTemplateProperty(TemplateProcessor $processor, string $property, array $data): void
{
$fieldType = data_get($data, 'field_type');
$value = data_get($data, 'value');
if ($this->isSignatureField($property, $value)) {
$processor->setImageValue($property, $value);
} elseif ($fieldType === 'html') {
if (empty($value)) {
$processor->setValue($property, $value);
return;
}
$markup = $this->sanitizeHtml($value);
$phpWord = new \PhpOffice\PhpWord\PhpWord;
$section = $phpWord->addSection();
Html::addHtml($section, $markup);
$xmlWriter = new XMLWriter;
$containerWriter = new Container($xmlWriter, $section, false);
$containerWriter->write();
$processor->replaceXmlBlock($property, $xmlWriter->getData());
} elseif ($this->isFileFieldWithImage($fieldType)) {
foreach ($value as $key => $fileObject) {
$processor->setValue($property, '${'.$property.$key.'}'.'${'.$property.'}');
$path = $this->extractPath($fileObject);
$imageInfo = $path ? @getimagesize($path) : null;
// TemplateProcessor supports only following mime type
if ($imageInfo && in_array($imageInfo['mime'], ['image/jpeg', 'image/png', 'image/bmp', 'image/gif'])) {
$processor->setImageValue($property.$key, $fileObject);
} else {
Settings::setOutputEscapingEnabled(true);
$processor->setValue($property.$key, $path);
}
}
$processor->setValue($property, '');
} else {
Settings::setOutputEscapingEnabled(false);
$value = htmlspecialchars($value);
$value = preg_replace('~\R~u', '</w:t><w:br/><w:t>', $value);
$processor->setValue($property, $value);
}
}
protected function sanitizeHtml(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;
}
/**
* Checks if the given field is a signature field.
*/
protected function isSignatureField(string $property, mixed $value): bool
{
return in_array($property, ['signature', 'second_signature']) && $value;
}
/**
* Checks if the field type is 'file' and the value contains an image path.
*/
protected function isFileFieldWithImage(string $fieldType): bool
{
return $fieldType === 'file';
}
/**
* Extracts image path or direct value based on whether the value is an array.
*/
protected function extractPath(mixed $value): mixed
{
return is_array($value) ? data_get($value, 'path') : $value;
}
/**
* Save the processed template to the output file and return its path.
*/
protected function finalizeTemplateProcessing(TemplateProcessor $processor, string $outputFile): string
{
$processor->saveAs($outputFile);
return $outputFile;
}
}
This works for me:
\PhpOffice\PhpWord\Settings::setOutputEscapingEnabled(true);
if (empty($value)) {
$processor->setValue($property, $value);
return;
}
$markup = $this->sanitizeHtml($value);
$phpWord = new \PhpOffice\PhpWord\PhpWord;
$section = $phpWord->addSection();
Html::addHtml($section, $markup);
$xmlWriter = new XMLWriter;
$containerWriter = new Container($xmlWriter, $section, false);
$containerWriter->write();
$processor->replaceXmlBlock($property, $xmlWriter->getData());
protected function sanitizeHtml(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;
}