phpms-wordphpword

How to use template processor from PhpWord with more complex sections like tables, images, links?


How to use template processor to add more content than just simple values?

$templateProcessor = new TemplateProcessor(resource_path('prints/form.docx'));

$templateProcessor->setValue('date-now', $this->date(Carbon::now()));
$templateProcessor->setValue('title', $title);

$phpWord = IOFactory::load(resource_path('prints/form.docx'));
$section = $phpWord->addSection();

$section->addTitle($title, 0);
$section->addTextBreak();
// Add whole pages, with tables, images, titles, links
// Combine $templateProcessor replaceXmlBlock with $secion

Solution

  • Currently PhpWord dosnt provide a straight way of combining TemplateProcessor with a more complex use of IOFactory::load or sections.

    Using only IOFactory also destroys already setup styling for the document like header, footer, titles and so on.

    To get around the issue of losing formatting, images and other relationships inside the document, I made an extended template processor by copying various parts of PhPWord.

    
    use PhpOffice\PhpWord\Element\AbstractContainer;
    use PhpOffice\PhpWord\Element\Image;
    use PhpOffice\PhpWord\Element\Link;
    use PhpOffice\PhpWord\Exception\Exception;
    use PhpOffice\PhpWord\Media;
    use PhpOffice\PhpWord\PhpWord;
    use PhpOffice\PhpWord\Settings;
    use PhpOffice\PhpWord\Shared\XMLWriter;
    use PhpOffice\PhpWord\Shared\ZipArchive;
    use PhpOffice\PhpWord\TemplateProcessor;
    
    class ExtendedTemplateProcessor extends TemplateProcessor {
        protected array $rIdMediaMap = [];
    
        /**
         * @param PhpWord $phpWord
         *
         * @throws Exception
         */
        public function setupRelationships(PhpWord $phpWord): void {
            $originalRid = $this->getNextRelationsIndex($this->getMainPartName());
            $rid = $originalRid;
    
            $xmlWriter = new XMLWriter();
    
            $sections = $phpWord->getSections();
            foreach ($sections as $section) {
                $this->fixRelId($section->getElements(), $rid);
            }
    
            $sectionMedia = Media::getElements('section');
            if (!empty($sectionMedia)) {
                $this->addFilesToPackage($this->zip(), $sectionMedia);
    
                foreach ($sectionMedia as $element) {
                    $rid = $this->rIdMediaMap[$element['source']] ?? null;
    
                    if ($rid === null) {
                        report('No rId for media!');
    
                        continue;
                    }
    
                    $this->writeMediaRel($xmlWriter, $rid, $element);
                }
            }
    
            $mediaXml = $xmlWriter->getData();
    
            // Add link as relationship in XML relationships.
            $mainPartName = $this->getMainPartName();
            $this->tempDocumentRelations[$mainPartName] = str_replace('</Relationships>', $mediaXml,
                $this->tempDocumentRelations[$mainPartName]) . '</Relationships>';
        }
    
        protected function fixRelId(array $elements, &$id): void {
            foreach ($elements as $element) {
                if (
                    $element instanceof Link
                    || $element instanceof Image
                ) {
                    $rId = $this->rIdMediaMap[$element->getSource()] ?? null;
    
                    if ($rId === null) {
                        $rId = $id++;
                        $this->rIdMediaMap[$element->getSource()] = $rId;
                    }
    
                    $element->setRelationId($rId - 6);
                }
    
                if ($element instanceof AbstractContainer) {
                    $this->fixRelId($element->getElements(), $id);
                }
            }
        }
    
        /**
         * Write media relationships.
         *
         * @param XMLWriter $xmlWriter
         * @param int       $relId
         * @param array     $mediaRel
         *
         * @throws Exception
         */
        protected function writeMediaRel(XMLWriter $xmlWriter, int $relId, array $mediaRel): void {
            $typePrefix = 'officeDocument/2006/relationships/';
            $typeMapping = ['image' => 'image', 'object' => 'oleObject', 'link' => 'hyperlink'];
            $targetMapping = ['image' => 'media/', 'object' => 'embeddings/'];
    
            $mediaType = $mediaRel['type'];
            $type = $typeMapping[$mediaType] ?? $mediaType;
            $targetPrefix = $targetMapping[$mediaType] ?? '';
            $target = $mediaRel['target'];
            $targetMode = ($type == 'hyperlink') ? 'External' : '';
    
            $this->writeRel($xmlWriter, $relId, $typePrefix . $type, $targetPrefix . $target, $targetMode);
        }
    
        /**
         * Write individual rels entry.
         *
         * Format:
         * <Relationship Id="rId..." Type="http://..." Target="....xml" TargetMode="..." />
         *
         * @param int    $relId      Relationship ID
         * @param string $type       Relationship type
         * @param string $target     Relationship target
         * @param string $targetMode Relationship target mode
         *
         * @throws Exception
         */
        protected function writeRel(XMLWriter $xmlWriter, int $relId, string $type, string $target, string $targetMode = ''): void {
            if ($type != '' && $target != '') {
                if (strpos($relId, 'rId') === false) {
                    $relId = 'rId' . $relId;
                }
                $xmlWriter->startElement('Relationship');
                $xmlWriter->writeAttribute('Id', $relId);
                $xmlWriter->writeAttribute('Type', 'http://schemas.openxmlformats.org/' . $type);
                $xmlWriter->writeAttribute('Target', $target);
                if ($targetMode != '') {
                    $xmlWriter->writeAttribute('TargetMode', $targetMode);
                }
                $xmlWriter->endElement();
            } else {
                throw new Exception('Invalid parameters passed.');
            }
        }
    
        /**
         * Add files to package.
         *
         * @param ZipArchive $zip
         * @param mixed      $elements
         *
         * @throws Exception
         */
        protected function addFilesToPackage(ZipArchive $zip, array $elements): void {
            $types = [];
    
            foreach ($elements as $element) {
                $type = $element['type']; // image|object|link
    
                if (!in_array($type, ['image', 'object'])) {
                    continue;
                }
    
                $target = 'word/media/' . $element['target'];
    
                // Retrieve GD image content or get local media
                if (isset($element['isMemImage']) && $element['isMemImage']) {
                    $imageContents = $element['imageString'];
                    $zip->addFromString($target, $imageContents);
                } else {
                    $this->addFileToPackage($zip, $element['source'], $target);
                }
    
                if ($type === 'image' && !str_contains($this->tempDocumentContentTypes, "Extension=\"{$element['imageExtension']}\"")) {
                    $types[$element['imageExtension']] = $element['imageType'];
                }
            }
    
            $types = array_map(function ($value, $key) {
                return str_replace(['{ext}', '{type}'], [$key, $value], '<Default Extension="{ext}" ContentType="{type}"/>');
            }, $types, array_keys($types));
    
            $this->tempDocumentContentTypes = str_replace('</Types>', join("\n", $types), $this->tempDocumentContentTypes) . '</Types>';
        }
    
        /**
         * Add file to package.
         *
         * Get the actual source from an archive image.
         *
         * @param ZipArchive $zipPackage
         * @param string     $source
         * @param string     $target
         *
         * @throws Exception
         */
        protected function addFileToPackage(ZipArchive $zipPackage, string $source, string $target): void {
            $isArchive = str_contains($source, 'zip://');
            $actualSource = null;
    
            if ($isArchive) {
                $source = substr($source, 6);
                [$zipFilename, $imageFilename] = explode('#', $source);
    
                $zip = new ZipArchive();
    
                if ($zip->open($zipFilename) !== false) {
                    if ($zip->locateName($imageFilename)) {
                        $zip->extractTo(Settings::getTempDir(), $imageFilename);
                        $actualSource = Settings::getTempDir() . DIRECTORY_SEPARATOR . $imageFilename;
                    }
                }
    
                $zip->close();
            } else {
                $actualSource = $source;
            }
    
            if (null !== $actualSource) {
                $zipPackage->addFile($actualSource, $target);
            }
        }
    }
    

    And this is how to use the extended template processor to combine complex sections.

    $templateProcessor = new ExtendedTemplateProcessor(resource_path('prints/form.docx'));
    
    $templateProcessor->setValue('date-now', $this->date(Carbon::now()));
    $templateProcessor->setValue('title', $title);
    
    $phpWord = IOFactory::load(resource_path('prints/form.docx'));
    $section = $phpWord->addSection();
    
    $section->addTitle($title, 0);
    $section->addTextBreak();
    
    // This function adds more complicated things like addLink, addTable, addImage or even Html::addHtml($section, $value) 
    $this->formGroup($section, $config->children, $model, $model->toArray());
    
    // Call this from extended template processor to setup and fix document relationships and add images to the document.
    $templateProcessor->setupRelationships($phpWord);
    // Next lines writes the sections, tables and other conent to the document.
    $xmlWriter = new XMLWriter();
    (new Container($xmlWriter, $section, false))->write();
    $templateProcessor->replaceXmlBlock('content', $xmlWriter->getData());
    
    $templateProcessor->save();
    

    The important part is $templateProcessor->setupRelationships($phpWord); were the document that the templateProcessor is using gets fixed up with proper relIds and media gets added to the document.