Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Rss Writer Laravel Package

suin/php-rss-writer

A lightweight PHP library for generating RSS 2.0 feeds. Build channels and items with a clean, fluent API and output valid XML. Handles enclosures, categories, pubDates, and GUIDs, installs via Composer, and fits into any PHP project.

Deep Wiki
Context7

Getting Started

Install the package with Composer. It has no Laravel service provider, facade, or config file — it is a plain PHP library you instantiate yourself.

composer require suin/php-rss-writer

Look first at the package README in vendor/suin/php-rss-writer/ and skim its small src/ directory. The entire public API is three classes: Suin\RSSWriter\Feed, Suin\RSSWriter\Channel, and Suin\RSSWriter\Item. The pattern is always the same: configure an object with chainable setters, then call appendTo() to attach it to its parent.

A minimal blog feed endpoint:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Suin\RSSWriter\Channel;
use Suin\RSSWriter\Feed;
use Suin\RSSWriter\Item;

class FeedController extends Controller
{
    public function __invoke()
    {
        $feed = new Feed();

        $channel = new Channel();
        $channel
            ->title(config('app.name'))
            ->description('Latest posts')
            ->url(config('app.url'))
            ->language('en')
            ->lastBuildDate(now()->timestamp)
            ->appendTo($feed);

        Post::published()
            ->latest('published_at')
            ->limit(50)
            ->get()
            ->each(function (Post $post) use ($channel) {
                $item = new Item();
                $item
                    ->title($post->title)
                    ->description($post->excerpt)
                    ->url(route('posts.show', $post))
                    ->guid(route('posts.show', $post), true)
                    ->pubDate($post->published_at->timestamp)
                    ->appendTo($channel);
            });

        return response($feed->render(), 200, [
            'Content-Type' => 'application/rss+xml; charset=UTF-8',
        ]);
    }
}

Register the route and add a discovery link in your layout:

Route::get('feed.xml', FeedController::class)->name('feed');
<link rel="alternate" type="application/rss+xml" title="RSS" href="{{ route('feed') }}">

That is the first use case: expose your Eloquent posts as a valid RSS 2.0 document. Everything else is a variation on mapping models to Item objects.

Implementation Patterns

Extract a feed builder. Do not let a controller grow into your feed definition. Create app/Feeds/PostFeed.php with a build(): Feed method, and keep the controller responsible only for HTTP concerns. This makes the same feed reusable from a controller, an Artisan command, and tests.

namespace App\Feeds;

use App\Models\Post;
use Suin\RSSWriter\Channel;
use Suin\RSSWriter\Feed;
use Suin\RSSWriter\Item;

class PostFeed
{
    public function build(): Feed
    {
        $feed = new Feed();
        $channel = new Channel();

        $channel
            ->title(config('app.name'))
            ->description('Latest posts')
            ->url(config('app.url'))
            ->language('en')
            ->appendTo($feed);

        Post::published()->latest('published_at')->limit(50)->get()
            ->each(fn (Post $post) => $this->addItem($channel, $post));

        return $feed;
    }

    private function addItem(Channel $channel, Post $post): void
    {
        $item = new Item();
        $item
            ->title($post->title)
            ->description($post->excerpt)
            ->url(route('posts.show', $post))
            ->guid(route('posts.show', $post), true)
            ->pubDate($post->published_at->timestamp)
            ->appendTo($channel);
    }
}

Cache the rendered XML, not the objects. Rendering is cheap but the database query is not, and feed readers poll aggressively. Cache the string returned by render() and bust it from model events.

$xml = Cache::remember('rss:posts', now()->addMinutes(10), fn () => $this->feed->build()->render());

return response($xml, 200, ['Content-Type' => 'application/rss+xml; charset=UTF-8']);
// In the Post model
protected static function booted(): void
{
    static::saved(fn () => Cache::forget('rss:posts'));
    static::deleted(fn () => Cache::forget('rss:posts'));
}

Generate a static file when you want zero runtime cost. A scheduled command writing public/feed.xml is a good fit for high-traffic sites, CDN offloading, or feeds that change infrequently.

File::put(public_path('feed.xml'), app(PostFeed::class)->build()->render());
// routes/console.php or a scheduler
$schedule->call(fn () => Artisan::call('feed:generate'))->hourly();

Build parameterized feeds for categories, tags, or authors. Give the builder a query scope or an applyFilters() method, then register separate routes such as /feed.xml, /tags/{tag}/feed.xml, and /authors/{user}/feed.xml. Reuse the same Channel/Item mapping; only the query changes.

Use enclosure() for podcasts. The package supports the RSS <enclosure> element on items, which is enough for basic podcast distribution.

$item
    ->title($episode->title)
    ->description($episode->summary)
    ->url(route('episodes.show', $episode))
    ->guid(route('episodes.show', $episode), true)
    ->pubDate($episode->published_at->timestamp)
    ->enclosure($episode->audio_url, $episode->size_in_bytes, 'audio/mpeg')
    ->appendTo($channel);

For full Apple Podcasts support you will need iTunes-specific tags (itunes:author, itunes:category, itunes:image, itunes:duration), which are not first-class methods in this package. Post-process the rendered XML with DOMDocument and createElementNS() if you need them.

Test the response, not the library. The package is small and predictable, so assert your HTTP contract instead of unit-testing Feed internals.

$response = $this->get(route('feed'));

$response->assertOk();
$response->assertHeader('Content-Type', 'application/rss+xml; charset=UTF-8');
$this->assertStringContainsString('<item>', $response->getContent());
$this->assertStringContainsString(route('posts.show', $post), $response->getContent());

Validate real output with the W3C Feed Validation Service before shipping; it catches missing channel metadata and malformed dates that tests will not.

Gotchas and Tips

pubDate() and lastBuildDate() expect Unix timestamps, not Carbon instances or date strings. Passing a Carbon object can produce a type error or a silently wrong date. Always use ->timestamp or ->getTimestamp(). The library formats the value itself; do not pre-format it.

The second argument to guid() is the isPermaLink flag. If you use a URL, pass true. If you use a database ID or UUID, pass false, otherwise readers may try to follow a non-URL as a link.

Description values are XML-escaped, not treated as HTML. The library builds output through SimpleXML, so passing <p>Hello</p> from a rich-text editor will be escaped and may appear as literal markup in some readers. Use a plain-text excerpt, run strip_tags(), or post-process the XML if you genuinely need HTML content. Do not assume CDATA is added for you.

Use absolute URLs everywhere. route() and url() are correct; '/posts/1' is not. Feed readers, podcast clients, and aggregators frequently reject or mishandle relative links.

Set the Content-Type header explicitly. If you return the rendered string directly from a controller, Laravel defaults to text/html. Use response($xml, 200, ['Content-Type' => 'application/rss+xml; charset=UTF-8']) or ->header(...). Avoid echo inside a controller, which bypasses middleware and header handling.

This package is stale. The last release was in 2017. It is simple enough that it generally keeps working on modern PHP, but it is not actively maintained and has no Laravel integration. Test it on your PHP version, especially on PHP 8.x. For new Laravel projects, weigh it against spatie/laravel-feed (Laravel-native, discovery links, more active) or a maintained feed library. Choose this one when you want a tiny dependency and full manual control over the XML.

Limit the number of items. The feed is assembled in memory and rendered in one pass. Fifty items is typical; several thousand is not. If you must expose a large archive, paginate or generate static segment files.

Item order matters. Append newest first — latest('published_at') — because many readers display items in document order. There is no automatic sorting.

Watch your application timezone only for human-readable output. Unix timestamps are absolute, so pubDate() is safe across timezones. The risk is passing a localized date string instead of a timestamp.

Empty channels are valid XML but poor RSS. A feed with no items may still render, but validators and some readers complain. Decide whether to return an empty channel, a 404, or a "no posts yet" placeholder.

Extension points are limited. The classes are plain PHP objects and can be wrapped in your builder, but adding new XML namespaces or complex child elements is easier by post-processing the rendered string with DOMDocument than by subclassing Item or Channel. Keep that post-processing in one place so it remains testable.

Name collisions are possible but rare. Suin\RSSWriter\Feed and Item are generic names; alias them in files that also reference domain models called Item or Feed, or import them with use ... as RssFeed.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager