
Short answer: To serve an RSS feed from a Laminas MVC application, install the laminas-feed component, add a literal route for /feed.xml, write a controller action that reads the newest published rows, fill a Feed object with an entry for each row and call its export method for RSS. Return the XML in a response with the application/rss+xml content type, add an ETag and a 304 answer for repeat requests, test the route with curl and a validator, then give the public address to an auto-poster so every new item reaches your social channels.
Laminas is the community-maintained successor of the Zend Framework, and it still runs a large number of business applications, portals and content platforms. Teams that maintain them know the strengths of the framework: a modular structure, a service container and a mature set of components, one of which writes and reads feeds. Sooner or later such an application publishes something that people want to follow: articles, release notes, offers, events or listings. A feed is the standard way to let readers, aggregators and auto-posters watch that content without an account or an API key. This guide builds a complete Laminas RSS feed with a clean query, stable identifiers, correct dates and conditional requests, explains each decision and ends with the checks that keep the feed dependable once other systems rely on it.
A feed is worth adding whenever the application publishes things that people outside it want to follow:
If your content already lives in a blog or a CMS, use the feed it provides instead; see how to find your WordPress feed link. If you use another PHP framework, the same idea is covered for Symfony e Laravel; only the controller style changes.
Add the feed component with Composer. It is a separate package, so it does not come with the skeleton application.
composer require laminas/laminas-feedThen register a literal route and the controller in the module configuration. Keep the address short and permanent, because auto-posters and readers store it. The public address of the site belongs in your configuration as well, under a key such as site.url. Behind a reverse proxy, the request may show an internal host name or a plain http scheme, and a feed full of internal addresses is useless to every reader.
use Laminas\Router\Http\Literal;
return [
'router' => [
'routes' => [
'feed' => [
'type' => Literal::class,
'options' => [
'route' => '/feed.xml',
'defaults' => [
'controller' => Controller\FeedController::class,
'action' => 'index',
],
],
],
],
],
'controllers' => [
'factories' => [
Controller\FeedController::class => Controller\FeedControllerFactory::class,
],
],
];The controller receives a PDO connection and the public address from a small factory. The action selects the newest published posts, builds the feed and answers. The condition on the publication time matters: without it, an item scheduled for tomorrow would appear in the feed today, and an auto-poster would share it before it exists on the website. Publication times are stored as UTC timestamps.
namespace Application\Controller;
use DateTime;
use PDO;
use Laminas\Feed\Writer\Feed;
use Laminas\Mvc\Controller\AbstractActionController;
final class FeedController extends AbstractActionController
{
public function __construct(private PDO $pdo, private string $siteUrl)
{
}
public function indexAction()
{
$stmt = $this->pdo->prepare(
'SELECT id, title, slug, summary, published_at FROM posts
WHERE status = :status AND published_at <= :now
ORDER BY published_at DESC LIMIT 20'
);
$stmt->execute(['status' => 'published', 'now' => time()]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$feed = new Feed();
$feed->setTitle('Example News');
$feed->setLink($this->siteUrl . '/');
$feed->setFeedLink($this->siteUrl . '/feed.xml', 'rss');
$feed->setDescription('The latest posts from Example News');
$feed->setLanguage('en');
$newest = $rows ? (int) $rows[0]['published_at'] : 0;
$feed->setDateModified(new DateTime('@' . $newest));
foreach ($rows as $row) {
$moment = new DateTime('@' . (int) $row['published_at']);
$entry = $feed->createEntry();
$entry->setTitle($row['title']);
$entry->setLink($this->siteUrl . '/' . $row['slug'] . '/');
$entry->setId('post-' . $row['id']);
$entry->setDateCreated($moment);
$entry->setDateModified($moment);
$entry->setDescription($row['summary']);
$feed->addEntry($entry);
}
$xml = $feed->export('rss');
$etag = '"' . hash('sha256', $xml) . '"';
$response = $this->getResponse();
$headers = $response->getHeaders();
$headers->addHeaderLine('ETag', $etag);
$headers->addHeaderLine('Cache-Control', 'public, max-age=300');
$sent = $this->getRequest()->getHeader('If-None-Match');
if ($sent && $sent->getFieldValue() === $etag) {
return $response->setStatusCode(304);
}
$headers->addHeaderLine('Content-Type', 'application/rss+xml; charset=utf-8');
return $response->setContent($xml);
}
}namespace Application\Controller;
use PDO;
use Psr\Container\ContainerInterface;
use Laminas\ServiceManager\Factory\FactoryInterface;
final class FeedControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null)
{
$config = $container->get('config');
return new FeedController($container->get(PDO::class), $config['site']['url']);
}
}Open the route in a browser or with curl. You should see well-formed XML with at most twenty items, newest first. The component builds the document with the DOM extension of PHP, so special characters in a title or a summary are encoded safely and the feed stays valid. Returning a response object from the action tells the framework to skip view rendering, which is exactly what a feed needs.
The ETag in the example is a hash of the finished XML, so the XML must not change unless the content changes. That is why the feed’s modification date is set from the newest entry and not from the clock. If a date were filled in with the current time, the hash would change on every request, the ETag would never match and the 304 answer would never be sent; every poll would then download the whole file. Always set such dates from the data, and request the same feed twice with curl to confirm that the two bodies are identical. The same rule applies to any generator that stamps the current time into its output, in any language.
| Feed field | Source in the example | Rule to keep |
|---|---|---|
| title | The title column | Plain text, no markup |
| link | Site address plus slug | Absolute address that returns 200 directly |
| guid | The row id with a prefix | Never changes, even if the slug changes |
| pubDate | The publication timestamp | RFC 822 format, never in the future |
| description | The summary column | One or two sentences that make sense alone |
The identifier is the most important decision. An auto-poster remembers which items it has already shared by their GUID. If the value changes when someone edits a slug or republishes an article, the same story is shared again. The writer marks an identifier that is not a valid address as a non-permanent link, so a database id with a fixed prefix is exactly right. The problem and its fixes are described in why a feed auto-posts the same article twice.
RSS dates follow RFC 822, with English abbreviations for days and months. The component accepts a DateTime object or a timestamp and writes the date in that shape, so you never format it by hand. Create the objects from UTC timestamps, as the example does with the at sign prefix, so that the zone in the output is predictable. Do not use ISO 8601 in an RSS document; that format belongs to Atom feeds, and many RSS readers reject it. If a validator complains about dates, read the guide to RSS date format errors.
Auto-posters, readers and search engines request a feed often, and almost always nothing has changed. The controller handles that with two small pieces. The ETag header carries a hash of the body, and a request whose If-None-Match header matches it receives a 304 answer with no body. The Cache-Control header allows shared caches to keep the response for five minutes. The background to both techniques is in the guide to ETags and conditional GET e the guide to feed caching.
Choose the cache period with care. A long period delays every new post by the same amount, because shared caches keep serving the old list until it expires. A few minutes is a good balance for most sites. If the table grows large, add an index on the status and publication columns so that the query stays fast, or store the rendered XML in the Laminas cache component for a short time.
In production, serve the application with PHP-FPM behind a web server that terminates HTTPS. Keep the public address in your own configuration rather than trusting forwarded headers, so that links in the feed are always correct. Check that the server does not strip the ETag and Cache-Control headers and that it does not add its own long cache that keeps old XML for too long. After every deployment, request the feed once to confirm that it still returns 200 and that the address is exactly the one you gave to the auto-poster. If the feed ever stops updating, this checklist shows what to inspect first.
An auto-poster is more useful when each item carries a picture. The entry object can carry an enclosure with the address, the length in bytes and the type of the image, and you can fill it from an image column. Use an absolute https address and a file of reasonable size. Without an image, many networks show a plain text card. How tools read these elements is explained in the guide to enclosure tags. If you want HTML inside the description, read the CDATA guide first.
There is a longer checklist in the feed validation guide and a short curl routine in the guide to debugging a feed with curl.
PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed route, connect your accounts and choose how each post is built from the title, the description or the page title. PostRSS checks the feed as often as every minute and shares each new item, with its image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, X, Bluesky, Mastodon and Telegram. Keyword filters, UTM parameters and posting windows let you tune each channel, and webhooks connect PostRSS to your own systems. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.
A Laminas application needs only a route, a controller action and the laminas-feed component to serve a dependable RSS feed: select published items dated in the past, let the component build the document, use a stable GUID, create dates from UTC timestamps, set the modification date from the newest entry and send the result with the right content type, an ETag and a short public cache. Test the route with curl and a validator, keep the address unchanged and connect it to an auto-poster. After that, every item you publish can reach your social channels automatically.
Yes, as a separate component. The laminas-feed package includes a writer that builds RSS and Atom documents from a feed object and entry objects, and it also contains a reader for feeds published by others.
It is its direct successor. Laminas continues the Zend Framework code under a new name, and the feed writer works in the same way, so older guides for the Zend component are still a good guide to the concepts.
Most often the body changes between requests, for example because a date is filled in with the current time. Set the modification date from the newest entry and the hash stays the same until the content changes.
Between ten and twenty of the newest published items is enough. An auto-poster only needs recent items, and a smaller file is faster to download and parse.
Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new items are shared as soon as they appear.
What changed in the networks, what broke, and how to fix it before it costs you reach.
Criadas pela Internet Solutions, a equipe por trás do PostRSS. Cada uma economiza seu tempo de um jeito diferente.