diff --git a/composer.json b/composer.json index d16824c..dbea369 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ } ], "require": { - "php": ">=5.4.0" + "php": ">=5.4.0", + "symfony/property-access": "^2.7|^3.0" }, "require-dev": { "phpunit/phpunit": "^4.8|^5.0|^6.1" diff --git a/src/Reader/Reader.php b/src/Reader/Reader.php new file mode 100644 index 0000000..061407d --- /dev/null +++ b/src/Reader/Reader.php @@ -0,0 +1,46 @@ +readers = $readers; + } + + /** + * Read feed from JSON + * + * @param string $json + * @return \JDecool\JsonFeed\Feed + */ + public function createFromJson($json) + { + $content = json_decode($json, true); + if (!is_array($content)) { + throw new InvalidArgumentException('Invalid JSONFeed string'); + } + + if (!isset($content['version'])) { + throw new RuntimeException('JSONFeed version is not defined'); + } + + if (!isset($this->readers[$content['version']])) { + throw new RuntimeException(sprintf('No reader for version "%s"', $content['version'])); + } + + return $this->readers[$content['version']]->readFromJson($json); + } +} diff --git a/src/Reader/ReaderBuilder.php b/src/Reader/ReaderBuilder.php new file mode 100644 index 0000000..6baf34b --- /dev/null +++ b/src/Reader/ReaderBuilder.php @@ -0,0 +1,20 @@ + Version1\FeedReader::create(), + ]); + } +} diff --git a/src/Reader/ReaderInterface.php b/src/Reader/ReaderInterface.php new file mode 100644 index 0000000..dd62e6f --- /dev/null +++ b/src/Reader/ReaderInterface.php @@ -0,0 +1,16 @@ +accessor = PropertyAccess::createPropertyAccessor(); + } + + /** + * {@inheritdoc} + */ + public function readFromJson($json) + { + $content = json_decode($json, true); + if (!is_array($content)) { + throw new InvalidArgumentException('Invalid JSONFeed string'); + } + + return $this->readFeedNode($content); + } + + /** + * Browse feed node + * + * @param array $content + * @return Feed + */ + private function readFeedNode(array $content) + { + $feed = new Feed(''); + + foreach ($content as $key => $value) { + if ('version' === $key) { + continue; + } + + switch ($key) { + case 'author': + $feed->setAuthor($this->readAuthorNode($value)); + break; + + case 'hubs': + $feed->setHubs(array_map([$this, 'readHubNode'], $value)); + break; + + case 'items': + $feed->setItems(array_map([$this, 'readItemNode'], $value)); + break; + + default: + $this->accessor->setValue($feed, $key, $value); + } + } + + return $feed; + } + + /** + * Browse item node + * + * @param array $content + * @return Item + */ + private function readItemNode(array $content) + { + $id = isset($content['id']) ? $content['id'] : ''; + + $item = new Item($id); + foreach ($content as $key => $value) { + if ('id' === $key) { + continue; + } + + switch ($key) { + case 'attachments': + $item->setAttachments(array_map([$this, 'readAttachmentNode'], $value)); + break; + + case 'author': + $item->setAuthor($this->readAuthorNode($value)); + break; + + case 'date_published': + case 'date_modified': + $this->accessor->setValue($item, $key, new DateTime($value)); + break; + + default: + $this->accessor->setValue($item, $key, $value); + } + } + + return $item; + } + + /** + * Browse author node + * + * @param array $content + * @return Author + */ + private function readAuthorNode(array $content) + { + $name = (isset($content['name'])) ? $content['name'] : ''; + + $author = new Author($name); + foreach ($content as $key => $value) { + if ('name' === $key) { + continue; + } + + $this->accessor->setValue($author, $key, $value); + } + + return $author; + } + + /** + * Browse hub node + * + * @param array $content + * @return Hub + */ + private function readHubNode(array $content) + { + $type = isset($content['type']) ? $content['type'] : ''; + $url = isset($content['url']) ? $content['url'] : ''; + + return new Hub($type, $url); + } + + /** + * Browse attachment node + * + * @param array $content + * @return Attachment + */ + private function readAttachmentNode(array $content) + { + $url = isset($content['url']) ? $content['url'] : ''; + $mimeType = isset($content['mime_type']) ? $content['mime_type'] : ''; + + $attachment = new Attachment($url, $mimeType); + foreach ($content as $key => $value) { + switch ($key) { + case 'size_in_bytes': + $attachment->setSize($value); + break; + + case 'duration_in_seconds': + $attachment->setDuration($value); + break; + } + } + + return $attachment; + } +} diff --git a/test/Reader/ReaderBuilderTest.php b/test/Reader/ReaderBuilderTest.php new file mode 100644 index 0000000..39178b4 --- /dev/null +++ b/test/Reader/ReaderBuilderTest.php @@ -0,0 +1,16 @@ +assertInstanceOf('JDecool\JsonFeed\Reader\Reader', $builder->build()); + } +} diff --git a/test/Reader/ReaderTest.php b/test/Reader/ReaderTest.php new file mode 100644 index 0000000..57664c7 --- /dev/null +++ b/test/Reader/ReaderTest.php @@ -0,0 +1,79 @@ +getMockBuilder('JDecool\JsonFeed\Reader\ReaderInterface')->getMock(); + $feedReader->expects($this->once()) + ->method('readFromJson'); + + $reader = new Reader([ + 'foo' => $feedReader, + ]); + + $reader->createFromJson($json); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessage No reader for version "foo" + */ + public function testCreateFromJsonWithInvalidReader() + { + $json = <<createFromJson($json); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Invalid JSONFeed string + */ + public function testCreateFromJsonWithInvalidString() + { + $json = <<createFromJson($json); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessage JSONFeed version is not defined + */ + public function testCreateFromJsonWithoutVersion() + { + $json = <<createFromJson($json); + } +} diff --git a/test/Reader/Version1/FeedReaderTest.php b/test/Reader/Version1/FeedReaderTest.php new file mode 100644 index 0000000..ce5a9f9 --- /dev/null +++ b/test/Reader/Version1/FeedReaderTest.php @@ -0,0 +1,101 @@ +getFixtures('simple'); + $reader = FeedReader::create(); + + $feed = $reader->readFromJson($input); + $this->assertInstanceOf('JDecool\JsonFeed\Feed', $feed); + $this->assertEquals('My Example Feed', $feed->getTitle()); + $this->assertEquals('https://example.org/', $feed->getHomepageUrl()); + $this->assertEquals('https://example.org/feed.json', $feed->getFeedUrl()); + + $items = $feed->getItems(); + $this->assertCount(2, $items); + + $item2 = new Item('2'); + $item2->setContentText('This is a second item.'); + $item2->setUrl('https://example.org/second-item'); + $this->assertEquals($item2, $items[0]); + + $item1 = new Item('1'); + $item1->setContentHtml('

Hello, world!

'); + $item1->setUrl('https://example.org/initial-post'); + $this->assertEquals($item1, $items[1]); + } + + public function testPodcastFeed() + { + $input = $this->getFixtures('podcast'); + $reader = FeedReader::create(); + + $feed = $reader->readFromJson($input); + $this->assertInstanceOf('JDecool\JsonFeed\Feed', $feed); + $this->assertEquals('The Record', $feed->getTitle()); + $this->assertEquals('http://therecord.co/', $feed->getHomepageUrl()); + $this->assertEquals('http://therecord.co/feed.json', $feed->getFeedUrl()); + $this->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()); + + $items = $feed->getItems(); + $this->assertCount(1, $items); + + $attachment = new Attachment('http://therecord.co/downloads/The-Record-sp1e1-ChrisParrish.m4a', 'audio/x-m4a'); + $attachment->setSize(89970236); + $attachment->setDuration(6629); + + $item = new Item('http://therecord.co/chris-parrish'); + $item->setTitle('Special #1 - Chris Parrish'); + $item->setUrl('http://therecord.co/chris-parrish'); + $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.'); + $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.'); + $item->setSummary('Brent interviews Chris Parrish, co-host of The Record and one-half of Aged & Distilled.'); + $item->setDatePublished(new DateTime('2014-05-09T14:04:00-07:00')); + $item->addAttachment($attachment); + $this->assertEquals($item, $items[0]); + } + + public function testMicroblogFeed() + { + $input = $this->getFixtures('microblog'); + $reader = FeedReader::create(); + + $feed = $reader->readFromJson($input); + $this->assertInstanceOf('JDecool\JsonFeed\Feed', $feed); + $this->assertEquals('Brent Simmons’s Microblog', $feed->getTitle()); + $this->assertEquals('https://example.org/', $feed->getHomepageUrl()); + $this->assertEquals('https://example.org/feed.json', $feed->getFeedUrl()); + $this->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()); + + $items = $feed->getItems(); + $this->assertCount(1, $items); + + $item = new Item('2347259'); + $item->setUrl('https://example.org/2347259'); + $item->setContentText('Cats are neat. https://example.org/cats'); + $item->setDatePublished(new DateTime('2016-02-09T14:22:00+02:00')); + $this->assertEquals($item, $items[0]); + } + + private function getFixtures($name) + { + return file_get_contents(self::$fixturesPath.'/'.$name.'.json'); + } +}