![]() |
|
sfFeed2Plugin - 1.2.2Feed manager for reading and writing feeds |
|
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.
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.
This plugin contains four data structure classes:
sfFeedsfFeedItemsfFeedImagesfFeedEnclosureIt also contains specific classes containing specific input/output methods based on specific feed formats:
sfAtom1FeedsfRssFeedsfRss10FeedsfRss201FeedsfRss091FeedLast but not least, the most important (and smart) class is the feed manager, which contains only static methods:
sfFeedPeerUnit tests are available in the SVN repository.
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
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 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
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>
<icon>http://www.myblog.com/favicon.ico</icon>
<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>
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');
$feedImage = new sfFeedImage();
$feedImage->setFavicon('http://www.myblog.com/favicon.ico');
$feed->setImage($feedImage);
$c = new Criteria;
$c->addDescendingOrderByColumn(PostPeer::CREATED_AT);
$c->setLimit(5);
$posts = PostPeer::doSelect($c);
foreach ($posts as $post)
{
$item = new sfFeedItem();
$item->setTitle($post->getTitle());
$item->setLink('@permalink?stripped_title='.$post->getStrippedTitle());
$item->setAuthorName($post->getAuthor()->getName());
$item->setAuthorEmail($post->getAuthor()->getEmail());
$item->setPubdate($post->getCreatedAt('U'));
$item->setUniqueId($post->getStrippedTitle());
$item->setDescription($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 decorate_with(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
initialize() methodThe 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.
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, array('routeName' => '@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 routeName option if defined in the second argument of the method call. It is supposed to be a route name for the feed items. If present, the method 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 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.
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.
The other way to use a specific method for an item property is to pass a methods option to the convertObjectsToItems method: an associative array associating item properties with object methods. So, for instance, to tell the converter to use nothing for the feed author email and the getUserFirstName() method for the author name, you could write:
$postItems = sfFeedPeer::convertObjectsToItems($posts, array(
'routeName' => '@permalink',
'methods' => array(
'authorEmail' => *,
'authorName' => 'getUserFirstName'
)
));
You can also pass some arguments to the user defined method by passing an array composed of the method name and an array of arguments:
$postItems = sfFeedPeer::convertObjectsToItems($posts, array(
'routeName' => '@permalink',
'methods' => array(
'authorEmail' => *,
'authorName' => 'getUserFirstName',
'pubdate' => array('getPublishedAtDate', array('U')),
)
));
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',
'methods' => array('authorEmail' => *, 'authorName' => 'getUserFirstName')
)
);
}
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 1.0 RDF Site Summary
$feed = sfFeedPeer::newInstance('rss10');
// RSS 0.91 Userland
$feed = sfFeedPeer::newInstance('rss091');
// RSS 2.01 rev6
$feed = sfFeedPeer::newInstance('rss201');
You may want to display the latest posts of the symfony users group in your application. The steps to retrieve this information are to fetche a feed from the Internet, create an empty feed object, and populate it with the items of the feed. You can use the fromXML() method and the sfWebBrowserPlugin for that:
public function executeLastPosts()
{
$uri = 'http://groups.google.com/group/symfony-users/feed/rss_v2_0_msgs.xml';
$browser = new sfWebBrowser(array(
'user_agent' => 'sfFeedReader/0.9',
'timeout' => 5
));
$feedString = $browser->get($uri)->getResponseText();
$feed = new sfRssFeed();
$feed->setUrl($uri);
$feed->fromXml($feedString);
$this->feed = $feed;
}
Thanks to the sfFeedPeer shortcuts, this can be reduced to a single line:
public function executeLastPosts()
{
$this->feed = sfFeedPeer::createFromWeb('http://groups.google.com/group/symfony-users/feed/rss_v2_0_msgs.xml');
}
The createFromWeb() method first parse the response and tries to recognize a known feed format (RSS or Atom - The recognized formats are the same ones as above). Note that this 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>
The sfFeedPeer class contains a method 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 a feed of 10 posts populated with the latest posts from 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), array('limit' => 10));
}
sfFeedPeer classtoXML method to sfRssFeed, sfRss10Feed and sfAtom1Feed (based on an idea from Frank Stelzer)sfFeedPeer::createFromXml() methodsfFeed methods were describedsfFeedPeer::createFromWeb() now throws an exception whenever the fetched URL returns an errorsfFeed constructorsfRssFeed causing badly formatted feeds in dev envsfFeedPeer::convertObjectsToItems() signature changed ($objects, $options = array()) to have similar signature to that of sfFeedPeer::createFromObjects()sfFeedPeer::convertObjectsToItems()sfFeedPeer::convertObjectsToItems() now populates the feed item contentsfFeedPeer::createFromWeb() can now use a custom userAgent option to be seen as a custom user agent from the outsidesfFeedPeer::createFromWeb() has a better detection of Atom feedssfFeedPeer::aggregate() when defining a feed formatsfFeedPeer::createFromWeb() is smarter at determining the type of feed it readssfFeedPeer::aggregate() now handles items with the same date without overriding one.sfFeedPeer preventing items creation from objectssfFeedPeer::aggregate now remember their original feed propertiessfFeedPeer::aggregate can now limit the total number of items returnedsfFeed::keepOnlyItems($count) methodsfRssReed now looks for a dc:creator of no regular author is found<content:encoded> handling in RSS 1 and 2 feeds (based on a patch from Jeff Merlet)sfRss10Feed class and unit testssfRssFeed conversionsgetLatestPostDate() method to sfFeed and unit tested it.sfRss201Feed and sfRss901Feed. These classes are not really useful anyway, since all their code was refactored to sfRssFeed.sfAtom1Feed conversions__toString() and initialize() methods to sfFeedEnclosurecontent property to sfFeedItemfromXML() methods now expect a string rather than a simpleXML objectsfFeedPeer::createFromObjects() signature changed ($objects, $parameters = array())