source: cpc/trunk/project/plugins/sfFeed2Plugin/README @ 1267

Last change on this file since 1267 was 1267, checked in by popeye, 11 years ago

Mise à jour du plugin sfFeed2Plugin

File size: 20.6 KB
Line 
1= sfFeed2 plugin =
2
3The `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.
4
5== Possible uses ==
6
7 * serving a RSS/Atom feed based on model objects
8 * Using web feeds as data source
9 * Feed aggregator
10
11As 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.
12
13== Contents ==
14
15This plugin contains four data structure classes:
16
17 * `sfFeed`
18 * `sfFeedItem`
19 * `sfFeedImage`
20 * `sfFeedEnclosure`
21
22It also contains specific classes containing specific input/output methods based on specific feed formats:
23
24 * `sfAtom1Feed`
25 * `sfRssFeed`
26 * `sfRss10Feed`
27 * `sfRss201Feed`
28 * `sfRss091Feed`
29
30Last but not least, the most important (and smart) class is the feed manager, which contains only static methods:
31
32 * `sfFeedPeer`
33
34Unit tests are available in the SVN repository.
35
36== Installation ==
37
38Installation of the plugin differs on the version of symfony you are using.
39
40  * Install the plugin (symfony 1.0):
41{{{
42$ symfony plugin-install http://plugins.symfony-project.com/sfFeed2Plugin
43}}}
44
45  * Install the plugin (symfony 1.1+):
46{{{
47$ symfony plugin:install sfFeed2Plugin
48}}}
49
50  * 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
51
52  * Clear the cache to enable the autoloading to find the new class
53{{{
54$ symfony cc
55}}}
56
57== Tutorials ==
58
59=== Building a feed from an array of objects ===
60
61==== Example data ====
62
63Let's take an example of a simple blog application with a `Post` and an `Author` table:
64
65||''Post''    || ''Author''
66||id          || id
67||author_id   || first_name
68||title       || last_name
69||description || email
70||body        ||
71||created_at  ||
72
73The `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:
74
75{{{
76public function getStrippedTitle()
77{
78  $text = strtolower($this->getTitle());
79
80  // strip all non word chars
81  $text = preg_replace('/\W/', ' ', $text);
82  // replace all white space sections with a dash
83  $text = preg_replace('/\ +/', '-', $text);
84  // trim dashes
85  $text = preg_replace('/\-$/', '', $text);
86  $text = preg_replace('/^\-/', '', $text);
87
88  return $text;
89}
90}}}
91
92The `Author` class is extended by a custom `->getName()` method as follows:
93
94{{{
95public function getName()
96{
97  return $this->getFirstName().' '.$this->getLastName();
98}
99}}}
100
101If 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].
102
103The `routing.yml` contains the following rule:
104
105{{{
106post:
107    url:   /permalink/:stripped_title
108    param: { module: post, action: read }
109}}}
110
111If 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].
112
113A special `feed` module is built for the occasion, and all the actions and templates will be placed in it.
114
115{{{$ symfony init-module myapp feed}}}
116
117==== Expected result ====
118
119The 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:
120
121{{{
122<?xml version="1.0" encoding="utf-8"?>
123<feed xmlns="http://www.w3.org/2005/Atom">
124
125  <title>The mouse blog</title>
126  <link href="http://www.myblog.com/" />
127  <updated>2005-12-11T16:23:51Z</updated>
128  <author>
129    <name>Peter Clive</name>
130    <author_email>pclive@myblog.com</author_email>
131  </author>
132  <id>4543D55FF756G734</id>
133  <icon>http://www.myblog.com/favicon.ico</icon>
134  <entry>
135    <title>I love mice</title>
136    <link href="http://www.myblog.com/permalink/i-love-mice" />
137    <id>i-love-mice</id>
138    <author>
139      <name>Peter Clive</name>
140      <author_email>pclive@myblog.com</author_email>
141    </author>
142    <updated>2005-12-11T16:23:51Z</updated>
143    <summary>Ever since I bought my first mouse, I can't live without one.</summary>
144  </entry>
145
146  <entry>
147    <title>A mouse is better than a fish</title>
148    <link href="http://www.myblog.com/permalink/a-mouse-is-better-than-a-fish" />
149    <id>a-mouse-is-better-than-a-fish</id>
150    <author>
151      <name>Bob Walter</name>
152      <author_email>bwalter@myblog.com</author_email>
153    </author>
154    <updated>2005-12-09T09:11:42Z</updated>
155    <summary>I had a fish for four years, and now I'm sick. They smell.</summary>
156  </entry>
157
158</feed>
159}}}
160
161==== Using the creators and setters ====
162
163To 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:
164
165{{{
166public function executeLastPosts()
167{
168  $feed = new sfAtom1Feed();
169
170  $feed->setTitle('The mouse blog');
171  $feed->setLink('http://www.myblog.com/');
172  $feed->setAuthorEmail('pclive@myblog.com');
173  $feed->setAuthorName('Peter Clive');
174
175  $feedImage = new sfFeedImage();
176  $feedImage->setFavicon('http://www.myblog.com/favicon.ico');
177  $feed->setImage($feedImage);
178
179  $c = new Criteria;
180  $c->addDescendingOrderByColumn(PostPeer::CREATED_AT);
181  $c->setLimit(5);
182  $posts = PostPeer::doSelect($c);
183
184  foreach ($posts as $post)
185  {
186    $item = new sfFeedItem();
187    $item->setTitle($post->getTitle());
188    $item->setLink('@permalink?stripped_title='.$post->getStrippedTitle());
189    $item->setAuthorName($post->getAuthor()->getName());
190    $item->setAuthorEmail($post->getAuthor()->getEmail());
191    $item->setPubdate($post->getCreatedAt('U'));
192    $item->setUniqueId($post->getStrippedTitle());
193    $item->setDescription($post->getDescription());
194
195    $feed->addItem($item);
196  }
197
198  $this->feed = $feed;
199}
200}}}
201
202At 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:
203
204{{{
205<?php decorate_with(false) ?>
206<?php echo $feed->asXml(ESC_RAW) ?>
207}}}
208
209The content type is automatically set by the `asXML()` method, depending on the feed format (Atom1 in this example).
210
211When called from a feed aggregator, the result of the action is now exactly the Atom feed described above:
212
213{{{http://www.myblog.com/feed/lastPosts}}}
214
215==== Using the `initialize()` method ====
216
217The 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:
218
219{{{
220public function executeLastPosts()
221{
222  $feed = new sfAtom1Feed();
223
224  $feed->initialize(array(
225    'title'       => 'The mouse blog',
226    'link'        => 'http://www.myblog.com/',
227    'authorEmail' => 'pclive@myblog.com',
228    'authorName'  => 'Peter Clive'
229  ));
230
231  $c = new Criteria;
232  $c->addDescendingOrderByColumn(PostPeer::CREATED_AT);
233  $c->setLimit(5);
234  $posts = PostPeer::doSelect($c);
235
236  foreach ($posts as $post)
237  {
238    $item = new sfFeedItem();
239    $item->initialize(array(
240      'title'       => $post->getTitle(),
241      'link'        => '@permalink?stripped_title='.$post->getStrippedTitle(),
242      'authorName'  => $post->getAuthor()->getName(),
243      'authorEmail' => $post->getAuthor()->getEmail(),
244      'pubDate'     => $post->getCreatedAt(),
245      'uniqueId'    => $post->getStrippedTitle(),
246      'description' => $post->getDescription(),
247    ));
248
249    $feed->addItem($item);
250  }
251
252  $this->feed = $feed;
253}
254}}}
255
256It has exactly the same effect as the previous listing, but the syntax is clearer.
257
258==== Using the object converter ====
259
260As 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:
261
262{{{
263public function executeLastPosts()
264{
265  $feed = new sfAtom1Feed();
266
267  $feed->initialize(array(
268    'title'       => 'The mouse blog',
269    'link'        => 'http://www.myblog.com/',
270    'authorEmail' => 'pclive@myblog.com',
271    'authorName'  => 'Peter Clive'
272  ));
273
274  $c = new Criteria;
275  $c->addDescendingOrderByColumn(PostPeer::CREATED_AT);
276  $c->setLimit(5);
277  $posts = PostPeer::doSelect($c);
278
279  $postItems = sfFeedPeer::convertObjectsToItems($posts, array('routeName' => '@permalink'))
280  $feed->addItems($postItems);
281
282  $this->feed = $feed;
283}
284}}}
285
286The rules governing the `sfFeedPeer::convertObjectsToItems` algorithm are as follows:
287
288 * To set the item `title`, it looks for a `getFeedTitle()`, a `getTitle()`, a `getName()` or a `__toString()` method.
289
290    In the example, the `Post` object has a `getName()` method.
291
292 * 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.
293
294    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.
295
296 * 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.
297
298    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.
299
300 * To set the publication date, it looks for a `getFeedPubdate()`, `getPubdate()`, `getCreatedAt()` or a `getDate()` method.
301
302    In the example, the `Post` object has a `getCreatedAt`
303
304The 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.
305
306All 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.
307
308==== Defining custom values for the feed ====
309
310In 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.
311
312For 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:
313
314{{{
315public function getFeedAuthorEmail()
316{
317  return '';
318}
319}}}
320
321This method will be found before the `getAuthor()` method, and the feed will not disclose the publishers' email addresses.
322
323The 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:
324
325{{{
326  $postItems = sfFeedPeer::convertObjectsToItems($posts, array(
327    'routeName' => '@permalink',
328    'methods'   => array(
329      'authorEmail' => '',
330      'authorName'  => 'getUserFirstName'
331    )
332  ));
333}}}
334
335You can also pass some arguments to the user defined method by passing an array composed of the method name and an array of arguments:
336
337{{{
338  $postItems = sfFeedPeer::convertObjectsToItems($posts, array(
339    'routeName' => '@permalink',
340    'methods'   => array(
341      'authorEmail' => '',
342      'authorName'  => 'getUserFirstName',
343      'pubdate'     => array('getPublishedAtDate', array('U')),
344    )
345  ));
346}}}
347
348==== Using the sfFeedPeer static methods ====
349
350The `sfFeedPeer` class offer helper methods that facilitate the creation and population of feed items.
351
352When 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:
353
354{{{
355$feed = sfFeedPeer::newInstance('atom1');
356// same as
357$feed = new sfAtom1Feed();
358}}}
359
360The steps described in the `executeLastPosts` listing occur in almost every feed construction process, so the `sfFeedPeer` can reduce the code above to a simpler:
361
362{{{
363public function executeLastPosts()
364{
365  $c = new Criteria;
366  $c->addDescendingOrderByColumn(PostPeer::CREATED_AT);
367  $c->setLimit(5);
368  $posts = PostPeer::doSelect($c);
369
370  $this->feed = sfFeedPeer::createFromObjects(
371    $posts,
372    array(
373      'format'      => 'atom1',
374      'title'       => 'The mouse blog',
375      'link'        => 'http://www.myblog.com/',
376      'authorEmail' => 'pclive@myblog.com',
377      'authorName'  => 'Peter Clive'
378      'routeName'   => '@permalink',
379      'methods'     => array('authorEmail' => '', 'authorName'  => 'getUserFirstName')
380    )
381  );
382}
383}}}
384
385==== Using other formats ====
386
387The methods described below can be transposed to build other RSS feeds. Simply change the parameter given to the feed factory:
388
389{{{
390// Atom 1
391$feed = sfFeedPeer::newInstance('atom1');
392// RSS 1.0 RDF Site Summary
393$feed = sfFeedPeer::newInstance('rss10');
394// RSS 0.91 Userland
395$feed = sfFeedPeer::newInstance('rss091');
396// RSS 2.01 rev6
397$feed = sfFeedPeer::newInstance('rss201');
398}}}
399
400=== Fetching a feed from the web and displaying it ===
401
402You 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:
403
404{{{
405public function executeLastPosts()
406{
407  $uri = 'http://groups.google.com/group/symfony-users/feed/rss_v2_0_msgs.xml';
408
409  $browser = new sfWebBrowser(array(
410      'user_agent' => 'sfFeedReader/0.9',
411      'timeout'    => 5
412  ));
413  $feedString = $browser->get($uri)->getResponseText();
414
415  $feed = new sfRssFeed();
416  $feed->setUrl($uri);
417  $feed->fromXml($feedString);
418  $this->feed = $feed;
419}
420}}}
421
422Thanks to the `sfFeedPeer` shortcuts, this can be reduced to a single line:
423
424{{{
425public function executeLastPosts()
426{
427  $this->feed = sfFeedPeer::createFromWeb('http://groups.google.com/group/symfony-users/feed/rss_v2_0_msgs.xml');
428}
429}}}
430
431The `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.
432
433Once the feed is built, it is very easy to use it for the display in the template:
434
435{{{
436<h2>Latests posts from the mailing-list</h2>
437<ul>
438  <?php foreach($feed->getItems() as $post): ?>
439  <li>
440    <?php echo format_date($post->getPubDate(), 'd/MM H:mm') ?> -
441    <?php echo link_to($post->getTitle(), $post->getLink()) ?>
442    by <?php echo $post->getAuthorName() ?>
443  </li>
444  <?php endforeach; ?>
445</ul>
446}}}
447
448=== Aggregating several feeds ===
449
450The `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.
451
452For 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:
453
454{{{
455public function executeLastPosts()
456{
457  $feed1 = sfFeedPeer::createFromWeb('http://groups.google.com/group/symfony-users/feed/rss_v2_0_msgs.xml');
458  $feed2 = sfFeedPeer::createFromWeb('http://groups.google.com/group/symfony-devs/feed/rss_v2_0_msgs.xml');
459  $this->feed = sfFeedPeer::aggregate(array($feed1, $feed2), array('limit' => 10));
460}
461}}}
462
463By 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.
464
465=== Adding an image to the feed ===
466
467To add an image to the feed, use the sfFeedImage class
468
469{{{
470$feed = new sfRss201Feed();
471
472$feedImage = new sfFeedImage();
473$feedImage->setLink('http://www.example.org/images/feed-image.png');
474$feedImage->setTitle('My Title');
475$feed->setImage($feedImage)
476}}}}
477
478== TODO ==
479
480 * unit test the `sfFeedPeer` class
481 * Populate feedItems from a pager rather than from an array of objects
482 * Deal with time zones (i.e. store dates in GMT, and handle input and output with a time zone)
483
484== Changelog ==
485
486== Trunk ==
487
488 * fabien: fixed response content-type encoding
489 * francois: Fixed encoding inside CDATA sections
490 * fabien: fixed routing for symfony 1.1 and optimized the 1.0 version
491 * fabien: added the possibility to pass arguments to methods that convert an object to an item
492 * Pascal.Borreli : symfony coding practices : removed lib closing tag (#2657)
493 * Fabian Lange : added image capabilities (#2551)
494
495=== 2007-04-15 | 0.9.4 Beta ===
496
497 * francois: Added a `toXML` method to `sfRssFeed`, `sfRss10Feed` and `sfAtom1Feed` (based on an idea from Frank Stelzer)
498 * Frank.Stelzer: Added a new `sfFeedPeer::createFromXml()` method
499 * Frank.Stelzer: Fixed errors in the documentation where `sfFeed` methods were described
500 * francois: `sfFeedPeer::createFromWeb()` now throws an exception whenever the fetched URL returns an error
501 * Markus.Staab: Fixed a typo in `sfFeed` constructor
502 * francois: Fixed a warning in `sfRssFeed` causing badly formatted feeds in dev env
503 * francois: '''BC break''' `sfFeedPeer::convertObjectsToItems()` signature changed (`$objects, $options = array()`) to have similar signature to that of `sfFeedPeer::createFromObjects()`
504 * francois: Added the ability to define one method per feed property in `sfFeedPeer::convertObjectsToItems()`
505 * francois: `sfFeedPeer::convertObjectsToItems()` now populates the feed item content
506 * francois: `sfFeedPeer::createFromWeb()` can now use a custom `userAgent` option to be seen as a custom user agent from the outside
507 * francois: `sfFeedPeer::createFromWeb()` has a better detection of Atom feeds
508 * francois: Fixed a bug in generated Atom1 feeds (summary attribute)
509 * francois: Fixed a bug in `sfFeedPeer::aggregate()` when defining a feed format
510
511=== 2007-03-13 | 0.9.3 Beta ===
512
513 * francois: `sfFeedPeer::createFromWeb()` is smarter at determining the type of feed it reads
514 * francois: Added smart guessing of publication date for RSS and Atom feeds, based on URL scheme
515 * francois: `sfFeedPeer::aggregate()` now handles items with the same date without overriding one.
516 * francois: Fixed two bugs in `sfFeedPeer` preventing items creation from objects
517 * francois: Items aggregated in `sfFeedPeer::aggregate` now remember their original feed properties
518 * francois: `sfFeedPeer::aggregate` can now limit the total number of items returned
519 * francois: Added `sfFeed::keepOnlyItems($count)` method
520 * francois: `sfRssReed` now looks for a `dc:creator` of no regular author is found
521
522=== 2007-03-03 | 0.9.2 Beta ===
523
524 * francois: Added a way to get an item description based on a content when no description exists
525 * francois: Added `<content:encoded>` handling in RSS 1 and 2 feeds (based on a patch from Jeff Merlet)
526
527=== 2007-02-22 | 0.9.1 Beta ===
528
529 * francois: Added the `sfRss10Feed` class and unit tests
530 * francois: Changed feed type detection technique to avoid building a simpleXML element twice
531
532=== 2007-02-21 | 0.9.0 Beta ===
533
534 * francois: Added more unit tests
535 * francois: Improved `sfRssFeed` conversions
536 * francois: Moved `getLatestPostDate()` method to `sfFeed` and unit tested it.
537 * 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`.
538
539=== 2007-02-19 | 0.8.1 Alpha ===
540
541 * francois: Added much more unit tests
542 * francois: Improved `sfAtom1Feed` conversions
543 * francois: Added `__toString()` and `initialize()` methods to `sfFeedEnclosure`
544 * francois: Added `content` property to `sfFeedItem`
545 * francois: '''BC break''' `fromXML()` methods now expect a string rather than a simpleXML object
546 * francois: '''BC break''' `sfFeedPeer::createFromObjects()` signature changed ($objects, $parameters = array())
547
548
549=== 2007-02-18 | 0.8.0 Alpha ===
550
551 * francois: Initial release
Note: See TracBrowser for help on using the repository browser.