laminas/laminas-feed
laminas-feed is a PHP library for creating, reading, and validating RSS and Atom feeds. It offers parsers, writers, and feed classes with support for extensions like Dublin Core, iTunes, and Podcast, plus HTTP client integration for importing remote feeds.
laminas/laminas-feed is a framework-agnostic RSS/Atom reader and writer. For a Laravel app, treat it as a low-level feed XML library: you still own routing, caching, queuing, validation, and persistence.
Minimal setup:
composer require laminas/laminas-feed
There is no Laravel service provider to install. Look first at:
Laminas\Feed\Writer\Feed for generating RSS/Atom.Laminas\Feed\Reader\Reader for consuming feeds.First useful use case: expose an RSS feed for published Eloquent posts.
use App\Models\Post;
use Illuminate\Support\Str;
use Laminas\Feed\Writer\Feed;
Route::get('/feed', function () {
$feed = new Feed();
$feed->setTitle(config('app.name').' Blog');
$feed->setLink(route('home'));
$feed->setDescription('Latest published posts');
$feed->setFeedLink(route('feed.rss'), 'rss');
$feed->setDateModified(now());
$feed->setLanguage('en');
Post::query()
->with('author')
->whereNotNull('published_at')
->latest('published_at')
->limit(50)
->get()
->each(function (Post $post) use ($feed) {
$entry = $feed->createEntry();
$entry->setId((string) $post->id);
$entry->setTitle($post->title);
$entry->setLink(route('posts.show', $post));
$entry->setDateCreated($post->published_at);
$entry->setDateModified($post->updated_at);
$entry->setDescription(Str::limit(strip_tags($post->body), 300));
$entry->setContent($post->body);
$entry->addAuthor([
'name' => $post->author->name,
'email' => $post->author->email,
]);
$feed->addEntry($entry);
});
return response($feed->export('rss'), 200)
->header('Content-Type', 'application/rss+xml; charset=UTF-8');
})->name('feed.rss');
Validate the output with the W3C Feed Validator before shipping. For Atom, use $feed->export('atom') and application/atom+xml.
Generate feeds through a service, not inline controllers. Wrap feed construction in an application service so it can be reused by controllers, console commands, and tests.
final class BlogFeedService
{
public function rss(): string
{
// build and return $feed->export('rss');
}
public function atom(): string
{
// build and return $feed->export('atom');
}
}
Cache rendered XML and use HTTP validators. Feed endpoints are read-heavy and change infrequently.
$xml = Cache::remember('feed:rss', now()->addMinutes(10), fn () => $feedService->rss());
return response($xml)
->setEtag('"'.sha1($xml).'"')
->header('Content-Type', 'application/rss+xml; charset=UTF-8');
Consume external feeds with Laravel’s HTTP client, then Reader::importString(). This keeps Laravel’s timeout, retry, proxy, and test fakes in control.
use Illuminate\Support\Facades\Http;
use Laminas\Feed\Reader\Reader;
$response = Http::timeout(10)
->retry(2, 200)
->get($feedUrl);
$feed = Reader::importString($response->body());
foreach ($feed as $entry) {
ExternalPost::updateOrCreate(
['guid' => $entry->getId() ?: $entry->getLink()],
[
'title' => $entry->getTitle(),
'url' => $entry->getLink(),
'summary' => $entry->getDescription(),
'content' => $entry->getContent(),
'published_at' => $entry->getDateCreated(),
'updated_at' => $entry->getDateModified(),
]
);
}
Use Reader::import($url) only for prototypes or trusted internal URLs. For user-supplied URLs, fetch via Http first so you can validate the URL, limit size, set timeouts, and block private network targets.
Treat feed ingestion as a queued/scheduled job. External feeds are slow and unreliable.
Schedule::job(new RefreshExternalFeed($feed))->hourly()->withoutOverlapping();
Store ETag and Last-Modified response headers and send If-None-Match / If-Modified-Since on subsequent fetches where the upstream supports it.
Map RSS and Atom deliberately. RSS and Atom expose different strengths. RSS is broadly compatible and simple; Atom is better for IDs, updated timestamps, and richer content. Use export('rss') and export('atom') from the same service if you need both, and set Content-Type correctly.
Use absolute URLs everywhere. Feed readers do not reliably resolve relative links or relative image URLs. Use route() or url() for entry links, enclosure URLs, images, and self-links.
For podcasts, set enclosures explicitly. Podcast feeds usually fail because the enclosure is missing or malformed.
$entry->setEnclosure([
'uri' => $episode->audio_url,
'type' => 'audio/mpeg',
'length' => $episode->audio_size_in_bytes,
]);
The iTunes and PodcastIndex extensions are first-class in recent versions. Check the extension docs before hand-writing namespaced XML.
Normalize and persist consumed entries idempotently. Use the entry ID or link as a stable key. Do not assume all feeds provide both.
$key = $entry->getId() ?: $entry->getLink();
Test with fixtures and Laravel fakes. For the writer, parse $feed->export('rss') with simplexml_load_string() and assert node values. For the reader, use Http::fake() plus saved feed XML responses.
Reader::import(). It can become an SSRF vector. Validate scheme and host, block localhost/private IP ranges, set timeouts, limit response size, and prefer Http::get() + Reader::importString().setContent() as-is; the writer handles XML escaping. Pre-escaping causes double-encoded entities. Use strip_tags() for short descriptions when appropriate.setId() and date fields explicitly.addAuthor(['name' => ..., 'email' => ...]) is safest when producing RSS. Missing required author fields can produce invalid feeds.getContent(), getEnclosure(), and date getters can be null depending on the feed. Invalid date formats can throw exceptions since 2.13.1, so wrap ingestion in try/catch and log the feed URL.How can I help you explore Laravel packages today?