├── stress.ini ├── src └── Xml │ ├── Exception │ ├── ExceptionInterface.php │ └── RuntimeException.php │ ├── Reader │ ├── Loader │ │ ├── Loader.php │ │ ├── xml_stream_loader.php │ │ ├── xml_file_loader.php │ │ └── xml_string_loader.php │ ├── Configurator │ │ ├── Configurator.php │ │ ├── substitute_entities.php │ │ ├── xsd_schema.php │ │ └── parser_options.php │ ├── Matcher │ │ ├── Matcher.php │ │ ├── document_element.php │ │ ├── not.php │ │ ├── element_name.php │ │ ├── element_position.php │ │ ├── element_local_name.php │ │ ├── namespaced_element.php │ │ ├── all.php │ │ ├── any.php │ │ ├── attribute_name.php │ │ ├── attribute_local_name.php │ │ ├── attribute_value.php │ │ ├── attribute_local_value.php │ │ ├── namespaced_attribute.php │ │ ├── namespaced_attribute_value.php │ │ ├── sequence.php │ │ └── nested.php │ ├── Signal.php │ ├── Node │ │ ├── AttributeNode.php │ │ ├── ElementNode.php │ │ ├── Pointer.php │ │ └── NodeSequence.php │ └── MatchingNode.php │ ├── Writer │ ├── Opener │ │ ├── Opener.php │ │ ├── memory_opener.php │ │ ├── xml_stream_opener.php │ │ └── xml_file_opener.php │ ├── Applicative │ │ ├── Applicative.php │ │ └── flush.php │ ├── Configurator │ │ ├── Configurator.php │ │ └── indentation.php │ ├── Builder │ │ ├── Builder.php │ │ ├── raw.php │ │ ├── value.php │ │ ├── attribute.php │ │ ├── attributes.php │ │ ├── children.php │ │ ├── prefixed_attribute.php │ │ ├── namespaced_attribute.php │ │ ├── cdata.php │ │ ├── comment.php │ │ ├── element.php │ │ ├── namespace_attribute.php │ │ ├── document.php │ │ ├── prefixed_element.php │ │ ├── prefixed_attributes.php │ │ ├── namespaced_attributes.php │ │ └── namespaced_element.php │ └── Mapper │ │ └── memory_output.php │ ├── Dom │ ├── Builder │ │ ├── Builder.php │ │ ├── value.php │ │ ├── escaped_value.php │ │ ├── attribute.php │ │ ├── default_xmlns_attribute.php │ │ ├── children.php │ │ ├── namespaced_attribute.php │ │ ├── attributes.php │ │ ├── xmlns_attribute.php │ │ ├── xmlns_attributes.php │ │ ├── namespaced_attributes.php │ │ ├── element.php │ │ ├── cdata.php │ │ ├── namespaced_element.php │ │ └── nodes.php │ ├── Loader │ │ ├── Loader.php │ │ ├── xml_node_loader.php │ │ ├── xml_string_loader.php │ │ ├── xml_file_loader.php │ │ └── xml_document_loader.php │ ├── Traverser │ │ ├── Action.php │ │ ├── Visitor.php │ │ ├── Action │ │ │ ├── Noop.php │ │ │ ├── RemoveNode.php │ │ │ ├── ReplaceNode.php │ │ │ └── RenameNode.php │ │ ├── Visitor │ │ │ ├── AbstractVisitor.php │ │ │ ├── SortAttributes.php │ │ │ └── RemoveNamespaces.php │ │ └── Traverser.php │ ├── Xpath │ │ ├── Configurator │ │ │ ├── Configurator.php │ │ │ ├── all_functions.php │ │ │ ├── php_namespace.php │ │ │ ├── functions.php │ │ │ ├── namespaces.php │ │ │ └── namespaced_functions.php │ │ └── Locator │ │ │ ├── Locator.php │ │ │ ├── query.php │ │ │ ├── evaluate.php │ │ │ └── query_single.php │ ├── Predicate │ │ ├── is_prefixed_node_name.php │ │ ├── is_whitespace.php │ │ ├── is_non_empty_text.php │ │ ├── is_default_xmlns_attribute.php │ │ ├── is_text.php │ │ ├── is_attribute.php │ │ ├── is_element.php │ │ ├── is_cdata.php │ │ ├── is_document.php │ │ ├── is_document_element.php │ │ └── is_xmlns_attribute.php │ ├── Configurator │ │ ├── Configurator.php │ │ ├── utf8.php │ │ ├── normalize.php │ │ ├── comparable.php │ │ ├── document_uri.php │ │ ├── format_output.php │ │ ├── optimize_namespaces.php │ │ ├── traverse.php │ │ ├── trim_spaces.php │ │ ├── pretty_print.php │ │ ├── canonicalize.php │ │ └── validator.php │ ├── Validator │ │ ├── Validator.php │ │ ├── xsd_validator.php │ │ ├── validator_chain.php │ │ └── internal_xsd_validator.php │ ├── Mapper │ │ ├── Mapper.php │ │ ├── xslt_template.php │ │ └── xml_string.php │ ├── Locator │ │ ├── Xmlns │ │ │ ├── linked_namespaces.php │ │ │ └── recursive_linked_namespaces.php │ │ ├── Node │ │ │ ├── children.php │ │ │ ├── value.php │ │ │ └── detect_document.php │ │ ├── root_namespace.php │ │ ├── Element │ │ │ ├── locate_by_tag_name.php │ │ │ ├── locate_by_namespaced_tag_name.php │ │ │ ├── children.php │ │ │ ├── parent_element.php │ │ │ ├── siblings.php │ │ │ └── ancestors.php │ │ ├── document_element.php │ │ ├── Xsd │ │ │ ├── locate_all_xsd_schemas.php │ │ │ ├── locate_no_namespaced_xsd_schemas.php │ │ │ └── locate_namespaced_xsd_schemas.php │ │ ├── Attribute │ │ │ ├── attributes_list.php │ │ │ └── xmlns_attributes_list.php │ │ ├── elements_with_tagname.php │ │ └── elements_with_namespaced_tagname.php │ ├── Assert │ │ ├── assert_element.php │ │ ├── assert_attribute.php │ │ ├── assert_cdata.php │ │ ├── assert_document.php │ │ └── assert_dom_node_list.php │ ├── Manipulator │ │ ├── Node │ │ │ ├── append_external_node.php │ │ │ ├── import_node_deeply.php │ │ │ ├── remove_namespace.php │ │ │ ├── rename.php │ │ │ ├── replace_by_external_node.php │ │ │ ├── remove.php │ │ │ └── replace_by_external_nodes.php │ │ ├── Xmlns │ │ │ ├── rename.php │ │ │ └── rename_element_namespace.php │ │ ├── Element │ │ │ ├── copy_named_xmlns_attributes.php │ │ │ └── rename.php │ │ ├── Attribute │ │ │ └── rename.php │ │ ├── append.php │ │ └── Document │ │ │ └── optimize_namespaces.php │ └── Xpath.php │ ├── Xslt │ ├── Loader │ │ ├── Loader.php │ │ └── from_template_document.php │ ├── Configurator │ │ ├── Configurator.php │ │ ├── all_functions.php │ │ ├── loader.php │ │ ├── functions.php │ │ ├── namespaced_functions.php │ │ ├── security_preferences.php │ │ ├── profiler.php │ │ └── parameters.php │ ├── Transformer │ │ ├── Transformer.php │ │ └── document_to_string.php │ └── Processor.php │ ├── Xsd │ └── Schema │ │ ├── Manipulator │ │ ├── Manipulator.php │ │ ├── overwrite_with_local_files.php │ │ └── base_path.php │ │ ├── Schema.php │ │ └── SchemaCollection.php │ ├── Encoding │ ├── Internal │ │ ├── Decoder │ │ │ └── Builder │ │ │ │ ├── name.php │ │ │ │ ├── attribute.php │ │ │ │ ├── unwrap_element.php │ │ │ │ ├── attributes.php │ │ │ │ ├── namespaces.php │ │ │ │ ├── group_child_elements.php │ │ │ │ ├── grouped_children.php │ │ │ │ └── element.php │ │ ├── Encoder │ │ │ └── Builder │ │ │ │ ├── is_node_list.php │ │ │ │ ├── root.php │ │ │ │ ├── children.php │ │ │ │ ├── parent_node.php │ │ │ │ ├── normalize_data.php │ │ │ │ ├── element.php │ │ │ │ └── xmlns_inheriting_element.php │ │ └── wrap_exception.php │ ├── XmlSerializable.php │ ├── xml_encode.php │ ├── typed.php │ ├── element_encode.php │ ├── xml_decode.php │ ├── element_decode.php │ ├── document_encode.php │ └── Exception │ │ └── EncodingException.php │ ├── ErrorHandling │ ├── issue_level_from_xml_error.php │ ├── Assertion │ │ └── assert_strict_prefixed_name.php │ ├── issue_from_xml_error.php │ ├── disallow_libxml_false_returns.php │ ├── issue_collection_from_xml_errors.php │ ├── disallow_issues.php │ ├── detect_issues.php │ ├── Issue │ │ ├── Issue.php │ │ ├── IssueCollection.php │ │ └── Level.php │ └── stop_on_first_issue.php │ ├── Internal │ └── configure.php │ └── Xmlns │ └── Xmlns.php ├── CONTRIBUTING.md ├── phpunit.stress.xml ├── infection.json.dist ├── LICENSE ├── psalm.xml ├── .php-cs-fixer.dist.php ├── README.md ├── composer.json └── docs └── xsd.md /stress.ini: -------------------------------------------------------------------------------- 1 | pcov.enabled=0 2 | opcache.enable_=1 3 | opcache.enable_cli=1 4 | opcache.jit_buffer_size=100M 5 | opcache.jit=tracing 6 | -------------------------------------------------------------------------------- /src/Xml/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | nodeValue ?? '') === ''; 12 | } 13 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/Matcher.php: -------------------------------------------------------------------------------- 1 | nodeValue ?? '') !== ''; 12 | } 13 | -------------------------------------------------------------------------------- /src/Xml/Dom/Traverser/Visitor.php: -------------------------------------------------------------------------------- 1 | prefix === null; 12 | } 13 | -------------------------------------------------------------------------------- /src/Xml/Dom/Predicate/is_text.php: -------------------------------------------------------------------------------- 1 | nodeName; 15 | } 16 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/Builder.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function __invoke(XMLWriter $writer): Generator; 16 | } 17 | -------------------------------------------------------------------------------- /src/Xml/Writer/Opener/memory_opener.php: -------------------------------------------------------------------------------- 1 | XMLWriter::toMemory(); 16 | } 17 | -------------------------------------------------------------------------------- /src/Xml/Dom/Predicate/is_cdata.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | function linked_namespaces(Element $node): array 14 | { 15 | return $node->getInScopeNamespaces(); 16 | } 17 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Node/children.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | function children(Node $node): NodeList 14 | { 15 | return NodeList::fromDOMNodeList($node->childNodes); 16 | } 17 | -------------------------------------------------------------------------------- /src/Xml/Dom/Predicate/is_document_element.php: -------------------------------------------------------------------------------- 1 | documentElement === $node; 13 | } 14 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Xmlns/recursive_linked_namespaces.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | function recursive_linked_namespaces(Element $node): array 14 | { 15 | return $node->getDescendantNamespaces(); 16 | } 17 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Decoder/Builder/attribute.php: -------------------------------------------------------------------------------- 1 | $attribute->nodeValue, 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/root_namespace.php: -------------------------------------------------------------------------------- 1 | document_element()($document)->namespaceURI; 16 | } 17 | -------------------------------------------------------------------------------- /src/Xml/Reader/Signal.php: -------------------------------------------------------------------------------- 1 | stopRequested = true; 13 | } 14 | 15 | public function stopRequested(): bool 16 | { 17 | return $this->stopRequested; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Xml/Writer/Mapper/memory_output.php: -------------------------------------------------------------------------------- 1 | outputMemory(); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Dom/Predicate/is_xmlns_attribute.php: -------------------------------------------------------------------------------- 1 | namespaceURI === Xmlns::xmlns()->value(); 17 | } 18 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Element/locate_by_tag_name.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | function locate_by_tag_name(Element $node, string $tag): NodeList 14 | { 15 | return NodeList::fromDOMHTMLCollection($node->getElementsByTagName($tag)); 16 | } 17 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/document_element.php: -------------------------------------------------------------------------------- 1 | parent(); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Dom/Assert/assert_element.php: -------------------------------------------------------------------------------- 1 | assert($node); 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/value.php: -------------------------------------------------------------------------------- 1 | substitutedNodeValue = $value; 17 | 18 | return $node; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/Xml/Dom/Configurator/utf8.php: -------------------------------------------------------------------------------- 1 | charset = 'UTF-8'; 17 | 18 | return $document; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/Xml/Dom/Configurator/normalize.php: -------------------------------------------------------------------------------- 1 | normalize(); 17 | 18 | return $document; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Decoder/Builder/unwrap_element.php: -------------------------------------------------------------------------------- 1 | $element 13 | */ 14 | function unwrap_element(array $element): array|string 15 | { 16 | return first($element) ?? '*UNKNOWN*'; 17 | } 18 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/not.php: -------------------------------------------------------------------------------- 1 | !$matcher($sequence); 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/escaped_value.php: -------------------------------------------------------------------------------- 1 | assert($node); 19 | } 20 | -------------------------------------------------------------------------------- /src/Xml/Dom/Assert/assert_cdata.php: -------------------------------------------------------------------------------- 1 | assert($node); 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Dom/Assert/assert_document.php: -------------------------------------------------------------------------------- 1 | assert($node); 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Dom/Assert/assert_dom_node_list.php: -------------------------------------------------------------------------------- 1 | assert($node); 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/attribute.php: -------------------------------------------------------------------------------- 1 | setAttribute($name, $value); 17 | 18 | return $node; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/element_name.php: -------------------------------------------------------------------------------- 1 | current()->name() === $name; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/document_element.php: -------------------------------------------------------------------------------- 1 | assert_element($document->documentElement); 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Dom/Manipulator/Node/append_external_node.php: -------------------------------------------------------------------------------- 1 | appendChild($copy); 17 | 18 | return $copy; 19 | } 20 | -------------------------------------------------------------------------------- /src/Xml/Reader/Configurator/substitute_entities.php: -------------------------------------------------------------------------------- 1 | parser_options([ 16 | XMLReader::SUBST_ENTITIES => $flag, 17 | ])($reader); 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Dom/Xpath/Configurator/all_functions.php: -------------------------------------------------------------------------------- 1 | registerPhpFunctions(); 18 | 19 | return $xpath; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/Xml/Xslt/Configurator/all_functions.php: -------------------------------------------------------------------------------- 1 | registerPhpFunctions(); 17 | 18 | return $processor; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/element_position.php: -------------------------------------------------------------------------------- 1 | current()->position() === $position; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Writer/Applicative/flush.php: -------------------------------------------------------------------------------- 1 | flush($empty); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/Xml/Dom/Xpath/Configurator/php_namespace.php: -------------------------------------------------------------------------------- 1 | Xmlns::phpXpath()->value()])($xpath); 18 | 19 | return $xpath; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/element_local_name.php: -------------------------------------------------------------------------------- 1 | current()->localName() === $localName; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Element/locate_by_namespaced_tag_name.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | function locate_by_namespaced_tag_name(Element $node, string $namespace, string $localTagName): NodeList 14 | { 15 | return NodeList::fromDOMHTMLCollection($node->getElementsByTagNameNS($namespace, $localTagName)); 16 | } 17 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Node/value.php: -------------------------------------------------------------------------------- 1 | $type 15 | * 16 | * @return T 17 | * 18 | * @throws CoercionException 19 | */ 20 | function value(Node $node, TypeInterface $type) 21 | { 22 | return $type->coerce($node->substitutedNodeValue ?? ''); 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/ErrorHandling/issue_level_from_xml_error.php: -------------------------------------------------------------------------------- 1 | level) { 13 | LIBXML_ERR_ERROR => Level::error(), 14 | LIBXML_ERR_FATAL => Level::fatal(), 15 | LIBXML_ERR_WARNING => Level::warning(), 16 | default => null 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/raw.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | function raw(string $value): Closure 15 | { 16 | return 17 | /** 18 | * @return Generator 19 | */ 20 | static function (XMLWriter $writer) use ($value): Generator { 21 | yield $writer->writeRaw($value); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/value.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | function value(string $value): Closure 15 | { 16 | return 17 | /** 18 | * @return Generator 19 | */ 20 | static function (XMLWriter $writer) use ($value): Generator { 21 | yield $writer->text($value); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Encoder/Builder/is_node_list.php: -------------------------------------------------------------------------------- 1 | matches($data); 19 | } 20 | -------------------------------------------------------------------------------- /src/Xml/ErrorHandling/Assertion/assert_strict_prefixed_name.php: -------------------------------------------------------------------------------- 1 | ownerDocument); 19 | } 20 | -------------------------------------------------------------------------------- /src/Xml/Dom/Traverser/Action/RemoveNode.php: -------------------------------------------------------------------------------- 1 | $target->rename(Xmlns::xmlns()->value(), $newQName)); 18 | 19 | return $target; 20 | } 21 | -------------------------------------------------------------------------------- /src/Xml/Dom/Configurator/document_uri.php: -------------------------------------------------------------------------------- 1 | documentURI = $documentUri; 18 | 19 | return $document; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/default_xmlns_attribute.php: -------------------------------------------------------------------------------- 1 | setAttributeNS(Xmlns::xmlns()->value(), 'xmlns', $namespaceURI); 18 | 19 | return $node; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/Xml/Dom/Traverser/Visitor/AbstractVisitor.php: -------------------------------------------------------------------------------- 1 | disallow_issues(static function () use ($stream) : XMLWriter { 19 | return XMLWriter::toStream($stream); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/Xml/Dom/Xpath/Configurator/functions.php: -------------------------------------------------------------------------------- 1 | $functions 12 | * 13 | * @return Closure(XPath): XPath 14 | */ 15 | function functions(array $functions): Closure 16 | { 17 | return static function (XPath $xpath) use ($functions) : XPath { 18 | php_namespace()($xpath); 19 | $xpath->registerPhpFunctions($functions); 20 | 21 | return $xpath; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/Xslt/Configurator/functions.php: -------------------------------------------------------------------------------- 1 | $functions 12 | * 13 | * @return Closure(XSLTProcessor): XSLTProcessor 14 | */ 15 | function functions(array $functions): Closure 16 | { 17 | return static function (XSLTProcessor $processor) use ($functions) : XSLTProcessor { 18 | $processor->registerPhpFunctions($functions); 19 | 20 | return $processor; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/children.php: -------------------------------------------------------------------------------- 1 | $builders 14 | * 15 | * @return Closure(T): T 16 | */ 17 | function children(callable ...$builders): Closure 18 | { 19 | return static function (Node $node) use ($builders): Node { 20 | foreach ($builders as $builder) { 21 | $node->appendChild($builder($node)); 22 | } 23 | 24 | return $node; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/Xml/Dom/Configurator/format_output.php: -------------------------------------------------------------------------------- 1 | formatOutput = $formatOutput; 19 | 20 | return $document; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/Xml/ErrorHandling/issue_from_xml_error.php: -------------------------------------------------------------------------------- 1 | code, 20 | $error->column, 21 | $error->message, 22 | $error->file, 23 | $error->line, 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Xsd/locate_all_xsd_schemas.php: -------------------------------------------------------------------------------- 1 | current(); 17 | 18 | return $current->localName() === $localName && $current->namespace() === $namespace; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/wrap_exception.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function xmlSerialize(): mixed; 22 | } 23 | -------------------------------------------------------------------------------- /src/Xml/ErrorHandling/disallow_libxml_false_returns.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | function attributes_list(Node $node): NodeList 17 | { 18 | if (!is_element($node)) { 19 | return NodeList::empty(); 20 | } 21 | 22 | $attributes = values($node->attributes); 23 | 24 | return new NodeList(...$attributes); 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Dom/Xpath/Configurator/namespaces.php: -------------------------------------------------------------------------------- 1 | $namespaces 12 | * 13 | * @return Closure(XPath): XPath 14 | */ 15 | function namespaces(array $namespaces): Closure 16 | { 17 | return static function (XPath $xpath) use ($namespaces) : XPath { 18 | foreach ($namespaces as $prefix => $namespaceURI) { 19 | $xpath->registerNamespace($prefix, $namespaceURI); 20 | } 21 | 22 | return $xpath; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Attribute/xmlns_attributes_list.php: -------------------------------------------------------------------------------- 1 | 15 | * @throws RuntimeException 16 | */ 17 | function xmlns_attributes_list(Node $node): NodeList 18 | { 19 | return attributes_list($node) 20 | ->filter(static fn (Attr $attribute): bool => is_xmlns_attribute($attribute)); 21 | } 22 | -------------------------------------------------------------------------------- /src/Xml/Reader/Loader/xml_stream_loader.php: -------------------------------------------------------------------------------- 1 | disallow_issues( 18 | static fn (): XMLReader => XMLReader::fromStream($stream, $encoding, $flags, $documentUri) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/Xml/Dom/Configurator/optimize_namespaces.php: -------------------------------------------------------------------------------- 1 | $document->schemaValidate($xsd)); 19 | 20 | return $issues; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Element/children.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | function children(Node $node): NodeList 17 | { 18 | /** @var list $children */ 19 | $children = filter( 20 | $node->childNodes, 21 | static fn (Node $node): bool => is_element($node) 22 | ); 23 | 24 | return new NodeList(...$children); 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Element/parent_element.php: -------------------------------------------------------------------------------- 1 | parentNode; 18 | if (!$parent|| !is_element($parent)) { 19 | throw RuntimeException::withMessage('Can not find parent element for '.$child::class.' '.$child->nodeName); 20 | } 21 | 22 | return $parent; 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/attribute.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | function attribute(string $name, string $value): Closure 15 | { 16 | return 17 | /** 18 | * @return Generator 19 | */ 20 | static function (XMLWriter $writer) use ($name, $value): Generator { 21 | yield $writer->startAttribute($name); 22 | yield $writer->text($value); 23 | yield $writer->endAttribute(); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/all.php: -------------------------------------------------------------------------------- 1 | $matchers 13 | * 14 | * @return Closure(NodeSequence): bool 15 | */ 16 | function all(callable ... $matchers): Closure 17 | { 18 | return static fn (NodeSequence $sequence): bool => Iter\all( 19 | $matchers, 20 | /** 21 | * @param callable(NodeSequence): bool $matcher 22 | */ 23 | static fn (callable $matcher): bool => $matcher($sequence) 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/any.php: -------------------------------------------------------------------------------- 1 | $matchers 13 | * 14 | * @return Closure(NodeSequence): bool 15 | */ 16 | function any(callable ... $matchers): Closure 17 | { 18 | return static fn (NodeSequence $sequence): bool => Iter\any( 19 | $matchers, 20 | /** 21 | * @param callable(NodeSequence): bool $matcher 22 | */ 23 | static fn (callable $matcher): bool => $matcher($sequence) 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Dom/Configurator/traverse.php: -------------------------------------------------------------------------------- 1 | $visitors 15 | * 16 | * @return Closure(XMLDocument): XMLDocument 17 | */ 18 | function traverse(Visitor ... $visitors): Closure 19 | { 20 | return static function (XMLDocument $document) use ($visitors): XMLDocument { 21 | Document::fromUnsafeDocument($document)->traverse(...$visitors); 22 | 23 | return $document; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/attribute_name.php: -------------------------------------------------------------------------------- 1 | current()->attributes(), 20 | static fn (AttributeNode $attribute): bool => $attribute->name() === $name 21 | ); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/ErrorHandling/issue_collection_from_xml_errors.php: -------------------------------------------------------------------------------- 1 | $errors 15 | */ 16 | function issue_collection_from_xml_errors(array $errors): IssueCollection 17 | { 18 | return new IssueCollection( 19 | ...Vec\filter_nulls(Dict\map( 20 | $errors, 21 | static fn (LibXMLError $error): ?Issue => issue_from_xml_error($error) 22 | )) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/namespaced_attribute.php: -------------------------------------------------------------------------------- 1 | setAttributeNS($namespace, $qualifiedName, $value); 20 | 21 | return $node; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/Encoding/xml_encode.php: -------------------------------------------------------------------------------- 1 | $configurators 13 | * 14 | * @throws EncodingException 15 | */ 16 | function xml_encode(array $data, callable ... $configurators): string 17 | { 18 | return wrap_exception( 19 | static function () use ($data, $configurators): string { 20 | return document_encode($data, ...$configurators)->toXmlString(); 21 | } 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/Dom/Traverser/Action/ReplaceNode.php: -------------------------------------------------------------------------------- 1 | newNode); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/attributes.php: -------------------------------------------------------------------------------- 1 | $attributes 13 | * @return Closure(XMLWriter): Generator 14 | */ 15 | function attributes(array $attributes): Closure 16 | { 17 | return 18 | /** 19 | * @return Generator 20 | */ 21 | static function (XMLWriter $writer) use ($attributes): Generator { 22 | foreach ($attributes as $key => $value) { 23 | yield from attribute($key, $value)($writer); 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/attributes.php: -------------------------------------------------------------------------------- 1 | $attributes 13 | * @return Closure(Element): Element 14 | */ 15 | function attributes(array $attributes): Closure 16 | { 17 | return static function (Element $node) use ($attributes): Element { 18 | return reduce_with_keys( 19 | $attributes, 20 | static fn (Element $node, string $name, string $value) 21 | => attribute($name, $value)($node), 22 | $node 23 | ); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Dom/Configurator/trim_spaces.php: -------------------------------------------------------------------------------- 1 | toUnsafeDocument(); 21 | 22 | $trimmed->formatOutput = false; 23 | 24 | return $trimmed; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/Xml/Reader/Loader/xml_file_loader.php: -------------------------------------------------------------------------------- 1 | disallow_issues( 18 | static function () use ($file, $encoding, $flags): XMLReader { 19 | Assert::fileExists($file); 20 | 21 | return XMLReader::fromUri($file, $encoding, $flags); 22 | } 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/attribute_local_name.php: -------------------------------------------------------------------------------- 1 | current()->attributes(), 20 | static fn (AttributeNode $attribute): bool => $attribute->localName() === $localName 21 | ); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Element/siblings.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | function siblings(Node $node): NodeList 17 | { 18 | /** @var NodeList $siblings */ 19 | $siblings = new NodeList(...filter( 20 | $node->parentNode?->childNodes?->getIterator() ?? [], 21 | static fn (Node $sibling): bool => is_element($sibling) && $sibling !== $node 22 | )); 23 | 24 | return $siblings; 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Dom/Loader/xml_node_loader.php: -------------------------------------------------------------------------------- 1 | disallow_issues(static function () use ($importedNode): XMLDocument { 19 | $document = XMLDocument::createEmpty(); 20 | 21 | append_external_node($document, $importedNode); 22 | 23 | return $document; 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Dom/Configurator/pretty_print.php: -------------------------------------------------------------------------------- 1 | toUnsafeDocument(); 21 | 22 | $prettyPrinted->formatOutput = true; 23 | 24 | return $prettyPrinted; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/children.php: -------------------------------------------------------------------------------- 1 | )> $nodeBuilders 13 | * 14 | * @return Closure(XMLWriter): Generator 15 | */ 16 | function children(iterable $nodeBuilders): Closure 17 | { 18 | return 19 | /** 20 | * @return Generator 21 | */ 22 | static function (XMLWriter $writer) use ($nodeBuilders): Generator { 23 | foreach ($nodeBuilders as $nodeBuilder) { 24 | yield from $nodeBuilder($writer); 25 | } 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/prefixed_attribute.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | function prefixed_attribute(string $prefix, string $name, string $value): Closure 15 | { 16 | return 17 | /** 18 | * @return Generator 19 | */ 20 | static function (XMLWriter $writer) use ($prefix, $name, $value): Generator { 21 | yield $writer->startAttributeNs($prefix, $name, null); 22 | yield $writer->text($value); 23 | yield $writer->endAttribute(); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Dom/Mapper/xslt_template.php: -------------------------------------------------------------------------------- 1 | $configurators 15 | * @return Closure(XMLDocument): string 16 | */ 17 | function xslt_template(Document $template, callable ... $configurators): Closure 18 | { 19 | return static fn (XMLDocument $document): string => 20 | Processor::fromTemplateDocument($template, ...$configurators)->transformDocumentToString( 21 | Document::fromUnsafeDocument($document) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/xmlns_attribute.php: -------------------------------------------------------------------------------- 1 | setAttributeNS(Xmlns::xmlns()->value(), $prefixed, $namespaceURI); 22 | 23 | return $node; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/attribute_value.php: -------------------------------------------------------------------------------- 1 | current()->attributes(), 20 | static fn (AttributeNode $attribute): bool => $attribute->name() === $name && $attribute->value() === $value 21 | ); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/Encoding/typed.php: -------------------------------------------------------------------------------- 1 | $type 17 | * @param list $configurators 18 | * 19 | * @return T 20 | * 21 | * @throws CoercionException 22 | * @throws EncodingException 23 | */ 24 | function typed(string $xml, TypeInterface $type, callable ... $configurators) 25 | { 26 | return $type->coerce(xml_decode($xml, ...$configurators)); 27 | } 28 | -------------------------------------------------------------------------------- /src/Xml/Dom/Traverser/Action/RenameNode.php: -------------------------------------------------------------------------------- 1 | newQName, $this->newNamespaceURI); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Xml/Reader/Loader/xml_string_loader.php: -------------------------------------------------------------------------------- 1 | disallow_issues( 18 | static function () use ($xml, $encoding, $flags): XMLReader { 19 | Assert::notEmpty($xml, 'The provided XML can not be empty!'); 20 | 21 | return XMLReader::fromString($xml, $encoding, $flags); 22 | } 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/elements_with_tagname.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | function elements_with_tagname(string $tagName): Closure 17 | { 18 | return 19 | /** 20 | * @return NodeList 21 | */ 22 | static fn (XMLDocument $document): NodeList 23 | => locate_by_tag_name( 24 | document_element()($document), 25 | $tagName 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/Xml/Xslt/Configurator/namespaced_functions.php: -------------------------------------------------------------------------------- 1 | $functions 12 | * 13 | * @return Closure(XSLTProcessor): XSLTProcessor 14 | */ 15 | function namespaced_functions(string $namespace, array $functions): Closure 16 | { 17 | return static function (XSLTProcessor $processor) use ($namespace, $functions) : XSLTProcessor { 18 | foreach ($functions as $functionName => $callback) { 19 | $processor->registerPhpFunctionNS($namespace, $functionName, $callback); 20 | } 21 | 22 | return $processor; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/attribute_local_value.php: -------------------------------------------------------------------------------- 1 | current()->attributes(), 20 | static fn (AttributeNode $attribute): bool => $attribute->localName() === $localName && $attribute->value() === $value 21 | ); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/namespaced_attribute.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | function namespaced_attribute(string $namespace, ?string $prefix, string $name, string $value): Closure 15 | { 16 | return 17 | /** 18 | * @return Generator 19 | */ 20 | static function (XMLWriter $writer) use ($namespace, $prefix, $name, $value): Generator { 21 | yield $writer->startAttributeNs($prefix, $name, $namespace); 22 | yield $writer->text($value); 23 | yield $writer->endAttribute(); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/xmlns_attributes.php: -------------------------------------------------------------------------------- 1 | $attributes - A map of namespace prefix with namespace URI 13 | * @return Closure(Element): Element 14 | */ 15 | function xmlns_attributes(array $attributes): Closure 16 | { 17 | return static function (Element $node) use ($attributes): Element { 18 | return reduce_with_keys( 19 | $attributes, 20 | static fn (Element $node, string $name, string $value) 21 | => xmlns_attribute($name, $value)($node), 22 | $node 23 | ); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Dom/Loader/xml_string_loader.php: -------------------------------------------------------------------------------- 1 | disallow_issues(static function () use ($xml, $options, $override_encoding): XMLDocument { 19 | return XMLDocument::createFromString($xml, $options, $override_encoding); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/namespaced_attributes.php: -------------------------------------------------------------------------------- 1 | $attributes 13 | * @return Closure(Element): Element 14 | */ 15 | function namespaced_attributes(string $namespace, array $attributes): Closure 16 | { 17 | return static function (Element $node) use ($namespace, $attributes): Element { 18 | return reduce_with_keys( 19 | $attributes, 20 | static fn (Element $node, string $name, string $value) 21 | => namespaced_attribute($namespace, $name, $value)($node), 22 | $node 23 | ); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/namespaced_attribute.php: -------------------------------------------------------------------------------- 1 | current()->attributes(), 20 | static fn (AttributeNode $attribute): bool => $attribute->localName() === $localName && $attribute->namespace() === $namespace 21 | ); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | VeeWee/XML is an open source, community-driven project. If you'd like to contribute, 4 | feel free to do this, but remember to follow this few simple rules: 5 | 6 | ## Branching strategy 7 | 8 | - __Always__ base your changes on the `master` branch (all new development happens here), 9 | - When you create Pull Request, always select `master` branch as target, otherwise it 10 | will be closed (this is selected by default). 11 | 12 | ## Coverage 13 | 14 | - All classes and functions that are added to the project .should be covered by Tests. 15 | - All classes and functions need to be documented 16 | 17 | ## Code style / Formatting 18 | 19 | - All code in the `src` folder must follow the PSR-2 standard that is enforced by code styling tools. 20 | -------------------------------------------------------------------------------- /phpunit.stress.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | tests/Stress 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Xml/Dom/Xpath/Configurator/namespaced_functions.php: -------------------------------------------------------------------------------- 1 | $functions 12 | * 13 | * @return Closure(XPath): XPath 14 | */ 15 | function namespaced_functions(string $namespace, string $prefix, array $functions): Closure 16 | { 17 | return static function (XPath $xpath) use ($namespace, $prefix, $functions) : XPath { 18 | namespaces([$prefix => $namespace])($xpath); 19 | foreach ($functions as $functionName => $callback) { 20 | $xpath->registerPhpFunctionNS($namespace, $functionName, $callback); 21 | } 22 | 23 | return $xpath; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Dom/Loader/xml_file_loader.php: -------------------------------------------------------------------------------- 1 | disallow_issues(static function () use ($file, $options, $override_encoding): XMLDocument { 19 | Assert::fileExists($file); 20 | 21 | return XMLDocument::createFromFile($file, $options, $override_encoding); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/Dom/Manipulator/Element/copy_named_xmlns_attributes.php: -------------------------------------------------------------------------------- 1 | forEach(static function (Attr $xmlns) use ($target) { 19 | if ($xmlns->prefix !== null && !$target->hasAttribute($xmlns->nodeName)) { 20 | xmlns_attribute($xmlns->localName, $xmlns->value)($target); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/cdata.php: -------------------------------------------------------------------------------- 1 | )> $configurators 13 | * 14 | * @return Closure(XMLWriter): Generator 15 | */ 16 | function cdata(callable ...$configurators): Closure 17 | { 18 | return 19 | /** 20 | * @return Generator 21 | */ 22 | static function (XMLWriter $writer) use ($configurators): Generator { 23 | yield $writer->startCdata(); 24 | foreach ($configurators as $configurator) { 25 | yield from $configurator($writer); 26 | } 27 | 28 | yield $writer->endCdata(); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "minMsi": 100, 8 | "minCoveredMsi": 100, 9 | "logs": { 10 | "text": ".phpunit.cache/infection.log", 11 | "html": ".phpunit.cache/infection" 12 | }, 13 | "mutators": { 14 | "@default": true, 15 | "ReturnRemoval": { 16 | "ignore": [ 17 | "VeeWee\\Xml\\Dom\\Traverser\\Visitor\\RemoveNamespaces::onNodeLeave", 18 | "VeeWee\\Xml\\Dom\\Traverser\\Visitor\\SortAttributes::onNodeEnter", 19 | "VeeWee\\Xml\\Reader\\Reader::provide", 20 | ] 21 | }, 22 | "CastInt": { 23 | "ignore": [ 24 | "VeeWee\\Xml\\*Exception::__construct" 25 | ] 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/comment.php: -------------------------------------------------------------------------------- 1 | )> $configurators 13 | * 14 | * @return Closure(XMLWriter): Generator 15 | */ 16 | function comment(callable ...$configurators): Closure 17 | { 18 | return 19 | /** 20 | * @return Generator 21 | */ 22 | static function (XMLWriter $writer) use ($configurators): Generator { 23 | yield $writer->startComment(); 24 | foreach ($configurators as $configurator) { 25 | yield from $configurator($writer); 26 | } 27 | 28 | yield $writer->endComment(); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/element.php: -------------------------------------------------------------------------------- 1 | $configurators 16 | * 17 | * @return Closure(Node): Element 18 | */ 19 | function element(string $name, callable ...$configurators): Closure 20 | { 21 | return static function (Node $node) use ($name, $configurators): Element { 22 | $document = detect_document($node); 23 | 24 | return assert_element( 25 | configure(...$configurators)($document->createElement($name)) 26 | ); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/cdata.php: -------------------------------------------------------------------------------- 1 | $configurators 16 | * 17 | * @return Closure(Node): CDATASection 18 | */ 19 | function cdata(string $data, ...$configurators): Closure 20 | { 21 | return static function (Node $node) use ($data, $configurators): CDATASection { 22 | $document = detect_document($node); 23 | 24 | return assert_cdata( 25 | configure(...$configurators)($document->createCDATASection($data)) 26 | ); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/Xml/Writer/Configurator/indentation.php: -------------------------------------------------------------------------------- 1 | setIndent(true), 19 | 'Unable to enable writer indentation.' 20 | ); 21 | 22 | disallow_libxml_false_returns( 23 | $writer->setIndentString($indentation), 24 | 'Unable to register indentation string.' 25 | ); 26 | 27 | return $writer; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/elements_with_namespaced_tagname.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | function elements_with_namespaced_tagname(string $namespace, string $localTagName): Closure 17 | { 18 | return 19 | /** 20 | * @return NodeList 21 | */ 22 | static fn (XMLDocument $document): NodeList 23 | => locate_by_namespaced_tag_name( 24 | document_element()($document), 25 | $namespace, 26 | $localTagName 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/element.php: -------------------------------------------------------------------------------- 1 | )> $configurators 13 | * 14 | * @return Closure(XMLWriter): Generator 15 | */ 16 | function element(string $name, callable ...$configurators): Closure 17 | { 18 | return 19 | /** 20 | * @return Generator 21 | */ 22 | static function (XMLWriter $writer) use ($name, $configurators): Generator { 23 | yield $writer->startElement($name); 24 | foreach ($configurators as $configurator) { 25 | yield from $configurator($writer); 26 | } 27 | 28 | yield $writer->endElement(); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/namespace_attribute.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | function namespace_attribute(string $namespace, ?string $prefix = null): Closure 16 | { 17 | return 18 | /** 19 | * @return Generator 20 | */ 21 | static function (XMLWriter $writer) use ($namespace, $prefix): Generator { 22 | if ($prefix !== null) { 23 | yield from prefixed_attribute('xmlns', $prefix, $namespace)($writer); 24 | return; 25 | } 26 | 27 | yield from attribute('xmlns', $namespace)($writer); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/Xml/Writer/Opener/xml_file_opener.php: -------------------------------------------------------------------------------- 1 | disallow_issues(static function () use ($file) : XMLWriter { 21 | // Try to create the file first. 22 | // If the file exists, it will truncated. (Default behaviour of XMLWriter as well) 23 | // If it cannot be created, it will throw exceptions. 24 | write($file, '', WriteMode::Truncate); 25 | 26 | return XMLWriter::toUri($file); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/document.php: -------------------------------------------------------------------------------- 1 | )> $configurators 13 | * 14 | * @return Closure(XMLWriter): Generator 15 | */ 16 | function document(string $version, string $charset, callable ... $configurators): Closure 17 | { 18 | return 19 | /** 20 | * @return Generator 21 | */ 22 | static function (XMLWriter $writer) use ($version, $charset, $configurators): Generator { 23 | yield $writer->startDocument($version, $charset); 24 | foreach ($configurators as $configurator) { 25 | yield from $configurator($writer); 26 | } 27 | yield $writer->endDocument(); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Element/ancestors.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | function ancestors(Node $node): NodeList 17 | { 18 | return new NodeList( 19 | ...( 20 | /** 21 | * @return Generator 22 | */ 23 | static function (Node $next) { 24 | while (($parent = $next->parentNode) !== null) { 25 | if (is_element($parent)) { 26 | yield $parent; 27 | } 28 | $next = $parent; 29 | } 30 | } 31 | )($node) 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/Xml/Encoding/element_encode.php: -------------------------------------------------------------------------------- 1 | $configurators 15 | * 16 | * @throws EncodingException 17 | */ 18 | function element_encode(array $data, callable ... $configurators): string 19 | { 20 | return wrap_exception( 21 | static function () use ($data, $configurators): string { 22 | $doc = document_encode($data, ...$configurators); 23 | $root = $doc->locate(document_element()); 24 | 25 | return xml_string()($root); 26 | } 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/namespaced_attribute_value.php: -------------------------------------------------------------------------------- 1 | current()->attributes(), 20 | static fn (AttributeNode $attribute): bool => 21 | $attribute->localName() === $localName 22 | && $attribute->namespace() === $namespace 23 | && $attribute->value() === $value 24 | ); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Decoder/Builder/attributes.php: -------------------------------------------------------------------------------- 1 | reduce( 22 | attributes_list($element)->filter(static fn (Attr $attr): bool => !is_xmlns_attribute($attr)), 23 | static fn (array $attributes, Attr $attr): array 24 | => merge($attributes, attribute($attr)), 25 | [] 26 | ) 27 | ]); 28 | } 29 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/prefixed_element.php: -------------------------------------------------------------------------------- 1 | )> $configurators 13 | * 14 | * @return Closure(XMLWriter): Generator 15 | */ 16 | function prefixed_element(string $prefix, string $name, callable ...$configurators): Closure 17 | { 18 | return 19 | /** 20 | * @return Generator 21 | */ 22 | static function (XMLWriter $writer) use ($prefix, $name, $configurators): Generator { 23 | yield $writer->startElementNs($prefix, $name, null); 24 | foreach ($configurators as $configurator) { 25 | yield from $configurator($writer); 26 | } 27 | 28 | yield $writer->endElement(); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/Xml/Dom/Manipulator/Node/import_node_deeply.php: -------------------------------------------------------------------------------- 1 | importNode($source, true), 24 | 'Cannot import node: Node Type Not Supported' 25 | ); 26 | } 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Decoder/Builder/namespaces.php: -------------------------------------------------------------------------------- 1 | xmlns_attributes_list($element)->reduce( 23 | static fn (array $namespaces, Attr $node) => merge($namespaces, [ 24 | ($node->prefix !== null ? $node->localName : '') => $node->value 25 | ]), 26 | [] 27 | ), 28 | ]); 29 | } 30 | -------------------------------------------------------------------------------- /src/Xml/Reader/Configurator/xsd_schema.php: -------------------------------------------------------------------------------- 1 | disallow_issues(static function () use ($reader, $schemaFile): XMLReader { 20 | Assert::fileExists($schemaFile); 21 | 22 | disallow_libxml_false_returns( 23 | @$reader->setSchema($schemaFile), 24 | 'Unable to apply XSD schema to the XML Reader.' 25 | ); 26 | 27 | return $reader; 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/prefixed_attributes.php: -------------------------------------------------------------------------------- 1 | $attributes 14 | * @return Closure(XMLWriter): Generator 15 | */ 16 | function prefixed_attributes(array $attributes): Closure 17 | { 18 | return 19 | /** 20 | * @return Generator 21 | */ 22 | static function (XMLWriter $writer) use ($attributes): Generator { 23 | foreach ($attributes as $key => $value) { 24 | assert_strict_prefixed_name($key); 25 | [$prefix, $name] = explode(':', $key); 26 | 27 | yield from prefixed_attribute($prefix, $name, $value)($writer); 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/namespaced_element.php: -------------------------------------------------------------------------------- 1 | $configurators 16 | * 17 | * @return Closure(Node): Element 18 | */ 19 | function namespaced_element(string $namespace, string $qualifiedName, callable ...$configurators): Closure 20 | { 21 | return static function (Node $node) use ($namespace, $qualifiedName, $configurators): Element { 22 | $document = detect_document($node); 23 | 24 | return assert_element( 25 | configure(...$configurators)($document->createElementNS($namespace, $qualifiedName)) 26 | ); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/Xml/Dom/Configurator/canonicalize.php: -------------------------------------------------------------------------------- 1 | documentElement) { 20 | return $document; 21 | } 22 | 23 | return Document::fromLoader( 24 | xml_string_loader( 25 | non_empty_string()->assert($document->C14N()), 26 | LIBXML_NSCLEAN + LIBXML_NOCDATA 27 | ), 28 | pretty_print(), 29 | normalize(), 30 | )->toUnsafeDocument(); 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/Xml/Dom/Manipulator/Node/remove_namespace.php: -------------------------------------------------------------------------------- 1 | removeAttributeNode($target), 25 | 'Could not remove xmlns attribute from dom element' 26 | ); 27 | 28 | return $target; 29 | } 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/Xml/Xslt/Loader/from_template_document.php: -------------------------------------------------------------------------------- 1 | importStyleSheet($template->toUnsafeDocument()), 23 | 'Unable to load XSLT stylesheet document' 24 | ); 25 | } 26 | ); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/Xml/Dom/Manipulator/Attribute/rename.php: -------------------------------------------------------------------------------- 1 | match(true) { 19 | is_xmlns_attribute($target) => rename_xmlns_attribute($target, $newQName), 20 | default => (static function () use ($target, $newNamespaceURI, $newQName): Attr { 21 | $target->rename($newNamespaceURI, $newQName); 22 | return $target; 23 | })() 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/namespaced_attributes.php: -------------------------------------------------------------------------------- 1 | $attributes 13 | * @return Closure(XMLWriter): Generator 14 | */ 15 | function namespaced_attributes(string $namespace, array $attributes): Closure 16 | { 17 | return 18 | /** 19 | * @return Generator 20 | */ 21 | static function (XMLWriter $writer) use ($namespace, $attributes): Generator { 22 | foreach ($attributes as $key => $value) { 23 | $parts = explode(':', $key, 2); 24 | $name = array_pop($parts); 25 | $prefix = $parts ? $parts[0] : null; 26 | 27 | yield from namespaced_attribute($namespace, $prefix, $name, $value)($writer); 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/Xml/Xslt/Transformer/document_to_string.php: -------------------------------------------------------------------------------- 1 | disallow_issues( 19 | static function () use ($document, $processor): string { 20 | // Result can also be null ... undocumentedly! 21 | return (string) disallow_libxml_false_returns( 22 | $processor->transformToXML($document->toUnsafeDocument()), 23 | 'Unable to apply the XSLT template' 24 | ); 25 | } 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/Xml/Writer/Builder/namespaced_element.php: -------------------------------------------------------------------------------- 1 | )> $configurators 13 | * 14 | * @return Closure(XMLWriter): Generator 15 | */ 16 | function namespaced_element(string $namespace, ?string $prefix, string $name, callable ...$configurators): Closure 17 | { 18 | return 19 | /** 20 | * @return Generator 21 | */ 22 | static function (XMLWriter $writer) use ($namespace, $prefix, $name, $configurators): Generator { 23 | yield $writer->startElementNs($prefix, $name, $namespace); 24 | foreach ($configurators as $configurator) { 25 | yield from $configurator($writer); 26 | } 27 | 28 | yield $writer->endElement(); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Decoder/Builder/group_child_elements.php: -------------------------------------------------------------------------------- 1 | > 12 | * @psalm-internal VeeWee\Xml\Encoding 13 | * @return GroupedElements 14 | */ 15 | function group_child_elements(Element $element): array 16 | { 17 | /** @var GroupedElements $grouped */ 18 | $grouped = []; 19 | foreach (children($element) as $child) { 20 | $key = name($child); 21 | 22 | if (array_key_exists($key, $grouped)) { 23 | $data = $grouped[$key]; 24 | $grouped[$key] = is_array($data) ? [...$data, $child] : [$data, $child]; 25 | continue; 26 | } 27 | 28 | $grouped[$key] = $child; 29 | } 30 | 31 | return $grouped; 32 | } 33 | -------------------------------------------------------------------------------- /src/Xml/Encoding/xml_decode.php: -------------------------------------------------------------------------------- 1 | $configurators 17 | * 18 | * @throws EncodingException 19 | */ 20 | function xml_decode(string $xml, callable ... $configurators): array 21 | { 22 | return wrap_exception( 23 | static function () use ($xml, $configurators): array { 24 | $doc = Document::fromXmlString($xml, ...$configurators); 25 | $root = $doc->locate(document_element()); 26 | 27 | return element($root); 28 | } 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/Xml/Encoding/element_decode.php: -------------------------------------------------------------------------------- 1 | $configurators 17 | * 18 | * @throws EncodingException 19 | */ 20 | function element_decode(Element $element, callable ... $configurators): array 21 | { 22 | return wrap_exception( 23 | static function () use ($element, $configurators): array { 24 | $doc = Document::fromXmlNode($element, ...$configurators); 25 | $root = $doc->locate(document_element()); 26 | 27 | return element($root); 28 | } 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Encoder/Builder/root.php: -------------------------------------------------------------------------------- 1 | 18 | * 19 | * @throws EncodingException 20 | * @throws InvariantViolationException 21 | */ 22 | function root(array $data): Closure 23 | { 24 | if (is_node_list($data)) { 25 | throw EncodingException::invalidRoot('list'); 26 | } 27 | 28 | return nodes( 29 | ...map_with_key( 30 | $data, 31 | static fn (string $key, array|string $value): Closure 32 | => parent_node($key, $value) 33 | ) 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Encoder/Builder/children.php: -------------------------------------------------------------------------------- 1 | is_array($data) 29 | ? element($name, $data) 30 | : xmlns_inheriting_element($name, [value($data)]) 31 | ) 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/Xml/Internal/configure.php: -------------------------------------------------------------------------------- 1 | Iter\reduce( 27 | $stages, 28 | /** 29 | * @param T $input 30 | * @param (callable(T): T) $next 31 | * 32 | * @return T 33 | */ 34 | static fn (mixed $input, callable $next): mixed => $next($input), 35 | $input 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/Xml/Dom/Mapper/xml_string.php: -------------------------------------------------------------------------------- 1 | disallow_issues( 21 | static function () use ($node): string { 22 | $document = detect_document($node); 23 | $node = is_document($node) ? null : $node; 24 | 25 | return disallow_libxml_false_returns( 26 | non_empty_string()->assert($document->saveXML($node)), 27 | 'Unable to output XML as string' 28 | ); 29 | } 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/Xml/Encoding/document_encode.php: -------------------------------------------------------------------------------- 1 | $configurators 16 | * 17 | * @throws EncodingException 18 | */ 19 | function document_encode(array $data, callable ... $configurators): Document 20 | { 21 | return wrap_exception( 22 | static function () use ($data, $configurators): Document { 23 | $doc = Document::configure(...$configurators); 24 | $doc->build( 25 | root( 26 | normalize_data($data) 27 | ) 28 | ); 29 | 30 | return $doc; 31 | } 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/Xml/Dom/Validator/validator_chain.php: -------------------------------------------------------------------------------- 1 | $validators 14 | * @return Closure(XMLDocument): IssueCollection 15 | */ 16 | function validator_chain(callable ... $validators): Closure 17 | { 18 | return static fn (XMLDocument $document): IssueCollection => 19 | reduce( 20 | $validators, 21 | /** 22 | * @param callable(XMLDocument): IssueCollection $validator 23 | */ 24 | static fn (IssueCollection $issues, callable $validator): IssueCollection 25 | => new IssueCollection( 26 | ...$issues->getIterator(), 27 | ...$validator($document)->getIterator() 28 | ), 29 | new IssueCollection() 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/Xml/Xsd/Schema/Schema.php: -------------------------------------------------------------------------------- 1 | namespace; 31 | } 32 | 33 | public function location(): string 34 | { 35 | return $this->location; 36 | } 37 | 38 | public function withLocation(string $location): self 39 | { 40 | $clone = clone $this; 41 | $clone->location = $location; 42 | 43 | return $clone; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Exception/EncodingException.php: -------------------------------------------------------------------------------- 1 | getCode() : 0), 17 | $previous 18 | ); 19 | } 20 | 21 | public static function invalidRoot(string $actualType): self 22 | { 23 | return new self('Invalid parent node provided. Expected type array, got '.$actualType); 24 | } 25 | 26 | public static function wrapException(Exception $exception): self 27 | { 28 | if ($exception instanceof EncodingException) { 29 | return $exception; 30 | } 31 | 32 | return new self($exception->getMessage(), $exception); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Encoder/Builder/parent_node.php: -------------------------------------------------------------------------------- 1 | rename_attribute($target, $newQName, $newNamespaceURI), 23 | is_element($target) => rename_element($target, $newQName, $newNamespaceURI), 24 | default => throw RuntimeException::withMessage('Can not rename dom node with type ' . get_class($target)) 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Decoder/Builder/grouped_children.php: -------------------------------------------------------------------------------- 1 | $child 22 | */ 23 | static fn (array $children, string $name, Element|array $child): array 24 | => merge( 25 | $children, 26 | [ 27 | $name => is_array($child) 28 | ? [...map($child, static fn (Element $child): array|string 29 | => unwrap_element(element($child)))] 30 | : unwrap_element(element($child)) 31 | ] 32 | ), 33 | [], 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Encoder/Builder/normalize_data.php: -------------------------------------------------------------------------------- 1 | xmlSerialize()); 27 | } 28 | 29 | if (is_iterable($data)) { 30 | return map( 31 | $data, 32 | static fn (mixed $value): array|string => normalize_data($value) 33 | ); 34 | } 35 | 36 | if ($data instanceof JsonSerializable) { 37 | return normalize_data($data->jsonSerialize()); 38 | } 39 | 40 | return string()->coerce($data); 41 | } 42 | -------------------------------------------------------------------------------- /src/Xml/Dom/Manipulator/Node/replace_by_external_node.php: -------------------------------------------------------------------------------- 1 | parentNode; 22 | Assert::notNull($parentNode, 'Could not replace a node without parent node. ('.get_class($target).')'); 23 | $copy = import_node_deeply($target, $source); 24 | 25 | disallow_libxml_false_returns( 26 | $parentNode->replaceChild($copy, $target), 27 | 'Could not replace the child node.' 28 | ); 29 | 30 | return $copy; 31 | } 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/Xml/Dom/Xpath/Locator/query.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | function query(string $query, ?Node $node = null): Closure 19 | { 20 | return static function (XPath $xpath) use ($query, $node): NodeList { 21 | $node = $node ?? $xpath->document->documentElement; 22 | 23 | $list = disallow_issues( 24 | static fn (): DOMNodeList => assert_dom_node_list( 25 | disallow_libxml_false_returns( 26 | $xpath->query($query, $node), 27 | 'Failed querying XPath query: '.$query 28 | ) 29 | ), 30 | ); 31 | 32 | return NodeList::fromDOMNodeList($list); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Decoder/Builder/element.php: -------------------------------------------------------------------------------- 1 | 15 | * @throws RuntimeException 16 | */ 17 | function element(Element $element): array 18 | { 19 | $name = name($element); 20 | $children = grouped_children($element); 21 | $attributes = attributes($element); 22 | $namespaces = namespaces($element); 23 | 24 | if (!count($children) && !count($attributes) && !count($namespaces)) { 25 | return [$name => $element->textContent ?? '']; 26 | } 27 | 28 | return [ 29 | $name => filter( 30 | merge( 31 | $namespaces, 32 | $attributes, 33 | $children ?: ['@value' => $element->textContent ?? ''] 34 | ), 35 | static fn (mixed $data): bool => $data !== [] 36 | ) 37 | ]; 38 | } 39 | -------------------------------------------------------------------------------- /src/Xml/Xslt/Configurator/security_preferences.php: -------------------------------------------------------------------------------- 1 | $preferences 29 | * 30 | * @return Closure(XSLTProcessor): XSLTProcessor 31 | */ 32 | function security_preferences(int $preferences): Closure 33 | { 34 | return static function (XSLTProcessor $processor) use ($preferences) : XSLTProcessor { 35 | $processor->setSecurityPrefs($preferences); 36 | 37 | return $processor; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-MAX_INT VeeWee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/sequence.php: -------------------------------------------------------------------------------- 1 | $matcherSequence 19 | * 20 | * @return Closure(NodeSequence): bool 21 | */ 22 | function sequence(callable ... $matcherSequence): Closure 23 | { 24 | return static function (NodeSequence $sequence) use ($matcherSequence) : bool { 25 | if (count($matcherSequence) !== $sequence->count()) { 26 | return false; 27 | } 28 | 29 | foreach ($sequence->replay() as $index => $currentSequence) { 30 | $matcher = $matcherSequence[$index]; 31 | if (!$matcher($currentSequence)) { 32 | return false; 33 | } 34 | } 35 | 36 | return true; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/Xml/Dom/Manipulator/Node/remove.php: -------------------------------------------------------------------------------- 1 | removeAttributeNode($target), 29 | 'Could not remove attribute from dom element' 30 | ); 31 | 32 | return $target; 33 | } 34 | 35 | return $parent->removeChild($target); 36 | } 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/Xml/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | getCode() : 0), 17 | $previous 18 | ); 19 | } 20 | 21 | public static function withMessage(string $message): self 22 | { 23 | return new self($message); 24 | } 25 | 26 | public static function fromIssues(string $message, IssueCollection $errors): self 27 | { 28 | return new self($message . PHP_EOL . $errors->toString()); 29 | } 30 | 31 | public static function combineExceptionWithIssues(Throwable $exception, IssueCollection $errors): self 32 | { 33 | return new self( 34 | $exception->getMessage() . PHP_EOL . $errors->toString(), 35 | $exception 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Xml/Xslt/Configurator/profiler.php: -------------------------------------------------------------------------------- 1 | disallow_issues(static function () use ($processor, $profilingFile) : XSLTProcessor { 22 | // ext-xsl doesn't trigger errors if the file does not exist. We'll do it for you! 23 | Assert::notEmpty($profilingFile); 24 | $dir = dirname($profilingFile); 25 | Assert::directory($dir); 26 | Assert::writable($dir); 27 | 28 | disallow_libxml_false_returns( 29 | $processor->setProfiling($profilingFile), 30 | 'Could not set a profiling file on the XSLTProcessor.' 31 | ); 32 | 33 | return $processor; 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/Xml/Dom/Loader/xml_document_loader.php: -------------------------------------------------------------------------------- 1 | disallow_issues(static function () use ($importedDocument, $options, $override_encoding): XMLDocument { 24 | 25 | if ($importedDocument->documentElement === null) { 26 | return XMLDocument::createEmpty($importedDocument->xmlVersion, $importedDocument->xmlEncoding); 27 | } 28 | 29 | return XMLDocument::createFromString( 30 | non_empty_string()->assert($importedDocument->saveXml()), 31 | $options, 32 | $override_encoding 33 | ); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/Xml/ErrorHandling/disallow_issues.php: -------------------------------------------------------------------------------- 1 | proceed( 24 | /** 25 | * @param T $value 26 | * @return T 27 | */ 28 | static function ($value) use ($issues) { 29 | if (count($issues)) { 30 | throw RuntimeException::fromIssues('XML issues detected: ', $issues); 31 | } 32 | 33 | return $value; 34 | }, 35 | /** 36 | * @throws RuntimeException 37 | * @return never 38 | */ 39 | static function (Throwable $exception) use ($issues) { 40 | throw RuntimeException::combineExceptionWithIssues($exception, $issues); 41 | } 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/Xml/Xsd/Schema/Manipulator/overwrite_with_local_files.php: -------------------------------------------------------------------------------- 1 | $map - Key=namspace, value=location 13 | * 14 | * @return Closure(SchemaCollection): SchemaCollection 15 | */ 16 | function overwrite_with_local_files(array $map): Closure 17 | { 18 | return static fn (SchemaCollection $schemas): SchemaCollection => 19 | new SchemaCollection( 20 | ...$schemas->map( 21 | static function (Schema $schema) use ($map): Schema { 22 | /** @psalm-suppress RiskyTruthyFalsyComparison */ 23 | if (!$namespace = $schema->namespace()) { 24 | return $schema; 25 | } 26 | 27 | if (!array_key_exists($namespace, $map)) { 28 | return $schema; 29 | } 30 | 31 | return Schema::withNamespace($namespace, $map[$namespace]); 32 | } 33 | ) 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Xsd/locate_no_namespaced_xsd_schemas.php: -------------------------------------------------------------------------------- 1 | value(); 22 | $documentElement = document_element()($document); 23 | $attributes = $documentElement->attributes; 24 | if (!$schemaLocNoNamespace = $attributes->getNamedItemNS($schemaNs, 'noNamespaceSchemaLocation')) { 25 | return new SchemaCollection(); 26 | } 27 | 28 | $parts = split(trim($schemaLocNoNamespace->textContent ?? ''), '/\s+/'); 29 | 30 | return new SchemaCollection( 31 | ...map( 32 | $parts, 33 | static fn (string $location) => Schema::withoutNamespace($location) 34 | ) 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/Xml/Dom/Manipulator/append.php: -------------------------------------------------------------------------------- 1 | disallow_issues( 22 | static function () use ($target, $nodes) { 23 | foreach ($nodes as $node) { 24 | // Attributes cannot be appended with appendChild. 25 | // Setting the attribute node to the element is the correct way to append an attribute. 26 | if (is_attribute($node) && is_element($target)) { 27 | $target->setAttributeNode($node); 28 | continue; 29 | } 30 | 31 | $target->appendChild($node); 32 | } 33 | 34 | return $target; 35 | } 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/Xml/Dom/Configurator/validator.php: -------------------------------------------------------------------------------- 1 | filter(static fn (Issue $issue): bool => $issue->level()->value() >= $minimumLevel->value()); 30 | 31 | if ($issues->count()) { 32 | throw RuntimeException::fromIssues('Invalid XML', $issues); 33 | } 34 | 35 | return $document; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/Xml/Dom/Locator/Xsd/locate_namespaced_xsd_schemas.php: -------------------------------------------------------------------------------- 1 | value(); 21 | $documentElement = document_element()($document); 22 | $attributes = $documentElement->attributes; 23 | $collection = new SchemaCollection(); 24 | 25 | if (!$schemaLocation = $attributes->getNamedItemNS($schemaNs, 'schemaLocation')) { 26 | return $collection; 27 | } 28 | 29 | $parts = split(trim($schemaLocation->textContent ?? ''), '/\s+/'); 30 | $partsCount = count($parts); 31 | for ($k = 0; $k < $partsCount; $k += 2) { 32 | $collection = $collection->add(Schema::withNamespace($parts[$k], $parts[$k + 1])); 33 | } 34 | 35 | return $collection; 36 | } 37 | -------------------------------------------------------------------------------- /src/Xml/Dom/Validator/internal_xsd_validator.php: -------------------------------------------------------------------------------- 1 | $schemaManipulators 17 | * @return Closure(XMLDocument): IssueCollection 18 | */ 19 | function internal_xsd_validator(callable ... $schemaManipulators): Closure 20 | { 21 | return static function (XMLDocument $document) use ($schemaManipulators) : IssueCollection { 22 | $schemas = configure(...$schemaManipulators)(locate_all_xsd_schemas($document)); 23 | 24 | return validator_chain( 25 | ...$schemas->map( 26 | /** 27 | * @return Closure(XMLDocument): IssueCollection 28 | */ 29 | static fn (Schema $schema): Closure => xsd_validator($schema->location()) 30 | ) 31 | )($document); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/Xml/Dom/Xpath/Locator/evaluate.php: -------------------------------------------------------------------------------- 1 | $type 17 | * 18 | * @return Closure(XPath): T 19 | */ 20 | function evaluate(string $query, TypeInterface $type, ?Node $node = null): Closure 21 | { 22 | return 23 | /** 24 | * @return T 25 | */ 26 | static function (XPath $xpath) use ($query, $node, $type) { 27 | $node = $node ?? $xpath->document->documentElement; 28 | 29 | return disallow_issues( 30 | /** 31 | * @return T 32 | */ 33 | static fn () => $type->coerce( 34 | disallow_libxml_false_returns( 35 | $xpath->evaluate($query, $node), 36 | 'Failed evaluating XPath query: '.$query 37 | ) 38 | ), 39 | ); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/Xml/Xslt/Configurator/parameters.php: -------------------------------------------------------------------------------- 1 | $parameters 19 | * 20 | * @return Closure(XSLTProcessor): XSLTProcessor 21 | */ 22 | function parameters(array $parameters): Closure 23 | { 24 | return static fn (XSLTProcessor $processor) 25 | => disallow_issues(static function () use ($processor, $parameters) : XSLTProcessor { 26 | disallow_libxml_false_returns( 27 | $processor->setParameter('', $parameters), 28 | 'Could not set the provided XSLTProcessor parameters.' 29 | ); 30 | 31 | return $processor; 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/Xml/Dom/Builder/nodes.php: -------------------------------------------------------------------------------- 1 | |Node)> $builders 15 | * 16 | * @return Closure(Node): list 17 | */ 18 | function nodes(callable ... $builders): Closure 19 | { 20 | return 21 | /** 22 | * @return list 23 | */ 24 | static fn (Node $node): array 25 | => reduce( 26 | $builders, 27 | /** 28 | * @param list $builds 29 | * @param callable(Node): (Node|list) $builder 30 | * @return list 31 | */ 32 | static function (array $builds, callable $builder) use ($node): array { 33 | $result = $builder(detect_document($node)); 34 | $newBuilds = is_array($result) ? $result : [$result]; 35 | 36 | return [...$builds, ...$newBuilds]; 37 | }, 38 | [] 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/Xml/ErrorHandling/detect_issues.php: -------------------------------------------------------------------------------- 1 | , IssueCollection} 24 | */ 25 | function detect_issues(callable $run): array 26 | { 27 | $previousErrorReporting = libxml_use_internal_errors(true); 28 | libxml_clear_errors(); 29 | 30 | $result = Result\wrap($run(...)); 31 | 32 | $errors = libxml_get_errors(); 33 | libxml_clear_errors(); 34 | libxml_use_internal_errors($previousErrorReporting); 35 | 36 | return [$result, issue_collection_from_xml_errors($errors)]; 37 | } 38 | -------------------------------------------------------------------------------- /src/Xml/Dom/Traverser/Visitor/SortAttributes.php: -------------------------------------------------------------------------------- 1 | sort(static fn (Attr $a, Attr $b): int => $a->nodeName <=> $b->nodeName) 26 | ->forEach( 27 | static function (Attr $attr) use ($node): void { 28 | disallow_issues(static function () use ($node, $attr) { 29 | remove($attr); 30 | append($attr)($node); 31 | }); 32 | } 33 | ); 34 | 35 | return new Action\Noop(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Xml/Reader/Node/AttributeNode.php: -------------------------------------------------------------------------------- 1 | name, 27 | $reader->localName, 28 | $reader->namespaceURI, 29 | $reader->prefix, 30 | $reader->value 31 | ); 32 | } 33 | 34 | public function name(): string 35 | { 36 | return $this->name; 37 | } 38 | 39 | public function localName(): string 40 | { 41 | return $this->localName; 42 | } 43 | 44 | public function namespace(): string 45 | { 46 | return $this->namespace; 47 | } 48 | 49 | public function namespaceAlias(): string 50 | { 51 | return $this->namespaceAlias; 52 | } 53 | 54 | public function value(): string 55 | { 56 | return $this->value; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Xml/Dom/Manipulator/Document/optimize_namespaces.php: -------------------------------------------------------------------------------- 1 | $info->namespaceURI ?? '' 28 | ))); 29 | 30 | foreach (sort($namespaceURIs) as $index => $namespaceURI) { 31 | $currentPrefix = $prefix . ((string) ($index+1)); 32 | xmlns_attribute($currentPrefix, $namespaceURI)($documentElement); 33 | rename_element_namespace($documentElement, $namespaceURI, $prefix . ((string) ($index+1))); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Xml/Dom/Traverser/Traverser.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private array $visitors; 17 | 18 | /** 19 | * @no-named-arguments 20 | */ 21 | public function __construct(Visitor ... $visitors) 22 | { 23 | $this->visitors = $visitors; 24 | } 25 | 26 | public function traverse(Node $node): Node 27 | { 28 | $this->enterNode($node); 29 | 30 | foreach (attributes_list($node) as $attribute) { 31 | $this->traverse($attribute); 32 | } 33 | 34 | foreach (children($node) as $child) { 35 | $this->traverse($child); 36 | } 37 | 38 | $this->leaveNode($node); 39 | 40 | return $node; 41 | } 42 | 43 | private function enterNode(Node $node): void 44 | { 45 | foreach ($this->visitors as $visitor) { 46 | $visitor->onNodeEnter($node)($node); 47 | } 48 | } 49 | 50 | private function leaveNode(Node $node): void 51 | { 52 | foreach ($this->visitors as $visitor) { 53 | $visitor->onNodeLeave($node)($node); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Xml/Xmlns/Xmlns.php: -------------------------------------------------------------------------------- 1 | xmlns = $xmlns; 17 | } 18 | 19 | /** 20 | * @psalm-pure 21 | */ 22 | public static function xmlns(): self 23 | { 24 | return new self('http://www.w3.org/2000/xmlns/'); 25 | } 26 | 27 | /** 28 | * @psalm-pure 29 | */ 30 | public static function xml(): self 31 | { 32 | return new self('http://www.w3.org/XML/1998/namespace'); 33 | } 34 | 35 | /** 36 | * @psalm-pure 37 | */ 38 | public static function xsi(): self 39 | { 40 | return new self('http://www.w3.org/2001/XMLSchema-instance'); 41 | } 42 | 43 | /** 44 | * @psalm-pure 45 | */ 46 | public static function phpXpath(): self 47 | { 48 | return new self('http://php.net/xpath'); 49 | } 50 | 51 | /** 52 | * @psalm-pure 53 | */ 54 | public static function load(string $namespace): self 55 | { 56 | return new self($namespace); 57 | } 58 | 59 | public function value(): string 60 | { 61 | return $this->xmlns; 62 | } 63 | 64 | public function matches(Xmlns $other): bool 65 | { 66 | return $this->value() === $other->value(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Xml/Dom/Manipulator/Element/rename.php: -------------------------------------------------------------------------------- 1 | prefix !== null && $newPrefix !== $target->prefix && $target->hasAttribute('xmlns:'.$target->prefix)) { 29 | $target->removeAttribute('xmlns:'.$target->prefix); 30 | } 31 | 32 | disallow_issues(static fn () => $target->rename($newNamespaceURI, $newQName)); 33 | 34 | return $target; 35 | } 36 | -------------------------------------------------------------------------------- /src/Xml/ErrorHandling/Issue/Issue.php: -------------------------------------------------------------------------------- 1 | level; 27 | } 28 | 29 | public function code(): int 30 | { 31 | return $this->code; 32 | } 33 | 34 | public function column(): int 35 | { 36 | return $this->column; 37 | } 38 | 39 | public function message(): string 40 | { 41 | return $this->message; 42 | } 43 | 44 | public function file(): string 45 | { 46 | return $this->file; 47 | } 48 | 49 | public function line(): int 50 | { 51 | return $this->line; 52 | } 53 | 54 | /** 55 | * @psalm-suppress MissingThrowsDocblock 56 | */ 57 | public function toString(): string 58 | { 59 | return Str\format( 60 | '[%s] %s: %s (%s) on line %s,%s', 61 | Str\uppercase($this->level->toString()), 62 | $this->file, 63 | $this->message, 64 | $this->code, 65 | $this->line, 66 | $this->column 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Xml/Reader/Configurator/parser_options.php: -------------------------------------------------------------------------------- 1 | , 32 | * bool> $options 33 | * 34 | * @return Closure(XMLReader): XMLReader 35 | */ 36 | function parser_options(array $options): Closure 37 | { 38 | return static fn (XMLReader $reader): XMLReader => disallow_issues( 39 | static function () use ($reader, $options): XMLReader { 40 | foreach ($options as $property => $value) { 41 | disallow_libxml_false_returns( 42 | $reader->setParserProperty($property, $value), 43 | 'Unable to set the parser property "'.((string) $property).'" to the XML Reader.' 44 | ); 45 | } 46 | 47 | return $reader; 48 | } 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/Xml/Dom/Xpath/Locator/query_single.php: -------------------------------------------------------------------------------- 1 | document->documentElement; 30 | $list = disallow_issues( 31 | static fn (): NodeList => assert_dom_node_list( 32 | disallow_libxml_false_returns( 33 | $xpath->query($query, $node), 34 | 'Failed querying XPath query: '.$query 35 | ) 36 | ), 37 | ); 38 | 39 | Assert::count( 40 | $list, 41 | 1, 42 | format('Expected to find only one node that matches %s. Got %s', $query, count($list)) 43 | ); 44 | 45 | return $list->item(0); 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/Xml/Xsd/Schema/Manipulator/base_path.php: -------------------------------------------------------------------------------- 1 | '' !== $path && ( 27 | strspn($path, '/\\', 0, 1) 28 | || ( 29 | strlen($path) > 3 && ctype_alpha($path[0]) 30 | && ':' === $path[1] 31 | && strspn($path, '/\\', 2, 1) 32 | ) 33 | || null !== parse_url($path, PHP_URL_SCHEME) 34 | ); 35 | 36 | return static fn (SchemaCollection $schemas): SchemaCollection => 37 | new SchemaCollection( 38 | ...$schemas->map( 39 | static fn (Schema $schema): Schema 40 | => $isAbsolutePath($schema->location()) ? $schema : $schema->withLocation( 41 | rtrim($basePath, '/\\') . DIRECTORY_SEPARATOR . $schema->location() 42 | ) 43 | ) 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/Xml/Xslt/Processor.php: -------------------------------------------------------------------------------- 1 | processor = $processor; 21 | } 22 | 23 | /** 24 | * @param list $configurators 25 | */ 26 | public static function configure(callable ... $configurators): self 27 | { 28 | return new self( 29 | configure(...$configurators)(new XSLTProcessor()) 30 | ); 31 | } 32 | 33 | /** 34 | * @param list $configurators 35 | */ 36 | public static function fromTemplateDocument(Document $template, callable ... $configurators): self 37 | { 38 | return self::configure( 39 | loader(from_template_document($template)), 40 | ...$configurators 41 | ); 42 | } 43 | 44 | /** 45 | * @template T 46 | * @param callable(XSLTProcessor): T $transformer 47 | * @return T 48 | */ 49 | public function transform(callable $transformer): mixed 50 | { 51 | return $transformer($this->processor); 52 | } 53 | 54 | public function transformDocumentToString(Document $document): string 55 | { 56 | return $this->transform(document_to_string($document)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Xml/Reader/Node/ElementNode.php: -------------------------------------------------------------------------------- 1 | $attributes 16 | */ 17 | public function __construct( 18 | public int $position, 19 | public string $name, 20 | public string $localName, 21 | public string $namespace, 22 | public string $namespaceAlias, 23 | public array $attributes 24 | ) { 25 | } 26 | 27 | /** 28 | * @param callable(): list $attributesProvider 29 | */ 30 | public static function fromReader(XMLReader $reader, int $position, callable $attributesProvider): self 31 | { 32 | return new self( 33 | $position, 34 | $reader->name, 35 | $reader->localName, 36 | $reader->namespaceURI, 37 | $reader->prefix, 38 | $attributesProvider() 39 | ); 40 | } 41 | 42 | public function position(): int 43 | { 44 | return $this->position; 45 | } 46 | 47 | public function name(): string 48 | { 49 | return $this->name; 50 | } 51 | 52 | public function localName(): string 53 | { 54 | return $this->localName; 55 | } 56 | 57 | public function namespace(): string 58 | { 59 | return $this->namespace; 60 | } 61 | 62 | public function namespaceAlias(): string 63 | { 64 | return $this->namespaceAlias; 65 | } 66 | 67 | /** 68 | * @return list 69 | */ 70 | public function attributes(): array 71 | { 72 | return $this->attributes; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Xml/ErrorHandling/Issue/IssueCollection.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class IssueCollection implements Countable, IteratorAggregate 19 | { 20 | /** 21 | * @var list 22 | */ 23 | private array $issues; 24 | 25 | /** 26 | * @no-named-arguments 27 | */ 28 | public function __construct(Issue ...$errors) 29 | { 30 | $this->issues = $errors; 31 | } 32 | 33 | /** 34 | * @psalm-suppress LessSpecificImplementedReturnType 35 | * @return Iterator, Issue> 36 | */ 37 | public function getIterator(): Iterator 38 | { 39 | return Iterator::create($this->issues); 40 | } 41 | 42 | public function count(): int 43 | { 44 | return count($this->issues); 45 | } 46 | 47 | /** 48 | * @param (callable(Issue): bool) $filter 49 | */ 50 | public function filter(callable $filter): self 51 | { 52 | return new self(...Dict\filter($this->issues, $filter(...))); 53 | } 54 | 55 | public function getHighestLevel(): ?Level 56 | { 57 | $issue = Math\max_by($this->issues, static fn (Issue $issue): int => $issue->level()->value()); 58 | 59 | return $issue ? $issue->level() : null; 60 | } 61 | 62 | public function toString(): string 63 | { 64 | $values = Vec\values(Dict\map($this->issues, static fn (Issue $error): string => $error->toString())); 65 | 66 | return Str\join($values, PHP_EOL); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Xml/Reader/MatchingNode.php: -------------------------------------------------------------------------------- 1 | xml; 30 | } 31 | 32 | public function nodeSequence(): NodeSequence 33 | { 34 | return $this->nodeSequence; 35 | } 36 | 37 | /** 38 | * @param list $configurators 39 | * 40 | * @throws RuntimeException 41 | */ 42 | public function intoDocument(callable ... $configurators): Document 43 | { 44 | return Document::fromXmlString($this->xml, ...$configurators); 45 | } 46 | 47 | /** 48 | * @param list $configurators 49 | * 50 | * @throws RuntimeException 51 | * @throws EncodingException 52 | */ 53 | public function decode(callable ... $configurators): array 54 | { 55 | return xml_decode($this->xml, ...$configurators); 56 | } 57 | 58 | /** 59 | * @param callable(NodeSequence): bool $matcher 60 | */ 61 | public function matches(callable $matcher): bool 62 | { 63 | return $matcher($this->nodeSequence); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/Xml/Reader/Node/Pointer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private array $siblingsPerDepth; 16 | 17 | private int $depth; 18 | 19 | private NodeSequence $nodeSequence; 20 | 21 | private function __construct() 22 | { 23 | $this->nodeSequence = new NodeSequence(); 24 | $this->depth = 0; 25 | $this->siblingsPerDepth = []; 26 | } 27 | 28 | public static function create(): self 29 | { 30 | return new self(); 31 | } 32 | 33 | public function getDepth(): int 34 | { 35 | return $this->depth; 36 | } 37 | 38 | public function getNextSiblingPosition(): int 39 | { 40 | return ($this->siblingsPerDepth[$this->depth] ?? 0) + 1; 41 | } 42 | 43 | public function getNodeSequence(): NodeSequence 44 | { 45 | return $this->nodeSequence; 46 | } 47 | 48 | public function enterElement(ElementNode $element): void 49 | { 50 | $depth = $this->depth; 51 | $this->siblingsPerDepth[$depth] = isset($this->siblingsPerDepth[$depth]) ? ($this->siblingsPerDepth[$depth]+1) : 1; 52 | $this->depth++; 53 | $this->nodeSequence = $this->nodeSequence->append($element); 54 | } 55 | 56 | /** 57 | * @throws InvalidArgumentException 58 | */ 59 | public function leaveElement(): void 60 | { 61 | Assert::greaterThan($this->depth, 0, 'Currently at root level. Can not leave element!'); 62 | 63 | unset($this->siblingsPerDepth[$this->depth]); 64 | $this->depth--; 65 | $this->nodeSequence = $this->nodeSequence->pop(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Xml/ErrorHandling/stop_on_first_issue.php: -------------------------------------------------------------------------------- 1 | 24 | * 25 | * @psalm-suppress InvalidReturnType - Psalm gets lost here and thinks it returns "never" 26 | * 27 | * @throws RuntimeException 28 | */ 29 | function stop_on_first_issue(callable $tick, callable $run): Generator 30 | { 31 | $previousErrorReporting = libxml_use_internal_errors(true); 32 | libxml_clear_errors(); 33 | 34 | $detectIssues = static function (): void { 35 | $issues = issue_collection_from_xml_errors(Vec\values(libxml_get_errors())); 36 | if ($issues->count()) { 37 | throw RuntimeException::fromIssues('Detected issues during the parsing of the XML Stream', $issues); 38 | } 39 | }; 40 | 41 | try { 42 | while ($tick()) { 43 | $result = $run(); 44 | if ($result !== null) { 45 | yield $result; 46 | } 47 | 48 | $detectIssues(); 49 | } 50 | 51 | $detectIssues(); 52 | } finally { 53 | libxml_clear_errors(); 54 | libxml_use_internal_errors($previousErrorReporting); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Xml/ErrorHandling/Issue/Level.php: -------------------------------------------------------------------------------- 1 | value; 48 | } 49 | 50 | /** 51 | * @return 'warning'|'fatal'|'error' 52 | */ 53 | public function toString(): string 54 | { 55 | if ($this->isWarning()) { 56 | return 'warning'; 57 | } 58 | 59 | if ($this->isError()) { 60 | return 'error'; 61 | } 62 | 63 | return 'fatal'; 64 | } 65 | 66 | public function matches(Level $level): bool 67 | { 68 | return $level->value() === $this->value; 69 | } 70 | 71 | public function isError(): bool 72 | { 73 | return $this->matches(self::error()); 74 | } 75 | 76 | public function isFatal(): bool 77 | { 78 | return $this->matches(self::fatal()); 79 | } 80 | 81 | public function isWarning(): bool 82 | { 83 | return $this->matches(self::warning()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Xml/Dom/Manipulator/Node/replace_by_external_nodes.php: -------------------------------------------------------------------------------- 1 | $sources 18 | * @return array 19 | */ 20 | function replace_by_external_nodes(Node $target, iterable $sources): array 21 | { 22 | return disallow_issues( 23 | /** 24 | * @return array 25 | */ 26 | static function () use ($target, $sources) : array { 27 | $parentNode = $target->parentNode; 28 | Assert::notNull($parentNode, 'Could not replace a node without parent node. ('.get_class($target).')'); 29 | $copies = map( 30 | $sources, 31 | static fn (Node $source): Node => import_node_deeply($target, $source) 32 | ); 33 | 34 | // Documents can only contain one element, so in case of documentElement, we remove it first to avoid errors. 35 | if (is_document_element($target)) { 36 | $parentNode->removeChild($target); 37 | $target = null; 38 | } 39 | 40 | foreach ($copies as $copy) { 41 | $parentNode->insertBefore($copy, $target); 42 | } 43 | 44 | // In case of all other elements : the target only gets removed after replacement. 45 | if ($target) { 46 | $parentNode->removeChild($target); 47 | } 48 | 49 | return $copies; 50 | } 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/Xml/Xsd/Schema/SchemaCollection.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class SchemaCollection implements Countable, IteratorAggregate 19 | { 20 | /** 21 | * @var list 22 | */ 23 | private array $schemas; 24 | 25 | /** 26 | * @no-named-arguments 27 | */ 28 | public function __construct(Schema ... $schemas) 29 | { 30 | $this->schemas = $schemas; 31 | } 32 | 33 | public function getIterator(): Traversable 34 | { 35 | yield from $this->schemas; 36 | } 37 | 38 | public function count(): int 39 | { 40 | return count($this->schemas); 41 | } 42 | 43 | public function add(Schema $schema): self 44 | { 45 | return new self(...[...$this->schemas, $schema]); 46 | } 47 | 48 | /** 49 | * @param callable(SchemaCollection): SchemaCollection $manipulator 50 | */ 51 | public function manipulate(callable $manipulator): self 52 | { 53 | /** @psalm-suppress ImpureFunctionCall */ 54 | return $manipulator($this); 55 | } 56 | 57 | /** 58 | * @param callable(Schema): bool $filter 59 | */ 60 | public function filter(callable $filter): self 61 | { 62 | /** @psalm-suppress ImpureFunctionCall */ 63 | return new self(...filter($this->schemas, $filter(...))); 64 | } 65 | 66 | /** 67 | * @template T 68 | * @param callable(Schema): T $mapper 69 | * 70 | * @return list 71 | */ 72 | public function map(callable $mapper) 73 | { 74 | /** @psalm-suppress ImpureFunctionCall */ 75 | return map($this->schemas, $mapper(...)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Xml/Dom/Manipulator/Xmlns/rename_element_namespace.php: -------------------------------------------------------------------------------- 1 | forEach( 23 | static fn (Element $child) => rename_element_namespace($child, $namespaceURI, $newPrefix) 24 | ); 25 | 26 | attributes_list($element)->forEach(static function (Attr $attr) use ($namespaceURI, $newPrefix, $element) { 27 | if ($attr->namespaceURI === $namespaceURI) { 28 | $attr->rename($namespaceURI, $newPrefix . ':' . $attr->localName); 29 | } 30 | 31 | if (is_xmlns_attribute($attr) && $attr->value === $namespaceURI) { 32 | try { 33 | $attr->rename($attr->namespaceURI, 'xmlns:' . $newPrefix); 34 | 35 | } catch (DOMException $e) { 36 | if ($e->getCode() === INVALID_MODIFICATION_ERR) { 37 | // Remove the attribute that would become a duplicate 38 | $element->removeAttributeNode($attr); 39 | } else { 40 | // @codeCoverageIgnoreStart 41 | throw $e; 42 | // @codeCoverageIgnoreEnd 43 | } 44 | } 45 | $attr->rename($attr->namespaceURI, 'xmlns:' . $newPrefix); 46 | } 47 | }); 48 | 49 | if ($element->namespaceURI === $namespaceURI) { 50 | $element->rename($namespaceURI, $newPrefix . ':' . $element->localName); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setFinder( 5 | \Symfony\Component\Finder\Finder::create() 6 | ->in([ 7 | __DIR__ . '/src', 8 | __DIR__ . '/tests', 9 | __DIR__ . '/build', 10 | __DIR__ . '/tools', 11 | ]) 12 | ->name('*.php') 13 | ) 14 | ->setRiskyAllowed(true) 15 | ->setRules([ 16 | '@PSR2' => true, 17 | 'align_multiline_comment' => true, 18 | 'array_indentation' => true, 19 | 'declare_strict_types' => true, 20 | 'final_class' => true, 21 | 'global_namespace_import' => [ 22 | 'import_classes' => true, 23 | 'import_constants' => true, 24 | 'import_functions' => true, 25 | ], 26 | 'list_syntax' => [ 27 | 'syntax' => 'short', 28 | ], 29 | 'constant_case' => [ 30 | 'case' => 'lower', 31 | ], 32 | 'multiline_comment_opening_closing' => true, 33 | 'native_function_casing' => true, 34 | 'no_empty_phpdoc' => true, 35 | 'no_leading_import_slash' => true, 36 | 'no_superfluous_phpdoc_tags' => [ 37 | 'allow_mixed' => true, 38 | ], 39 | 'no_unused_imports' => true, 40 | 'no_useless_else' => true, 41 | 'no_useless_return' => true, 42 | 'ordered_imports' => [ 43 | 'imports_order' => ['class', 'function', 'const'], 44 | ], 45 | 'ordered_interfaces' => true, 46 | 'php_unit_test_annotation' => true, 47 | 'php_unit_test_case_static_method_calls' => [ 48 | 'call_type' => 'static', 49 | ], 50 | 'php_unit_method_casing' => [ 51 | 'case' => 'snake_case', 52 | ], 53 | 'single_import_per_statement' => true, 54 | 'single_trait_insert_per_statement' => true, 55 | 'static_lambda' => true, 56 | 'strict_comparison' => true, 57 | 'strict_param' => true, 58 | 'nullable_type_declaration_for_default_null_value' => true, 59 | ]) 60 | ; 61 | -------------------------------------------------------------------------------- /src/Xml/Reader/Matcher/nested.php: -------------------------------------------------------------------------------- 1 | $matchers 26 | * 27 | * @return Closure(NodeSequence): bool 28 | */ 29 | function nested(callable ... $matchers): Closure 30 | { 31 | return static function (NodeSequence $sequence) use ($matchers) : bool { 32 | $lastMatchedAtIndex = -1; 33 | $currentMatcher = array_shift($matchers); 34 | if ($currentMatcher === null) { 35 | return false; 36 | } 37 | 38 | $stepCount = $sequence->count(); 39 | foreach ($sequence->replay() as $index => $step) { 40 | // Slice the step NodeSequence based on previous "match" breakpoint 41 | // and see if it matches on current matcher: 42 | $step = $step->slice($lastMatchedAtIndex + 1); 43 | if (!$currentMatcher($step)) { 44 | continue; 45 | } 46 | 47 | // If there was a match, select the next matcher and store the last matched NodeSequence index. 48 | $currentMatcher = array_shift($matchers); 49 | $lastMatchedAtIndex = $index; 50 | 51 | // If the list of matchers is empty 52 | // The function will return true if the element is the last step in the complete sequence. 53 | // Otherwise, the nested match has an even deeper element on which we don't wish to match. 54 | if ($currentMatcher === null) { 55 | $isLastStep = $index === $stepCount - 1; 56 | 57 | return $isLastStep; 58 | } 59 | } 60 | 61 | return false; 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # <XML /> 2 | 3 | *XML without worries* 4 | 5 | This package aims to provide all tools for dealing with XML in PHP without worries. 6 | You will find a type-safe, declarative API that deals with errors for you! 7 | 8 | 9 | ## Installation 10 | 11 | ``` 12 | composer require veewee/xml 13 | ``` 14 | 15 | ## Components 16 | 17 | * [DOM](docs/dom.md): Operate on XML documents through the DOM API. 18 | * [Encoding](docs/encoding.md): Provides `xml_encode()` and `xml_decode()` so that you can deal with XML just like you deal with JSON! 19 | * [ErrorHandling](docs/error-handling.md): Provides the tools you need to safely deal with XML. 20 | * [Reader](docs/reader.md): Memory-safe XML reader. 21 | * [Writer](docs/writer.md): Memory-safe XML writer. 22 | * [XSD](docs/xsd.md): Tools for working with XSD schemas. 23 | * [XSLT](docs/xslt.md): Transform XML documents into something else. 24 | 25 | ## Spec compliance 26 | 27 | Starting from v4, this package opt's-in to the [spec compliance mode](https://wiki.php.net/rfc/opt_in_dom_spec_compliance) that has been released in PHP 8.4. 28 | Since these changes cannot be ported to older PHP versions, v3 of this package will be maintained actively for a longer period. 29 | This will give you a grace period to upgrade your PHP versions, packages, ... 30 | This little bump in the road is necessary to provide you with a better, more stable and spec-compliant package in the future. 31 | 32 | Support table: 33 | 34 | | veewee/xml | PHP | LTS | 35 | |------------|--------------------|-----| 36 | | 3.0 - 3.1 | 8.1, 8.2, 8.3 | NO | 37 | | 3.2 | 8.2, 8.3 | NO | 38 | | 3.3+ | 8.2, 8.3, 8.4, 8.5 | YES | 39 | | 4.0+ | 8.4+ | YES | 40 | 41 | 42 | ## Roadmap 43 | 44 | These components are not implemented yet, but have been thought about. 45 | Stay tuned if you want to use these! 46 | 47 | * External: [Saxon/C](https://www.saxonica.com/saxon-c/php_api.xml): XSLT 3.0/2.0, XQuery 3.1, XPath 3.1 and Schema Validation 1.0/1.1 48 | * Awaiting PHP8 support: https://saxonica.plan.io/issues/4842 49 | * ~~External: [XSLT2](https://github.com/genkgo/xsl)~~ (prefer saxon/c) 50 | 51 | ## About 52 | 53 | ### Submitting bugs and feature requests 54 | 55 | Bugs and feature request are tracked on [GitHub](https://github.com/veewee/xml/issues). 56 | Please take a look at our rules before [contributing your code](CONTRIBUTING.md). 57 | 58 | ### License 59 | 60 | veewee/xml is licensed under the [MIT License](LICENSE). 61 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Encoder/Builder/element.php: -------------------------------------------------------------------------------- 1 | assert($data['@attributes'] ?? null); 38 | $namespaces = $nullableMap->assert($data['@namespaces'] ?? null); 39 | $cdata = union(string(), null())->assert($data['@cdata'] ?? null); 40 | $value = union(string(), null())->assert($data['@value'] ?? null); 41 | 42 | $element = filter_keys( 43 | $data, 44 | static fn (string $key): bool => !in_array($key, ['@attributes', '@namespaces', '@value', '@cdata'], true) 45 | ); 46 | 47 | $namedNamespaces = filter_keys($namespaces ?? []); 48 | 49 | /** @var list $children */ 50 | $children = filter_nulls([ 51 | $attributes !== null ? attributes($attributes) : null, 52 | $namedNamespaces ? xmlns_attributes($namedNamespaces) : null, 53 | $cdata !== null ? childrenBuilder(cdata($cdata)) : null, 54 | $value !== null ? escaped_value($value) : null, 55 | ...values(map_with_key( 56 | $element, 57 | /** 58 | * @param string|array $value 59 | * @return Closure(Element): Element 60 | */ 61 | static fn (string $name, string|array $value): Closure 62 | => parent_node($name, $value) 63 | )), 64 | ]); 65 | 66 | return xmlns_inheriting_element($name, $children, $namespaces); 67 | } 68 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "veewee/xml", 3 | "description": "XML without worries", 4 | "keywords": [ 5 | "dom", "xml", "xsd", "xslt", "dom-manipulation", "reader", "writer", "xpath", 6 | "xml_encode", "xml_decode", "array-to-xml", "xml-to-array" 7 | ], 8 | "type": "library", 9 | "require": { 10 | "php": "~8.4.0 || ~8.5.0", 11 | "ext-dom": "*", 12 | "ext-libxml": "*", 13 | "ext-xml": "*", 14 | "ext-xsl": "*", 15 | "ext-xmlreader": "*", 16 | "ext-xmlwriter": "*", 17 | "azjezz/psl": "^3.0 || ~4.0", 18 | "webmozart/assert": "^1.10" 19 | }, 20 | "require-dev": { 21 | "symfony/finder": "^7.1", 22 | "veewee/composer-run-parallel": "^1.0.0", 23 | "vimeo/psalm": "~6.13", 24 | "php-standard-library/psalm-plugin": "^2.2", 25 | "phpunit/phpunit": "~12.3", 26 | "php-cs-fixer/shim": "~3.88", 27 | "infection/infection": "^0.31" 28 | }, 29 | "license": "MIT", 30 | "authors": [ 31 | { 32 | "name": "Toon Verwerft", 33 | "email": "toonverwerft@gmail.com" 34 | } 35 | ], 36 | "autoload": { 37 | "psr-4": { 38 | "VeeWee\\Xml\\": "src/Xml" 39 | }, 40 | "files": [ 41 | "src/bootstrap.php" 42 | ] 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "VeeWee\\Tests\\": "tests/" 47 | } 48 | }, 49 | "scripts": { 50 | "autoload": [ 51 | "@php build/bootstrap.php" 52 | ], 53 | "cs": "PHP_CS_FIXER_IGNORE_ENV=1 ./vendor/bin/php-cs-fixer fix --dry-run", 54 | "cs:fix": "PHP_CS_FIXER_IGNORE_ENV=1 ./vendor/bin/php-cs-fixer fix", 55 | "psalm": "./vendor/bin/psalm --no-cache --stats", 56 | "tests": "./vendor/bin/phpunit --coverage-text --color", 57 | "stress": [ 58 | "Composer\\Config::disableProcessTimeout", 59 | "@php -c stress.ini ./vendor/bin/phpunit --configuration phpunit.stress.xml --no-coverage" 60 | ], 61 | "testquality": "@parallel coverage infection", 62 | "coverage": "@php ./tools/full-coverage-check.php .phpunit.cache/clover/clover.xml", 63 | "infection": [ 64 | "Composer\\Config::disableProcessTimeout", 65 | "./vendor/bin/infection --show-mutations -v" 66 | ], 67 | "ci": [ 68 | "@autoload", 69 | "@parallel cs psalm tests", 70 | "@parallel coverage infection stress" 71 | ] 72 | }, 73 | "config": { 74 | "allow-plugins": { 75 | "veewee/composer-run-parallel": true, 76 | "infection/extension-installer": true 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Xml/Dom/Xpath.php: -------------------------------------------------------------------------------- 1 | $configurators 28 | */ 29 | public static function fromDocument(Document $document, callable ... $configurators): self 30 | { 31 | return new self( 32 | configure(...$configurators)(new DOMXPath($document->toUnsafeDocument())) 33 | ); 34 | } 35 | 36 | /** 37 | * @param list $configurators 38 | * @throws RuntimeException 39 | * @throws InvalidArgumentException 40 | */ 41 | public static function fromUnsafeNode(Node $node, callable ... $configurators): self 42 | { 43 | return self::fromDocument( 44 | Document::fromUnsafeDocument( 45 | detect_document($node) 46 | ), 47 | ...$configurators 48 | ); 49 | } 50 | 51 | /** 52 | * @template T 53 | * @param callable(DOMXPath): T $locator 54 | * 55 | * @return T 56 | * @throws RuntimeException 57 | */ 58 | public function locate(callable $locator) 59 | { 60 | return $locator($this->xpath); 61 | } 62 | 63 | /** 64 | * @return NodeList 65 | *@throws RuntimeException 66 | */ 67 | public function query(string $expression, ?Node $contextNode = null): NodeList 68 | { 69 | return $this->locate(query($expression, $contextNode)); 70 | } 71 | 72 | /** 73 | * @throws RuntimeException 74 | * @throws InvalidArgumentException 75 | */ 76 | public function querySingle(string $expression, ?Node $contextNode = null): Node 77 | { 78 | return $this->locate(query_single($expression, $contextNode)); 79 | } 80 | 81 | /** 82 | * @template T 83 | * 84 | * @param TypeInterface $type 85 | * 86 | * @return T 87 | * @throws RuntimeException 88 | */ 89 | public function evaluate(string $expression, TypeInterface $type, ?Node $contextNode = null) 90 | { 91 | return $this->locate(evaluate($expression, $type, $contextNode)); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Xml/Encoding/Internal/Encoder/Builder/xmlns_inheriting_element.php: -------------------------------------------------------------------------------- 1 | $children 21 | * @param array $namespaces 22 | * 23 | * @return Closure(Element): Element 24 | */ 25 | function xmlns_inheriting_element(string $name, array $children, ?array $namespaces = []): Closure 26 | { 27 | return static function (XMLDocument|Element $parent) use ($namespaces, $name, $children): Element { 28 | 29 | $defaultNamespace = $namespaces[''] ?? null; 30 | 31 | // These rules apply for non prefixed elements only: 32 | // If no local namespace has been defined: lookup the default local namespace of the closest parent element. 33 | // Use that specific local namespace to create the element if one could be found. 34 | // Otherwise, just create a non-namespaced element. 35 | if (!is_prefixed_node_name($name)) { 36 | // Try to find the inherited default XMLNS for non prefixed elements without a desired local namespace. 37 | if ($defaultNamespace === null && is_element($parent)) { 38 | $defaultNamespace = $parent->lookupNamespaceURI(''); 39 | } 40 | 41 | return $defaultNamespace !== null 42 | ? namespacedElementBuilder($defaultNamespace, $name, ...$children)($parent) 43 | : elementBuilder($name, ...$children)($parent); 44 | } 45 | 46 | // Prefixed elements can be created as regular elements: 47 | // The configured xmlns attributes will be added by the $children. 48 | // If a local namespace is configured, make sure to register it on the node manually. 49 | [$prefix] = explode(':', $name); 50 | $prefixedNamespace = $namespaces[$prefix] ?? (is_element($parent) ? $parent->lookupNamespaceURI($prefix) : null); 51 | 52 | Assert::notNull($prefixedNamespace, 'No namespace URI could be found for prefix: '.$prefix); 53 | 54 | $defaultXmlns = $defaultNamespace !== null ? [default_xmlns_attribute($defaultNamespace)] : []; 55 | return namespacedElementBuilder( 56 | $prefixedNamespace, 57 | $name, 58 | ...$defaultXmlns, 59 | ...$children, 60 | )($parent); 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/Xml/Reader/Node/NodeSequence.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private array $elementNodes; 20 | 21 | /** 22 | * @no-named-arguments 23 | */ 24 | public function __construct(ElementNode ... $elementNodes) 25 | { 26 | $this->elementNodes = $elementNodes; 27 | } 28 | 29 | /** 30 | * @throws InvalidArgumentException 31 | */ 32 | public function pop(): self 33 | { 34 | $this->calculateNonEmptyElementsCount(); 35 | $popped = $this->elementNodes; 36 | array_pop($popped); 37 | return new self(...$popped); 38 | } 39 | 40 | public function append(ElementNode $element): self 41 | { 42 | return new self(...[...$this->elementNodes, $element]); 43 | } 44 | 45 | /** 46 | * @throws InvalidArgumentException 47 | */ 48 | public function current(): ElementNode 49 | { 50 | return $this->elementNodes[$this->calculateNonEmptyElementsCount() -1]; 51 | } 52 | 53 | public function parent(): ?ElementNode 54 | { 55 | $count = count($this->elementNodes); 56 | if ($count <= 1) { 57 | return null; 58 | } 59 | 60 | return $this->elementNodes[$count - 2]; 61 | } 62 | 63 | /** 64 | * @return list 65 | */ 66 | public function sequence(): array 67 | { 68 | return $this->elementNodes; 69 | } 70 | 71 | public function count(): int 72 | { 73 | return count($this->elementNodes); 74 | } 75 | 76 | /** 77 | * @param non-negative-int $start 78 | * @param non-negative-int|null $length 79 | */ 80 | public function slice(int $start, ?int $length = null): self 81 | { 82 | return new self(...slice($this->elementNodes, $start, $length)); 83 | } 84 | 85 | /** 86 | * Replays every step in the sequence 87 | * 88 | * @return Generator 89 | */ 90 | public function replay(): Generator 91 | { 92 | $step = new self(); 93 | foreach ($this->elementNodes as $index => $node) { 94 | $step = $step->append($node); 95 | yield $index => $step; 96 | } 97 | } 98 | 99 | /** 100 | * @throws InvalidArgumentException 101 | */ 102 | private function calculateNonEmptyElementsCount(): int 103 | { 104 | $count = count($this->elementNodes); 105 | Assert::true($count > 0, 'The node sequence is empty. Can not fetch current item!'); 106 | 107 | return $count; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /docs/xsd.md: -------------------------------------------------------------------------------- 1 | # XSD Components 2 | 3 | Tools for working with XSD schemas. 4 | 5 | ## Example 6 | 7 | ```php 8 | use VeeWee\XML\DOM\Document; 9 | use VeeWee\Xml\Xsd\Schema\Manipulator; 10 | use function VeeWee\Xml\Dom\Locator\Xsd\locate_all_xsd_schemas; 11 | 12 | $doc = Document::fromXmlFile('some.xml'); 13 | $schemas = $doc->map(locate_all_xsd_schemas(...)) 14 | ->manipulate(Manipulator\base_path('/var/www')) 15 | ->manipulate(Manipulator\overwrite_with_local_files([ 16 | 'http://www.w3.org/2001/XMLSchema' => '/local/XMLSchema.xsd' 17 | ])); 18 | ``` 19 | 20 | It consists out of following components: 21 | 22 | * [Schemas](#schemas): Can be used to create a list of one or multiple XSD schemas. 23 | 24 | 25 | ## Schemas 26 | 27 | Can be used to keep track of the detected schema's inside a XML document. 28 | 29 | This is what a schema looks like: 30 | 31 | ```php 32 | namespace VeeWee\Xml\Xsd\Schema; 33 | 34 | #[Immutable] 35 | final class Schema 36 | { 37 | public static function withoutNamespace(string $location): self; 38 | public static function withNamespace(string $namespace, string $location): self; 39 | 40 | public function namespace(): ?string; 41 | public function location(): string; 42 | 43 | public function withLocation(string $location): self; 44 | } 45 | ``` 46 | 47 | In XML, you'll often have a collection of schema's to work with: 48 | 49 | ```php 50 | namespace VeeWee\Xml\Xsd\Schema; 51 | 52 | #[Extends iterable] 53 | #[Immutable] 54 | final class SchemaCollection implements Countable, IteratorAggregate 55 | { 56 | // Check the source code for a list of all functions you can use on this collection! 57 | } 58 | ``` 59 | 60 | This package provides some tools to work with schema's: 61 | 62 | * [Manipulators](#schema-manipulators): Can be used to manipulate a list of detected XSD schemas. 63 | 64 | ### Schema Manipulators 65 | 66 | 67 | ## Manipulators 68 | 69 | Can be used to manipulate a list of detected XSD schemas. 70 | 71 | #### base_path 72 | 73 | If your XML document contains relative paths, you need to determine a base path. 74 | This base path can be a path on the server or a URL. 75 | 76 | ```php 77 | use VeeWee\Xml\Xsd\Schema\Manipulator; 78 | 79 | $schemas = $schemas->manipulate( 80 | Manipulator\base_path('/var/www') 81 | ); 82 | ``` 83 | 84 | #### overwrite_with_local_files 85 | 86 | Some files on e.g. W3.org can take forever to download over the internet. 87 | They put a sleep in place so that packages like this won't spam them every second. 88 | You can however, locally download the files and map their namespace to the local file like this the snippet below. 89 | 90 | That way, you won't have to wait forever in order to validate your XML file! 91 | 92 | 93 | ```php 94 | use VeeWee\Xml\Xsd\Schema\Manipulator; 95 | 96 | $schemas = $schemas->manipulate(Manipulator\overwrite_with_local_files([ 97 | 'http://www.w3.org/2001/XMLSchema' => '/local/XMLSchema.xsd' 98 | ])); 99 | ``` 100 | 101 | ### Writing your own manipulator 102 | 103 | A manipulator can be any `callable` that takes a `SchemaCollection` and manipulates it into a new `SchemaCollection`. 104 | 105 | ```php 106 | namespace VeeWee\Xml\Xsd\Schema\Manipulator; 107 | 108 | use VeeWee\Xml\Xsd\Schema\SchemaCollection; 109 | 110 | interface Manipulator 111 | { 112 | public function __invoke(SchemaCollection $schemaCollection): SchemaCollection; 113 | } 114 | ``` 115 | 116 | -------------------------------------------------------------------------------- /src/Xml/Dom/Traverser/Visitor/RemoveNamespaces.php: -------------------------------------------------------------------------------- 1 | filter = $filter; 30 | } 31 | 32 | public static function all(): self 33 | { 34 | return new self(); 35 | } 36 | 37 | public static function prefixed(): self 38 | { 39 | return new self( 40 | static fn (Attr | Element $node): bool => $node->prefix !== null 41 | ); 42 | } 43 | 44 | public static function unprefixed(): self 45 | { 46 | return new self( 47 | static fn (Attr | Element $node): bool => $node->prefix === null 48 | ); 49 | } 50 | 51 | /** 52 | * @param list $prefixes 53 | */ 54 | public static function byPrefixNames(array $prefixes): self 55 | { 56 | return new self( 57 | static fn (Attr | Element $node): bool => match(true) { 58 | is_xmlns_attribute($node) => contains($prefixes, $node->prefix !== null ? $node->localName : ''), 59 | default => contains($prefixes, $node->prefix ?? '') 60 | } 61 | ); 62 | } 63 | 64 | /** 65 | * @param list $URIs 66 | */ 67 | public static function byNamespaceURIs(array $URIs): self 68 | { 69 | return new self( 70 | static fn (Attr | Element $node): bool => match(true) { 71 | is_xmlns_attribute($node) => contains($URIs, $node->value), 72 | default => contains($URIs, $node->namespaceURI), 73 | } 74 | ); 75 | } 76 | 77 | public function onNodeEnter(Node $node): Action 78 | { 79 | if (is_xmlns_attribute($node)) { 80 | return new Action\Noop(); 81 | } 82 | 83 | if (!$this->shouldDealWithNode($node)) { 84 | return new Action\Noop(); 85 | } 86 | 87 | /** @var Element | Attr $node */ 88 | return new Action\RenameNode($node->localName, null); 89 | } 90 | 91 | /** 92 | * @throws RuntimeException 93 | */ 94 | public function onNodeLeave(Node $node): Action 95 | { 96 | if (!is_xmlns_attribute($node)) { 97 | return new Action\Noop(); 98 | } 99 | 100 | if (!$this->shouldDealWithNode($node)) { 101 | return new Action\Noop(); 102 | } 103 | 104 | return new Action\RemoveNode(); 105 | } 106 | 107 | private function shouldDealWithNode(Node $node): bool 108 | { 109 | if (!is_element($node) && !is_attribute($node)) { 110 | return false; 111 | } 112 | 113 | // This shortcut avoids renaming nodes that already have no namespace. Speeding up the process. 114 | if ($node->namespaceURI === null) { 115 | // @infection-ignore-all 116 | return false; 117 | } 118 | 119 | if ($this->filter === null) { 120 | return true; 121 | } 122 | 123 | return ($this->filter)($node); 124 | } 125 | } 126 | --------------------------------------------------------------------------------