Skip to content

Custom Views

Rumen Damyanov edited this page Jul 29, 2025 · 1 revision

Custom Views Examples

This guide shows how to create and use custom view templates for RSS and Atom feeds, allowing you to customize the XML output format.

Understanding Views

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

Basic Custom RSS View

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>

Extended Atom View with Custom Elements

<?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>

Using Custom Views

Plain PHP Implementation

<?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);

JSON Feed 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);

Laravel Custom Views

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>

Laravel Controller with Custom Views

<?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');
    }
}

Podcast RSS Template

<?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>

Symfony Custom Views with Twig

<?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>

Symfony Controller with Twig Views

<?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']
        );
    }
}

View Inheritance

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

Testing Custom Views

<?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']);
    }
}

Best Practices

  1. Template Organization: Keep view files organized in dedicated directories
  2. Escaping: Always escape output to prevent XSS attacks
  3. Validation: Validate XML output with tools like xmllint
  4. Performance: Cache rendered templates when possible
  5. Standards: Follow RSS 2.0, Atom 1.0, and JSON Feed specifications
  6. Testing: Write tests for custom view logic

Next Steps

Clone this wiki locally