Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.55% covered (success)
92.55%
87 / 94
90.00% covered (success)
90.00%
27 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractElement
92.55% covered (success)
92.55%
87 / 94
90.00% covered (success)
90.00%
27 / 30
64.64
0.00% covered (danger)
0.00%
0 / 1
 getPhpWord
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPhpWord
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSectionId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDocPart
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDocPart
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDocPartId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMediaPart
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getElementIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setElementIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getElementId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setElementId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRelationId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRelationId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNestedLevel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCommentsRangeStart
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCommentRangeStart
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setCommentRangeStart
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 getCommentsRangeEnd
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCommentRangeEnd
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setCommentRangeEnd
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
6.03
 getParent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setParentContainer
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 setMediaRelation
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
7
 setCollectionRelation
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isInSection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNewStyle
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 setTrackChange
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTrackChange
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setChangeInfo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setEnumVal
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3/**
4 * This file is part of PHPWord - A pure PHP library for reading and writing
5 * word processing documents.
6 *
7 * PHPWord is free software distributed under the terms of the GNU Lesser
8 * General Public License version 3 as published by the Free Software Foundation.
9 *
10 * For the full copyright and license information, please read the LICENSE
11 * file that was distributed with this source code. For the full list of
12 * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
13 *
14 * @see         https://github.com/PHPOffice/PHPWord
15 *
16 * @license     http://www.gnu.org/licenses/lgpl.txt LGPL version 3
17 */
18
19namespace PhpOffice\PhpWord\Element;
20
21use DateTime;
22use InvalidArgumentException;
23use PhpOffice\PhpWord\Collection\Comments;
24use PhpOffice\PhpWord\Media;
25use PhpOffice\PhpWord\PhpWord;
26use PhpOffice\PhpWord\Style;
27
28/**
29 * Element abstract class.
30 *
31 * @since 0.10.0
32 */
33abstract class AbstractElement
34{
35    /**
36     * PhpWord object.
37     *
38     * @var ?PhpWord
39     */
40    protected $phpWord;
41
42    /**
43     * Section Id.
44     *
45     * @var int
46     */
47    protected $sectionId;
48
49    /**
50     * Document part type: Section|Header|Footer|Footnote|Endnote.
51     *
52     * Used by textrun and cell container to determine where the element is
53     * located because it will affect the availability of other element,
54     * e.g. footnote will not be available when $docPart is header or footer.
55     *
56     * @var string
57     */
58    protected $docPart = 'Section';
59
60    /**
61     * Document part Id.
62     *
63     * For header and footer, this will be = ($sectionId - 1) * 3 + $index
64     * because the max number of header/footer in every page is 3, i.e.
65     * AUTO, FIRST, and EVEN (AUTO = ODD)
66     *
67     * @var int
68     */
69    protected $docPartId = 1;
70
71    /**
72     * Index of element in the elements collection (start with 1).
73     *
74     * @var int
75     */
76    protected $elementIndex = 1;
77
78    /**
79     * Unique Id for element.
80     *
81     * @var string
82     */
83    protected $elementId;
84
85    /**
86     * Relation Id.
87     *
88     * @var int
89     */
90    protected $relationId;
91
92    /**
93     * Depth of table container nested level; Primarily used for RTF writer/reader.
94     *
95     * 0 = Not in a table; 1 = in a table; 2 = in a table inside another table, etc.
96     *
97     * @var int
98     */
99    private $nestedLevel = 0;
100
101    /**
102     * A reference to the parent.
103     *
104     * @var null|AbstractElement
105     */
106    private $parent;
107
108    /**
109     * changed element info.
110     *
111     * @var TrackChange
112     */
113    private $trackChange;
114
115    /**
116     * Parent container type.
117     *
118     * @var string
119     */
120    private $parentContainer;
121
122    /**
123     * Has media relation flag; true for Link, Image, and Object.
124     *
125     * @var bool
126     */
127    protected $mediaRelation = false;
128
129    /**
130     * Is part of collection; true for Title, Footnote, Endnote, Chart, and Comment.
131     *
132     * @var bool
133     */
134    protected $collectionRelation = false;
135
136    /**
137     * The start position for the linked comments.
138     *
139     * @var Comments
140     */
141    protected $commentsRangeStart;
142
143    /**
144     * The end position for the linked comments.
145     *
146     * @var Comments
147     */
148    protected $commentsRangeEnd;
149
150    /**
151     * Get PhpWord.
152     */
153    public function getPhpWord(): ?PhpWord
154    {
155        return $this->phpWord;
156    }
157
158    /**
159     * Set PhpWord as reference.
160     */
161    public function setPhpWord(?PhpWord $phpWord = null): void
162    {
163        $this->phpWord = $phpWord;
164    }
165
166    /**
167     * Get section number.
168     *
169     * @return int
170     */
171    public function getSectionId()
172    {
173        return $this->sectionId;
174    }
175
176    /**
177     * Set doc part.
178     *
179     * @param string $docPart
180     * @param int $docPartId
181     */
182    public function setDocPart($docPart, $docPartId = 1): void
183    {
184        $this->docPart = $docPart;
185        $this->docPartId = $docPartId;
186    }
187
188    /**
189     * Get doc part.
190     *
191     * @return string
192     */
193    public function getDocPart()
194    {
195        return $this->docPart;
196    }
197
198    /**
199     * Get doc part Id.
200     *
201     * @return int
202     */
203    public function getDocPartId()
204    {
205        return $this->docPartId;
206    }
207
208    /**
209     * Return media element (image, object, link) container name.
210     *
211     * @return string section|headerx|footerx|footnote|endnote
212     */
213    private function getMediaPart()
214    {
215        $mediaPart = $this->docPart;
216        if ($mediaPart == 'Header' || $mediaPart == 'Footer') {
217            $mediaPart .= $this->docPartId;
218        }
219
220        return strtolower($mediaPart);
221    }
222
223    /**
224     * Get element index.
225     *
226     * @return int
227     */
228    public function getElementIndex()
229    {
230        return $this->elementIndex;
231    }
232
233    /**
234     * Set element index.
235     *
236     * @param int $value
237     */
238    public function setElementIndex($value): void
239    {
240        $this->elementIndex = $value;
241    }
242
243    /**
244     * Get element unique ID.
245     *
246     * @return string
247     */
248    public function getElementId()
249    {
250        return $this->elementId;
251    }
252
253    /**
254     * Set element unique ID from 6 first digit of md5.
255     */
256    public function setElementId(): void
257    {
258        $this->elementId = substr(md5((string) mt_rand()), 0, 6);
259    }
260
261    /**
262     * Get relation Id.
263     *
264     * @return int
265     */
266    public function getRelationId()
267    {
268        return $this->relationId;
269    }
270
271    /**
272     * Set relation Id.
273     *
274     * @param int $value
275     */
276    public function setRelationId($value): void
277    {
278        $this->relationId = $value;
279    }
280
281    /**
282     * Get nested level.
283     *
284     * @return int
285     */
286    public function getNestedLevel()
287    {
288        return $this->nestedLevel;
289    }
290
291    /**
292     * Get comments start.
293     */
294    public function getCommentsRangeStart(): ?Comments
295    {
296        return $this->commentsRangeStart;
297    }
298
299    /**
300     * Get comment start.
301     */
302    public function getCommentRangeStart(): ?Comment
303    {
304        if ($this->commentsRangeStart != null) {
305            return $this->commentsRangeStart->getItem($this->commentsRangeStart->countItems());
306        }
307
308        return null;
309    }
310
311    /**
312     * Set comment start.
313     */
314    public function setCommentRangeStart(Comment $value): void
315    {
316        if ($this instanceof Comment) {
317            throw new InvalidArgumentException('Cannot set a Comment on a Comment');
318        }
319        if ($this->commentsRangeStart == null) {
320            $this->commentsRangeStart = new Comments();
321        }
322        // Set ID early to avoid duplicates.
323        if ($value->getElementId() == null) {
324            $value->setElementId();
325        }
326        foreach ($this->commentsRangeStart->getItems() as $comment) {
327            if ($value->getElementId() == $comment->getElementId()) {
328                return;
329            }
330        }
331        $idxItem = $this->commentsRangeStart->addItem($value);
332        $this->commentsRangeStart->getItem($idxItem)->setStartElement($this);
333    }
334
335    /**
336     * Get comments end.
337     */
338    public function getCommentsRangeEnd(): ?Comments
339    {
340        return $this->commentsRangeEnd;
341    }
342
343    /**
344     * Get comment end.
345     */
346    public function getCommentRangeEnd(): ?Comment
347    {
348        if ($this->commentsRangeEnd != null) {
349            return $this->commentsRangeEnd->getItem($this->commentsRangeEnd->countItems());
350        }
351
352        return null;
353    }
354
355    /**
356     * Set comment end.
357     */
358    public function setCommentRangeEnd(Comment $value): void
359    {
360        if ($this instanceof Comment) {
361            throw new InvalidArgumentException('Cannot set a Comment on a Comment');
362        }
363        if ($this->commentsRangeEnd == null) {
364            $this->commentsRangeEnd = new Comments();
365        }
366        // Set ID early to avoid duplicates.
367        if ($value->getElementId() == null) {
368            $value->setElementId();
369        }
370        foreach ($this->commentsRangeEnd->getItems() as $comment) {
371            if ($value->getElementId() == $comment->getElementId()) {
372                return;
373            }
374        }
375        $idxItem = $this->commentsRangeEnd->addItem($value);
376        $this->commentsRangeEnd->getItem($idxItem)->setEndElement($this);
377    }
378
379    /**
380     * Get parent element.
381     *
382     * @return null|AbstractElement
383     */
384    public function getParent()
385    {
386        return $this->parent;
387    }
388
389    /**
390     * Set parent container.
391     *
392     * Passed parameter should be a container, except for Table (contain Row) and Row (contain Cell)
393     */
394    public function setParentContainer(self $container): void
395    {
396        $this->parentContainer = substr(get_class($container), strrpos(get_class($container), '\\') + 1);
397        $this->parent = $container;
398
399        // Set nested level
400        $this->nestedLevel = $container->getNestedLevel();
401        if ($this->parentContainer == 'Cell') {
402            ++$this->nestedLevel;
403        }
404
405        // Set phpword
406        $this->setPhpWord($container->getPhpWord());
407
408        // Set doc part
409        if (!$this instanceof Footnote) {
410            $this->setDocPart($container->getDocPart(), $container->getDocPartId());
411        }
412
413        $this->setMediaRelation();
414        $this->setCollectionRelation();
415    }
416
417    /**
418     * Set relation Id for media elements (link, image, object; legacy of OOXML).
419     *
420     * - Image element needs to be passed to Media object
421     * - Icon needs to be set for Object element
422     */
423    private function setMediaRelation(): void
424    {
425        if (!$this instanceof Link && !$this instanceof Image && !$this instanceof OLEObject) {
426            return;
427        }
428
429        $elementName = substr(static::class, strrpos(static::class, '\\') + 1);
430        if ($elementName == 'OLEObject') {
431            $elementName = 'Object';
432        }
433        $mediaPart = $this->getMediaPart();
434        $source = $this->getSource();
435        $image = null;
436        if ($this instanceof Image) {
437            $image = $this;
438        }
439        $rId = Media::addElement($mediaPart, strtolower($elementName), $source, $image);
440        $this->setRelationId($rId);
441
442        if ($this instanceof OLEObject) {
443            $icon = $this->getIcon();
444            $rId = Media::addElement($mediaPart, 'image', $icon, new Image($icon));
445            $this->setImageRelationId($rId);
446        }
447    }
448
449    /**
450     * Set relation Id for elements that will be registered in the Collection subnamespaces.
451     */
452    private function setCollectionRelation(): void
453    {
454        if ($this->collectionRelation === true && $this->phpWord instanceof PhpWord) {
455            $elementName = substr(static::class, strrpos(static::class, '\\') + 1);
456            $addMethod = "add{$elementName}";
457            $rId = $this->phpWord->$addMethod($this);
458            $this->setRelationId($rId);
459        }
460    }
461
462    /**
463     * Check if element is located in Section doc part (as opposed to Header/Footer).
464     *
465     * @return bool
466     */
467    public function isInSection()
468    {
469        return $this->docPart == 'Section';
470    }
471
472    /**
473     * Set new style value.
474     *
475     * @param mixed $styleObject Style object
476     * @param null|array|string|Style $styleValue Style value
477     * @param bool $returnObject Always return object
478     *
479     * @return mixed
480     */
481    protected function setNewStyle($styleObject, $styleValue = null, $returnObject = false)
482    {
483        if (null !== $styleValue && is_array($styleValue)) {
484            $styleObject->setStyleByArray($styleValue);
485            $style = $styleObject;
486        } else {
487            $style = $returnObject ? $styleObject : $styleValue;
488        }
489
490        return $style;
491    }
492
493    /**
494     * Sets the trackChange information.
495     */
496    public function setTrackChange(TrackChange $trackChange): void
497    {
498        $this->trackChange = $trackChange;
499    }
500
501    /**
502     * Gets the trackChange information.
503     *
504     * @return TrackChange
505     */
506    public function getTrackChange()
507    {
508        return $this->trackChange;
509    }
510
511    /**
512     * Set changed.
513     *
514     * @param string $type INSERTED|DELETED
515     * @param string $author
516     * @param null|DateTime|int $date allways in UTC
517     */
518    public function setChangeInfo($type, $author, $date = null): void
519    {
520        $this->trackChange = new TrackChange($type, $author, $date);
521    }
522
523    /**
524     * Set enum value.
525     *
526     * @param null|string $value
527     * @param string[] $enum
528     * @param null|string $default
529     *
530     * @return null|string
531     *
532     * @todo Merge with the same method in AbstractStyle
533     */
534    protected function setEnumVal($value = null, $enum = [], $default = null)
535    {
536        if ($value !== null && trim($value) != '' && !empty($enum) && !in_array($value, $enum)) {
537            throw new InvalidArgumentException("Invalid style value: {$value}");
538        } elseif ($value === null || trim($value) == '') {
539            $value = $default;
540        }
541
542        return $value;
543    }
544}