RSS feeds in CakePHP

Recently I noticed that Thunderbird doesn’t import and update RSS feeds anymore that are (partially) invalid.
Well, mine always were I guess. Back in the days (4 years ago) I wasn’t aware that the author tag was only for email addresses, not for usernames.
One has to say that it also wasn’t possible in an easy way to handle "dc:creator" instead of author.
So my feed from back then had this and a few other "errors" in them. But they worked, so I never bothered. Until recently.

Upgrading more apps to 2.x made me realize that there now are such cool View classes for other types of data (Json, Csv, Xml).
And since RSS is pretty much XML, I thought, why the hell are we even using a really old and partially working helper + view + layout?
It should have been upgraded to a View class, as well, IMO. So I opened a ticket for discussion and started to do that.

Goals:

  • Support view-less actions via serialize.
  • Get rid of the ridiculously verbose "inline" namespace declarations.
  • Simplify the use of namespaces and their prefixes (auto-add only those that are actually used).
  • Support CDATA (unescaped content).

First, I just dropped all the helper code into the class. That didn’t really work out as the rendering of the existing code renders each item by itself, thus not having any information about the global namespaces. I had to drop the whole thing and start from scratch, orienting myself on how the XmlClass does it.
So I used my old feed content to slowly get it working again using a complete array structure and only a single Xml::fromArray() call at the very end.
I dropped the class into my Tools plugin and added a few basic tests.

If you include the plugin, don’t forget the CakePlugin::load('Tools') or CakePlugin::loadAll() call.

Usage

We first need an action to output our feed using Router::parseExtensions(array('rss')) (in your routes or bootstrap file) and access it via:

/controller/action.rss

We also need to add $this->viewClass = 'Tools.Rss' to every action that outputs RSS as the RequestHandler component only switches yet for the core Xml and Json View classes.

A basic feed contains at least a title, description and a link for both channel and items. It is also advised to add the atom:link to the location of the feed itself.

$this->viewClass = 'Tools.Rss'; // Important if you do not have an auto-switch for the rss extension
$atomLink = array('controller' => 'topics', 'action' => 'feed', 'ext' => 'rss'); // example controller and action
$data = array(
    'channel' => array(
        'title' => 'Channel title',
        'link' => 'http://channel.example.org',
        'description' => 'Channel description',
        'atom:link' => array('@href' => $atomLink),
    ),
    'items' => array(
        array('title' => 'Title One', 'link' => 'http://example.org/one', 
            'author' => '[email protected]', 'description' => 'Content one'),
        array('title' => 'Title Two', 'link' => 'http://example.org/two', 
            'author' => '[email protected]', 'description' => 'Content two'),
    )
);
$this->set(array('data' => $data, '_serialize' => 'data'));

It is also possible to use one of the already built in namespaces – e.g. if you want to display a post’s username instead of email (which you should^^).
You can also add the content itself as CDATA. The description needs to be plain text, so if you have HTML markup, make sure to strip that out for the description but pass it unescaped to the content namespace tag for it.

$data = array(
    'channel' => array(
        'title' => 'Channel title',
        'link' => 'http://channel.example.org',
        'description' => 'Channel description'
    ),
    'items' => array(
        array('title' => 'Title One', 'link' => 'http://example.org/one', 
            'dc:creator' => 'Mr Bean', 'description' => 'Content one',
            'content:encoded' => 'Some <b>HTML</b> content'),
        array('title' => 'Title Two', 'link' => 'http://example.org/two', 
            'dc:creator' => 'Luke Skywalker', 'description' => 'Content two',
            'content:encoded' => 'Some <b>more HTML</b> content'),
    )
);
$this->set(array('data' => $data, '_serialize' => 'data'));

Extendability

You can easily register new namespaces, e.g. to support the google data feeds (xmlns:g="http://base.google.com/ns/1.0"):

$data = array(
    'document' => array(
        'namespace' => array(
            'g' => 'http://base.google.com/ns/1.0'
        )
    )
    'channel' => array(
        ...
    ),
    'items' => array(
        array('g:price' => 25, ...),
    )
);
$this->set(array('data' => $data, '_serialize' => 'data'));

So forget that there was a helper and a complicated way to set up RSS feeds via view files. This is the way to go.

View class mapping

See the documentation on how to use view class mapping to automatically respond with the RssView for each request to the rss extension:

'rss' => 'Tools.Rss'

With the help of parseExtensions() and RequestHandler this will save you the extra view config line in your actions.

Passing params.

If you need to pass params to this view, use query strings:

.../action.rss?key1=value1&key2=value2

Demo

sandbox.dereuromark.de/sandbox/rss_examples (including feed validation check)

Source code: RssExamplesController.php

Vista

There are still lots of things that could be implemented.
It still does not handle all the use cases possible, for example.

It also stands to discussion if one could further generalize the class to not only support RSS feeds, but other type of feeds, as well.

CakePHP 3

This article is 2.x only.
For CakePHP 3 please see the 3.0 Feed Plugin as the RssView class has been extracted into this stand-alone plugin.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.