Loading core/modules/jsonapi/src/Controller/EntityResource.php +7 −5 Changes for core/modules/jsonapi/src/Controller/EntityResource.php: 7 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -672,9 +672,10 @@ public function addToRelationshipData(ResourceType $resource_type, FieldableEnti return $this->getRelationship($resource_type, $entity, $related, $request, $status); } $main_property_name = $field_definition->getItemDefinition()->getMainPropertyName(); foreach ($new_resource_identifiers as $new_resource_identifier) { $new_field_value = [$main_property_name => $this->getEntityFromResourceIdentifier($new_resource_identifier)->id()]; // We assume all entity reference fields have an 'entity' computed // property that can be used to assign the needed values. $new_field_value = ['entity' => $this->getEntityFromResourceIdentifier($new_resource_identifier)]; // Remove `arity` from the received extra properties, otherwise this // will fail field validation. $new_field_value += array_diff_key($new_resource_identifier->getMeta(), array_flip([ResourceIdentifier::ARITY_KEY])); Loading Loading @@ -760,9 +761,10 @@ protected function doPatchIndividualRelationship(EntityInterface $entity, array * The field definition of the entity field to be updated. */ protected function doPatchMultipleRelationship(EntityInterface $entity, array $resource_identifiers, FieldDefinitionInterface $field_definition) { $main_property_name = $field_definition->getItemDefinition()->getMainPropertyName(); $entity->{$field_definition->getName()} = array_map(function (ResourceIdentifier $resource_identifier) use ($main_property_name) { $field_properties = [$main_property_name => $this->getEntityFromResourceIdentifier($resource_identifier)->id()]; $entity->{$field_definition->getName()} = array_map(function (ResourceIdentifier $resource_identifier) { // We assume all entity reference fields have an 'entity' computed // property that can be used to assign the needed values. $field_properties = ['entity' => $this->getEntityFromResourceIdentifier($resource_identifier)]; // Remove `arity` from the received extra properties, otherwise this // will fail field validation. $field_properties += array_diff_key($resource_identifier->getMeta(), array_flip([ResourceIdentifier::ARITY_KEY])); Loading core/modules/jsonapi/tests/modules/jsonapi_test_field_type/config/schema/jsonapi_test_field_type.schema.yml 0 → 100644 +11 −0 Changes for core/modules/jsonapi/tests/modules/jsonapi_test_field_type/config/schema/jsonapi_test_field_type.schema.yml: 11 added lines, 0 removed lines. Original line number Diff line number Diff line field.storage_settings.jsonapi_test_field_type_entity_reference_uuid: type: field.storage_settings.entity_reference label: 'Entity reference field storage settings' field.field_settings.jsonapi_test_field_type_entity_reference_uuid: type: field.field_settings.entity_reference label: 'Entity reference field settings' field.value.jsonapi_test_field_type_entity_reference_uuid: type: field.value.entity_reference label: 'Default value' core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/Plugin/Field/FieldType/EntityReferenceUuidItem.php 0 → 100644 +241 −0 Changes for core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/Plugin/Field/FieldType/EntityReferenceUuidItem.php: 241 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\jsonapi_test_field_type\Plugin\Field\FieldType; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\TypedData\EntityDataDefinition; use Drupal\Core\Field\Attribute\FieldType; use Drupal\Core\Field\EntityReferenceFieldItemList; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\TypedData\DataReferenceDefinition; use Drupal\Core\TypedData\DataReferenceTargetDefinition; /** * Defines the 'entity_reference_uuid' entity field type. * * Supported settings (below the definition's 'settings' key) are: * - target_type: The entity type to reference. Required. * * @property string $target_uuid */ #[FieldType( id: 'jsonapi_test_field_type_entity_reference_uuid', label: new TranslatableMarkup('Entity reference UUID'), description: new TranslatableMarkup('An entity field containing an entity reference by UUID.'), category: 'reference', default_widget: 'entity_reference_autocomplete', default_formatter: 'entity_reference_label', list_class: EntityReferenceFieldItemList::class, )] class EntityReferenceUuidItem extends EntityReferenceItem { /** * {@inheritdoc} */ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { $settings = $field_definition->getSettings(); $target_type_info = \Drupal::entityTypeManager()->getDefinition($settings['target_type']); $properties = parent::propertyDefinitions($field_definition); $target_uuid_definition = DataReferenceTargetDefinition::create('string') ->setLabel(new TranslatableMarkup('@label UUID', ['@label' => $target_type_info->getLabel()])); $target_uuid_definition->setRequired(TRUE); $properties['target_uuid'] = $target_uuid_definition; $properties['entity'] = DataReferenceDefinition::create('entity') ->setLabel($target_type_info->getLabel()) ->setDescription(new TranslatableMarkup('The referenced entity by UUID')) // The entity object is computed out of the entity ID. ->setComputed(TRUE) ->setReadOnly(FALSE) ->setTargetDefinition(EntityDataDefinition::create($settings['target_type'])) // We can add a constraint for the target entity type. The list of // referenceable bundles is a field setting, so the corresponding // constraint is added dynamically in ::getConstraints(). ->addConstraint('EntityType', $settings['target_type']); return $properties; } /** * {@inheritdoc} */ public static function mainPropertyName() { return 'target_uuid'; } /** * {@inheritdoc} */ public static function schema(FieldStorageDefinitionInterface $field_definition) { $columns = [ 'target_uuid' => [ 'description' => 'The UUID of the target entity.', 'type' => 'varchar_ascii', 'length' => 128, ], ]; return [ 'columns' => $columns, 'indexes' => [ 'target_uuid' => ['target_uuid'], ], ]; } /** * {@inheritdoc} */ public function setValue($values, $notify = TRUE): void { if (isset($values) && !is_array($values)) { // If either a scalar or an object was passed as the value for the item, // assign it to the 'entity' or 'target_uuid' depending on values type. if (is_object($values)) { $this->set('entity', $values, $notify); } else { $this->set('target_uuid', $values, $notify); } } else { parent::setValue($values, FALSE); // Support setting the field item with only one property, but make sure // values stay in sync if only property is passed. // NULL is a valid value, so we use array_key_exists(). if (is_array($values) && array_key_exists('target_uuid', $values) && !isset($values['entity'])) { $this->onChange('target_uuid', FALSE); } elseif (is_array($values) && !array_key_exists('target_uuid', $values) && isset($values['entity'])) { $this->onChange('entity', FALSE); } elseif (is_array($values) && array_key_exists('target_uuid', $values) && isset($values['entity'])) { // If both properties are passed, verify the passed values match. The // only exception we allow is when we have a new entity: in this case // its actual id and target_uuid will be different, due to the new // entity marker. $entity_uuid = $this->get('entity')->get('uuid'); // If the entity has been saved and we're trying to set both the // target_uuid and the entity values with a non-null target UUID, then // the value for target_uuid should match the UUID of the entity value. if (!$this->entity->isNew() && $values['target_uuid'] !== NULL && ($entity_uuid !== $values['target_uuid'])) { throw new \InvalidArgumentException('The target UUID and entity passed to the entity reference item do not match.'); } } // Notify the parent if necessary. if ($notify && $this->parent) { $this->parent->onChange($this->getName()); } } } /** * {@inheritdoc} */ public function onChange($property_name, $notify = TRUE): void { // Make sure that the target UUID and the target property stay in sync. if ($property_name === 'entity') { $property = $this->get('entity'); if ($target_uuid = $property->isTargetNew() ? NULL : $property->getValue()->uuid()) { $this->writePropertyValue('target_uuid', $target_uuid); } } elseif ($property_name === 'target_uuid') { $property = $this->get('entity'); $entity_type = $property->getDataDefinition()->getConstraint('EntityType'); $entities = \Drupal::entityTypeManager()->getStorage($entity_type)->loadByProperties(['uuid' => $this->get('target_uuid')->getValue()]); if ($entity = array_shift($entities)) { assert($entity instanceof EntityInterface); $this->writePropertyValue('target_uuid', $entity->uuid()); $this->writePropertyValue('entity', $entity); } } parent::onChange($property_name, $notify); } /** * {@inheritdoc} */ public function isEmpty() { // Avoid loading the entity by first checking the 'target_uuid'. if ($this->target_uuid !== NULL) { return FALSE; } if ($this->entity && $this->entity instanceof EntityInterface) { return FALSE; } return TRUE; } /** * {@inheritdoc} */ public function preSave(): void { if ($this->hasNewEntity()) { // Save the entity if it has not already been saved by some other code. if ($this->entity->isNew()) { $this->entity->save(); } // Make sure the parent knows we are updating this property so it can // react properly. $this->target_uuid = $this->entity->uuid(); } if (!$this->isEmpty() && $this->target_uuid === NULL) { $this->target_uuid = $this->entity->uuid(); } } /** * {@inheritdoc} */ public static function generateSampleValue(FieldDefinitionInterface $field_definition): array { $manager = \Drupal::service('plugin.manager.entity_reference_selection'); // Instead of calling $manager->getSelectionHandler($field_definition) // replicate the behavior to be able to override the sorting settings. $options = [ 'target_type' => $field_definition->getFieldStorageDefinition()->getSetting('target_type'), 'handler' => $field_definition->getSetting('handler'), 'handler_settings' => $field_definition->getSetting('handler_settings') ?: [], 'entity' => NULL, ]; $entity_type = \Drupal::entityTypeManager()->getDefinition($options['target_type']); $options['handler_settings']['sort'] = [ 'field' => $entity_type->getKey('uuid'), 'direction' => 'DESC', ]; $selection_handler = $manager->getInstance($options); // Select a random number of references between the last 50 referenceable // entities created. if ($referenceable = $selection_handler->getReferenceableEntities(NULL, 'CONTAINS', 50)) { $group = array_rand($referenceable); return ['target_uuid' => array_rand($referenceable[$group])]; } return []; } /** * Determines whether the item holds an unsaved entity. * * This is notably used for "autocreate" widgets, and more generally to * support referencing freshly created entities (they will get saved * automatically as the hosting entity gets saved). * * @return bool * TRUE if the item holds an unsaved entity. */ public function hasNewEntity() { return !$this->isEmpty() && $this->target_uuid === NULL && $this->entity->isNew(); } } core/modules/jsonapi/tests/src/Functional/JsonApiRelationshipTest.php 0 → 100644 +157 −0 Changes for core/modules/jsonapi/tests/src/Functional/JsonApiRelationshipTest.php: 157 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Tests\jsonapi\Functional; use Drupal\Core\Url; use Drupal\entity_test\EntityTestHelper; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use GuzzleHttp\RequestOptions; /** * JSON:API resource tests. * * @group jsonapi * * @internal */ class JsonApiRelationshipTest extends JsonApiFunctionalTestBase { /** * {@inheritdoc} */ protected static $modules = [ 'basic_auth', 'entity_test', 'jsonapi_test_field_type', ]; /** * The entity type ID. */ protected string $entityTypeId = 'entity_test'; /** * The entity bundle. */ protected string $bundle = 'entity_test'; /** * The field name. */ protected string $fieldName = 'field_child'; /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); EntityTestHelper::createBundle($this->bundle, 'Parent', $this->entityTypeId); FieldStorageConfig::create([ 'field_name' => $this->fieldName, 'type' => 'jsonapi_test_field_type_entity_reference_uuid', 'entity_type' => $this->entityTypeId, 'cardinality' => 1, 'settings' => [ 'target_type' => $this->entityTypeId, ], ])->save(); FieldConfig::create([ 'field_name' => $this->fieldName, 'entity_type' => $this->entityTypeId, 'bundle' => $this->bundle, 'label' => $this->randomString(), 'settings' => [ 'handler' => 'default', 'handler_settings' => [], ], ])->save(); \Drupal::service('router.builder')->rebuild(); } /** * Test relationships without target_id as main property. * * @see https://www.drupal.org/project/drupal/issues/3476224 */ public function testPatchHandleUUIDPropertyReferenceFieldIssue3127883(): void { $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); $user = $this->drupalCreateUser([ 'administer entity_test content', 'view test entity', ]); // Create parent and child entities. $storage = $this->container->get('entity_type.manager') ->getStorage($this->entityTypeId); $parentEntity = $storage ->create([ 'type' => $this->bundle, ]); $parentEntity->save(); $childUuid = $this->container->get('uuid')->generate(); $childEntity = $storage ->create([ 'type' => $this->bundle, 'uuid' => $childUuid, ]); $childEntity->save(); $uuid = $childEntity->uuid(); $this->assertEquals($childUuid, $uuid); // 1. Successful PATCH to the related endpoint. $url = Url::fromUri(sprintf('internal:/jsonapi/%s/%s/%s/relationships/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid(), $this->fieldName)); $request_options = [ RequestOptions::HEADERS => [ 'Content-Type' => 'application/vnd.api+json', 'Accept' => 'application/vnd.api+json', ], RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw], RequestOptions::JSON => [ 'data' => [ 'id' => $childUuid, 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle), ], ], ]; $response = $this->request('PATCH', $url, $request_options); $this->assertSame(204, $response->getStatusCode(), (string) $response->getBody()); $parentEntity = $storage->loadUnchanged($parentEntity->id()); $this->assertEquals($childEntity->uuid(), $parentEntity->get($this->fieldName)->target_uuid); // Reset the relationship. $parentEntity->set($this->fieldName, NULL) ->save(); $parentEntity = $storage->loadUnchanged($parentEntity->id()); $this->assertTrue($parentEntity->get($this->fieldName)->isEmpty()); // 2. Successful PATCH to individual endpoint. $url = Url::fromUri(sprintf('internal:/jsonapi/%s/%s/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid())); $request_options[RequestOptions::JSON] = [ 'data' => [ 'id' => $parentEntity->uuid(), 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle), 'relationships' => [ $this->fieldName => [ 'data' => [ [ 'id' => $childUuid, 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle), ], ], ], ], ], ]; $response = $this->request('PATCH', $url, $request_options); $this->assertSame(200, $response->getStatusCode(), (string) $response->getBody()); $parentEntity = $storage->loadUnchanged($parentEntity->id()); $this->assertEquals($childEntity->uuid(), $parentEntity->get($this->fieldName)->target_uuid); } } Loading
core/modules/jsonapi/src/Controller/EntityResource.php +7 −5 Changes for core/modules/jsonapi/src/Controller/EntityResource.php: 7 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -672,9 +672,10 @@ public function addToRelationshipData(ResourceType $resource_type, FieldableEnti return $this->getRelationship($resource_type, $entity, $related, $request, $status); } $main_property_name = $field_definition->getItemDefinition()->getMainPropertyName(); foreach ($new_resource_identifiers as $new_resource_identifier) { $new_field_value = [$main_property_name => $this->getEntityFromResourceIdentifier($new_resource_identifier)->id()]; // We assume all entity reference fields have an 'entity' computed // property that can be used to assign the needed values. $new_field_value = ['entity' => $this->getEntityFromResourceIdentifier($new_resource_identifier)]; // Remove `arity` from the received extra properties, otherwise this // will fail field validation. $new_field_value += array_diff_key($new_resource_identifier->getMeta(), array_flip([ResourceIdentifier::ARITY_KEY])); Loading Loading @@ -760,9 +761,10 @@ protected function doPatchIndividualRelationship(EntityInterface $entity, array * The field definition of the entity field to be updated. */ protected function doPatchMultipleRelationship(EntityInterface $entity, array $resource_identifiers, FieldDefinitionInterface $field_definition) { $main_property_name = $field_definition->getItemDefinition()->getMainPropertyName(); $entity->{$field_definition->getName()} = array_map(function (ResourceIdentifier $resource_identifier) use ($main_property_name) { $field_properties = [$main_property_name => $this->getEntityFromResourceIdentifier($resource_identifier)->id()]; $entity->{$field_definition->getName()} = array_map(function (ResourceIdentifier $resource_identifier) { // We assume all entity reference fields have an 'entity' computed // property that can be used to assign the needed values. $field_properties = ['entity' => $this->getEntityFromResourceIdentifier($resource_identifier)]; // Remove `arity` from the received extra properties, otherwise this // will fail field validation. $field_properties += array_diff_key($resource_identifier->getMeta(), array_flip([ResourceIdentifier::ARITY_KEY])); Loading
core/modules/jsonapi/tests/modules/jsonapi_test_field_type/config/schema/jsonapi_test_field_type.schema.yml 0 → 100644 +11 −0 Changes for core/modules/jsonapi/tests/modules/jsonapi_test_field_type/config/schema/jsonapi_test_field_type.schema.yml: 11 added lines, 0 removed lines. Original line number Diff line number Diff line field.storage_settings.jsonapi_test_field_type_entity_reference_uuid: type: field.storage_settings.entity_reference label: 'Entity reference field storage settings' field.field_settings.jsonapi_test_field_type_entity_reference_uuid: type: field.field_settings.entity_reference label: 'Entity reference field settings' field.value.jsonapi_test_field_type_entity_reference_uuid: type: field.value.entity_reference label: 'Default value'
core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/Plugin/Field/FieldType/EntityReferenceUuidItem.php 0 → 100644 +241 −0 Changes for core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/Plugin/Field/FieldType/EntityReferenceUuidItem.php: 241 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\jsonapi_test_field_type\Plugin\Field\FieldType; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\TypedData\EntityDataDefinition; use Drupal\Core\Field\Attribute\FieldType; use Drupal\Core\Field\EntityReferenceFieldItemList; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\TypedData\DataReferenceDefinition; use Drupal\Core\TypedData\DataReferenceTargetDefinition; /** * Defines the 'entity_reference_uuid' entity field type. * * Supported settings (below the definition's 'settings' key) are: * - target_type: The entity type to reference. Required. * * @property string $target_uuid */ #[FieldType( id: 'jsonapi_test_field_type_entity_reference_uuid', label: new TranslatableMarkup('Entity reference UUID'), description: new TranslatableMarkup('An entity field containing an entity reference by UUID.'), category: 'reference', default_widget: 'entity_reference_autocomplete', default_formatter: 'entity_reference_label', list_class: EntityReferenceFieldItemList::class, )] class EntityReferenceUuidItem extends EntityReferenceItem { /** * {@inheritdoc} */ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { $settings = $field_definition->getSettings(); $target_type_info = \Drupal::entityTypeManager()->getDefinition($settings['target_type']); $properties = parent::propertyDefinitions($field_definition); $target_uuid_definition = DataReferenceTargetDefinition::create('string') ->setLabel(new TranslatableMarkup('@label UUID', ['@label' => $target_type_info->getLabel()])); $target_uuid_definition->setRequired(TRUE); $properties['target_uuid'] = $target_uuid_definition; $properties['entity'] = DataReferenceDefinition::create('entity') ->setLabel($target_type_info->getLabel()) ->setDescription(new TranslatableMarkup('The referenced entity by UUID')) // The entity object is computed out of the entity ID. ->setComputed(TRUE) ->setReadOnly(FALSE) ->setTargetDefinition(EntityDataDefinition::create($settings['target_type'])) // We can add a constraint for the target entity type. The list of // referenceable bundles is a field setting, so the corresponding // constraint is added dynamically in ::getConstraints(). ->addConstraint('EntityType', $settings['target_type']); return $properties; } /** * {@inheritdoc} */ public static function mainPropertyName() { return 'target_uuid'; } /** * {@inheritdoc} */ public static function schema(FieldStorageDefinitionInterface $field_definition) { $columns = [ 'target_uuid' => [ 'description' => 'The UUID of the target entity.', 'type' => 'varchar_ascii', 'length' => 128, ], ]; return [ 'columns' => $columns, 'indexes' => [ 'target_uuid' => ['target_uuid'], ], ]; } /** * {@inheritdoc} */ public function setValue($values, $notify = TRUE): void { if (isset($values) && !is_array($values)) { // If either a scalar or an object was passed as the value for the item, // assign it to the 'entity' or 'target_uuid' depending on values type. if (is_object($values)) { $this->set('entity', $values, $notify); } else { $this->set('target_uuid', $values, $notify); } } else { parent::setValue($values, FALSE); // Support setting the field item with only one property, but make sure // values stay in sync if only property is passed. // NULL is a valid value, so we use array_key_exists(). if (is_array($values) && array_key_exists('target_uuid', $values) && !isset($values['entity'])) { $this->onChange('target_uuid', FALSE); } elseif (is_array($values) && !array_key_exists('target_uuid', $values) && isset($values['entity'])) { $this->onChange('entity', FALSE); } elseif (is_array($values) && array_key_exists('target_uuid', $values) && isset($values['entity'])) { // If both properties are passed, verify the passed values match. The // only exception we allow is when we have a new entity: in this case // its actual id and target_uuid will be different, due to the new // entity marker. $entity_uuid = $this->get('entity')->get('uuid'); // If the entity has been saved and we're trying to set both the // target_uuid and the entity values with a non-null target UUID, then // the value for target_uuid should match the UUID of the entity value. if (!$this->entity->isNew() && $values['target_uuid'] !== NULL && ($entity_uuid !== $values['target_uuid'])) { throw new \InvalidArgumentException('The target UUID and entity passed to the entity reference item do not match.'); } } // Notify the parent if necessary. if ($notify && $this->parent) { $this->parent->onChange($this->getName()); } } } /** * {@inheritdoc} */ public function onChange($property_name, $notify = TRUE): void { // Make sure that the target UUID and the target property stay in sync. if ($property_name === 'entity') { $property = $this->get('entity'); if ($target_uuid = $property->isTargetNew() ? NULL : $property->getValue()->uuid()) { $this->writePropertyValue('target_uuid', $target_uuid); } } elseif ($property_name === 'target_uuid') { $property = $this->get('entity'); $entity_type = $property->getDataDefinition()->getConstraint('EntityType'); $entities = \Drupal::entityTypeManager()->getStorage($entity_type)->loadByProperties(['uuid' => $this->get('target_uuid')->getValue()]); if ($entity = array_shift($entities)) { assert($entity instanceof EntityInterface); $this->writePropertyValue('target_uuid', $entity->uuid()); $this->writePropertyValue('entity', $entity); } } parent::onChange($property_name, $notify); } /** * {@inheritdoc} */ public function isEmpty() { // Avoid loading the entity by first checking the 'target_uuid'. if ($this->target_uuid !== NULL) { return FALSE; } if ($this->entity && $this->entity instanceof EntityInterface) { return FALSE; } return TRUE; } /** * {@inheritdoc} */ public function preSave(): void { if ($this->hasNewEntity()) { // Save the entity if it has not already been saved by some other code. if ($this->entity->isNew()) { $this->entity->save(); } // Make sure the parent knows we are updating this property so it can // react properly. $this->target_uuid = $this->entity->uuid(); } if (!$this->isEmpty() && $this->target_uuid === NULL) { $this->target_uuid = $this->entity->uuid(); } } /** * {@inheritdoc} */ public static function generateSampleValue(FieldDefinitionInterface $field_definition): array { $manager = \Drupal::service('plugin.manager.entity_reference_selection'); // Instead of calling $manager->getSelectionHandler($field_definition) // replicate the behavior to be able to override the sorting settings. $options = [ 'target_type' => $field_definition->getFieldStorageDefinition()->getSetting('target_type'), 'handler' => $field_definition->getSetting('handler'), 'handler_settings' => $field_definition->getSetting('handler_settings') ?: [], 'entity' => NULL, ]; $entity_type = \Drupal::entityTypeManager()->getDefinition($options['target_type']); $options['handler_settings']['sort'] = [ 'field' => $entity_type->getKey('uuid'), 'direction' => 'DESC', ]; $selection_handler = $manager->getInstance($options); // Select a random number of references between the last 50 referenceable // entities created. if ($referenceable = $selection_handler->getReferenceableEntities(NULL, 'CONTAINS', 50)) { $group = array_rand($referenceable); return ['target_uuid' => array_rand($referenceable[$group])]; } return []; } /** * Determines whether the item holds an unsaved entity. * * This is notably used for "autocreate" widgets, and more generally to * support referencing freshly created entities (they will get saved * automatically as the hosting entity gets saved). * * @return bool * TRUE if the item holds an unsaved entity. */ public function hasNewEntity() { return !$this->isEmpty() && $this->target_uuid === NULL && $this->entity->isNew(); } }
core/modules/jsonapi/tests/src/Functional/JsonApiRelationshipTest.php 0 → 100644 +157 −0 Changes for core/modules/jsonapi/tests/src/Functional/JsonApiRelationshipTest.php: 157 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Tests\jsonapi\Functional; use Drupal\Core\Url; use Drupal\entity_test\EntityTestHelper; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use GuzzleHttp\RequestOptions; /** * JSON:API resource tests. * * @group jsonapi * * @internal */ class JsonApiRelationshipTest extends JsonApiFunctionalTestBase { /** * {@inheritdoc} */ protected static $modules = [ 'basic_auth', 'entity_test', 'jsonapi_test_field_type', ]; /** * The entity type ID. */ protected string $entityTypeId = 'entity_test'; /** * The entity bundle. */ protected string $bundle = 'entity_test'; /** * The field name. */ protected string $fieldName = 'field_child'; /** * {@inheritdoc} */ protected function setUp(): void { parent::setUp(); EntityTestHelper::createBundle($this->bundle, 'Parent', $this->entityTypeId); FieldStorageConfig::create([ 'field_name' => $this->fieldName, 'type' => 'jsonapi_test_field_type_entity_reference_uuid', 'entity_type' => $this->entityTypeId, 'cardinality' => 1, 'settings' => [ 'target_type' => $this->entityTypeId, ], ])->save(); FieldConfig::create([ 'field_name' => $this->fieldName, 'entity_type' => $this->entityTypeId, 'bundle' => $this->bundle, 'label' => $this->randomString(), 'settings' => [ 'handler' => 'default', 'handler_settings' => [], ], ])->save(); \Drupal::service('router.builder')->rebuild(); } /** * Test relationships without target_id as main property. * * @see https://www.drupal.org/project/drupal/issues/3476224 */ public function testPatchHandleUUIDPropertyReferenceFieldIssue3127883(): void { $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); $user = $this->drupalCreateUser([ 'administer entity_test content', 'view test entity', ]); // Create parent and child entities. $storage = $this->container->get('entity_type.manager') ->getStorage($this->entityTypeId); $parentEntity = $storage ->create([ 'type' => $this->bundle, ]); $parentEntity->save(); $childUuid = $this->container->get('uuid')->generate(); $childEntity = $storage ->create([ 'type' => $this->bundle, 'uuid' => $childUuid, ]); $childEntity->save(); $uuid = $childEntity->uuid(); $this->assertEquals($childUuid, $uuid); // 1. Successful PATCH to the related endpoint. $url = Url::fromUri(sprintf('internal:/jsonapi/%s/%s/%s/relationships/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid(), $this->fieldName)); $request_options = [ RequestOptions::HEADERS => [ 'Content-Type' => 'application/vnd.api+json', 'Accept' => 'application/vnd.api+json', ], RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw], RequestOptions::JSON => [ 'data' => [ 'id' => $childUuid, 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle), ], ], ]; $response = $this->request('PATCH', $url, $request_options); $this->assertSame(204, $response->getStatusCode(), (string) $response->getBody()); $parentEntity = $storage->loadUnchanged($parentEntity->id()); $this->assertEquals($childEntity->uuid(), $parentEntity->get($this->fieldName)->target_uuid); // Reset the relationship. $parentEntity->set($this->fieldName, NULL) ->save(); $parentEntity = $storage->loadUnchanged($parentEntity->id()); $this->assertTrue($parentEntity->get($this->fieldName)->isEmpty()); // 2. Successful PATCH to individual endpoint. $url = Url::fromUri(sprintf('internal:/jsonapi/%s/%s/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid())); $request_options[RequestOptions::JSON] = [ 'data' => [ 'id' => $parentEntity->uuid(), 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle), 'relationships' => [ $this->fieldName => [ 'data' => [ [ 'id' => $childUuid, 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle), ], ], ], ], ], ]; $response = $this->request('PATCH', $url, $request_options); $this->assertSame(200, $response->getStatusCode(), (string) $response->getBody()); $parentEntity = $storage->loadUnchanged($parentEntity->id()); $this->assertEquals($childEntity->uuid(), $parentEntity->get($this->fieldName)->target_uuid); } }