= sfFeed2 plugin =
[[PageOutline]]
The `sfFeed2Plugin` offers an object interface for feeds and feed items, feed input methods using a web feed or an array of objects as source, and feed output methods for displaying items on a page and serving feeds through a symfony application.
== Possible uses ==
* serving a RSS/Atom feed based on model objects
* Using web feeds as data source
* Feed aggregator
As compared with the `sfFeedPlugin`, this plugin has a cleaner code separation in classes and offers more features. The syntax differs, but many classes have the same names, therefore the two plugins are not compatible.
== Contents ==
This plugin contains three data structure classes:
* `sfFeed`
* `sfFeedItem`
* `sfFeedEnclosure`
It also contains specific classes containing specific input/output methods based on specific feed formats:
* `sfAtom1Feed`
* `sfRssFeed`
* `sfRss201rev2Feed`
* `sfRssUserland091Feed`
Last but not least, the most important (and smart) class is the feed manager, which contains only static methods:
* `sfFeedPeer`
Unit tests are available in the SVN repository.
== Installation ==
* Install the plugin
{{{
$ symfony plugin-install http://plugins.symfony-project.com/sfFeed2Plugin
}}}
* Alternatively, if you don't have PEAR installed, you can download the latest package attached to this plugin's wiki page and extract it under your project's `plugins/` directory
* Clear the cache to enable the autoloading to find the new class
{{{
$ symfony cc
}}}
== Tutorials ==
=== Building a feed from an array of objects ===
==== Example data ====
Let's take an example of a simple blog application with a `Post` and an `Author` table:
||''Post'' || ''Author''
||id || id
||author_id || first_name
||title || last_name
||description || email
||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 [http://www.symfony-project.com/book/trunk/08-Inside-the-Model-Layer#Extending%20the%20Model 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 [http://www.symfony-project.com/book/trunk/09-Links-and-the-Routing-System 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 [http://en.wikipedia.org/wiki/Atom_%28standard%29 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>
}}}
==== Using the creators and setters ====
To build the feed, you need to initialize it with a certain format and options, and to add feed items based on the objects resulting from a database request. With the syntax of the `sfFeed` and `sfFeedItem` class, that would give:
{{{
public function executeLastPosts()
{
$feed = new sfAtom1Feed();
$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;
}
}}}
At the end of the action, the `$feed` variable contains a `sfAtom1Feed` object which includes several `sfFeedItem` objects. To transform the object into an actual Atom feed, the `lastPostsSuccess.php` template simply contains:
{{{
<?php $sf_context->getResponse()->setLayout(false) ?>
<?php echo $feed->asXml() ?>
}}}
The content type is automatically set by the `asXML()` method, depending on the feed format (Atom1 in this example).
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}}}
==== Using the `initialize()` method ====
The use of all the setters for the feed and item construction can be a little annoying, since there is a lot of information to define. Both the `sfFeed` and the `sfFeedItem` classes provide an `initialize()` method that uses an associative array for a shorter syntax:
{{{
public function executeLastPosts()
{
$feed = new sfAtom1Feed();
$feed->initialize(array(
'title' => 'The mouse blog',
'link' => 'http://www.myblog.com/',
'authorEmail' => 'pclive@myblog.com',
'authorName' => '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->initialize(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->addItem($item);
}
$this->feed = $feed;
}
}}}
It has exactly the same effect as the previous listing, but the syntax is clearer.
==== Using the object converter ====
As the method names that are used to build a feed item based on an object are more or less always the same, the `sfFeedPeer` can try to do it on its own:
{{{
public function executeLastPosts()
{
$feed = new sfAtom1Feed();
$feed->initialize(array(
'title' => 'The mouse blog',
'link' => 'http://www.myblog.com/',
'authorEmail' => 'pclive@myblog.com',
'authorName' => 'Peter Clive'
));
$c = new Criteria;
$c->addDescendingOrderByColumn(PostPeer::CREATED_AT);
$c->setLimit(5);
$posts = PostPeer::doSelect($c);
$postItems = sfFeedPeer::convertObjectsToItems($posts, '@permalink')
$feed->addItems($postItems);
$this->feed = $feed;
}
}}}
The rules governing the `sfFeedPeer::convertObjectsToItems` algorithm are as follows:
* To set the item `title`, it looks for a `getFeedTitle()`, a `getTitle()`, a `getName()` or a `__toString()` method.
In the example, the `Post` object has a `getName()` method.
* To set the `link`, it uses the second argument of the method call, which is supposed to be a route name for the feed items. 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 a `getFeedLink()`, `getLink()`, `getUrl()` method in the object.
In the example, the route name given as parameter is `@permalink`. The routing rule contains a `:stripped_title` parameter and the `Post` object has a `getStrippedTitle()` method, so the `convertObjectsToItems` method is able to define the URIs to link to.
* To set the author's email, it looks for a `getFeedAuthorEmail` or a `getAuthorEmail`. If there is no such method, it looks for a `getAuthor()`, `getUser()` or `getPerson()` method. If the result returned is an object, it looks in this object for a `getEmail` or a `getMail` method.
In the example, the `Post` object has a `getAuthor()`, and the `Author` object has a `getName()`. 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 a `getDate()` method.
In the example, the `Post` object has a `getCreatedAt`
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](http://www.symfony-project.com/trac/browser/plugins/sfFeed2Plugin/lib) to discover all the deduction algorithms.
All in all, the way the accessors of the `Post` and `Author` objects are built allow the built-in algorithm of the `convertObjectsToItems` method to work, and the creation of the feed to be more simple.
==== Defining custom values for the feed ====
In the list of rules 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.
==== Using the sfFeedPeer static methods ====
The `sfFeedPeer` class offer helper methods that facilitate the creation and population of feed items.
When the feed format is determined at runtime, create feed objects using the `sfFeedPeer::newInstance()` method, which is a factory, rather that using the `new` command:
{{
$feed = sfFeedPeer::newInstance('atom1');
// same as
$feed = new sfAtom1Feed();
}}
The steps described in the `executeLastPosts` listing occur in almost every feed construction process, so the `sfFeedPeer` can reduce the code above to a simpler:
{{{
public function executeLastPosts()
{
$c = new Criteria;
$c->addDescendingOrderByColumn(PostPeer::CREATED_AT);
$c->setLimit(5);
$posts = PostPeer::doSelect($c);
$this->feed = sfFeedPeer::createFromObjects(
$posts,
array(
'format' => 'atom1',
'title' => 'The mouse blog',
'link' => 'http://www.myblog.com/',
'authorEmail' => 'pclive@myblog.com',
'authorName' => 'Peter Clive'
'routeName' => '@permalink'
)
);
}
}}}
==== Using other formats ====
The methods described below can be transposed to build other RSS feeds. Simply change the parameter given to the feed factory:
{{{
// Atom 1
$feed = sfFeedPeer::newInstance('atom1');
// RSS 0.91
$feed = sfFeedPeer::newInstance('rssUserland091');
// RSS 2.01
$feed = sfFeedPeer::newInstance('rss201rev2');
}}}
=== Fetching a feed from the web and displaying it ===
You may want to display the latest posts of the symfony users group in your application. The `sfFeedPeer` class proposes a method that fetches a feed from the Internet, analyzes its type to create a specific feed format object accordingly, and populates it with the items of the feed. This all takes one line:
{{{
public function executeLastPosts()
{
$this->feed = sfFeedPeer::createFromWeb('http://groups.google.com/group/symfony-users/feed/rss_v2_0_msgs.xml');
}
}}}
The recognized formats are the same ones as above. Note that the `createFromWeb()` method depends on the `sfWebBrowserPlugin`, so this plugin must be installed to make the method work.
Once the feed is built, it is very easy to use it for the display in the template:
{{{
<h2>Latests posts from the mailing-list</h2>
<ul>
<?php foreach($feed->getItems() as $post): ?>
<li>
<?php echo format_date($post->getPubDate(), 'd/MM H:mm') ?> -
<?php echo link_to($post->getTitle(), $post->getLink()) ?>
by <?php echo $post->getAuthorName() ?>
</li>
<?php endforeach; ?>
</ul>
}}}
=== Aggregating several feeds ===
The `sfFeedPeer` class contains a mathod called `aggregate()`, which merges several feeds and reorders the items chronologically. Using it is very simple: just pass an array of feeds as parameters, and you receive a new feed object with all the items within.
For instance, here is how you could display the posts of both the users and the devs groups:
{{{
public function executeLastPosts()
{
$feed1 = sfFeedPeer::createFromWeb('http://groups.google.com/group/symfony-users/feed/rss_v2_0_msgs.xml');
$feed2 = sfFeedPeer::createFromWeb('http://groups.google.com/group/symfony-devs/feed/rss_v2_0_msgs.xml');
$this->feed = sfFeedPeer::aggregate(array($feed1, $feed2));
}
}}}
== TODO ==
* Populate feedItems from a pager rather than from an array of objects
* Add other formats : RDF, trac wiki, etc.
* Add more unit tests
== Changelog ==
=== 2007-02-19 | 0.8.1 Alpha ===
* francois: Much more unit tests
* francois: Much better sfAtom1Feed conversions
* francois: added __toString() and initialize() methods to sfFeedEnclosure
* francois: added content property to sfFeedItem
* francois: '''BC break''' the fromXML() methods now expect a string rather than a simpleXML object
* francois: '''BC break''' sfFeedPeer::createFromObjects() signature changed ($objects, $parameters = array())
=== 2007-02-18 | 0.8.0 Alpha ===
* francois: Initial release