From 635a88540e314be6dc632d3480c4fbfe0bf042d3 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 3 Mar 2020 11:25:38 -0500 Subject: [PATCH] Various changes - Preliminary work on enclosures - Feed and entry language - Better API docs - Class constant visibility --- composer.json | 4 +- composer.lock | 92 ++++++++++++++++++++++++++++++++++- lib/Enclosure/Collection.php | 22 +++++++++ lib/Enclosure/Enclosure.php | 19 ++++++++ lib/Entry.php | 27 +++++++++- lib/Exception.php | 5 +- lib/Feed.php | 2 + lib/Metadata.php | 1 + lib/Parser/Construct.php | 14 ++++-- lib/Parser/Entry.php | 47 ++++++++++++------ lib/Parser/Feed.php | 3 ++ lib/Parser/JSON/Construct.php | 4 +- lib/Parser/JSON/Entry.php | 66 +++++++++++++++++++------ lib/Parser/JSON/Feed.php | 53 ++++++++++++-------- lib/Parser/XML/XPath.php | 2 +- tests/cases/JSON/entry.json | 31 ++++++++++++ tests/cases/JSON/feed.json | 2 + 17 files changed, 328 insertions(+), 66 deletions(-) create mode 100644 lib/Enclosure/Collection.php create mode 100644 lib/Enclosure/Enclosure.php diff --git a/composer.json b/composer.json index 6138df8..03e12c8 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,9 @@ "ext-json": "*", "ext-dom": "*", "ext-intl": "*", - "sabre/uri": "^2.0" + "sabre/uri": "^2.0", + "ralouphie/mimey": "^2.1", + "psr/http-message": "^1.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 3e32663..0faff87 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,98 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4591fe69b23762fb439aec3883d74eeb", + "content-hash": "d9af1379d5c9a22f374e013981a951b5", "packages": [ + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "ralouphie/mimey", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/mimey.git", + "reference": "8f74e6da73f9df7bd965e4e123f3d8fb9acb89ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/mimey/zipball/8f74e6da73f9df7bd965e4e123f3d8fb9acb89ba", + "reference": "8f74e6da73f9df7bd965e4e123f3d8fb9acb89ba", + "shasum": "" + }, + "require": { + "php": "^5.4|^7.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^1.1", + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mimey\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "PHP package for converting file extensions to MIME types and vice versa.", + "time": "2019-03-08T08:49:03+00:00" + }, { "name": "sabre/uri", "version": "2.2.0", diff --git a/lib/Enclosure/Collection.php b/lib/Enclosure/Collection.php new file mode 100644 index 0000000..5afcb3b --- /dev/null +++ b/lib/Enclosure/Collection.php @@ -0,0 +1,22 @@ +people = new PersonCollection; $this->categories = new CategoryCollection; + $this->enclosures = new EnclosureCollection; } } diff --git a/lib/Exception.php b/lib/Exception.php index 9e39eb0..5f47d11 100644 --- a/lib/Exception.php +++ b/lib/Exception.php @@ -7,12 +7,11 @@ declare(strict_types=1); namespace JKingWeb\Lax; abstract class Exception extends \Exception { - const SYMBOLS = [ + public const SYMBOLS = [ // Parsing: 0x1100 "notJSONType" => [0x1111, "Document Content-Type is not either that of JSON Feed or generic JSON"], "notJSON" => [0x1112, "Document is not valid JSON"], - "notJSONFeed" => [0x1113, "Document is not a JSON Feed document"], - "unsupportedJSONFeedVersion" => [0x1114, "Document specifies an unsupported JSON Feed version"] + "notJSONFeed" => [0x1113, "Document is not a JSON Feed document"] ]; public function __construct(string $symbol, \Exception $e = null) { diff --git a/lib/Feed.php b/lib/Feed.php index e01d036..6daf8cf 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -30,6 +30,8 @@ class Feed { * The version is largely advisory, but may be used when converting between formats */ public $version; + /** @var string $lang The human language of the newsfeed as a whole */ + public $lang; /** @var string $id The globally unique identifier for the newsfeed * * For some formats, such as RSS 2.0 and JSON Feed, this may be he same as the newsfeed URL diff --git a/lib/Metadata.php b/lib/Metadata.php index ffa35a3..83f927b 100644 --- a/lib/Metadata.php +++ b/lib/Metadata.php @@ -8,6 +8,7 @@ namespace JKingWeb\Lax; class Metadata { public $url; + public $type; public $cached = false; public $lastModified; public $etag; diff --git a/lib/Parser/Construct.php b/lib/Parser/Construct.php index 7d4e0bf..00f4f17 100644 --- a/lib/Parser/Construct.php +++ b/lib/Parser/Construct.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace JKingWeb\Lax\Parser; +use JKingWeb\Lax\Collection; use JKingWeb\Lax\Date; trait Construct { @@ -29,8 +30,7 @@ trait Construct { /** Resolves a relative URL against a base URL */ protected function resolveUrl(string $url, string $base = null): string { - $base = $base ?? ""; - return \Sabre\Uri\resolve($base, $url); + return \Sabre\Uri\resolve($base ?? "", $url); } /** Tests whether a string is a valid e-mail address @@ -50,9 +50,7 @@ trait Construct { return false; } $addr = "$local@$domain"; - // PHP 7.1 and above have the constant defined FIXME: Review if removing support for PHP 7.0 - $flags = defined("\FILTER_FLAG_EMAIL_UNICODE") ? \FILTER_FLAG_EMAIL_UNICODE : 0; - return (bool) filter_var($addr, \FILTER_VALIDATE_EMAIL, $flags); + return (bool) filter_var($addr, \FILTER_VALIDATE_EMAIL, \FILTER_FLAG_EMAIL_UNICODE); } protected function parseDate(string $date): ?Date { @@ -70,4 +68,10 @@ trait Construct { } return $out ?: null; } + + protected function empty($o): bool { + return !array_filter((array) $o, function($v) { + return !is_null($v) && (!$v instanceof Collection || sizeof($v) > 0); + }); + } } diff --git a/lib/Parser/Entry.php b/lib/Parser/Entry.php index f3745ef..dae620d 100644 --- a/lib/Parser/Entry.php +++ b/lib/Parser/Entry.php @@ -8,31 +8,50 @@ namespace JKingWeb\Lax\Parser; use JKingWeb\Lax\Person\Collection as PersonCollection; use JKingWeb\Lax\Category\Collection as CategoryCollection; +use JKingWeb\Lax\Enclosure\Collection as EnclosureCollection; use JKingWeb\Lax\Date; use JKingWeb\Lax\Text; interface Entry { - /** General function to fetch the entry title */ + /** Returns the globally unique identifier of the entry; this is usually a URI */ + public function getId(): ?string; + + /** Returns the human language of the entry */ + public function getLang(): ?string; + + /** Returns the title text of the entry, which may be plain text or HTML */ public function getTitle(): ?Text; - /** General function to fetch the categories of an entry */ - public function getCategories(): CategoryCollection; + /** Returns the URL of the published article this entry summarizes */ + public function getLink(): ?string; - /** General function to fetch the entry identifier */ - public function getId(): ?string; + /** Returns the URL of an article related to the entry */ + public function getRelatedLink(): ?string; - /** General function to fetch a collection of people associated with an entry */ - public function getPeople(): PersonCollection; + /** Returns the content of the entry, either in plain text or HTML */ + public function getContent(): ?Text; - /** General function to fetch the entry's modification date */ - public function getDateModified(): ?Date; + /** Returns a short description of the entry, either in plain text or HTML; this should be distinct from the content */ + public function getSummary(): ?Text; - /** General function to fetch the entry's creation date */ + /** Returns the date and time at which the entry was first made available */ public function getDateCreated(): ?Date; - /** General function to fetch the Web URL of the entry */ - public function getLink(): ?string; + /** Returns the date and time at which the entry was last modified */ + public function getDateModified(): ?Date; - /** General function to fetch the URL of a article related to the entry */ - public function getRelatedLink(): ?string; + /** Returns the URL of a large image used as a banner when displaying the entry + * + * This is only used by JSON Feed entries + */ + public function getBanner(): ?string; + + /** Returns a collection of categories associated with the entry */ + public function getCategories(): CategoryCollection; + + /** Returns a collection of persons associated with the entry*/ + public function getPeople(): PersonCollection; + + /** Returns a collection of external files associated with the entry i.e. attachments */ + public function getEnclosures(): EnclosureCollection; } diff --git a/lib/Parser/Feed.php b/lib/Parser/Feed.php index 7c9a58d..b759884 100644 --- a/lib/Parser/Feed.php +++ b/lib/Parser/Feed.php @@ -16,6 +16,9 @@ interface Feed { /** Returns the globally unique identifier of the newsfeed; this is usually a URI */ public function getId(): ?string; + /** Returns the human language of the newsfeed */ + public function getLang(): ?string; + /** Returns the canonical URL of the newsfeed, as contained in the document itself */ public function getUrl(): ?string; diff --git a/lib/Parser/JSON/Construct.php b/lib/Parser/JSON/Construct.php index dd46147..d27eacb 100644 --- a/lib/Parser/JSON/Construct.php +++ b/lib/Parser/JSON/Construct.php @@ -86,9 +86,7 @@ trait Construct { $p->name = $this->fetchMember("name", "str", $o); $p->url = $this->fetchUrl("url", $o); $p->avatar = $this->fetchUrl("avatar", $o); - if (array_filter((array) $p, function($v) { - return !is_null($v); - })) { + if (!$this->empty($p)) { // if any keys are set the person is valid $p->role = "author"; return $p; diff --git a/lib/Parser/JSON/Entry.php b/lib/Parser/JSON/Entry.php index ef5bf2a..1d97d2c 100644 --- a/lib/Parser/JSON/Entry.php +++ b/lib/Parser/JSON/Entry.php @@ -10,8 +10,10 @@ use JKingWeb\Lax\Feed as FeedStruct; use JKingWeb\Lax\Entry as EntryStruct; use JKingWeb\Lax\Person\Collection as PersonCollection; use JKingWeb\Lax\Category\Collection as CategoryCollection; +use JKingWeb\Lax\Enclosure\Collection as EnclosureCollection; use JKingWeb\Lax\Category\Category; use JKingWeb\Lax\Date; +use JKingWeb\Lax\Enclosure\Enclosure; use JKingWeb\Lax\Text; class Entry implements \JKingWeb\Lax\Parser\Entry { @@ -20,6 +22,8 @@ class Entry implements \JKingWeb\Lax\Parser\Entry { protected $url; /** @var \JKingWeb\Lax\Feed */ protected $feed; + /** @var \Mimey\MimeTypes */ + protected $mime; public function __construct(\stdClass $data, FeedStruct $feed) { $this->data = $data; @@ -33,6 +37,7 @@ class Entry implements \JKingWeb\Lax\Parser\Entry { public function parse(EntryStruct $entry = null): EntryStruct { $entry = $this->init($entry ?? new EntryStruct); + $entry->lang = $this->getLang(); $entry->id = $this->getId(); $entry->link = $this->getLink(); $entry->relatedLink = $this->getRelatedLink(); @@ -41,24 +46,10 @@ class Entry implements \JKingWeb\Lax\Parser\Entry { $entry->dateCreated = $this->getDateCreated(); $entry->people = $this->getPeople(); $entry->categories = $this->getCategories(); + $entry->enclosures = $this->getEnclosures(); return $entry; } - public function getCategories(): CategoryCollection { - $out = new CategoryCollection; - foreach ($this->fetchMember("tags", "array") ?? [] as $tag) { - if (is_string($tag)) { - $tag = $this->trimText($tag); - if (strlen($tag)) { - $c = new Category; - $c->name = $tag; - $out[] = $c; - } - } - } - return $out; - } - public function getId(): ?string { $id = $this->fetchMember("id", "str") ?? $this->fetchMember("id", "int") ?? $this->fetchMember("id", "float"); if (is_null($id)) { @@ -102,6 +93,10 @@ class Entry implements \JKingWeb\Lax\Parser\Entry { } } + public function getLang(): ?string { + return $this->fetchMember("language", "str") ?? $this->feed->lang; + } + public function getPeople(): PersonCollection { return $this->getAuthorsV1() ?? $this->getAuthorV1() ?? $this->feed->people ?? new PersonCollection; } @@ -139,4 +134,45 @@ class Entry implements \JKingWeb\Lax\Parser\Entry { } return $out; } + + public function getBanner(): ?string { + return $this->fetchUrl("banner_image"); + } + + public function getEnclosures(): EnclosureCollection { + $out = new EnclosureCollection; + foreach ($this->fetchMember("attachments", "array") as $attachment) { + $url = $this->fetchUrl("url", $attachment); + if ($url) { + $m = new Enclosure; + $m->url = $url; + $m->type = $this->fetchMember("mime_type", "str", $attachment); + $m->title = $this->fetchMember("title", "str", $attachment); + $m->size = $this->fetchMember("size_in_bytes", "int", $attachment); + $m->duration = $this->fetchMember("duration_in_seconds", "int", $attachment); + // detect media type from file name if no type is provided + if (!$m->type) { + $ext = ""; + $m->type = ($this->mime ?? ($this->mime = new \Mimey\MimeTypes))->getMimeType($ext); + } + $out[] = $m; + } + } + return $out; + } + + public function getCategories(): CategoryCollection { + $out = new CategoryCollection; + foreach ($this->fetchMember("tags", "array") ?? [] as $tag) { + if (is_string($tag)) { + $tag = $this->trimText($tag); + if (strlen($tag)) { + $c = new Category; + $c->name = $tag; + $out[] = $c; + } + } + } + return $out; + } } diff --git a/lib/Parser/JSON/Feed.php b/lib/Parser/JSON/Feed.php index bef3694..43225d2 100644 --- a/lib/Parser/JSON/Feed.php +++ b/lib/Parser/JSON/Feed.php @@ -18,13 +18,12 @@ use JKingWeb\Lax\Parser\JSON\Entry as EntryParser; class Feed implements \JKingWeb\Lax\Parser\Feed { use Construct; - const MIME_TYPES = [ + protected const MIME_TYPES = [ "application/json", // generic JSON "application/feed+json", // JSON Feed-specific type "text/json", // obsolete type for JSON ]; - - const VERSIONS = [ + protected const VERSIONS = [ 'https://jsonfeed.org/version/1' => "1", 'https://jsonfeed.org/version/1.1' => "1.1", ]; @@ -34,7 +33,7 @@ class Feed implements \JKingWeb\Lax\Parser\Feed { protected $url; /** Constructs a feed parser without actually doing anything */ - public function __construct(string $data, string $contentType = "", string $url = "") { + public function __construct(string $data, string $contentType = null, string $url = null) { $this->data = $data; $this->contentType = $contentType; $this->url = $url; @@ -59,14 +58,16 @@ class Feed implements \JKingWeb\Lax\Parser\Feed { return $feed; } - /** Parses the feed to extract sundry metadata */ + /** Parses the feed to extract data */ public function parse(FeedStruct $feed = null): FeedStruct { $feed = $this->init($feed ?? new FeedStruct); + $feed->meta->url = $this->url; $feed->sched->expired = $this->getExpired(); - $feed->title = $this->getTitle(); $feed->id = $this->getId(); + $feed->lang = $this->getLang(); $feed->url = $this->getUrl(); $feed->link = $this->getLink(); + $feed->title = $this->getTitle(); $feed->summary = $this->getSummary(); $feed->dateModified = $this->getDateModified(); $feed->icon = $this->getIcon(); @@ -77,44 +78,54 @@ class Feed implements \JKingWeb\Lax\Parser\Feed { return $feed; } + /** {@inheritdoc} + * + * For JSON feeds this is always the feed URL specified in the feed + */ + public function getId(): ?string { + return $this->fetchUrl("feed_url"); + } + + public function getLang(): ?string { + return $this->fetchMember("language", "str"); + } + public function getUrl(): ?string { return $this->fetchUrl("feed_url"); } + public function getLink(): ?string { + return $this->fetchUrl("home_page_url"); + } + public function getTitle(): ?Text { return $this->fetchText("title"); } - public function getLink(): ?string { - return $this->fetchUrl("home_page_url"); + /** {@inheritdoc} + * + * JSON feeds themselves don't have dates, so this always returns null + */ + public function getDateModified(): ?Date { + return null; } public function getSummary(): ?Text { return $this->fetchText("description"); } - /** JSON Feed does not have categories at the feed level, so this always returns and empty collection + /** {@inheritdoc} + * + * JSON Feed does not have categories at the feed level, so this always returns and empty collection */ public function getCategories(): CategoryCollection { return new CategoryCollection; } - /** For JSON feeds this is always the feed URL specified in the feed - */ - public function getId(): ?string { - return $this->fetchUrl("feed_url"); - } - public function getPeople(): PersonCollection { return $this->getAuthorsV1() ?? $this->getAuthorV1() ?? new PersonCollection; } - /** JSON feeds themselves don't have dates, so this always returns null - */ - public function getDateModified(): ?Date { - return null; - } - public function getIcon(): ?string { return $this->fetchUrl("favicon"); } diff --git a/lib/Parser/XML/XPath.php b/lib/Parser/XML/XPath.php index 1067ae0..6607c10 100644 --- a/lib/Parser/XML/XPath.php +++ b/lib/Parser/XML/XPath.php @@ -7,7 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Lax\Parser\XML; class XPath extends \DOMXpath { - const NS = [ + public const NS = [ 'atom' => "http://www.w3.org/2005/Atom", // Atom syndication format https://tools.ietf.org/html/rfc4287 'rss1' => "http://purl.org/rss/1.0/", // RDF site summary 1.0 http://purl.org/rss/1.0/spec 'rss0' => "http://channel.netscape.com/rdf/simple/0.9/", // RDF Site Summary 0.90 http://www.rssboard.org/rss-0-9-0 diff --git a/tests/cases/JSON/entry.json b/tests/cases/JSON/entry.json index 51ea638..2435b37 100644 --- a/tests/cases/JSON/entry.json +++ b/tests/cases/JSON/entry.json @@ -171,5 +171,36 @@ } ] } + }, + { + "description": "Entry language", + "input": { + "version": "https://jsonfeed.org/version/1", + "language": "en", + "items": [ + { + "id": 1, + "language": "fr" + }, + { + "id": "2" + } + ] + }, + "output": { + "format": "json", + "version": "1", + "lang": "en", + "entries": [ + { + "id": "1", + "lang": "fr" + }, + { + "id": "2", + "lang": "en" + } + ] + } } ] \ No newline at end of file diff --git a/tests/cases/JSON/feed.json b/tests/cases/JSON/feed.json index 42177d6..5f049c4 100644 --- a/tests/cases/JSON/feed.json +++ b/tests/cases/JSON/feed.json @@ -218,6 +218,7 @@ "description": "Basic example", "input": { "version": "https://jsonfeed.org/version/1", + "language": "en", "title": "Example title", "feed_url": "http://example.com/", "home_page_url": "http://example.net/", @@ -230,6 +231,7 @@ "output": { "format": "json", "version": "1", + "lang": "en", "title": "Example title", "id": "http://example.com/", "url": "http://example.com/",