├── .github └── funding.yml ├── composer.json ├── example-atom.php ├── example-rss.php ├── license.md ├── readme.md └── src └── Feed.php /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: dg 2 | custom: "https://nette.org/make-donation?to=rss-php" 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dg/rss-php", 3 | "description": "RSS & Atom Feeds for PHP is a very small and easy-to-use library for consuming an RSS and Atom feed", 4 | "keywords": ["rss", "atom", "feed"], 5 | "homepage": "https://github.com/dg/rss-php", 6 | "license": ["BSD-3-Clause"], 7 | "authors": [ 8 | { 9 | "name": "David Grudl", 10 | "homepage": "https://davidgrudl.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.3", 15 | "ext-simplexml": "*" 16 | }, 17 | "autoload": { 18 | "classmap": ["src/"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example-atom.php: -------------------------------------------------------------------------------- 1 | 14 | 15 |
content) ?>
25 | 26 | 27 | -------------------------------------------------------------------------------- /example-rss.php: -------------------------------------------------------------------------------- 1 | 13 | 14 |description) ?>
17 | 18 | item as $item): ?> 19 |description) ?>
26 | 27 | 28 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, Copyright (c) 2008 David Grudl 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of David Grudl nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | RSS & Atom Feeds for PHP 2 | ======================== 3 | 4 | [](https://packagist.org/packages/dg/rss-php) 5 | [](https://github.com/dg/rss-php/releases) 6 | [](https://github.com/dg/rss-php/blob/master/license.md) 7 | 8 | RSS & Atom Feeds for PHP is a very small and easy-to-use library for consuming an RSS and Atom feeds. 9 | 10 | It requires PHP 5.3 or newer with CURL extension or enabled allow_url_fopen 11 | and is licensed under the New BSD License. You can obtain the latest version from 12 | our [GitHub repository](https://github.com/dg/rss-php/releases) or install it via Composer: 13 | 14 | ``` 15 | composer require dg/rss-php 16 | ``` 17 | 18 | [Support Me](https://github.com/sponsors/dg) 19 | -------------------------------------------- 20 | 21 | Do you like RSS? Are you looking forward to the new features? 22 | 23 | [](https://github.com/sponsors/dg) 24 | 25 | Thank you! 26 | 27 | 28 | Usage 29 | ----- 30 | 31 | Download RSS feed from URL: 32 | 33 | ```php 34 | $rss = Feed::loadRss($url); 35 | ``` 36 | 37 | The returned properties are SimpleXMLElement objects. Extracting 38 | the information from the channel is easy: 39 | 40 | ```php 41 | echo 'Title: ', $rss->title; 42 | echo 'Description: ', $rss->description; 43 | echo 'Link: ', $rss->url; 44 | 45 | foreach ($rss->item as $item) { 46 | echo 'Title: ', $item->title; 47 | echo 'Link: ', $item->url; 48 | echo 'Timestamp: ', $item->timestamp; 49 | echo 'Description ', $item->description; 50 | echo 'HTML encoded content: ', $item->{'content:encoded'}; 51 | } 52 | ``` 53 | 54 | Download Atom feed from URL: 55 | 56 | ```php 57 | $atom = Feed::loadAtom($url); 58 | ``` 59 | 60 | You can also enable caching: 61 | 62 | ```php 63 | Feed::$cacheDir = __DIR__ . '/tmp'; 64 | Feed::$cacheExpire = '5 hours'; 65 | ``` 66 | 67 | You can setup a User-Agent if needed: 68 | 69 | ```php 70 | Feed::$userAgent = "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"; 71 | ``` 72 | 73 | If you like it, **[please make a donation now](https://nette.org/make-donation?to=rss-php)**. Thank you! 74 | -------------------------------------------------------------------------------- /src/Feed.php: -------------------------------------------------------------------------------- 1 | channel) { 37 | return self::fromRss($xml); 38 | } else { 39 | return self::fromAtom($xml); 40 | } 41 | } 42 | 43 | 44 | /** 45 | * Loads RSS feed. 46 | * @param string RSS feed URL 47 | * @param string optional user name 48 | * @param string optional password 49 | * @return Feed 50 | * @throws FeedException 51 | */ 52 | public static function loadRss($url, $user = null, $pass = null) 53 | { 54 | return self::fromRss(self::loadXml($url, $user, $pass)); 55 | } 56 | 57 | 58 | /** 59 | * Loads Atom feed. 60 | * @param string Atom feed URL 61 | * @param string optional user name 62 | * @param string optional password 63 | * @return Feed 64 | * @throws FeedException 65 | */ 66 | public static function loadAtom($url, $user = null, $pass = null) 67 | { 68 | return self::fromAtom(self::loadXml($url, $user, $pass)); 69 | } 70 | 71 | 72 | private static function fromRss(SimpleXMLElement $xml) 73 | { 74 | if (!$xml->channel) { 75 | throw new FeedException('Invalid feed.'); 76 | } 77 | 78 | self::adjustNamespaces($xml); 79 | 80 | foreach ($xml->channel->item as $item) { 81 | // converts namespaces to dotted tags 82 | self::adjustNamespaces($item); 83 | 84 | // generate 'url' & 'timestamp' tags 85 | $item->url = (string) $item->link; 86 | if (isset($item->{'dc:date'})) { 87 | $item->timestamp = strtotime($item->{'dc:date'}); 88 | } elseif (isset($item->pubDate)) { 89 | $item->timestamp = strtotime($item->pubDate); 90 | } 91 | } 92 | $feed = new self; 93 | $feed->xml = $xml->channel; 94 | return $feed; 95 | } 96 | 97 | 98 | private static function fromAtom(SimpleXMLElement $xml) 99 | { 100 | if (!in_array('http://www.w3.org/2005/Atom', $xml->getDocNamespaces(), true) 101 | && !in_array('http://purl.org/atom/ns#', $xml->getDocNamespaces(), true) 102 | ) { 103 | throw new FeedException('Invalid feed.'); 104 | } 105 | 106 | // generate 'url' & 'timestamp' tags 107 | foreach ($xml->entry as $entry) { 108 | $entry->url = (string) $entry->link['href']; 109 | $entry->timestamp = strtotime($entry->updated); 110 | } 111 | $feed = new self; 112 | $feed->xml = $xml; 113 | return $feed; 114 | } 115 | 116 | 117 | /** 118 | * Returns property value. Do not call directly. 119 | * @param string tag name 120 | * @return SimpleXMLElement 121 | */ 122 | public function __get($name) 123 | { 124 | return $this->xml->{$name}; 125 | } 126 | 127 | 128 | /** 129 | * Sets value of a property. Do not call directly. 130 | * @param string property name 131 | * @param mixed property value 132 | * @return void 133 | */ 134 | public function __set($name, $value) 135 | { 136 | throw new Exception("Cannot assign to a read-only property '$name'."); 137 | } 138 | 139 | 140 | /** 141 | * Converts a SimpleXMLElement into an array. 142 | * @param SimpleXMLElement 143 | * @return array 144 | */ 145 | public function toArray(SimpleXMLElement $xml = null) 146 | { 147 | if ($xml === null) { 148 | $xml = $this->xml; 149 | } 150 | 151 | if (!$xml->children()) { 152 | return (string) $xml; 153 | } 154 | 155 | $arr = []; 156 | foreach ($xml->children() as $tag => $child) { 157 | if (count($xml->$tag) === 1) { 158 | $arr[$tag] = $this->toArray($child); 159 | } else { 160 | $arr[$tag][] = $this->toArray($child); 161 | } 162 | } 163 | 164 | return $arr; 165 | } 166 | 167 | 168 | /** 169 | * Load XML from cache or HTTP. 170 | * @param string 171 | * @param string 172 | * @param string 173 | * @return SimpleXMLElement 174 | * @throws FeedException 175 | */ 176 | private static function loadXml($url, $user, $pass) 177 | { 178 | $e = self::$cacheExpire; 179 | $cacheFile = self::$cacheDir . '/feed.' . md5(serialize(func_get_args())) . '.xml'; 180 | 181 | if (self::$cacheDir 182 | && (time() - @filemtime($cacheFile) <= (is_string($e) ? strtotime($e) - time() : $e)) 183 | && $data = @file_get_contents($cacheFile) 184 | ) { 185 | // ok 186 | } elseif ($data = trim(self::httpRequest($url, $user, $pass))) { 187 | if (self::$cacheDir) { 188 | file_put_contents($cacheFile, $data); 189 | } 190 | } elseif (self::$cacheDir && $data = @file_get_contents($cacheFile)) { 191 | // ok 192 | } else { 193 | throw new FeedException('Cannot load feed.'); 194 | } 195 | 196 | return new SimpleXMLElement($data, LIBXML_NOWARNING | LIBXML_NOERROR | LIBXML_NOCDATA); 197 | } 198 | 199 | 200 | /** 201 | * Process HTTP request. 202 | * @param string 203 | * @param string 204 | * @param string 205 | * @return string|false 206 | * @throws FeedException 207 | */ 208 | private static function httpRequest($url, $user, $pass) 209 | { 210 | if (extension_loaded('curl')) { 211 | $curl = curl_init(); 212 | curl_setopt($curl, CURLOPT_URL, $url); 213 | if ($user !== null || $pass !== null) { 214 | curl_setopt($curl, CURLOPT_USERPWD, "$user:$pass"); 215 | } 216 | curl_setopt($curl, CURLOPT_USERAGENT, self::$userAgent); // some feeds require a user agent 217 | curl_setopt($curl, CURLOPT_HEADER, false); 218 | curl_setopt($curl, CURLOPT_TIMEOUT, 20); 219 | curl_setopt($curl, CURLOPT_ENCODING, ''); 220 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); // no echo, just return result 221 | curl_setopt($curl, CURLOPT_USERAGENT, ''); 222 | if (!ini_get('open_basedir')) { 223 | curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); // sometime is useful :) 224 | } 225 | $result = curl_exec($curl); 226 | return curl_errno($curl) === 0 && curl_getinfo($curl, CURLINFO_HTTP_CODE) === 200 227 | ? $result 228 | : false; 229 | 230 | } else { 231 | $context = null; 232 | if ($user !== null && $pass !== null) { 233 | $options = [ 234 | 'http' => [ 235 | 'method' => 'GET', 236 | 'header' => 'Authorization: Basic ' . base64_encode($user . ':' . $pass) . "\r\n", 237 | ], 238 | ]; 239 | $context = stream_context_create($options); 240 | } 241 | 242 | return file_get_contents($url, false, $context); 243 | } 244 | } 245 | 246 | 247 | /** 248 | * Generates better accessible namespaced tags. 249 | * @param SimpleXMLElement 250 | * @return void 251 | */ 252 | private static function adjustNamespaces($el) 253 | { 254 | foreach ($el->getNamespaces(true) as $prefix => $ns) { 255 | if ($prefix === '') { 256 | continue; 257 | } 258 | $children = $el->children($ns); 259 | foreach ($children as $tag => $content) { 260 | $el->{$prefix . ':' . $tag} = $content; 261 | } 262 | } 263 | } 264 | } 265 | 266 | 267 | 268 | /** 269 | * An exception generated by Feed. 270 | */ 271 | class FeedException extends Exception 272 | { 273 | } 274 | --------------------------------------------------------------------------------