SxmlDebug::dumpAddNamespace()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 14
cts 14
cp 1
rs 9.3142
c 0
b 0
f 0
cc 3
eloc 15
nc 3
nop 1
crap 3
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: twhiston
5
 * Date: 27/11/16
6
 * Time: 13:57
7
 */
8
9
namespace twhiston\simplexml_debug;
10
11
12
/**
13
 * Class SxmlDebug
14
 *
15
 * @package twhiston\simplexml_debug
16
 */
17
class SxmlDebug {
18
19
20
  /**
21
   * Character to use for indenting strings
22
   */
23
  const INDENT = "\t";
24
  /**
25
   * How much of a string to extract
26
   */
27
  const EXTRACT_SIZE = 15;
28
29
  /**
30
   * Output a summary of the node or list of nodes referenced by a particular
31
   * SimpleXML object Rather than attempting a recursive inspection, presents
32
   * statistics aimed at understanding what your SimpleXML code is doing.
33
   *
34
   * @param \SimpleXMLElement $sxml The object to inspect
35
   * @return string output string
36
   *
37
   */
38 5
  public static function dump(\SimpleXMLElement $sxml) {
39
40 5
    $dump = '';
41
    // Note that the header is added at the end, so we can add stats
42 5
    $dump .= '[' . PHP_EOL;
43
44
    // SimpleXML objects can be either a single node, or (more commonly) a list of 0 or more nodes
45
    // I haven't found a reliable way of distinguishing between the two cases
46
    // Note that for a single node, foreach($node) acts like foreach($node->children())
47
    // Numeric array indexes, however, operate consistently: $node[0] just returns the node
48 5
    $item_index = 0;
49 5
    while (isset($sxml[$item_index])) {
50
51
      /** @var \SimpleXMLElement $item */
52 5
      $item = $sxml[$item_index];
53 5
      $item_index++;
54
55
      // It's surprisingly hard to find something which behaves consistently differently for an attribute and an element within SimpleXML
56
      // The below relies on the fact that the DOM makes a much clearer distinction
57
      // Note that this is not an expensive conversion, as we are only swapping PHP wrappers around an existing LibXML resource
58 5
      if (dom_import_simplexml($item) instanceOf \DOMAttr) {
59 1
        $dump .= self::dumpAddAttribute($item);
60
      } else {
61 4
        $dump .= self::dumpAddElement($item);
62
      }
63
    }
64 5
    $dump .= ']' . PHP_EOL;
65
66
    // Add on the header line, with the total number of items output
67 5
    return self::getHeaderLine($item_index) . $dump;
68
  }
69
70
  /**
71
   * @param \SimpleXMLElement $item
72
   * @return string
73
   */
74 5
  private static function dumpAddNamespace(\SimpleXMLElement $item): string {
75
76 5
    $dump = '';
77
    // To what namespace does this attribute belong? Returns array( alias => URI )
78 5
    $ns = $item->getNamespaces(FALSE);
79 5
    if (!empty($ns)) {
80 3
      $dump .= self::INDENT . self::INDENT . 'Namespace: \'' . reset($ns) .
81 3
               '\'' .
82 3
               PHP_EOL;
83 3
      if (key($ns) == '') {
84 1
        $dump .= self::INDENT . self::INDENT . '(Default Namespace)' . PHP_EOL;
85
      } else {
86 2
        $dump .= self::INDENT . self::INDENT . 'Namespace Alias: \'' .
87 2
                 key($ns) .
88 2
                 '\'' .
89 2
                 PHP_EOL;
90
      }
91
    }
92
93 5
    return $dump;
94
  }
95
96
  /**
97
   * @param      $title
98
   * @param      $data
99
   * @param int  $indent
100
   * @param bool $backtick
101
   * @return string
102
   */
103 5
  private static function dumpGetLine($title,
104
                                      $data,
105
                                      $indent = 1,
106
                                      $backtick = TRUE): string {
107 5
    return str_repeat(self::INDENT, $indent) . $title . ': ' .
108 5
           ($backtick ? '\'' : '') . $data .
109 5
           ($backtick ? '\'' : '') . PHP_EOL;
110
  }
111
112
  /**
113
   * @param \SimpleXMLElement $item
114
   * @return string
115
   */
116 1
  private static function dumpAddAttribute(\SimpleXMLElement $item): string {
117
118 1
    $dump = self::INDENT . 'Attribute {' . PHP_EOL;
119
120 1
    $dump .= self::dumpAddNamespace($item);
121
122 1
    $dump .= self::dumpGetLine('Name', $item->getName(), 2);
123 1
    $dump .= self::dumpGetLine('Value', (string) $item, 2);
124
125 1
    $dump .= self::INDENT . '}' . PHP_EOL;
126 1
    return $dump;
127
128
  }
129
130
  /**
131
   * @param \SimpleXMLElement $item
132
   * @return string
133
   */
134 4
  private static function dumpAddElement(\SimpleXMLElement $item): string {
135
136 4
    $dump = self::INDENT . 'Element {' . PHP_EOL;
137
138 4
    $dump .= self::dumpAddNamespace($item);
139
140 4
    $dump .= self::dumpGetLine('Name', $item->getName(), 2);
141 4
    $dump .= self::dumpGetLine('String Content', (string) $item, 2);
142
143
    // Now some statistics about attributes and children, by namespace
144
145
    // This returns all namespaces used by this node and all its descendants,
146
    // 	whether declared in this node, in its ancestors, or in its descendants
147 4
    $all_ns = $item->getNamespaces(TRUE);
148
    // If the default namespace is never declared, it will never show up using the below code
149 4
    if (!array_key_exists('', $all_ns)) {
150 3
      $all_ns[''] = NULL;
151
    }
152
153 4
    foreach ($all_ns as $ns_alias => $ns_uri) {
154 4
      $children = $item->children($ns_uri);
155 4
      $attributes = $item->attributes($ns_uri);
156
157
      // Somewhat confusingly, in the case where a parent element is missing the xmlns declaration,
158
      //	but a descendant adds it, SimpleXML will look ahead and fill $all_ns[''] incorrectly
159
      if (
160 4
        empty($ns_alias)
161
        &&
162 4
        NULL !== $ns_uri
163
        &&
164 4
        count($children) === 0
165
        &&
166 4
        count($attributes) === 0
167
      ) {
168
        // Try looking for a default namespace without a known URI
169
        $ns_uri = NULL;
170
        $children = $item->children($ns_uri);
171
        $attributes = $item->attributes($ns_uri);
172
      }
173
174
      // Don't show zero-counts, as they're not that useful
175 4
      if (count($children) === 0 && count($attributes) === 0) {
176 2
        continue;
177
      }
178
179 4
      $ns_label = (($ns_alias === '') ? 'Default Namespace' :
180 4
        "Namespace $ns_alias");
181
182 4
      $dump .= self::INDENT . self::INDENT . 'Content in ' . $ns_label .
183 4
               PHP_EOL;
184
185 4
      if (NULL !== $ns_uri) {
186 3
        $dump .= self::dumpGetLine('Namespace URI', $ns_uri, 3);
187
      }
188
189
190 4
      $dump .= self::dumpGetLine('Children',
191 4
                                 self::dumpGetChildDetails($children),
192 4
                                 3,
193 4
                                 FALSE);
194
195
196 4
      $dump .= self::dumpGetLine('Attributes',
197 4
                                 self::dumpGetAttributeDetails($attributes),
198 4
                                 3,
199 4
                                 FALSE);
200
    }
201
202 4
    return $dump . self::INDENT . '}' . PHP_EOL;
203
  }
204
205
  /**
206
   * @param \SimpleXMLElement $children
207
   * @return string
208
   */
209 4
  private static function dumpGetChildDetails(\SimpleXMLElement $children): string {
210
    // Count occurrence of child element names, rather than listing them all out
211 4
    $child_names = [];
212 4
    foreach ($children as $sx_child) {
213
      // Below is a rather clunky way of saying $child_names[ $sx_child->getName() ]++;
214
      // 	which avoids Notices about unset array keys
215 3
      $child_node_name = $sx_child->getName();
216 3
      if (array_key_exists($child_node_name, $child_names)) {
217
        $child_names[$child_node_name]++;
218
      } else {
219 3
        $child_names[$child_node_name] = 1;
220
      }
221
    }
222 4
    ksort($child_names);
223 4
    $child_name_output = [];
224 4
    foreach ($child_names as $name => $count) {
225 3
      $child_name_output[] = "$count '$name'";
226
    }
227
228 4
    $childrenString = count($children);
229
    // Don't output a trailing " - " if there are no children
230 4
    if (count($children) > 0) {
231 3
      $childrenString .= ' - ' . implode(', ', $child_name_output);
232
    }
233 4
    return $childrenString;
234
  }
235
236
  /**
237
   * @param \SimpleXMLElement $attributes
238
   * @return string
239
   */
240 4
  private static function dumpGetAttributeDetails(\SimpleXMLElement $attributes): string {
241
// Attributes can't be duplicated, but I'm going to put them in alphabetical order
242 4
    $attribute_names = [];
243 4
    foreach ($attributes as $sx_attribute) {
244 1
      $attribute_names[] = "'" . $sx_attribute->getName() . "'";
245
    }
246 4
    ksort($attribute_names);
247
248 4
    $attString = count($attributes);
249
    // Don't output a trailing " - " if there are no attributes
250 4
    if (count($attributes) > 0) {
251 1
      $attString .= ' - ' . implode(', ', $attribute_names);
252
    }
253 4
    return $attString;
254
  }
255
256
  /**
257
   * @param $index
258
   * @return string
259
   */
260 9
  private static function getHeaderLine($index): string {
261
262 9
    return 'SimpleXML object (' . $index . ' item' .
263 9
           ($index > 1 ? 's' : '') . ')' . PHP_EOL;
264
  }
265
266
  /**
267
   * Output a tree-view of the node or list of nodes referenced by a particular
268
   * SimpleXML object Unlike simplexml_dump(), this processes the entire XML
269
   * tree recursively, while attempting to be more concise and readable than
270
   * the XML itself. Additionally, the output format is designed as a hint of
271
   * the syntax needed to traverse the object.
272
   *
273
   * @param \SimpleXMLElement $sxml                   The object to inspect
274
   * @param boolean           $include_string_content Default false. If true,
275
   *                                                  will summarise textual
276
   *                                                  content, as well as child
277
   *                                                  elements and attribute
278
   *                                                  names
279
   * @return null|string Nothing, or output, depending on $return param
280
   *
281
   */
282 4
  public static function tree(\SimpleXMLElement $sxml,
283
                              $include_string_content = FALSE): string {
284
285 4
    $dump = '';
286
    // Note that the header is added at the end, so we can add stats
287
288
    // The initial object passed in may be a single node or a list of nodes, so we need an outer loop first
289
    // Note that for a single node, foreach($node) acts like foreach($node->children())
290
    // Numeric array indexes, however, operate consistently: $node[0] just returns the node
291 4
    $root_item_index = 0;
292 4
    while (isset($sxml[$root_item_index])) {
293 4
      $root_item = $sxml[$root_item_index];
294
295
      // Special case if the root is actually an attribute
296
      // It's surprisingly hard to find something which behaves consistently differently for an attribute and an element within SimpleXML
297
      // The below relies on the fact that the DOM makes a much clearer distinction
298
      // Note that this is not an expensive conversion, as we are only swapping PHP wrappers around an existing LibXML resource
299 4
      if (dom_import_simplexml($root_item) instanceOf \DOMAttr) {
300
        // To what namespace does this attribute belong? Returns array( alias => URI )
301
        $ns = $root_item->getNamespaces(FALSE);
302
        if (key($ns)) {
303
          $dump .= key($ns) . ':';
304
        }
305
        $dump .= $root_item->getName() . '="' . (string) $root_item . '"' .
306
                 PHP_EOL;
307
      } else {
308
        // Display the root node as a numeric key reference, plus a hint as to its tag name
309
        // e.g. '[42] // <Answer>'
310
311
        // To what namespace does this attribute belong? Returns array( alias => URI )
312 4
        $ns = $root_item->getNamespaces(FALSE);
313 4
        if (key($ns)) {
314
          $root_node_name = key($ns) . ':' . $root_item->getName();
315
        } else {
316 4
          $root_node_name = $root_item->getName();
317
        }
318 4
        $dump .= "[$root_item_index] // <$root_node_name>" . PHP_EOL;
319
320
        // This function is effectively recursing depth-first through the tree,
321
        // but this is managed manually using a stack rather than actual recursion
322
        // Each item on the stack is of the form array(int $depth, SimpleXMLElement $element, string $header_row)
323 4
        $dump .= SxmlDebug::recursivelyProcessNode(
324
          $root_item,
325 4
          1,
326
          $include_string_content
327
        );
328
      }
329
330 4
      $root_item_index++;
331
    }
332
333
    // Add on the header line, with the total number of items output
334 4
    $dump = self::getHeaderLine($root_item_index) . $dump;
335
336 4
    return $dump;
337
338
  }
339
340
341
  /**
342
   * @param string $stringContent
343
   * @param        $depth
344
   * @return string
345
   */
346 1
  private static function treeGetStringExtract(string $stringContent,
347
                                               $depth): string {
348 1
    $string_extract = preg_replace('/\s+/', ' ', trim($stringContent));
349 1
    if (strlen($string_extract) > SxmlDebug::EXTRACT_SIZE) {
350 1
      $string_extract = substr($string_extract, 0, SxmlDebug::EXTRACT_SIZE)
351 1
                        . '...';
352
    }
353 1
    return (strlen($stringContent) > 0) ?
354 1
      str_repeat(SxmlDebug::INDENT, $depth)
355 1
      . '(string) '
356 1
      . "'$string_extract'"
357 1
      . ' (' . strlen($stringContent) . ' chars)'
358 1
      . PHP_EOL : '';
359
360
  }
361
362
  /**
363
   * @param \SimpleXMLElement $item
364
   * @return array
365
   */
366 4
  private static function treeGetNamespaces(\SimpleXMLElement $item): array {
367
    // To what namespace does this element belong? Returns array( alias => URI )
368 4
    $item_ns = $item->getNamespaces(FALSE);
369 4
    if (empty($item_ns)) {
370 3
      $item_ns = ['' => NULL];
371
    }
372
373
    // This returns all namespaces used by this node and all its descendants,
374
    // 	whether declared in this node, in its ancestors, or in its descendants
375 4
    $all_ns = $item->getNamespaces(TRUE);
376
    // If the default namespace is never declared, it will never show up using the below code
377 4
    if (!array_key_exists('', $all_ns)) {
378 3
      $all_ns[''] = NULL;
379
    }
380
381
    // Prioritise "current" namespace by merging into onto the beginning of the list
382
    // (it will be added to the beginning and the duplicate entry dropped)
383 4
    return array_merge($item_ns, $all_ns);
384
  }
385
386
  /**
387
   * @param \SimpleXMLElement $attributes
388
   * @param string            $nsAlias
389
   * @param int               $depth
390
   * @param bool              $isCurrentNamespace
391
   * @param bool              $includeStringContent
392
   * @return string
393
   */
394 4
  private static function treeProcessAttributes(\SimpleXMLElement $attributes,
395
                                                string $nsAlias,
396
                                                int $depth,
397
                                                bool $isCurrentNamespace,
398
                                                bool $includeStringContent): string {
399
400 4
    $dump = '';
401 4
    if (count($attributes) > 0) {
402 4
      if (!$isCurrentNamespace) {
403 1
        $dump .= str_repeat(self::INDENT, $depth)
404 1
                 . "->attributes('$nsAlias', true)" . PHP_EOL;
405
      }
406
407 4
      foreach ($attributes as $sx_attribute) {
408
        // Output the attribute
409 4
        if ($isCurrentNamespace) {
410
          // In current namespace
411
          // e.g. ['attribName']
412 3
          $dump .= str_repeat(self::INDENT, $depth)
413 3
                   . "['" . $sx_attribute->getName() . "']"
414 3
                   . PHP_EOL;
415 3
          $string_display_depth = $depth + 1;
416
        } else {
417
          // After a call to ->attributes()
418
          // e.g. ->attribName
419 1
          $dump .= str_repeat(self::INDENT, $depth + 1)
420 1
                   . '->' . $sx_attribute->getName()
421 1
                   . PHP_EOL;
422 1
          $string_display_depth = $depth + 2;
423
        }
424
425 4
        if ($includeStringContent) {
426
          // Show a chunk of the beginning of the content string, collapsing whitespace HTML-style
427 4
          $dump .= self::treeGetStringExtract((string) $sx_attribute,
428
                                              $string_display_depth);
429
        }
430
      }
431
    }
432 4
    return $dump;
433
  }
434
435
  /**
436
   * @param \SimpleXMLElement $children
437
   * @param string            $nsAlias
438
   * @param int               $depth
439
   * @param bool              $isCurrentNamespace
440
   * @param bool              $includeStringContent
441
   * @return string
442
   */
443 4
  private static function treeProcessChildren(\SimpleXMLElement $children,
444
                                              string $nsAlias,
445
                                              int $depth,
446
                                              bool $isCurrentNamespace,
447
                                              bool $includeStringContent): string {
448
449 4
    $dump = '';
450 4
    if (count($children) > 0) {
451 4
      if ($isCurrentNamespace) {
452 4
        $display_depth = $depth;
453
      } else {
454 1
        $dump .= str_repeat(self::INDENT, $depth)
455 1
                 . "->children('$nsAlias', true)" . PHP_EOL;
456 1
        $display_depth = $depth + 1;
457
      }
458
459
      // Recurse through the children with headers showing how to access them
460 4
      $child_names = [];
461 4
      foreach ($children as $sx_child) {
462
        // Below is a rather clunky way of saying $child_names[ $sx_child->getName() ]++;
463
        // 	which avoids Notices about unset array keys
464 4
        $child_node_name = $sx_child->getName();
465 4
        if (array_key_exists($child_node_name, $child_names)) {
466 4
          $child_names[$child_node_name]++;
467
        } else {
468 4
          $child_names[$child_node_name] = 1;
469
        }
470
471
        // e.g. ->Foo[0]
472 4
        $dump .= str_repeat(self::INDENT, $display_depth)
473 4
                 . '->' . $sx_child->getName()
474 4
                 . '[' . ($child_names[$child_node_name] - 1) . ']'
475 4
                 . PHP_EOL;
476
477 4
        $dump .= self::recursivelyProcessNode(
478
          $sx_child,
479 4
          $display_depth + 1,
480
          $includeStringContent
481
        );
482
      }
483
    }
484 4
    return $dump;
485
  }
486
487
  /**
488
   * @param $item
489
   * @param $depth
490
   * @param $include_string_content
491
   * @return string
492
   */
493 4
  private static function recursivelyProcessNode(\SimpleXMLElement $item,
494
                                                 $depth,
495
                                                 $include_string_content): string {
496
497 4
    $dump = '';
498
499 4
    if ($include_string_content) {
500
      // Show a chunk of the beginning of the content string, collapsing whitespace HTML-style
501 1
      $dump = self::treeGetStringExtract((string) $item, $depth);
502
    }
503
504 4
    $itemNs = self::treeGetNamespaces($item);
505 4
    foreach ($itemNs as $ns_alias => $ns_uri) {
506
507
508
      // If things are in the current namespace, display them a bit differently
509 4
      $is_current_namespace = ($ns_uri === reset($itemNs));
510
511 4
      $dump .= self::treeProcessAttributes($item->attributes($ns_alias, TRUE),
512
                                           $ns_alias,
513
                                           $depth,
514
                                           $is_current_namespace,
515
                                           $include_string_content);
516 4
      $dump .= self::treeProcessChildren($item->children($ns_alias, TRUE),
517
                                         $ns_alias,
518
                                         $depth,
519
                                         $is_current_namespace,
520
                                         $include_string_content);
521
    }
522
523 4
    return $dump;
524
  }
525
526
}