Browse Source

Various changes

- Preliminary work on enclosures
- Feed and entry language
- Better API docs
- Class constant visibility
master
J. King 4 years ago
parent
commit
635a88540e
  1. 4
      composer.json
  2. 92
      composer.lock
  3. 22
      lib/Enclosure/Collection.php
  4. 19
      lib/Enclosure/Enclosure.php
  5. 27
      lib/Entry.php
  6. 5
      lib/Exception.php
  7. 2
      lib/Feed.php
  8. 1
      lib/Metadata.php
  9. 14
      lib/Parser/Construct.php
  10. 47
      lib/Parser/Entry.php
  11. 3
      lib/Parser/Feed.php
  12. 4
      lib/Parser/JSON/Construct.php
  13. 66
      lib/Parser/JSON/Entry.php
  14. 53
      lib/Parser/JSON/Feed.php
  15. 2
      lib/Parser/XML/XPath.php
  16. 31
      tests/cases/JSON/entry.json
  17. 2
      tests/cases/JSON/feed.json

4
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": {

92
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",

22
lib/Enclosure/Collection.php

@ -0,0 +1,22 @@
<?php
/** @license MIT
* Copyright 2018 J. King et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Lax\Enclosure;
class Collection extends \JKingWeb\Lax\Collection {
/** Returns the primary ("best") enclosure of the collection
*
* Videos are preferred over audios, which are preferred over images, which are preferred over anything else
*
* Videos are first ranked by length, then resolution (total pixels), then bitrate, then size; audios are ranked by length, then bitrate, then size; images are ranked by resolution (total pixels), then size
*
*/
public function primary(): ?Enclosure {
# stub
return null;
}
}

19
lib/Enclosure/Enclosure.php

@ -0,0 +1,19 @@
<?php
/** @license MIT
* Copyright 2018 J. King et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Lax\Enclosure;
class Enclosure {
public $url;
public $type;
public $title;
public $height;
public $width;
public $duration;
public $bitrate;
public $size;
public $preferred;
}

27
lib/Entry.php

@ -8,19 +8,42 @@ namespace JKingWeb\Lax;
use JKingWeb\Lax\Category\Collection as CategoryCollection;
use JKingWeb\Lax\Person\Collection as PersonCollection;
use JKingWeb\Lax\Enclosure\Collection as EnclosureCollection;
class Entry {
/** @var string $id The persistent identifier of the entry. This is often identical to the URL of the entry, but the latter may change
*
* While identifiers are usually supposed to be globally unique, in practice they are frequently only unique within the context of a particular newsfeed
*/
public $id;
/** @var string $lang The human language of the entry */
public $lang;
/** @var string $link The URL of the entry as published somwehere else, usually on the World Wide Web */
public $link;
/** @var string $relatedLink The URL of an article related to the entry. For example, if the entry is a commentary on an essay, this property might provide the URL of that essay */
public $relatedLink;
/** @var \JKingWeb\Lax\Text $title The title of the entry */
public $title;
/** @var \JKingWeb\Lax\Text $content The content of the entry
*
* This may be merely a summary or excerpt for many newsfeeds */
public $content;
/** @var \JKingWeb\Lax\Text $summary A short summary or excerpt of the entry, distinct from the content */
public $summary;
/** @var \JKingWeb\Lax\Date $dateCreated The date and time at which the entry was first made available */
public $dateCreated;
/** @var \JKingWeb\Lax\Date $dateModified The date and time at which the entry was last modified */
public $dateModified;
/** @var \JKingWeb\Lax\Category\Collection $categories The set of categories associated with the entry */
public $categories;
/** @var \JKingWeb\Lax\Person\Collection $people The set of people (authors, contributors, etc) associated with the entry */
public $people;
public $dateModified;
public $dateCreated;
/** @var \JKingWeb\Lax\Enclosures\Collection $enclosures The set of external files (i.e. enclosuresor attachments) associated with the entry */
public $enclosures;
public function __construct() {
$this->people = new PersonCollection;
$this->categories = new CategoryCollection;
$this->enclosures = new EnclosureCollection;
}
}

5
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) {

2
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

1
lib/Metadata.php

@ -8,6 +8,7 @@ namespace JKingWeb\Lax;
class Metadata {
public $url;
public $type;
public $cached = false;
public $lastModified;
public $etag;

14
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);
});
}
}

47
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;
}

3
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;

4
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;

66
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;
}
}

53
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");
}

2
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

31
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"
}
]
}
}
]

2
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/",

Loading…
Cancel
Save