├── .bootstrap.php ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── LICENSE ├── README.md ├── composer.json ├── doc ├── index.md ├── read-feed.md └── render-feed.md ├── phpunit.xml.dist ├── src ├── Attachment.php ├── Author.php ├── Exceptions │ ├── InvalidFeedException.php │ └── RuntimeException.php ├── Feed.php ├── Hub.php ├── Item.php ├── Reader │ ├── Reader.php │ ├── ReaderBuilder.php │ ├── ReaderInterface.php │ └── Version1 │ │ └── FeedReader.php ├── Versions.php └── Writer │ ├── RendererFactory.php │ ├── RendererInterface.php │ └── Version1 │ └── Renderer.php └── test ├── AttachmentTest.php ├── AuthorTest.php ├── FeedTest.php ├── Fixtures ├── authors.json ├── extension.json ├── microblog.json ├── podcast.json └── simple.json ├── HubTest.php ├── ItemTest.php ├── Reader ├── ReaderBuilderTest.php ├── ReaderTest.php └── Version1 │ └── FeedReaderTest.php └── Writer ├── RendererFactoryTest.php └── Version1 └── RendererTest.php /.bootstrap.php: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ->exclude('vendor') 6 | ; 7 | 8 | return (new PhpCsFixer\Config()) 9 | ->setRiskyAllowed(true) 10 | ->setRules([ 11 | '@PSR2' => true, 12 | 'array_syntax' => ['syntax' => 'short'], 13 | 'blank_line_after_opening_tag' => true, 14 | 'declare_strict_types' => true, 15 | 'function_declaration' => ['closure_function_spacing' => 'none'], 16 | 'single_import_per_statement' => false, 17 | 'strict_comparison' => true, 18 | 'strict_param' => true, 19 | ]) 20 | ->setFinder($finder) 21 | ; 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jérémy DECOOL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSONFeed 2 | ======== 3 | 4 | [![Build Status](https://travis-ci.org/jdecool/jsonfeed.svg?branch=master)](https://travis-ci.org/jdecool/jsonfeed?branch=master) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/jdecool/jsonfeed/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/jdecool/jsonfeed/?branch=master) 6 | [![Latest Stable Version](https://poser.pugx.org/jdecool/jsonfeed/v/stable.png)](https://packagist.org/packages/jdecool/jsonfeed) 7 | 8 | [JSONFeed](https://jsonfeed.org) is a pragmatic syndication format, like RSS and Atom, but with one big difference: 9 | it’s JSON instead of XML. 10 | 11 | This library provides functionnalits for mananaging feed through your PHP code. It provides a natural syntax for accessing 12 | elements of feed. 13 | 14 | To learn how to use the library, [read the documentation](doc/index.md) 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jdecool/jsonfeed", 3 | "description": "PHP JSONFeed library", 4 | "type": "library", 5 | "keywords": ["feed", "json", "jsonfeed"], 6 | "homepage": "http://jsonfeed.org", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Jérémy DECOOL", 11 | "email": "contact@jdecool.fr" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.3.0", 16 | "symfony/property-access": "^4.4|^5.0|^6.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "^9.5", 20 | "friendsofphp/php-cs-fixer": "^3.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "JDecool\\JsonFeed\\": "src" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "JDecool\\Test\\JsonFeed\\": "test" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | Getting started with JSONFeed 2 | ============================= 3 | 4 | ## Installation 5 | 6 | To use this library, you need to install it with [composer](https://getcomposer.org): 7 | 8 | ```bash 9 | composer require jdecool/jsonfeed 10 | ``` 11 | 12 | ## Usage 13 | 14 | * [Render a feed](render-feed.md) 15 | * [Read a feed](read-feed.md) 16 | -------------------------------------------------------------------------------- /doc/read-feed.md: -------------------------------------------------------------------------------- 1 | Read a feed 2 | =========== 3 | 4 | Read a feed is very simple. You just need to create a `Reader` which will parse 5 | the JSONFeed data and will return a `Feed` object. 6 | 7 | ```php 8 | $json = file_get_content('http://foo/bar'); 9 | 10 | $builder = JDecool\JsonFeed\Reader\ReaderBuilder(); 11 | $reader = $builder->build(); 12 | 13 | $feed = $reader->createFromJson($json); // Will be a JDecool\JsonFeed\Feed object 14 | ``` 15 | 16 | The `ReaderBuilder::build` method access a boolean as parameter to define if the 17 | parse will through an exception if an error is detected (default value is `true`). 18 | -------------------------------------------------------------------------------- /doc/render-feed.md: -------------------------------------------------------------------------------- 1 | Render a feed 2 | ============= 3 | 4 | To render JSONFeed, you need to create and fill a `Feed` object. 5 | 6 | ```php 7 | use DateTime; 8 | use DateTimeZone; 9 | use JDecool\JsonFeed\Author; 10 | use JDecool\JsonFeed\Feed; 11 | use JDecool\JsonFeed\Item; 12 | 13 | $author = new Author('Brent Simmons'); 14 | $author->setUrl('http://example.org/'); 15 | $author->setAvatar('https://example.org/avatar.png'); 16 | 17 | $item = new Item('2347259'); 18 | $item->setUrl('https://example.org/2347259'); 19 | $item->setDatePublished(new DateTime('2016-02-09 14:22:00', new DateTimeZone('+0200'))); 20 | $item->setContentText('Cats are neat. https://example.org/cats'); 21 | $item->addExtension('blue_shed', [ 22 | 'about' => 'https://blueshed-podcasts.com/json-feed-extension-docs', 23 | 'explicit': false, 24 | 'copyright' => '1948 by George Orwell', 25 | 'owner' => 'Big Brother and the Holding Company', 26 | 'subtitle' => 'All shouting, all the time. Double. Plus. Good.' 27 | ]); 28 | 29 | $feed = new Feed('Brent Simmons’s Microblog'); 30 | $feed->setUserComment('This is a microblog feed. You can add this to your feed reader using the following URL: https://example.org/feed.json'); 31 | $feed->setHomepageUrl('https://example.org/'); 32 | $feed->setFeedUrl('https://example.org/feed.json'); 33 | $feed->setAuthor($author); 34 | $feed->addItem($item); 35 | ``` 36 | 37 | After that, you have to choose which renderer will render your feed: 38 | 39 | ```php 40 | use JDecool\JsonFeed\Writer\RendererFactory; 41 | use JDecool\JsonFeed\Versions; 42 | 43 | $factory = new RendererFactory(); 44 | $renderer = $factory->createRenderer(Versions::VERSION_1); 45 | ``` 46 | 47 | Finaly render your JSONFeed data: 48 | 49 | ```php 50 | header('Content-Type: application/json; charset=UTF-8'); 51 | echo $renderer->render($feed); 52 | ``` 53 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | test 17 | 18 | 19 | 20 | 21 | 22 | ./src/ 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Attachment.php: -------------------------------------------------------------------------------- 1 | url = $url; 36 | $this->mimeType = $mimeType; 37 | } 38 | 39 | /** 40 | * Get the location of the attachment 41 | * 42 | * @return string 43 | */ 44 | public function getUrl() 45 | { 46 | return $this->url; 47 | } 48 | 49 | /** 50 | * Get the type of the attachment 51 | * 52 | * @return string 53 | */ 54 | public function getMimeType() 55 | { 56 | return $this->mimeType; 57 | } 58 | 59 | /** 60 | * Get the name for the attachment 61 | * 62 | * @return string 63 | */ 64 | public function getTitle() 65 | { 66 | return $this->title; 67 | } 68 | 69 | /** 70 | * Set the name of the attachment 71 | * 72 | * @param string $title 73 | * @return Attachment 74 | */ 75 | public function setTitle($title) 76 | { 77 | $this->title = $title; 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Get the size of content 84 | * 85 | * @return float|int 86 | */ 87 | public function getSize() 88 | { 89 | return $this->size; 90 | } 91 | 92 | /** 93 | * Set the size of content 94 | * 95 | * @param float|int $size 96 | * @return Attachment 97 | */ 98 | public function setSize($size) 99 | { 100 | $this->size = $size; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * Get the duration of the attachment 107 | * 108 | * @return float|int 109 | */ 110 | public function getDuration() 111 | { 112 | return $this->duration; 113 | } 114 | 115 | /** 116 | * Specifies how long the attachment takes to listen to or watch 117 | * 118 | * @param float|int $duration 119 | * @return Attachment 120 | */ 121 | public function setDuration($duration) 122 | { 123 | $this->duration = $duration; 124 | 125 | return $this; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Author.php: -------------------------------------------------------------------------------- 1 | name = $name; 29 | } 30 | 31 | /** 32 | * Get the author’s name 33 | * 34 | * @return string 35 | */ 36 | public function getName() 37 | { 38 | return $this->name; 39 | } 40 | 41 | /** 42 | * Get the URL of a site owned by the author 43 | * 44 | * @return string 45 | */ 46 | public function getUrl() 47 | { 48 | return $this->url; 49 | } 50 | 51 | /** 52 | * Set the URL of a site owned by the author 53 | * 54 | * @param string $url 55 | * @return Author 56 | */ 57 | public function setUrl($url) 58 | { 59 | $this->url = $url; 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * Get the URL for an image for the author 66 | * 67 | * @return string 68 | */ 69 | public function getAvatar() 70 | { 71 | return $this->avatar; 72 | } 73 | 74 | /** 75 | * Set the URL for an image for the author 76 | * 77 | * @param string $avatar 78 | * @return Author 79 | */ 80 | public function setAvatar($avatar) 81 | { 82 | $this->avatar = $avatar; 83 | 84 | return $this; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidFeedException.php: -------------------------------------------------------------------------------- 1 | title = $title; 57 | 58 | $this->hubs = []; 59 | $this->items = []; 60 | } 61 | 62 | /** 63 | * Get the name of the feed 64 | * 65 | * @return string 66 | */ 67 | public function getTitle() 68 | { 69 | return $this->title; 70 | } 71 | 72 | /** 73 | * Set the name of the feed 74 | * 75 | * @param string $title 76 | * @return Feed 77 | */ 78 | public function setTitle($title) 79 | { 80 | $this->title = $title; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Get the URL of the resource that the feed describes 87 | * 88 | * @return string 89 | */ 90 | public function getHomepageUrl() 91 | { 92 | return $this->homepageUrl; 93 | } 94 | 95 | /** 96 | * Set the URL of the resource that the feed describes 97 | * 98 | * @param string $homepageUrl 99 | * @return Feed 100 | */ 101 | public function setHomepageUrl($homepageUrl) 102 | { 103 | $this->homepageUrl = $homepageUrl; 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Get the URL of the feed, and serves as the unique identifier for the feed 110 | * 111 | * @return string 112 | */ 113 | public function getFeedUrl() 114 | { 115 | return $this->feedUrl; 116 | } 117 | 118 | /** 119 | * Set the URL of the feed, and serves as the unique identifier for the feed 120 | * 121 | * @param string $feedUrl 122 | * @return Feed 123 | */ 124 | public function setFeedUrl($feedUrl) 125 | { 126 | $this->feedUrl = $feedUrl; 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Get more detail, beyond the `title`, on what the feed is about 133 | * 134 | * @return string 135 | */ 136 | public function getDescription() 137 | { 138 | return $this->description; 139 | } 140 | 141 | /** 142 | * Set more detail, beyond the `title`, on what the feed is about 143 | * 144 | * @param string $description 145 | * @return Feed 146 | */ 147 | public function setDescription($description) 148 | { 149 | $this->description = $description; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * Get a description of the purpose of the feed 156 | * 157 | * @return string 158 | */ 159 | public function getUserComment() 160 | { 161 | return $this->userComment; 162 | } 163 | 164 | /** 165 | * Set a description of the purpose of the feed 166 | * 167 | * @param string $userComment 168 | * @return Feed 169 | */ 170 | public function setUserComment($userComment) 171 | { 172 | $this->userComment = $userComment; 173 | 174 | return $this; 175 | } 176 | 177 | /** 178 | * Get the URL of a feed that provides the next n items, where n is determined by the publisher 179 | * 180 | * @return string 181 | */ 182 | public function getNextUrl() 183 | { 184 | return $this->nextUrl; 185 | } 186 | 187 | /** 188 | * Set the URL of a feed that provides the next n items, where n is determined by the publisher 189 | * 190 | * @param string $nextUrl 191 | * @return Feed 192 | */ 193 | public function setNextUrl($nextUrl) 194 | { 195 | $this->nextUrl = $nextUrl; 196 | 197 | return $this; 198 | } 199 | 200 | /** 201 | * Get the URL of an image for the feed suitable to be used in a timeline, much the way an avatar might be used 202 | * 203 | * @return string 204 | */ 205 | public function getIcon() 206 | { 207 | return $this->icon; 208 | } 209 | 210 | /** 211 | * Set the URL of an image for the feed suitable to be used in a timeline, much the way an avatar might be used 212 | * 213 | * @param string $icon 214 | * @return Feed 215 | */ 216 | public function setIcon($icon) 217 | { 218 | $this->icon = $icon; 219 | 220 | return $this; 221 | } 222 | 223 | /** 224 | * Get the URL of an image for the feed suitable to be used in a source list 225 | * 226 | * @return string 227 | */ 228 | public function getFavicon() 229 | { 230 | return $this->favicon; 231 | } 232 | 233 | /** 234 | * Set the URL of an image for the feed suitable to be used in a source list 235 | * 236 | * @param string $favicon 237 | * @return Feed 238 | */ 239 | public function setFavicon($favicon) 240 | { 241 | $this->favicon = $favicon; 242 | 243 | return $this; 244 | } 245 | 246 | /** 247 | * Get the feed author 248 | * 249 | * @return Author 250 | */ 251 | public function getAuthor() 252 | { 253 | return $this->author; 254 | } 255 | 256 | /** 257 | * Set the feed author 258 | * 259 | * @param Author $author 260 | * @return Feed 261 | */ 262 | public function setAuthor(Author $author) 263 | { 264 | $this->author = $author; 265 | 266 | return $this; 267 | } 268 | 269 | /** 270 | * Set the feed author 271 | * 272 | * @param string $name 273 | * @param string $url 274 | * @return Feed 275 | */ 276 | public function addAuthor($name, $url = null) 277 | { 278 | $author = new Author($name); 279 | $author->setUrl($url); 280 | 281 | return $this->setAuthor($author); 282 | } 283 | 284 | /** 285 | * Says whether or not the feed is finished 286 | * 287 | * @return bool 288 | */ 289 | public function isExpired() 290 | { 291 | return $this->expired; 292 | } 293 | 294 | /** 295 | * Set the feed has expired 296 | * 297 | * @param bool $expired 298 | * @return Feed 299 | */ 300 | public function setExpired($expired) 301 | { 302 | $this->expired = $expired; 303 | 304 | return $this; 305 | } 306 | 307 | /** 308 | * Get endpoints that can be used to subscribe to real-time notifications from the publisher of this feed 309 | * 310 | * @return Hub[] 311 | */ 312 | public function getHubs() 313 | { 314 | return $this->hubs; 315 | } 316 | 317 | /** 318 | * Add endpoint that can be used to subscribe to real-time notifications from the publisher of this feed 319 | * 320 | * @param Hub $hub 321 | * @return Feed 322 | */ 323 | public function addHub(Hub $hub) 324 | { 325 | if (!in_array($hub, $this->hubs, true)) { 326 | $this->hubs[] = $hub; 327 | } 328 | 329 | return $this; 330 | } 331 | 332 | /** 333 | * Set endpoints that can be used to subscribe to real-time notifications from the publisher of this feed 334 | * 335 | * @param Hub[] $hubs 336 | * @return Feed 337 | */ 338 | public function setHubs(array $hubs) 339 | { 340 | $this->hubs = $hubs; 341 | 342 | return $this; 343 | } 344 | 345 | /** 346 | * Get feed items 347 | * 348 | * @return Item[] 349 | */ 350 | public function getItems() 351 | { 352 | return $this->items; 353 | } 354 | 355 | /** 356 | * Add feed item 357 | * 358 | * @param Item $item 359 | * @return Feed 360 | */ 361 | public function addItem(Item $item) 362 | { 363 | if (!in_array($item, $this->items, true)) { 364 | $this->items[] = $item; 365 | } 366 | 367 | return $this; 368 | } 369 | 370 | /** 371 | * Set feed items 372 | * 373 | * @param Item[] $items 374 | * @return Feed 375 | */ 376 | public function setItems(array $items) 377 | { 378 | $this->items = $items; 379 | 380 | return $this; 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/Hub.php: -------------------------------------------------------------------------------- 1 | type = $type; 28 | $this->url = $url; 29 | } 30 | 31 | /** 32 | * Get hub type 33 | * 34 | * @return string 35 | */ 36 | public function getType() 37 | { 38 | return $this->type; 39 | } 40 | 41 | /** 42 | * Get hub URL 43 | * 44 | * @return string 45 | */ 46 | public function getUrl() 47 | { 48 | return $this->url; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Item.php: -------------------------------------------------------------------------------- 1 | id = $id; 65 | 66 | $this->tags = []; 67 | $this->attachments = []; 68 | $this->extensions = []; 69 | } 70 | 71 | /** 72 | * Get unique identifer for that item for that feed over time 73 | * 74 | * @return string 75 | */ 76 | public function getId() 77 | { 78 | return $this->id; 79 | } 80 | 81 | /** 82 | * Get the URL of the resource described by the item 83 | * 84 | * @return string 85 | */ 86 | public function getUrl() 87 | { 88 | return $this->url; 89 | } 90 | 91 | /** 92 | * Set the URL of the resource described by the item 93 | * 94 | * @param string $url 95 | * @return Item 96 | */ 97 | public function setUrl($url) 98 | { 99 | $this->url = $url; 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * Get the URL of a page elsewhere 106 | * 107 | * @return string 108 | */ 109 | public function getExternalUrl() 110 | { 111 | return $this->externalUrl; 112 | } 113 | 114 | /** 115 | * Set the URL of a page elsewhere 116 | * 117 | * @param string $externalUrl 118 | * @return Item 119 | */ 120 | public function setExternalUrl($externalUrl) 121 | { 122 | $this->externalUrl = $externalUrl; 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * Get plaintext title 129 | * 130 | * @return string 131 | */ 132 | public function getTitle() 133 | { 134 | return $this->title; 135 | } 136 | 137 | /** 138 | * Set plaintext title 139 | * 140 | * @param string $title 141 | * @return Item 142 | */ 143 | public function setTitle($title) 144 | { 145 | $this->title = $title; 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * Get the HTML content for the item 152 | * 153 | * @return string 154 | */ 155 | public function getContentHtml() 156 | { 157 | return $this->contentHtml; 158 | } 159 | 160 | /** 161 | * Set the HTML content for the item 162 | * 163 | * @param string $contentHtml 164 | * @return Item 165 | */ 166 | public function setContentHtml($contentHtml) 167 | { 168 | $this->contentHtml = $contentHtml; 169 | 170 | return $this; 171 | } 172 | 173 | /** 174 | * Get plain text content for the item 175 | * 176 | * @return string 177 | */ 178 | public function getContentText() 179 | { 180 | return $this->contentText; 181 | } 182 | 183 | /** 184 | * Set plain text content for the item 185 | * 186 | * @param string $contentText 187 | * @return Item 188 | */ 189 | public function setContentText($contentText) 190 | { 191 | $this->contentText = $contentText; 192 | 193 | return $this; 194 | } 195 | 196 | /** 197 | * Get a plain text sentence or two describing the item 198 | * 199 | * @return string 200 | */ 201 | public function getSummary() 202 | { 203 | return $this->summary; 204 | } 205 | 206 | /** 207 | * Set a plain text sentence or two describing the item 208 | * 209 | * @param string $summary 210 | * @return Item 211 | */ 212 | public function setSummary($summary) 213 | { 214 | $this->summary = $summary; 215 | 216 | return $this; 217 | } 218 | 219 | /** 220 | * Get the URL of the main image for the item 221 | * 222 | * @return string 223 | */ 224 | public function getImage() 225 | { 226 | return $this->image; 227 | } 228 | 229 | /** 230 | * Set the URL of the main image for the item 231 | * 232 | * @param string $image 233 | * @return Item 234 | */ 235 | public function setImage($image) 236 | { 237 | $this->image = $image; 238 | 239 | return $this; 240 | } 241 | 242 | /** 243 | * Get the URL of an image to use as a banner 244 | * 245 | * @return string 246 | */ 247 | public function getBannerImage() 248 | { 249 | return $this->bannerImage; 250 | } 251 | 252 | /** 253 | * Set the URL of an image to use as a banner 254 | * 255 | * @param string $bannerImage 256 | * @return Item 257 | */ 258 | public function setBannerImage($bannerImage) 259 | { 260 | $this->bannerImage = $bannerImage; 261 | 262 | return $this; 263 | } 264 | 265 | /** 266 | * Get the item published date 267 | * 268 | * @return DateTime 269 | */ 270 | public function getDatePublished() 271 | { 272 | return $this->datePublished; 273 | } 274 | 275 | /** 276 | * Set the item published date 277 | * 278 | * @param DateTime $datePublished 279 | * @return Item 280 | */ 281 | public function setDatePublished(DateTime $datePublished) 282 | { 283 | $this->datePublished = $datePublished; 284 | 285 | return $this; 286 | } 287 | 288 | /** 289 | * Get the item modified date 290 | * 291 | * @return DateTime 292 | */ 293 | public function getDateModified() 294 | { 295 | return $this->dateModified; 296 | } 297 | 298 | /** 299 | * Set the item modified date 300 | * 301 | * @param DateTime $dateModified 302 | * @return Item 303 | */ 304 | public function setDateModified(DateTime $dateModified) 305 | { 306 | $this->dateModified = $dateModified; 307 | 308 | return $this; 309 | } 310 | 311 | /** 312 | * Get item author 313 | * 314 | * @return Author 315 | */ 316 | public function getAuthor() 317 | { 318 | return $this->author; 319 | } 320 | 321 | /** 322 | * Set item author 323 | * 324 | * @param Author $author 325 | * @return Item 326 | */ 327 | public function setAuthor(Author $author) 328 | { 329 | $this->author = $author; 330 | 331 | return $this; 332 | } 333 | 334 | /** 335 | * Get item tags 336 | * 337 | * @return string[] 338 | */ 339 | public function getTags() 340 | { 341 | return $this->tags; 342 | } 343 | 344 | /** 345 | * Add item tag 346 | * @param string $tag 347 | * @return Item 348 | */ 349 | public function addTag($tag) 350 | { 351 | if (!in_array($tag, $this->tags, true)) { 352 | $this->tags[] = $tag; 353 | } 354 | 355 | return $this; 356 | } 357 | 358 | /** 359 | * Set item tags 360 | * 361 | * @param string[] $tags 362 | * @return Item 363 | */ 364 | public function setTags(array $tags) 365 | { 366 | $this->tags = $tags; 367 | 368 | return $this; 369 | } 370 | 371 | /** 372 | * Get item attachments 373 | * 374 | * @return Attachment[] 375 | */ 376 | public function getAttachments() 377 | { 378 | return $this->attachments; 379 | } 380 | 381 | /** 382 | * Add item attachment 383 | * 384 | * @param Attachment $attachment 385 | * @return Item 386 | */ 387 | public function addAttachment(Attachment $attachment) 388 | { 389 | if (!in_array($attachment, $this->attachments, true)) { 390 | $this->attachments[] = $attachment; 391 | } 392 | 393 | return $this; 394 | } 395 | 396 | /** 397 | * Set item attachments 398 | * 399 | * @param Attachment[] $attachments 400 | * @return Item 401 | */ 402 | public function setAttachments(array $attachments) 403 | { 404 | $this->attachments = $attachments; 405 | 406 | return $this; 407 | } 408 | 409 | /** 410 | * Add an extension to the item 411 | * 412 | * @param string $key 413 | * @param array $value 414 | * @return Item 415 | */ 416 | public function addExtension($key, array $value) 417 | { 418 | if (!is_string($key)) { 419 | throw new InvalidArgumentException('Extension key must be a string'); 420 | } 421 | 422 | $this->extensions[$key] = $value; 423 | 424 | return $this; 425 | } 426 | 427 | /** 428 | * Get all extensions 429 | * 430 | * @return array 431 | */ 432 | public function getExtensions() 433 | { 434 | return $this->extensions; 435 | } 436 | 437 | /** 438 | * Get an extension 439 | * 440 | * @param string $key 441 | * @return array|null 442 | */ 443 | public function getExtension($key) 444 | { 445 | if (!isset($this->extensions[$key])) { 446 | return null; 447 | } 448 | 449 | return $this->extensions[$key]; 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /src/Reader/Reader.php: -------------------------------------------------------------------------------- 1 | readers = $readers; 23 | } 24 | 25 | /** 26 | * Read feed from JSON 27 | * 28 | * @param string $json 29 | * @return \JDecool\JsonFeed\Feed 30 | * 31 | * @throws InvalidFeedException 32 | * @throws RuntimeException 33 | */ 34 | public function createFromJson($json) 35 | { 36 | $content = json_decode($json, true); 37 | if (!is_array($content)) { 38 | throw InvalidFeedException::invalidJsonException(); 39 | } 40 | 41 | if (!isset($content['version'])) { 42 | throw InvalidFeedException::undefinedVersionException(); 43 | } 44 | 45 | if (!isset($this->readers[$content['version']])) { 46 | throw RuntimeException::noReaderRegisteredException($content['version']); 47 | } 48 | 49 | return $this->readers[$content['version']]->readFromJson($json); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Reader/ReaderBuilder.php: -------------------------------------------------------------------------------- 1 | Version1\FeedReader::create($isErrorEnabled), 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Reader/ReaderInterface.php: -------------------------------------------------------------------------------- 1 | accessor = PropertyAccess::createPropertyAccessor(); 43 | $this->isErrorEnabled = $isErrorEnabled; 44 | } 45 | 46 | /** 47 | * Define if errors are enable on parsing feed data 48 | * 49 | * @param bool $enable 50 | * @return FeedReader 51 | */ 52 | public function enableErrorOnParsing($enable) 53 | { 54 | $this->isErrorEnabled = $enable; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function readFromJson($json) 63 | { 64 | $content = json_decode($json, true); 65 | if (!is_array($content)) { 66 | throw InvalidFeedException::invalidJsonException(); 67 | } 68 | 69 | return $this->readFeedNode($content); 70 | } 71 | 72 | /** 73 | * Browse feed node 74 | * 75 | * @param array $content 76 | * @return Feed 77 | */ 78 | private function readFeedNode(array $content) 79 | { 80 | $feed = new Feed(''); 81 | 82 | foreach ($content as $key => $value) { 83 | if ('version' === $key) { 84 | continue; 85 | } 86 | 87 | switch ($key) { 88 | case 'author': 89 | $feed->setAuthor($this->readAuthorNode($value)); 90 | break; 91 | 92 | case 'hubs': 93 | $feed->setHubs(array_map([$this, 'readHubNode'], $value)); 94 | break; 95 | 96 | case 'items': 97 | $feed->setItems(array_map([$this, 'readItemNode'], $value)); 98 | break; 99 | 100 | default: 101 | try { 102 | $this->accessor->setValue($feed, $key, $value); 103 | } catch (NoSuchPropertyException $e) { 104 | if ($this->isErrorEnabled) { 105 | throw InvalidFeedException::invalidFeedProperty($key); 106 | } 107 | } 108 | } 109 | } 110 | 111 | return $feed; 112 | } 113 | 114 | /** 115 | * Browse item node 116 | * 117 | * @param array $content 118 | * @return Item 119 | */ 120 | private function readItemNode(array $content) 121 | { 122 | $id = isset($content['id']) ? $content['id'] : ''; 123 | 124 | $item = new Item($id); 125 | foreach ($content as $key => $value) { 126 | if ('id' === $key) { 127 | continue; 128 | } 129 | 130 | switch ($key) { 131 | case 'attachments': 132 | $item->setAttachments(array_map([$this, 'readAttachmentNode'], $value)); 133 | break; 134 | 135 | case 'author': 136 | $item->setAuthor($this->readAuthorNode($value)); 137 | break; 138 | 139 | case 'date_published': 140 | case 'date_modified': 141 | $this->accessor->setValue($item, $key, new DateTime($value)); 142 | break; 143 | 144 | default: 145 | try { 146 | if ('_' === $key[0]) { 147 | $item->addExtension(substr($key, 1), $value); 148 | } else { 149 | $this->accessor->setValue($item, $key, $value); 150 | } 151 | } catch (NoSuchPropertyException $e) { 152 | if ($this->isErrorEnabled) { 153 | throw InvalidFeedException::invalidItemProperty($key); 154 | } 155 | } 156 | break; 157 | } 158 | } 159 | 160 | return $item; 161 | } 162 | 163 | /** 164 | * Browse author node 165 | * 166 | * @param array $content 167 | * @return Author 168 | */ 169 | private function readAuthorNode(array $content) 170 | { 171 | $name = (isset($content['name'])) ? $content['name'] : ''; 172 | 173 | $author = new Author($name); 174 | foreach ($content as $key => $value) { 175 | if ('name' === $key) { 176 | continue; 177 | } 178 | 179 | try { 180 | $this->accessor->setValue($author, $key, $value); 181 | } catch (NoSuchPropertyException $e) { 182 | if ($this->isErrorEnabled) { 183 | throw InvalidFeedException::invalidAuthorProperty($key); 184 | } 185 | } 186 | } 187 | 188 | return $author; 189 | } 190 | 191 | /** 192 | * Browse hub node 193 | * 194 | * @param array $content 195 | * @return Hub 196 | */ 197 | private function readHubNode(array $content) 198 | { 199 | $type = isset($content['type']) ? $content['type'] : ''; 200 | $url = isset($content['url']) ? $content['url'] : ''; 201 | 202 | return new Hub($type, $url); 203 | } 204 | 205 | /** 206 | * Browse attachment node 207 | * 208 | * @param array $content 209 | * @return Attachment 210 | */ 211 | private function readAttachmentNode(array $content) 212 | { 213 | $url = isset($content['url']) ? $content['url'] : ''; 214 | $mimeType = isset($content['mime_type']) ? $content['mime_type'] : ''; 215 | 216 | $attachment = new Attachment($url, $mimeType); 217 | foreach ($content as $key => $value) { 218 | switch ($key) { 219 | case 'size_in_bytes': 220 | $attachment->setSize($value); 221 | break; 222 | 223 | case 'duration_in_seconds': 224 | $attachment->setDuration($value); 225 | break; 226 | } 227 | } 228 | 229 | return $attachment; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/Versions.php: -------------------------------------------------------------------------------- 1 | renderers = []; 21 | } 22 | 23 | /** 24 | * Create specific renderer 25 | * 26 | * @param string $version 27 | * @return Version1\Renderer 28 | * 29 | * @throws RuntimeException 30 | */ 31 | public function createRenderer($version = Versions::VERSION_1) 32 | { 33 | if (isset($this->renderers[$version])) { 34 | return $this->renderers[$version]; 35 | } 36 | 37 | switch ($version) { 38 | case Versions::VERSION_1: 39 | return new Version1\Renderer(); 40 | } 41 | 42 | throw RuntimeException::noRendererRegisteredException($version); 43 | } 44 | 45 | /** 46 | * Register a custom renderer 47 | * 48 | * @param string $key 49 | * @param RendererInterface $renderer 50 | * @return RendererFactory 51 | */ 52 | public function registerRenderer($key, RendererInterface $renderer) 53 | { 54 | $this->renderers[$key] = $renderer; 55 | 56 | return $this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Writer/RendererInterface.php: -------------------------------------------------------------------------------- 1 | flags = $flags; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function render(Feed $feed) 39 | { 40 | $result = [ 41 | 'version' => Versions::VERSION_1, 42 | 'title' => $feed->getTitle(), 43 | ]; 44 | 45 | if ($homepageUrl = $feed->getHomepageUrl()) { 46 | $result['home_page_url'] = $homepageUrl; 47 | } 48 | 49 | if ($feedUrl = $feed->getFeedUrl()) { 50 | $result['feed_url'] = $feedUrl; 51 | } 52 | 53 | if ($description = $feed->getDescription()) { 54 | $result['description'] = $description; 55 | } 56 | 57 | if ($userComment = $feed->getUserComment()) { 58 | $result['user_comment'] = $userComment; 59 | } 60 | 61 | if ($nextUrl = $feed->getNextUrl()) { 62 | $result['next_url'] = $nextUrl; 63 | } 64 | 65 | if ($icon = $feed->getIcon()) { 66 | $result['icon'] = $icon; 67 | } 68 | 69 | if ($favicon = $feed->getFavicon()) { 70 | $result['favicon'] = $favicon; 71 | } 72 | 73 | if ($author = $feed->getAuthor()) { 74 | $result['author'] = $this->renderAuthor($author); 75 | } 76 | 77 | if (null !== $expired = $feed->isExpired()) { 78 | $result['expired'] = (bool) $expired; 79 | } 80 | 81 | if ($items = $feed->getItems()) { 82 | $result['items'] = array_map(function(Item $item) { 83 | return $this->renderItem($item); 84 | }, $items); 85 | } 86 | 87 | if ($hubs = $feed->getHubs()) { 88 | $result['hubs'] = array_map(function(Hub $hub) { 89 | return $this->renderHub($hub); 90 | }, $hubs); 91 | } 92 | 93 | return json_encode($result, $this->flags); 94 | } 95 | 96 | /** 97 | * Render item 98 | * 99 | * @param Item $item 100 | * @return array 101 | */ 102 | private function renderItem(Item $item) 103 | { 104 | $result = [ 105 | 'id' => $item->getId(), 106 | ]; 107 | 108 | if ($url = $item->getUrl()) { 109 | $result['url'] = $url; 110 | } 111 | 112 | if ($externalUrl = $item->getExternalUrl()) { 113 | $result['external_url'] = $externalUrl; 114 | } 115 | 116 | if ($title = $item->getTitle()) { 117 | $result['title'] = $title; 118 | } 119 | 120 | if ($contentHtml = $item->getContentHtml()) { 121 | $result['content_html'] = $contentHtml; 122 | } 123 | 124 | if ($contentText = $item->getContentText()) { 125 | $result['content_text'] = $contentText; 126 | } 127 | 128 | if ($summary = $item->getSummary()) { 129 | $result['summary'] = $summary; 130 | } 131 | 132 | if ($image = $item->getImage()) { 133 | $result['image'] = $image; 134 | } 135 | 136 | if ($bannerImage = $item->getBannerImage()) { 137 | $result['banner_image'] = $bannerImage; 138 | } 139 | 140 | if ($datePublished = $item->getDatePublished()) { 141 | $result['date_published'] = $datePublished->format(DateTime::ATOM); 142 | } 143 | 144 | if ($dateModified = $item->getDateModified()) { 145 | $result['date_modified'] = $dateModified->format(DateTime::ATOM); 146 | } 147 | 148 | if ($tags = $item->getTags()) { 149 | $result['tags'] = $tags; 150 | } 151 | 152 | if ($attachments = $item->getAttachments()) { 153 | $result['attachments'] = array_map(function(Attachment $attachment) { 154 | return $this->renderAttachment($attachment); 155 | }, $attachments); 156 | } 157 | 158 | if ($author = $item->getAuthor()) { 159 | $result['author'] = $this->renderAuthor($author); 160 | } 161 | 162 | if ($extensions = $item->getExtensions()) { 163 | foreach ($extensions as $key => $extension) { 164 | $result['_'.$key] = $extension; 165 | } 166 | } 167 | 168 | return $result; 169 | } 170 | 171 | /** 172 | * Render attachment 173 | * 174 | * @param Attachment $attachment 175 | * @return array 176 | */ 177 | private function renderAttachment(Attachment $attachment) 178 | { 179 | $result = [ 180 | 'url' => $attachment->getUrl(), 181 | 'mime_type' => $attachment->getMimeType(), 182 | ]; 183 | 184 | if ($title = $attachment->getTitle()) { 185 | $result['title'] = $title; 186 | } 187 | 188 | if ($size = $attachment->getSize()) { 189 | $result['size_in_bytes'] = $size; 190 | } 191 | 192 | if ($duration = $attachment->getDuration()) { 193 | $result['duration_in_seconds'] = $duration; 194 | } 195 | 196 | return $result; 197 | } 198 | 199 | /** 200 | * Render author 201 | * 202 | * @param Author $author 203 | * @return array 204 | */ 205 | private function renderAuthor(Author $author) 206 | { 207 | $result = []; 208 | 209 | if ($name = $author->getName()) { 210 | $result['name'] = $name; 211 | } 212 | 213 | if ($url = $author->getUrl()) { 214 | $result['url'] = $url; 215 | } 216 | 217 | if ($avatar = $author->getAvatar()) { 218 | $result['avatar'] = $avatar; 219 | } 220 | 221 | return $result; 222 | } 223 | 224 | /** 225 | * Render hub 226 | * 227 | * @param Hub $hub 228 | * @return array 229 | */ 230 | private function renderHub(Hub $hub) 231 | { 232 | return [ 233 | 'type' => $hub->getType(), 234 | 'url' => $hub->getUrl(), 235 | ]; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /test/AttachmentTest.php: -------------------------------------------------------------------------------- 1 | getUrl()); 17 | static::assertEquals('application/bar', $attachment->getMimeType()); 18 | static::assertNull($attachment->getTitle()); 19 | static::assertNull($attachment->getSize()); 20 | static::assertNull($attachment->getDuration()); 21 | } 22 | 23 | public function testFullObject(): void 24 | { 25 | $attachment = new Attachment('file://foo', 'application/bar'); 26 | $attachment 27 | ->setTitle('My title') 28 | ->setSize(500) 29 | ->setDuration(25) 30 | ; 31 | 32 | static::assertEquals('file://foo', $attachment->getUrl()); 33 | static::assertEquals('application/bar', $attachment->getMimeType()); 34 | static::assertEquals('My title', $attachment->getTitle()); 35 | static::assertEquals(500, $attachment->getSize()); 36 | static::assertEquals(25, $attachment->getDuration()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/AuthorTest.php: -------------------------------------------------------------------------------- 1 | getName()); 17 | static::assertNull($author->getUrl()); 18 | static::assertNull($author->getAvatar()); 19 | } 20 | 21 | public function testFullObject(): void 22 | { 23 | $author = new Author('foo'); 24 | $author 25 | ->setUrl('file://bar') 26 | ->setAvatar('file://image') 27 | ; 28 | 29 | static::assertEquals('foo', $author->getName()); 30 | static::assertEquals('file://bar', $author->getUrl()); 31 | static::assertEquals('file://image', $author->getAvatar()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/FeedTest.php: -------------------------------------------------------------------------------- 1 | getTitle()); 18 | static::assertNull($feed->getHomepageUrl()); 19 | static::assertNull($feed->getFeedUrl()); 20 | static::assertNull($feed->getDescription()); 21 | static::assertNull($feed->getUserComment()); 22 | static::assertNull($feed->getNextUrl()); 23 | static::assertNull($feed->getIcon()); 24 | static::assertNull($feed->getFavicon()); 25 | static::assertNull($feed->getAuthor()); 26 | static::assertNull($feed->isExpired()); 27 | static::assertEmpty($feed->getHubs()); 28 | static::assertEmpty($feed->getItems()); 29 | } 30 | 31 | public function testFeedWithOneItem(): void 32 | { 33 | $item = new Item('itemId'); 34 | $feed = new Feed('My feed'); 35 | $feed->addItem($item); 36 | 37 | $feedItems = $feed->getItems(); 38 | static::assertEquals(1, count($feedItems)); 39 | static::assertEquals($item, $feedItems[0]); 40 | } 41 | 42 | public function testFeedWithTwoItems(): void 43 | { 44 | $item1 = new Item('itemId1'); 45 | $item2 = new Item('itemId2'); 46 | 47 | $feed = new Feed('My feed'); 48 | $feed->addItem($item1); 49 | $feed->addItem($item2); 50 | 51 | $feedItems = $feed->getItems(); 52 | static::assertEquals(2, count($feedItems)); 53 | static::assertEquals($item1, $feedItems[0]); 54 | static::assertEquals($item2, $feedItems[1]); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/Fixtures/authors.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "https://jsonfeed.org/version/1", 3 | "title": "My Example Feed", 4 | "feed_url": "https://example.org/feed.json", 5 | "author": { 6 | "name": "Global Author" 7 | }, 8 | "items": [ 9 | { 10 | "id": "2", 11 | "content_text": "This is a second item.", 12 | "url": "https://example.org/2", 13 | "author": { 14 | "name": "Author 2" 15 | } 16 | }, 17 | { 18 | "id": "1", 19 | "content_html": "

This is the first item.

", 20 | "url": "https://example.org/1", 21 | "author": { 22 | "name": "Author 1" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /test/Fixtures/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "https://jsonfeed.org/version/1", 3 | "title": "My Example Feed", 4 | "items": [ 5 | { 6 | "id": "2", 7 | "content_text": "This is a second item.", 8 | "url": "https://example.org/second-item", 9 | "_extItem2": { 10 | "foo": "value", 11 | "bar": "value" 12 | }, 13 | "_extAuthor": { 14 | "john": "doe", 15 | "jane": "doe" 16 | } 17 | }, 18 | { 19 | "id": "1", 20 | "content_html": "

Hello, world!

", 21 | "url": "https://example.org/initial-post", 22 | "_extAuthor": { 23 | "john": "doe", 24 | "jane": "doe" 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test/Fixtures/microblog.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "https://jsonfeed.org/version/1", 3 | "user_comment": "This is a microblog feed. You can add this to your feed reader using the following URL: https://example.org/feed.json", 4 | "title": "Brent Simmons’s Microblog", 5 | "home_page_url": "https://example.org/", 6 | "feed_url": "https://example.org/feed.json", 7 | "author": { 8 | "name": "Brent Simmons", 9 | "url": "http://example.org/", 10 | "avatar": "https://example.org/avatar.png" 11 | }, 12 | "items": [ 13 | { 14 | "id": "2347259", 15 | "url": "https://example.org/2347259", 16 | "content_text": "Cats are neat. https://example.org/cats", 17 | "date_published": "2016-02-09T14:22:00+02:00" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/Fixtures/podcast.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "https://jsonfeed.org/version/1", 3 | "user_comment": "This is a podcast feed. You can add this feed to your podcast client using the following URL: http://therecord.co/feed.json", 4 | "title": "The Record", 5 | "home_page_url": "http://therecord.co/", 6 | "feed_url": "http://therecord.co/feed.json", 7 | "items": [ 8 | { 9 | "id": "http://therecord.co/chris-parrish", 10 | "title": "Special #1 - Chris Parrish", 11 | "url": "http://therecord.co/chris-parrish", 12 | "content_text": "Chris has worked at Adobe and as a founder of Rogue Sheep, which won an Apple Design Award for Postage. Chris’s new company is Aged & Distilled with Guy English — which shipped Napkin, a Mac app for visual collaboration. Chris is also the co-host of The Record. He lives on Bainbridge Island, a quick ferry ride from Seattle.", 13 | "content_html": "Chris has worked at Adobe and as a founder of Rogue Sheep, which won an Apple Design Award for Postage. Chris’s new company is Aged & Distilled with Guy English — which shipped Napkin, a Mac app for visual collaboration. Chris is also the co-host of The Record. He lives on Bainbridge Island, a quick ferry ride from Seattle.", 14 | "summary": "Brent interviews Chris Parrish, co-host of The Record and one-half of Aged & Distilled.", 15 | "date_published": "2014-05-09T14:04:00-07:00", 16 | "attachments": [ 17 | { 18 | "url": "http://therecord.co/downloads/The-Record-sp1e1-ChrisParrish.m4a", 19 | "mime_type": "audio/x-m4a", 20 | "size_in_bytes": 89970236, 21 | "duration_in_seconds": 6629 22 | } 23 | ] 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /test/Fixtures/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "https://jsonfeed.org/version/1", 3 | "title": "My Example Feed", 4 | "home_page_url": "https://example.org/", 5 | "feed_url": "https://example.org/feed.json", 6 | "items": [ 7 | { 8 | "id": "2", 9 | "content_text": "This is a second item.", 10 | "url": "https://example.org/second-item" 11 | }, 12 | { 13 | "id": "1", 14 | "content_html": "

Hello, world!

", 15 | "url": "https://example.org/initial-post" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/HubTest.php: -------------------------------------------------------------------------------- 1 | getType()); 17 | static::assertEquals('file://bar', $hub->getUrl()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/ItemTest.php: -------------------------------------------------------------------------------- 1 | getId()); 19 | static::assertNull($item->getUrl()); 20 | static::assertNull($item->getExternalUrl()); 21 | static::assertNull($item->getTitle()); 22 | static::assertNull($item->getContentHtml()); 23 | static::assertNull($item->getContentText()); 24 | static::assertNull($item->getSummary()); 25 | static::assertNull($item->getImage()); 26 | static::assertNull($item->getBannerImage()); 27 | static::assertNull($item->getDatePublished()); 28 | static::assertNull($item->getDateModified()); 29 | static::assertEmpty($item->getAuthor()); 30 | static::assertEmpty($item->getTags()); 31 | static::assertEmpty($item->getAttachments()); 32 | } 33 | 34 | public function testAddAuthor(): void 35 | { 36 | $author = new Author('foo'); 37 | 38 | $item = new Item('myid'); 39 | $item->setAuthor($author); 40 | 41 | static::assertEquals($author, $item->getAuthor()); 42 | } 43 | 44 | public function testTagsEmpty(): void 45 | { 46 | $item = new Item('myid'); 47 | static::assertEmpty($item->getTags()); 48 | } 49 | 50 | public function testAddTagsOneElement(): void 51 | { 52 | $item = new Item('myid'); 53 | $item->addTag('tag1'); 54 | 55 | static::assertEquals(1, count($item->getTags())); 56 | static::assertEquals(['tag1'], $item->getTags()); 57 | } 58 | 59 | public function testAddTagsTwoElements(): void 60 | { 61 | $item = new Item('myid'); 62 | $item->addTag('tag1'); 63 | $item->addTag('tag2'); 64 | 65 | static::assertEquals(2, count($item->getTags())); 66 | static::assertEquals(['tag1', 'tag2'], $item->getTags()); 67 | } 68 | 69 | public function testSetTags(): void 70 | { 71 | $tags = ['tag1', 'tag2']; 72 | 73 | $item = new Item('myid'); 74 | $item->setTags($tags); 75 | 76 | static::assertEquals($tags, $item->getTags()); 77 | } 78 | 79 | public function testAttachmentEmpty(): void 80 | { 81 | $item = new Item('myid'); 82 | static::assertEmpty($item->getAttachments()); 83 | } 84 | 85 | public function testAddAttachmentOneElement(): void 86 | { 87 | $attachment = new Attachment('foo1', 'bar1'); 88 | 89 | $item = new Item('myid'); 90 | $item->addAttachment($attachment); 91 | 92 | static::assertEquals(1, count($item->getAttachments())); 93 | static::assertEquals([$attachment], $item->getAttachments()); 94 | } 95 | 96 | public function testAddAttachmentTwoElements(): void 97 | { 98 | $attachment1 = new Attachment('foo1', 'bar1'); 99 | $attachment2 = new Attachment('foo2', 'bar2'); 100 | 101 | $item = new Item('myid'); 102 | $item->addAttachment($attachment1); 103 | $item->addAttachment($attachment2); 104 | 105 | static::assertEquals(2, count($item->getAttachments())); 106 | static::assertEquals([$attachment1, $attachment2], $item->getAttachments()); 107 | } 108 | 109 | public function testSetAttachments(): void 110 | { 111 | $attachments = [ 112 | new Attachment('foo1', 'bar1'), 113 | new Attachment('foo2', 'bar2'), 114 | ]; 115 | 116 | $item = new Item('myid'); 117 | $item->setAttachments($attachments); 118 | 119 | static::assertEquals(2, count($item->getAttachments())); 120 | static::assertEquals($attachments, $item->getAttachments()); 121 | } 122 | 123 | public function testAddExtension(): void 124 | { 125 | $extension1 = [ 126 | 'about' => 'https://blueshed-podcasts.com/json-feed-extension-docs', 127 | 'explicit' => false, 128 | 'copyright' => '1948 by George Orwell', 129 | 'owner' => 'Big Brother and the Holding Company', 130 | 'subtitle' => 'All shouting, all the time. Double. Plus. Good.' 131 | ]; 132 | 133 | $item = new Item('myid'); 134 | $item->addExtension('blue_shed', $extension1); 135 | 136 | static::assertEquals(1, count($item->getExtensions())); 137 | static::assertEquals($extension1, $item->getExtension('blue_shed')); 138 | 139 | $extension2 = [ 140 | 'foo1' => 'bar1', 141 | 'foo2' => 'bar2', 142 | ]; 143 | $item->addExtension('blue_shed2', $extension2); 144 | 145 | static::assertEquals(2, count($item->getExtensions())); 146 | static::assertEquals($extension1, $item->getExtension('blue_shed')); 147 | static::assertEquals($extension2, $item->getExtension('blue_shed2')); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /test/Reader/ReaderBuilderTest.php: -------------------------------------------------------------------------------- 1 | build()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/Reader/ReaderTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('JDecool\JsonFeed\Reader\ReaderInterface')->getMock(); 24 | $feedReader->expects($this->once()) 25 | ->method('readFromJson'); 26 | 27 | $reader = new Reader([ 28 | 'foo' => $feedReader, 29 | ]); 30 | 31 | $reader->createFromJson($json); 32 | } 33 | 34 | public function testCreateFromJsonWithInvalidReader(): void 35 | { 36 | $json = <<expectException(RuntimeException::class); 46 | $this->expectExceptionMessage('No reader registered for version "foo"'); 47 | 48 | $reader->createFromJson($json); 49 | } 50 | 51 | public function testCreateFromJsonWithInvalidString(): void 52 | { 53 | $json = <<expectException(InvalidFeedException::class); 63 | $this->expectExceptionMessage('Invalid JSONFeed string'); 64 | 65 | $reader->createFromJson($json); 66 | } 67 | 68 | public function testCreateFromJsonWithoutVersion(): void 69 | { 70 | $json = <<expectException(InvalidFeedException::class); 79 | $this->expectExceptionMessage('Undefined JSONFeed version'); 80 | 81 | $reader->createFromJson($json); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/Reader/Version1/FeedReaderTest.php: -------------------------------------------------------------------------------- 1 | getFixtures('simple'); 27 | $reader = FeedReader::create(); 28 | 29 | $feed = $reader->readFromJson($input); 30 | static::assertInstanceOf('JDecool\JsonFeed\Feed', $feed); 31 | static::assertEquals('My Example Feed', $feed->getTitle()); 32 | static::assertEquals('https://example.org/', $feed->getHomepageUrl()); 33 | static::assertEquals('https://example.org/feed.json', $feed->getFeedUrl()); 34 | 35 | $items = $feed->getItems(); 36 | static::assertCount(2, $items); 37 | 38 | $item2 = new Item('2'); 39 | $item2->setContentText('This is a second item.'); 40 | $item2->setUrl('https://example.org/second-item'); 41 | static::assertEquals($item2, $items[0]); 42 | 43 | $item1 = new Item('1'); 44 | $item1->setContentHtml('

Hello, world!

'); 45 | $item1->setUrl('https://example.org/initial-post'); 46 | static::assertEquals($item1, $items[1]); 47 | } 48 | 49 | public function testPodcastFeed(): void 50 | { 51 | $input = $this->getFixtures('podcast'); 52 | $reader = FeedReader::create(); 53 | 54 | $feed = $reader->readFromJson($input); 55 | static::assertInstanceOf('JDecool\JsonFeed\Feed', $feed); 56 | static::assertEquals('The Record', $feed->getTitle()); 57 | static::assertEquals('http://therecord.co/', $feed->getHomepageUrl()); 58 | static::assertEquals('http://therecord.co/feed.json', $feed->getFeedUrl()); 59 | static::assertEquals('This is a podcast feed. You can add this feed to your podcast client using the following URL: http://therecord.co/feed.json', $feed->getUserComment()); 60 | 61 | $items = $feed->getItems(); 62 | static::assertCount(1, $items); 63 | 64 | $attachment = new Attachment('http://therecord.co/downloads/The-Record-sp1e1-ChrisParrish.m4a', 'audio/x-m4a'); 65 | $attachment->setSize(89970236); 66 | $attachment->setDuration(6629); 67 | 68 | $item = new Item('http://therecord.co/chris-parrish'); 69 | $item->setTitle('Special #1 - Chris Parrish'); 70 | $item->setUrl('http://therecord.co/chris-parrish'); 71 | $item->setContentText('Chris has worked at Adobe and as a founder of Rogue Sheep, which won an Apple Design Award for Postage. Chris’s new company is Aged & Distilled with Guy English — which shipped Napkin, a Mac app for visual collaboration. Chris is also the co-host of The Record. He lives on Bainbridge Island, a quick ferry ride from Seattle.'); 72 | $item->setContentHtml('Chris has worked at Adobe and as a founder of Rogue Sheep, which won an Apple Design Award for Postage. Chris’s new company is Aged & Distilled with Guy English — which shipped Napkin, a Mac app for visual collaboration. Chris is also the co-host of The Record. He lives on Bainbridge Island, a quick ferry ride from Seattle.'); 73 | $item->setSummary('Brent interviews Chris Parrish, co-host of The Record and one-half of Aged & Distilled.'); 74 | $item->setDatePublished(new DateTime('2014-05-09T14:04:00-07:00')); 75 | $item->addAttachment($attachment); 76 | static::assertEquals($item, $items[0]); 77 | } 78 | 79 | public function testMicroblogFeed(): void 80 | { 81 | $input = $this->getFixtures('microblog'); 82 | $reader = FeedReader::create(); 83 | 84 | $feed = $reader->readFromJson($input); 85 | static::assertInstanceOf('JDecool\JsonFeed\Feed', $feed); 86 | static::assertEquals('Brent Simmons’s Microblog', $feed->getTitle()); 87 | static::assertEquals('https://example.org/', $feed->getHomepageUrl()); 88 | static::assertEquals('https://example.org/feed.json', $feed->getFeedUrl()); 89 | static::assertEquals('This is a microblog feed. You can add this to your feed reader using the following URL: https://example.org/feed.json', $feed->getUserComment()); 90 | 91 | $items = $feed->getItems(); 92 | static::assertCount(1, $items); 93 | 94 | $item = new Item('2347259'); 95 | $item->setUrl('https://example.org/2347259'); 96 | $item->setContentText('Cats are neat. https://example.org/cats'); 97 | $item->setDatePublished(new DateTime('2016-02-09T14:22:00+02:00')); 98 | static::assertEquals($item, $items[0]); 99 | } 100 | 101 | public function testAuthorsFeed(): void 102 | { 103 | $input = $this->getFixtures('authors'); 104 | $reader = FeedReader::create(); 105 | 106 | $feed = $reader->readFromJson($input); 107 | static::assertInstanceOf('JDecool\JsonFeed\Feed', $feed); 108 | static::assertEquals('My Example Feed', $feed->getTitle()); 109 | static::assertEquals('Global Author', $feed->getAuthor()->getName()); 110 | static::assertEquals('https://example.org/feed.json', $feed->getFeedUrl()); 111 | 112 | $items = $feed->getItems(); 113 | static::assertCount(2, $items); 114 | 115 | $item2Author = new Author('Author 2'); 116 | $item2 = new Item('2'); 117 | $item2->setUrl('https://example.org/2'); 118 | $item2->setContentText('This is a second item.'); 119 | $item2->setAuthor($item2Author); 120 | static::assertEquals('Author 2', $item2->getAuthor()->getName()); 121 | static::assertEquals($item2, $items[0]); 122 | 123 | $item1Author = new Author('Author 1'); 124 | $item1 = new Item('1'); 125 | $item1->setUrl('https://example.org/1'); 126 | $item1->setContentHtml('

This is the first item.

'); 127 | $item1->setAuthor($item1Author); 128 | static::assertEquals('Author 1', $item1->getAuthor()->getName()); 129 | static::assertEquals($item1, $items[1]); 130 | } 131 | 132 | public function testReaderWithJsonSyntaxError(): void 133 | { 134 | $input = <<expectException(InvalidFeedException::class); 144 | $this->expectExceptionMessage('Invalid JSONFeed string'); 145 | 146 | $reader->readFromJson($input); 147 | } 148 | 149 | public function testReaderWithInvalidProperty(): void 150 | { 151 | $input = <<expectException(InvalidFeedException::class); 162 | $this->expectExceptionMessage('Invalid feed property "custom"'); 163 | 164 | $reader->readFromJson($input); 165 | } 166 | 167 | public function testReaderWithInvalidAuthorProperty(): void 168 | { 169 | $input = <<expectException(InvalidFeedException::class); 183 | $this->expectExceptionMessage('Invalid author property "foo"'); 184 | 185 | $reader->readFromJson($input); 186 | } 187 | 188 | public function testReaderWithInvalidItemProperty(): void 189 | { 190 | $input = <<expectException(InvalidFeedException::class); 210 | $this->expectExceptionMessage('Invalid item property "foo"'); 211 | 212 | $reader->readFromJson($input); 213 | } 214 | 215 | public function testReaderWithInvalidPropertyWithErrorEnabled(): void 216 | { 217 | $input = <<readFromJson($input); 228 | static::assertInstanceOf('JDecool\JsonFeed\Feed', $feed); 229 | static::assertEquals('Brent Simmons’s Microblog', $feed->getTitle()); 230 | } 231 | 232 | public function testReadExtensions(): void 233 | { 234 | $input = $this->getFixtures('extension'); 235 | $reader = FeedReader::create(); 236 | 237 | $feed = $reader->readFromJson($input); 238 | static::assertInstanceOf('JDecool\JsonFeed\Feed', $feed); 239 | static::assertEquals('My Example Feed', $feed->getTitle()); 240 | 241 | $items = $feed->getItems(); 242 | static::assertCount(2, $items); 243 | 244 | $item = new Item('2'); 245 | $item->setContentText('This is a second item.'); 246 | $item->setUrl('https://example.org/second-item'); 247 | $item->addExtension('extItem2', ['foo' => 'value', 'bar' => 'value']); 248 | $item->addExtension('extAuthor', ['john' => 'doe', 'jane' => 'doe']); 249 | static::assertEquals($item, $items[0]); 250 | 251 | $item = new Item('1'); 252 | $item->setContentHtml('

Hello, world!

'); 253 | $item->setUrl('https://example.org/initial-post'); 254 | $item->addExtension('extAuthor', ['john' => 'doe', 'jane' => 'doe']); 255 | static::assertEquals($item, $items[1]); 256 | } 257 | 258 | private function getFixtures($name) 259 | { 260 | return file_get_contents(self::$fixturesPath.'/'.$name.'.json'); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /test/Writer/RendererFactoryTest.php: -------------------------------------------------------------------------------- 1 | createRenderer(); 20 | static::assertInstanceOf('JDecool\JsonFeed\Writer\Version1\Renderer', $renderer); 21 | } 22 | 23 | public function testCreateVersion1Renderer(): void 24 | { 25 | $factory = new RendererFactory(); 26 | 27 | $renderer = $factory->createRenderer(Versions::VERSION_1); 28 | static::assertInstanceOf('JDecool\JsonFeed\Writer\Version1\Renderer', $renderer); 29 | } 30 | 31 | public function testCreateWrongVersionRenderer(): void 32 | { 33 | $factory = new RendererFactory(); 34 | 35 | $this->expectException(RuntimeException::class); 36 | $this->expectExceptionMessage('No renderer registered for version "foo"'); 37 | 38 | $factory->createRenderer('foo'); 39 | } 40 | 41 | public function testRegisterCustomProvider(): void 42 | { 43 | $customRenderer = $this->getMockBuilder('JDecool\JsonFeed\Writer\RendererInterface')->getMock(); 44 | $customRenderer->method('render') 45 | ->willReturn('custom render') 46 | ; 47 | 48 | $factory = new RendererFactory(); 49 | $factory->registerRenderer('custom', $customRenderer); 50 | 51 | $renderer = $factory->createRenderer('custom'); 52 | static::assertEquals('custom render', $renderer->render(new Feed('My feed'))); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/Writer/Version1/RendererTest.php: -------------------------------------------------------------------------------- 1 | setHomepageUrl('https://example.org/'); 29 | $feed->setFeedUrl('https://example.org/feed.json'); 30 | 31 | $item2 = new Item('2'); 32 | $item2->setContentText('This is a second item.'); 33 | $item2->setUrl('https://example.org/second-item'); 34 | $feed->addItem($item2); 35 | 36 | $item1 = new Item('1'); 37 | $item1->setContentHtml('

Hello, world!

'); 38 | $item1->setUrl('https://example.org/initial-post'); 39 | $feed->addItem($item1); 40 | 41 | $expected = $this->getFixtures('simple'); 42 | 43 | $render = new Renderer(); 44 | static::assertJsonStringEqualsJsonString($expected, $render->render($feed)); 45 | } 46 | 47 | public function testPodcastFeed(): void 48 | { 49 | $attachment = new Attachment('http://therecord.co/downloads/The-Record-sp1e1-ChrisParrish.m4a', 'audio/x-m4a'); 50 | $attachment->setSize(89970236); 51 | $attachment->setDuration(6629); 52 | 53 | $item = new Item('http://therecord.co/chris-parrish'); 54 | $item->setTitle('Special #1 - Chris Parrish'); 55 | $item->setUrl('http://therecord.co/chris-parrish'); 56 | $item->setContentHtml('Chris has worked at Adobe and as a founder of Rogue Sheep, which won an Apple Design Award for Postage. Chris’s new company is Aged & Distilled with Guy English — which shipped Napkin, a Mac app for visual collaboration. Chris is also the co-host of The Record. He lives on Bainbridge Island, a quick ferry ride from Seattle.'); 57 | $item->setContentText('Chris has worked at Adobe and as a founder of Rogue Sheep, which won an Apple Design Award for Postage. Chris’s new company is Aged & Distilled with Guy English — which shipped Napkin, a Mac app for visual collaboration. Chris is also the co-host of The Record. He lives on Bainbridge Island, a quick ferry ride from Seattle.'); 58 | $item->setSummary('Brent interviews Chris Parrish, co-host of The Record and one-half of Aged & Distilled.'); 59 | $item->setDatePublished(new DateTime('2014-05-09 14:04:00', new DateTimeZone('Etc/GMT+7'))); 60 | $item->addAttachment($attachment); 61 | 62 | $feed = new Feed('The Record'); 63 | $feed->setUserComment('This is a podcast feed. You can add this feed to your podcast client using the following URL: http://therecord.co/feed.json'); 64 | $feed->setHomepageUrl('http://therecord.co/'); 65 | $feed->setFeedUrl('http://therecord.co/feed.json'); 66 | $feed->addItem($item); 67 | 68 | $expected = $this->getFixtures('podcast'); 69 | 70 | $render = new Renderer(); 71 | static::assertJsonStringEqualsJsonString($expected, $render->render($feed)); 72 | } 73 | 74 | public function testMicroblogFeed(): void 75 | { 76 | $author = new Author('Brent Simmons'); 77 | $author->setUrl('http://example.org/'); 78 | $author->setAvatar('https://example.org/avatar.png'); 79 | 80 | $item = new Item('2347259'); 81 | $item->setUrl('https://example.org/2347259'); 82 | $item->setDatePublished(new DateTime('2016-02-09 14:22:00', new DateTimeZone('Etc/GMT-2'))); 83 | $item->setContentText('Cats are neat. https://example.org/cats'); 84 | 85 | $feed = new Feed('Brent Simmons’s Microblog'); 86 | $feed->setUserComment('This is a microblog feed. You can add this to your feed reader using the following URL: https://example.org/feed.json'); 87 | $feed->setHomepageUrl('https://example.org/'); 88 | $feed->setFeedUrl('https://example.org/feed.json'); 89 | $feed->setAuthor($author); 90 | $feed->addItem($item); 91 | 92 | $expected = $this->getFixtures('microblog'); 93 | 94 | $render = new Renderer(); 95 | static::assertJsonStringEqualsJsonString($expected, $render->render($feed)); 96 | } 97 | 98 | public function testAuthorsFeed(): void 99 | { 100 | $feedAuthor = new Author('Global Author'); 101 | $feed = new Feed('My Example Feed'); 102 | $feed->setFeedUrl('https://example.org/feed.json'); 103 | $feed->setAuthor($feedAuthor); 104 | 105 | $item2Author = new Author('Author 2'); 106 | $item2 = new Item('2'); 107 | $item2->setUrl('https://example.org/2'); 108 | $item2->setContentText('This is a second item.'); 109 | $item2->setAuthor($item2Author); 110 | $feed->addItem($item2); 111 | 112 | $item1Author = new Author('Author 1'); 113 | $item1 = new Item('1'); 114 | $item1->setUrl('https://example.org/1'); 115 | $item1->setContentHtml('

This is the first item.

'); 116 | $item1->setAuthor($item1Author); 117 | $feed->addItem($item1); 118 | 119 | $expected = $this->getFixtures('authors'); 120 | 121 | $render = new Renderer(); 122 | static::assertJsonStringEqualsJsonString($expected, $render->render($feed)); 123 | } 124 | 125 | public function testRenderExtension(): void 126 | { 127 | $feed = new Feed('My Example Feed'); 128 | 129 | $item2 = new Item('2'); 130 | $item2->setContentText('This is a second item.'); 131 | $item2->setUrl('https://example.org/second-item'); 132 | $item2->addExtension('extItem2', ['foo' => 'value', 'bar' => 'value']); 133 | $item2->addExtension('extAuthor', ['john' => 'doe', 'jane' => 'doe']); 134 | $feed->addItem($item2); 135 | 136 | $item1 = new Item('1'); 137 | $item1->setContentHtml('

Hello, world!

'); 138 | $item1->setUrl('https://example.org/initial-post'); 139 | $item1->addExtension('extAuthor', ['john' => 'doe', 'jane' => 'doe']); 140 | $feed->addItem($item1); 141 | 142 | $expected = $this->getFixtures('extension'); 143 | 144 | $render = new Renderer(); 145 | static::assertJsonStringEqualsJsonString($expected, $render->render($feed)); 146 | } 147 | 148 | private function getFixtures($name) 149 | { 150 | return file_get_contents(self::$fixturesPath.'/'.$name.'.json'); 151 | } 152 | } 153 | --------------------------------------------------------------------------------