Complex classes like XmlElement often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use XmlElement, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
41 | class XmlElement implements ContainerInterface |
||
42 | { |
||
43 | use Accessors; |
||
44 | |||
45 | /** |
||
46 | * Settings for tiding up XML output |
||
47 | * |
||
48 | * @var array |
||
49 | */ |
||
50 | public static $tidy = [ |
||
51 | 'indent' => true, |
||
52 | 'input-xml' => true, |
||
53 | 'output-xml' => true, |
||
54 | 'drop-empty-paras' => false, |
||
55 | 'wrap' => 0 |
||
56 | ]; |
||
57 | |||
58 | /** @var string */ |
||
59 | private $_localName; |
||
60 | /** @var null|string|false */ |
||
61 | private $_prefix = null; |
||
62 | |||
63 | /** @var array */ |
||
64 | private $_namespaces = []; |
||
65 | /** @var array */ |
||
66 | private $_attributes = []; |
||
67 | |||
68 | /** |
||
69 | * @var XmlElement |
||
70 | */ |
||
71 | private $_parent; |
||
72 | |||
73 | /** |
||
74 | * @var XmlElement[] |
||
75 | */ |
||
76 | private $_children = []; |
||
77 | |||
78 | /** |
||
79 | * Initializes element with given name and URI |
||
80 | * |
||
81 | * @param string $name Element name, including prefix if needed |
||
82 | * @param string $uri Namespace URI of element |
||
83 | */ |
||
84 | 20 | protected function init(string $name, string $uri = null) |
|
85 | { |
||
86 | 20 | list($name, $prefix) = self::resolve($name); |
|
87 | |||
88 | 20 | $this->_localName = $name; |
|
89 | 20 | $this->_prefix = $prefix; |
|
90 | |||
91 | 20 | if ($uri !== null) { |
|
92 | 9 | $this->namespace = $uri; |
|
93 | } |
||
94 | 20 | } |
|
95 | |||
96 | /** |
||
97 | * XmlElement constructor |
||
98 | * |
||
99 | * @param string $name Element name, including prefix if needed |
||
100 | * @param string $uri Namespace URI of element |
||
101 | * @param mixed $content Content of element |
||
102 | */ |
||
103 | 16 | public function __construct(string $name, string $uri = null, $content = null) |
|
104 | { |
||
105 | 16 | $this->init($name, $uri); |
|
106 | 16 | $this->append($content); |
|
107 | 16 | } |
|
108 | |||
109 | /** |
||
110 | * Elements named constructor, same for every subclass. |
||
111 | * It's used for factory creation. |
||
112 | * |
||
113 | * @param string $name Element name, including prefix if needed |
||
114 | * @param string $uri Namespace URI of element |
||
115 | * |
||
116 | * @return static |
||
117 | */ |
||
118 | 4 | public static function plain(string $name, string $uri = null) |
|
119 | { |
||
120 | /** @var XmlElement $element */ |
||
121 | 4 | $element = (new \ReflectionClass(static::class))->newInstanceWithoutConstructor(); |
|
122 | 4 | $element->init($name, $uri); |
|
123 | |||
124 | 4 | return $element; |
|
125 | } |
||
126 | |||
127 | /** |
||
128 | * @see $innerXml |
||
129 | * @return string |
||
130 | */ |
||
131 | 2 | public function getInnerXml() |
|
132 | { |
||
133 | return implode('', array_map(function ($element) { |
||
134 | 2 | if (is_string($element)) { |
|
135 | 1 | return htmlspecialchars($element); |
|
136 | } elseif ($element instanceof XmlElement) { |
||
137 | 1 | return $element->xml(false); |
|
138 | } |
||
139 | |||
140 | return (string)$element; |
||
141 | 2 | }, $this->_children)); |
|
142 | } |
||
143 | |||
144 | /** |
||
145 | * Returns XML representation of element |
||
146 | * |
||
147 | * @param bool $clean Result will be cleaned if set to true |
||
148 | * |
||
149 | * @return string |
||
150 | */ |
||
151 | 3 | public function xml(bool $clean = true): string |
|
152 | { |
||
153 | 3 | if ($this->namespace && $this->_prefix === null) { |
|
154 | 1 | $this->_prefix = $this->lookupPrefix($this->namespace); |
|
155 | } |
||
156 | |||
157 | 3 | $attributes = $this->attributes(); |
|
158 | |||
159 | 3 | $result = "<{$this->name}"; |
|
160 | $result .= ' ' . implode(' ', array_map(function ($key, $value) { |
||
161 | 1 | return $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"'; |
|
162 | 3 | }, array_keys($attributes), array_values($attributes))); |
|
163 | |||
164 | 3 | if (!empty($this->_children)) { |
|
165 | 1 | $result .= ">{$this->innerXml}</{$this->name}>"; |
|
166 | } else { |
||
167 | 3 | $result .= "/>"; |
|
168 | } |
||
169 | |||
170 | 3 | return $clean && function_exists('tidy_repair_string') ? tidy_repair_string($result, self::$tidy) : $result; |
|
171 | } |
||
172 | |||
173 | /** |
||
174 | * Looks up prefix associated with given URI |
||
175 | * |
||
176 | * @param string|null $uri |
||
177 | * @return string|false |
||
178 | */ |
||
179 | 10 | public function lookupPrefix(string $uri = null) |
|
180 | { |
||
181 | 10 | return $this->getNamespaces()[ $uri ] ?? false; |
|
182 | } |
||
183 | |||
184 | /** |
||
185 | * Looks up URI associated with given prefix |
||
186 | * |
||
187 | * @param string|null $prefix |
||
188 | * @return string|false |
||
189 | */ |
||
190 | 15 | public function lookupUri(string $prefix = null) |
|
191 | { |
||
192 | 15 | return array_search($prefix, $this->getNamespaces()) ?: false; |
|
193 | } |
||
194 | |||
195 | /** |
||
196 | * Returns element's namespaces |
||
197 | * |
||
198 | * @param bool $parent Include namespaces from parent? |
||
199 | * @return array |
||
200 | */ |
||
201 | 17 | public function getNamespaces($parent = true): array |
|
202 | { |
||
203 | 17 | if (!$this->_parent) { |
|
204 | 17 | return $this->_namespaces; |
|
205 | } |
||
206 | |||
207 | 9 | if ($parent) { |
|
208 | 9 | return array_merge($this->_namespaces, $this->_parent->getNamespaces()); |
|
209 | } else { |
||
210 | 1 | return array_diff_assoc($this->_namespaces, $this->_parent->getNamespaces()); |
|
211 | } |
||
212 | } |
||
213 | |||
214 | /** |
||
215 | * Sets XML attribute of element |
||
216 | * |
||
217 | * @param string $attribute Attribute name, optionally with prefix |
||
218 | * @param mixed $value Attribute value |
||
219 | * @param string|null $uri XML Namespace URI of attribute, prefix will be automatically looked up |
||
220 | */ |
||
221 | 4 | public function setAttribute(string $attribute, $value, string $uri = null) |
|
222 | { |
||
223 | 4 | $attribute = $this->_prefix($attribute, $uri); |
|
224 | 3 | if ($value === null) { |
|
225 | unset($this->_attributes[ $attribute ]); |
||
226 | |||
227 | return; |
||
228 | } |
||
229 | |||
230 | 3 | $this->_attributes[ $attribute ] = $value; |
|
231 | 3 | } |
|
232 | |||
233 | /** |
||
234 | * Returns value of specified attribute. |
||
235 | * |
||
236 | * @param string $attribute Attribute name, optionally with prefix |
||
237 | * @param string|null $uri XML Namespace URI of attribute, prefix will be automatically looked up |
||
238 | * @return bool|mixed |
||
239 | */ |
||
240 | 2 | public function getAttribute(string $attribute, string $uri = null) |
|
241 | { |
||
242 | 2 | return $this->_attributes[ $this->_prefix($attribute, $uri) ] ?? false; |
|
243 | } |
||
244 | |||
245 | /** |
||
246 | * Checks if attribute exists |
||
247 | * |
||
248 | * @param string $attribute Attribute name, optionally with prefix |
||
249 | * @param string|null $uri XML Namespace URI of attribute, prefix will be automatically looked up |
||
250 | * |
||
251 | * @return bool |
||
252 | */ |
||
253 | 2 | public function hasAttribute(string $attribute, string $uri = null) |
|
254 | { |
||
255 | 2 | return isset($this->_attributes[ $this->_prefix($attribute, $uri) ]); |
|
256 | } |
||
257 | |||
258 | /** |
||
259 | * Returns element's parent |
||
260 | * @return XmlElement|null |
||
261 | */ |
||
262 | 1 | public function getParent() |
|
263 | { |
||
264 | 1 | return $this->_parent; |
|
265 | } |
||
266 | |||
267 | /** |
||
268 | * Sets element's parent |
||
269 | * @param XmlElement $parent |
||
270 | */ |
||
271 | 9 | protected function setParent(XmlElement $parent) |
|
272 | { |
||
273 | 9 | if (!$this->_prefix && ($prefix = $parent->lookupPrefix($this->namespace)) !== false) { |
|
274 | 1 | $this->_namespaces[ $this->namespace ] = $prefix; |
|
275 | 1 | $this->_prefix = $prefix; |
|
276 | } |
||
277 | |||
278 | 9 | $this->_parent = $parent; |
|
279 | 9 | if ($this->namespace === false) { |
|
280 | 4 | $this->namespace = $parent->namespace; |
|
281 | } |
||
282 | 9 | } |
|
283 | |||
284 | /** |
||
285 | * Appends child to element |
||
286 | * |
||
287 | * @param XmlElement|string $element |
||
288 | * |
||
289 | * @return XmlElement|string Same as $element |
||
290 | */ |
||
291 | 16 | public function append($element) |
|
292 | { |
||
293 | 16 | if (empty($element)) { |
|
294 | 16 | return false; |
|
295 | } |
||
296 | |||
297 | 11 | if(is_array($element)) { |
|
298 | array_walk($element, [$this, 'append']); |
||
299 | return $element; |
||
300 | } |
||
301 | |||
302 | 11 | if (!is_string($element) && !$element instanceof XmlElement) { |
|
303 | 1 | throw new InvalidArgumentException(helper\format( |
|
304 | 1 | '$element should be either string or object of {class} class. or array of given types, {type} given', [ |
|
305 | 1 | 'class' => XmlElement::class, |
|
306 | 1 | 'type' => helper\typeof($element) |
|
307 | ] |
||
308 | )); |
||
309 | } |
||
310 | |||
311 | 10 | if ($element instanceof XmlElement) { |
|
312 | 9 | $element->parent = $this; |
|
313 | } |
||
314 | |||
315 | 10 | return $this->_children[] = $element; |
|
316 | } |
||
317 | |||
318 | /** |
||
319 | * Returns namespace URI associated with element |
||
320 | * |
||
321 | * @return false|string |
||
322 | */ |
||
323 | 15 | public function getNamespace() |
|
327 | |||
328 | /** |
||
329 | * Adds namespace to element, and associates it with prefix. |
||
330 | * |
||
331 | * @param string $uri Namespace URI |
||
332 | * @param string|bool|null $prefix Prefix which will be used for namespace, false for using element's prefix |
||
333 | * and null for no prefix |
||
334 | */ |
||
335 | 13 | public function setNamespace(string $uri, $prefix = false) |
|
336 | { |
||
337 | 13 | if ($prefix === false) { |
|
338 | 11 | $prefix = $this->_prefix; |
|
339 | } |
||
340 | |||
341 | 13 | $this->_namespaces[ $uri ] = $prefix; |
|
342 | 13 | } |
|
343 | |||
344 | 7 | public function getName() |
|
348 | |||
349 | 5 | public function getChildren() |
|
350 | { |
||
351 | 5 | return $this->_children; |
|
352 | } |
||
353 | |||
354 | 15 | public function getPrefix() |
|
355 | { |
||
356 | 15 | return $this->_prefix; |
|
357 | } |
||
358 | |||
359 | 10 | public function getLocalName() |
|
360 | { |
||
361 | 10 | return $this->_localName; |
|
362 | } |
||
363 | |||
364 | 6 | public function getAttributes() |
|
365 | { |
||
366 | 6 | return $this->_attributes; |
|
367 | } |
||
368 | |||
369 | /** |
||
370 | * Returns one element at specified index (for default the first one). |
||
371 | * |
||
372 | * @param string $name Requested element tag name |
||
373 | * @param string $uri Requested element namespace |
||
374 | * @param int $index Index of element to retrieve |
||
375 | * |
||
376 | * @return XmlElement|false Retrieved element |
||
377 | */ |
||
378 | 1 | public function element(string $name, string $uri = null, int $index = 0) |
|
382 | |||
383 | /** |
||
384 | * Retrieves array of matching elements |
||
385 | * |
||
386 | * @param string $name Requested element tag name |
||
387 | * @param string|null $uri Requested element namespace |
||
388 | * |
||
389 | * @return XmlElement[] Found Elements |
||
390 | */ |
||
391 | 2 | public function elements($name, $uri = null) : array |
|
392 | { |
||
393 | 2 | $predicate = filter\tag($name); |
|
394 | 2 | if ($uri !== null) { |
|
395 | 1 | $predicate = filter\all($predicate, filter\xmlns($uri)); |
|
396 | } |
||
397 | |||
398 | 2 | return $this->all($predicate); |
|
399 | } |
||
400 | |||
401 | /** |
||
402 | * Filters element with given predicate |
||
403 | * |
||
404 | * @param callable|string $predicate Predicate or class name |
||
405 | * |
||
406 | * @return XmlElement[] |
||
407 | */ |
||
408 | 2 | public function all($predicate) |
|
409 | { |
||
410 | 2 | return array_values(array_filter($this->_children, filter\predicate($predicate))); |
|
411 | } |
||
412 | |||
413 | /** |
||
414 | * Iterates over matching elements |
||
415 | * |
||
416 | * @param callable|string $predicate Predicate or class name |
||
417 | * |
||
418 | * @return XmlElement|false |
||
419 | */ |
||
420 | 1 | public function get($predicate) |
|
421 | { |
||
422 | 1 | $predicate = filter\predicate($predicate); |
|
423 | 1 | foreach ($this->_children as $index => $child) { |
|
424 | 1 | if ($predicate($child)) { |
|
425 | 1 | return $child; |
|
426 | } |
||
427 | } |
||
428 | |||
429 | 1 | return false; |
|
430 | } |
||
431 | |||
432 | public function has($predicate) |
||
433 | { |
||
434 | return $this->get($predicate) !== false; |
||
435 | } |
||
436 | |||
437 | /** |
||
438 | * @param string|null $query |
||
439 | * @return XPathQuery |
||
440 | */ |
||
441 | 1 | public function query(string $query = null) |
|
442 | { |
||
443 | 1 | return new XPathQuery($query, $this); |
|
444 | } |
||
445 | |||
446 | /** |
||
447 | * Helper for retrieving all arguments (including namespaces) |
||
448 | * |
||
449 | * @return array |
||
450 | */ |
||
451 | 3 | private function attributes(): array |
|
452 | { |
||
453 | 3 | $namespaces = $this->getNamespaces(false); |
|
454 | 3 | $namespaces = array_map(function ($prefix, $uri) { |
|
455 | 1 | return [$prefix ? "xmlns:{$prefix}" : 'xmlns', $uri]; |
|
456 | 3 | }, array_values($namespaces), array_keys($namespaces)); |
|
457 | |||
458 | 3 | return array_merge( |
|
459 | 3 | $this->_attributes, |
|
460 | 3 | array_combine(array_column($namespaces, 0), array_column($namespaces, 1)) |
|
461 | ); |
||
462 | } |
||
463 | |||
464 | /** |
||
465 | * Prefixes $name with attribute associated with $uri |
||
466 | * |
||
467 | * @param string $name Name to prefix |
||
468 | * @param string $uri Namespace URI |
||
469 | * |
||
470 | * @return string |
||
471 | */ |
||
472 | 4 | protected function _prefix(string $name, string $uri = null): string |
|
484 | |||
485 | 2 | public function __toString() |
|
486 | { |
||
487 | 2 | return trim($this->xml(true)); |
|
488 | } |
||
489 | |||
490 | /** |
||
491 | * Splits name into local-name and prefix |
||
492 | * |
||
493 | * @param $name |
||
494 | * @return array [$name, $prefix] |
||
495 | */ |
||
496 | 20 | public static function resolve($name) |
|
497 | { |
||
498 | 20 | $prefix = null; |
|
499 | 20 | if (($pos = strpos($name, ':')) !== false) { |
|
500 | 2 | $prefix = substr($name, 0, $pos); |
|
501 | 2 | $name = substr($name, $pos + 1); |
|
502 | } |
||
503 | |||
504 | 20 | return [$name, $prefix]; |
|
505 | } |
||
506 | } |
||
507 |