Passed
Push — master ( d0e934...b4bd91 )
by Caen
04:29 queued 12s
created

RssFeedService::canGenerateFeed()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 4
nc 4
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
/** @noinspection PhpComposerExtensionStubsInspection */
4
5
namespace Hyde\Framework\Services;
6
7
use Hyde\Framework\Hyde;
8
use Hyde\Framework\Models\Pages\MarkdownPost;
9
use SimpleXMLElement;
10
11
/**
12
 * @see \Hyde\Framework\Testing\Feature\Services\RssFeedServiceTest
13
 * @see https://validator.w3.org/feed/docs/rss2.html
14
 * @phpstan-consistent-constructor
15
 */
16
class RssFeedService
17
{
18
    public SimpleXMLElement $feed;
19
20
    public function __construct()
21
    {
22
        if (! extension_loaded('simplexml') || config('testing.mock_disabled_extensions', false) === true) {
23
            throw new \Exception('The ext-simplexml extension is not installed, but is required to generate RSS feeds.');
24
        }
25
26
        $this->feed = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?>
27
            <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" />');
28
        $this->feed->addChild('channel');
29
30
        $this->addInitialChannelItems();
31
    }
32
33
    /**
34
     * @throws \Exception
35
     */
36
    public function generate(): static
37
    {
38
        /** @var MarkdownPost $post */
39
        foreach (MarkdownPost::getLatestPosts() as $post) {
40
            $this->addItem($post);
41
        }
42
43
        return $this;
44
    }
45
46
    public function getXML(): string|false
47
    {
48
        return $this->feed->asXML();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->feed->asXML() could return the type true which is incompatible with the type-hinted return false|string. Consider adding an additional type-check to rule them out.
Loading history...
49
    }
50
51
    protected function addItem(MarkdownPost $post): void
52
    {
53
        $item = $this->feed->channel->addChild('item');
54
        $item->addChild('title', $post->findTitleForDocument());
55
        $item->addChild('link', $post->getCanonicalLink());
56
        $item->addChild('guid', $post->getCanonicalLink());
57
        $item->addChild('description', $post->getPostDescription());
58
59
        $this->addAdditionalItemData($item, $post);
60
    }
61
62
    protected function addAdditionalItemData(SimpleXMLElement $item, MarkdownPost $post): void
63
    {
64
        if (isset($post->date)) {
65
            $item->addChild('pubDate', $post->date->dateTimeObject->format(DATE_RSS));
66
        }
67
68
        if (isset($post->author)) {
69
            $item->addChild('dc:creator', $post->author->getName(), 'http://purl.org/dc/elements/1.1/');
70
        }
71
72
        if (isset($post->category)) {
73
            $item->addChild('category', $post->category);
74
        }
75
76
        if (isset($post->image)) {
77
            $image = $item->addChild('enclosure');
78
            $image->addAttribute('url', isset($post->image->path) ? Hyde::url('media/'.basename($post->image->path)) : $post->image->getSource());
0 ignored issues
show
Bug introduced by
It seems like $post->image->path can also be of type null; however, parameter $path of basename() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

78
            $image->addAttribute('url', isset($post->image->path) ? Hyde::url('media/'.basename(/** @scrutinizer ignore-type */ $post->image->path)) : $post->image->getSource());
Loading history...
79
            $image->addAttribute('type', str_ends_with($post->image->getSource(), '.png') ? 'image/png' : 'image/jpeg');
0 ignored issues
show
Bug introduced by
It seems like $post->image->getSource() can also be of type null; however, parameter $haystack of str_ends_with() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

79
            $image->addAttribute('type', str_ends_with(/** @scrutinizer ignore-type */ $post->image->getSource(), '.png') ? 'image/png' : 'image/jpeg');
Loading history...
80
            $image->addAttribute('length', $post->image->getContentLength());
81
        }
82
    }
83
84
    protected function addInitialChannelItems(): void
85
    {
86
        $this->feed->channel->addChild('title', static::getTitle());
87
        $this->feed->channel->addChild('link', static::getLink());
88
        $this->feed->channel->addChild('description', static::getDescription());
89
90
        $atomLink = $this->feed->channel->addChild('atom:link', namespace: 'http://www.w3.org/2005/Atom');
91
        $atomLink->addAttribute('href', static::getLink().'/'.static::getDefaultOutputFilename());
92
        $atomLink->addAttribute('rel', 'self');
93
        $atomLink->addAttribute('type', 'application/rss+xml');
94
95
        $this->addAdditionalChannelData();
96
    }
97
98
    protected function addAdditionalChannelData(): void
99
    {
100
        $this->feed->channel->addChild('language', config('site.language', 'en'));
101
        $this->feed->channel->addChild('generator', 'HydePHP '.Hyde::version());
102
        $this->feed->channel->addChild('lastBuildDate', date(DATE_RSS));
103
    }
104
105
    protected static function xmlEscape(string $string): string
106
    {
107
        return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT, 'UTF-8');
108
    }
109
110
    public static function getDescription(): string
111
    {
112
        return static::xmlEscape(
113
            config(
114
                'hyde.rss_description',
115
                static::getTitle().' RSS Feed'
116
            )
117
        );
118
    }
119
120
    public static function getTitle(): string
121
    {
122
        return static::xmlEscape(
123
            config('site.name', 'HydePHP')
124
        );
125
    }
126
127
    public static function getLink(): string
128
    {
129
        return static::xmlEscape(
130
            rtrim(
131
                config('site.url') ?? 'http://localhost',
132
                '/'
133
            )
134
        );
135
    }
136
137
    public static function getDefaultOutputFilename(): string
138
    {
139
        return config('hyde.rss_filename', 'feed.xml');
140
    }
141
142
    public static function generateFeed(): string
143
    {
144
        return (new static)->generate()->getXML();
145
    }
146
}
147