Skip to content

Instantly share code, notes, and snippets.

@KKSzymanowski
Created January 29, 2024 13:35
Show Gist options
  • Save KKSzymanowski/acf892616805aed9900289b9bb1ede6d to your computer and use it in GitHub Desktop.
Save KKSzymanowski/acf892616805aed9900289b9bb1ede6d to your computer and use it in GitHub Desktop.
Fill in text form fields in DOCX
<?php
namespace App\Word;
use DOMAttr;
use DOMElement;
use DOMXPath;
use Exception;
use Illuminate\Support\Arr;
use PhpOffice\PhpWord\Shared\XMLReader;
use Ramsey\Uuid\Uuid;
use ZipArchive;
class FormFieldHelper
{
protected $templatePath;
protected $dom;
protected $xpath;
protected $formFields;
public function __construct($templatePath)
{
$this->templatePath = $templatePath;
$this->open();
}
protected function open()
{
$xmlReader = new XMLReader();
$this->dom = $xmlReader->getDomFromZip($this->templatePath, 'word/document.xml');
$this->xpath = new DOMXpath($this->dom);
$this->formFields = [];
/** @var DOMElement $element */
foreach ($this->xpath->query('//w:fldChar/w:ffData') as $element) {
$nameElements = $this->xpath->query('w:name', $element);
if (count($nameElements) != 1) {
continue;
}
$name = $nameElements[0]->getAttribute('w:val');
/** @var DOMElement $current */
$current = $start = $element->parentNode->parentNode;
$emptyTextElements = [];
while ($current = $current->nextSibling) {
$textElements = $this->xpath->query('w:t', $current);
/** @var DOMElement $textElement */
foreach ($textElements as $textElement) {
if ($textElement->nodeValue == mb_chr(hexdec('2002'))) {
$emptyTextElements[] = $current;
break;
}
}
$fldCharElements = $this->xpath->query('w:fldChar/@w:fldCharType', $current);
/** @var DOMAttr $fldCharElement */
foreach ($fldCharElements as $fldCharElement) {
if ($fldCharElement->value == 'end') {
break 2;
}
}
}
if (count($emptyTextElements) == 0) {
continue;
}
$this->formFields[$name] = [
'mainNode' => $start,
'emptyTextElements' => $emptyTextElements,
];
}
}
public function getFieldNames()
{
return array_keys($this->formFields);
}
public function setFieldValues($fieldValues)
{
$fieldsKeys = array_keys($this->formFields);
sort($fieldsKeys);
$fieldValuesKeys = array_keys($fieldValues);
sort($fieldValuesKeys);
if ($fieldsKeys != $fieldValuesKeys) {
throw new Exception('Form field values do not match for template ' . $this->templatePath . '. Expected: ' . implode(',', array_keys($this->formFields)) . ', provided: ' . implode(',', array_keys($fieldValues)));
}
foreach ($this->formFields as $name => $field) {
/** @var DOMElement[] $emptyTextElements */
$emptyTextElements = $field['emptyTextElements'];
/** @var DOMElement $firstAfterDeleted */
$firstAfterDeleted = Arr::last($emptyTextElements)->nextSibling;
foreach ($emptyTextElements as $emptyTextElement) {
$field['mainNode']->parentNode->removeChild($emptyTextElement);
}
/*
Create this structure:
<w:r w:rsidR="00F545D9">
<w:rPr>
<w:noProof/>
<w:lang w:val="pl-PL"/>
</w:rPr>
<w:t><CONTENT HERE>></w:t>
</w:r>
*/
$wr = $this->dom->createElement('w:r');
$wrpr = $this->dom->createElement('w:rPr');
$wlang = $this->dom->createElement('w:lang');
$wlang->setAttribute('w:val', 'pl-PL');
$wrpr->appendChild($this->dom->createElement('w:noProof'));
$wrpr->appendChild($wlang);
$wt = $this->dom->createElement('w:t');
$wt->nodeValue = $fieldValues[$name];
$wr->appendChild($wrpr);
$wr->appendChild($wt);
$firstAfterDeleted->parentNode->insertBefore($wr, $firstAfterDeleted);
}
return $this;
}
public function save($outputDir)
{
$tempFile = tmpfile();
$tmpFilename = stream_get_meta_data($tempFile)['uri'];
copy($this->templatePath, $tmpFilename);
$zip = new ZipArchive();
if (!$zip->open($tmpFilename)) {
throw new Exception('Could not open ' . $tmpFilename);
}
$zip->addFromString('word/document.xml', $this->dom->saveXML());
$zip->close();
$fileName = Uuid::uuid4() . '.docx';
copy($tmpFilename, $outputDir . '/' . $fileName);
return $fileName;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment