Skip to content

Commit 3631936

Browse files
Word2007 Reader : Support for FormFields (#2653)
* Added code to read FormFields (text input, dropdown and checkbox) from a Word file * Fixed code style issues and added a testcase for reading a FormField of type checkbox * Fixed minor issue found by Scrutinizer * Fixed CI --------- Co-authored-by: Vincent Kool <vincentkool@gmail.com>
1 parent 00febf5 commit 3631936

File tree

4 files changed

+332
-8
lines changed

4 files changed

+332
-8
lines changed

docs/changes/2.x/2.0.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- PDF Writer : Documented how to specify a PDF renderer, when working with the PDF writer, as well as the three available choices by [@settermjd](https://github.com/settermjd) in [#2642](https://github.com/PHPOffice/PHPWord/pull/2642)
99
- Word2007 Reader: Support for Paragraph Border Style by [@damienfa](https://github.com/damienfa) in [#2651](https://github.com/PHPOffice/PHPWord/pull/2651)
1010
- Word2007 Writer: Support for field REF by [@crystoline](https://github.com/crystoline) in [#2652](https://github.com/PHPOffice/PHPWord/pull/2652)
11+
- Word2007 Reader : Support for FormFields by [@vincentKool](https://github.com/vincentKool) in [#2653](https://github.com/PHPOffice/PHPWord/pull/2653)
1112

1213
### Bug fixes
1314

phpstan-baseline.neon

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,6 @@ parameters:
235235
count: 1
236236
path: src/PhpWord/Reader/Word2007/AbstractPart.php
237237

238-
-
239-
message: "#^Parameter \\#1 \\$count of method PhpOffice\\\\PhpWord\\\\Element\\\\AbstractContainer\\:\\:addTextBreak\\(\\) expects int, null given\\.$#"
240-
count: 1
241-
path: src/PhpWord/Reader/Word2007/AbstractPart.php
242-
243238
-
244239
message: "#^Parameter \\#1 \\$depth of method PhpOffice\\\\PhpWord\\\\Element\\\\AbstractContainer\\:\\:addListItemRun\\(\\) expects int, string\\|null given\\.$#"
245240
count: 1

src/PhpWord/Reader/Word2007/AbstractPart.php

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use PhpOffice\PhpWord\ComplexType\TblWidth as TblWidthComplexType;
2525
use PhpOffice\PhpWord\Element\AbstractContainer;
2626
use PhpOffice\PhpWord\Element\AbstractElement;
27+
use PhpOffice\PhpWord\Element\FormField;
2728
use PhpOffice\PhpWord\Element\TextRun;
2829
use PhpOffice\PhpWord\Element\TrackChange;
2930
use PhpOffice\PhpWord\PhpWord;
@@ -192,8 +193,44 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par
192193
// Paragraph style
193194
$paragraphStyle = $xmlReader->elementExists('w:pPr', $domNode) ? $this->readParagraphStyle($xmlReader, $domNode) : null;
194195

195-
// PreserveText
196-
if ($xmlReader->elementExists('w:r/w:instrText', $domNode)) {
196+
if ($xmlReader->elementExists('w:r/w:fldChar/w:ffData', $domNode)) {
197+
// FormField
198+
$partOfFormField = false;
199+
$formNodes = [];
200+
$formType = null;
201+
$textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag', $domNode);
202+
if ($textRunContainers > 0) {
203+
$nodes = $xmlReader->getElements('*', $domNode);
204+
$paragraph = $parent->addTextRun($paragraphStyle);
205+
foreach ($nodes as $node) {
206+
if ($xmlReader->elementExists('w:fldChar/w:ffData', $node)) {
207+
$partOfFormField = true;
208+
$formNodes[] = $node;
209+
if ($xmlReader->elementExists('w:fldChar/w:ffData/w:ddList', $node)) {
210+
$formType = 'dropdown';
211+
} elseif ($xmlReader->elementExists('w:fldChar/w:ffData/w:textInput', $node)) {
212+
$formType = 'textinput';
213+
} elseif ($xmlReader->elementExists('w:fldChar/w:ffData/w:checkBox', $node)) {
214+
$formType = 'checkbox';
215+
}
216+
} elseif ($partOfFormField &&
217+
$xmlReader->elementExists('w:fldChar', $node) &&
218+
'end' == $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar')
219+
) {
220+
$formNodes[] = $node;
221+
$partOfFormField = false;
222+
// Process the form fields
223+
$this->readFormField($xmlReader, $formNodes, $paragraph, $paragraphStyle, $formType);
224+
} elseif ($partOfFormField) {
225+
$formNodes[] = $node;
226+
} else {
227+
// normal runs
228+
$this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle);
229+
}
230+
}
231+
}
232+
} elseif ($xmlReader->elementExists('w:r/w:instrText', $domNode)) {
233+
// PreserveText
197234
$ignoreText = false;
198235
$textContent = '';
199236
$fontStyle = $this->readFontStyle($xmlReader, $domNode);
@@ -272,7 +309,7 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par
272309
// Text and TextRun
273310
$textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag|w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode);
274311
if (0 === $textRunContainers) {
275-
$parent->addTextBreak(null, $paragraphStyle);
312+
$parent->addTextBreak(1, $paragraphStyle);
276313
} else {
277314
$nodes = $xmlReader->getElements('*', $domNode);
278315
$paragraph = $parent->addTextRun($paragraphStyle);
@@ -282,6 +319,109 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par
282319
}
283320
}
284321

322+
/**
323+
* @param DOMElement[] $domNodes
324+
* @param AbstractContainer $parent
325+
* @param mixed $paragraphStyle
326+
* @param string $formType
327+
*/
328+
private function readFormField(XMLReader $xmlReader, array $domNodes, $parent, $paragraphStyle, $formType): void
329+
{
330+
if (!in_array($formType, ['textinput', 'checkbox', 'dropdown'])) {
331+
return;
332+
}
333+
334+
$formField = $parent->addFormField($formType, null, $paragraphStyle);
335+
$ffData = $xmlReader->getElement('w:fldChar/w:ffData', $domNodes[0]);
336+
337+
foreach ($xmlReader->getElements('*', $ffData) as $node) {
338+
/** @var DOMElement $node */
339+
switch ($node->localName) {
340+
case 'name':
341+
$formField->setName($node->getAttribute('w:val'));
342+
343+
break;
344+
case 'ddList':
345+
$listEntries = [];
346+
foreach ($xmlReader->getElements('*', $node) as $ddListNode) {
347+
switch ($ddListNode->localName) {
348+
case 'result':
349+
$formField->setValue($xmlReader->getAttribute('w:val', $ddListNode));
350+
351+
break;
352+
case 'default':
353+
$formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode));
354+
355+
break;
356+
case 'listEntry':
357+
$listEntries[] = $xmlReader->getAttribute('w:val', $ddListNode);
358+
359+
break;
360+
}
361+
}
362+
$formField->setEntries($listEntries);
363+
if (null !== $formField->getValue()) {
364+
$formField->setText($listEntries[$formField->getValue()]);
365+
}
366+
367+
break;
368+
case 'textInput':
369+
foreach ($xmlReader->getElements('*', $node) as $ddListNode) {
370+
switch ($ddListNode->localName) {
371+
case 'default':
372+
$formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode));
373+
374+
break;
375+
case 'format':
376+
case 'maxLength':
377+
break;
378+
}
379+
}
380+
381+
break;
382+
case 'checkBox':
383+
foreach ($xmlReader->getElements('*', $node) as $ddListNode) {
384+
switch ($ddListNode->localName) {
385+
case 'default':
386+
$formField->setDefault($xmlReader->getAttribute('w:val', $ddListNode));
387+
388+
break;
389+
case 'checked':
390+
$formField->setValue($xmlReader->getAttribute('w:val', $ddListNode));
391+
392+
break;
393+
case 'size':
394+
case 'sizeAuto':
395+
break;
396+
}
397+
}
398+
399+
break;
400+
}
401+
}
402+
403+
if ('textinput' == $formType) {
404+
$ignoreText = true;
405+
$textContent = '';
406+
foreach ($domNodes as $node) {
407+
if ($xmlReader->elementExists('w:fldChar', $node)) {
408+
$fldCharType = $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar');
409+
if ('separate' == $fldCharType) {
410+
$ignoreText = false;
411+
} elseif ('end' == $fldCharType) {
412+
$ignoreText = true;
413+
}
414+
}
415+
416+
if (false === $ignoreText) {
417+
$textContent .= $xmlReader->getValue('w:t', $node);
418+
}
419+
}
420+
$formField->setValue(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'));
421+
$formField->setText(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'));
422+
}
423+
}
424+
285425
/**
286426
* Returns the depth of the Heading, returns 0 for a Title.
287427
*

tests/PhpWordTests/Reader/Word2007/ElementTest.php

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,4 +355,192 @@ public function testReadDrawing(): void
355355
$elements = $phpWord->getSection(0)->getElements();
356356
self::assertInstanceOf('PhpOffice\PhpWord\Element\TextRun', $elements[0]);
357357
}
358+
359+
/**
360+
* Test reading FormField - DROPDOWN.
361+
*/
362+
public function testReadFormFieldDropdown(): void
363+
{
364+
$documentXml = '<w:p>
365+
<w:r>
366+
<w:t>Reference</w:t>
367+
</w:r>
368+
<w:r>
369+
<w:fldChar w:fldCharType="begin">
370+
<w:ffData>
371+
<w:name w:val="DropDownList1"/>
372+
<w:enabled/>
373+
<w:calcOnExit w:val="0"/>
374+
<w:ddList>
375+
<w:result w:val="2"/>
376+
<w:listEntry w:val="TBD"/>
377+
<w:listEntry w:val="Option One"/>
378+
<w:listEntry w:val="Option Two"/>
379+
<w:listEntry w:val="Option Three"/>
380+
<w:listEntry w:val="Other"/>
381+
</w:ddList>
382+
</w:ffData>
383+
</w:fldChar>
384+
</w:r>
385+
<w:r>
386+
<w:instrText xml:space="preserve"> FORMDROPDOWN </w:instrText>
387+
</w:r>
388+
<w:r>
389+
<w:rPr>
390+
<w:lang w:val="en-GB"/>
391+
</w:rPr>
392+
</w:r>
393+
<w:r>
394+
<w:rPr>
395+
<w:lang w:val="en-GB"/>
396+
</w:rPr>
397+
<w:fldChar w:fldCharType="separate"/>
398+
</w:r>
399+
<w:r>
400+
<w:rPr>
401+
<w:lang w:val="en-GB"/>
402+
</w:rPr>
403+
<w:fldChar w:fldCharType="end"/>
404+
</w:r>
405+
</w:p>';
406+
407+
$phpWord = $this->getDocumentFromString(['document' => $documentXml]);
408+
409+
$elements = $phpWord->getSection(0)->getElements();
410+
self::assertInstanceOf('PhpOffice\PhpWord\Element\TextRun', $elements[0]);
411+
412+
$subElements = $elements[0]->getElements();
413+
414+
self::assertInstanceOf('PhpOffice\PhpWord\Element\Text', $subElements[0]);
415+
self::assertEquals('Reference', $subElements[0]->getText());
416+
417+
self::assertInstanceOf('PhpOffice\PhpWord\Element\FormField', $subElements[1]);
418+
self::assertEquals('dropdown', $subElements[1]->getType());
419+
self::assertEquals('DropDownList1', $subElements[1]->getName());
420+
self::assertEquals('2', $subElements[1]->getValue());
421+
self::assertEquals('Option Two', $subElements[1]->getText());
422+
self::assertEquals(['TBD', 'Option One', 'Option Two', 'Option Three', 'Other'], $subElements[1]->getEntries());
423+
}
424+
425+
/**
426+
* Test reading FormField - textinput.
427+
*/
428+
public function testReadFormFieldTextinput(): void
429+
{
430+
$documentXml = '<w:p>
431+
<w:r>
432+
<w:t>Fieldname</w:t>
433+
</w:r>
434+
<w:r>
435+
<w:fldChar w:fldCharType="begin">
436+
<w:ffData>
437+
<w:name w:val="TextInput2"/>
438+
<w:enabled/>
439+
<w:calcOnExit w:val="0"/>
440+
<w:textInput>
441+
<w:default w:val="TBD"/>
442+
<w:maxLength w:val="200"/>
443+
</w:textInput>
444+
</w:ffData>
445+
</w:fldChar>
446+
</w:r>
447+
<w:r>
448+
<w:instrText xml:space="preserve"> FORMTEXT </w:instrText>
449+
</w:r>
450+
<w:r>
451+
<w:rPr>
452+
<w:lang w:val="en-GB"/>
453+
</w:rPr>
454+
</w:r>
455+
<w:r>
456+
<w:rPr>
457+
<w:lang w:val="en-GB"/>
458+
</w:rPr>
459+
<w:fldChar w:fldCharType="separate"/>
460+
</w:r>
461+
<w:r w:rsidR="00807709">
462+
<w:rPr>
463+
<w:noProof/>
464+
<w:lang w:val="en-GB"/>
465+
</w:rPr>
466+
<w:t>This is some sample text</w:t>
467+
</w:r>
468+
<w:r>
469+
<w:rPr>
470+
<w:lang w:val="en-GB"/>
471+
</w:rPr>
472+
<w:fldChar w:fldCharType="end"/>
473+
</w:r>
474+
</w:p>';
475+
476+
$phpWord = $this->getDocumentFromString(['document' => $documentXml]);
477+
478+
$elements = $phpWord->getSection(0)->getElements();
479+
self::assertInstanceOf('PhpOffice\PhpWord\Element\TextRun', $elements[0]);
480+
481+
$subElements = $elements[0]->getElements();
482+
483+
self::assertInstanceOf('PhpOffice\PhpWord\Element\Text', $subElements[0]);
484+
self::assertEquals('Fieldname', $subElements[0]->getText());
485+
486+
self::assertInstanceOf('PhpOffice\PhpWord\Element\FormField', $subElements[1]);
487+
self::assertEquals('textinput', $subElements[1]->getType());
488+
self::assertEquals('TextInput2', $subElements[1]->getName());
489+
self::assertEquals('This is some sample text', $subElements[1]->getValue());
490+
self::assertEquals('This is some sample text', $subElements[1]->getText());
491+
}
492+
493+
/**
494+
* Test reading FormField - checkbox.
495+
*/
496+
public function testReadFormFieldCheckbox(): void
497+
{
498+
$documentXml = '<w:p>
499+
<w:pPr/>
500+
<w:r>
501+
<w:fldChar w:fldCharType="begin">
502+
<w:ffData>
503+
<w:enabled w:val="1"/>
504+
<w:name w:val="SomeCheckbox"/>
505+
<w:calcOnExit w:val="0"/>
506+
<w:checkBox>
507+
<w:sizeAuto w:val=""/>
508+
<w:default w:val="0"/>
509+
<w:checked w:val="0"/>
510+
</w:checkBox>
511+
</w:ffData>
512+
</w:fldChar>
513+
</w:r>
514+
<w:r>
515+
<w:rPr/>
516+
<w:instrText xml:space="preserve">FORMCHECKBOX</w:instrText>
517+
</w:r>
518+
<w:r>
519+
<w:rPr/>
520+
<w:fldChar w:fldCharType="separate"/>
521+
</w:r>
522+
<w:r>
523+
<w:rPr/>
524+
<w:t xml:space="preserve"> </w:t>
525+
</w:r>
526+
<w:r>
527+
<w:rPr/>
528+
<w:fldChar w:fldCharType="end"/>
529+
</w:r>
530+
</w:p>';
531+
532+
$phpWord = $this->getDocumentFromString(['document' => $documentXml]);
533+
534+
$elements = $phpWord->getSection(0)->getElements();
535+
self::assertInstanceOf('PhpOffice\PhpWord\Element\TextRun', $elements[0]);
536+
537+
$subElements = $elements[0]->getElements();
538+
539+
// $this->assertInstanceOf('PhpOffice\PhpWord\Element\Text', $subElements[0]);
540+
// $this->assertEquals('Fieldname', $subElements[0]->getText());
541+
542+
self::assertInstanceOf('PhpOffice\PhpWord\Element\FormField', $subElements[0]);
543+
self::assertEquals('checkbox', $subElements[0]->getType());
544+
self::assertEquals('SomeCheckbox', $subElements[0]->getName());
545+
}
358546
}

0 commit comments

Comments
 (0)