Added RSS feed builder classes.
This commit is contained in:
parent
67ea562983
commit
93f0f27561
5 changed files with 418 additions and 17 deletions
17
TODO.md
17
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.
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2410.21941
|
||||
0.2410.32039
|
||||
|
|
147
src/Syndication/FeedBuilder.php
Normal file
147
src/Syndication/FeedBuilder.php
Normal file
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
// FeedBuilder.php
|
||||
// Created: 2024-10-02
|
||||
// Updated: 2024-10-03
|
||||
|
||||
namespace Index\Syndication;
|
||||
|
||||
use DateTimeInterface;
|
||||
use DOMDocument;
|
||||
use Stringable;
|
||||
use RuntimeException;
|
||||
use Index\{XDateTime,XString};
|
||||
|
||||
/**
|
||||
* Provides an RSS feed builder with Atom extensions.
|
||||
*/
|
||||
class FeedBuilder implements Stringable {
|
||||
private string $title = 'Feed';
|
||||
private string $description = '';
|
||||
private string $updatedAt = '';
|
||||
private string $contentUrl = '';
|
||||
private string $feedUrl = '';
|
||||
|
||||
/** @var FeedItemBuilder[] */
|
||||
private array $items = [];
|
||||
|
||||
/**
|
||||
* Sets the title of the feed.
|
||||
*
|
||||
* @param string $title Feed title.
|
||||
*/
|
||||
public function setTitle(string $title): void {
|
||||
$this->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();
|
||||
}
|
||||
}
|
137
src/Syndication/FeedItemBuilder.php
Normal file
137
src/Syndication/FeedItemBuilder.php
Normal file
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
// FeedItemBuilder.php
|
||||
// Created: 2024-10-02
|
||||
// Updated: 2024-10-03
|
||||
|
||||
namespace Index\Syndication;
|
||||
|
||||
use DateTimeInterface;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use Index\{XDateTime,XString};
|
||||
|
||||
/**
|
||||
* Provides an RSS feed item builder with Atom extensions.
|
||||
*/
|
||||
class FeedItemBuilder {
|
||||
private string $title = 'Feed Item';
|
||||
private string $description = '';
|
||||
private string $createdAt = '';
|
||||
private string $updatedAt = '';
|
||||
private string $contentUrl = '';
|
||||
private string $commentsUrl = '';
|
||||
private string $authorName = '';
|
||||
private string $authorUrl = '';
|
||||
|
||||
/**
|
||||
* Sets the title of this feed item.
|
||||
*
|
||||
* @param string $title Item title.
|
||||
*/
|
||||
public function setTitle(string $title): void {
|
||||
$this->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;
|
||||
}
|
||||
}
|
132
tests/SyndicationTest.php
Normal file
132
tests/SyndicationTest.php
Normal file
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
// SyndicationTest.php
|
||||
// Created: 2024-10-03
|
||||
// Updated: 2024-10-03
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Index\Syndication\{FeedBuilder,FeedItemBuilder};
|
||||
|
||||
#[CoversClass(FeedBuilder::class)]
|
||||
#[CoversClass(FeedItemBuilder::class)]
|
||||
final class SyndicationTest extends TestCase {
|
||||
private const BLANK_FEED = <<<XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Feed</title>
|
||||
</channel>
|
||||
</rss>
|
||||
|
||||
XML;
|
||||
|
||||
private const NO_ITEMS_FEED = <<<XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Index RSS</title>
|
||||
<pubDate>Thu, 03 Oct 24 20:06:25 +0000</pubDate>
|
||||
<link>https://example.railgun.sh/articles</link>
|
||||
<description><![CDATA[<b>This should accept HTML!</b>]]></description>
|
||||
<atom:link href="https://example.railgun.sh/feed.xml" ref="self" type="application/rss+xml"/>
|
||||
</channel>
|
||||
</rss>
|
||||
|
||||
XML;
|
||||
|
||||
private const ITEMS_FEED = <<<XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Index RSS</title>
|
||||
<pubDate>Thu, 03 Oct 24 20:06:25 +0000</pubDate>
|
||||
<link>https://example.railgun.sh/articles</link>
|
||||
<description><![CDATA[<b>This should accept HTML!</b>]]></description>
|
||||
<atom:link href="https://example.railgun.sh/feed.xml" ref="self" type="application/rss+xml"/>
|
||||
<item>
|
||||
<title>Article 1</title>
|
||||
<pubDate>Sat, 28 Sep 24 01:13:05 +0000</pubDate>
|
||||
<link>https://example.railgun.sh/articles/1</link>
|
||||
<guid>https://example.railgun.sh/articles/1</guid>
|
||||
<comments>https://example.railgun.sh/articles/1#comments</comments>
|
||||
<description><![CDATA[<p>This is an article!</p>
|
||||
<p>It has <b>HTML</b> and <em>multiple lines</em> and all that <u>awesome</u> stuff!!!!</p>
|
||||
<h2>Meaning it can also have sections</h2>
|
||||
<p>Technology is so cool...</p>]]></description>
|
||||
<atom:author>
|
||||
<atom:name>flashwave</atom:name>
|
||||
<atom:uri>https://example.railgun.sh/author/1</atom:uri>
|
||||
</atom:author>
|
||||
</item>
|
||||
<item>
|
||||
<title>Article 2</title>
|
||||
<pubDate>Sun, 29 Sep 24 04:59:45 +0000</pubDate>
|
||||
<link>https://example.railgun.sh/articles/2</link>
|
||||
<guid>https://example.railgun.sh/articles/2</guid>
|
||||
<comments>https://example.railgun.sh/articles/2#comments</comments>
|
||||
<description><![CDATA[<p>hidden author spooky</p>]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Article 3 without a description</title>
|
||||
<pubDate>Mon, 30 Sep 24 08:46:25 +0000</pubDate>
|
||||
<link>https://example.railgun.sh/articles/3</link>
|
||||
<guid>https://example.railgun.sh/articles/3</guid>
|
||||
<comments>https://example.railgun.sh/articles/3#comments</comments>
|
||||
<atom:author>
|
||||
<atom:name>Anonymous</atom:name>
|
||||
</atom:author>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
|
||||
XML;
|
||||
|
||||
public function testFeedBuilder(): void {
|
||||
$feed = new FeedBuilder;
|
||||
|
||||
$this->assertEquals(self::BLANK_FEED, $feed->toXmlString());
|
||||
|
||||
$feed->setTitle('Index RSS');
|
||||
$feed->setDescription('<b>This should accept HTML!</b>');
|
||||
$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(<<<HTML
|
||||
<p>This is an article!</p>
|
||||
<p>It has <b>HTML</b> and <em>multiple lines</em> and all that <u>awesome</u> stuff!!!!</p>
|
||||
<h2>Meaning it can also have sections</h2>
|
||||
<p>Technology is so cool...</p>
|
||||
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(<<<HTML
|
||||
<p>hidden author spooky</p>
|
||||
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());
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue