Prefix => URI. */ protected $namespaces; /** * @param string $root Root element name. * @param string $item Repeating item element name. * @param bool $pretty Indent output. * @param array $namespaces Namespace declarations (prefix => URI). * @throws \InvalidArgumentException For invalid element names. */ public function __construct( string $root, string $item, bool $pretty = false, array $namespaces = array() ) { $this->namespaces = $namespaces; $this->check_name( $root ); $this->check_name( $item ); $this->root = $root; $this->item = $item; $this->pretty = $pretty; } /** * {@inheritdoc} */ public function content_type(): string { return 'application/xml; charset=UTF-8'; } /** * Renders items as XML. * * @param array> $items Mapped items. * @return string */ public function render( array $items ): string { $writer = $this->open(); $this->start_root( $writer, $this->root ); foreach ( $items as $item ) { $this->write_item( $writer, $this->item, $item ); } $writer->endElement(); $writer->endDocument(); return $writer->outputMemory(); } /** * Opens a writer with the document declaration. * * @return \XMLWriter */ protected function open(): \XMLWriter { $writer = new \XMLWriter(); $writer->openMemory(); $writer->setIndent( $this->pretty ); $writer->setIndentString( ' ' ); $writer->startDocument( '1.0', 'UTF-8' ); return $writer; } /** * Starts the root element and declares the namespaces on it. * * @param \XMLWriter $writer Writer. * @param string $name Root name. * @param array $attributes Extra attributes (for example version="2.0"). */ protected function start_root( \XMLWriter $writer, string $name, array $attributes = array() ): void { $writer->startElement( $name ); foreach ( $attributes as $attribute => $value ) { $writer->writeAttribute( $attribute, $value ); } foreach ( $this->namespaces as $prefix => $uri ) { $writer->writeAttribute( 'xmlns:' . $prefix, Text_Cleaner::clean( $uri ) ); } } /** * Throws unless a name is valid and any prefix it carries has been declared. * * @param string $name Element or attribute name. * @param bool $attribute Whether it is an attribute name. * @throws \InvalidArgumentException When the name cannot be written safely. */ protected function check_name( string $name, bool $attribute = false ): void { $valid = $attribute ? Path_Validator::is_valid_attribute_name( $name, true ) : Path_Validator::is_valid_name( $name, true ); if ( ! $valid ) { throw new \InvalidArgumentException( 'Invalid XML name.' ); } $prefix = Path_Validator::prefix_of( $name ); if ( null !== $prefix && 'xml' !== $prefix && ! isset( $this->namespaces[ $prefix ] ) ) { throw new \InvalidArgumentException( 'Undeclared XML namespace prefix.' ); } } /** * Writes one item element: its attributes, then its children in the order they were mapped. * * @param \XMLWriter $writer Writer. * @param string $name Item element name. * @param array $item Item. */ protected function write_item( \XMLWriter $writer, string $name, array $item ): void { $writer->startElement( $name ); $this->write_attributes( $writer, isset( $item[ Mapping_Engine::ATTRIBUTES ] ) ? $item[ Mapping_Engine::ATTRIBUTES ] : array() ); $this->write_group( $writer, $item ); $writer->endElement(); } /** * Writes attributes. * * @param \XMLWriter $writer Writer. * @param mixed $attributes Name => value. */ private function write_attributes( \XMLWriter $writer, $attributes ): void { if ( ! is_array( $attributes ) ) { return; } foreach ( $attributes as $name => $value ) { $this->check_name( (string) $name, true ); $writer->writeAttribute( (string) $name, Text_Cleaner::clean( (string) $value ) ); } } /** * Writes the children of a group. * * @param \XMLWriter $writer Writer. * @param array $group Group node. * @throws \InvalidArgumentException For invalid element names. */ protected function write_group( \XMLWriter $writer, array $group ): void { foreach ( $group as $name => $value ) { $name = (string) $name; if ( Mapping_Engine::ATTRIBUTES === $name || Mapping_Engine::VALUE === $name ) { continue; } $this->check_name( $name ); $this->write_node( $writer, $name, $value ); } } /** * Writes one element for a node of any shape. * * @param \XMLWriter $writer Writer. * @param string $name Element name. * @param mixed $value Node. * @throws \InvalidArgumentException For invalid element names. */ private function write_node( \XMLWriter $writer, string $name, $value ): void { if ( $value instanceof Term_List ) { $this->check_name( $value->item_name ); $writer->startElement( $name ); $this->write_attributes( $writer, $value->attributes ); foreach ( $value->names as $term_name ) { $this->write_leaf( $writer, $value->item_name, $term_name ); } $writer->endElement(); return; } if ( ! is_array( $value ) ) { $this->write_leaf( $writer, $name, $value ); return; } if ( array_key_exists( Mapping_Engine::VALUE, $value ) ) { $this->write_valued( $writer, $name, $value ); return; } if ( Text_Cleaner::is_list( $value ) ) { foreach ( $value as $part ) { $this->write_leaf( $writer, $name, $part ); } return; } $writer->startElement( $name ); $this->write_attributes( $writer, isset( $value[ Mapping_Engine::ATTRIBUTES ] ) ? $value[ Mapping_Engine::ATTRIBUTES ] : array() ); $this->write_group( $writer, $value ); $writer->endElement(); } /** * Writes an element whose text sits beside attributes. A repeated value repeats the element, * attributes included. * * @param \XMLWriter $writer Writer. * @param string $name Element name. * @param array $node Node with "@value" and optional "@attributes". */ private function write_valued( \XMLWriter $writer, string $name, array $node ): void { $attributes = isset( $node[ Mapping_Engine::ATTRIBUTES ] ) ? $node[ Mapping_Engine::ATTRIBUTES ] : array(); $value = $node[ Mapping_Engine::VALUE ]; $values = ( is_array( $value ) && Text_Cleaner::is_list( $value ) ) ? $value : array( $value ); foreach ( $values as $part ) { $writer->startElement( $name ); $this->write_attributes( $writer, $attributes ); if ( null !== $part && '' !== (string) $part ) { $writer->text( Text_Cleaner::clean( (string) $part ) ); } $writer->endElement(); } } /** * Writes a leaf element. * * @param \XMLWriter $writer Writer. * @param string $name Element name. * @param mixed $value String or null. */ protected function write_leaf( \XMLWriter $writer, string $name, $value ): void { $writer->startElement( $name ); if ( null !== $value && '' !== (string) $value ) { $writer->text( Text_Cleaner::clean( (string) $value ) ); } $writer->endElement(); } }