Skip to content

Commit 0ea59e7

Browse files
committed
Word2007 Reader : Added support for Comments
1 parent 34db7cf commit 0ea59e7

File tree

13 files changed

+216
-279
lines changed

13 files changed

+216
-279
lines changed

docs/changes/1.x/1.2.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- HTML Writer : Added border-spacing to default styles for table by [@kernusr](https://github.com/kernusr) in GH-2451
1212
- Word2007 Reader : Support for table cell borders and margins by [@kernusr](https://github.com/kernusr) in GH-2454
1313
- PDF Writer : Add config for defining the default font by [@MikeMaldini](https://github.com/MikeMaldini) in [#2262](https://github.com/PHPOffice/PHPWord/pull/2262) & [#2468](https://github.com/PHPOffice/PHPWord/pull/2468)
14+
- Word2007 Reader : Added support for Comments by [@shaedrich](https://github.com/shaedrich) in [#2161](https://github.com/PHPOffice/PHPWord/pull/2161) & [#2469](https://github.com/PHPOffice/PHPWord/pull/2469)
1415

1516
### Bug fixes
1617

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Below are the supported features for each file formats.
9595
| | Footer | :material-check: | | | | |
9696
| | Footnote | :material-check: | | | | |
9797
| | Endnote | :material-check: | | | | |
98-
| | Comments | | | | | |
98+
| | Comments | :material-check: | | | | |
9999
| **Graphs** | 2D basic graphs | | | | | |
100100
| | 2D advanced graphs | | | | | |
101101
| | 3D graphs | | | | | |

phpstan-baseline.neon

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,6 @@ parameters:
230230
count: 2
231231
path: src/PhpWord/Reader/Word2007/AbstractPart.php
232232

233-
-
234-
message: "#^Call to method setChangeInfo\\(\\) on an unknown class PhpOffice\\\\PhpWord\\\\Reader\\\\Word2007\\\\AbstractElement\\.$#"
235-
count: 1
236-
path: src/PhpWord/Reader/Word2007/AbstractPart.php
237-
238233
-
239234
message: "#^Method PhpOffice\\\\PhpWord\\\\Reader\\\\Word2007\\\\AbstractPart\\:\\:getHeadingDepth\\(\\) never returns float so it can be removed from the return type\\.$#"
240235
count: 1
@@ -250,11 +245,6 @@ parameters:
250245
count: 1
251246
path: src/PhpWord/Reader/Word2007/AbstractPart.php
252247

253-
-
254-
message: "#^PHPDoc tag @var for variable \\$element contains unknown class PhpOffice\\\\PhpWord\\\\Reader\\\\Word2007\\\\AbstractElement\\.$#"
255-
count: 1
256-
path: src/PhpWord/Reader/Word2007/AbstractPart.php
257-
258248
-
259249
message: "#^Parameter \\#1 \\$count of method PhpOffice\\\\PhpWord\\\\Element\\\\AbstractContainer\\:\\:addTextBreak\\(\\) expects int, null given\\.$#"
260250
count: 1

src/PhpWord/Collection/AbstractCollection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public function setItem($index, $item): void
7979
*/
8080
public function addItem($item)
8181
{
82-
$index = $this->countItems() + 1;
82+
$index = $this->countItems();
8383
$this->items[$index] = $item;
8484

8585
return $index;

src/PhpWord/PhpWord.php

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
namespace PhpOffice\PhpWord;
1919

2020
use BadMethodCallException;
21-
use InvalidArgumentException;
22-
use PhpOffice\PhpWord\Element\AbstractElement;
2321
use PhpOffice\PhpWord\Element\Section;
2422
use PhpOffice\PhpWord\Exception\Exception;
2523

@@ -70,14 +68,7 @@ class PhpWord
7068
private $metadata = [];
7169

7270
/**
73-
* Comment reference cache
74-
*
75-
* @var array
76-
*/
77-
private $commentReferenceCache = [];
78-
79-
/**
80-
* Create new instance
71+
* Create new instance.
8172
*
8273
* Collections are created dynamically
8374
*/
@@ -336,7 +327,7 @@ public function save($filename, $format = 'Word2007', $download = false)
336327
}
337328

338329
/**
339-
* Create new section
330+
* Create new section.
340331
*
341332
* @deprecated 0.10.0
342333
*
@@ -352,7 +343,7 @@ public function createSection($settings = null)
352343
}
353344

354345
/**
355-
* Get document properties object
346+
* Get document properties object.
356347
*
357348
* @deprecated 0.12.0
358349
*
@@ -366,7 +357,7 @@ public function getDocumentProperties()
366357
}
367358

368359
/**
369-
* Set document properties object
360+
* Set document properties object.
370361
*
371362
* @deprecated 0.12.0
372363
*
@@ -382,40 +373,4 @@ public function setDocumentProperties($documentProperties)
382373

383374
return $this;
384375
}
385-
386-
/**
387-
* Cache commentReference (as well as commentRangeStart and commentRangeEnd) for later use
388-
*
389-
* @param 'start'|'end' $type
390-
* @param string $id,
391-
* @param $element
392-
*
393-
* @return self
394-
*/
395-
public function cacheCommentReference(string $type, string $id, AbstractElement $element)
396-
{
397-
//dump('cacheCommentReference', func_get_args(), array_key_exists($id, $this->commentReferenceCache));
398-
if (!in_array($type, [ 'start', 'end' ])) {
399-
throw new InvalidArgumentException('Type must be "start" or "end"');
400-
}
401-
402-
if (!array_key_exists($id, $this->commentReferenceCache)) {
403-
$this->commentReferenceCache[$id] = (object)[
404-
"start" => null,
405-
"end" => null
406-
];
407-
}
408-
$this->commentReferenceCache[$id]->{$type} = $element;
409-
410-
return $this;
411-
}
412-
413-
public function getCommentReference(string $id)
414-
{
415-
if (!array_key_exists($id, $this->commentReferenceCache)) {
416-
//dd($this->commentReferenceCache);
417-
throw new InvalidArgumentException('Comment with id '.$id.' isn\'t referenced in document');
418-
}
419-
return $this->commentReferenceCache[$id];
420-
}
421376
}

src/PhpWord/Reader/Word2007.php

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
namespace PhpOffice\PhpWord\Reader;
1919

20+
use Exception;
21+
use PhpOffice\PhpWord\Element\AbstractElement;
2022
use PhpOffice\PhpWord\PhpWord;
23+
use PhpOffice\PhpWord\Reader\Word2007\AbstractPart;
2124
use PhpOffice\PhpWord\Shared\XMLReader;
2225
use PhpOffice\PhpWord\Shared\ZipArchive;
2326

@@ -42,24 +45,34 @@ public function load($docFile)
4245
{
4346
$phpWord = new PhpWord();
4447
$relationships = $this->readRelationships($docFile);
48+
$commentRefs = [];
4549

4650
$steps = [
47-
['stepPart' => 'document', 'stepItems' => [
48-
'styles' => 'Styles',
49-
'numbering' => 'Numbering',
50-
]],
51-
['stepPart' => 'main', 'stepItems' => [
52-
'officeDocument' => 'Document',
53-
'core-properties' => 'DocPropsCore',
54-
'extended-properties' => 'DocPropsApp',
55-
'custom-properties' => 'DocPropsCustom',
56-
]],
57-
['stepPart' => 'document', 'stepItems' => [
58-
'endnotes' => 'Endnotes',
59-
'footnotes' => 'Footnotes',
60-
'settings' => 'Settings',
61-
'comments' => 'Comments'
62-
]],
51+
[
52+
'stepPart' => 'document',
53+
'stepItems' => [
54+
'styles' => 'Styles',
55+
'numbering' => 'Numbering',
56+
],
57+
],
58+
[
59+
'stepPart' => 'main',
60+
'stepItems' => [
61+
'officeDocument' => 'Document',
62+
'core-properties' => 'DocPropsCore',
63+
'extended-properties' => 'DocPropsApp',
64+
'custom-properties' => 'DocPropsCustom',
65+
],
66+
],
67+
[
68+
'stepPart' => 'document',
69+
'stepItems' => [
70+
'endnotes' => 'Endnotes',
71+
'footnotes' => 'Footnotes',
72+
'settings' => 'Settings',
73+
'comments' => 'Comments',
74+
],
75+
],
6376
];
6477

6578
foreach ($steps as $step) {
@@ -73,7 +86,8 @@ public function load($docFile)
7386
if (isset($stepItems[$relType])) {
7487
$partName = $stepItems[$relType];
7588
$xmlFile = $relItem['target'];
76-
$this->readPart($phpWord, $relationships, $partName, $docFile, $xmlFile);
89+
$part = $this->readPart($phpWord, $relationships, $commentRefs, $partName, $docFile, $xmlFile);
90+
$commentRefs = $part->getCommentReferences();
7791
}
7892
}
7993
}
@@ -84,21 +98,23 @@ public function load($docFile)
8498
/**
8599
* Read document part.
86100
*
87-
* @param array $relationships
88-
* @param string $partName
89-
* @param string $docFile
90-
* @param string $xmlFile
101+
* @param array<string, array<string, null|AbstractElement>> $commentRefs
91102
*/
92-
private function readPart(PhpWord $phpWord, $relationships, $partName, $docFile, $xmlFile): void
103+
private function readPart(PhpWord $phpWord, array $relationships, array $commentRefs, string $partName, string $docFile, string $xmlFile): AbstractPart
93104
{
94105
$partClass = "PhpOffice\\PhpWord\\Reader\\Word2007\\{$partName}";
95-
if (class_exists($partClass)) {
96-
/** @var \PhpOffice\PhpWord\Reader\Word2007\AbstractPart $part Type hint */
97-
$part = new $partClass($docFile, $xmlFile);
98-
$part->setImageLoading($this->hasImageLoading());
99-
$part->setRels($relationships);
100-
$part->read($phpWord);
106+
if (!class_exists($partClass)) {
107+
throw new Exception(sprintf('The part "%s" doesn\'t exist', $partClass));
101108
}
109+
110+
/** @var AbstractPart $part Type hint */
111+
$part = new $partClass($docFile, $xmlFile);
112+
$part->setImageLoading($this->hasImageLoading());
113+
$part->setRels($relationships);
114+
$part->setCommentReferences($commentRefs);
115+
$part->read($phpWord);
116+
117+
return $part;
102118
}
103119

104120
/**

src/PhpWord/Reader/Word2007/AbstractPart.php

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919

2020
use DateTime;
2121
use DOMElement;
22+
use InvalidArgumentException;
2223
use PhpOffice\PhpWord\ComplexType\TblWidth as TblWidthComplexType;
2324
use PhpOffice\PhpWord\Element\AbstractContainer;
25+
use PhpOffice\PhpWord\Element\AbstractElement;
2426
use PhpOffice\PhpWord\Element\TextRun;
2527
use PhpOffice\PhpWord\Element\TrackChange;
2628
use PhpOffice\PhpWord\PhpWord;
@@ -67,6 +69,13 @@ abstract class AbstractPart
6769
*/
6870
protected $rels = [];
6971

72+
/**
73+
* Comment references.
74+
*
75+
* @var array<string, array<string, AbstractElement>>
76+
*/
77+
protected $commentRefs = [];
78+
7079
/**
7180
* Image Loading.
7281
*
@@ -113,6 +122,62 @@ public function hasImageLoading(): bool
113122
return $this->imageLoading;
114123
}
115124

125+
/**
126+
* Get comment references.
127+
*
128+
* @return array<string, array<string, null|AbstractElement>>
129+
*/
130+
public function getCommentReferences(): array
131+
{
132+
return $this->commentRefs;
133+
}
134+
135+
/**
136+
* Set comment references.
137+
*
138+
* @param array<string, array<string, null|AbstractElement>> $commentRefs
139+
*/
140+
public function setCommentReferences(array $commentRefs): self
141+
{
142+
$this->commentRefs = $commentRefs;
143+
144+
return $this;
145+
}
146+
147+
/**
148+
* Set comment reference.
149+
*/
150+
private function setCommentReference(string $type, string $id, AbstractElement $element): self
151+
{
152+
if (!in_array($type, ['start', 'end'])) {
153+
throw new InvalidArgumentException('Type must be "start" or "end"');
154+
}
155+
156+
if (!array_key_exists($id, $this->commentRefs)) {
157+
$this->commentRefs[$id] = [
158+
'start' => null,
159+
'end' => null,
160+
];
161+
}
162+
$this->commentRefs[$id][$type] = $element;
163+
164+
return $this;
165+
}
166+
167+
/**
168+
* Get comment reference.
169+
*
170+
* @return array<string, null|AbstractElement>
171+
*/
172+
protected function getCommentReference(string $id): array
173+
{
174+
if (!array_key_exists($id, $this->commentRefs)) {
175+
throw new InvalidArgumentException(sprintf('Comment with id %s isn\'t referenced in document', $id));
176+
}
177+
178+
return $this->commentRefs[$id];
179+
}
180+
116181
/**
117182
* Read w:p.
118183
*
@@ -126,10 +191,18 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par
126191
// Paragraph style
127192
$paragraphStyle = null;
128193
$headingDepth = null;
129-
if ($xmlReader->elementExists('w:commentReference', $domNode) || $xmlReader->elementExists('w:commentRangeStart', $domNode) || $xmlReader->elementExists('w:commentRangeEnd', $domNode)) {
194+
if ($xmlReader->elementExists('w:commentReference', $domNode)
195+
|| $xmlReader->elementExists('w:commentRangeStart', $domNode)
196+
|| $xmlReader->elementExists('w:commentRangeEnd', $domNode)
197+
) {
130198
$nodes = $xmlReader->getElements('w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode);
131199
$node = current(iterator_to_array($nodes));
132-
$id = $node->attributes->getNamedItem('id')->value;
200+
if ($node) {
201+
$attributeIdentifier = $node->attributes->getNamedItem('id');
202+
if ($attributeIdentifier) {
203+
$id = $attributeIdentifier->nodeValue;
204+
}
205+
}
133206
}
134207
if ($xmlReader->elementExists('w:pPr', $domNode)) {
135208
$paragraphStyle = $this->readParagraphStyle($xmlReader, $domNode);
@@ -235,7 +308,7 @@ private function getHeadingDepth(?array $paragraphStyle = null)
235308
*/
236309
protected function readRun(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart, $paragraphStyle = null): void
237310
{
238-
if (in_array($domNode->nodeName, array('w:ins', 'w:del', 'w:smartTag', 'w:hyperlink', 'w:commentReference'))) {
311+
if (in_array($domNode->nodeName, ['w:ins', 'w:del', 'w:smartTag', 'w:hyperlink', 'w:commentReference'])) {
239312
$nodes = $xmlReader->getElements('*', $domNode);
240313
foreach ($nodes as $node) {
241314
$this->readRun($xmlReader, $node, $parent, $docPart, $paragraphStyle);
@@ -248,13 +321,15 @@ protected function readRun(XMLReader $xmlReader, DOMElement $domNode, $parent, $
248321
}
249322
}
250323

251-
if($xmlReader->elementExists('.//*["commentReference"=local-name()]', $domNode)) {
252-
$curEl = iterator_to_array($xmlReader->getElements('.//*["commentReference"=local-name()]', $domNode))[0];
253-
$id = $curEl->attributes->getNamedItem('id')->value;
254-
//$path = './/*[("commentRangeStart"=local-name() or "commentRangeEnd"=local-name()) and @*[local-name()="id" and .="'.$id.'"]]';
255-
//$range = $xmlReader->getElements($path);
256-
$this->phpWord->cacheCommentReference('start', $id, $parent->getElement($parent->countElements() - 1));
257-
$this->phpWord->cacheCommentReference('end', $id, $parent->getElement($parent->countElements() - 1));
324+
if ($xmlReader->elementExists('.//*["commentReference"=local-name()]', $domNode)) {
325+
$node = iterator_to_array($xmlReader->getElements('.//*["commentReference"=local-name()]', $domNode))[0];
326+
$attributeIdentifier = $node->attributes->getNamedItem('id');
327+
if ($attributeIdentifier) {
328+
$id = $attributeIdentifier->nodeValue;
329+
330+
$this->setCommentReference('start', $id, $parent->getElement($parent->countElements() - 1));
331+
$this->setCommentReference('end', $id, $parent->getElement($parent->countElements() - 1));
332+
}
258333
}
259334
}
260335

@@ -353,6 +428,7 @@ protected function readRunChild(XMLReader $xmlReader, DOMElement $node, Abstract
353428
$type = ($runParent->nodeName == 'w:del') ? TrackChange::DELETED : TrackChange::INSERTED;
354429
$author = $runParent->getAttribute('w:author');
355430
$date = DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $runParent->getAttribute('w:date'));
431+
$date = $date instanceof DateTime ? $date : null;
356432
$element->setChangeInfo($type, $author, $date);
357433
}
358434
}

0 commit comments

Comments
 (0)