1: <?php
2:
3: namespace Alpha\Util\Feed;
4:
5: use Alpha\Util\Logging\Logger;
6: use Alpha\Exception\IllegalArguementException;
7: use Alpha\Model\ActiveRecord;
8: use DOMDocument;
9: use DOMElement;
10:
11: /**
12: * Base feed class for generating syndication feeds.
13: *
14: * @since 1.0
15: *
16: * @author John Collins <dev@alphaframework.org>
17: * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
18: * @copyright Copyright (c) 2015, John Collins (founder of Alpha Framework).
19: * All rights reserved.
20: *
21: * <pre>
22: * Redistribution and use in source and binary forms, with or
23: * without modification, are permitted provided that the
24: * following conditions are met:
25: *
26: * * Redistributions of source code must retain the above
27: * copyright notice, this list of conditions and the
28: * following disclaimer.
29: * * Redistributions in binary form must reproduce the above
30: * copyright notice, this list of conditions and the
31: * following disclaimer in the documentation and/or other
32: * materials provided with the distribution.
33: * * Neither the name of the Alpha Framework nor the names
34: * of its contributors may be used to endorse or promote
35: * products derived from this software without specific
36: * prior written permission.
37: *
38: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
39: * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
40: * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
41: * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
42: * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
43: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
44: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
45: * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
46: * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
47: * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
48: * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
49: * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
50: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
51: * </pre>
52: */
53: abstract class Feed
54: {
55: /**
56: * The DOMDocument object used to create the feed.
57: *
58: * @var DOMDocument
59: *
60: * @since 1.0
61: */
62: protected $rssDoc;
63:
64: /**
65: * The DOMElement object used to hold the item or entry elements.
66: *
67: * @var DOMElement
68: *
69: * @since 1.0
70: */
71: protected $docElement;
72:
73: /**
74: * Holds the DOMElement to which metadata is added for the feed.
75: *
76: * @var DOMElement
77: *
78: * @since 1.0
79: */
80: protected $root;
81:
82: /**
83: * The actual root tag used in each feed type.
84: *
85: * @var string
86: *
87: * @since 1.0
88: */
89: protected $rootTag;
90:
91: /**
92: * An array of feed items.
93: *
94: * @var array
95: *
96: * @since 1.0
97: */
98: protected $items;
99:
100: /**
101: * If the feed format has a channel or not.
102: *
103: * @var bool
104: *
105: * @since 1.0
106: */
107: protected $hasChannel = true;
108:
109: /**
110: * Maps the tags to the feed-specific tags.
111: *
112: * @var array
113: *
114: * @since 1.0
115: */
116: protected $tagMap = array('item' => 'item', 'feeddesc' => 'description', 'itemdesc' => 'description');
117:
118: /**
119: * The BO which we will serve up in this feed.
120: *
121: * @var Alpha\Model\ActiveRecord
122: *
123: * @since 1.0
124: */
125: private $BO;
126:
127: /**
128: * An array containing BO field names -> RSS field name mappings.
129: *
130: * @var array
131: *
132: * @since 1.0
133: */
134: protected $fieldNameMappings;
135:
136: /**
137: * The XML namespace to use in the generated feed.
138: *
139: * @var string
140: */
141: protected $nameSpace;
142:
143: /**
144: * Trace logger.
145: *
146: * @var Alpha\Util\Loggin\Logger
147: *
148: * @since 1.0
149: */
150: private static $logger = null;
151:
152: /**
153: * The constructor.
154: *
155: * @param string $BOName The fully-qualifified classname of the BO to render a feed for.
156: * @param string $title The title of the feed.
157: * @param string $url The base URL for the feed.
158: * @param string $description The description of the feed.
159: * @param string $pubDate The publish date, only used in Atom feeds.
160: * @param int $id The feed id, only used in Atom feeds.
161: * @param int $limit The amount of items to render in the feed.
162: *
163: * @throws IllegalArguementException
164: *
165: * @since 1.0
166: */
167: public function __construct($BOName, $title, $url, $description, $pubDate = null, $id = null, $limit = 10)
168: {
169: self::$logger = new Logger('Feed');
170: self::$logger->debug('>>__construct(BOName=['.$BOName.'], title=['.$title.'], url=['.$url.'], description=['.$description.'], pubDate=['.$pubDate.'], id=['.$id.'], limit=['.$limit.'])');
171:
172: $this->rssDoc = new DOMDocument();
173: $this->rssDoc->loadXML($this->rootTag);
174: $this->docElement = $this->rssDoc->documentElement;
175:
176: if (!class_exists($BOName)) {
177: throw new IllegalArguementException('Unable to load the class definition for the class ['.$BOName.'] while trying to generate a feed!');
178: }
179:
180: $this->BO = new $BOName();
181:
182: if ($this->hasChannel) {
183: $root = $this->createFeedElement('channel');
184: $this->root = $this->docElement->appendChild($root);
185: } else {
186: $this->root = $this->docElement;
187: }
188:
189: $this->createRSSNode('feed', $this->root, $title, $url, $description, $pubDate, $id);
190:
191: self::$logger->debug('<<__construct');
192: }
193:
194: /**
195: * Method to load all of the BO items to the feed from the database, from the newest to the
196: * $limit provided.
197: *
198: * @param int $limit The amount of items to render in the feed.
199: * @param string $sortBy The name of the field to sort the feed by.
200: *
201: * @since 1.0
202: */
203: public function loadBOs($limit, $sortBy)
204: {
205: $BOs = $this->BO->loadAll(0, $limit, $sortBy, 'DESC');
206:
207: ActiveRecord::disconnect();
208:
209: foreach ($BOs as $BO) {
210: $this->addBO($BO);
211: }
212: }
213:
214: /**
215: * Method for adding a BO to the current feed.
216: *
217: * @param Alpha\Model\ActiveRecord $BO
218: */
219: public function addBO($BO)
220: {
221: $title = $BO->get($this->fieldNameMappings['title']);
222: $url = $BO->get($this->fieldNameMappings['url']);
223:
224: if (isset($this->fieldNameMappings['description'])) {
225: $description = $BO->get($this->fieldNameMappings['description']);
226: } else {
227: $description = '';
228: }
229:
230: if (isset($this->fieldNameMappings['pubDate'])) {
231: $dateTS = strtotime($BO->get($this->fieldNameMappings['pubDate']));
232: $pubDate = date(DATE_ATOM, $dateTS);
233: } else {
234: $pubDate = '';
235: }
236:
237: if (isset($this->fieldNameMappings['id'])) {
238: $id = $BO->get($this->fieldNameMappings['id']);
239: } else {
240: $id = '';
241: }
242:
243: $this->addItem($title, $url, $description, $pubDate, $id);
244: }
245:
246: /**
247: * Method for mapping BO fieldnames to feed field names.
248: *
249: * @param string $title The title of the feed.
250: * @param string $url The base URL for the feed.
251: * @param string $description The description of the feed.
252: * @param string $pubDate The publish date, only used in Atom feeds.
253: * @param int $id The feed id, only used in Atom feeds.
254: *
255: * @since 1.0
256: */
257: public function setFieldMappings($title, $url, $description = null, $pubDate = null, $id = null)
258: {
259: $this->fieldNameMappings = array(
260: 'title' => $title,
261: 'url' => $url,
262: );
263:
264: if (isset($description)) {
265: $this->fieldNameMappings['description'] = $description;
266: }
267:
268: if (isset($pubDate)) {
269: $this->fieldNameMappings['pubDate'] = $pubDate;
270: }
271:
272: if (isset($id)) {
273: $this->fieldNameMappings['id'] = $id;
274: }
275: }
276:
277: /**
278: * Method for creating a new feed element.
279: *
280: * @param string $name The name of the element.
281: * @param string $value The value of the element.
282: *
283: * @return DOMElement
284: *
285: * @since 1.0
286: */
287: protected function createFeedElement($name, $value = null)
288: {
289: $value = htmlspecialchars($value);
290:
291: if ($this->nameSpace == null) {
292: return $this->rssDoc->createElement($name, $value);
293: } else {
294: return $this->rssDoc->createElementNS($this->nameSpace, $name, $value);
295: }
296: }
297:
298: /**
299: * Method for creating link elements (note that Atom has a different format).
300: *
301: * @param DOMElement $parent The parent element.
302: * @param string $url The URL for the link.
303: *
304: * @since 1.0
305: */
306: protected function createLink($parent, $url)
307: {
308: $link = $this->createFeedElement('link', $url);
309: $parent->appendChild($link);
310: }
311:
312: /**
313: * Method for creating an RSS node with a title, url and description.
314: *
315: * @param int $type Can be either (item|feed) to indicate the type of node we are creating.
316: * @param DOMElement $parent The parent element.
317: * @param string $title The title of the feed.
318: * @param string $url The base URL for the feed.
319: * @param string $description The description of the feed.
320: * @param string $pubDate The publish date, only used in Atom feeds.
321: * @param int $id The feed id, only used in Atom feeds.
322: *
323: * @since 1.0
324: *
325: * @throws Alpha\Exception\IllegalArguementException
326: */
327: protected function createRSSNode($type, $parent, $title, $url, $description, $pubDate = null, $id = null)
328: {
329: $this->createLink($parent, $url);
330: $title = $this->createFeedElement('title', $title);
331: $parent->appendChild($title);
332:
333: if ($type == 'item') {
334: $titletag = $this->tagMap['itemdesc'];
335: } elseif ($type == 'feed') {
336: $titletag = $this->tagMap['feeddesc'];
337: } else {
338: throw new IllegalArguementException('The type paramater ['.$type.'] provided is invalid!');
339: }
340:
341: $description = $this->createFeedElement($titletag, $description);
342: $parent->appendChild($description);
343:
344: // id elements and updated elements are just for Atom!
345: if ($id != null) {
346: $idnode = $this->createFeedElement('id', $id);
347: $parent->appendChild($idnode);
348: }
349:
350: if ($pubDate != null) {
351: $datenode = $this->createFeedElement('updated', $pubDate);
352: $parent->appendChild($datenode);
353: }
354: }
355:
356: /**
357: * Method for adding an item to a feed.
358: *
359: * @param string $title The title of the feed.
360: * @param string $url The base URL for the feed.
361: * @param string $description The description of the feed.
362: * @param string $pubDate The publish date, only used in Atom feeds.
363: * @param int $id The feed id, only used in Atom feeds.
364: *
365: * @since 1.0
366: */
367: protected function addItem($title, $url, $description = null, $pubDate = null, $id = null)
368: {
369: $item = $this->createFeedElement($this->tagMap['item']);
370:
371: if ($this->docElement->appendChild($item)) {
372: $this->createRSSNode('item', $item, $title, $url, $description, $pubDate, $id);
373: }
374: }
375:
376: /**
377: * Returns the formatted XML for the feed as a string.
378: *
379: * @return string
380: *
381: * @since 1.0
382: */
383: public function render()
384: {
385: if ($this->rssDoc) {
386: $this->rssDoc->formatOutput = true;
387:
388: return $this->rssDoc->saveXML();
389: } else {
390: return '';
391: }
392: }
393: }
394: