PHP Universal Feed Generator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

175 lines
4.6 KiB

  1. <?php
  2. namespace FeedWriter;
  3. use \Iterator;
  4. use \Exception;
  5. use \stdClass;
  6. use \ArrayIterator;
  7. class Feed {
  8. const RSS1 = 'RSS 1.0';
  9. const RSS2 = 'RSS 2.0';
  10. const ATOM = 'ATOM';
  11. protected $_iter = null;
  12. protected $_map = [];
  13. protected $_views = [];
  14. /**
  15. * Generate one or more views of a collection from a given iterator or array.
  16. *
  17. * Takes a data source, an array describing a mapping from instances of the
  18. * collection to items in the view, and a list of views to generate.
  19. *
  20. * This all sounds more abstract than it actually is, so let's run through
  21. * some examples:
  22. *
  23. * <code>
  24. *
  25. * use \FeedWriter\Feed;
  26. * use \FeedWriter\View\Atom;
  27. *
  28. * // Here's an array of arrays describing some blog posts. These could also
  29. * // be objects with properties like $blog_posts->writer.
  30. *
  31. * $blog_posts = [
  32. * [ 'date_created' => '2013-02-14', 'writer' => 'Brennen Bearnes', 'text' => 'Hate.' ],
  33. * [ 'date_created' => '2013-07-04', 'writer' => 'Brennen Bearnes', 'text' => 'Explosions.' ],
  34. * [ 'date_created' => '2013-08-31', 'writer' => 'Brennen Bearnes', 'text' => 'A feed thingy.' ],
  35. * ];
  36. *
  37. * // Here's a map that explains how to find the properties we need for our
  38. * // view:
  39. * $entry_from_post = [
  40. * 'date' => 'date_created',
  41. * 'author' => 'writer',
  42. * 'content' => 'text'
  43. * ];
  44. *
  45. * $feed = new Feed($blog_posts, $entry_from_post, ['atom' => new Atom]);
  46. *
  47. * // If everything went ok, views are now available as properties of the feed.
  48. * if (! $feed->error())
  49. * echo $feed->atom->render();
  50. * else
  51. * echo 'error: ' . $feed->error();
  52. *
  53. * </code>
  54. *
  55. * @param $source mixed iterator or array
  56. * @param $map array describing mapping of fields to fields
  57. * @param $views array one or more views to generate
  58. */
  59. public function __construct ($source, array $map, array $views)
  60. {
  61. if (is_array($source))
  62. $iter = new ArrayIterator($source);
  63. else
  64. $iter = $source;
  65. $this->setIter($iter);
  66. $this->setViewsAndMap($views, $map);
  67. $this->spin();
  68. }
  69. /**
  70. * Explode if anybody tries to set a property directly.
  71. */
  72. public function __set ($name, $view)
  73. {
  74. throw new Exception("You can't directly set properties on Feed. See documentation for constructor.");
  75. }
  76. /**
  77. * Get a view.
  78. */
  79. public function __get ($name)
  80. {
  81. return $this->_views[$name];
  82. }
  83. /**
  84. * Is a given view set?
  85. */
  86. public function __isset ($name)
  87. {
  88. return isset($this->_views[$name]);
  89. }
  90. /**
  91. * Stash views and map, making sure that the map provides for anything
  92. * the view explicitly requires.
  93. */
  94. protected function setViewsAndMap (array $views, array $map)
  95. {
  96. if (! count($views))
  97. throw new Exception('Feed requires that at least one view instance be passed in)');
  98. if (! is_array($map))
  99. throw new Exception('$map must be an array');
  100. if (! count($map))
  101. throw new Exception('$map must provide values');
  102. // Validate map for each view:
  103. foreach ($views as $name => $view) {
  104. if (! isset($view->require))
  105. continue;
  106. foreach ($view->require as $field) {
  107. if (! isset($map[ $field ])) {
  108. throw new Exception('$map should provide a mapping for ' . $field);
  109. }
  110. }
  111. }
  112. $this->_views = $views;
  113. $this->_map = $map;
  114. }
  115. protected function setIter (Iterator $iter)
  116. {
  117. if (! ($iter instanceof Iterator))
  118. throw new Exception('$iter must be an iterator');
  119. $this->_iter = $iter;
  120. }
  121. /**
  122. * Do the business of handing off values to views.
  123. */
  124. protected function spin ()
  125. {
  126. $output_item = (object)[];
  127. while ($this->_iter->valid()) {
  128. $input_item = $this->_iter->current();
  129. if (is_array($input_item))
  130. $input_item = (object)$input_item;
  131. foreach ($this->_map as $output_key => &$input_key) {
  132. if (is_string($input_key))
  133. {
  134. // shorthand for "use this field on the object for the value"
  135. if (isset($input_item->$input_key))
  136. $output_item->$output_key = $input_item->$input_key;
  137. else
  138. throw new Exception("unable to access $input_key on input item: " . print_r($input_item, 1));
  139. }
  140. elseif (is_callable($input_key))
  141. {
  142. // if we have a function, pass it the item and expect it to
  143. // return a value for our output key
  144. $output_item->$output_key = $input_key($input_item);
  145. }
  146. }
  147. foreach ($this->_views as $name => $view) {
  148. $view->collect($output_item);
  149. }
  150. $this->_iter->next();
  151. }
  152. }
  153. }