diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt index d492ebe27a..7a7a6b7f30 100644 --- a/core/misc/cspell/dictionary.txt +++ b/core/misc/cspell/dictionary.txt @@ -1825,6 +1825,7 @@ unsets unsetting unshortened unsimplified +unskip unsticky unstripped unsynchronized diff --git a/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php index c2d306d4b9..fe508b9708 100644 --- a/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php @@ -14,6 +14,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Installer\InstallerKernel; use Drupal\Core\TypedData\DataReferenceTargetDefinition; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException; @@ -179,7 +180,7 @@ public function get($entity_type_id, $bundle) { throw new PreconditionFailedHttpException('Server error. The current route is malformed.'); } - return $this->getByTypeName("$entity_type_id--$bundle"); + return static::lookupResourceType($this->all(), $entity_type_id, $bundle); } /** @@ -423,19 +424,34 @@ protected function calculateRelatableResourceTypes(ResourceType $resource_type, */ protected function getRelatableResourceTypesFromFieldDefinition(FieldDefinitionInterface $field_definition, array $resource_types) { $item_definition = $field_definition->getItemDefinition(); - $entity_type_id = $item_definition->getSetting('target_type'); $handler_settings = $item_definition->getSetting('handler_settings'); + $target_bundles = empty($handler_settings['target_bundles']) ? $this->getAllBundlesForEntityType($entity_type_id) : $handler_settings['target_bundles']; + $relatable_resource_types = []; - $has_target_bundles = isset($handler_settings['target_bundles']) && !empty($handler_settings['target_bundles']); - $target_bundles = $has_target_bundles ? - $handler_settings['target_bundles'] - : $this->getAllBundlesForEntityType($entity_type_id); + foreach ($target_bundles as $target_bundle) { + if ($resource_type = static::lookupResourceType($resource_types, $entity_type_id, $target_bundle)) { + $relatable_resource_types[] = $resource_type; + } + // Do not warn during the site installation since system integrity + // is not guaranteed in this period and the warnings may pop up falsy, + // adding confusion to the process. + elseif (!InstallerKernel::installationAttempted()) { + trigger_error( + sprintf( + 'The "%s" at "%s:%s" references the "%s:%s" entity type that does not exist. Please take action.', + $field_definition->getName(), + $field_definition->getTargetEntityTypeId(), + $field_definition->getTargetBundle(), + $entity_type_id, + $target_bundle + ), + E_USER_WARNING + ); + } + } - return array_map(function ($target_bundle) use ($entity_type_id, $resource_types) { - $type_name = "$entity_type_id--$target_bundle"; - return isset($resource_types[$type_name]) ? $resource_types[$type_name] : NULL; - }, $target_bundles); + return $relatable_resource_types; } /** @@ -473,7 +489,36 @@ protected function isReferenceFieldDefinition(FieldDefinitionInterface $field_de * The bundle IDs. */ protected function getAllBundlesForEntityType($entity_type_id) { - return array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id)); + // Ensure all keys are strings because numeric values are allowed as bundle + // names and "array_keys()" casts "42" to 42. + return array_map('strval', array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id))); + } + + /** + * Lookup a resource type by entity type ID and bundle name. + * + * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types + * The list of resource types to do a lookup. + * @param string $entity_type_id + * The entity type of a seekable resource type. + * @param string $bundle + * The entity bundle of a seekable resource type. + * + * @return \Drupal\jsonapi\ResourceType\ResourceType|null + * The resource type or NULL if one cannot be found. + */ + protected static function lookupResourceType(array $resource_types, $entity_type_id, $bundle) { + if (isset($resource_types["$entity_type_id--$bundle"])) { + return $resource_types["$entity_type_id--$bundle"]; + } + + foreach ($resource_types as $resource_type) { + if ($resource_type->getEntityTypeId() === $entity_type_id && $resource_type->getBundle() === $bundle) { + return $resource_type; + } + } + + return NULL; } } diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_resource_type_aliasing/jsonapi_test_resource_type_aliasing.info.yml b/core/modules/jsonapi/tests/modules/jsonapi_test_resource_type_aliasing/jsonapi_test_resource_type_aliasing.info.yml new file mode 100644 index 0000000000..828588185d --- /dev/null +++ b/core/modules/jsonapi/tests/modules/jsonapi_test_resource_type_aliasing/jsonapi_test_resource_type_aliasing.info.yml @@ -0,0 +1,3 @@ +name: 'JSON:API test resource type aliasing' +type: module +package: Testing diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_resource_type_aliasing/jsonapi_test_resource_type_aliasing.services.yml b/core/modules/jsonapi/tests/modules/jsonapi_test_resource_type_aliasing/jsonapi_test_resource_type_aliasing.services.yml new file mode 100644 index 0000000000..9c02e50e34 --- /dev/null +++ b/core/modules/jsonapi/tests/modules/jsonapi_test_resource_type_aliasing/jsonapi_test_resource_type_aliasing.services.yml @@ -0,0 +1,5 @@ +services: + Drupal\jsonapi_test_resource_type_aliasing\ResourceType\AliasingResourceTypeRepository: + decorates: jsonapi.resource_type.repository + parent: jsonapi.resource_type.repository + public: false diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_resource_type_aliasing/src/ResourceType/AliasedResourceType.php b/core/modules/jsonapi/tests/modules/jsonapi_test_resource_type_aliasing/src/ResourceType/AliasedResourceType.php new file mode 100644 index 0000000000..761dc8ee59 --- /dev/null +++ b/core/modules/jsonapi/tests/modules/jsonapi_test_resource_type_aliasing/src/ResourceType/AliasedResourceType.php @@ -0,0 +1,33 @@ +getEntityTypeId(), + $resource_type->getBundle(), + $resource_type->getDeserializationTargetClass(), + $resource_type->isInternal(), + $resource_type->isLocatable(), + $resource_type->isMutable(), + $resource_type->isVersionable(), + $resource_type->getFields() + ); + // Alias the resource type name with an alternative pattern. + $this->typeName = $alias; + } + +} diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_resource_type_aliasing/src/ResourceType/AliasingResourceTypeRepository.php b/core/modules/jsonapi/tests/modules/jsonapi_test_resource_type_aliasing/src/ResourceType/AliasingResourceTypeRepository.php new file mode 100644 index 0000000000..6b8bc234db --- /dev/null +++ b/core/modules/jsonapi/tests/modules/jsonapi_test_resource_type_aliasing/src/ResourceType/AliasingResourceTypeRepository.php @@ -0,0 +1,24 @@ +id(), $bundle); + $base_resource_type = parent::createResourceType($entity_type, $bundle); + return new AliasedResourceType($base_resource_type, $alias); + } + +} diff --git a/core/modules/jsonapi/tests/src/Kernel/ResourceType/RelatedResourceTypesTest.php b/core/modules/jsonapi/tests/src/Kernel/ResourceType/RelatedResourceTypesTest.php index c8afcce764..3ee33a9c6e 100644 --- a/core/modules/jsonapi/tests/src/Kernel/ResourceType/RelatedResourceTypesTest.php +++ b/core/modules/jsonapi/tests/src/Kernel/ResourceType/RelatedResourceTypesTest.php @@ -4,6 +4,7 @@ use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase; use Drupal\node\Entity\NodeType; +use PHPUnit\Framework\Error\Warning; /** * @coversDefaultClass \Drupal\jsonapi\ResourceType\ResourceType @@ -179,4 +180,35 @@ public function getRelatableResourceTypesByFieldProvider() { ]; } + /** + * Ensure a graceful failure when a field can references a missing bundle. + * + * @covers \Drupal\jsonapi\ResourceType\ResourceTypeRepository::all + * @covers \Drupal\jsonapi\ResourceType\ResourceTypeRepository::calculateRelatableResourceTypes + * @covers \Drupal\jsonapi\ResourceType\ResourceTypeRepository::getRelatableResourceTypesFromFieldDefinition + * + * @link https://www.drupal.org/project/drupal/issues/2996114 + */ + public function testGetRelatableResourceTypesFromFieldDefinition() { + $field_config_storage = $this->container->get('entity_type.manager')->getStorage('field_config'); + + static::assertCount(0, $this->resourceTypeRepository->get('node', 'foo')->getRelatableResourceTypesByField('field_relationship')); + $this->createEntityReferenceField('node', 'foo', 'field_ref_with_missing_bundle', 'Related entity', 'node', 'default', [ + 'target_bundles' => ['missing_bundle'], + ]); + $fields = $field_config_storage->loadByProperties(['field_name' => 'field_ref_with_missing_bundle']); + static::assertSame(['missing_bundle'], $fields['node.foo.field_ref_with_missing_bundle']->getItemDefinition()->getSetting('handler_settings')['target_bundles']); + + try { + $this->resourceTypeRepository->get('node', 'foo')->getRelatableResourceTypesByField('field_ref_with_missing_bundle'); + static::fail('The above code must produce a warning since the "missing_bundle" does not exist.'); + } + catch (Warning $e) { + static::assertSame( + 'The "field_ref_with_missing_bundle" at "node:foo" references the "node:missing_bundle" entity type that does not exist. Please take action.', + $e->getMessage() + ); + } + } + } diff --git a/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeNameAliasTest.php b/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeNameAliasTest.php new file mode 100644 index 0000000000..5ba541a729 --- /dev/null +++ b/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeNameAliasTest.php @@ -0,0 +1,87 @@ +container + ->get('entity_type.manager') + ->getStorage('node_type') + ->create(['type' => 'page']) + ->save(); + } + + /** + * Ensures resource repository works with publicly renamed resource types. + * + * @covers ::get + * @covers ::all + * @covers ::getByTypeName + */ + public function test() { + $repository = $this->container->get('jsonapi.resource_type.repository'); + + static::assertInstanceOf(ResourceType::class, $repository->get('user', 'user')); + static::assertNull($repository->getByTypeName('user--user')); + static::assertInstanceOf(ResourceType::class, $repository->getByTypeName('user==user')); + + static::assertInstanceOf(ResourceType::class, $repository->get('node', 'page')); + static::assertNull($repository->getByTypeName('node--page')); + static::assertInstanceOf(ResourceType::class, $repository->getByTypeName('node==page')); + + foreach ($repository->all() as $id => $resource_type) { + static::assertSame( + $resource_type->getTypeName(), + $id, + 'The key is always equal to the type name.' + ); + + static::assertNotSame( + sprintf('%s--%s', $resource_type->getEntityTypeId(), $resource_type->getBundle()), + $id, + 'The type name can be renamed so it differs from the internal.' + ); + } + } + +} diff --git a/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php b/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php index d4461fe9c9..54b6ed07b1 100644 --- a/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php +++ b/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php @@ -187,6 +187,16 @@ public function testResourceTypeFieldAliasing() { $this->assertSame($this->resourceTypeRepository->getByTypeName('node--page')->getPublicName('uid'), 'owner'); } + /** + * Tests that resource type fields can be aliased per resource type. + */ + public function testResourceTypeNameAliasing() { + // When this test is implemented, ensure the the tested behaviors in + // ResourceTypeNameAliasTest have been covered and remove it. Then remove + // the jsonapi_test_resource_type_aliasing test module. + $this->markTestSkipped('Unskip this test in https://www.drupal.org/project/drupal/issues/3105318'); + } + /** * Tests that resource type fields can be disabled per resource type. */