php-feed-io/feed-io
feed-io is a PHP library to read and write RSS, Atom, and JSONFeed feeds. It offers auto-discovery, PSR-7 responses with cache headers, PSR-18 HTTP client integration, a CLI, logging, and content filtering for only the newest items.
Architecture fit. feed-io is a framework-agnostic PHP library, not a Laravel package. There is no service provider, no facade, no config file, and no published config. It integrates through constructor injection on a single facade class (FeedIo\FeedIo) that takes exactly two collaborators: an HTTP client adapter and a PSR-3 logger. That is actually a clean fit for Laravel's container — we bind the facade as a singleton in an application service provider and let the container supply Illuminate\Log\Logger (which satisfies PSR-3) and a PSR-18 client (Guzzle 7 ships with Laravel and implements PSR-18). The cost is that we own the wrapper: roughly one provider, one config array (user agent, timeouts, redirect policy), and one thin domain interface. Budget this as internal glue, not zero-effort adoption.
The capability surface is well matched to a typical Laravel content/product workload: read RSS/Atom/JSONFeed with format auto-detection, HTML <link rel="alternate"> discovery, conditional GET via If-Modified-Since, getItemsSince() and a filter chain for incremental consumption, getNextUpdate() for poll scheduling, PSR-7 response generation with computed cache headers, and enclosure/media support for podcast-style payloads. Read and write are both in scope, so one dependency covers ingestion and syndication.
Integration feasibility. Low technical friction. composer require php-feed-io/feed-io on PHP 8.1+ pulls v6.x; runtime deps are only psr/log plus a PSR-18 client we already have. A working read path is ~15 lines. The main feasibility caveat is documentation drift: the README's primary example still references the removed FeedIo\Factory in places and shows both the deprecated FeedIo\Adapter\Guzzle\Client and the current generic FeedIo\Adapter\Http\Client. Expect developers to copy the wrong one. Treat the source (src/FeedIo/Reader/Result.php, the adapter directory) as more authoritative than the README for anything non-trivial.
Technical risk. Ranked by what would actually hurt:
alexdebril/feed-io, which has orders of magnitude more adoption. Before adopting, confirm which repository is canonical, who the maintainers are, and whether this is a rename, a fork, or a competing fork. This is a blocking question, not a footnote — it determines whether we inherit a decade of field-hardened parsing or an unvetted reimplementation.TypeError, which is a reminder that hostile input hardening is ongoing.getDateTimeBuilder()->setFeedTimezone() is process-wide and must be reset. In queue workers, Octane, or any long-lived process this can silently corrupt every subsequent feed's timestamps. Enforce a try/finally in our wrapper.Key questions for the maintainer/eval spike. Which repo is canonical and what is the release cadence commitment? Is there a security disclosure process? What is the JSONFeed spec version coverage (1.0 vs 1.1)? Does conditional GET correctly persist and replay ETag/Last-Modified across calls? Is there a maximum response size guard? How are namespaced extensions (iTunes, Dublin Core) round-tripped? What is the observed behavior on truncated/partial documents?
Position: conditional adopt for evaluation, not blanket adopt. Opportunity score of 53 with this risk profile reads as "underused but unproven" rather than "hidden gem." Proceed to a time-boxed spike, gated on resolving question 1.
Stack fit. Target PHP 8.1+ and pin ^6.0. Do not support the 4.x/5.x lines — the 6.0 release removed the Factory, so the API surface differs and half the internet's tutorials are stale. Compose a single FeedIoServiceProvider that registers FeedIo\FeedIo as a singleton with a configured PSR-18 client (explicit connect/read timeouts, capped redirects, a descriptive User-Agent we can identify in third-party logs) and the framework logger. Expose config keys for timeout, user agent, max redirects, and cache TTL — not for everything, since every config key is a support surface.
Wrap the library behind a small internal interface (FeedReader, FeedWriter) rather than type-hinting FeedIo in application code. Two methods return our own DTOs. This is cheap now and is the difference between a one-week swap and a two-month migration if the provenance question comes back badly.
Migration path. If we are already on SimplePie, Laminas\Feed, or willvincent/feeds, migrate read paths first in a single ingestion service, leaving write paths untouched. Expect these specific API deltas: no Factory (instantiate FeedIo directly), no FeedIo\Adapter\Guzzle\Client (use FeedIo\Adapter\Http\Client wrapping any PSR-18 client), and Result::getItemsSince() / getFilteredItems() replacing older manual date filtering. Any vendor code or blog snippet referencing Factory will fail at runtime, not at autoload — catch this in review.
Compatibility. Three seams need explicit handling:
Http facade, which is not PSR-18; use a dedicated Guzzle client so we can set timeouts and handler stack independently of the app's default client.getPsrResponse() returns a PSR-7 response with accurate Last-Modified/ETag headers. For the common case, prefer toAtom()/toRss() or getPsrResponse() and then copy body plus headers into an Illuminate\Http\Response. Do not blindly return the PSR-7 object; convert deliberately so cache headers and Content-Type survive.Sequencing.
try/finally timezone reset.next_update_at sourced from getNextUpdate(). Idempotent item storage keyed on guid/id with a fallback hash of link + published date.Total roughly 8–11 engineering days for a production-grade ingestion feature, of which the library itself accounts for maybe two.
Maintenance. MIT licensed, changelog and release notes are disciplined, and dependency updates are automated (Dependabot, with a CI check that changelogs are updated — a good hygiene signal). Releases are dependency-bump-heavy with occasional small features and bug fixes; v6.3.x is essentially maintenance. The real maintenance risk is not abandonment but fork ambiguity: if this is a continuation repo, we should pin an exact version and mirror it internally via a Composer VCS or Satis entry so an upstream disappearance is survivable. Set a quarterly review to re-check star count, dependents, and release cadence as a proxy for health. Given 0 dependents, treat any future release as unreviewed until read.
Support. Community discussions and GitHub issues only; no vendor SLA. Nothing in the package is likely to be a production incident source on its own, but when it is, we will be reading source. Assign one engineer as owner and write a one-page internal runbook covering the wrapper, the failure taxonomy below, and the version-pinning policy. Do not let this become an unowned dependency in the ingestion path.
Scaling. The work is outbound I/O and XML parsing — never do it in a user request cycle. One queued job per feed with concurrency limits, and use getNextUpdate() to schedule the next poll rather than fixed intervals; that is the package's most operationally valuable feature and it is easy to leave on the table. A sleepy-feed heuristic means dormant feeds cost almost nothing. Persist ETag/Last-Modified per feed and replay them so conditional GET actually saves bandwidth — the library will send the header but it will not remember it for us. For large feed volumes, shard by poll schedule and cap per-run item counts. Memory scales with document size; enforce a response size ceiling before parsing.
Failure modes to instrument. Malformed or truncated XML/JSON (parse exception — isolate per feed, never fail the batch); HTTP 4xx/5xx and redirect loops (mark feed unhealthy after N consecutive failures, back off exponentially, surface in an admin view); encoding mismatches and mojibake in titles; missing timezones producing wrong publish dates (the global setFeedTimezone leak is the sharp edge — audit that the reset always runs); duplicate items from inconsistent guid usage across publishers (dedupe on a composite hash, not on link alone); oversized payloads; and silent empty results that are indistinguishable from a healthy feed with no new items — log item counts explicitly so "feed broke three weeks ago" is detectable. Wrap all outbound calls in a circuit breaker per host so one dead publisher cannot exhaust worker capacity.
Ramp-up. Low for a mid-level Laravel engineer: half a day to a productive read path, one day to understand the Result/filter/scheduling model. The friction is entirely documentation-shaped — README examples reference removed APIs, and the "right" pattern (generic Http\Client + PSR-18, no Factory) is stated in prose rather than in the first code block. Have the spike owner write a 30-line internal "how we use feed-io" snippet and link it from the provider; that single artifact will save more time than the upstream docs.
How can I help you explore Laravel packages today?