-
Notifications
You must be signed in to change notification settings - Fork 94
Custom Views
Rumen Damyanov edited this page Jul 29, 2025
·
1 revision
This guide shows how to create and use custom view templates for RSS and Atom feeds, allowing you to customize the XML output format.
The php-feed package uses template views to generate XML output. You can:
- Customize existing RSS/Atom templates
- Create entirely new feed formats
- Add custom namespaces and elements
- Implement conditional formatting
Create a custom RSS template:
<?php
// custom-rss.blade.php (for Laravel) or custom-rss.php (for plain PHP)
?>
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title><?= htmlspecialchars($channel['title']) ?></title>
<description><?= htmlspecialchars($channel['description']) ?></description>
<link><?= htmlspecialchars($channel['link']) ?></link>
<atom:link href="<?= htmlspecialchars($channel['link']) ?>" rel="self" type="application/rss+xml" />
<?php if (isset($channel['language'])): ?>
<language><?= htmlspecialchars($channel['language']) ?></language>
<?php endif; ?>
<?php if (isset($channel['copyright'])): ?>
<copyright><?= htmlspecialchars($channel['copyright']) ?></copyright>
<?php endif; ?>
<lastBuildDate><?= date('r') ?></lastBuildDate>
<generator>PHP Feed Generator</generator>
<?php foreach ($items as $item): ?>
<item>
<title><?= htmlspecialchars($item['title']) ?></title>
<description><![CDATA[<?= $item['description'] ?>]]></description>
<link><?= htmlspecialchars($item['link']) ?></link>
<guid isPermaLink="true"><?= htmlspecialchars($item['link']) ?></guid>
<pubDate><?= date('r', strtotime($item['pubdate'])) ?></pubDate>
<?php if (isset($item['author'])): ?>
<dc:creator><?= htmlspecialchars($item['author']) ?></dc:creator>
<?php endif; ?>
<?php if (isset($item['category']) && is_array($item['category'])): ?>
<?php foreach ($item['category'] as $category): ?>
<category><?= htmlspecialchars($category) ?></category>
<?php endforeach; ?>
<?php endif; ?>
<?php if (isset($item['content'])): ?>
<content:encoded><![CDATA[<?= $item['content'] ?>]]></content:encoded>
<?php endif; ?>
<?php if (isset($item['enclosure'])): ?>
<enclosure url="<?= htmlspecialchars($item['enclosure']['url']) ?>"
length="<?= $item['enclosure']['length'] ?>"
type="<?= htmlspecialchars($item['enclosure']['type']) ?>" />
<?php endif; ?>
</item>
<?php endforeach; ?>
</channel>
</rss>
<?php
// custom-atom.blade.php or custom-atom.php
?>
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:georss="http://www.georss.org/georss"
xmlns:media="http://search.yahoo.com/mrss/">
<title><?= htmlspecialchars($channel['title']) ?></title>
<subtitle><?= htmlspecialchars($channel['description']) ?></subtitle>
<link href="<?= htmlspecialchars($channel['link']) ?>" />
<link href="<?= htmlspecialchars($channel['feedUrl'] ?? $channel['link']) ?>" rel="self" />
<id><?= htmlspecialchars($channel['link']) ?></id>
<updated><?= date('c') ?></updated>
<?php if (isset($channel['author'])): ?>
<author>
<name><?= htmlspecialchars($channel['author']) ?></name>
<?php if (isset($channel['authorEmail'])): ?>
<email><?= htmlspecialchars($channel['authorEmail']) ?></email>
<?php endif; ?>
</author>
<?php endif; ?>
<generator uri="https://github.com/RumenDamyanov/php-feed">PHP Feed Generator</generator>
<?php foreach ($items as $item): ?>
<entry>
<title><?= htmlspecialchars($item['title']) ?></title>
<link href="<?= htmlspecialchars($item['link']) ?>" />
<id><?= htmlspecialchars($item['id'] ?? $item['link']) ?></id>
<updated><?= date('c', strtotime($item['pubdate'])) ?></updated>
<?php if (isset($item['author'])): ?>
<author>
<name><?= htmlspecialchars($item['author']) ?></name>
</author>
<?php endif; ?>
<summary type="html"><![CDATA[<?= $item['description'] ?>]]></summary>
<?php if (isset($item['content'])): ?>
<content type="html"><![CDATA[<?= $item['content'] ?>]]></content>
<?php endif; ?>
<?php if (isset($item['category']) && is_array($item['category'])): ?>
<?php foreach ($item['category'] as $category): ?>
<category term="<?= htmlspecialchars($category) ?>" />
<?php endforeach; ?>
<?php endif; ?>
<?php if (isset($item['location'])): ?>
<georss:point><?= htmlspecialchars($item['location']['lat']) ?> <?= htmlspecialchars($item['location']['lng']) ?></georss:point>
<?php endif; ?>
<?php if (isset($item['media'])): ?>
<media:content url="<?= htmlspecialchars($item['media']['url']) ?>"
type="<?= htmlspecialchars($item['media']['type']) ?>"
medium="<?= htmlspecialchars($item['media']['medium']) ?>" />
<?php endif; ?>
<?php if (isset($item['thumbnail'])): ?>
<media:thumbnail url="<?= htmlspecialchars($item['thumbnail']) ?>" />
<?php endif; ?>
</entry>
<?php endforeach; ?>
</feed>
<?php
require 'vendor/autoload.php';
use Rumenx\Feed\FeedFactory;
class CustomViewFeed
{
private string $viewsPath;
public function __construct(string $viewsPath = './views')
{
$this->viewsPath = $viewsPath;
}
public function generateFeed(string $format = 'rss'): string
{
$feed = FeedFactory::create([
'views' => [
'rss' => $this->viewsPath . '/custom-rss.php',
'atom' => $this->viewsPath . '/custom-atom.php',
'json' => $this->viewsPath . '/custom-json.php'
]
]);
$feed->setTitle('My Custom Blog');
$feed->setDescription('A blog with custom feed formatting');
$feed->setLink('https://example.com');
// Add items with custom properties
$feed->addItem([
'title' => 'Post with Custom Elements',
'author' => 'John Doe',
'link' => 'https://example.com/post-1',
'pubdate' => date('c'),
'description' => 'Short description',
'content' => '<p>Full HTML content here...</p>',
'category' => ['PHP', 'Web Development'],
'location' => ['lat' => '40.7128', 'lng' => '-74.0060'],
'media' => [
'url' => 'https://example.com/media/video.mp4',
'type' => 'video/mp4',
'medium' => 'video'
],
'thumbnail' => 'https://example.com/thumbnails/post-1.jpg'
]);
return $feed->render($format);
}
}
// Usage
$customFeed = new CustomViewFeed('./templates');
$format = $_GET['format'] ?? 'rss';
header('Content-Type: application/' . ($format === 'json' ? 'json' : 'xml') . '; charset=utf-8');
echo $customFeed->generateFeed($format);
Create a JSON feed view:
<?php
// custom-json.php
header('Content-Type: application/json; charset=utf-8');
$jsonFeed = [
'version' => 'https://jsonfeed.org/version/1.1',
'title' => $channel['title'],
'description' => $channel['description'],
'home_page_url' => $channel['link'],
'feed_url' => $channel['feedUrl'] ?? $channel['link'],
'language' => $channel['language'] ?? 'en',
'items' => []
];
if (isset($channel['author'])) {
$jsonFeed['authors'] = [
['name' => $channel['author']]
];
}
foreach ($items as $item) {
$jsonItem = [
'id' => $item['id'] ?? $item['link'],
'title' => $item['title'],
'content_html' => $item['content'] ?? $item['description'],
'summary' => $item['description'],
'url' => $item['link'],
'date_published' => date('c', strtotime($item['pubdate']))
];
if (isset($item['author'])) {
$jsonItem['authors'] = [['name' => $item['author']]];
}
if (isset($item['category']) && is_array($item['category'])) {
$jsonItem['tags'] = $item['category'];
}
if (isset($item['thumbnail'])) {
$jsonItem['image'] = $item['thumbnail'];
}
if (isset($item['attachments'])) {
$jsonItem['attachments'] = $item['attachments'];
}
$jsonFeed['items'][] = $jsonItem;
}
echo json_encode($jsonFeed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Using Laravel's Blade templating:
<?php
// resources/views/feeds/custom-rss.blade.php
?>
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<title>{{ $channel['title'] }}</title>
<description>{{ $channel['description'] }}</description>
<link>{{ $channel['link'] }}</link>
<atom:link href="{{ $channel['feedUrl'] ?? $channel['link'] }}" rel="self" type="application/rss+xml" />
@isset($channel['language'])
<language>{{ $channel['language'] }}</language>
@endisset
@isset($channel['image'])
<image>
<url>{{ $channel['image']['url'] }}</url>
<title>{{ $channel['image']['title'] ?? $channel['title'] }}</title>
<link>{{ $channel['image']['link'] ?? $channel['link'] }}</link>
</image>
@endisset
<lastBuildDate>{{ now()->toRfc2822String() }}</lastBuildDate>
<generator>{{ config('app.name') }} Feed Generator</generator>
@foreach($items as $item)
<item>
<title>{{ $item['title'] }}</title>
<description><![CDATA[{!! $item['description'] !!}]]></description>
<link>{{ $item['link'] }}</link>
<guid isPermaLink="true">{{ $item['link'] }}</guid>
<pubDate>{{ \Carbon\Carbon::parse($item['pubdate'])->toRfc2822String() }}</pubDate>
@isset($item['author'])
<dc:creator>{{ $item['author'] }}</dc:creator>
@endisset
@isset($item['category'])
@if(is_array($item['category']))
@foreach($item['category'] as $category)
<category>{{ $category }}</category>
@endforeach
@else
<category>{{ $item['category'] }}</category>
@endif
@endisset
@isset($item['content'])
<content:encoded><![CDATA[{!! $item['content'] !!}]]></content:encoded>
@endisset
@isset($item['media'])
<media:content url="{{ $item['media']['url'] }}"
type="{{ $item['media']['type'] }}"
@isset($item['media']['fileSize'])fileSize="{{ $item['media']['fileSize'] }}"@endisset />
@endisset
@isset($item['enclosure'])
<enclosure url="{{ $item['enclosure']['url'] }}"
length="{{ $item['enclosure']['length'] }}"
type="{{ $item['enclosure']['type'] }}" />
@endisset
</item>
@endforeach
</channel>
</rss>
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Controller;
use Illuminate\Http\Response;
use Rumenx\Feed\FeedFactory;
use App\Models\Post;
class CustomFeedController extends Controller
{
public function podcast(): Response
{
$feed = FeedFactory::create([
'views' => [
'rss' => resource_path('views/feeds/podcast-rss.blade.php')
]
]);
$feed->setTitle(config('app.name') . ' Podcast');
$feed->setDescription('Weekly tech podcast episodes');
$feed->setLink(url('/podcast'));
// Add podcast-specific channel data
$feed->setChannelData([
'language' => 'en-us',
'copyright' => '© 2024 ' . config('app.name'),
'image' => [
'url' => asset('images/podcast-logo.jpg'),
'title' => config('app.name') . ' Podcast',
'link' => url('/podcast')
],
'itunes' => [
'subtitle' => 'Tech discussions and tutorials',
'author' => 'Tech Team',
'summary' => 'Weekly technology podcast covering development, tools, and industry trends',
'category' => 'Technology'
]
]);
$episodes = Post::where('type', 'podcast')
->published()
->latest()
->limit(50)
->get();
foreach ($episodes as $episode) {
$feed->addItem([
'title' => $episode->title,
'description' => $episode->excerpt,
'content' => $episode->content,
'link' => route('podcast.show', $episode->slug),
'pubdate' => $episode->published_at,
'author' => $episode->author->name,
'duration' => $episode->duration, // e.g., "45:32"
'enclosure' => [
'url' => $episode->audio_url,
'length' => $episode->file_size,
'type' => 'audio/mpeg'
],
'itunes' => [
'duration' => $episode->duration,
'episode' => $episode->episode_number,
'episodeType' => 'full'
]
]);
}
return response($feed->render('rss'))
->header('Content-Type', 'application/rss+xml; charset=utf-8');
}
}
<?php
// resources/views/feeds/podcast-rss.blade.php
?>
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>{{ $channel['title'] }}</title>
<description>{{ $channel['description'] }}</description>
<link>{{ $channel['link'] }}</link>
<language>{{ $channel['language'] ?? 'en-us' }}</language>
<copyright>{{ $channel['copyright'] }}</copyright>
@isset($channel['image'])
<image>
<url>{{ $channel['image']['url'] }}</url>
<title>{{ $channel['image']['title'] }}</title>
<link>{{ $channel['image']['link'] }}</link>
</image>
@endisset
@isset($channel['itunes'])
<itunes:subtitle>{{ $channel['itunes']['subtitle'] }}</itunes:subtitle>
<itunes:author>{{ $channel['itunes']['author'] }}</itunes:author>
<itunes:summary>{{ $channel['itunes']['summary'] }}</itunes:summary>
<itunes:category text="{{ $channel['itunes']['category'] }}" />
<itunes:image href="{{ $channel['image']['url'] }}" />
<itunes:explicit>no</itunes:explicit>
@endisset
<lastBuildDate>{{ now()->toRfc2822String() }}</lastBuildDate>
@foreach($items as $item)
<item>
<title>{{ $item['title'] }}</title>
<description><![CDATA[{!! $item['description'] !!}]]></description>
<link>{{ $item['link'] }}</link>
<guid isPermaLink="true">{{ $item['link'] }}</guid>
<pubDate>{{ \Carbon\Carbon::parse($item['pubdate'])->toRfc2822String() }}</pubDate>
<author>{{ $item['author'] }}</author>
@isset($item['enclosure'])
<enclosure url="{{ $item['enclosure']['url'] }}"
length="{{ $item['enclosure']['length'] }}"
type="{{ $item['enclosure']['type'] }}" />
@endisset
@isset($item['itunes'])
<itunes:duration>{{ $item['itunes']['duration'] }}</itunes:duration>
<itunes:episode>{{ $item['itunes']['episode'] }}</itunes:episode>
<itunes:episodeType>{{ $item['itunes']['episodeType'] }}</itunes:episodeType>
@endisset
@isset($item['content'])
<content:encoded><![CDATA[{!! $item['content'] !!}]]></content:encoded>
@endisset
</item>
@endforeach
</channel>
</rss>
<?php
// templates/feeds/custom-rss.xml.twig
?>
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ channel.title }}</title>
<description>{{ channel.description }}</description>
<link>{{ channel.link }}</link>
<atom:link href="{{ channel.feedUrl ?? channel.link }}" rel="self" type="application/rss+xml" />
{% if channel.language is defined %}
<language>{{ channel.language }}</language>
{% endif %}
<lastBuildDate>{{ "now"|date("r") }}</lastBuildDate>
<generator>Symfony Feed Generator</generator>
{% for item in items %}
<item>
<title>{{ item.title }}</title>
<description><![CDATA[{{ item.description|raw }}]]></description>
<link>{{ item.link }}</link>
<guid isPermaLink="true">{{ item.link }}</guid>
<pubDate>{{ item.pubdate|date("r") }}</pubDate>
{% if item.author is defined %}
<dc:creator>{{ item.author }}</dc:creator>
{% endif %}
{% if item.category is defined and item.category is iterable %}
{% for category in item.category %}
<category>{{ category }}</category>
{% endfor %}
{% endif %}
{% if item.content is defined %}
<content:encoded><![CDATA[{{ item.content|raw }}]]></content:encoded>
{% endif %}
</item>
{% endfor %}
</channel>
</rss>
<?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 Twig\Environment;
class TwigFeedController extends AbstractController
{
#[Route('/feed/custom/{format}', name: 'custom_feed', defaults: ['format' => 'rss'])]
public function customFeed(Environment $twig, string $format): Response
{
$feed = FeedFactory::create([
'views' => [
'rss' => $this->renderView('feeds/custom-rss.xml.twig'),
'atom' => $this->renderView('feeds/custom-atom.xml.twig')
]
]);
$feed->setTitle($this->getParameter('app.name'));
$feed->setDescription('Custom formatted feed');
$feed->setLink($this->generateUrl('homepage', [], urlType: 'absolute'));
// Add sample items
$feed->addItem([
'title' => 'Sample Post',
'description' => 'Sample description',
'link' => $this->generateUrl('post_show', ['id' => 1], urlType: 'absolute'),
'pubdate' => new \DateTime(),
'author' => 'Author Name',
'category' => ['Technology', 'PHP']
]);
$contentType = $format === 'atom' ? 'application/atom+xml' : 'application/rss+xml';
return new Response(
$feed->render($format),
200,
['Content-Type' => $contentType . '; charset=utf-8']
);
}
}
Create base templates for consistency:
<?php
// base-rss.blade.php
?>
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" @yield('namespaces')>
<channel>
@include('feeds.partials.channel-info')
@yield('channel-extensions')
@foreach($items as $item)
<item>
@include('feeds.partials.item-core', ['item' => $item])
@yield('item-extensions', ['item' => $item])
</item>
@endforeach
</channel>
</rss>
<?php
// podcast-rss.blade.php
?>
@extends('feeds.base-rss')
@section('namespaces')
xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
@endsection
@section('channel-extensions')
<itunes:subtitle>{{ $channel['itunes']['subtitle'] }}</itunes:subtitle>
<itunes:author>{{ $channel['itunes']['author'] }}</itunes:author>
<itunes:image href="{{ $channel['image']['url'] }}" />
@endsection
@section('item-extensions')
@isset($item['enclosure'])
<enclosure url="{{ $item['enclosure']['url'] }}"
length="{{ $item['enclosure']['length'] }}"
type="{{ $item['enclosure']['type'] }}" />
@endisset
@isset($item['itunes']['duration'])
<itunes:duration>{{ $item['itunes']['duration'] }}</itunes:duration>
@endisset
@endsection
<?php
use PHPUnit\Framework\TestCase;
use Rumenx\Feed\FeedFactory;
class CustomViewTest extends TestCase
{
public function testCustomRssView(): void
{
$feed = FeedFactory::create([
'views' => [
'rss' => __DIR__ . '/fixtures/test-rss.php'
]
]);
$feed->setTitle('Test Feed');
$feed->setDescription('Test Description');
$feed->setLink('https://example.com');
$feed->addItem([
'title' => 'Test Item',
'description' => 'Test Description',
'link' => 'https://example.com/item',
'pubdate' => '2024-01-01T00:00:00Z'
]);
$xml = $feed->render('rss');
$this->assertStringContainsString('<title>Test Feed</title>', $xml);
$this->assertStringContainsString('<item>', $xml);
$this->assertStringContainsString('Test Item', $xml);
}
public function testJsonFeedView(): void
{
$feed = FeedFactory::create([
'views' => [
'json' => __DIR__ . '/fixtures/test-json.php'
]
]);
$feed->setTitle('JSON Test Feed');
$feed->addItem([
'title' => 'JSON Item',
'description' => 'JSON Description',
'link' => 'https://example.com/json-item',
'pubdate' => '2024-01-01T00:00:00Z'
]);
$json = $feed->render('json');
$data = json_decode($json, true);
$this->assertEquals('JSON Test Feed', $data['title']);
$this->assertCount(1, $data['items']);
$this->assertEquals('JSON Item', $data['items'][0]['title']);
}
}
- Template Organization: Keep view files organized in dedicated directories
- Escaping: Always escape output to prevent XSS attacks
-
Validation: Validate XML output with tools like
xmllint
- Performance: Cache rendered templates when possible
- Standards: Follow RSS 2.0, Atom 1.0, and JSON Feed specifications
- Testing: Write tests for custom view logic
- Learn about Caching to optimize view rendering
- Check Advanced Features for complex view scenarios
- See framework examples: Laravel or Symfony