Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/joshuaestes/sffeed2plugin

git mirror
https://github.com/joshuaestes/sffeed2plugin

Last synced: 1 day ago
JSON representation

git mirror

Awesome Lists containing this project

README

        

= 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 four data structure classes:

* `sfFeed`
* `sfFeedItem`
* `sfFeedImage`
* `sfFeedEnclosure`

It also contains specific classes containing specific input/output methods based on specific feed formats:

* `sfAtom1Feed`
* `sfRssFeed`
* `sfRss10Feed`
* `sfRss201Feed`
* `sfRss091Feed`

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 ==

Installation of the plugin differs on the version of symfony you are using.

* Install the plugin (symfony 1.0):
{{{
$ symfony plugin-install http://plugins.symfony-project.com/sfFeed2Plugin
}}}

* Install the plugin (symfony 1.1+):
{{{
$ symfony plugin:install 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:

{{{

The mouse blog

2005-12-11T16:23:51Z

Peter Clive
[email protected]

4543D55FF756G734
http://www.myblog.com/favicon.ico

I love mice

i-love-mice

Peter Clive
[email protected]

2005-12-11T16:23:51Z
Ever since I bought my first mouse, I can't live without one.


A mouse is better than a fish

a-mouse-is-better-than-a-fish

Bob Walter
[email protected]

2005-12-09T09:11:42Z
I had a fish for four years, and now I'm sick. They smell.

}}}

==== 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('[email protected]');
$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:

{{{

asXml(ESC_RAW) ?>
}}}

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' => '[email protected]',
'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' => '[email protected]',
'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 [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.

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')),
)
));
}}}

==== 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' => '[email protected]',
'authorName' => 'Peter Clive'
'routeName' => '@permalink',
'methods' => array('authorEmail' => '', 'authorName' => 'getUserFirstName')
)
);
}
}}}

==== 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 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');
}}}

=== 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 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 [http://www.symfony-project.com/trac/wiki/sfWebBrowserPlugin 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:

{{{

Latests posts from the mailing-list



    getItems() as $post): ?>

  • getPubDate(), 'd/MM H:mm') ?> -
    getTitle(), $post->getLink()) ?>
    by getAuthorName() ?>



}}}

=== Aggregating several feeds ===

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));
}
}}}

By default, the aggregator sorts all items in reverse chronological order. If you wish to sort them in chronological order instead, add 'sort' => 'chronological' to the parameters array.

=== Adding an image to the feed ===

To add an image to the feed, use the sfFeedImage class

{{{
$feed = new sfRss201Feed();

$feedImage = new sfFeedImage();
$feedImage->setLink('http://www.example.org/images/feed-image.png');
$feedImage->setTitle('My Title');
$feed->setImage($feedImage)
}}}}

== TODO ==

* unit test the `sfFeedPeer` class
* Populate feedItems from a pager rather than from an array of objects
* Deal with time zones (i.e. store dates in GMT, and handle input and output with a time zone)

== Changelog ==

== Trunk ==

* fabien: fixed response content-type encoding
* francois: Fixed encoding inside CDATA sections
* fabien: fixed routing for symfony 1.1 and optimized the 1.0 version
* fabien: added the possibility to pass arguments to methods that convert an object to an item
* Pascal.Borreli : symfony coding practices : removed lib closing tag (#2657)
* Fabian Lange : added image capabilities (#2551)

=== 2007-04-15 | 0.9.4 Beta ===

* francois: Added a `toXML` method to `sfRssFeed`, `sfRss10Feed` and `sfAtom1Feed` (based on an idea from Frank Stelzer)
* Frank.Stelzer: Added a new `sfFeedPeer::createFromXml()` method
* Frank.Stelzer: Fixed errors in the documentation where `sfFeed` methods were described
* francois: `sfFeedPeer::createFromWeb()` now throws an exception whenever the fetched URL returns an error
* Markus.Staab: Fixed a typo in `sfFeed` constructor
* francois: Fixed a warning in `sfRssFeed` causing badly formatted feeds in dev env
* francois: '''BC break''' `sfFeedPeer::convertObjectsToItems()` signature changed (`$objects, $options = array()`) to have similar signature to that of `sfFeedPeer::createFromObjects()`
* francois: Added the ability to define one method per feed property in `sfFeedPeer::convertObjectsToItems()`
* francois: `sfFeedPeer::convertObjectsToItems()` now populates the feed item content
* francois: `sfFeedPeer::createFromWeb()` can now use a custom `userAgent` option to be seen as a custom user agent from the outside
* francois: `sfFeedPeer::createFromWeb()` has a better detection of Atom feeds
* francois: Fixed a bug in generated Atom1 feeds (summary attribute)
* francois: Fixed a bug in `sfFeedPeer::aggregate()` when defining a feed format

=== 2007-03-13 | 0.9.3 Beta ===

* francois: `sfFeedPeer::createFromWeb()` is smarter at determining the type of feed it reads
* francois: Added smart guessing of publication date for RSS and Atom feeds, based on URL scheme
* francois: `sfFeedPeer::aggregate()` now handles items with the same date without overriding one.
* francois: Fixed two bugs in `sfFeedPeer` preventing items creation from objects
* francois: Items aggregated in `sfFeedPeer::aggregate` now remember their original feed properties
* francois: `sfFeedPeer::aggregate` can now limit the total number of items returned
* francois: Added `sfFeed::keepOnlyItems($count)` method
* francois: `sfRssReed` now looks for a `dc:creator` of no regular author is found

=== 2007-03-03 | 0.9.2 Beta ===

* francois: Added a way to get an item description based on a content when no description exists
* francois: Added `` handling in RSS 1 and 2 feeds (based on a patch from Jeff Merlet)

=== 2007-02-22 | 0.9.1 Beta ===

* francois: Added the `sfRss10Feed` class and unit tests
* francois: Changed feed type detection technique to avoid building a simpleXML element twice

=== 2007-02-21 | 0.9.0 Beta ===

* francois: Added more unit tests
* francois: Improved `sfRssFeed` conversions
* francois: Moved `getLatestPostDate()` method to `sfFeed` and unit tested it.
* francois: '''BC break''' Renamed specialized RSS classes to `sfRss201Feed` and `sfRss901Feed`. These classes are not really useful anyway, since all their code was refactored to `sfRssFeed`.

=== 2007-02-19 | 0.8.1 Alpha ===

* francois: Added much more unit tests
* francois: Improved `sfAtom1Feed` conversions
* francois: Added `__toString()` and `initialize()` methods to `sfFeedEnclosure`
* francois: Added `content` property to `sfFeedItem`
* francois: '''BC break''' `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