# sfFeed2 plugin 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 [Chapter 8](http://www.symfony-project.com/book/trunk/08-Inside-the-Model-Layer#Extending%20the%20Model). 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](http://www.symfony-project.com/book/trunk/09-Links-and-the-Routing-System). 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](http://en.wikipedia.org/wiki/Atom_%28standard%29) 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 = sfFeedPeer::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](http://en.wikipedia.org/wiki/Factory_object) method creates an instance of the `sfFeed` class for the 'Atom' format. 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 $sf_context->getResponse()->setLayout(false) ?> <?php echo $feed->asXml() ?> The content type is automatically set by the `asXML()` method, depending on the feed format used during initialization. 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 = sfFeedPeer::newInstance('atom1'); $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 = sfFeedPeer::newInstance('atom1'); $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 [the source of the `sfFeed` class](browse)(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 proxy methods The plugin offers proxy methods to make repetitive tasks easier. For instance, the steps described in the previous 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( 'atom1', array( 'title' => 'The mouse blog', 'link' => 'http://www.myblog.com/', 'authorEmail' => 'pclive@myblog.com', 'authorName' => 'Peter Clive' 'routeName' => '@permalink' ), $posts ); } #### 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> ## 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-18 | 0.8.0 Alpha * francois: Initial release