Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
14 / 14
CRAP
100.00% covered (success)
100.00%
1 / 1
RssFeedService
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
14 / 14
24
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 generate
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getXML
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addItem
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 addAdditionalItemData
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
7
 addInitialChannelItems
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 addAdditionalChannelData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getDescription
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 xmlEscape
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTitle
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLink
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultOutputFilename
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 generateFeed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canGenerateFeed
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3namespace Hyde\Framework\Services;
4
5use Hyde\Framework\Helpers\Features;
6use Hyde\Framework\Hyde;
7use Hyde\Framework\Models\MarkdownPost;
8use SimpleXMLElement;
9
10/**
11 * @see \Tests\Feature\Services\RssFeedServiceTest
12 * @see https://validator.w3.org/feed/docs/rss2.html
13 */
14class RssFeedService
15{
16    public SimpleXMLElement $feed;
17
18    public function __construct()
19    {
20        $this->feed = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?>
21            <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" />');
22        $this->feed->addChild('channel');
23
24        $this->addInitialChannelItems();
25    }
26
27    /**
28     * @throws \Exception
29     */
30    public function generate(): self
31    {
32        /** @var MarkdownPost $post */
33        foreach (Hyde::getLatestPosts() as $post) {
34            $this->addItem($post);
35        }
36
37        return $this;
38    }
39
40    public function getXML(): string
41    {
42        return $this->feed->asXML();
43    }
44
45    protected function addItem(MarkdownPost $post): void
46    {
47        $item = $this->feed->channel->addChild('item');
48        $item->addChild('title', $post->findTitleForDocument());
49        $item->addChild('link', $post->getCanonicalLink());
50        $item->addChild('guid', $post->getCanonicalLink());
51        $item->addChild('description', $post->getPostDescription());
52
53        $this->addAdditionalItemData($item, $post);
54    }
55
56    protected function addAdditionalItemData(SimpleXMLElement $item, MarkdownPost $post): void
57    {
58        if (isset($post->date)) {
59            $item->addChild('pubDate', $post->date->dateTimeObject->format(DATE_RSS));
60        }
61
62        if (isset($post->author)) {
63            $item->addChild('dc:creator', $post->author->getName(), 'http://purl.org/dc/elements/1.1/');
64        }
65
66        if (isset($post->category)) {
67            $item->addChild('category', $post->category);
68        }
69
70        if (isset($post->image)) {
71            $image = $item->addChild('enclosure');
72            $image->addAttribute('url', isset($post->image->path) ? Hyde::uriPath('media/'.basename($post->image->path)) : $post->image->getSource());
73            $image->addAttribute('type', str_ends_with($post->image->getSource(), '.png') ? 'image/png' : 'image/jpeg');
74            $image->addAttribute('length', $post->image->getContentLength());
75        }
76    }
77
78    protected function addInitialChannelItems(): void
79    {
80        $this->feed->channel->addChild('title', static::getTitle());
81        $this->feed->channel->addChild('link', static::getLink());
82        $this->feed->channel->addChild('description', $this->getDescription());
83
84        $atomLink = $this->feed->channel->addChild('atom:link', namespace: 'http://www.w3.org/2005/Atom');
85        $atomLink->addAttribute('href', static::getLink().'/'.static::getDefaultOutputFilename());
86        $atomLink->addAttribute('rel', 'self');
87        $atomLink->addAttribute('type', 'application/rss+xml');
88
89        $this->addAdditionalChannelData();
90    }
91
92    protected function addAdditionalChannelData(): void
93    {
94        $this->feed->channel->addChild('language', config('hyde.language', 'en'));
95        $this->feed->channel->addChild('generator', 'HydePHP '.Hyde::version());
96        $this->feed->channel->addChild('lastBuildDate', date(DATE_RSS));
97    }
98
99    protected function getDescription(): string
100    {
101        return static::xmlEscape(
102            config('hyde.rss_description',
103                static::getTitle().' RSS Feed')
104        );
105    }
106
107    protected static function xmlEscape(string $string): string
108    {
109        return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT, 'UTF-8');
110    }
111
112    public static function getTitle(): string
113    {
114        return static::xmlEscape(
115            config('hyde.name', 'HydePHP')
116        );
117    }
118
119    public static function getLink(): string
120    {
121        return static::xmlEscape(
122            rtrim(
123                config('hyde.site_url') ?? 'http://localhost', '/'
124            )
125        );
126    }
127
128    public static function getDefaultOutputFilename(): string
129    {
130        return config('hyde.rss_filename', 'feed.xml');
131    }
132
133    public static function generateFeed(): string
134    {
135        return (new static)->generate()->getXML();
136    }
137
138    public static function canGenerateFeed(): bool
139    {
140        return (Hyde::uriPath() !== false)
141            && config('hyde.generate_rss_feed', true)
142            && Features::hasBlogPosts()
143            && extension_loaded('simplexml');
144    }
145}