Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
96 / 96 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
1 / 1 |
AbstractContainer | |
100.00% |
96 / 96 |
|
100.00% |
7 / 7 |
28 | |
100.00% |
1 / 1 |
__call | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
7 | |||
addElement | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
4 | |||
getElements | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getElement | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
removeElement | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
6 | |||
countElements | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
checkValidity | |
100.00% |
46 / 46 |
|
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 | |
19 | namespace PhpOffice\PhpWord\Element; |
20 | |
21 | use BadMethodCallException; |
22 | use PhpOffice\Math\Math; |
23 | use ReflectionClass; |
24 | |
25 | /** |
26 | * Container abstract class. |
27 | * |
28 | * @method Text addText(string $text, mixed $fStyle = null, mixed $pStyle = null) |
29 | * @method TextRun addTextRun(mixed $pStyle = null) |
30 | * @method Bookmark addBookmark(string $name) |
31 | * @method Link addLink(string $target, string $text = null, mixed $fStyle = null, mixed $pStyle = null, boolean $internal = false) |
32 | * @method PreserveText addPreserveText(string $text, mixed $fStyle = null, mixed $pStyle = null) |
33 | * @method void addTextBreak(int $count = 1, mixed $fStyle = null, mixed $pStyle = null) |
34 | * @method ListItem addListItem(string $txt, int $depth = 0, mixed $font = null, mixed $list = null, mixed $para = null) |
35 | * @method ListItemRun addListItemRun(int $depth = 0, mixed $listStyle = null, mixed $pStyle = null) |
36 | * @method Footnote addFootnote(mixed $pStyle = null) |
37 | * @method Endnote addEndnote(mixed $pStyle = null) |
38 | * @method CheckBox addCheckBox(string $name, $text, mixed $fStyle = null, mixed $pStyle = null) |
39 | * @method Title addTitle(mixed $text, int $depth = 1, int $pageNumber = null) |
40 | * @method TOC addTOC(mixed $fontStyle = null, mixed $tocStyle = null, int $minDepth = 1, int $maxDepth = 9) |
41 | * @method PageBreak addPageBreak() |
42 | * @method Table addTable(mixed $style = null) |
43 | * @method Image addImage(string $source, mixed $style = null, bool $isWatermark = false, $name = null) |
44 | * @method OLEObject addOLEObject(string $source, mixed $style = null) |
45 | * @method TextBox addTextBox(mixed $style = null) |
46 | * @method Field addField(string $type = null, array $properties = array(), array $options = array(), mixed $text = null) |
47 | * @method Line addLine(mixed $lineStyle = null) |
48 | * @method Shape addShape(string $type, mixed $style = null) |
49 | * @method Chart addChart(string $type, array $categories, array $values, array $style = null, $seriesName = null) |
50 | * @method FormField addFormField(string $type, mixed $fStyle = null, mixed $pStyle = null) |
51 | * @method SDT addSDT(string $type) |
52 | * @method Formula addFormula(Math $math) |
53 | * @method Ruby addRuby(TextRun $baseText, TextRun $rubyText, \PhpOffice\PhpWord\ComplexType\RubyProperties $properties) |
54 | * @method \PhpOffice\PhpWord\Element\OLEObject addObject(string $source, mixed $style = null) deprecated, use addOLEObject instead |
55 | * |
56 | * @since 0.10.0 |
57 | */ |
58 | abstract class AbstractContainer extends AbstractElement |
59 | { |
60 | /** |
61 | * Elements collection. |
62 | * |
63 | * @var AbstractElement[] |
64 | */ |
65 | protected $elements = []; |
66 | |
67 | /** |
68 | * Container type Section|Header|Footer|Footnote|Endnote|Cell|TextRun|TextBox|ListItemRun|TrackChange. |
69 | * |
70 | * @var string |
71 | */ |
72 | protected $container; |
73 | |
74 | /** |
75 | * Magic method to catch all 'addElement' variation. |
76 | * |
77 | * This removes addText, addTextRun, etc. When adding new element, we have to |
78 | * add the model in the class docblock with `@method`. |
79 | * |
80 | * Warning: This makes capitalization matters, e.g. addCheckbox or addcheckbox won't work. |
81 | * |
82 | * @param mixed $function |
83 | * @param mixed $args |
84 | * |
85 | * @return AbstractElement |
86 | */ |
87 | public function __call($function, $args) |
88 | { |
89 | $elements = [ |
90 | 'Text', 'TextRun', 'Bookmark', 'Link', 'PreserveText', 'TextBreak', |
91 | 'ListItem', 'ListItemRun', 'Table', 'Image', 'Object', 'OLEObject', |
92 | 'Footnote', 'Endnote', 'CheckBox', 'TextBox', 'Field', |
93 | 'Line', 'Shape', 'Title', 'TOC', 'PageBreak', |
94 | 'Chart', 'FormField', 'SDT', 'Comment', |
95 | 'Formula', 'Ruby', |
96 | ]; |
97 | $functions = []; |
98 | foreach ($elements as $element) { |
99 | $functions['add' . strtolower($element)] = $element == 'Object' ? 'OLEObject' : $element; |
100 | } |
101 | |
102 | // Run valid `add` command |
103 | $function = strtolower($function); |
104 | if (isset($functions[$function])) { |
105 | $element = $functions[$function]; |
106 | |
107 | // Special case for TextBreak |
108 | // @todo Remove the `$count` parameter in 1.0.0 to make this element similiar to other elements? |
109 | if ($element == 'TextBreak') { |
110 | [$count, $fontStyle, $paragraphStyle] = array_pad($args, 3, null); |
111 | if ($count === null) { |
112 | $count = 1; |
113 | } |
114 | for ($i = 1; $i <= $count; ++$i) { |
115 | $this->addElement($element, $fontStyle, $paragraphStyle); |
116 | } |
117 | } else { |
118 | // All other elements |
119 | array_unshift($args, $element); // Prepend element name to the beginning of args array |
120 | |
121 | return call_user_func_array([$this, 'addElement'], $args); |
122 | } |
123 | } |
124 | |
125 | return null; |
126 | } |
127 | |
128 | /** |
129 | * Add element. |
130 | * |
131 | * Each element has different number of parameters passed |
132 | * |
133 | * @param string $elementName |
134 | * |
135 | * @return AbstractElement |
136 | */ |
137 | protected function addElement($elementName) |
138 | { |
139 | $elementClass = __NAMESPACE__ . '\\' . $elementName; |
140 | $this->checkValidity($elementName); |
141 | |
142 | // Get arguments |
143 | $args = func_get_args(); |
144 | $withoutP = in_array($this->container, ['TextRun', 'Footnote', 'Endnote', 'ListItemRun', 'Field']); |
145 | if ($withoutP && ($elementName == 'Text' || $elementName == 'PreserveText')) { |
146 | $args[3] = null; // Remove paragraph style for texts in textrun |
147 | } |
148 | |
149 | // Create element using reflection |
150 | $reflection = new ReflectionClass($elementClass); |
151 | $elementArgs = $args; |
152 | array_shift($elementArgs); // Shift the $elementName off the beginning of array |
153 | |
154 | /** @var AbstractElement $element Type hint */ |
155 | $element = $reflection->newInstanceArgs($elementArgs); |
156 | |
157 | // Set parent container |
158 | $element->setParentContainer($this); |
159 | $element->setElementIndex($this->countElements() + 1); |
160 | $element->setElementId(); |
161 | |
162 | $this->elements[] = $element; |
163 | |
164 | return $element; |
165 | } |
166 | |
167 | /** |
168 | * Get all elements. |
169 | * |
170 | * @return AbstractElement[] |
171 | */ |
172 | public function getElements() |
173 | { |
174 | return $this->elements; |
175 | } |
176 | |
177 | /** |
178 | * Returns the element at the requested position. |
179 | * |
180 | * @param int $index |
181 | * |
182 | * @return null|AbstractElement |
183 | */ |
184 | public function getElement($index) |
185 | { |
186 | if (array_key_exists($index, $this->elements)) { |
187 | return $this->elements[$index]; |
188 | } |
189 | |
190 | return null; |
191 | } |
192 | |
193 | /** |
194 | * Removes the element at requested index. |
195 | * |
196 | * @param AbstractElement|int $toRemove |
197 | */ |
198 | public function removeElement($toRemove): void |
199 | { |
200 | if (is_int($toRemove) && array_key_exists($toRemove, $this->elements)) { |
201 | unset($this->elements[$toRemove]); |
202 | } elseif ($toRemove instanceof AbstractElement) { |
203 | foreach ($this->elements as $key => $element) { |
204 | if ($element->getElementId() === $toRemove->getElementId()) { |
205 | unset($this->elements[$key]); |
206 | |
207 | return; |
208 | } |
209 | } |
210 | } |
211 | } |
212 | |
213 | /** |
214 | * Count elements. |
215 | * |
216 | * @return int |
217 | */ |
218 | public function countElements() |
219 | { |
220 | return count($this->elements); |
221 | } |
222 | |
223 | /** |
224 | * Check if a method is allowed for the current container. |
225 | * |
226 | * @param string $method |
227 | * |
228 | * @return bool |
229 | */ |
230 | private function checkValidity($method) |
231 | { |
232 | $generalContainers = [ |
233 | 'Section', 'Header', 'Footer', 'Footnote', 'Endnote', 'Cell', 'TextRun', 'TextBox', 'ListItemRun', 'TrackChange', |
234 | ]; |
235 | |
236 | $validContainers = [ |
237 | 'Text' => $generalContainers, |
238 | 'Bookmark' => $generalContainers, |
239 | 'Link' => $generalContainers, |
240 | 'TextBreak' => $generalContainers, |
241 | 'Image' => $generalContainers, |
242 | 'OLEObject' => $generalContainers, |
243 | 'Field' => $generalContainers, |
244 | 'Line' => $generalContainers, |
245 | 'Shape' => $generalContainers, |
246 | 'FormField' => $generalContainers, |
247 | 'SDT' => $generalContainers, |
248 | 'TrackChange' => $generalContainers, |
249 | 'TextRun' => ['Section', 'Header', 'Footer', 'Cell', 'TextBox', 'TrackChange', 'ListItemRun'], |
250 | 'ListItem' => ['Section', 'Header', 'Footer', 'Cell', 'TextBox'], |
251 | 'ListItemRun' => ['Section', 'Header', 'Footer', 'Cell', 'TextBox'], |
252 | 'Table' => ['Section', 'Header', 'Footer', 'Cell', 'TextBox'], |
253 | 'CheckBox' => ['Section', 'Header', 'Footer', 'Cell', 'TextRun'], |
254 | 'TextBox' => ['Section', 'Header', 'Footer', 'Cell'], |
255 | 'Footnote' => ['Section', 'TextRun', 'Cell', 'ListItemRun'], |
256 | 'Endnote' => ['Section', 'TextRun', 'Cell'], |
257 | 'PreserveText' => ['Section', 'Header', 'Footer', 'Cell'], |
258 | 'Title' => ['Section', 'Cell', 'Header'], |
259 | 'TOC' => ['Section'], |
260 | 'PageBreak' => ['Section'], |
261 | 'Chart' => ['Section', 'Cell'], |
262 | ]; |
263 | |
264 | // Special condition, e.g. preservetext can only exists in cell when |
265 | // the cell is located in header or footer |
266 | $validSubcontainers = [ |
267 | 'PreserveText' => [['Cell'], ['Header', 'Footer', 'Section']], |
268 | 'Footnote' => [['Cell', 'TextRun'], ['Section']], |
269 | 'Endnote' => [['Cell', 'TextRun'], ['Section']], |
270 | ]; |
271 | |
272 | // Check if a method is valid for current container |
273 | if (isset($validContainers[$method])) { |
274 | if (!in_array($this->container, $validContainers[$method])) { |
275 | throw new BadMethodCallException("Cannot add {$method} in {$this->container}."); |
276 | } |
277 | } |
278 | |
279 | // Check if a method is valid for current container, located in other container |
280 | if (isset($validSubcontainers[$method])) { |
281 | $rules = $validSubcontainers[$method]; |
282 | $containers = $rules[0]; |
283 | $allowedDocParts = $rules[1]; |
284 | foreach ($containers as $container) { |
285 | if ($this->container == $container && !in_array($this->getDocPart(), $allowedDocParts)) { |
286 | throw new BadMethodCallException("Cannot add {$method} in {$this->container}."); |
287 | } |
288 | } |
289 | } |
290 | |
291 | return true; |
292 | } |
293 | } |