-
Notifications
You must be signed in to change notification settings - Fork 94
Symfony
This guide shows how to use php-feed with Symfony applications.
Add the package to your Symfony project:
composer require rumenx/php-feed
No bundle registration is needed - the package works out of the box!
Create a controller to handle RSS/Atom feeds:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Rumenx\Feed\FeedFactory;
use App\Repository\PostRepository;
class FeedController extends AbstractController
{
#[Route('/feed', name: 'feed_posts', methods: ['GET'])]
public function posts(PostRepository $postRepository): Response
{
$feed = FeedFactory::create();
$feed->setTitle($this->getParameter('app.name') . ' - Latest Posts');
$feed->setDescription('Latest blog posts');
$feed->setLink($this->generateUrl('homepage', [], urlType: 'absolute'));
$feed->setLanguage('en');
// Get latest published posts
$posts = $postRepository->findLatestPublished(20);
foreach ($posts as $post) {
$feed->addItem([
'title' => $post->getTitle(),
'author' => $post->getAuthor()->getName(),
'link' => $this->generateUrl('post_show', ['slug' => $post->getSlug()], urlType: 'absolute'),
'pubdate' => $post->getCreatedAt()->format('c'),
'description' => $post->getExcerpt(),
'category' => array_map(fn($cat) => $cat->getName(), $post->getCategories()->toArray())
]);
}
$xml = $feed->render('rss');
return new Response($xml, 200, [
'Content-Type' => 'application/rss+xml; charset=utf-8'
]);
}
}
Here's how to create feeds from Doctrine entities:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Rumenx\Feed\FeedFactory;
use App\Repository\PostRepository;
use App\Repository\NewsRepository;
use App\Repository\ProductRepository;
class FeedController extends AbstractController
{
#[Route('/feed/posts', name: 'feed_posts')]
public function posts(PostRepository $repository): Response
{
return $this->createFeed(
$repository->findLatestPublished(20),
'Blog Posts',
'Latest blog posts',
'post_show'
);
}
#[Route('/feed/news', name: 'feed_news')]
public function news(NewsRepository $repository): Response
{
return $this->createFeed(
$repository->findLatestPublished(20),
'News Updates',
'Latest news and updates',
'news_show'
);
}
#[Route('/feed/products', name: 'feed_products')]
public function products(ProductRepository $repository): Response
{
return $this->createFeed(
$repository->findLatestAvailable(50),
'New Products',
'Latest products in our store',
'product_show'
);
}
private function createFeed(array $items, string $title, string $description, string $route): Response
{
$feed = FeedFactory::create();
$feed->setTitle($this->getParameter('app.name') . ' - ' . $title);
$feed->setDescription($description);
$feed->setLink($this->generateUrl('homepage', [], urlType: 'absolute'));
$feed->setLanguage($this->getParameter('locale'));
foreach ($items as $item) {
$feed->addItem([
'title' => $item->getTitle(),
'author' => $item->getAuthor()?->getName() ?? 'Unknown',
'link' => $this->generateUrl($route, ['slug' => $item->getSlug()], urlType: 'absolute'),
'pubdate' => $item->getCreatedAt()->format('c'),
'description' => $item->getExcerpt() ?? $item->getDescription() ?? substr($item->getContent(), 0, 300),
'category' => array_map(fn($cat) => $cat->getName(), $item->getCategories()->toArray())
]);
}
return new Response($feed->render('rss'), 200, [
'Content-Type' => 'application/rss+xml; charset=utf-8'
]);
}
}
Example repository methods to fetch feed content:
<?php
namespace App\Repository;
use App\Entity\Post;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class PostRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Post::class);
}
/**
* @return Post[]
*/
public function findLatestPublished(int $limit = 20): array
{
return $this->createQueryBuilder('p')
->andWhere('p.published = :published')
->setParameter('published', true)
->orderBy('p.createdAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
/**
* @return Post[]
*/
public function findByCategory(string $categorySlug, int $limit = 20): array
{
return $this->createQueryBuilder('p')
->join('p.categories', 'c')
->andWhere('c.slug = :categorySlug')
->andWhere('p.published = :published')
->setParameter('categorySlug', $categorySlug)
->setParameter('published', true)
->orderBy('p.createdAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
}
Configure your feed routes in config/routes.yaml
:
# config/routes.yaml
feed_posts:
path: /feed
controller: App\Controller\FeedController::posts
methods: [GET]
feed_rss:
path: /feed.rss
controller: App\Controller\FeedController::posts
methods: [GET]
feed_xml:
path: /feed.xml
controller: App\Controller\FeedController::posts
methods: [GET]
feed_atom:
path: /feed.atom
controller: App\Controller\FeedController::atom
methods: [GET]
# Category-specific feeds
feed_category:
path: /feed/category/{slug}
controller: App\Controller\FeedController::category
methods: [GET]
requirements:
slug: '[a-z0-9-]+'
Add feed discovery links to your base template:
In templates/base.html.twig
:
<!DOCTYPE html>
<html lang="{{ app.request.locale }}">
<head>
<meta charset="UTF-8">
<title>{% block title %}{{ app_name }}{% endblock %}</title>
{# Feed Discovery Links #}
<link rel="alternate" type="application/rss+xml" title="{{ app_name }} - RSS Feed" href="{{ url('feed_rss') }}">
<link rel="alternate" type="application/atom+xml" title="{{ app_name }} - Atom Feed" href="{{ url('feed_atom') }}">
{% block stylesheets %}{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
Create a dedicated service for feed generation:
<?php
namespace App\Service;
use Rumenx\Feed\FeedFactory;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class FeedService
{
public function __construct(
private UrlGeneratorInterface $urlGenerator,
private string $appName,
private string $appLocale = 'en'
) {}
public function createFeed(string $title, string $description, string $link = ''): object
{
$feed = FeedFactory::create();
$feed->setTitle($this->appName . ' - ' . $title);
$feed->setDescription($description);
$feed->setLink($link ?: $this->urlGenerator->generate('homepage', [], UrlGeneratorInterface::ABSOLUTE_URL));
$feed->setLanguage($this->appLocale);
return $feed;
}
public function addEntityItem(object $feed, object $entity, string $route, array $routeParams = []): void
{
$feed->addItem([
'title' => $entity->getTitle(),
'author' => $entity->getAuthor()?->getName() ?? 'Unknown',
'link' => $this->urlGenerator->generate($route, $routeParams, UrlGeneratorInterface::ABSOLUTE_URL),
'pubdate' => $entity->getCreatedAt()->format('c'),
'description' => $entity->getExcerpt() ?? $entity->getDescription() ?? '',
'category' => $this->extractCategories($entity)
]);
}
private function extractCategories(object $entity): array
{
if (method_exists($entity, 'getCategories')) {
return array_map(fn($cat) => $cat->getName(), $entity->getCategories()->toArray());
}
return [];
}
}
Register the service in config/services.yaml
:
# config/services.yaml
services:
App\Service\FeedService:
arguments:
$appName: '%app.name%'
$appLocale: '%locale%'
Use the service in your controller:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Service\FeedService;
use App\Repository\PostRepository;
class FeedController extends AbstractController
{
#[Route('/feed', name: 'feed_posts')]
public function posts(FeedService $feedService, PostRepository $postRepository): Response
{
$feed = $feedService->createFeed('Latest Posts', 'Latest blog posts from our website');
$posts = $postRepository->findLatestPublished(20);
foreach ($posts as $post) {
$feedService->addEntityItem($feed, $post, 'post_show', ['slug' => $post->getSlug()]);
}
return new Response($feed->render('rss'), 200, [
'Content-Type' => 'application/rss+xml; charset=utf-8'
]);
}
}
Example with Symfony's cache component:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Rumenx\Feed\FeedFactory;
use App\Repository\PostRepository;
class FeedController extends AbstractController
{
#[Route('/feed', name: 'feed_posts')]
public function posts(CacheInterface $cache, PostRepository $postRepository): Response
{
$feedXml = $cache->get('blog_feed_rss', function (ItemInterface $item) use ($postRepository) {
$item->expiresAfter(3600); // Cache for 1 hour
return $this->generatePostsFeed($postRepository);
});
return new Response($feedXml, 200, [
'Content-Type' => 'application/rss+xml; charset=utf-8',
'Cache-Control' => 'public, max-age=3600'
]);
}
private function generatePostsFeed(PostRepository $postRepository): string
{
$feed = FeedFactory::create();
$feed->setTitle($this->getParameter('app.name') . ' - Blog Posts');
$feed->setDescription('Latest posts from our blog');
$feed->setLink($this->generateUrl('homepage', [], urlType: 'absolute'));
$posts = $postRepository->findLatestPublished(20);
foreach ($posts as $post) {
$feed->addItem([
'title' => $post->getTitle(),
'author' => $post->getAuthor()->getName(),
'link' => $this->generateUrl('post_show', ['slug' => $post->getSlug()], urlType: 'absolute'),
'pubdate' => $post->getCreatedAt()->format('c'),
'description' => $post->getExcerpt()
]);
}
return $feed->render('rss');
}
}
Create a podcast feed with audio files:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Rumenx\Feed\FeedFactory;
use App\Repository\EpisodeRepository;
class PodcastController extends AbstractController
{
#[Route('/podcast/feed', name: 'podcast_feed')]
public function feed(EpisodeRepository $episodeRepository): Response
{
$feed = FeedFactory::create();
$feed->setTitle('My Podcast');
$feed->setDescription('Weekly episodes about technology and programming');
$feed->setLink($this->generateUrl('podcast_index', [], urlType: 'absolute'));
$feed->setLanguage('en');
$episodes = $episodeRepository->findPublished(50);
foreach ($episodes as $episode) {
$feed->addItem([
'title' => $episode->getTitle(),
'author' => 'Podcast Host',
'link' => $this->generateUrl('podcast_episode', ['slug' => $episode->getSlug()], urlType: 'absolute'),
'pubdate' => $episode->getPublishedAt()->format('c'),
'description' => $episode->getDescription(),
'enclosure' => [
'url' => $episode->getAudioUrl(),
'type' => 'audio/mpeg',
'length' => $episode->getFileSize()
]
]);
}
return new Response($feed->render('rss'), 200, [
'Content-Type' => 'application/rss+xml; charset=utf-8'
]);
}
}
Create a console command to generate static feed files:
<?php
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
use Rumenx\Feed\FeedFactory;
use App\Repository\PostRepository;
#[AsCommand(
name: 'feeds:generate',
description: 'Generate static RSS and Atom feed files'
)]
class GenerateFeedsCommand extends Command
{
public function __construct(
private PostRepository $postRepository,
private string $publicDir
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$filesystem = new Filesystem();
$io->title('Generating RSS feeds');
// Generate RSS feed
$rssFeed = $this->createFeed('rss');
$filesystem->dumpFile($this->publicDir . '/feed.rss', $rssFeed);
// Generate Atom feed
$atomFeed = $this->createFeed('atom');
$filesystem->dumpFile($this->publicDir . '/feed.atom', $atomFeed);
$io->success('Feeds generated successfully!');
return Command::SUCCESS;
}
private function createFeed(string $format): string
{
$feed = FeedFactory::create();
$feed->setTitle('My Blog');
$feed->setDescription('Latest posts from my blog');
$feed->setLink('https://example.com');
$posts = $this->postRepository->findLatestPublished(20);
foreach ($posts as $post) {
$feed->addItem([
'title' => $post->getTitle(),
'author' => $post->getAuthor()->getName(),
'link' => 'https://example.com/posts/' . $post->getSlug(),
'pubdate' => $post->getCreatedAt()->format('c'),
'description' => $post->getExcerpt()
]);
}
return $feed->render($format);
}
}
Example test for your feed controller:
<?php
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use App\Entity\Post;
use App\Entity\User;
class FeedControllerTest extends WebTestCase
{
public function testPostsFeedReturnsValidXml(): void
{
$client = static::createClient();
// Create test data
$entityManager = $client->getContainer()->get('doctrine')->getManager();
$author = new User();
$author->setName('Test Author');
$entityManager->persist($author);
$post = new Post();
$post->setTitle('Test Post');
$post->setContent('Test content');
$post->setAuthor($author);
$post->setPublished(true);
$entityManager->persist($post);
$entityManager->flush();
// Request feed
$crawler = $client->request('GET', '/feed');
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/rss+xml; charset=utf-8');
// Verify XML structure
$xml = simplexml_load_string($client->getResponse()->getContent());
$this->assertNotFalse($xml);
$this->assertEquals('rss', $xml->getName());
$this->assertGreaterThan(0, count($xml->channel->item));
}
}
Add feed-related parameters to your config/services.yaml
:
# config/services.yaml
parameters:
app.name: 'My Symfony Blog'
app.description: 'A blog about Symfony and PHP'
app.url: 'https://myblog.com'
services:
# ... other services
- Learn about Caching strategies
- See Custom Views for custom Twig templates
- Check out Advanced Features for more options