diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 347cf26..c350ee1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,12 +2,33 @@ name: CI
on:
push:
+ branches: [master]
pull_request:
env:
- COVERAGE_PHP_VERSION: "8.3"
+ PSALM_PHP_VERSION: "8.4"
+ COVERAGE_PHP_VERSION: "8.4"
jobs:
+ psalm:
+ name: Psalm
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ env.PSALM_PHP_VERSION }}
+
+ - name: Install composer dependencies
+ uses: ramsey/composer-install@v3
+
+ - name: Run Psalm
+ run: vendor/bin/psalm --no-progress
+
phpunit:
name: PHPUnit
runs-on: ubuntu-latest
@@ -16,17 +37,14 @@ jobs:
fail-fast: false
matrix:
php-version:
- - "7.2"
- - "7.3"
- - "7.4"
- - "8.0"
- "8.1"
- "8.2"
- "8.3"
+ - "8.4"
deps:
- "highest"
include:
- - php-version: "7.2"
+ - php-version: "8.1"
deps: "lowest"
steps:
diff --git a/README.md b/README.md
index 6babfb0..8dc026e 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ composer require brick/structured-data
### Requirements
-This library requires PHP 7.2 or later. It makes use of the following extensions:
+This library requires PHP 8.1 or later. It makes use of the following extensions:
- [dom](https://www.php.net/manual/en/book.dom.php)
- [json](https://www.php.net/manual/en/book.json.php)
@@ -39,7 +39,7 @@ optimizing existing code, etc.), `y` is incremented.
**When a breaking change is introduced, a new `0.x` version cycle is always started.**
-It is therefore safe to lock your project to a given release cycle, such as `0.1.*`.
+It is therefore safe to lock your project to a given release cycle, such as `0.2.*`.
If you need to upgrade to a newer release cycle, check the [release history](https://github.com/brick/structured-data/releases)
for a list of changes introduced by each further `0.x.0` version.
diff --git a/composer.json b/composer.json
index 6866213..0827ad7 100644
--- a/composer.json
+++ b/composer.json
@@ -11,15 +11,16 @@
],
"license": "MIT",
"require": {
- "php": "^7.2 || ^8.0",
+ "php": "^8.1",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
- "sabre/uri": "^2.1"
+ "sabre/uri": "^2.1 || ^3.0"
},
"require-dev": {
"phpunit/phpunit": "^8.0 || ^9.0",
- "php-coveralls/php-coveralls": "^2.0"
+ "php-coveralls/php-coveralls": "^2.0",
+ "vimeo/psalm": "6.12.0"
},
"autoload": {
"psr-4": {
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
new file mode 100644
index 0000000..16e2cec
--- /dev/null
+++ b/psalm-baseline.xml
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $this->readJson($node->textContent, $url)]]>
+
+
+
+
+
+
+
+
+
+
+ parentNode;
+
+ if ($itemprop->isSameNode($node)) {
+ return true;
+ }
+
+ if ($itemprop->attributes->getNamedItem('itemscope')) {
+ return false;
+ }
+ }
+ }]]>
+
+
+ $this->nodeToItem($node, $xpath, $url)]]>
+ parentNode;
+
+ if ($itemprop->isSameNode($node)) {
+ return true;
+ }
+
+ if ($itemprop->attributes->getNamedItem('itemscope')) {
+ return false;
+ }
+ }
+ }]]>
+
+
+
+ textContent)]]>
+
+
+ attributes->getNamedItem('itemprop')->textContent]]>
+
+
+
+
+
+
+
+
+
+
+
+ parentNode;
+
+ if ($itemprop->isSameNode($node)) {
+ return true;
+ }
+
+ if ($itemprop->attributes->getNamedItem('typeof')) {
+ return false;
+ }
+ }
+
+ // Unreachable, but makes static analysis happy
+ return false;
+ }]]>
+
+
+ $this->nodeToItem($node, $xpath, $url, self::PREDEFINED_PREFIXES, null)]]>
+ parentNode;
+
+ if ($itemprop->isSameNode($node)) {
+ return true;
+ }
+
+ if ($itemprop->attributes->getNamedItem('typeof')) {
+ return false;
+ }
+ }
+
+ // Unreachable, but makes static analysis happy
+ return false;
+ }]]>
+
+
+
+ textContent]]>
+ textContent)]]>
+
+
+ attributes->getNamedItem('property')->textContent]]>
+ textContent]]>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..b99bd54
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DOMBuilder.php b/src/DOMBuilder.php
index f1fbddb..78bc882 100644
--- a/src/DOMBuilder.php
+++ b/src/DOMBuilder.php
@@ -6,7 +6,7 @@
use DOMDocument;
-class DOMBuilder
+final class DOMBuilder
{
/**
* Builds a DOMDocument from an HTML string.
diff --git a/src/HTMLReader.php b/src/HTMLReader.php
index 529d4a2..d28c95b 100644
--- a/src/HTMLReader.php
+++ b/src/HTMLReader.php
@@ -4,12 +4,9 @@
namespace Brick\StructuredData;
-class HTMLReader
+final class HTMLReader
{
- /**
- * @var Reader
- */
- private $reader;
+ private readonly Reader $reader;
/**
* HTMLReader constructor.
diff --git a/src/Item.php b/src/Item.php
index 0255fcd..dea1a36 100644
--- a/src/Item.php
+++ b/src/Item.php
@@ -4,33 +4,29 @@
namespace Brick\StructuredData;
-use TypeError;
-
/**
* An item, such as a Thing in schema.org's vocabulary.
*/
-class Item
+final class Item
{
/**
* The global identifier of the item, if any.
- *
- * @var string|null
*/
- private $id;
+ private readonly ?string $id;
/**
* The types this Item implements, as URLs.
*
- * @var array
+ * @var string[]
*/
- private $types;
+ private readonly array $types;
/**
* The properties, as a map of property name to list of values.
*
* @var array>
*/
- private $properties = [];
+ private array $properties = [];
/**
* Item constructor.
@@ -57,9 +53,9 @@ public function getId() : ?string
/**
* Returns the list of types this Item implements.
*
- * Each type is a represented as a URL, e.g. http://schema.org/Product .
+ * Each type is represented as a URL, e.g. http://schema.org/Product .
*
- * @return array
+ * @return string[]
*/
public function getTypes() : array
{
@@ -100,12 +96,8 @@ public function getProperty(string $name) : array
*
* @return void
*/
- public function addProperty(string $name, $value) : void
+ public function addProperty(string $name, Item|string $value) : void
{
- if (! $value instanceof Item && ! is_string($value)) {
- throw new TypeError(sprintf('Property value must be an instance of %s or a string.', Item::class));
- }
-
$this->properties[$name][] = $value;
}
}
diff --git a/src/JsonLdWriter.php b/src/JsonLdWriter.php
index 2cdfe87..50b6389 100644
--- a/src/JsonLdWriter.php
+++ b/src/JsonLdWriter.php
@@ -7,7 +7,7 @@
/**
* Exports Items to JSON-LD.
*/
-class JsonLdWriter
+final class JsonLdWriter
{
/**
* Exports a list of Items as JSON-LD.
@@ -18,11 +18,12 @@ class JsonLdWriter
*/
public function write(Item ...$items) : string
{
- $items = array_map(function(Item $item) {
- return $this->convertItem($item);
- }, $items);
+ $items = array_map(
+ fn(Item $item) => $this->convertItem($item),
+ $items,
+ );
- return json_encode($this->extractIfSingle($items), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+ return json_encode($this->extractIfSingle($items), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
}
/**
@@ -62,7 +63,7 @@ private function convertItem(Item $item) : array
*
* @return mixed
*/
- private function extractIfSingle(array $values)
+ private function extractIfSingle(array $values) : mixed
{
if (count($values) === 1) {
return $values[0];
diff --git a/src/Reader/JsonLdReader.php b/src/Reader/JsonLdReader.php
index e0e42ef..f949126 100644
--- a/src/Reader/JsonLdReader.php
+++ b/src/Reader/JsonLdReader.php
@@ -7,6 +7,7 @@
use Brick\StructuredData\Item;
use Brick\StructuredData\Reader;
+use Override;
use stdClass;
use DOMDocument;
@@ -29,12 +30,12 @@
*
* https://json-ld.org/spec/latest/json-ld/
*/
-class JsonLdReader implements Reader
+final class JsonLdReader implements Reader
{
/**
* @var string[]
*/
- private $iriProperties;
+ private readonly array $iriProperties;
/**
* JsonLdReader constructor.
@@ -51,9 +52,7 @@ public function __construct(array $iriProperties = [])
$this->iriProperties = $iriProperties;
}
- /**
- * @inheritDoc
- */
+ #[Override]
public function read(DOMDocument $document, string $url) : array
{
$xpath = new DOMXPath($document);
@@ -65,9 +64,10 @@ public function read(DOMDocument $document, string $url) : array
return [];
}
- $items = array_map(function(DOMNode $node) use ($url) {
- return $this->readJson($node->textContent, $url);
- }, $nodes);
+ $items = array_map(
+ fn(DOMNode $node) => $this->readJson($node->textContent, $url),
+ $nodes,
+ );
return array_merge(...$items);
}
@@ -84,7 +84,7 @@ public function read(DOMDocument $document, string $url) : array
*/
private function readJson(string $json, string $url) : array
{
- $data = json_decode($json);
+ $data = json_decode($json, flags: JSON_THROW_ON_ERROR);
if ($data === null) {
return [];
@@ -101,9 +101,10 @@ private function readJson(string $json, string $url) : array
}
if (is_array($data)) {
- $items = array_map(function($item) use ($url) {
- return is_object($item) ? $this->readItem($item, $url, null) : null;
- }, $data);
+ $items = array_map(
+ fn($item) => is_object($item) ? $this->readItem($item, $url, null) : null,
+ $data,
+ );
$items = array_filter($items);
$items = array_values($items);
@@ -134,7 +135,7 @@ private function readItem(stdClass $item, string $url, ?string $vocabulary) : It
if (isset($item->{'@id'}) && is_string($item->{'@id'})) {
try {
$id = resolve($url, $item->{'@id'}); // always relative to the document URL, no support for @base
- } catch (InvalidUriException $e) {
+ } catch (InvalidUriException) {
// ignore
}
}
@@ -148,9 +149,10 @@ private function readItem(stdClass $item, string $url, ?string $vocabulary) : It
$type = $this->resolveTerm($type, $vocabulary);
$types = [$type];
} elseif (is_array($type)) {
- $types = array_map(function($type) use ($vocabulary) {
- return is_string($type) ? $this->resolveTerm($type, $vocabulary) : null;
- }, $types);
+ $types = array_map(
+ fn($type) => is_string($type) ? $this->resolveTerm($type, $vocabulary) : null,
+ $types,
+ );
$types = array_filter($types);
$types = array_values($types);
@@ -230,17 +232,17 @@ private function resolveTerm(string $term, ?string $vocabulary)
* @param string $name The property name.
* @param mixed $value The property value. Any JSON type.
* @param string $url The URL the document was retrieved from, for relative URL resolution.
- * @param string|null $vocabulary The currently vocabulary URL, if any.
+ * @param string|null $vocabulary The current vocabulary URL, if any.
*
* @return Item|string|null The value, or NULL if the input value is NULL or an array.
*/
- private function getPropertyValue(string $name, $value, string $url, ?string $vocabulary)
+ private function getPropertyValue(string $name, mixed $value, string $url, ?string $vocabulary) : Item|string|null
{
if (is_string($value)) {
if (in_array($name, $this->iriProperties, true)) {
try {
$value = resolve($url, $value);
- } catch (InvalidUriException $e) {
+ } catch (InvalidUriException) {
// ignore
}
}
@@ -274,7 +276,7 @@ private function checkVocabularyUrl(string $url) : ?string
{
try {
$parts = parse($url);
- } catch (InvalidUriException $e) {
+ } catch (InvalidUriException) {
return null;
}
diff --git a/src/Reader/MicrodataReader.php b/src/Reader/MicrodataReader.php
index 1f1e692..6c7c68e 100644
--- a/src/Reader/MicrodataReader.php
+++ b/src/Reader/MicrodataReader.php
@@ -11,6 +11,7 @@
use DOMNode;
use DOMXPath;
+use Override;
use Sabre\Uri\InvalidUriException;
use function Sabre\Uri\resolve;
@@ -21,11 +22,9 @@
*
* @todo support for the itemref attribute
*/
-class MicrodataReader implements Reader
+final class MicrodataReader implements Reader
{
- /**
- * @inheritDoc
- */
+ #[Override]
public function read(DOMDocument $document, string $url) : array
{
$xpath = new DOMXPath($document);
@@ -38,9 +37,10 @@ public function read(DOMDocument $document, string $url) : array
$nodes = $xpath->query('//*[@itemscope and not(@itemprop)]');
$nodes = iterator_to_array($nodes);
- return array_map(function(DOMNode $node) use ($xpath, $url) {
- return $this->nodeToItem($node, $xpath, $url);
- }, $nodes);
+ return array_map(
+ fn(DOMNode $node) => $this->nodeToItem($node, $xpath, $url),
+ $nodes,
+ );
}
/**
@@ -84,9 +84,7 @@ private function nodeToItem(DOMNode $node, DOMXPath $xpath, string $url) : Item
* If the itemtype attribute is missing or parsing it in this way finds no tokens, the item is said to have
* no item types.
*/
- $types = array_values(array_filter($types, function(string $type) {
- return $type !== '';
- }));
+ $types = array_values(array_filter($types, fn(string $type) => $type !== ''));
} else {
$types = [];
}
@@ -111,9 +109,6 @@ private function nodeToItem(DOMNode $node, DOMXPath $xpath, string $url) : Item
return false;
}
}
-
- // Unreachable, but makes static analysis happy
- return false;
});
$vocabularyIdentifier = $this->getVocabularyIdentifier($types);
@@ -139,7 +134,7 @@ private function nodeToItem(DOMNode $node, DOMXPath $xpath, string $url) : Item
* We therefore consider anything containing these characters as an absolute URL, and only prepend the
* vocabulary identifier if none of these characters are found.
*/
- if (strpos($name, '.') === false && strpos($name, ':') === false) {
+ if (!str_contains($name, '.') && !str_contains($name, ':')) {
$name = $vocabularyIdentifier . $name;
}
@@ -158,10 +153,8 @@ private function nodeToItem(DOMNode $node, DOMXPath $xpath, string $url) : Item
* @param DOMNode $node A DOMNode representing an element with the itemprop attribute.
* @param DOMXPath $xpath A DOMXPath object created from the node's document element.
* @param string $url The URL the document was retrieved from, for relative URL resolution.
- *
- * @return Item|string
*/
- private function getPropertyValue(DOMNode $node, DOMXPath $xpath, string $url)
+ private function getPropertyValue(DOMNode $node, DOMXPath $xpath, string $url) : Item|string
{
/**
* If the element also has an itemscope attribute: the value is the item created by the element.
@@ -194,7 +187,7 @@ private function getPropertyValue(DOMNode $node, DOMXPath $xpath, string $url)
if ($attr !== null) {
try {
return resolve($url, $attr->textContent);
- } catch (InvalidUriException $e) {
+ } catch (InvalidUriException) {
return '';
}
}
@@ -213,7 +206,7 @@ private function getPropertyValue(DOMNode $node, DOMXPath $xpath, string $url)
if ($attr !== null) {
try {
return resolve($url, $attr->textContent);
- } catch (InvalidUriException $e) {
+ } catch (InvalidUriException) {
return '';
}
}
@@ -230,7 +223,7 @@ private function getPropertyValue(DOMNode $node, DOMXPath $xpath, string $url)
if ($attr !== null) {
try {
return resolve($url, $attr->textContent);
- } catch (InvalidUriException $e) {
+ } catch (InvalidUriException) {
return '';
}
}
diff --git a/src/Reader/RdfaLiteReader.php b/src/Reader/RdfaLiteReader.php
index ffde4ac..0e2ed23 100644
--- a/src/Reader/RdfaLiteReader.php
+++ b/src/Reader/RdfaLiteReader.php
@@ -11,6 +11,7 @@
use DOMNode;
use DOMXPath;
+use Override;
use Sabre\Uri\InvalidUriException;
use function Sabre\Uri\resolve;
use function Sabre\Uri\parse;
@@ -23,7 +24,7 @@
*
* @todo support for the prefix attribute; only predefined prefixes are supported right now
*/
-class RdfaLiteReader implements Reader
+final class RdfaLiteReader implements Reader
{
/**
* The predefined RDFa prefixes.
@@ -84,22 +85,21 @@ class RdfaLiteReader implements Reader
'xsd' => 'http://www.w3.org/2001/XMLSchema#',
];
- /**
- * @inheritDoc
- */
+ #[Override]
public function read(DOMDocument $document, string $url) : array
{
$xpath = new DOMXPath($document);
/**
- * Top-level item have a typeof attribute and no property attribute.
+ * Top-level item has a typeof attribute and no property attribute.
*/
$nodes = $xpath->query('//*[@typeof and not(@property)]');
$nodes = iterator_to_array($nodes);
- return array_map(function(DOMNode $node) use ($xpath, $url) {
- return $this->nodeToItem($node, $xpath, $url, self::PREDEFINED_PREFIXES, null);
- }, $nodes);
+ return array_map(
+ fn(DOMNode $node) => $this->nodeToItem($node, $xpath, $url, self::PREDEFINED_PREFIXES, null),
+ $nodes,
+ );
}
/**
@@ -119,7 +119,7 @@ private function nodeToItem(DOMNode $node, DOMXPath $xpath, string $url, array $
$vocabulary = $this->updateVocabulary($node, $vocabulary);
/**
- * The resource attribute holds the item identifier, than must be resolved relative to the current URL.
+ * The resource attribute holds the item identifier, that must be resolved relative to the current URL.
*
* https://www.w3.org/TR/rdfa-lite/#resource
*/
@@ -150,9 +150,7 @@ private function nodeToItem(DOMNode $node, DOMXPath $xpath, string $url, array $
}, $types);
// Remove empty values
- $types = array_values(array_filter($types, function(string $type) {
- return $type !== '';
- }));
+ $types = array_values(array_filter($types, fn(string $type) => $type !== ''));
$item = new Item($id, ...$types);
@@ -245,7 +243,7 @@ private function isValidAbsoluteURL(string $url) : bool
{
try {
$parts = parse($url);
- } catch (InvalidUriException $e) {
+ } catch (InvalidUriException) {
return false;
}
@@ -292,7 +290,7 @@ private function checkVocabularyUrl(string $url) : ?string
{
try {
$parts = parse($url);
- } catch (InvalidUriException $e) {
+ } catch (InvalidUriException) {
return null;
}
@@ -319,10 +317,8 @@ private function checkVocabularyUrl(string $url) : ?string
* @param string $url The URL the document was retrieved from, for relative URL resolution.
* @param string[] $prefixes The prefixes in use, as a map of prefix to vocabulary URL.
* @param string|null $vocabulary The URL of the vocabulary in use, if any.
- *
- * @return Item|string
*/
- private function getPropertyValue(DOMNode $node, DOMXPath $xpath, string $url, array $prefixes, ?string $vocabulary)
+ private function getPropertyValue(DOMNode $node, DOMXPath $xpath, string $url, array $prefixes, ?string $vocabulary) : Item|string
{
// If the element also has an typeof attribute, create an item from the element
$attr = $node->attributes->getNamedItem('typeof');
@@ -344,7 +340,7 @@ private function getPropertyValue(DOMNode $node, DOMXPath $xpath, string $url, a
if ($attr !== null) {
try {
return resolve($url, $attr->textContent);
- } catch (InvalidUriException $e) {
+ } catch (InvalidUriException) {
return '';
}
}
@@ -355,7 +351,7 @@ private function getPropertyValue(DOMNode $node, DOMXPath $xpath, string $url, a
if ($attr !== null) {
try {
return resolve($url, $attr->textContent);
- } catch (InvalidUriException $e) {
+ } catch (InvalidUriException) {
return '';
}
}
diff --git a/src/Reader/ReaderChain.php b/src/Reader/ReaderChain.php
index 08c7e1e..0d684bb 100644
--- a/src/Reader/ReaderChain.php
+++ b/src/Reader/ReaderChain.php
@@ -7,16 +7,17 @@
use Brick\StructuredData\Reader;
use DOMDocument;
+use Override;
/**
* Chains several schema readers and returns the aggregate results.
*/
-class ReaderChain implements Reader
+final class ReaderChain implements Reader
{
/**
* @var Reader[]
*/
- private $readers;
+ private readonly array $readers;
/**
* ReaderChain constructor.
@@ -28,9 +29,7 @@ public function __construct(Reader ...$readers)
$this->readers = $readers;
}
- /**
- * @inheritDoc
- */
+ #[Override]
public function read(DOMDocument $document, string $url) : array
{
if (! $this->readers) {