Passed
Push — master ( c3fcb2...479941 )
by Nicola
03:52
created

FeedReaderService::dateObject()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace eNTiDi\FeedReader;
4
5
use GuzzleHttp\Client;
6
use Psr\SimpleCache\CacheInterface;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\ORM;
9
use SilverStripe\ORM\ArrayList;
10
use SilverStripe\ORM\FieldType\DBField;
11
use SilverStripe\View\ArrayData;
12
use SimpleXMLElement;
13
14
class FeedReaderService
15
{
16
    private $url;
17
    private $expiration;
18
    private $options;
19
    private $summary_len;
20
21
    private static function getCache()
22
    {
23
        return Injector::inst()->get(CacheInterface::class . '.FeedReader');
24
    }
25
26
    private function getKey()
27
    {
28
        return urlencode($this->url);
29
    }
30
31
    private static function dateObject($node)
32
    {
33
        $text = (string) $node;
34
        if (empty($text)) {
35
            return null;
36
        }
37
38
        $timestamp = strtotime($text);
39
        if ($timestamp === false) {
40
            return null;
41
        }
42
43
        return DBField::create_field('DBDatetime', $timestamp);
44
    }
45
46
    private static function excerpt($html, $maxlen)
47
    {
48
        // Strip HTML tags and convert blank chains to a single space
49
        $excerpt = trim(preg_replace('/\s+/', ' ', strip_tags($html)));
50
        if (strlen($excerpt) <= $maxlen) {
51
            return $excerpt;
52
        }
53
54
        // Try to cut the excerpt on a word boundary
55
        $pivot = strrpos(substr($excerpt, 0, $maxlen - 2), ' ');
56
        if ($pivot === false || $pivot < $maxlen - 15) {
57
            $pivot = $maxlen - 3;
58
        }
59
        $excerpt = rtrim(substr($excerpt, 0, $pivot));
60
61
        // Ellipsize the final result
62
        return rtrim($excerpt, '.') . '...';
63
    }
64
65
    private function appendRSS2Items(&$items, $xml)
66
    {
67
        foreach ($xml->xpath('//channel/item') as $node) {
68
            $content = (string) $node->description;
69
            $row = new ArrayData([
70
                'Id'      => (string) $node->guid,
71
                'Link'    => (string) $node->link,
72
                'Date'    => self::dateObject($node->pubDate),
73
                'Title'   => (string) $node->title,
74
75
                // RSS 2.0 does not have a summary field: generate it
76
                // from an excerpt of the "description" field
77
                'Summary' => self::excerpt($content, $this->getSummaryLen()),
78
79
                'Content' => $content
80
            ]);
81
            $items->push($row);
82
        }
83
    }
84
85
    private function appendAtom1Items(&$items, $xml)
86
    {
87
        $xml->registerXPathNamespace('A', 'http://www.w3.org/2005/Atom');
88
        foreach ($xml->xpath('//A:feed/A:entry') as $node) {
89
            $summary = (string) $node->summary;
90
            if (! $node->content) {
91
                // Atom 1.0 does not require <content> elements, so
92
                // ensure it is at least populated with $summary
93
                $content = $summary;
94
            } elseif ($node->content->count() > 0) {
95
                $content = $node->content->children()[0]->asXML();
96
            } else {
97
                $content = (string) $node->content;
98
            }
99
            $row = new ArrayData([
100
                'Id'      => (string) $node->id,
101
                'Link'    => (string) $node->link['href'],
102
                'Date'    => self::dateObject($node->updated),
103
                'Title'   => (string) $node->title,
104
                'Summary' => $summary,
105
                'Content' => $content,
106
            ]);
107
            $items->push($row);
108
        }
109
    }
110
111
    public function __construct($url, $expiration = 3600, $options = [])
112
    {
113
        $this->url        = $url;
114
        $this->expiration = $expiration;
115
        $this->options    = $options;
116
    }
117
118
    public function setSummaryLen($maxlen)
119
    {
120
        $this->summary_len = $maxlen;
121
    }
122
123
    public function getSummaryLen()
124
    {
125
        return is_int($this->summary_len) ? $this->summary_len : 155;
126
    }
127
128
    public function getItems()
129
    {
130
        $cache = self::getCache();
131
        $key   = $this->getKey();
132
        if ($cache->has($key)) {
133
            $items = $cache->get($key);
134
        } else {
135
            $client   = new Client($this->options + [ 'timeout' => 2 ]);
136
            $response = $client->request('GET', $this->url);
137
            $code     = $response->getStatusCode();
138
            $data     = $response->getBody()->getContents();
139
            if ($code != 200) {
140
                user_error("RSS fetch error ($code). The response body is '$data'", E_USER_ERROR);
141
            }
142
143
            $xml = new SimpleXMLElement($data);
144
            $items = new ArrayList();
145
            $this->appendRSS2Items($items, $xml);
146
            $this->appendAtom1Items($items, $xml);
147
            $cache->set($key, $items, $this->expiration);
148
        }
149
        return $items;
150
    }
151
152
    public function clearCache()
153
    {
154
        $cache = self::getCache();
155
        $cache->delete($this->getKey());
156
    }
157
}
158