You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Laravel中使用PHPWord替换DOCX模板HTML占位符时生成损坏文件的解决求助

Laravel中使用PHPWord替换DOCX模板HTML占位符时生成损坏文件的解决求助

大家好,我目前在Laravel项目里使用PHPWord包处理DOCX模板替换,需求是把模板里的${html}占位符替换成带格式的HTML内容(比如<p>Hello,<br><br><strong>Welcome</strong></p>)。但现在遇到的问题是,PHPWord会把HTML标签直接作为纯文本插入,没法渲染出对应的格式(比如粗体、换行效果)。

我翻了PHPWord的官方文档,没找到处理HTML解析替换的清晰方案,后来参考了一个实现思路,但按照这个思路写代码后,生成的DOCX文件直接损坏了,根本打不开。下面是我写的DocxKeyReplacer服务类代码,麻烦各位帮忙看看哪里出了问题,或者有没有更可靠的实现方式?

<?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($processor, $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;
    }
}

我主要的疑问点在处理HTML字段的这段逻辑:通过创建临时的PhpWord实例和Section,用Html类转换HTML内容,再生成XML来替换模板块,但这么做为什么会导致文件损坏呢?有没有正确的方式可以实现HTML格式的模板替换?

内容来源于stack exchange

火山引擎 最新活动