diff --git a/.github/workflows/build-2.x.yml b/.github/workflows/build-2.x.yml index 609d68a..bafa0e5 100644 --- a/.github/workflows/build-2.x.yml +++ b/.github/workflows/build-2.x.yml @@ -21,8 +21,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ["7.3", "7.4"] - drupal-version: ["8.9.11", "9.1.5"] + php-versions: ["7.4", "8.0", "8.1"] + drupal-version: ["9.3.x", "9.4.x-dev"] services: mysql: diff --git a/composer.json b/composer.json index 1efd6f4..e5f4f7e 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,9 @@ "role": "Maintainer" } ], + "require" : { + "php": ">=7.4" + }, "require-dev": { "phpunit/phpunit": "^8", "squizlabs/php_codesniffer": "^3", diff --git a/src/Normalizer/ContentEntityNormalizer.php b/src/Normalizer/ContentEntityNormalizer.php index 79c55b9..bd8ce60 100644 --- a/src/Normalizer/ContentEntityNormalizer.php +++ b/src/Normalizer/ContentEntityNormalizer.php @@ -2,6 +2,7 @@ namespace Drupal\jsonld\Normalizer; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\hal\LinkManager\LinkManagerInterface; @@ -163,7 +164,14 @@ public function normalize($entity, $format = NULL, array $context = []) { // but the interface (typehint) does not. // We could check if serializer implements normalizer interface // to avoid any possible errors in case someone swaps serializer. - $normalized = array_merge_recursive($normalized, $normalized_property); + $normalized = NestedArray::mergeDeepArray( + [ + $normalized, + $normalized_property, + ] + ); + // Deduplicate the @type elements and arrays of entity references. + $normalized = self::deduplicateTypesAndReferences($normalized); } } // Clean up @graph if this is the top-level entity diff --git a/src/Normalizer/NormalizerBase.php b/src/Normalizer/NormalizerBase.php index 9836880..7de3f48 100644 --- a/src/Normalizer/NormalizerBase.php +++ b/src/Normalizer/NormalizerBase.php @@ -64,4 +64,54 @@ public static function escapePrefix($predicate, array $namespaces) { return $namespaces[$exploded[0]] . $exploded[1]; } + /** + * Deduplicate lists of @types and predicate to entity references. + * + * @param array $array + * The array to deduplicate. + * + * @return array + * The deduplicated array. + */ + protected static function deduplicateTypesAndReferences(array $array): array { + if (isset($array['@graph'])) { + // Should only be run on a top level Jsonld array. + foreach ($array['@graph'] as $object_key => $object_value) { + foreach ($object_value as $key => $values) { + if ($key == '@type' && is_array($values)) { + $array['@graph'][$object_key]['@type'] = array_unique($values); + } + elseif ($key != '@id' && is_array($array['@graph'][$object_key][$key]) + && count($array['@graph'][$object_key][$key]) > 1) { + $array['@graph'][$object_key][$key] = self::deduplicateArrayOfIds($array['@graph'][$object_key][$key]); + } + } + } + } + return $array; + } + + /** + * Deduplicate multi-dimensional array based on the `@id` value. + * + * @param array $array + * The multi-dimensional array. + * + * @return array + * The deduplicated multi-dimensional array. + */ + private static function deduplicateArrayOfIds(array $array): array { + $temp_array = []; + if (!isset($array[0]['@id'])) { + // No @id key, so just return the original array. + return $array; + } + foreach ($array as $val) { + if (array_search($val['@id'], array_column($temp_array, '@id')) === FALSE) { + $temp_array[] = $val; + } + } + return $temp_array; + } + } diff --git a/tests/src/Kernel/JsonldContextGeneratorTest.php b/tests/src/Kernel/JsonldContextGeneratorTest.php index 9aeff21..85d5c99 100644 --- a/tests/src/Kernel/JsonldContextGeneratorTest.php +++ b/tests/src/Kernel/JsonldContextGeneratorTest.php @@ -63,7 +63,7 @@ public function setUp() :void { ]; // Save bundle mapping config. - $rdfMapping = rdf_get_mapping('entity_test', 'rdf_source') + rdf_get_mapping('entity_test', 'rdf_source') ->setBundleMapping(['types' => $types]) ->setFieldMapping('created', $mapping) ->save(); diff --git a/tests/src/Kernel/JsonldHookTest.php b/tests/src/Kernel/JsonldHookTest.php index 3fa76ba..b7ae0b7 100644 --- a/tests/src/Kernel/JsonldHookTest.php +++ b/tests/src/Kernel/JsonldHookTest.php @@ -37,7 +37,7 @@ public function setUp() : void { */ public function testAlterNormalizedJsonld() { - list($entity, $expected) = $this->generateTestEntity(); + list($entity, $expected) = JsonldTestEntityGenerator::create()->generateNewEntity(); $expected['@graph'][] = [ "@id" => "json_alter_normalize_hooks", "http://purl.org/dc/elements/1.1/title" => "The hook is tested.", diff --git a/tests/src/Kernel/JsonldKernelTestBase.php b/tests/src/Kernel/JsonldKernelTestBase.php index e75f419..24c8e8f 100644 --- a/tests/src/Kernel/JsonldKernelTestBase.php +++ b/tests/src/Kernel/JsonldKernelTestBase.php @@ -2,7 +2,6 @@ namespace Drupal\Tests\jsonld\Kernel; -use Drupal\entity_test\Entity\EntityTest; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\jsonld\Encoder\JsonldEncoder; @@ -14,7 +13,6 @@ use Drupal\KernelTests\KernelTestBase; use Drupal\serialization\EntityResolver\ChainEntityResolver; use Drupal\serialization\EntityResolver\TargetIdResolver; -use Drupal\user\Entity\User; use Symfony\Component\Serializer\Serializer; use Drupal\language\Entity\ConfigurableLanguage; @@ -141,6 +139,9 @@ protected function setUp() : void { ])->setFieldMapping('field_test_entity_reference', [ 'properties' => ['dc:references'], 'datatype' => 'xsd:nonNegativeInteger', + ])->setFieldMapping('field_test_entity_reference2', [ + 'properties' => ['dc:publisher'], + 'datatype' => 'xsd:nonNegativeInteger', ]) ->save(); @@ -174,6 +175,23 @@ protected function setUp() : void { 'translatable' => FALSE, ])->save(); + // Create the a second test entity reference field. + FieldStorageConfig::create([ + 'field_name' => 'field_test_entity_reference2', + 'entity_type' => 'entity_test', + 'type' => 'entity_reference', + 'translatable' => FALSE, + 'settings' => [ + 'target_type' => 'entity_test', + ], + ])->save(); + FieldConfig::create([ + 'entity_type' => 'entity_test', + 'field_name' => 'field_test_entity_reference2', + 'bundle' => 'entity_test', + 'translatable' => FALSE, + ])->save(); + $entity_manager = \Drupal::service('entity_type.manager'); $link_manager = \Drupal::service('hal.link_manager'); $uuid_resolver = \Drupal::service('serializer.entity_resolver.uuid'); @@ -199,127 +217,4 @@ protected function setUp() : void { $this->serializer = new Serializer($normalizers, $encoders); } - /** - * Generate a test entity and the expected normalized array. - * - * @return array - * with [ the entity, the normalized array ]. - * - * @throws \Drupal\Core\Entity\EntityStorageException - * Problem saving the entity. - * @throws \Exception - * Problem creating a DateTime. - */ - protected function generateTestEntity() { - $target_entity = EntityTest::create([ - 'name' => $this->randomMachineName(), - 'langcode' => 'en', - 'field_test_entity_reference' => NULL, - ]); - $target_entity->getFieldDefinition('created')->setTranslatable(FALSE); - $target_entity->getFieldDefinition('user_id')->setTranslatable(FALSE); - $target_entity->save(); - - $target_user = User::create([ - 'name' => $this->randomMachineName(), - 'langcode' => 'en', - ]); - $target_user->save(); - - rdf_get_mapping('entity_test', 'entity_test')->setBundleMapping( - [ - 'types' => [ - "schema:ImageObject", - ], - ])->setFieldMapping('field_test_text', [ - 'properties' => ['dc:description'], - ])->setFieldMapping('user_id', [ - 'properties' => ['schema:author'], - ])->setFieldMapping('modified', [ - 'properties' => ['schema:dateModified'], - 'datatype' => 'xsd:dateTime', - ])->save(); - - $tz = new \DateTimeZone('UTC'); - $dt = new \DateTime(NULL, $tz); - $created = $dt->format("U"); - $created_iso = $dt->format(\DateTime::W3C); - // Create an entity. - $values = [ - 'langcode' => 'en', - 'name' => $this->randomMachineName(), - 'type' => 'entity_test', - 'bundle' => 'entity_test', - 'user_id' => $target_user->id(), - 'created' => [ - 'value' => $created, - ], - 'field_test_text' => [ - 'value' => $this->randomMachineName(), - 'format' => 'full_html', - ], - 'field_test_entity_reference' => [ - 'target_id' => $target_entity->id(), - ], - ]; - - $entity = EntityTest::create($values); - $entity->save(); - - $id = "http://localhost/entity_test/" . $entity->id() . "?_format=jsonld"; - $target_id = "http://localhost/entity_test/" . $target_entity->id() . "?_format=jsonld"; - $user_id = "http://localhost/user/" . $target_user->id() . "?_format=jsonld"; - - $expected = [ - "@graph" => [ - [ - "@id" => $id, - "@type" => [ - 'http://schema.org/ImageObject', - ], - "http://purl.org/dc/terms/references" => [ - [ - "@id" => $target_id, - ], - ], - "http://purl.org/dc/terms/description" => [ - [ - "@value" => $values['field_test_text']['value'], - "@language" => "en", - ], - ], - "http://purl.org/dc/terms/title" => [ - [ - "@language" => "en", - "@value" => $values['name'], - ], - ], - "http://schema.org/author" => [ - [ - "@id" => $user_id, - ], - ], - "http://schema.org/dateCreated" => [ - [ - "@type" => "http://www.w3.org/2001/XMLSchema#dateTime", - "@value" => $created_iso, - ], - ], - ], - [ - "@id" => $user_id, - "@type" => "http://localhost/rest/type/user/user", - ], - [ - "@id" => $target_id, - "@type" => [ - "http://schema.org/ImageObject", - ], - ], - ], - ]; - - return [$entity, $expected]; - } - } diff --git a/tests/src/Kernel/JsonldTestEntityGenerator.php b/tests/src/Kernel/JsonldTestEntityGenerator.php new file mode 100644 index 0000000..8d5c4f1 --- /dev/null +++ b/tests/src/Kernel/JsonldTestEntityGenerator.php @@ -0,0 +1,271 @@ +referrableEntity1 = $this->generateReferrableEntity(); + $this->referrableEntity1->save(); + + $this->referrableEntity2 = $this->generateReferrableEntity(); + $this->referrableEntity2->save(); + + $this->user = User::create([ + 'name' => $this->randomMachineName(), + 'langcode' => 'en', + ]); + $this->user->save(); + + rdf_get_mapping('entity_test', 'entity_test')->setBundleMapping( + [ + 'types' => [ + "schema:ImageObject", + ], + ])->setFieldMapping('field_test_text', [ + 'properties' => ['dc:description'], + ])->setFieldMapping('user_id', [ + 'properties' => ['schema:author'], + ])->setFieldMapping('modified', [ + 'properties' => ['schema:dateModified'], + 'datatype' => 'xsd:dateTime', + ])->save(); + } + + /** + * Static construction method. + * + * @return \Drupal\Tests\jsonld\Kernel\JsonldTestEntityGenerator + * A new test entity generator. + */ + public static function create() { + return new JsonldTestEntityGenerator(); + } + + /** + * Create a new random entity. + * + * @return \Drupal\entity_test\Entity\EntityTest + * The new test entity. + */ + private function generateReferrableEntity(): EntityTest { + $target_entity = EntityTest::create([ + 'name' => $this->randomMachineName(), + 'langcode' => 'en', + 'field_test_entity_reference' => NULL, + ]); + $target_entity->getFieldDefinition('created')->setTranslatable(FALSE); + $target_entity->getFieldDefinition('user_id')->setTranslatable(FALSE); + return $target_entity; + } + + /** + * Make both reference fields point to the same entity. + * + * @return \Drupal\Tests\jsonld\Kernel\JsonldTestEntityGenerator + * This test entity generator. + */ + public function makeDuplicateReference(): JsonldTestEntityGenerator { + $this->referrableEntity2 = $this->referrableEntity1; + return $this; + } + + /** + * Make reference field 2 use the same RDF predicate as reference field 1. + * + * @return \Drupal\Tests\jsonld\Kernel\JsonldTestEntityGenerator + * This test entity generator. + * + * @throws \Drupal\Core\Entity\EntityStorageException + * Error altering the RDF mapping. + */ + public function makeDuplicateReferenceMapping(): JsonldTestEntityGenerator { + rdf_get_mapping('entity_test', 'entity_test') + ->setFieldMapping('field_test_entity_reference2', [ + 'properties' => ['dc:references'], + 'datatype' => 'xsd:nonNegativeInteger', + ] + )->save(); + $this->referrableEntity2Predicate = $this->referableEntity1Predicate; + return $this; + } + + /** + * Generate the entity and expected Json-ld array. + * + * @return array + * Array of [ entity , expected jsonld array ]. + * + * @throws \Drupal\Core\Entity\EntityStorageException + * Error saving the entity. + */ + public function generateNewEntity(): array { + $dt = new \DateTime(NULL, new \DateTimeZone('UTC')); + $created = $dt->format("U"); + $created_iso = $dt->format(\DateTime::W3C); + // Create an entity. + $values = [ + 'langcode' => 'en', + 'name' => $this->randomMachineName(), + 'type' => 'entity_test', + 'bundle' => 'entity_test', + 'user_id' => $this->user->id(), + 'created' => [ + 'value' => $created, + ], + 'field_test_text' => [ + 'value' => $this->randomMachineName(), + 'format' => 'full_html', + ], + 'field_test_entity_reference' => [ + 'target_id' => $this->referrableEntity1->id(), + ], + 'field_test_entity_reference2' => [ + 'target_id' => $this->referrableEntity2->id(), + ], + ]; + + $entity = EntityTest::create($values); + $entity->save(); + + $id = "http://localhost/entity_test/" . $entity->id() . "?_format=jsonld"; + $target_id = "http://localhost/entity_test/" . $this->referrableEntity1->id() . "?_format=jsonld"; + $target_id_2 = "http://localhost/entity_test/" . $this->referrableEntity2->id() . "?_format=jsonld"; + $user_id = "http://localhost/user/" . $this->user->id() . "?_format=jsonld"; + + $expected = [ + "@graph" => [ + [ + "@id" => $id, + "@type" => [ + 'http://schema.org/ImageObject', + ], + "http://purl.org/dc/terms/references" => [ + [ + "@id" => $target_id, + ], + ], + "http://purl.org/dc/terms/publisher" => [ + [ + "@id" => $target_id_2, + ], + ], + "http://purl.org/dc/terms/description" => [ + [ + "@value" => $values['field_test_text']['value'], + "@language" => "en", + ], + ], + "http://purl.org/dc/terms/title" => [ + [ + "@language" => "en", + "@value" => $values['name'], + ], + ], + "http://schema.org/author" => [ + [ + "@id" => $user_id, + ], + ], + "http://schema.org/dateCreated" => [ + [ + "@type" => "http://www.w3.org/2001/XMLSchema#dateTime", + "@value" => $created_iso, + ], + ], + ], + [ + "@id" => $user_id, + "@type" => "http://localhost/rest/type/user/user", + ], + [ + "@id" => $target_id, + "@type" => [ + "http://schema.org/ImageObject", + ], + ], + ], + ]; + // If we mapped two entities to the same predicate we remove one. + if ($this->referableEntity1Predicate == $this->referrableEntity2Predicate) { + if ($this->referrableEntity1 !== $this->referrableEntity2) { + // If there are two different entities referred to, then merge them. + $expected['@graph'][0][self::DCTERMS_REFERENCES] = array_merge( + $expected['@graph'][0][self::DCTERMS_REFERENCES], + $expected['@graph'][0][self::DCTERMS_PUBLISHER] + ); + } + unset($expected['@graph'][0][self::DCTERMS_PUBLISHER]); + } + // If the 2 referenced entities are different both need to have an entry. + if ($target_id !== $target_id_2) { + $expected['@graph'][] = [ + "@id" => $target_id_2, + "@type" => [ + "http://schema.org/ImageObject", + ], + ]; + } + + return [$entity, $expected]; + } + +} diff --git a/tests/src/Kernel/Normalizer/JsonldContentEntityNormalizerTest.php b/tests/src/Kernel/Normalizer/JsonldContentEntityNormalizerTest.php index 27c2d64..1a9cce6 100644 --- a/tests/src/Kernel/Normalizer/JsonldContentEntityNormalizerTest.php +++ b/tests/src/Kernel/Normalizer/JsonldContentEntityNormalizerTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\jsonld\Kernel\Normalizer; use Drupal\Tests\jsonld\Kernel\JsonldKernelTestBase; +use Drupal\Tests\jsonld\Kernel\JsonldTestEntityGenerator; /** * Tests the JSON-LD Normalizer. @@ -32,7 +33,7 @@ protected function setUp() :void { */ public function testSimpleNormalizeJsonld() { - list($entity, $expected) = $this->generateTestEntity(); + list($entity, $expected) = JsonldTestEntityGenerator::create()->generateNewEntity(); $normalized = $this->serializer->normalize($entity, $this->format); $this->assertEquals($expected, $normalized, "Did not normalize correctly."); @@ -51,7 +52,7 @@ public function testSimpleNormalizeJsonld() { */ public function testLocalizedNormalizeJsonld() { - list($entity, $expected) = $this->generateTestEntity(); + list($entity, $expected) = JsonldTestEntityGenerator::create()->generateNewEntity(); $existing_entity_values = $entity->toArray(); $target_entity_tl_id = $existing_entity_values['field_test_entity_reference'][0]['target_id']; @@ -90,4 +91,43 @@ public function testLocalizedNormalizeJsonld() { } + /** + * Where multiple referenced entities are tied to the same rdf mapping. + */ + public function testDeduplicateEntityReferenceMappings(): void { + + list($entity, $expected) = JsonldTestEntityGenerator::create()->makeDuplicateReferenceMapping()->generateNewEntity(); + + $normalized = $this->serializer->normalize($entity, $this->format); + + $this->assertEquals($expected, $normalized, "Did not normalize correctly."); + } + + /** + * Test where multiple fields rdf mapping is referencing the same entity. + */ + public function testDeduplicateEntityReferenceIds(): void { + + list($entity, $expected) = JsonldTestEntityGenerator::create()->makeDuplicateReference()->generateNewEntity(); + + $normalized = $this->serializer->normalize($entity, $this->format); + + $this->assertEquals($expected, $normalized, "Did not normalize correctly."); + } + + /** + * Test where multiple fields are. + * + * - are referencing the same entity. + * - sharing the same RDF mapping. + */ + public function testDuplicateEntityReferenceAndMappings(): void { + list($entity, $expected) = JsonldTestEntityGenerator::create()->makeDuplicateReference()->makeDuplicateReferenceMapping() + ->generateNewEntity(); + + $normalized = $this->serializer->normalize($entity, $this->format); + + $this->assertEquals($expected, $normalized, "Did not normalize correctly."); + } + }