How to build a syndication feed
Overview
Whether your application lists posts, images, news, questions or anything else, if the update rate is higher than once a month, you must provide a syndication feed (RSS, Atom, etc.) to your users so that they can keep up-to-date about your website from within a feed aggregator. The good news is, if your object model is built in the right way, this won't take you more than a couple lines to develop, since symfony provides a feed builder plugin that does it all for you.
Introduction
The example explored in this chapter is an simple blog application with a Post
and an Author
table:
Post | Author |
---|---|
id | id |
author_id | first_name |
title | last_name |
description | |
body | |
created_at |
The Post
class is extended by a getStrippedTitle()
method that transforms the title into a string that can be used in an URI, replacing spaces by dashes, upper case by lower case, and removing all special characters:
public function getStrippedTitle() { $text = strtolower($this->getTitle()); // strip all non word chars $text = preg_replace('/\W/', ' ', $text); // replace all white space sections with a dash $text = preg_replace('/\ +/', '-', $text); // trim dashes $text = preg_replace('/\-$/', '', $text); $text = preg_replace('/^\-/', '', $text); return $text; }
The Author
class is extended by a custom ->getName()
method as follows:
public function getName() { return $this->getFirstName().' '.$this->getLastName() }
If you need more details about the way to extend the model, refer to Chapter 8.
The routing.yml
contains the following rule:
post: url: /permalink/:stripped_title param: { module: post, action: read }
If you need more details about the routing system, refer to Chapter 9.
A special feed
module is built for the occasion, and all the actions and templates will be placed in it.
$ symfony init-module myapp feed
Expected result
The feed action has to output an Atom feed. As a reminder of all the information that need to be included in an Atom feed, here is an example:
<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom"> <title>The mouse blog</title> <link href="http://www.myblog.com/" /> <updated>2005-12-11T16:23:51Z</updated> <author> <name>Peter Clive</name> <author_email>pclive@myblog.com</author_email> </author> <id>4543D55FF756G734</id> <entry> <title>I love mice</title> <link href="http://www.myblog.com/permalink/i-love-mice" /> <id>i-love-mice</id> <author> <name>Peter Clive</name> <author_email>pclive@myblog.com</author_email> </author> <updated>2005-12-11T16:23:51Z</updated> <summary>Ever since I bought my first mouse, I can't live without one.</summary> </entry> <entry> <title>A mouse is better than a fish</title> <link href="http://www.myblog.com/permalink/a-mouse-is-better-than-a-fish" /> <id>a-mouse-is-better-than-a-fish</id> <author> <name>Bob Walter</name> <author_email>bwalter@myblog.com</author_email> </author> <updated>2005-12-09T09:11:42Z</updated> <summary>I had a fish for four years, and now I'm sick. They smell.</summary> </entry> </feed>
Install the plug-in
Symfony provides a sfFeed
plug-in that automates most of the feed generation. To install it, use the symfony command line:
$ symfony plugin-install http://plugins.symfony-project.com/sfFeedPlugin
This installs the classes of the plug-in in the plugins/
directory. TO let symfony autoload them, you need to clear the configuration cache:
$ symfony cc
If you want to learn more about plug-ins, how they extend the framework and how you can package the features that you use across several projects into a plug-in, refer to Chapter 17 of the symfony book.
Build the feed by hand
In the feed
module, create a lastPosts
action:
public function executeLastPosts() { $feed = sfFeed::newInstance('atom1'); $feed->setTitle('The mouse blog'); $feed->setLink('http://www.myblog.com/'); $feed->setAuthorEmail('pclive@myblog.com'); $feed->setAuthorName('Peter Clive'); $c = new Criteria; $c->addDescendingOrderByColumn(PostPeer::CREATED_AT); $c->setLimit(5); $posts = PostPeer::doSelect($c); foreach ($posts as $post) { $item = new sfFeedItem(); $item->setFeedTitle($post->getTitle()); $item->setFeedLink('@permalink?stripped_title='.$post->getStrippedTitle()); $item->setFeedAuthorName($post->getAuthor()->getName()); $item->setFeedAuthorEmail($post->getAuthor()->getEmail()); $item->setFeedPubdate($post->getCreatedAt('U')); $item->setFeedUniqueId($post->getStrippedTitle()); $item->setFeedDescription($post->getDescription()); $feed->addItem($item); } $this->feed = $feed; }
The initial factory method creates an instance of the sfFeed
class for the 'Atom' format. The sfFeed
and sfFeedItem
classes are symfony add-ons made especially for feed construction. At the end of the action, the $feed
variable contains a sfFeed
object which includes several sfFeedItem
objects. To transform the object into an actual Atom feed, the lastPostsSuccess.php
template simply contains:
<?php echo $feed->getFeed() ?>
The template must not be decorated by a layout. In addition, the resulting page has to be declared as a text/xml
content-type. So add a view.yml
in the feed
module config/
directory:
all: has_layout: off http_metas: content-type: text/xml
When called from a feed aggregator, the result of the action is now exactly the Atom feed described above:
http://www.myblog.com/feed/lastPosts
Use the short syntax
The use of all the setters for the item construction can be a little annoying, since there is a lot of information to define. Symfony provides a short hand syntax that produces the same effect, using an associative array:
public function executeLastPosts() { $feed = sfFeed::newInstance('atom1'); $feed->setTitle('The mouse blog'); $feed->setLink('http://www.myblog.com/'); $feed->setAuthorEmail('pclive@myblog.com'); $feed->setAuthorName('Peter Clive'); $c = new Criteria; $c->addDescendingOrderByColumn(PostPeer::CREATED_AT); $c->setLimit(5); $posts = PostPeer::doSelect($c); foreach ($posts as $post) { $item = array( 'title' => $post->getTitle(), 'link' => '@permalink?stripped_title='.$post->getStrippedTitle(), 'authorName' => $post->getAuthor()->getName(), 'authorEmail' => $post->getAuthor()->getEmail(), 'pubdate' => $post->getCreatedAt(), 'uniqueId' => $post->getStrippedTitle(), 'description' => $post->getDescription(), ); $feed->addItemFromArray($item); } $this->feed = $feed; }
It has exactly the same effect, but the syntax is clearer.
Let symfony do it for you
As the method names that are used to build a news field are more or less always the same, symfony can do exactly as above with only:
public function executeLastPosts() { $feed = sfFeed::newInstance('atom1'); $feed->setTitle('The mouse blog'); $feed->setLink('http://www.myblog.com/'); $feed->setAuthorEmail('pclive@myblog.com'); $feed->setAuthorName('Peter Clive'); $c = new Criteria; $c->addDescendingOrderByColumn(PostPeer::CREATED_AT); $c->setLimit(5); $posts = PostPeer::doSelect($c); $feed->setFeedItemsRouteName('@permalink'); $feed->setItems($posts); $this->feed = $feed; }
Isn't it pure magic?
The magic of the sfFeed
object
The getter functions of the Post
object are so clear that even symfony can understand them. That's because the sfFeed
class has built-in mechanisms to deduce relevant information from well-organised classes:
To set the item
title
, it looks for agetFeedTitle()
, agetTitle()
, agetName()
or a__toString()
method.In the example, the
Post
object has agetName()
method.To set the
link
, it looks for an feed item route name (defined by asetFeedItemsRouteName()
call). If there is one, it looks in the route url for parameters for which it could find a getter in the object methods. If not, it looks for agetFeedLink()
,getLink()
,getUrl()
method.In the example, one route name is defined above in the action (
@permalink
). The routing rule contains a:stripped_title
parameter and thePost
object has agetStripped_Title()
method, so thesfFeed
object is able to define the URIs to link to.To set the author's email, it looks for a
getFeedAuthorEmail
or agetAuthorEmail
. If there is no such method, it looks for agetAuthor()
,getUser()
orgetPerson()
method. If the result returned is an object, it looks in this object for agetEmail
or agetMail
method.In the example, the
Post
object has agetAuthor()
, and theAuthor
object has agetName()
. The same kind of rules is used for the author's name and URL.To set the publication date, it looks for a
getFeedPubdate()
,getPubdate()
,getCreatedAt()
or agetDate()
method.In the example, the
Post
object has agetCreatedAt
The same goes for the other possible fields of an Atom feed (including the categories, the summary, the unique id, etc.), and you are advised to browse the source of the sfFeed
class to discover all the deduction mechanisms.
All in all, the way the accessors of the Post
and Author
objects are built allow the built-in shortcuts of the sfFeed
to work, and the creation of the feed to be so simple.
Define custom values for the feed
In the list presented above, you can see that the first method name that the sfFeed
object looks for is always a getFeedXXX()
. This allows you to specify a custom value for each of the fields of a feed item by simply extended the model.
For instance, if you don't want the author's email to be published in the feed, just add the following getFeedAuthorEmail()
method to the Post
object:
public function getFeedAuthorEmail() { return ''; }
This method will be found before the getAuthor()
method, and the feed will not disclose the publishers' email addresses.
Use other formats
The methods described below can be transposed to build other RSS feeds. Simply change the parameter given to the feed factory:
// RSS 0.91 $feed = sfFeed::newInstance('rssUserland091'); // RSS 2.01 $feed = sfFeed::newInstance('rss201rev2');
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.