From 93f0f275614edad4c1c4c3f5e56f3da3b64cde05 Mon Sep 17 00:00:00 2001 From: flashwave Date: Thu, 3 Oct 2024 20:39:40 +0000 Subject: [PATCH] Added RSS feed builder classes. --- TODO.md | 17 +--- VERSION | 2 +- src/Syndication/FeedBuilder.php | 147 ++++++++++++++++++++++++++++ src/Syndication/FeedItemBuilder.php | 137 ++++++++++++++++++++++++++ tests/SyndicationTest.php | 132 +++++++++++++++++++++++++ 5 files changed, 418 insertions(+), 17 deletions(-) create mode 100644 src/Syndication/FeedBuilder.php create mode 100644 src/Syndication/FeedItemBuilder.php create mode 100644 tests/SyndicationTest.php diff --git a/TODO.md b/TODO.md index 19f84dd..8f1320a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,20 +1,5 @@ # TODO -## High Prio - - - Create tests for everything testable. - - - Create similarly address GD2/Imagick wrappers. - - - Figure out the SQL Query builder. - - Might make sense to just have a predicate builder and selector builder. + - Create similarly addressable GD2/Imagick wrappers. - Create a URL formatter. - - - Add RSS/Atom feed construction utilities. - -## Low Prio - - - Get guides working on phpdoc. - - - Create phpdoc template. diff --git a/VERSION b/VERSION index 199aff6..c5fd99e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2410.21941 +0.2410.32039 diff --git a/src/Syndication/FeedBuilder.php b/src/Syndication/FeedBuilder.php new file mode 100644 index 0000000..ff1d425 --- /dev/null +++ b/src/Syndication/FeedBuilder.php @@ -0,0 +1,147 @@ +title = $title; + } + + /** + * Sets the description of the feed, may contain HTML. + * + * @param string $description Feed description. + */ + public function setDescription(string $description): void { + $this->description = $description; + } + + /** + * Sets the last updated time for this feed. + * + * @param DateTimeInterface|int $updatedAt Timestamp at which the feed was last updated. + */ + public function setUpdatedAt(DateTimeInterface|int $updatedAt): void { + $this->updatedAt = XDateTime::toRfc822String($updatedAt); + } + + /** + * Sets the URL to the content this feed represents. + * + * @param string $url Content URL. + */ + public function setContentUrl(string $url): void { + $this->contentUrl = $url; + } + + /** + * Sets the URL to this feed's XML file. + * + * @param string $url Feed XML URL. + */ + public function setFeedUrl(string $url): void { + $this->feedUrl = $url; + } + + /** + * Creates a feed item using a builder. + * + * @param callable(FeedItemBuilder): void $handler Item builder closure. + */ + public function createEntry(callable $handler): void { + $builder = new FeedItemBuilder; + $handler($builder); + $this->items[] = $builder; + } + + /** + * Creates a DOMDocument from this builder. + * + * @return DOMDocument XML document representing this feed. + */ + public function toXmlDocument(): DOMDocument { + $document = new DOMDocument('1.0', 'UTF-8'); + $document->preserveWhiteSpace = false; + $document->formatOutput = true; + + $rss = $document->createElement('rss'); + $rss->setAttribute('version', '2.0'); + $rss->setAttribute('xmlns:atom', 'http://www.w3.org/2005/Atom'); + $document->appendChild($rss); + + $channel = $document->createElement('channel'); + + $elements = [ + 'title' => $this->title, + 'pubDate' => $this->updatedAt, + 'link' => $this->contentUrl, + ]; + foreach($elements as $elemName => $elemValue) + if(!XString::nullOrWhitespace($elemValue)) + $channel->appendChild($document->createElement($elemName, $elemValue)); + + if(!XString::nullOrWhitespace($this->description)) { + $description = $document->createElement('description'); + $description->appendChild($document->createCDATASection($this->description)); + $channel->appendChild($description); + } + + if(!XString::nullOrWhitespace($this->feedUrl)) { + $element = $document->createElement('atom:link'); + $element->setAttribute('href', $this->feedUrl); + $element->setAttribute('ref', 'self'); + $element->setAttribute('type', 'application/rss+xml'); + $channel->appendChild($element); + } + + foreach($this->items as $item) + $channel->appendChild($item->createElement($document)); + + $rss->appendChild($channel); + + return $document; + } + + /** + * Converts the XML Document to a printable string. + * + * @return string Printable XML document. + */ + public function toXmlString(): string { + $string = $this->toXmlDocument()->saveXML(); + if($string === false) + $string = ''; + + return $string; + } + + public function __toString(): string { + return $this->toXmlString(); + } +} diff --git a/src/Syndication/FeedItemBuilder.php b/src/Syndication/FeedItemBuilder.php new file mode 100644 index 0000000..b77db6d --- /dev/null +++ b/src/Syndication/FeedItemBuilder.php @@ -0,0 +1,137 @@ +title = $title; + } + + /** + * Sets the description of the feed item, may contain HTML. + * + * @param string $description Item description. + */ + public function setDescription(string $description): void { + $this->description = $description; + } + + /** + * Sets the creation time for this feed item. + * + * @param DateTimeInterface|int $createdAt Timestamp at which the feed item was created. + */ + public function setCreatedAt(DateTimeInterface|int $createdAt): void { + $this->createdAt = XDateTime::toRfc822String($createdAt); + } + + /** + * Sets the last updated time for this feed item. + * + * @param DateTimeInterface|int $updatedAt Timestamp at which the feed item was last updated. + */ + public function setUpdatedAt(DateTimeInterface|int $updatedAt): void { + $this->updatedAt = XDateTime::toIso8601String($updatedAt); + } + + /** + * Sets the URL to the content this feed item represents. + * + * @param string $url Content URL. + */ + public function setContentUrl(string $url): void { + $this->contentUrl = $url; + } + + /** + * Sets the URL to where comments can be made on the content this feed item represents. + * + * @param string $url Comments URL. + */ + public function setCommentsUrl(string $url): void { + $this->commentsUrl = $url; + } + + /** + * Sets the name of the author of this feed item. + * + * @param string $name Name of the author. + */ + public function setAuthorName(string $name): void { + $this->authorName = $name; + } + + /** + * Sets the URL for the author of this feed item. Has no effect if no author name is provided. + * + * @param string $url URL for the author. + */ + public function setAuthorUrl(string $url): void { + $this->authorUrl = $url; + } + + /** + * Creates an XML document based on this item builder. + * + * @param DOMDocument $document Document to which this element is to be appended. + * @return DOMElement Element created from this builder. + */ + public function createElement(DOMDocument $document): DOMElement { + $item = $document->createElement('item'); + + $elements = [ + 'title' => $this->title, + 'pubDate' => $this->createdAt, + 'atom:updated' => $this->updatedAt, + 'link' => $this->contentUrl, + 'guid' => $this->contentUrl, + 'comments' => $this->commentsUrl, + ]; + foreach($elements as $elemName => $elemValue) + if(!XString::nullOrWhitespace($elemValue)) + $item->appendChild($document->createElement($elemName, $elemValue)); + + if(!XString::nullOrWhitespace($this->description)) { + $description = $document->createElement('description'); + $description->appendChild($document->createCDATASection($this->description)); + $item->appendChild($description); + } + + if(!XString::nullOrWhitespace($this->authorName)) { + $author = $document->createElement('atom:author'); + $author->appendChild($document->createElement('atom:name', $this->authorName)); + + if(!XString::nullOrWhitespace($this->authorUrl)) + $author->appendChild($document->createElement('atom:uri', $this->authorUrl)); + + $item->appendChild($author); + } + + return $item; + } +} diff --git a/tests/SyndicationTest.php b/tests/SyndicationTest.php new file mode 100644 index 0000000..056a76a --- /dev/null +++ b/tests/SyndicationTest.php @@ -0,0 +1,132 @@ + + + + Feed + + + + XML; + + private const NO_ITEMS_FEED = << + + + Index RSS + Thu, 03 Oct 24 20:06:25 +0000 + https://example.railgun.sh/articles + This should accept HTML!]]> + + + + + XML; + + private const ITEMS_FEED = << + + + Index RSS + Thu, 03 Oct 24 20:06:25 +0000 + https://example.railgun.sh/articles + This should accept HTML!]]> + + + Article 1 + Sat, 28 Sep 24 01:13:05 +0000 + https://example.railgun.sh/articles/1 + https://example.railgun.sh/articles/1 + https://example.railgun.sh/articles/1#comments + This is an article!

+

It has HTML and multiple lines and all that awesome stuff!!!!

+

Meaning it can also have sections

+

Technology is so cool...

]]>
+ + flashwave + https://example.railgun.sh/author/1 + +
+ + Article 2 + Sun, 29 Sep 24 04:59:45 +0000 + https://example.railgun.sh/articles/2 + https://example.railgun.sh/articles/2 + https://example.railgun.sh/articles/2#comments + hidden author spooky

]]>
+
+ + Article 3 without a description + Mon, 30 Sep 24 08:46:25 +0000 + https://example.railgun.sh/articles/3 + https://example.railgun.sh/articles/3 + https://example.railgun.sh/articles/3#comments + + Anonymous + + +
+
+ + XML; + + public function testFeedBuilder(): void { + $feed = new FeedBuilder; + + $this->assertEquals(self::BLANK_FEED, $feed->toXmlString()); + + $feed->setTitle('Index RSS'); + $feed->setDescription('This should accept HTML!'); + $feed->setUpdatedAt(1727985985); + $feed->setContentUrl('https://example.railgun.sh/articles'); + $feed->setFeedUrl('https://example.railgun.sh/feed.xml'); + + $this->assertEquals(self::NO_ITEMS_FEED, $feed->toXmlString()); + + $feed->createEntry(function($item) { + $item->setTitle('Article 1'); + $item->setDescription(<<This is an article!

+

It has HTML and multiple lines and all that awesome stuff!!!!

+

Meaning it can also have sections

+

Technology is so cool...

+ HTML); + $item->setCreatedAt(1727485985); + $item->setContentUrl('https://example.railgun.sh/articles/1'); + $item->setCommentsUrl('https://example.railgun.sh/articles/1#comments'); + $item->setAuthorName('flashwave'); + $item->setAuthorUrl('https://example.railgun.sh/author/1'); + }); + $feed->createEntry(function($item) { + $item->setTitle('Article 2'); + $item->setDescription(<<hidden author spooky

+ HTML); + $item->setCreatedAt(1727585985); + $item->setContentUrl('https://example.railgun.sh/articles/2'); + $item->setCommentsUrl('https://example.railgun.sh/articles/2#comments'); + }); + $feed->createEntry(function($item) { + $item->setTitle('Article 3 without a description'); + $item->setCreatedAt(1727685985); + $item->setContentUrl('https://example.railgun.sh/articles/3'); + $item->setCommentsUrl('https://example.railgun.sh/articles/3#comments'); + $item->setAuthorName('Anonymous'); + }); + + $this->assertEquals(self::ITEMS_FEED, $feed->toXmlString()); + } +}