src/Controller/EntityResource.php | 35 +++++++++++++++------- src/EventSubscriber/ResourceResponseSubscriber.php | 2 ++ src/Normalizer/FieldItemNormalizer.php | 3 -- src/Normalizer/RelationshipItemNormalizer.php | 7 ++--- .../JsonApiDocumentTopLevelNormalizerValue.php | 1 + src/ParamConverter/EntityUuidConverter.php | 5 ---- .../JsonApiFunctionalMultilingualTest.php | 10 +++---- 7 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/Controller/EntityResource.php b/src/Controller/EntityResource.php index 85c2dc0..46d3359 100644 --- a/src/Controller/EntityResource.php +++ b/src/Controller/EntityResource.php @@ -115,6 +115,8 @@ class EntityResource { * The base JSON API resource type for the request to be served. * @param \Drupal\Core\Entity\EntityInterface $entity * The loaded entity. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. * * @return \Drupal\jsonapi\ResourceResponse * The response. @@ -122,7 +124,8 @@ class EntityResource { * @throws \Drupal\jsonapi\Exception\EntityAccessDeniedHttpException * Thrown when access to the entity is not allowed. */ - public function getIndividual(ResourceType $resource_type, EntityInterface $entity) { + public function getIndividual(ResourceType $resource_type, EntityInterface $entity, Request $request) { + $entity = static::getTranslatedEntity($entity, $request); $entity = static::getAccessCheckedEntity($entity); if ($entity instanceof EntityAccessDeniedHttpException) { throw $entity; @@ -370,7 +373,7 @@ class EntityResource { } // Each item of the collection data contains an array with 'entity' and // 'access' elements. - $collection_data = $this->loadEntitiesWithAccess($storage, $results); + $collection_data = $this->loadEntitiesWithAccess($storage, $results, $request); $entity_collection = new EntityCollection($collection_data); $entity_collection->setHasNextPage($has_next_page); @@ -399,11 +402,13 @@ class EntityResource { * The requested entity. * @param string $related_field * The related field name. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. * * @return \Drupal\jsonapi\ResourceResponse * The response. */ - public function getRelated(ResourceType $resource_type, FieldableEntityInterface $entity, $related_field) { + public function getRelated(ResourceType $resource_type, FieldableEntityInterface $entity, $related_field, Request $request) { /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */ $field_list = $entity->get($resource_type->getInternalName($related_field)); @@ -422,7 +427,8 @@ class EntityResource { ); $collection_data = []; foreach ($referenced_entities as $referenced_entity) { - $collection_data[] = static::getAccessCheckedEntity($referenced_entity); + $translation = static::getTranslatedEntity($referenced_entity, $request); + $collection_data[] = static::getAccessCheckedEntity($translation); } $entity_collection = new EntityCollection($collection_data, $field_list->getFieldDefinition()->getFieldStorageDefinition()->getCardinality()); $response = $this->buildWrappedResponse($entity_collection); @@ -452,8 +458,9 @@ class EntityResource { * The response. */ public function getRelationship(ResourceType $resource_type, FieldableEntityInterface $entity, $related_field, Request $request, $response_code = 200) { + $translation = static::getTranslatedEntity($entity, $request); /* @var \Drupal\Core\Field\FieldItemListInterface $field_list */ - $field_list = $entity->get($resource_type->getInternalName($related_field)); + $field_list = $translation->get($resource_type->getInternalName($related_field)); $response = $this->buildWrappedResponse($field_list, $response_code); // Add the host entity as a cacheable dependency. $response->addCacheableDependency($entity); @@ -923,14 +930,17 @@ class EntityResource { * The entity storage to load the entities from. * @param int[] $ids * Array of entity IDs. + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. * * @return array * An array of loaded entities and/or an access exceptions. */ - protected function loadEntitiesWithAccess(EntityStorageInterface $storage, array $ids) { + protected function loadEntitiesWithAccess(EntityStorageInterface $storage, array $ids, Request $request) { $output = []; foreach ($storage->loadMultiple($ids) as $entity) { - $output[$entity->id()] = static::getAccessCheckedEntity($entity); + $translation = static::getTranslatedEntity($entity, $request); + $output[$entity->id()] = static::getAccessCheckedEntity($translation); } return array_values($output); } @@ -947,9 +957,6 @@ class EntityResource { * three possible return values carry the access result cacheability. */ public static function getAccessCheckedEntity(EntityInterface $entity) { - /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */ - $entity_repository = \Drupal::service('entity.repository'); - $entity = $entity_repository->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']); $access = $entity->access('view', NULL, TRUE); $entity->addCacheableDependency($access); if (!$access->isAllowed()) { @@ -967,6 +974,14 @@ class EntityResource { return $entity; } + public static function getTranslatedEntity(EntityInterface $entity, Request $request) { + $langcode = $request->query->get('lang_code', NULL); + /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */ + $entity_repository = \Drupal::service('entity.repository'); + $entity = $entity_repository->getTranslationFromContext($entity, $langcode, ['operation' => 'entity_upcast']); + return $entity; + } + /** * Checks if the given entity exists. * diff --git a/src/EventSubscriber/ResourceResponseSubscriber.php b/src/EventSubscriber/ResourceResponseSubscriber.php index 99c7c7f..9dd9a34 100644 --- a/src/EventSubscriber/ResourceResponseSubscriber.php +++ b/src/EventSubscriber/ResourceResponseSubscriber.php @@ -113,6 +113,8 @@ class ResourceResponseSubscriber implements EventSubscriberInterface { $jsonapi_doc_object = $serializer->normalize($data, $format, [ 'request' => $request, 'resource_type' => $request->get('resource_type'), + // @todo Remove this, only necessary for \Drupal\jsonapi\Normalizer\RelationshipItemNormalizer::normalize() + 'langcode' => $request->query->get('lang_code', NULL), ]); // Having just normalized the data, we can associate its cacheability with // the response object. diff --git a/src/Normalizer/FieldItemNormalizer.php b/src/Normalizer/FieldItemNormalizer.php index 76591a8..3edeb5f 100644 --- a/src/Normalizer/FieldItemNormalizer.php +++ b/src/Normalizer/FieldItemNormalizer.php @@ -52,9 +52,6 @@ class FieldItemNormalizer extends NormalizerBase implements DenormalizerInterfac $values[$property_name] = $this->serializer->normalize($property, $format, $context); } - if (isset($context['langcode'])) { - $values['lang'] = $context['langcode']; - } // @todo Use the constant \Drupal\serialization\Normalizer\CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY instead of the 'cacheability' string when JSON API requires Drupal 8.5 or newer. $value = new FieldItemNormalizerValue($values, $context['cacheability']); unset($context['cacheability']); diff --git a/src/Normalizer/RelationshipItemNormalizer.php b/src/Normalizer/RelationshipItemNormalizer.php index 1d53045..1c241c1 100644 --- a/src/Normalizer/RelationshipItemNormalizer.php +++ b/src/Normalizer/RelationshipItemNormalizer.php @@ -8,6 +8,7 @@ use Drupal\jsonapi\Normalizer\Value\RelationshipItemNormalizerValue; use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface; use Drupal\jsonapi\Controller\EntityResource; +use Symfony\Component\HttpFoundation\Request; /** * Converts the Drupal entity reference item object to a JSON API structure. @@ -52,14 +53,12 @@ class RelationshipItemNormalizer extends FieldItemNormalizer { // this. $target_entity = $relationship_item->getTargetEntity(); $values = $relationship_item->getValue(); - if (isset($context['langcode'])) { - $values['lang'] = $context['langcode']; - } $host_field_name = $relationship_item->getParent()->getPropertyName(); if (!empty($context['include']) && in_array($host_field_name, $context['include']) && $target_entity !== NULL) { $context = $this->buildSubContext($context, $target_entity, $host_field_name); - $entity = EntityResource::getAccessCheckedEntity($target_entity); + $translation = EntityResource::getTranslatedEntity($target_entity, Request::create('/whatever/?lang_code=' . $context['langcode'])); + $entity = EntityResource::getAccessCheckedEntity($translation); $included_normalizer_value = $this->serializer->normalize(new JsonApiDocumentTopLevel($entity), $format, $context); } else { diff --git a/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php index 0002cc5..bb00fbd 100644 --- a/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php +++ b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php @@ -91,6 +91,7 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface, $this->isErrorsDocument = !empty($context['is_error_document']); if (!$this->isErrorsDocument) { + $this->addCacheContexts(['url.query_args:lang_code']); // @todo Make this unconditional in https://www.drupal.org/project/jsonapi/issues/2965056. if (!$context['request']->get('_on_relationship')) { // Make sure that different sparse fieldsets are cached differently. diff --git a/src/ParamConverter/EntityUuidConverter.php b/src/ParamConverter/EntityUuidConverter.php index 27a8eb6..47cb67a 100644 --- a/src/ParamConverter/EntityUuidConverter.php +++ b/src/ParamConverter/EntityUuidConverter.php @@ -29,11 +29,6 @@ class EntityUuidConverter extends EntityConverter { return NULL; } $entity = reset($entities); - // If the entity type is translatable, ensure we return the proper - // translation object for the current context. - if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) { - $entity = $this->entityManager->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']); - } return $entity; } return NULL; diff --git a/tests/src/Functional/JsonApiFunctionalMultilingualTest.php b/tests/src/Functional/JsonApiFunctionalMultilingualTest.php index 7b9f63f..c16754e 100644 --- a/tests/src/Functional/JsonApiFunctionalMultilingualTest.php +++ b/tests/src/Functional/JsonApiFunctionalMultilingualTest.php @@ -50,7 +50,7 @@ class JsonApiFunctionalMultilingualTest extends JsonApiFunctionalTestBase { $this->createDefaultContent(5, 5, TRUE, TRUE, static::IS_MULTILINGUAL, FALSE); // Test reading an individual entity. - $output = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $this->nodes[0]->uuid(), ['query' => ['include' => 'field_tags,field_image']])); + $output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $this->nodes[0]->uuid(), ['query' => ['lang_code' => 'ca', 'include' => 'field_tags,field_image']])); $this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data']['attributes']['title']); $included_tags = array_filter($output['included'], function ($entry) { @@ -61,24 +61,24 @@ class JsonApiFunctionalMultilingualTest extends JsonApiFunctionalTestBase { $this->assertEquals($tag_name, reset($included_tags)['attributes']['name']); $this->assertSame('alt text (ca)', $output['data']['relationships']['field_image']['data']['meta']['alt']); - $output = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $this->nodes[0]->uuid())); + $output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $this->nodes[0]->uuid(), ['query' => ['lang_code' => 'ca']])); $this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data']['attributes']['title']); // Test reading a collection of entities. - $output = Json::decode($this->drupalGet('/ca/jsonapi/node/article')); + $output = Json::decode($this->drupalGet('/jsonapi/node/article', ['query' => ['lang_code' => 'ca']])); $this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data'][0]['attributes']['title']); // Test reading a relationship. $output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $this->nodes[0]->uuid() . '/relationships/field_image')); $this->assertSame('alt text', $output['data']['meta']['alt']); - $output = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $this->nodes[0]->uuid() . '/relationships/field_image')); + $output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $this->nodes[0]->uuid() . '/relationships/field_image', ['query' => ['lang_code' => 'ca']])); $this->assertSame('alt text (ca)', $output['data']['meta']['alt']); // Test reading a related resource. $first_tag_name = $this->nodes[0]->get('field_tags')[0]->entity->getUntranslated()->getName(); $output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $this->nodes[0]->uuid() . '/field_tags')); $this->assertSame($first_tag_name, $output['data'][0]['attributes']['name']); - $output = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $this->nodes[0]->uuid() . '/field_tags')); + $output = Json::decode($this->drupalGet('/jsonapi/node/article/' . $this->nodes[0]->uuid() . '/field_tags', ['query' => ['lang_code' => 'ca']])); $this->assertSame("$first_tag_name (ca)", $output['data'][0]['attributes']['name']); }