phptemplatesms-wordphpword

How to Replace DOCX Placeholder with Formatted HTML in PHPWord where data came from the HubSpot?


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;
    }
}

Solution

  • 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;
    }