methods_plugin = new ClassMethodsPlugin($parser); } public function setParser(Parser $p): void { parent::setParser($p); $this->methods_plugin->setParser($p); } public function getTypes(): array { return ['object']; } public function getTriggers(): int { // SimpleXMLElement is a weirdo. No recursion (Or rather everything is // recursion) and depth limit will have to be handled manually anyway. return Parser::TRIGGER_BEGIN; } public function parseBegin(&$var, ContextInterface $c): ?AbstractValue { if (!$var instanceof SimpleXMLElement) { return null; } return $this->parseElement($var, $c); } protected function parseElement(SimpleXMLElement &$var, ContextInterface $c): SimpleXMLElementValue { $parser = $this->getParser(); $pdepth = $parser->getDepthLimit(); $cdepth = $c->getDepth(); $depthlimit = $pdepth && $cdepth >= $pdepth; $has_children = self::hasChildElements($var); if ($depthlimit && $has_children) { $x = new SimpleXMLElementValue($c, $var, [], null); $x->flags |= AbstractValue::FLAG_DEPTH_LIMIT; return $x; } $children = $this->getChildren($c, $var); $attributes = $this->getAttributes($c, $var); $toString = (string) $var; $string_body = !$has_children && \strlen($toString); $x = new SimpleXMLElementValue($c, $var, $children, \strlen($toString) ? $toString : null); if (self::$verbose) { $x = $this->methods_plugin->parseComplete($var, $x, Parser::TRIGGER_SUCCESS); } if ($attributes) { $x->addRepresentation(new ContainerRepresentation('Attributes', $attributes), 0); } if ($string_body) { $base = new BaseContext('(string) '.$c->getName()); $base->depth = $cdepth + 1; if (null !== ($ap = $c->getAccessPath())) { $base->access_path = '(string) '.$ap; } $toString = $parser->parse($toString, $base); $x->addRepresentation(new ValueRepresentation('toString', $toString, null, true), 0); } if ($children) { $x->addRepresentation(new ContainerRepresentation('Children', $children), 0); } return $x; } /** @psalm-return list */ protected function getAttributes(ContextInterface $c, SimpleXMLElement $var): array { $parser = $this->getParser(); $namespaces = \array_merge(['' => null], $var->getDocNamespaces()); $cdepth = $c->getDepth(); $ap = $c->getAccessPath(); $contents = []; foreach ($namespaces as $nsAlias => $_) { if ((bool) $nsAttribs = $var->attributes($nsAlias, true)) { foreach ($nsAttribs as $name => $attrib) { $obj = new ArrayContext($name); $obj->depth = $cdepth + 1; if (null !== $ap) { $obj->access_path = '(string) '.$ap; if ('' !== $nsAlias) { $obj->access_path .= '->attributes('.\var_export($nsAlias, true).', true)'; } $obj->access_path .= '['.\var_export($name, true).']'; } if ('' !== $nsAlias) { $obj->name = $nsAlias.':'.$obj->name; } $string = (string) $attrib; $attribute = $parser->parse($string, $obj); $contents[] = $attribute; } } } return $contents; } /** * Alright kids, let's learn about SimpleXMLElement::children! * children can take a namespace url or alias and provide a list of * child nodes. This is great since just accessing the members through * properties doesn't work on SimpleXMLElement when they have a * namespace at all! * * Unfortunately SimpleXML decided to go the retarded route of * categorizing elements by their tag name rather than by their local * name (to put it in Dom terms) so if you have something like this: * * * * * * * * * children(null) will get the first 2 results * * children('', true) will get the first 2 results * * children('http://localhost/') will get the last 2 results * * children('localhost', true) will get the last result * * So let's just give up and stick to aliases because fuck that mess! * * @psalm-return list */ protected function getChildren(ContextInterface $c, SimpleXMLElement $var): array { $namespaces = \array_merge(['' => null], $var->getDocNamespaces()); $cdepth = $c->getDepth(); $ap = $c->getAccessPath(); $contents = []; foreach ($namespaces as $nsAlias => $_) { if ((bool) $nsChildren = $var->children($nsAlias, true)) { $nsap = []; foreach ($nsChildren as $name => $child) { $base = new ClassOwnedContext((string) $name, SimpleXMLElement::class); $base->depth = $cdepth + 1; if ('' !== $nsAlias) { $base->name = $nsAlias.':'.$name; } if (null !== $ap) { if ('' === $nsAlias) { $base->access_path = $ap.'->'; } else { $base->access_path = $ap.'->children('.\var_export($nsAlias, true).', true)->'; } if (Utils::isValidPhpName((string) $name)) { $base->access_path .= (string) $name; } else { $base->access_path .= '{'.\var_export((string) $name, true).'}'; } if (isset($nsap[$base->access_path])) { ++$nsap[$base->access_path]; $base->access_path .= '['.$nsap[$base->access_path].']'; } else { $nsap[$base->access_path] = 0; } } $v = $this->parseElement($child, $base); $v->flags |= AbstractValue::FLAG_GENERATED; $contents[] = $v; } } } return $contents; } /** * More SimpleXMLElement bullshit. * * If we want to know if the element contains text we can cast to string. * Except if it contains text mixed with elements simplexml for some stupid * reason decides to concatenate the text from between those elements * rather than all the text in the hierarchy... * * So we have NO way of getting text nodes between elements, but we can * still tell if we have elements right? If we have elements we assume it's * not a string and call it a day! * * Well if you cast the element to an array attributes will be on it so * you'd have to remove that key, and if it's a string it'll also have the * 0 index used for the string contents too... * * Wait, can we use the 0 index to tell if it's a string? Nope! CDATA * doesn't show up AT ALL when casting to anything but string, and we'll * still get those concatenated strings of mostly whitespace if we just do * (string) and check the length. * * Luckily, I found the only way to do this reliably is through children(). * We still have to loop through all the namespaces and see if there's a * match but then we have the problem of the attributes showing up again... * * Or at least that's what var_dump says. And when we cast the result to * bool it's true too... But if we cast it to array then it's suddenly empty! * * Long story short the function below is the only way to reliably check if * a SimpleXMLElement has children */ protected static function hasChildElements(SimpleXMLElement $var): bool { $namespaces = \array_merge(['' => null], $var->getDocNamespaces()); foreach ($namespaces as $nsAlias => $_) { if ((array) $var->children($nsAlias, true)) { return true; } } return false; } }