Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
1 / 1
RssFeedGenerator
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
11 / 11
16
100.00% covered (success)
100.00%
1 / 1
 generate
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 constructBaseElement
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 addItem
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 addDynamicItemData
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 addBaseChannelItems
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 addAtomLinkItem
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getImageType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getImageLength
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFilename
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDescription
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChannel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/** @noinspection PhpComposerExtensionStubsInspection */
4/** @noinspection XmlUnusedNamespaceDeclaration */
5
6declare(strict_types=1);
7
8namespace Hyde\Framework\Features\XmlGenerators;
9
10use Hyde\Hyde;
11use SimpleXMLElement;
12use Hyde\Facades\Site;
13use Hyde\Facades\Config;
14use Hyde\Pages\MarkdownPost;
15use Hyde\Support\Filesystem\MediaFile;
16use Hyde\Framework\Features\Blogging\Models\FeaturedImage;
17
18use function date;
19use function assert;
20use function sprintf;
21use function implode;
22
23/**
24 * @see https://validator.w3.org/feed/docs/rss2.html
25 */
26class RssFeedGenerator extends BaseXmlGenerator
27{
28    public function generate(): static
29    {
30        MarkdownPost::getLatestPosts()->each(function (MarkdownPost $post): void {
31            $this->addItem($post);
32        });
33
34        return $this;
35    }
36
37    protected function constructBaseElement(): void
38    {
39        $this->xmlElement = new SimpleXMLElement(implode("\n", [
40            '<?xml version="1.0" encoding="UTF-8"?>',
41            '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" />',
42        ]));
43
44        $this->xmlElement->addChild('channel');
45
46        $this->addBaseChannelItems();
47        $this->addAtomLinkItem();
48    }
49
50    protected function addItem(MarkdownPost $post): void
51    {
52        $item = $this->getChannel()->addChild('item');
53
54        $this->addChild($item, 'title', $post->title);
55        $this->addChild($item, 'description', $post->description);
56
57        $this->addDynamicItemData($item, $post);
58    }
59
60    protected function addDynamicItemData(SimpleXMLElement $item, MarkdownPost $post): void
61    {
62        if ($post->getCanonicalUrl() !== null) {
63            $this->addChild($item, 'link', $post->getCanonicalUrl());
64            $this->addChild($item, 'guid', $post->getCanonicalUrl());
65        }
66
67        if (isset($post->date)) {
68            $this->addChild($item, 'pubDate', $post->date->dateTimeObject->format(DATE_RSS));
69        }
70
71        if (isset($post->author)) {
72            $item->addChild('dc:creator', $post->author->name, 'http://purl.org/dc/elements/1.1/');
73        }
74
75        if (isset($post->category)) {
76            $this->addChild($item, 'category', $post->category);
77        }
78
79        if (isset($post->image)) {
80            $image = $item->addChild('enclosure');
81
82            $image->addAttribute('url', Hyde::url($post->image->getSource()));
83            $image->addAttribute('type', $this->getImageType($post->image));
84            $image->addAttribute('length', $this->getImageLength($post->image));
85        }
86    }
87
88    protected function addBaseChannelItems(): void
89    {
90        $channel = $this->getChannel();
91
92        $this->addChild($channel, 'title', Site::name());
93        $this->addChild($channel, 'link', Site::url());
94        $this->addChild($channel, 'description', $this->getDescription());
95        $this->addChild($channel, 'language', Config::getString('hyde.language', 'en'));
96        $this->addChild($channel, 'generator', 'HydePHP '.Hyde::version());
97        $this->addChild($channel, 'lastBuildDate', date(DATE_RSS));
98    }
99
100    protected function addAtomLinkItem(): void
101    {
102        $atomLink = $this->getChannel()->addChild('atom:link', namespace: 'http://www.w3.org/2005/Atom');
103
104        $atomLink->addAttribute('href', $this->escape(Hyde::url($this->getFilename())));
105        $atomLink->addAttribute('rel', 'self');
106        $atomLink->addAttribute('type', 'application/rss+xml');
107    }
108
109    protected function getImageType(FeaturedImage $image): string
110    {
111        return (new MediaFile($image->getSource()))->getMimeType();
112    }
113
114    /** @return numeric-string */
115    protected function getImageLength(FeaturedImage $image): string
116    {
117        return (string) $image->getContentLength();
118    }
119
120    public static function getFilename(): string
121    {
122        return Config::getString('hyde.rss.filename', 'feed.xml');
123    }
124
125    public static function getDescription(): string
126    {
127        return Config::getString('hyde.rss.description', sprintf('%s RSS Feed', Site::name()));
128    }
129
130    protected function getChannel(): SimpleXMLElement
131    {
132        assert($this->xmlElement->channel instanceof SimpleXMLElement);
133
134        return $this->xmlElement->channel;
135    }
136}