dg/rss-php
dg/rss-php is a lightweight, dependency-free PHP library for reading RSS and Atom feeds. It offers a simple API for fetching and parsing feeds, handles namespaces and enclosures, and works across PHP versions from 5.3 onward.
composer require dg/rss-php
The whole API surface is two static methods. RSS::load($url, $timeout = 10) fetches a remote feed over HTTP(S) and parses it; RSS::loadFile($path) parses a local file. Both return a plain SimpleXMLElement representing the document root — there is no DTO layer, no config file, and no service container binding.
require __DIR__ . '/vendor/autoload.php';
$feed = RSS::load('https://blog.example.com/feed.xml');
foreach ($feed->channel->item as $item) {
printf("%s — %s\n", (string) $item->title, (string) $item->link);
}
Where to look first: the entire library is effectively one file (src/RSS.php, a few hundred lines). Read it before building on it — there is no hidden magic, and knowing exactly how it fetches and parses saves hours of guessing later.
First use case: a "latest posts" widget or a scheduled import job that pulls a handful of external feeds into your own table. That is the shape this package was designed for.
Wrap it immediately. The static API is untestable and unmockable, so put it behind one class from day one.
final class FeedReader
{
public function __construct(private int $timeout = 10) {}
/** @return array<int, array{title:string,link:string,date:?string,guid:string}> */
public function items(string $url): array
{
$feed = RSS::load($url, $this->timeout);
// RSS 2.0/RDF expose channel->item; Atom exposes entry.
$nodes = isset($feed->channel->item) ? $feed->channel->item : $feed->entry;
$out = [];
foreach ($nodes as $item) {
$out[] = [
'title' => trim((string) $item->title),
'link' => $this->link($item),
'date' => $this->date($item),
'guid' => $this->guid($item),
];
}
return $out;
}
}
Normalise links — this is the most common breakage. RSS uses a text node; Atom uses <link rel="alternate" href="…"/> and may include several links (self, enclosure, alternate).
private function link(SimpleXMLElement $item): string
{
if (isset($item->link['href'])) { // Atom
foreach ($item->link as $l) {
$rel = (string) ($l['rel'] ?? 'alternate');
if ($rel === 'alternate') return (string) $l['href'];
}
}
return (string) $item->link; // RSS
}
Normalise dates. pubDate (RFC 822) vs published/updated (RFC 3339). Have a null fallback.
$raw = (string) ($item->pubDate ?? $item->published ?? $item->updated ?? '');
$date = $raw !== '' ? new DateTimeImmutable($raw) : null;
Dedupe on ingest. Use guid/id when present, otherwise hash the stable parts:
$key = $guid ?: sha1($link . $title . ($date?->format('c') ?? ''));
FeedItem::updateOrCreate(['guid' => $key], $attributes);
Add a unique index on guid — feeds re-publish items constantly, and this is what stops duplicate rows.
Namespaced fields. content:encoded, dc:creator, and media:* are invisible unless you ask for the namespace explicitly:
$html = (string) $item->children('content', true)->encoded;
$thumb = (string) $item->children('media', true)->thumbnail->attributes()['url'];
$author = (string) $item->children('dc', true)->creator;
Caching and scheduling. The fetch is blocking and uncached. In Laravel, run it from a queued job or a scheduled command, persist the rows, and let the rest of the app read your own table. If you genuinely need it in a request cycle, wrap with Cache::remember("feed:$url", 600, fn () => $reader->items($url)).
load($url) or loadFile($path) only. If you need custom HTTP (auth, signed requests, a specific User-Agent), fetch with Laravel's Http::get() and parse with simplexml_load_string() yourself — but you then own all the RSS/Atom differences above.$item->title is an object. empty($item->title) is misleading for <title>0</title>, and comparisons silently fail. (string) $item->title everywhere.$item->link is a text node in RSS but an attribute-bearing element in Atom; guid vs id; description vs summary. Test against one real RSS 2.0 feed, one Atom feed, and one RDF feed before shipping.try/catch, log the URL plus a snippet of the raw body, and skip the feed rather than failing the whole job.file_put_contents('/tmp/feed.xml', $feed->asXML()), then read it. Guessing at $item->whatever is the slow path.content:encoded are third-party HTML; sanitise before rendering and never {!! $item->description !!} straight from an external feed.How can I help you explore Laravel packages today?