From 9c5d3e91c1359a9ade3f2d0e6818c1ce4a3f773c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Chris=20M=C3=BCller?=
<2566282+brotkrueml@users.noreply.github.com>
Date: Mon, 7 Jun 2021 15:17:51 +0200
Subject: [PATCH] [FEATURE] Add node identifier view helpers
Resolves: #67
---
CHANGELOG.md | 2 +-
Classes/Core/Model/BlankNodeIdentifier.php | 7 +-
Classes/Core/Model/NodeIdentifier.php | 7 +-
.../ViewHelpers/AbstractTypeViewHelper.php | 32 +++++-
.../BlankNodeIdentifierViewHelper.php | 42 +++++++
.../ViewHelpers/NodeIdentifierViewHelper.php | 48 ++++++++
Documentation/Developer/ViewHelpers.rst | 103 ++++++++++++++++++
Tests/Fixtures/Model/Type/Person.php | 27 +++++
.../Core/Model/BlankNodeIdentifierTest.php | 15 +++
Tests/Unit/Core/Model/NodeIdentifierTest.php | 15 +++
.../BlankNodeIdentifierViewHelperTest.php | 85 +++++++++++++++
.../NodeIdentifierViewHelperTest.php | 75 +++++++++++++
12 files changed, 450 insertions(+), 8 deletions(-)
create mode 100644 Classes/ViewHelpers/BlankNodeIdentifierViewHelper.php
create mode 100644 Classes/ViewHelpers/NodeIdentifierViewHelper.php
create mode 100644 Tests/Fixtures/Model/Type/Person.php
create mode 100644 Tests/Unit/ViewHelpers/BlankNodeIdentifierViewHelperTest.php
create mode 100644 Tests/Unit/ViewHelpers/NodeIdentifierViewHelperTest.php
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5f468b6..485106d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
-- Node identifier and blank node identifier (#65)
+- Node identifier and blank node identifier (#65, #67)
- Multiple types for a node (#64)
### Changed
diff --git a/Classes/Core/Model/BlankNodeIdentifier.php b/Classes/Core/Model/BlankNodeIdentifier.php
index 3e846008..6dfdb14e 100644
--- a/Classes/Core/Model/BlankNodeIdentifier.php
+++ b/Classes/Core/Model/BlankNodeIdentifier.php
@@ -14,7 +14,7 @@
/**
* @psalm-immutable
*/
-class BlankNodeIdentifier implements NodeIdentifierInterface
+class BlankNodeIdentifier implements NodeIdentifierInterface, \Stringable
{
/**
* The ID of the type (mapped to @id in result)
@@ -37,4 +37,9 @@ public function getId(): string
{
return $this->id;
}
+
+ public function __toString(): string
+ {
+ return $this->getId();
+ }
}
diff --git a/Classes/Core/Model/NodeIdentifier.php b/Classes/Core/Model/NodeIdentifier.php
index 5ab006eb..5fb47332 100644
--- a/Classes/Core/Model/NodeIdentifier.php
+++ b/Classes/Core/Model/NodeIdentifier.php
@@ -14,7 +14,7 @@
/**
* @psalm-immutable
*/
-class NodeIdentifier implements NodeIdentifierInterface
+class NodeIdentifier implements NodeIdentifierInterface, \Stringable
{
/**
* The ID of the type (mapped to @id in result)
@@ -33,4 +33,9 @@ public function getId(): string
{
return $this->id;
}
+
+ public function __toString(): string
+ {
+ return $this->getId();
+ }
}
diff --git a/Classes/Core/ViewHelpers/AbstractTypeViewHelper.php b/Classes/Core/ViewHelpers/AbstractTypeViewHelper.php
index 9b74bb72..1b32fefc 100644
--- a/Classes/Core/ViewHelpers/AbstractTypeViewHelper.php
+++ b/Classes/Core/ViewHelpers/AbstractTypeViewHelper.php
@@ -11,6 +11,7 @@
namespace Brotkrueml\Schema\Core\ViewHelpers;
+use Brotkrueml\Schema\Core\Model\NodeIdentifierInterface;
use Brotkrueml\Schema\Core\Model\TypeInterface;
use Brotkrueml\Schema\Core\TypeStack;
use Brotkrueml\Schema\Manager\SchemaManager;
@@ -45,7 +46,7 @@ public function initializeArguments()
parent::initializeArguments();
$this->registerArgument(static::ARGUMENT_AS, 'string', 'Property name for a child node to merge under the parent node', false, '');
- $this->registerArgument(static::ARGUMENT_ID, 'string', 'IRI to identify the node', false, '');
+ $this->registerArgument(static::ARGUMENT_ID, 'mixed', 'IRI or a node identifier to identify the node', false, '');
$this->registerArgument(static::ARGUMENT_IS_MAIN_ENTITY_OF_WEBPAGE, 'bool', 'Set to true, if the type is the primary content of the web page', false, false);
$this->registerArgument(static::ARGUMENT_SPECIFIC_TYPE, 'string', 'A specific type of the chosen type. Only the properties of the chosen type are valid', false, '');
@@ -154,10 +155,7 @@ private function assignArgumentsToItem(): void
return;
}
- if ($this->arguments[static::ARGUMENT_ID] !== '') {
- $this->model->setId($this->arguments[static::ARGUMENT_ID]);
- }
-
+ $this->assignIdToModel();
unset($this->arguments[static::ARGUMENT_ID]);
foreach ($this->arguments as $name => $value) {
@@ -178,4 +176,28 @@ private function assignArgumentsToItem(): void
$this->model->setProperty($name, $value);
}
}
+
+ public function assignIdToModel(): void
+ {
+ $id = $this->arguments[static::ARGUMENT_ID];
+ if ($id === '') {
+ return;
+ }
+
+ if (!\is_string($id) && !$id instanceof NodeIdentifierInterface) {
+ throw new ViewHelper\Exception(
+ \sprintf(
+ 'The %s argument has to be either a string or an instance of %s, %s given',
+ static::ARGUMENT_ID,
+ NodeIdentifierInterface::class,
+ \get_debug_type($id)
+ )
+ );
+ }
+
+ /**
+ * @psalm-suppress PossiblyNullReference
+ */
+ $this->model->setId($this->arguments[static::ARGUMENT_ID]);
+ }
}
diff --git a/Classes/ViewHelpers/BlankNodeIdentifierViewHelper.php b/Classes/ViewHelpers/BlankNodeIdentifierViewHelper.php
new file mode 100644
index 00000000..6f256d54
--- /dev/null
+++ b/Classes/ViewHelpers/BlankNodeIdentifierViewHelper.php
@@ -0,0 +1,42 @@
+
+ *
+ *
+ *
+ *
+ *
+ */
+final class BlankNodeIdentifierViewHelper extends AbstractViewHelper
+{
+ public static function renderStatic(
+ array $arguments,
+ \Closure $renderChildrenClosure,
+ RenderingContextInterface $renderingContext
+ ): BlankNodeIdentifier {
+ return new BlankNodeIdentifier();
+ }
+}
diff --git a/Classes/ViewHelpers/NodeIdentifierViewHelper.php b/Classes/ViewHelpers/NodeIdentifierViewHelper.php
new file mode 100644
index 00000000..dbfebeac
--- /dev/null
+++ b/Classes/ViewHelpers/NodeIdentifierViewHelper.php
@@ -0,0 +1,48 @@
+
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+final class NodeIdentifierViewHelper extends AbstractViewHelper
+{
+ public function initializeArguments()
+ {
+ parent::initializeArguments();
+
+ $this->registerArgument('id', 'string', 'The identifier for the node', true);
+ }
+
+ public static function renderStatic(
+ array $arguments,
+ \Closure $renderChildrenClosure,
+ RenderingContextInterface $renderingContext
+ ): NodeIdentifier {
+ return new NodeIdentifier($arguments['id']);
+ }
+}
diff --git a/Documentation/Developer/ViewHelpers.rst b/Documentation/Developer/ViewHelpers.rst
index bfbc534f..893354d2 100644
--- a/Documentation/Developer/ViewHelpers.rst
+++ b/Documentation/Developer/ViewHelpers.rst
@@ -232,6 +232,109 @@ which results in the output:
}
+.. index::
+ single: Node identifier view helper
+
+.. _schema-nodeIdentifier-view-helper:
+
+:html:`` view helper
+===========================================
+
+Sometimes it is useful to reference a node with just the ID. For this case the
+:html:`` view helper is available:
+
+.. code-block:: html
+
+
+
+
+
+
+This generates the following JSON-LD:
+
+.. code-block:: json
+ :emphasize-lines: 6,9,14,17
+
+ {
+ "@context": "https://schema.org/",
+ "@graph": [
+ {
+ "@type": "Person",
+ "@id": "https://example.org/#john-smith",
+ "name": "John Smith",
+ "knows": {
+ "@id": "https://example.org/#sarah-jane-smith"
+ }
+ },
+ {
+ "@type": "Person",
+ "@id": "https://example.org/#sarah-jane-smith",
+ "name": "Sarah Jane Smith",
+ "knows": {
+ "@id": "https://example.org/#john-smith"
+ }
+ }
+ ]
+ }
+
+The view helper has only one attribute which is required:
+
+.. option:: id
+
+This attribute defines the id and is mapped in JSON-LD to the ``@id`` property.
+
+
+.. index::
+ single: Blank node identifier view helper
+
+.. _schema-blankNodeIdentifier-view-helper:
+
+:html:`` view helper
+================================================
+
+Sometimes it is not necessary (or possible) to define a globally unique ID
+with an IRI. For these cases you can use a blank node identifier:
+
+.. code-block:: html
+
+
+
+
+
+
+This generates the following JSON-LD:
+
+.. code-block:: json
+ :emphasize-lines: 6,9,14,17
+
+ {
+ "@context": "https://schema.org/",
+ "@graph": [
+ {
+ "@type": "Person",
+ "@id": "_:b0",
+ "name": "John Smith",
+ "knows": {
+ "@id": "_:b1"
+ }
+ },
+ {
+ "@type": "Person",
+ "@id": "_:b1",
+ "name": "Sarah Jane Smith",
+ "knows": {
+ "@id": "_:b0"
+ }
+ }
+ ]
+ }
+
+The view helper has no arguments.
+
+You can find more information in the :ref:`Blank node identifier API section
+`.
+
+
.. index:: property view helper
:html:`` view helper
diff --git a/Tests/Fixtures/Model/Type/Person.php b/Tests/Fixtures/Model/Type/Person.php
new file mode 100644
index 00000000..41249e90
--- /dev/null
+++ b/Tests/Fixtures/Model/Type/Person.php
@@ -0,0 +1,27 @@
+__toString());
+ }
}
diff --git a/Tests/Unit/Core/Model/NodeIdentifierTest.php b/Tests/Unit/Core/Model/NodeIdentifierTest.php
index 2dba99df..37ab0904 100644
--- a/Tests/Unit/Core/Model/NodeIdentifierTest.php
+++ b/Tests/Unit/Core/Model/NodeIdentifierTest.php
@@ -25,6 +25,14 @@ public function subjectIsInstanceOfNodeIdentifierInterface(): void
self::assertInstanceOf(NodeIdentifierInterface::class, new NodeIdentifier('some-id'));
}
+ /**
+ * @test
+ */
+ public function subjectIsInstanceOfStringableInterface(): void
+ {
+ self::assertInstanceOf(\Stringable::class, new NodeIdentifier('some-id'));
+ }
+
/**
* @test
*/
@@ -34,4 +42,11 @@ public function getIdReturnsCorrectId(): void
self::assertSame('some-id', $subject->getId());
}
+
+ public function toStringReturnsBlankIdentifier(): void
+ {
+ $subject = new NodeIdentifier('some-id');
+
+ self::assertSame('some-id', $subject->__toString());
+ }
}
diff --git a/Tests/Unit/ViewHelpers/BlankNodeIdentifierViewHelperTest.php b/Tests/Unit/ViewHelpers/BlankNodeIdentifierViewHelperTest.php
new file mode 100644
index 00000000..809c5294
--- /dev/null
+++ b/Tests/Unit/ViewHelpers/BlankNodeIdentifierViewHelperTest.php
@@ -0,0 +1,85 @@
+defineCacheStubsWhichReturnEmptyEntry();
+
+ $typeRegistryStub = $this->createStub(TypeRegistry::class);
+ $map = [
+ ['Person', FixtureType\Person::class],
+ ];
+ $typeRegistryStub
+ ->method('resolveModelClassFromType')
+ ->willReturnMap($map);
+
+ GeneralUtility::setSingletonInstance(TypeRegistry::class, $typeRegistryStub);
+ }
+
+ protected function tearDown(): void
+ {
+ GeneralUtility::purgeInstances();
+ parent::tearDown();
+ }
+
+ /**
+ * @test
+ */
+ public function viewHelperUsedOncePrintsBlankNodeIdentifierCorrectly(): void
+ {
+ $actual = $this->renderTemplate('', []);
+
+ self::assertSame('_:b0', $actual);
+ }
+
+ /**
+ * @test
+ */
+ public function viewHelperUsedTwicePrintsBlankNodeIdentifiersCorrectly(): void
+ {
+ $actual = $this->renderTemplate(' ', []);
+
+ self::assertSame('_:b1 _:b2', $actual);
+ }
+
+ /**
+ * @test
+ */
+ public function usingVariablesAndThenAssignedToTypePropertiesBuildsSchemaCorrectly(): void
+ {
+ $template = <<
+
+
+
+EOF
+;
+
+ $this->renderTemplate($template, []);
+ $actual = $this->schemaManager->renderJsonLd();
+
+ $expected = '{"@context":"https://schema.org/","@graph":[{"@type":"Person","@id":"_:b3","name":"John Smith","knows":{"@id":"_:b4"}},{"@type":"Person","@id":"_:b4","name":"Sarah Jane Smith","knows":{"@id":"_:b3"}}]}';
+ self::assertSame(\sprintf(Extension::JSONLD_TEMPLATE, $expected), $actual);
+ }
+}
diff --git a/Tests/Unit/ViewHelpers/NodeIdentifierViewHelperTest.php b/Tests/Unit/ViewHelpers/NodeIdentifierViewHelperTest.php
new file mode 100644
index 00000000..ed394720
--- /dev/null
+++ b/Tests/Unit/ViewHelpers/NodeIdentifierViewHelperTest.php
@@ -0,0 +1,75 @@
+defineCacheStubsWhichReturnEmptyEntry();
+
+ $typeRegistryStub = $this->createStub(TypeRegistry::class);
+ $map = [
+ ['Person', FixtureType\Person::class],
+ ];
+ $typeRegistryStub
+ ->method('resolveModelClassFromType')
+ ->willReturnMap($map);
+
+ GeneralUtility::setSingletonInstance(TypeRegistry::class, $typeRegistryStub);
+ }
+
+ protected function tearDown(): void
+ {
+ GeneralUtility::purgeInstances();
+ parent::tearDown();
+ }
+
+ /**
+ * @test
+ */
+ public function viewHelperPrintsNodeIdentifiersCorrectly(): void
+ {
+ $actual = $this->renderTemplate('', []);
+
+ self::assertSame('some-id', $actual);
+ }
+
+ /**
+ * @test
+ */
+ public function usingVariablesAndThenAssignedToTypePropertiesBuildsSchemaCorrectly(): void
+ {
+ $template = <<
+
+
+
+EOF
+;
+
+ $this->renderTemplate($template, []);
+ $actual = $this->schemaManager->renderJsonLd();
+
+ $expected = '{"@context":"https://schema.org/","@graph":[{"@type":"Person","@id":"https://example.org/#john-smith","name":"John Smith","knows":{"@id":"https://example.org/#sarah-jane-smith"}},{"@type":"Person","@id":"https://example.org/#sarah-jane-smith","name":"Sarah Jane Smith","knows":{"@id":"https://example.org/#john-smith"}}]}';
+ self::assertSame(\sprintf(Extension::JSONLD_TEMPLATE, $expected), $actual);
+ }
+}