| Total Complexity | 42 |
| Total Lines | 240 |
| Duplicated Lines | 0 % |
| Changes | 2 | ||
| Bugs | 0 | Features | 0 |
Complex classes like XPath 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.
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 XPath, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 21 | class XPath |
||
| 22 | { |
||
| 23 | /** |
||
| 24 | * Search for an element with a certain name among the children of a reference element. |
||
| 25 | * |
||
| 26 | * @param \DOMNode $ref The DOMDocument or DOMElement where encrypted data is expected to be found as a child. |
||
| 27 | * @param string $name The name (possibly prefixed) of the element we are looking for. |
||
| 28 | * |
||
| 29 | * @return \DOMElement|false The element we are looking for, or false when not found. |
||
| 30 | * |
||
| 31 | * @throws \RuntimeException If no DOM document is available. |
||
| 32 | */ |
||
| 33 | public static function findElement(DOMNode $ref, string $name): DOMElement|false |
||
| 43 | } |
||
| 44 | |||
| 45 | |||
| 46 | /** |
||
| 47 | * Get an instance of DOMXPath associated with a DOMNode |
||
| 48 | * |
||
| 49 | * - Reuses a cached DOMXPath per document. |
||
| 50 | * - Registers core XML-related namespaces: 'xml' and 'xs'. |
||
| 51 | * - Enriches the XPath with all prefixed xmlns declarations found on the |
||
| 52 | * current node and its ancestors (up to the document element), so |
||
| 53 | * custom prefixes declared anywhere up the tree can be used in queries. |
||
| 54 | * |
||
| 55 | * @param \DOMNode $node The associated node |
||
| 56 | * @param bool $autoregister Whether to auto-register all namespaces used in the document |
||
| 57 | * @return \DOMXPath |
||
| 58 | */ |
||
| 59 | public static function getXPath(DOMNode $node, bool $autoregister = false): DOMXPath |
||
| 86 | } |
||
| 87 | |||
| 88 | |||
| 89 | /** |
||
| 90 | * Walk from the given node up to the document element, registering all prefixed xmlns declarations. |
||
| 91 | * |
||
| 92 | * Safety: |
||
| 93 | * - Only attributes in the XMLNS namespace (http://www.w3.org/2000/xmlns/). |
||
| 94 | * - Skip default xmlns (localName === 'xmlns') because XPath requires prefixes. |
||
| 95 | * - Skip empty URIs. |
||
| 96 | * - Do not override core 'xml' and 'xs' prefixes (already bound). |
||
| 97 | * - Nearest binding wins during this pass (prefixes are added once). |
||
| 98 | * |
||
| 99 | * @param \DOMXPath $xp |
||
| 100 | * @param \DOMNode $node |
||
| 101 | * @return array<string,string> Map of prefix => namespace URI that are bound after this pass |
||
| 102 | */ |
||
| 103 | private static function registerAncestorNamespaces(DOMXPath $xp, DOMNode $node): array |
||
| 104 | { |
||
| 105 | // Track prefix => uri to feed into subtree scan. Seed with core bindings. |
||
| 106 | $prefixToUri = [ |
||
| 107 | 'xml' => C_XML::NS_XML, |
||
| 108 | 'xs' => C_XS::NS_XS, |
||
| 109 | ]; |
||
| 110 | |||
| 111 | // Start from the nearest element (or documentElement if a DOMDocument is passed). |
||
| 112 | $current = $node instanceof DOMDocument |
||
| 113 | ? $node->documentElement |
||
| 114 | : ($node instanceof DOMElement ? $node : $node->parentNode); |
||
| 115 | |||
| 116 | $steps = 0; |
||
| 117 | |||
| 118 | while ($current instanceof DOMElement) { |
||
| 119 | if (++$steps > C_XML::UNBOUNDED_LIMIT) { |
||
| 120 | throw new RuntimeException(__METHOD__ . ': exceeded ancestor traversal limit'); |
||
| 121 | } |
||
| 122 | |||
| 123 | if ($current->hasAttributes()) { |
||
| 124 | foreach ($current->attributes as $attr) { |
||
| 125 | if ($attr->namespaceURI !== C_XML::NS_XMLNS) { |
||
| 126 | continue; |
||
| 127 | } |
||
| 128 | $prefix = $attr->localName; |
||
| 129 | $uri = (string) $attr->nodeValue; |
||
| 130 | |||
| 131 | if ( |
||
| 132 | $prefix === null || $prefix === '' || |
||
| 133 | $prefix === 'xmlns' || $uri === '' || |
||
| 134 | isset($prefixToUri[$prefix]) |
||
| 135 | ) { |
||
| 136 | continue; |
||
| 137 | } |
||
| 138 | |||
| 139 | $xp->registerNamespace($prefix, $uri); |
||
| 140 | $prefixToUri[$prefix] = $uri; |
||
| 141 | } |
||
| 142 | } |
||
| 143 | |||
| 144 | $current = $current->parentNode; |
||
| 145 | } |
||
| 146 | |||
| 147 | return $prefixToUri; |
||
| 148 | } |
||
| 149 | |||
| 150 | |||
| 151 | /** |
||
| 152 | * Single-pass subtree scan from the context element to bind prefixes used only on descendants. |
||
| 153 | * - Never rebind an already-registered prefix (collision-safe). |
||
| 154 | * - Skips 'xmlns' and empty URIs. |
||
| 155 | * - Bounded by UNBOUNDED_LIMIT. |
||
| 156 | * |
||
| 157 | * @param \DOMXPath $xp |
||
| 158 | * @param \DOMNode $node |
||
| 159 | * @param array<string,string> $prefixToUri |
||
| 160 | */ |
||
| 161 | private static function registerSubtreePrefixes(DOMXPath $xp, DOMNode $node, array $prefixToUri): void |
||
| 235 | } |
||
| 236 | } |
||
| 237 | } |
||
| 238 | } |
||
| 239 | |||
| 240 | |||
| 241 | /** |
||
| 242 | * Do an XPath query on an XML node. |
||
| 243 | * |
||
| 244 | * @param \DOMNode $node The XML node. |
||
| 245 | * @param string $query The query. |
||
| 246 | * @param \DOMXPath $xpCache The DOMXPath object |
||
| 247 | * @return array<\DOMNode> Array with matching DOM nodes. |
||
| 248 | */ |
||
| 249 | public static function xpQuery(DOMNode $node, string $query, DOMXPath $xpCache): array |
||
| 263 |