juststeveking/feed-parser
A simple Laravel package for fetching and parsing RSS and Atom feeds into a clean, consistent structure, so you can work with feed items and metadata through a friendly PHP API instead of handling raw XML yourself.
This is a 0.0.1 initial release, so treat the API as provisional and expect the surface to move before 1.0.
Install it:
composer require juststeveking/feed-parser
Pin the exact version ("juststeveking/feed-parser": "0.0.1") rather than using a caret constraint — see the Gotchas section on Composer's 0.0.x behaviour.
Read the source before the docs. At this stage the package is small enough that src/ is the documentation. Look for, in this order:
parse()/make() method, or a Parser class),Feed / Item value objects — these define the only fields you can rely on,Wire it up in a throwaway route or php artisan tinker session first:
$feed = FeedParser::parse('https://example.com/feed.xml');
$feed->title; // check which accessors actually exist
foreach ($feed->items as $item) {
dump($item->title, $item->link);
}
First real use case: a scheduled command that pulls one or two feeds you control and dumps the parsed structure to a log. That validates field names, date handling and encoding on real-world XML before you build an importer on top of it.
Wrap it in your own service, not scattered calls. Because this is pre-1.0, the value objects and method names may change between patch releases. Put a single class between your app and the package:
final class FeedReader
{
public function read(string $url): Feed
{
return Cache::remember("feed:{$url}", now()->addMinutes(15), fn () => FeedParser::parse($url));
}
}
Then a breaking change in 0.0.2 costs you one file, not thirty.
Import into a model with a stable identity key. Feeds re-publish items constantly; dedupe on the item GUID (RSS <guid>) or Atom <id>, falling back to the link:
foreach ($reader->read($url)->items as $item) {
Post::updateOrCreate(
['guid' => $item->id ?? $item->link],
[
'title' => $item->title,
'url' => $item->link,
'summary' => strip_tags($item->description ?? ''),
'posted_at' => $item->publishedAt,
],
);
}
Always cache. Parsing does a network round trip plus XML work; a 15–60 minute TTL per URL is almost always correct and protects you from hammering a third-party blog on every page load.
Normalise across formats yourself. RSS and Atom disagree on nearly every field name (see Gotchas). Build one small DTO that both formats collapse into, and let the rest of the app only ever see that DTO. This is the single highest-leverage integration tip for a package at this maturity.
Test with fixtures, not the network. Keep a handful of real .xml files in tests/Fixtures/ — one RSS 2.0, one Atom 1.0, one with CDATA, one with a non-UTF-8 declaration. Parse them from disk rather than over HTTP so the suite is hermetic and fast.
Drive it from a queued job, not a controller. Feed fetching is I/O with an unbounded tail latency; put it behind a queue with $tries and $backoff set, and record the last-seen ETag/Last-Modified if the package exposes response headers.
Composer treats 0.0.x as unstable. composer require juststeveking/feed-parser will likely resolve to ^0.0.1, which only allows >=0.0.1 <0.0.2. Any patch release will silently not be picked up by composer update, and conversely a hand-edit to a caret range can pull in an API change. Pin exactly and read the changelog on each bump. This is the number one hazard of adopting a 0.0.1.
XXE is the security item to check first. Before you point this at any untrusted URL, confirm the parser does not enable LIBXML_NOENT or external entity loading. XML parsers are a classic XXE vector, and a 0.0.1 feed library is exactly where that flag gets left in. If you can't confirm it, wrap libxml with libxml_disable_entity_loader() or parse behind a trusted set of hosts.
SSRF if the URL ever comes from user input. This package almost certainly accepts an arbitrary URL and fetches it. If that URL is user-supplied, you need to validate scheme, block private/link-local ranges and reject redirects to 169.254.169.254. Prefer fetching the XML yourself and passing the string, if the package supports a string/file entry point.
Http::fake() may not work. If the package uses cURL or file_get_contents rather than Laravel's Illuminate\Support\Http, Laravel's HTTP fake layer won't intercept it and tests will hit the network. Check the transport before you write your first test; if it's not the Laravel client, use local fixtures exclusively.
"Basic RSS and Atom" means gaps. Expect no JSON Feed, no iTunes/podcast namespaces, no Dublin Core (dc:creator, dc:date), no Media RSS, and possibly no <enclosure>/<content:encoded>. If your feed source is a podcast, a Substack, or anything with a namespace prefix, prototype against a real sample before committing to the package.
Cross-format field mismatches. RSS gives you pubDate and description; Atom gives you published/updated and summary/content, where content may be type="html" or type="xhtml" (nested markup, not a string). Every field access should be null-safe, and date handling should assume either a raw string or a DateTimeInterface depending on the format — verify against both fixture types.
Encoding lies. Feeds routinely declare encoding="ISO-8859-1" while containing UTF-8, or vice versa. Mojibake in titles is usually the source, not your database. Normalise with mb_convert_encoding after parsing, and store as utf8mb4.
Sanitise feed HTML before rendering. description/content is remote, attacker-influenced HTML. Run it through a purifier or strip_tags with an allowed list — never {!! !!} it raw into Blade.
No config file at 0.0.1. Don't expect publishable config for timeouts, retries, or user agent. Set those on your own HTTP client and feed the package a string/file where possible, or accept the defaults and add a hard timeout at the job level.
Extension points to watch for. The natural seam is format resolution (a parser-per-format registry). If the release exposes a way to register a custom parser or a custom Item hydration step, that's where you'd add JSON Feed or namespace support yourself rather than forking — and it's the feature most likely to arrive in a 0.1.0, so avoid deep coupling to the default parser.
How can I help you explore Laravel packages today?