diff --git a/core/modules/jsonapi/jsonapi.services.yml b/core/modules/jsonapi/jsonapi.services.yml index e54be7f2..2aed623d 100644 --- a/core/modules/jsonapi/jsonapi.services.yml +++ b/core/modules/jsonapi/jsonapi.services.yml @@ -186,6 +186,7 @@ services: - '@jsonapi.serializer' - '@datetime.time' - '@current_user' + - '@language_manager' jsonapi.file_upload: class: Drupal\jsonapi\Controller\FileUpload arguments: diff --git a/core/modules/jsonapi/src/Controller/EntityResource.php b/core/modules/jsonapi/src/Controller/EntityResource.php index 20d9c06d..9a7e0cfe 100644 --- a/core/modules/jsonapi/src/Controller/EntityResource.php +++ b/core/modules/jsonapi/src/Controller/EntityResource.php @@ -9,10 +9,12 @@ use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableResponseInterface; use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\Query\QueryInterface; @@ -22,6 +24,8 @@ use Drupal\Core\Entity\RevisionableStorageInterface; use Drupal\Core\Entity\RevisionLogInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Render\RenderContext; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; @@ -151,6 +155,13 @@ class EntityResource { */ protected $user; + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + /** * Instantiates an EntityResource object. * @@ -176,8 +187,10 @@ class EntityResource { * The time service. * @param \Drupal\Core\Session\AccountInterface $user * The current user account. + * @param \Drupal\Core\Language\LanguageManagerInterface|null $language_manager + * The language manager service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, ResourceTypeRepositoryInterface $resource_type_repository, RendererInterface $renderer, EntityRepositoryInterface $entity_repository, IncludeResolver $include_resolver, EntityAccessChecker $entity_access_checker, FieldResolver $field_resolver, SerializerInterface $serializer, TimeInterface $time, AccountInterface $user) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, ResourceTypeRepositoryInterface $resource_type_repository, RendererInterface $renderer, EntityRepositoryInterface $entity_repository, IncludeResolver $include_resolver, EntityAccessChecker $entity_access_checker, FieldResolver $field_resolver, SerializerInterface $serializer, TimeInterface $time, AccountInterface $user, LanguageManagerInterface $language_manager = NULL) { $this->entityTypeManager = $entity_type_manager; $this->fieldManager = $field_manager; $this->resourceTypeRepository = $resource_type_repository; @@ -189,6 +202,11 @@ class EntityResource { $this->serializer = $serializer; $this->time = $time; $this->user = $user; + if ($language_manager === NULL) { + @trigger_error('The language_manager service must be passed to ' . __NAMESPACE__ . '\EntityResource::__construct(). It was added in drupal:10.1.x and will be required before drupal:11.0.0. See https://www.drupal.org/node/3357049', E_USER_DEPRECATED); + $language_manager = \Drupal::service('language_manager'); + } + $this->languageManager = $language_manager; } /** @@ -902,7 +920,12 @@ class EntityResource { foreach ($sort->fields() as $field) { $path = $this->fieldResolver->resolveInternalEntityQueryPath($resource_type, $field[Sort::PATH_KEY]); $direction = $field[Sort::DIRECTION_KEY] ?? 'ASC'; - $langcode = $field[Sort::LANGUAGE_KEY] ?? NULL; + assert($entity_type instanceof EntityTypeInterface); + $langcode = NULL; + if ($entity_type->isTranslatable()) { + $language_type = $entity_type instanceof ContentEntityTypeInterface ? LanguageInterface::TYPE_CONTENT : LanguageInterface::TYPE_INTERFACE; + $langcode = $field[Sort::LANGUAGE_KEY] ?? $this->languageManager->getCurrentLanguage($language_type)->getId(); + } $query->sort($path, $direction, $langcode); } } diff --git a/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalMultilingualTest.php b/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalMultilingualTest.php index abe0a73d..80d07f4d 100644 --- a/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalMultilingualTest.php +++ b/core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalMultilingualTest.php @@ -57,14 +57,13 @@ class JsonApiFunctionalMultilingualTest extends JsonApiFunctionalTestBase { ]) ->setThirdPartySetting('content_translation', 'enabled', TRUE) ->save(); - - $this->createDefaultContent(5, 5, TRUE, TRUE, static::IS_MULTILINGUAL, FALSE); } /** * Tests reading multilingual content. */ public function testReadMultilingual() { + $this->createDefaultContent(5, 5, TRUE, TRUE, static::IS_MULTILINGUAL, FALSE); // Different databases have different sort orders, so a sort is required so // test expectations do not need to vary per database. $default_sort = ['sort' => 'drupal_internal__nid']; @@ -98,6 +97,7 @@ class JsonApiFunctionalMultilingualTest extends JsonApiFunctionalTestBase { * Tests updating a translation. */ public function testPatchTranslation() { + $this->createDefaultContent(5, 5, TRUE, TRUE, static::IS_MULTILINGUAL, FALSE); $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); $node = $this->nodes[0]; $uuid = $node->uuid(); @@ -197,6 +197,7 @@ class JsonApiFunctionalMultilingualTest extends JsonApiFunctionalTestBase { * Tests updating a translation fallback. */ public function testPatchTranslationFallback() { + $this->createDefaultContent(5, 5, TRUE, TRUE, static::IS_MULTILINGUAL, FALSE); $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); $node = $this->nodes[0]; $uuid = $node->uuid(); @@ -238,6 +239,7 @@ class JsonApiFunctionalMultilingualTest extends JsonApiFunctionalTestBase { * Tests creating a translation. */ public function testPostTranslation() { + $this->createDefaultContent(5, 5, TRUE, TRUE, static::IS_MULTILINGUAL, FALSE); $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); $this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), [ 'bypass node access', @@ -302,6 +304,7 @@ class JsonApiFunctionalMultilingualTest extends JsonApiFunctionalTestBase { * Tests deleting multilingual content. */ public function testDeleteMultilingual() { + $this->createDefaultContent(5, 5, TRUE, TRUE, static::IS_MULTILINGUAL, FALSE); $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE); $this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), [ 'bypass node access', @@ -322,4 +325,59 @@ class JsonApiFunctionalMultilingualTest extends JsonApiFunctionalTestBase { $this->assertNull(Node::load($this->nodes[0]->id())); } + /** + * Tests multilingual JSON:API calls. + */ + public function testMultilingualGet() { + $titles = [ + [ + 'en' => 'Apple', + 'ca' => 'Z-Apple', + ], + [ + 'en' => 'Blackberry', + 'ca' => 'Y-Blackberry', + ], + [ + 'en' => 'Google', + 'ca' => 'X-Google', + ], + [ + 'en' => 'Motorola', + 'ca' => 'W-Motorola', + ], + ]; + + $expected_english_order = ['Apple', 'Blackberry', 'Google', 'Motorola']; + $expected_ca_order = ['W-Motorola', 'X-Google', 'Y-Blackberry', 'Z-Apple']; + foreach ($titles as $title) { + $node = Node::create(['title' => $title['en'], 'type' => 'article', 'langcode' => 'en']); + $node->addTranslation('ca', ['title' => $title['ca']]); + $node->save(); + } + + $output = Json::decode($this->drupalGet('/jsonapi/node/article', [ + 'query' => [ + 'sort' => 'title', + ], + ])); + $output_titles = array_map(function ($result) { + return $result['attributes']['title']; + }, $output['data']); + $this->assertCount(4, $output_titles); + $this->assertSame($expected_english_order, $output_titles); + + // Check the nodes with the langcode url. + $output = Json::decode($this->drupalGet('/ca/jsonapi/node/article', [ + 'query' => [ + 'sort' => 'title', + ], + ])); + $output_titles = array_map(function ($result) { + return $result['attributes']['title']; + }, $output['data']); + $this->assertCount(4, $output_titles); + $this->assertSame($expected_ca_order, $output_titles); + } + } diff --git a/core/modules/jsonapi/tests/src/Kernel/Controller/EntityResourceTest.php b/core/modules/jsonapi/tests/src/Kernel/Controller/EntityResourceTest.php index c2f41792..3faec834 100644 --- a/core/modules/jsonapi/tests/src/Kernel/Controller/EntityResourceTest.php +++ b/core/modules/jsonapi/tests/src/Kernel/Controller/EntityResourceTest.php @@ -2,11 +2,22 @@ namespace Drupal\Tests\jsonapi\Kernel\Controller; +use Drupal\Component\Datetime\TimeInterface; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityRepositoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\jsonapi\Access\EntityAccessChecker; use Drupal\jsonapi\CacheableResourceResponse; +use Drupal\jsonapi\Context\FieldResolver; +use Drupal\jsonapi\Controller\EntityResource; +use Drupal\jsonapi\IncludeResolver; use Drupal\jsonapi\ResourceType\ResourceType; use Drupal\jsonapi\JsonApiResource\Data; use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; +use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase; @@ -15,6 +26,7 @@ use Drupal\user\Entity\User; use Drupal\user\RoleInterface; use Symfony\Component\HttpFoundation\InputBag; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\SerializerInterface; /** * @coversDefaultClass \Drupal\jsonapi\Controller\EntityResource @@ -237,4 +249,25 @@ class EntityResourceTest extends JsonapiKernelTestBase { $this->assertEquals(['node_list'], $response->getCacheableMetadata()->getCacheTags()); } + /** + * Test the language_manager argument deprecation. + * + * @group legacy + */ + public function testEntityResourceNewParameterDeprecation() { + $this->expectDeprecation('The language_manager service must be passed to ' . EntityResource::class . '::__construct(). It was added in drupal:10.1.x and will be required before drupal:11.0.0. See https://www.drupal.org/node/3357049'); + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $field_manager = $this->prophesize(EntityFieldManagerInterface::class); + $resource_type_repository = $this->prophesize(ResourceTypeRepositoryInterface::class); + $module_renderer_handler = $this->prophesize(RendererInterface::class); + $entity_repository = $this->prophesize(EntityRepositoryInterface::class); + $include_resolver = $this->prophesize(IncludeResolver::class); + $entity_access_checker = $this->prophesize(EntityAccessChecker::class); + $field_resolver = $this->prophesize(FieldResolver::class); + $serializer = $this->prophesize(SerializerInterface::class); + $time = $this->prophesize(TimeInterface::class); + $user = $this->prophesize(AccountInterface::class); + $this->assertNotNull(new EntityResource($entity_type_manager->reveal(), $field_manager->reveal(), $resource_type_repository->reveal(), $module_renderer_handler->reveal(), $entity_repository->reveal(), $include_resolver->reveal(), $entity_access_checker->reveal(), $field_resolver->reveal(), $serializer->reveal(), $time->reveal(), $user->reveal())); + } + }