vinelab/rss
Vinelab RSS is a Laravel package for building and serving RSS feeds from your application. Define channels and items with a fluent API, manage feed metadata, and output valid RSS XML for readers and aggregators.
Install via Composer:
composer require vinelab/rss
On Laravel 5.5+ the service provider is auto-discovered. On older apps (or if you're on a skeleton that disables discovery), register Vinelab\Rss\RssServiceProvider in config/app.php and optionally alias the facade:
'aliases' => [
'Rss' => Vinelab\Rss\Facades\Rss::class,
],
First use case — fetch a feed and iterate articles:
use Vinelab\Rss\Facades\Rss;
$feed = Rss::feed('https://example.com/feed.xml');
$channel = $feed->channel();
echo $channel->title;
foreach ($feed->articles() as $article) {
echo $article->title;
echo $article->link;
echo strip_tags($article->description);
}
Where to look first:
README.md — the canonical usage examples; the API surface is small.src/Rss.php / src/Reader.php — the entry point and HTTP/XML fetch logic.src/Feed.php — wraps a parsed feed; channel() and articles() live here.src/Channel.php and src/Item.php — the value objects you'll actually render.src/Facades/ and src/RssServiceProvider.php — container bindings if you need to swap or mock.Start with a single feed in a tinker session (php artisan tinker) to confirm the upstream responds and that the fields you need (title, link, description, pubDate, author, guid) are populated.
Wrap the facade behind a service. The facade is convenient but hard to fake precisely; inject a small FeedFetcher class that calls Rss::feed() internally. Your controllers/commands depend on the fetcher, and tests can swap it for a stub returning canned Feed objects.
Cache aggressively. Remote feeds are slow, rate-limited, and often return 304s you'd have to handle yourself. Wrap the call:
$feed = Cache::remember("rss:{$url}", now()->addMinutes(15), fn () => Rss::feed($url));
Cache the parsed result, not the raw XML, and key by URL. Invalidate on a schedule rather than per request.
Poll on a schedule, persist, dedupe. The typical production shape is a scheduled command:
// app/Console/Commands/ImportFeeds.php
foreach (config('feeds.sources') as $source) {
$feed = Rss::feed($source);
foreach ($feed->articles() as $article) {
FeedItem::firstOrCreate(
['guid' => $article->guid ?? $article->link],
[
'title' => $article->title,
'link' => $article->link,
'summary' => strip_tags($article->description ?? ''),
'published_at' => $article->pubDate ? Carbon::parse($article->pubDate) : null,
]
);
}
}
Register it with $schedule->command('feeds:import')->hourly(). This decouples user requests from upstream latency and gives you an audit trail.
Normalize into DTOs / API resources. Channel and Item expose loosely-typed properties (some are strings, pubDate may be an RFC-822 string or already parsed depending on version). Map them into your own value objects before exposing via API — this isolates the rest of the app from package quirks and makes it trivial to swap the library later.
Aggregate multiple sources. Loop over a config array of URLs, collect into a single collection, sort by published_at descending, and paginate. Do the fetching in a queued job per source so one slow feed doesn't block the rest; fan-in results via a cache or DB table.
Integrate with notifications. Combine with Laravel's notification system: after import, query newly created items and dispatch digests. Keep the RSS layer responsible only for fetching/parsing; business rules live in your domain code.
composer.json and add a smoke test that hits a known feed on CI. If PHP upgrades break the XML handling or a transitive dep, you'll want that failing fast.Http::timeout(10)->get($url)->body()) and feed it into the reader/parser if it supports a string input — otherwise proxy through your own cache.&, and bad entities. Wrap calls in try/catch (the package throws on parse failures; also check libxml_get_errors() if you debug). Sanitize by stripping the BOM and replacing bare ampersands before parsing if you control the fetch.SimpleXML assumes UTF-8 unless the XML declaration says otherwise. Non-UTF-8 feeds produce mojibake; run mb_convert_encoding or ensure the declaration is intact.pubDate is not a Carbon instance. RSS 2.0 uses RFC-822; always Carbon::parse($article->pubDate) and store in UTC. Don't rely on it being non-null — some feeds omit it.description. Descriptions frequently contain CDATA-wrapped HTML. Escape on output ({{ }}) or strip_tags() on storage. Never render raw feed content directly — it's an XSS vector.media:content (images), dc:creator, content:encoded, iTunes tags, and Atom-specific fields aren't first-class in this package. If you need them, parse the raw XML separately with SimpleXMLElement and use children($ns), or use the package only for the common fields and augment elsewhere.$feed->channel() and articles() are non-empty; treat empty as a failure, not a valid empty feed.Rss::shouldReceive(...) works, but the return types are concrete package classes. Easier to mock a thin wrapper interface than to build Feed/Item fixtures by hand — or construct them directly from a sample XML string.Feed/Channel/Item API your app already uses. This is the cleanest escape hatch if you need modern features without a rewrite.dd($feed->channel()) and dd($feed->articles()) first; if empty, dd(file_get_contents($url)) to confirm the response isn't an HTML error page or a 403. Enable libxml_use_internal_errors(true) around parsing in a scratch command to see the real failure.How can I help you explore Laravel packages today?