src/FeedIo/Formatter/XmlFormatter.php
<?php

declare(strict_types=1);

namespace FeedIo\Formatter;

use DOMDocument;
use DOMElement;
use FeedIo\Feed\ElementsAwareInterface;
use FeedIo\Feed\NodeInterface;
use FeedIo\Feed\StyleSheet;
use FeedIo\FeedInterface;
use FeedIo\Rule\OptionalField;
use FeedIo\RuleSet;
use FeedIo\Standard\XmlAbstract;
use FeedIo\FormatterInterface;

/**
 * Turns a FeedInterface instance into a XML document.
 *
 * Depends on :
 *  - FeedIo\StandardAbstract
 *
 */
class XmlFormatter implements FormatterInterface
{
    public function __construct(
        protected XmlAbstract $standard
    ) {
    }

    public function setHeaders(DOMDocument $document, FeedInterface $feed): XmlFormatter
    {
        $rules = $this->standard->getFeedRuleSet();
        $mainElement = $this->standard->getMainElement($document);
        $this->buildElements($rules, $document, $mainElement, $feed);

        return $this;
    }

    public function addItem(DOMDocument $document, NodeInterface $node): XmlFormatter
    {
        $domItem = $document->createElement($this->standard->getItemNodeName());
        $rules = $this->standard->getItemRuleSet();
        $this->buildElements($rules, $document, $domItem, $node);

        $this->standard->getMainElement($document)->appendChild($domItem);

        return $this;
    }

    public function buildElements(RuleSet $ruleSet, DOMDocument $document, DOMElement $rootElement, NodeInterface $node): void
    {
        $rules = $this->getAllRules($ruleSet, $node);

        foreach ($rules as $rule) {
            $rule->apply($document, $rootElement, $node);
        }
    }

    public function getAllRules(RuleSet $ruleSet, NodeInterface $node): iterable
    {
        $rules = $ruleSet->getRules();
        if ($node instanceof ElementsAwareInterface) {
            $optionalFields = $node->listElements();
            foreach ($optionalFields as $optionalField) {
                $rules[$optionalField] = new OptionalField($optionalField);
            }
        }

        return $rules;
    }

    public function getEmptyDocument(): DOMDocument
    {
        return new DOMDocument('1.0', 'utf-8');
    }

    public function getDocument(): DOMDocument
    {
        $document = $this->getEmptyDocument();

        return $this->standard->format($document);
    }

    /**
     * @param  FeedInterface $feed
     * @return string
     */
    public function toString(FeedInterface $feed): string
    {
        $document = $this->toDom($feed);

        return $document->saveXML();
    }

    public function toDom(FeedInterface $feed): DOMDocument
    {
        $document = $this->getDocument();

        $this->setHeaders($document, $feed);
        $this->setItems($document, $feed);
        $this->setNS($document, $feed);
        $this->setStyleSheet($document, $feed);

        return $document;
    }

    public function setNS(DOMDocument $document, FeedInterface $feed)
    {
        $firstChild = $document->firstChild;
        if ($firstChild instanceof DOMElement) {
            foreach ($feed->getNS() as $namespace => $dtd) {
                $firstChild->setAttributeNS(
                    'http://www.w3.org/2000/xmlns/', // xmlns namespace URI
                    'xmlns:'.$namespace,
                    $dtd
                );
            }
        }
    }

    public function setItems(DOMDocument $document, FeedInterface $feed): XmlFormatter
    {
        foreach ($feed as $item) {
            $this->addItem($document, $item);
        }

        return $this;
    }

    public function setStyleSheet(DOMDocument $document, FeedInterface $feed): XmlFormatter
    {
        $styleSheet = $feed->getStyleSheet();
        if ($styleSheet instanceof StyleSheet) {
            $attributes = sprintf('type="%s" href="%s"', $styleSheet->getType(), $styleSheet->getHref());
            $xsl = $document->createProcessingInstruction('xml-stylesheet', $attributes);
            $document->insertBefore($xsl, $document->firstChild);
        }

        return $this;
    }
}
