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