├── docker-compose.development.yml ├── .gitignore ├── Makefile ├── .travis.yml ├── tests ├── Bootstrap.php └── Suin │ └── RSSWriter │ ├── FeedTest.php │ ├── ChannelTest.php │ └── ItemTest.php ├── docker-compose.yml ├── src └── Suin │ └── RSSWriter │ ├── FeedInterface.php │ ├── SimpleXMLElement.php │ ├── Feed.php │ ├── ItemInterface.php │ ├── ChannelInterface.php │ ├── Item.php │ └── Channel.php ├── composer.json ├── phpunit.xml.dist ├── examples └── simple-feed.php └── README.md /docker-compose.development.yml: -------------------------------------------------------------------------------- 1 | docker-compose.yml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /composer.lock 4 | /tests/cover 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | docker-compose up php54 3 | docker-compose up php55 4 | docker-compose up php56 5 | docker-compose up php70 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | php: 6 | - 5.4 7 | - 5.5 8 | - 5.6 9 | - 7.0 10 | - master 11 | - hhvm 12 | 13 | matrix: 14 | fast_finish: true 15 | allow_failures: 16 | - php: master 17 | 18 | install: 19 | - composer install --prefer-dist 20 | 21 | script: 22 | - vendor/bin/phpunit --coverage-text 23 | 24 | cache: 25 | directories: 26 | - $HOME/.composer/cache 27 | -------------------------------------------------------------------------------- /tests/Bootstrap.php: -------------------------------------------------------------------------------- 1 | =5.4.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": ">=4.8.36 <6.0", 25 | "mockery/mockery": ">=0.9 <1.0", 26 | "suin/xoopsunit": ">=1.2" 27 | }, 28 | "autoload": { 29 | "psr-0": { 30 | "Suin\\RSSWriter": "src" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Suin/RSSWriter/SimpleXMLElement.php: -------------------------------------------------------------------------------- 1 | addChild($name, null, $namespace); 35 | $dom = dom_import_simplexml($element); 36 | $elementOwner = $dom->ownerDocument; 37 | $dom->appendChild($elementOwner->createCDATASection($value)); 38 | return $element; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | tests 11 | 12 | 13 | 14 | 15 | 23 | 24 | 25 | 26 | 27 | src 28 | 29 | src 30 | 31 | 32 | 33 | vendor 34 | 35 | 36 | 37 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Suin/RSSWriter/Feed.php: -------------------------------------------------------------------------------- 1 | channels[] = $channel; 24 | return $this; 25 | } 26 | 27 | /** 28 | * Render XML 29 | * @return string 30 | */ 31 | public function render() 32 | { 33 | $xml = new SimpleXMLElement('', LIBXML_NOERROR | LIBXML_ERR_NONE | LIBXML_ERR_FATAL); 34 | 35 | foreach ($this->channels as $channel) { 36 | $toDom = dom_import_simplexml($xml); 37 | $fromDom = dom_import_simplexml($channel->asXML()); 38 | $toDom->appendChild($toDom->ownerDocument->importNode($fromDom, true)); 39 | } 40 | 41 | $dom = new DOMDocument('1.0', 'UTF-8'); 42 | $dom->appendChild($dom->importNode(dom_import_simplexml($xml), true)); 43 | $dom->formatOutput = true; 44 | return $dom->saveXML(); 45 | } 46 | 47 | /** 48 | * Render XML 49 | * @return string 50 | */ 51 | public function __toString() 52 | { 53 | return $this->render(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/simple-feed.php: -------------------------------------------------------------------------------- 1 | title('Channel Title') 18 | ->description('Channel Description') 19 | ->url('http://blog.example.com') 20 | ->feedUrl('http://blog.example.com/rss') 21 | ->language('en-US') 22 | ->copyright('Copyright 2012, Foo Bar') 23 | ->pubDate(strtotime('Tue, 21 Aug 2012 19:50:37 +0900')) 24 | ->lastBuildDate(strtotime('Tue, 21 Aug 2012 19:50:37 +0900')) 25 | ->ttl(60) 26 | ->pubsubhubbub('http://example.com/feed.xml', 'http://pubsubhubbub.appspot.com') // This is optional. Specify PubSubHubbub discovery if you want. 27 | ->appendTo($feed); 28 | 29 | // Blog item 30 | $item = new Item(); 31 | $item 32 | ->title('Blog Entry Title') 33 | ->description('
Blog body
') 34 | ->contentEncoded('
Blog body
') 35 | ->url('http://blog.example.com/2012/08/21/blog-entry/') 36 | ->author('john@smith.com') 37 | ->creator('John Smith') 38 | ->pubDate(strtotime('Tue, 21 Aug 2012 19:50:37 +0900')) 39 | ->guid('http://blog.example.com/2012/08/21/blog-entry/', true) 40 | ->preferCdata(true) // By this, title and description become CDATA wrapped HTML. 41 | ->appendTo($channel); 42 | 43 | // Podcast item 44 | $item = new Item(); 45 | $item 46 | ->title('Some Podcast Entry') 47 | ->description('
Podcast body
') 48 | ->url('http://podcast.example.com/2012/08/21/podcast-entry/') 49 | ->enclosure('http://podcast.example.com/2012/08/21/podcast.mp3', 4889, 'audio/mpeg') 50 | ->appendTo($channel); 51 | 52 | echo $feed; // or echo $feed->render(); 53 | -------------------------------------------------------------------------------- /src/Suin/RSSWriter/ItemInterface.php: -------------------------------------------------------------------------------- 1 | channelInterface); 14 | $feed = new Feed(); 15 | $this->assertSame($feed, $feed->addChannel($channel)); 16 | $this->assertAttributeSame([$channel], 'channels', $feed); 17 | } 18 | 19 | public function testRender() 20 | { 21 | $feed = new Feed(); 22 | $xml1 = new SimpleXMLElement('channel1'); 23 | $xml2 = new SimpleXMLElement('channel2'); 24 | $xml3 = new SimpleXMLElement('channel3'); 25 | $channel1 = $this->createMock($this->channelInterface); 26 | $channel1->expects($this->once())->method('asXML')->will($this->returnValue($xml1)); 27 | $channel2 = $this->createMock($this->channelInterface); 28 | $channel2->expects($this->once())->method('asXML')->will($this->returnValue($xml2)); 29 | $channel3 = $this->createMock($this->channelInterface); 30 | $channel3->expects($this->once())->method('asXML')->will($this->returnValue($xml3)); 31 | $this->reveal($feed)->attr('channels', [$channel1, $channel2, $channel3]); 32 | $expect = ' 33 | 34 | channel1 35 | channel2 36 | channel3 37 | 38 | '; 39 | $this->assertXmlStringEqualsXmlString($expect, $feed->render()); 40 | } 41 | 42 | public function testRender_with_japanese() 43 | { 44 | $feed = new Feed(); 45 | $xml1 = new SimpleXMLElement('日本語1'); 46 | $xml2 = new SimpleXMLElement('日本語2'); 47 | $xml3 = new SimpleXMLElement('日本語3'); 48 | $channel1 = $this->createMock($this->channelInterface); 49 | $channel1->expects($this->once())->method('asXML')->will($this->returnValue($xml1)); 50 | $channel2 = $this->createMock($this->channelInterface); 51 | $channel2->expects($this->once())->method('asXML')->will($this->returnValue($xml2)); 52 | $channel3 = $this->createMock($this->channelInterface); 53 | $channel3->expects($this->once())->method('asXML')->will($this->returnValue($xml3)); 54 | $this->reveal($feed)->attr('channels', [$channel1, $channel2, $channel3]); 55 | $expect = <<< 'XML' 56 | 57 | 58 | 59 | 日本語1 60 | 61 | 62 | 日本語2 63 | 64 | 65 | 日本語3 66 | 67 | 68 | 69 | XML; 70 | $this->assertSame($expect, $feed->render()); 71 | 72 | } 73 | 74 | public function test__toString() 75 | { 76 | $feed = new Feed(); 77 | $xml1 = new SimpleXMLElement('channel1'); 78 | $xml2 = new SimpleXMLElement('channel2'); 79 | $xml3 = new SimpleXMLElement('channel3'); 80 | $channel1 = $this->createMock($this->channelInterface); 81 | $channel1->expects($this->once())->method('asXML')->will($this->returnValue($xml1)); 82 | $channel2 = $this->createMock($this->channelInterface); 83 | $channel2->expects($this->once())->method('asXML')->will($this->returnValue($xml2)); 84 | $channel3 = $this->createMock($this->channelInterface); 85 | $channel3->expects($this->once())->method('asXML')->will($this->returnValue($xml3)); 86 | $this->reveal($feed)->attr('channels', [$channel1, $channel2, $channel3]); 87 | $expect = ' 88 | 89 | channel1 90 | channel2 91 | channel3 92 | 93 | '; 94 | $this->assertXmlStringEqualsXmlString($expect, strval($feed)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # \Suin\RSSWriter 2 | 3 | `\Suin\RSSWriter` is yet another simple RSS writer library for PHP 5.4 or later. This component is Licensed under MIT license. 4 | 5 | This library can also be used to publish Podcasts. 6 | 7 | [![Latest Stable Version](https://poser.pugx.org/suin/php-rss-writer/v/stable)](https://packagist.org/packages/suin/php-rss-writer) 8 | [![Total Downloads](https://poser.pugx.org/suin/php-rss-writer/downloads)](https://packagist.org/packages/suin/php-rss-writer) 9 | [![Daily Downloads](https://poser.pugx.org/suin/php-rss-writer/d/daily)](https://packagist.org/packages/suin/php-rss-writer) 10 | [![License](https://poser.pugx.org/suin/php-rss-writer/license)](https://packagist.org/packages/suin/php-rss-writer) 11 | [![Build Status](https://travis-ci.org/suin/php-rss-writer.svg?branch=master)](https://travis-ci.org/suin/php-rss-writer) 12 | [![Codacy Badge](https://api.codacy.com/project/badge/grade/1c5e4e28e7e24f6ab7221b2166b5b6c7)](https://www.codacy.com/app/suinyeze/php-rss-writer) 13 | 14 | ## Quick demo 15 | 16 | 17 | ```php 18 | $feed = new Feed(); 19 | 20 | $channel = new Channel(); 21 | $channel 22 | ->title('Channel Title') 23 | ->description('Channel Description') 24 | ->url('http://blog.example.com') 25 | ->feedUrl('http://blog.example.com/rss') 26 | ->language('en-US') 27 | ->copyright('Copyright 2012, Foo Bar') 28 | ->pubDate(strtotime('Tue, 21 Aug 2012 19:50:37 +0900')) 29 | ->lastBuildDate(strtotime('Tue, 21 Aug 2012 19:50:37 +0900')) 30 | ->ttl(60) 31 | ->pubsubhubbub('http://example.com/feed.xml', 'http://pubsubhubbub.appspot.com') // This is optional. Specify PubSubHubbub discovery if you want. 32 | ->appendTo($feed); 33 | 34 | // Blog item 35 | $item = new Item(); 36 | $item 37 | ->title('Blog Entry Title') 38 | ->description('
Blog body
') 39 | ->contentEncoded('
Blog body
') 40 | ->url('http://blog.example.com/2012/08/21/blog-entry/') 41 | ->author('John Smith') 42 | ->pubDate(strtotime('Tue, 21 Aug 2012 19:50:37 +0900')) 43 | ->guid('http://blog.example.com/2012/08/21/blog-entry/', true) 44 | ->preferCdata(true) // By this, title and description become CDATA wrapped HTML. 45 | ->appendTo($channel); 46 | 47 | // Podcast item 48 | $item = new Item(); 49 | $item 50 | ->title('Some Podcast Entry') 51 | ->description('
Podcast body
') 52 | ->url('http://podcast.example.com/2012/08/21/podcast-entry/') 53 | ->enclosure('http://podcast.example.com/2012/08/21/podcast.mp3', 4889, 'audio/mpeg') 54 | ->appendTo($channel); 55 | 56 | echo $feed; // or echo $feed->render(); 57 | ``` 58 | 59 | Output: 60 | 61 | ```xml 62 | 63 | 64 | 65 | Channel Title 66 | http://blog.example.com 67 | Channel Description 68 | en-US 69 | Copyright 2012, Foo Bar 70 | Tue, 21 Aug 2012 10:50:37 +0000 71 | Tue, 21 Aug 2012 10:50:37 +0000 72 | 60 73 | 74 | 75 | 76 | <![CDATA[Blog Entry Title]]> 77 | http://blog.example.com/2012/08/21/blog-entry/ 78 | Blog body]]> 79 | Blog body]]> 80 | http://blog.example.com/2012/08/21/blog-entry/ 81 | Tue, 21 Aug 2012 10:50:37 +0000 82 | John Smith 83 | 84 | 85 | Some Podcast Entry 86 | http://podcast.example.com/2012/08/21/podcast-entry/ 87 | <div>Podcast body</div> 88 | 89 | 90 | 91 | 92 | ``` 93 | 94 | ## Installation 95 | 96 | ### Easy installation 97 | 98 | You can install directly via [Composer](https://getcomposer.org/): 99 | 100 | ```bash 101 | $ composer require suin/php-rss-writer 102 | ``` 103 | 104 | ### Manual installation 105 | 106 | Add the following code to your `composer.json` file: 107 | 108 | ```json 109 | { 110 | "require": { 111 | "suin/php-rss-writer": ">=1.0" 112 | } 113 | } 114 | ``` 115 | 116 | ...and run composer to install it: 117 | 118 | ```bash 119 | $ composer install 120 | ``` 121 | 122 | Finally, include `vendor/autoload.php` in your product: 123 | 124 | ```php 125 | require_once 'vendor/autoload.php'; 126 | ``` 127 | 128 | ## How to use 129 | 130 | The [`examples`](examples) directory contains usage examples for RSSWriter. 131 | 132 | If you want to know APIs, please see [`FeedInterface`](src/Suin/RSSWriter/FeedInterface.php), [`ChannelInterface`](src/Suin/RSSWriter/ChannelInterface.php) and [`ItemInterface`](src/Suin/RSSWriter/ItemInterface.php). 133 | 134 | ## How to Test 135 | 136 | ```sh 137 | $ vendor/bin/phpunit 138 | ``` 139 | 140 | ## Test through PHP 5.4 ~ PHP 7.0 141 | 142 | ```console 143 | $ docker-compose up 144 | ``` 145 | 146 | ## License 147 | 148 | MIT license 149 | -------------------------------------------------------------------------------- /src/Suin/RSSWriter/Item.php: -------------------------------------------------------------------------------- 1 | title = $title; 49 | return $this; 50 | } 51 | 52 | public function url($url) 53 | { 54 | $this->url = $url; 55 | return $this; 56 | } 57 | 58 | public function description($description) 59 | { 60 | $this->description = $description; 61 | return $this; 62 | } 63 | 64 | public function contentEncoded($content) 65 | { 66 | $this->contentEncoded = $content; 67 | return $this; 68 | } 69 | 70 | public function category($name, $domain = null) 71 | { 72 | $this->categories[] = [$name, $domain]; 73 | return $this; 74 | } 75 | 76 | public function categories(array $categories) 77 | { 78 | foreach ($categories as $cat) { 79 | $domain = null; 80 | if (is_array($cat) && !empty($cat)) { 81 | $domain = isset($cat[1]) ? $cat[1] : null; 82 | $cat = $cat[0]; 83 | } 84 | $this->category($cat, $domain); 85 | } 86 | return $this; 87 | } 88 | 89 | public function guid($guid, $isPermalink = false) 90 | { 91 | $this->guid = $guid; 92 | $this->isPermalink = $isPermalink; 93 | return $this; 94 | } 95 | 96 | public function pubDate($pubDate) 97 | { 98 | $this->pubDate = $pubDate; 99 | return $this; 100 | } 101 | 102 | public function enclosure($url, $length = 0, $type = 'audio/mpeg') 103 | { 104 | $this->enclosure = ['url' => $url, 'length' => $length, 'type' => $type]; 105 | return $this; 106 | } 107 | 108 | public function author($author) 109 | { 110 | $this->author = $author; 111 | return $this; 112 | } 113 | 114 | public function creator($creator) 115 | { 116 | $this->creator = $creator; 117 | return $this; 118 | } 119 | 120 | public function preferCdata($preferCdata) 121 | { 122 | $this->preferCdata = (bool)$preferCdata; 123 | return $this; 124 | } 125 | 126 | public function appendTo(ChannelInterface $channel) 127 | { 128 | $channel->addItem($this); 129 | return $this; 130 | } 131 | 132 | public function asXML() 133 | { 134 | $xml = new SimpleXMLElement('', LIBXML_NOERROR | LIBXML_ERR_NONE | LIBXML_ERR_FATAL); 135 | 136 | if ($this->title) { 137 | if ($this->preferCdata) { 138 | $xml->addCdataChild('title', $this->title); 139 | } else { 140 | $xml->addChild('title', $this->title); 141 | } 142 | } 143 | 144 | if ($this->url) { 145 | $xml->addChild('link', $this->url); 146 | } 147 | 148 | // At least one of or <description> must be present 149 | if ($this->description || ! $this->title) { 150 | if ($this->preferCdata) { 151 | $xml->addCdataChild('description', $this->description); 152 | } else { 153 | $xml->addChild('description', $this->description); 154 | } 155 | } 156 | 157 | if ($this->contentEncoded) { 158 | $xml->addCdataChild('xmlns:content:encoded', $this->contentEncoded); 159 | } 160 | 161 | foreach ($this->categories as $category) { 162 | $element = $xml->addChild('category', $category[0]); 163 | 164 | if (isset($category[1])) { 165 | $element->addAttribute('domain', $category[1]); 166 | } 167 | } 168 | 169 | if ($this->guid) { 170 | $guid = $xml->addChild('guid', $this->guid); 171 | 172 | if ($this->isPermalink === false) { 173 | $guid->addAttribute('isPermaLink', 'false'); 174 | } 175 | } 176 | 177 | if ($this->pubDate !== null) { 178 | $xml->addChild('pubDate', date(DATE_RSS, $this->pubDate)); 179 | } 180 | 181 | if (is_array($this->enclosure) && (count($this->enclosure) == 3)) { 182 | $element = $xml->addChild('enclosure'); 183 | $element->addAttribute('url', $this->enclosure['url']); 184 | $element->addAttribute('type', $this->enclosure['type']); 185 | 186 | if ($this->enclosure['length']) { 187 | $element->addAttribute('length', $this->enclosure['length']); 188 | } 189 | } 190 | 191 | if (!empty($this->author)) { 192 | $xml->addChild('author', $this->author); 193 | } 194 | 195 | if (!empty($this->creator)) { 196 | $xml->addChild('dc:creator', $this->creator,"http://purl.org/dc/elements/1.1/"); 197 | } 198 | 199 | return $xml; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Suin/RSSWriter/Channel.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace Suin\RSSWriter; 4 | 5 | /** 6 | * Class Channel 7 | * @package Suin\RSSWriter 8 | */ 9 | class Channel implements ChannelInterface 10 | { 11 | /** @var string */ 12 | protected $title; 13 | 14 | /** @var string */ 15 | protected $url; 16 | 17 | /** @var string */ 18 | protected $feedUrl; 19 | 20 | /** @var string */ 21 | protected $description; 22 | 23 | /** @var string */ 24 | protected $language; 25 | 26 | /** @var string */ 27 | protected $copyright; 28 | 29 | /** @var int */ 30 | protected $pubDate; 31 | 32 | /** @var int */ 33 | protected $lastBuildDate; 34 | 35 | /** @var int */ 36 | protected $ttl; 37 | 38 | /** @var string[] */ 39 | protected $pubsubhubbub; 40 | 41 | /** @var ItemInterface[] */ 42 | protected $items = []; 43 | 44 | /** 45 | * Set channel title 46 | * @param string $title 47 | * @return $this 48 | */ 49 | public function title($title) 50 | { 51 | $this->title = $title; 52 | return $this; 53 | } 54 | 55 | /** 56 | * Set channel URL 57 | * @param string $url 58 | * @return $this 59 | */ 60 | public function url($url) 61 | { 62 | $this->url = $url; 63 | return $this; 64 | } 65 | 66 | /** 67 | * Set URL of this feed 68 | * @param string $url 69 | * @return $this; 70 | */ 71 | public function feedUrl($url) 72 | { 73 | $this->feedUrl = $url; 74 | return $this; 75 | } 76 | 77 | /** 78 | * Set channel description 79 | * @param string $description 80 | * @return $this 81 | */ 82 | public function description($description) 83 | { 84 | $this->description = $description; 85 | return $this; 86 | } 87 | 88 | /** 89 | * Set ISO639 language code 90 | * 91 | * The language the channel is written in. This allows aggregators to group all 92 | * Italian language sites, for example, on a single page. A list of allowable 93 | * values for this element, as provided by Netscape, is here. You may also use 94 | * values defined by the W3C. 95 | * 96 | * @param string $language 97 | * @return $this 98 | */ 99 | public function language($language) 100 | { 101 | $this->language = $language; 102 | return $this; 103 | } 104 | 105 | /** 106 | * Set channel copyright 107 | * @param string $copyright 108 | * @return $this 109 | */ 110 | public function copyright($copyright) 111 | { 112 | $this->copyright = $copyright; 113 | return $this; 114 | } 115 | 116 | /** 117 | * Set channel published date 118 | * @param int $pubDate Unix timestamp 119 | * @return $this 120 | */ 121 | public function pubDate($pubDate) 122 | { 123 | $this->pubDate = $pubDate; 124 | return $this; 125 | } 126 | 127 | /** 128 | * Set channel last build date 129 | * @param int $lastBuildDate Unix timestamp 130 | * @return $this 131 | */ 132 | public function lastBuildDate($lastBuildDate) 133 | { 134 | $this->lastBuildDate = $lastBuildDate; 135 | return $this; 136 | } 137 | 138 | /** 139 | * Set channel ttl (minutes) 140 | * @param int $ttl 141 | * @return $this 142 | */ 143 | public function ttl($ttl) 144 | { 145 | $this->ttl = $ttl; 146 | return $this; 147 | } 148 | 149 | /** 150 | * Enable PubSubHubbub discovery 151 | * @param string $feedUrl 152 | * @param string $hubUrl 153 | * @return $this 154 | */ 155 | public function pubsubhubbub($feedUrl, $hubUrl) 156 | { 157 | $this->pubsubhubbub = [ 158 | 'feedUrl' => $feedUrl, 159 | 'hubUrl' => $hubUrl, 160 | ]; 161 | return $this; 162 | } 163 | 164 | /** 165 | * Add item object 166 | * @param ItemInterface $item 167 | * @return $this 168 | */ 169 | public function addItem(ItemInterface $item) 170 | { 171 | $this->items[] = $item; 172 | return $this; 173 | } 174 | 175 | /** 176 | * Append to feed 177 | * @param FeedInterface $feed 178 | * @return $this 179 | */ 180 | public function appendTo(FeedInterface $feed) 181 | { 182 | $feed->addChannel($this); 183 | return $this; 184 | } 185 | 186 | /** 187 | * Return XML object 188 | * @return SimpleXMLElement 189 | */ 190 | public function asXML() 191 | { 192 | $xml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8" ?><channel></channel>', LIBXML_NOERROR | LIBXML_ERR_NONE | LIBXML_ERR_FATAL); 193 | $xml->addChild('title', $this->title); 194 | $xml->addChild('link', $this->url); 195 | $xml->addChild('description', $this->description); 196 | 197 | if($this->feedUrl !== null) { 198 | $link = $xml->addChild('atom:link', '', "http://www.w3.org/2005/Atom"); 199 | $link->addAttribute('href',$this->feedUrl); 200 | $link->addAttribute('type','application/rss+xml'); 201 | $link->addAttribute('rel','self'); 202 | } 203 | 204 | if ($this->language !== null) { 205 | $xml->addChild('language', $this->language); 206 | } 207 | 208 | if ($this->copyright !== null) { 209 | $xml->addChild('copyright', $this->copyright); 210 | } 211 | 212 | if ($this->pubDate !== null) { 213 | $xml->addChild('pubDate', date(DATE_RSS, $this->pubDate)); 214 | } 215 | 216 | if ($this->lastBuildDate !== null) { 217 | $xml->addChild('lastBuildDate', date(DATE_RSS, $this->lastBuildDate)); 218 | } 219 | 220 | if ($this->ttl !== null) { 221 | $xml->addChild('ttl', $this->ttl); 222 | } 223 | 224 | if ($this->pubsubhubbub !== null) { 225 | $feedUrl = $xml->addChild('xmlns:atom:link'); 226 | $feedUrl->addAttribute('rel', 'self'); 227 | $feedUrl->addAttribute('href', $this->pubsubhubbub['feedUrl']); 228 | $feedUrl->addAttribute('type', 'application/rss+xml'); 229 | 230 | $hubUrl = $xml->addChild('xmlns:atom:link'); 231 | $hubUrl->addAttribute('rel', 'hub'); 232 | $hubUrl->addAttribute('href', $this->pubsubhubbub['hubUrl']); 233 | } 234 | 235 | foreach ($this->items as $item) { 236 | $toDom = dom_import_simplexml($xml); 237 | $fromDom = dom_import_simplexml($item->asXML()); 238 | $toDom->appendChild($toDom->ownerDocument->importNode($fromDom, true)); 239 | } 240 | 241 | return $xml; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /tests/Suin/RSSWriter/ChannelTest.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace Suin\RSSWriter; 4 | 5 | class ChannelTest extends \XoopsUnit\TestCase 6 | { 7 | private $itemInterface = '\Suin\RSSWriter\ItemInterface'; 8 | private $feedInterface = '\Suin\RSSWriter\FeedInterface'; 9 | 10 | public function testTitle() 11 | { 12 | $title = uniqid(); 13 | $channel = new Channel(); 14 | $this->assertSame($channel, $channel->title($title)); 15 | $this->assertAttributeSame($title, 'title', $channel); 16 | } 17 | 18 | public function testUrl() 19 | { 20 | $url = uniqid(); 21 | $channel = new Channel(); 22 | $this->assertSame($channel, $channel->url($url)); 23 | $this->assertAttributeSame($url, 'url', $channel); 24 | } 25 | 26 | public function testFeedUrl() 27 | { 28 | $channel = new Channel(); 29 | $this->assertSame($channel, $channel->feedUrl('http://example.com/feed.xml')); 30 | $feedUrlXml = '<atom:link xmlns:atom="http://www.w3.org/2005/Atom" href="http://example.com/feed.xml" type="application/rss+xml" rel="self"/>'; 31 | $this->assertContains($feedUrlXml, $channel->asXML()->asXML()); 32 | } 33 | 34 | public function testDescription() 35 | { 36 | $description = uniqid(); 37 | $channel = new Channel(); 38 | $this->assertSame($channel, $channel->description($description)); 39 | $this->assertAttributeSame($description, 'description', $channel); 40 | } 41 | 42 | public function testLanguage() 43 | { 44 | $language = uniqid(); 45 | $channel = new Channel(); 46 | $this->assertSame($channel, $channel->language($language)); 47 | $this->assertAttributeSame($language, 'language', $channel); 48 | } 49 | 50 | public function testCopyright() 51 | { 52 | $copyright = uniqid(); 53 | $channel = new Channel(); 54 | $this->assertSame($channel, $channel->copyright($copyright)); 55 | $this->assertAttributeSame($copyright, 'copyright', $channel); 56 | } 57 | 58 | public function testPubDate() 59 | { 60 | $pubDate = mt_rand(0, 9999999); 61 | $channel = new Channel(); 62 | $this->assertSame($channel, $channel->pubDate($pubDate)); 63 | $this->assertAttributeSame($pubDate, 'pubDate', $channel); 64 | } 65 | 66 | public function testLastBuildDate() 67 | { 68 | $lastBuildDate = mt_rand(0, 9999999); 69 | $channel = new Channel(); 70 | $this->assertSame($channel, $channel->lastBuildDate($lastBuildDate)); 71 | $this->assertAttributeSame($lastBuildDate, 'lastBuildDate', $channel); 72 | } 73 | 74 | public function testTtl() 75 | { 76 | $ttl = mt_rand(0, 99999999); 77 | $channel = new Channel(); 78 | $this->assertSame($channel, $channel->ttl($ttl)); 79 | $this->assertAttributeSame($ttl, 'ttl', $channel); 80 | } 81 | 82 | public function testPubsubhubbub() 83 | { 84 | $channel = new Channel(); 85 | $channel->pubsubhubbub('http://example.com/feed.xml', 'http://pubsubhubbub.appspot.com'); 86 | $xml = $channel->asXML()->asXML(); 87 | $this->assertContains('<atom:link rel="self" href="http://example.com/feed.xml" type="application/rss+xml"/>', $xml); 88 | $this->assertContains('<atom:link rel="hub" href="http://pubsubhubbub.appspot.com"/>', $xml); 89 | } 90 | 91 | public function testAddItem() 92 | { 93 | $item = $this->createMock($this->itemInterface); 94 | $channel = new Channel(); 95 | $this->assertSame($channel, $channel->addItem($item)); 96 | $this->assertAttributeSame([$item], 'items', $channel); 97 | } 98 | 99 | public function testAppendTo() 100 | { 101 | $channel = new Channel(); 102 | $feed = $this->createMock($this->feedInterface); 103 | $feed->expects($this->once())->method('addChannel')->with($channel); 104 | $this->assertSame($channel, $channel->appendTo($feed)); 105 | } 106 | 107 | /** 108 | * @param $expect 109 | * @param array $data 110 | * @dataProvider dataForAsXML 111 | */ 112 | public function testAsXML($expect, array $data) 113 | { 114 | $data = (object)$data; 115 | $channel = new Channel(); 116 | 117 | foreach ($data as $key => $value) { 118 | $this->reveal($channel)->attr($key, $value); 119 | } 120 | 121 | $this->assertXmlStringEqualsXmlString($expect, $channel->asXML()->asXML()); 122 | } 123 | 124 | public static function dataForAsXML() 125 | { 126 | $now = time(); 127 | $nowString = date(DATE_RSS, $now); 128 | 129 | return [ 130 | [ 131 | " 132 | <channel> 133 | <title>GoUpstate.com News Headlines 134 | http://www.goupstate.com/ 135 | The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site. 136 | 137 | ", 138 | [ 139 | 'title' => "GoUpstate.com News Headlines", 140 | 'url' => 'http://www.goupstate.com/', 141 | 'description' => "The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site.", 142 | ] 143 | ], 144 | [ 145 | " 146 | 147 | GoUpstate.com News Headlines 148 | http://www.goupstate.com/ 149 | The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site. 150 | en-us 151 | 152 | ", 153 | [ 154 | 'title' => "GoUpstate.com News Headlines", 155 | 'url' => 'http://www.goupstate.com/', 156 | 'description' => "The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site.", 157 | 'language' => 'en-us', 158 | ] 159 | ], 160 | [ 161 | " 162 | 163 | GoUpstate.com News Headlines 164 | http://www.goupstate.com/ 165 | The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site. 166 | {$nowString} 167 | 168 | ", 169 | [ 170 | 'title' => "GoUpstate.com News Headlines", 171 | 'url' => 'http://www.goupstate.com/', 172 | 'description' => "The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site.", 173 | 'pubDate' => $now, 174 | ] 175 | ], 176 | [ 177 | " 178 | 179 | GoUpstate.com News Headlines 180 | http://www.goupstate.com/ 181 | The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site. 182 | {$nowString} 183 | 184 | ", 185 | [ 186 | 'title' => "GoUpstate.com News Headlines", 187 | 'url' => 'http://www.goupstate.com/', 188 | 'description' => "The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site.", 189 | 'lastBuildDate' => $now, 190 | ] 191 | ], 192 | [ 193 | " 194 | 195 | GoUpstate.com News Headlines 196 | http://www.goupstate.com/ 197 | The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site. 198 | 60 199 | 200 | ", 201 | [ 202 | 'title' => "GoUpstate.com News Headlines", 203 | 'url' => 'http://www.goupstate.com/', 204 | 'description' => "The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site.", 205 | 'ttl' => 60, 206 | ] 207 | ], 208 | [ 209 | " 210 | 211 | GoUpstate.com News Headlines 212 | http://www.goupstate.com/ 213 | The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site. 214 | Copyright 2002, Spartanburg Herald-Journal 215 | 216 | ", 217 | [ 218 | 'title' => "GoUpstate.com News Headlines", 219 | 'url' => 'http://www.goupstate.com/', 220 | 'description' => "The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site.", 221 | 'copyright' => "Copyright 2002, Spartanburg Herald-Journal", 222 | ] 223 | ], 224 | ]; 225 | } 226 | 227 | public function testAppendTo_with_items() 228 | { 229 | $channel = new Channel(); 230 | 231 | $xml1 = new SimpleXMLElement('item1'); 232 | $xml2 = new SimpleXMLElement('item2'); 233 | $xml3 = new SimpleXMLElement('item3'); 234 | 235 | $item1 = $this->createMock($this->itemInterface); 236 | $item1->expects($this->once())->method('asXML')->will($this->returnValue($xml1)); 237 | $item2 = $this->createMock($this->itemInterface); 238 | $item2->expects($this->once())->method('asXML')->will($this->returnValue($xml2)); 239 | $item3 = $this->createMock($this->itemInterface); 240 | $item3->expects($this->once())->method('asXML')->will($this->returnValue($xml3)); 241 | 242 | $this->reveal($channel) 243 | ->attr('title', "GoUpstate.com News Headlines") 244 | ->attr('url', 'http://www.goupstate.com/') 245 | ->attr('description', "The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site.") 246 | ->attr('items', [$item1, $item2, $item3]); 247 | 248 | $expect = ' 249 | 250 | GoUpstate.com News Headlines 251 | http://www.goupstate.com/ 252 | The latest news from GoUpstate.com, a Spartanburg Herald-Journal Web site. 253 | 254 | item1 255 | 256 | 257 | item2 258 | 259 | 260 | item3 261 | 262 | 263 | '; 264 | 265 | $this->assertXmlStringEqualsXmlString($expect, $channel->asXML()->asXML()); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /tests/Suin/RSSWriter/ItemTest.php: -------------------------------------------------------------------------------- 1 | assertSame($item, $item->title($title)); 16 | $this->assertAttributeSame($title, 'title', $item); 17 | } 18 | 19 | public function testUrl() 20 | { 21 | $url = uniqid(); 22 | $item = new Item(); 23 | $this->assertSame($item, $item->url($url)); 24 | $this->assertAttributeSame($url, 'url', $item); 25 | } 26 | 27 | public function testDescription() 28 | { 29 | $description = uniqid(); 30 | $item = new Item(); 31 | $this->assertSame($item, $item->description($description)); 32 | $this->assertAttributeSame($description, 'description', $item); 33 | } 34 | 35 | public function testContentEncoded() 36 | { 37 | $item = new Item(); 38 | $this->assertSame($item, $item->contentEncoded('
contents
')); 39 | $this->assertAttributeSame('
contents
', 'contentEncoded', $item); 40 | 41 | $feed = new Feed(); 42 | $channel = new Channel(); 43 | $item->appendTo($channel); 44 | $channel->appendTo($feed); 45 | 46 | $expected = ' 47 | 48 | 49 | 50 | <link/> 51 | <description/> 52 | <item> 53 | <description/> 54 | <content:encoded><![CDATA[<div>contents</div>]]></content:encoded> 55 | </item> 56 | </channel> 57 | </rss>'; 58 | $this->assertXmlStringEqualsXmlString($expected, $feed->render()); 59 | } 60 | 61 | public function testCategory() 62 | { 63 | $category = uniqid(); 64 | $item = new Item(); 65 | $this->assertSame($item, $item->category($category)); 66 | $this->assertAttributeSame([ 67 | [$category, null], 68 | ], 'categories', $item); 69 | } 70 | 71 | public function testCategory_with_domain() 72 | { 73 | $category = uniqid(); 74 | $domain = uniqid(); 75 | $item = new Item(); 76 | $this->assertSame($item, $item->category($category, $domain)); 77 | $this->assertAttributeSame([ 78 | [$category, $domain], 79 | ], 'categories', $item); 80 | } 81 | 82 | public function testCategories() 83 | { 84 | $categories = ['a', 'b', ['c', 'domain'], 'd', ['e']]; 85 | $stored_categories = [ 86 | ['a', null], 87 | ['b', null], 88 | ['c', 'domain'], 89 | ['d', null], 90 | ['e', null], 91 | ]; 92 | $item = new Item(); 93 | $item->categories($categories); 94 | $this->assertAttributeSame($stored_categories, 'categories', $item); 95 | } 96 | 97 | public function testGuid() 98 | { 99 | $guid = uniqid(); 100 | $item = new Item(); 101 | $this->assertSame($item, $item->guid($guid)); 102 | $this->assertAttributeSame($guid, 'guid', $item); 103 | } 104 | 105 | public function testGuid_with_permalink() 106 | { 107 | $item = new Item(); 108 | $item->guid('guid', true); 109 | $this->assertAttributeSame(true, 'isPermalink', $item); 110 | 111 | $item->guid('guid', false); 112 | $this->assertAttributeSame(false, 'isPermalink', $item); 113 | 114 | $item->guid('guid'); // default 115 | $this->assertAttributeSame(false, 'isPermalink', $item); 116 | } 117 | 118 | public function testPubDate() 119 | { 120 | $pubDate = mt_rand(1000000, 9999999); 121 | $item = new Item(); 122 | $this->assertSame($item, $item->pubDate($pubDate)); 123 | $this->assertAttributeSame($pubDate, 'pubDate', $item); 124 | } 125 | 126 | public function testAppendTo() 127 | { 128 | $item = new Item(); 129 | $channel = $this->createMock($this->channelInterface); 130 | $channel->expects($this->once())->method('addItem')->with($item); 131 | $this->assertSame($item, $item->appendTo($channel)); 132 | } 133 | 134 | public function testEnclosure() 135 | { 136 | $url = uniqid(); 137 | $enclosure = ['url' => $url, 'length' => 0, 'type' => 'audio/mpeg']; 138 | $item = new Item(); 139 | $this->assertSame($item, $item->enclosure($url)); 140 | $this->assertAttributeSame($enclosure, 'enclosure', $item); 141 | } 142 | 143 | public function testAuthor() 144 | { 145 | $author = uniqid(); 146 | $item = new Item(); 147 | $this->assertSame($item, $item->author($author)); 148 | $this->assertAttributeSame($author, 'author', $item); 149 | } 150 | 151 | public function testCreator() 152 | { 153 | $creator = uniqid(); 154 | $item = new Item(); 155 | $this->assertSame($item, $item->creator($creator)); 156 | 157 | $creatorXml = '<dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">' . $creator . '</dc:creator>'; 158 | $this->assertContains($creatorXml, $item->asXML()->asXML()); 159 | } 160 | 161 | public function testPreferCdata() 162 | { 163 | $item = new Item(); 164 | $item->title('<h1>title</h1>'); 165 | $item->description('<p>description</p>'); 166 | 167 | // By default, prefer no CDATA on title and description 168 | $actualXml = $item->asXML()->asXML(); 169 | $this->assertContains('<title><h1>title</h1>', $actualXml); 170 | $this->assertContains('<p>description</p>', $actualXml); 171 | 172 | // Once prefer-cdata is enabled, title and description is wrapped by CDATA 173 | $item->preferCdata(true); 174 | $actualXml = $item->asXML()->asXML(); 175 | $this->assertContains('<![CDATA[<h1>title</h1>]]>', $actualXml); 176 | $this->assertContains('description

]]>
', $actualXml); 177 | 178 | // Of course, prefer-cdata can be disabled again 179 | $item->preferCdata(false); 180 | $actualXml = $item->asXML()->asXML(); 181 | $this->assertContains('<h1>title</h1>', $actualXml); 182 | $this->assertContains('<p>description</p>', $actualXml); 183 | 184 | // And like other APIs `preferCdata` is also fluent interface 185 | $obj = $item->preferCdata(true); 186 | $this->assertSame($obj, $item); 187 | } 188 | 189 | public function testAsXML() 190 | { 191 | $now = time(); 192 | $nowString = date(DATE_RSS, $now); 193 | 194 | $data = [ 195 | 'title' => "Venice Film Festival Tries to Quit Sinking", 196 | 'url' => 'http://nytimes.com/2004/12/07FEST.html', 197 | 'description' => "Some of the most heated chatter at the Venice Film Festival this week was about the way that the arrival of the stars at the Palazzo del Cinema was being staged.", 198 | 'categories' => [ 199 | ["Grateful Dead", null], 200 | ["MSFT", 'http://www.fool.com/cusips'], 201 | ], 202 | 'guid' => "http://inessential.com/2002/09/01.php#a2", 203 | 'isPermalink' => true, 204 | 'pubDate' => $now, 205 | 'enclosure' => [ 206 | 'url' => 'http://link-to-audio-file.com/test.mp3', 207 | 'length' => 4992, 208 | 'type' => 'audio/mpeg' 209 | ], 210 | 'author' => 'John Smith' 211 | ]; 212 | 213 | $item = new Item(); 214 | 215 | foreach ($data as $key => $value) { 216 | $this->reveal($item)->attr($key, $value); 217 | } 218 | 219 | $expect = " 220 | 221 | {$data['title']} 222 | {$data['url']} 223 | {$data['description']} 224 | {$data['categories'][0][0]} 225 | {$data['categories'][1][0]} 226 | {$data['guid']} 227 | {$nowString} 228 | 229 | {$data['author']} 230 | 231 | "; 232 | $this->assertXmlStringEqualsXmlString($expect, $item->asXML()->asXML()); 233 | } 234 | 235 | public function testAsXML_false_permalink() 236 | { 237 | $now = time(); 238 | $nowString = date(DATE_RSS, $now); 239 | 240 | $data = [ 241 | 'title' => "Venice Film Festival Tries to Quit Sinking", 242 | 'url' => 'http://nytimes.com/2004/12/07FEST.html', 243 | 'description' => "Some of the most heated chatter at the Venice Film Festival this week was about the way that the arrival of the stars at the Palazzo del Cinema was being staged.", 244 | 'categories' => [ 245 | ["Grateful Dead", null], 246 | ["MSFT", 'http://www.fool.com/cusips'], 247 | ], 248 | 'guid' => "http://inessential.com/2002/09/01.php#a2", 249 | 'isPermalink' => false, 250 | 'pubDate' => $now, 251 | 'enclosure' => [ 252 | 'url' => 'http://link-to-audio-file.com/test.mp3', 253 | 'length' => 4992, 254 | 'type' => 'audio/mpeg' 255 | ], 256 | 'author' => 'John Smith' 257 | ]; 258 | 259 | $item = new Item(); 260 | 261 | foreach ($data as $key => $value) { 262 | $this->reveal($item)->attr($key, $value); 263 | } 264 | 265 | $expect = " 266 | 267 | {$data['title']} 268 | {$data['url']} 269 | {$data['description']} 270 | {$data['categories'][0][0]} 271 | {$data['categories'][1][0]} 272 | {$data['guid']} 273 | {$nowString} 274 | 275 | {$data['author']} 276 | 277 | "; 278 | $this->assertXmlStringEqualsXmlString($expect, $item->asXML()->asXML()); 279 | } 280 | 281 | public function testAsXML_test_Japanese() 282 | { 283 | $data = [ 284 | 'title' => "Venice Film Festival", 285 | 'url' => 'http://nytimes.com/2004/12/07FEST.html', 286 | 'description' => "Some of the most heated chatter at the Venice Film Festival this week was about the way that the arrival of the stars at the Palazzo del Cinema was being staged.", 287 | ]; 288 | 289 | $item = new Item(); 290 | 291 | foreach ($data as $key => $value) { 292 | $this->reveal($item)->attr($key, $value); 293 | } 294 | 295 | $expect = " 296 | 297 | {$data['title']} 298 | {$data['url']} 299 | {$data['description']} 300 | 301 | "; 302 | 303 | $this->assertXmlStringEqualsXmlString($expect, $item->asXML()->asXML()); 304 | } 305 | 306 | public function test_with_amp() 307 | { 308 | $item = new Item(); 309 | $item 310 | ->title('test&test') 311 | ->url('url&url') 312 | ->description('desc&desc'); 313 | $expect = ' 314 | test&testurl&urldesc&desc 315 | '; 316 | 317 | $this->assertSame($expect, $item->asXML()->asXML()); 318 | } 319 | 320 | public function test_fail_safe_against_invalid_string() 321 | { 322 | $item = new Item(); 323 | $item 324 | ->title("test\0test") 325 | ->url("url\0test") 326 | ->description("desc\0desc"); 327 | $expect = ' 328 | testurldesc 329 | '; 330 | 331 | $this->assertSame($expect, $item->asXML()->asXML()); 332 | } 333 | } 334 | --------------------------------------------------------------------------------