Skip to content
Snippets Groups Projects
Commit a2072f19 authored by Al Munnings's avatar Al Munnings
Browse files

Issue #3432965: Available languages field for Nodes

parent 426b2169
No related branches found
No related tags found
No related merge requests found
Pipeline #139535 passed with warnings
Showing
with 615 additions and 25 deletions
......@@ -38,7 +38,7 @@ class View extends GraphQLComposeSchemaTypeBase {
$types[] = new InterfaceType([
'name' => $this->getPluginId(),
'description' => (string) $this->t('Views represent collections of curated data from the site.'),
'description' => (string) $this->t('Views represent collections of curated data from the CMS.'),
'fields' => fn() => [
'id' => [
'type' => Type::nonNull(Type::id()),
......
......@@ -11,13 +11,13 @@ use Drupal\graphql\GraphQL\ResolverRegistryInterface;
* Adds Schema Types defined by the GraphQL Compose plugin system.
*
* @SchemaExtension(
* id = "graphql_compose_type_schema_extension",
* name = "GraphQL Compose Types",
* id = "entity_schema_extension",
* name = "GraphQL Compose Entities",
* description = @Translation("GraphQL types defined by plugins."),
* schema = "graphql_compose",
* )
*/
class GraphQLComposeTypeSchemaExtension extends ResolverOnlySchemaExtensionPluginBase {
class EntitySchemaExtension extends ResolverOnlySchemaExtensionPluginBase {
/**
* {@inheritdoc}
......@@ -29,6 +29,13 @@ class GraphQLComposeTypeSchemaExtension extends ResolverOnlySchemaExtensionPlugi
foreach ($this->gqlEntityTypeManager->getPluginInstances() as $entity_type) {
$entity_type->registerResolvers($registry, $builder);
}
// Utility for junk.
$registry->addFieldResolver(
'UnsupportedType',
'unsupported',
$builder->callback(fn () => TRUE),
);
}
/**
......
......@@ -14,13 +14,13 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* The core schema without any entity types. Just config and utility.
*
* @SchemaExtension(
* id = "graphql_compose_schema_extension",
* name = "GraphQL Compose Schema Extension",
* id = "information_schema_extension",
* name = "GraphQL Compose Information",
* description = @Translation("Misc schema extensions for GraphQL Compose."),
* schema = "graphql_compose",
* )
*/
class GraphQLComposeSchemaExtension extends ResolverOnlySchemaExtensionPluginBase implements ContainerFactoryPluginInterface {
class InformationSchemaExtension extends ResolverOnlySchemaExtensionPluginBase implements ContainerFactoryPluginInterface {
/**
* The path alias manager.
......@@ -138,12 +138,6 @@ class GraphQLComposeSchemaExtension extends ResolverOnlySchemaExtensionPluginBas
);
}
// Utility for junk.
$registry->addFieldResolver(
'UnsupportedType',
'unsupported',
$builder->callback(fn () => TRUE),
);
}
}
<?php
declare(strict_types=1);
namespace Drupal\graphql_compose\Plugin\GraphQL\SchemaExtension;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\graphql\GraphQL\ResolverBuilder;
use Drupal\graphql\GraphQL\ResolverRegistryInterface;
/**
* Languages's are a common occurrence. Map LanguageInterface objects in schema.
*
* @SchemaExtension(
* id = "language_schema_extension",
* name = "GraphQL Compose Languages",
* description = @Translation("Add language support to schema."),
* schema = "graphql_compose",
* )
*/
class LanguageSchemaExtension extends ResolverOnlySchemaExtensionPluginBase implements ContainerFactoryPluginInterface {
/**
* {@inheritdoc}
*/
public function registerResolvers(ResolverRegistryInterface $registry): void {
$builder = new ResolverBuilder();
$registry->addFieldResolver(
'Language',
'id',
$builder->callback(fn (LanguageInterface $language) => $language->getId())
);
$registry->addFieldResolver(
'Language',
'name',
$builder->callback(fn (LanguageInterface $language) => $language->getName())
);
$registry->addFieldResolver(
'Language',
'direction',
$builder->callback(fn (LanguageInterface $language) => $language->getDirection())
);
// Add language resolvers.
$registry->addTypeResolver(
'Language',
fn($language) => $language instanceof LanguageInterface ? $language : NULL,
);
if ($this->languageManager->isMultilingual()) {
$registry->addFieldResolver(
'SchemaInformation',
'languages',
$builder->callback(fn () => $this->languageManager->getLanguages())
);
foreach ($this->gqlEntityTypeManager->getPluginInstances() as $entity_type) {
foreach ($entity_type->getBundles() as $bundle) {
if (!$bundle->isTranslatableContent()) {
continue;
}
$registry->addFieldResolver(
$bundle->getTypeSdl(),
'translations',
$builder->produce('entity_translations')
->map('entity', $builder->fromParent())
);
}
}
$registry->addFieldResolver(
'Translation',
'title',
$builder->produce('entity_label')
->map('entity', $builder->fromParent())
);
$registry->addFieldResolver(
'Translation',
'langcode',
$builder->produce('entity_language')
->map('entity', $builder->fromParent())
);
$registry->addFieldResolver(
'Translation',
'url',
$builder->compose(
$builder->produce('entity_url')
->map('entity', $builder->fromParent()),
$builder->produce('url_path')
->map('url', $builder->fromParent())
)
);
}
}
}
......@@ -8,6 +8,7 @@ use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\graphql\Plugin\SchemaExtensionPluginInterface;
......@@ -43,32 +44,35 @@ abstract class ResolverOnlySchemaExtensionPluginBase extends PluginBase implemen
* The plugin id.
* @param array $pluginDefinition
* The plugin definition array.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
* The entity field manager service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager service.
* @param \Drupal\graphql_compose\Plugin\GraphQLComposeEntityTypeManager $gqlEntityTypeManager
* The entity type plugin manager service.
* @param \Drupal\graphql_compose\Plugin\GraphQLComposeFieldTypeManager $gqlFieldTypeManager
* The field type plugin manager service.
* @param \Drupal\graphql_compose\Plugin\GraphQLComposeSchemaTypeManager $gqlSchemaTypeManager
* The schema type plugin manager service. *.
* The schema type plugin manager service.
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
* The language manager service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler service.
*/
public function __construct(
array $configuration,
$pluginId,
array $pluginDefinition,
protected ModuleHandlerInterface $moduleHandler,
protected ConfigFactoryInterface $configFactory,
protected EntityTypeManagerInterface $entityTypeManager,
protected EntityFieldManagerInterface $entityFieldManager,
protected EntityTypeManagerInterface $entityTypeManager,
protected GraphQLComposeEntityTypeManager $gqlEntityTypeManager,
protected GraphQLComposeFieldTypeManager $gqlFieldTypeManager,
protected GraphQLComposeSchemaTypeManager $gqlSchemaTypeManager,
protected LanguageManagerInterface $languageManager,
protected ModuleHandlerInterface $moduleHandler,
) {
parent::__construct($configuration, $pluginId, $pluginDefinition);
}
......@@ -81,13 +85,14 @@ abstract class ResolverOnlySchemaExtensionPluginBase extends PluginBase implemen
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler'),
$container->get('config.factory'),
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('entity_type.manager'),
$container->get('graphql_compose.entity_type_manager'),
$container->get('graphql_compose.field_type_manager'),
$container->get('graphql_compose.schema_type_manager'),
$container->get('language_manager'),
$container->get('module_handler'),
);
}
......
......@@ -4,10 +4,8 @@ declare(strict_types=1);
namespace Drupal\graphql_compose\Plugin\GraphQLCompose\FieldType;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\graphql\GraphQL\Execution\FieldContext;
use Drupal\graphql_compose\Plugin\GraphQL\DataProducer\FieldProducerItemInterface;
use Drupal\graphql_compose\Plugin\GraphQL\DataProducer\FieldProducerTrait;
use Drupal\graphql\GraphQL\Resolver\Composite;
use Drupal\graphql\GraphQL\ResolverBuilder;
use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeFieldTypeBase;
/**
......@@ -18,19 +16,16 @@ use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeFieldTypeBase;
* type_sdl = "Language",
* )
*/
class LanguageItem extends GraphQLComposeFieldTypeBase implements FieldProducerItemInterface {
use FieldProducerTrait;
class EntityLanguageItem extends GraphQLComposeFieldTypeBase {
/**
* {@inheritdoc}
*/
public function resolveFieldItem(FieldItemInterface $item, FieldContext $context) {
return [
'id' => $item->language->getId(),
'name' => $item->language->getName(),
'direction' => $item->language->getDirection(),
];
public function getProducers(ResolverBuilder $builder): Composite {
return $builder->compose(
$builder->produce('entity_language')
->map('entity', $builder->fromParent()),
);
}
}
......@@ -180,7 +180,7 @@ abstract class GraphQLComposeEntityTypeBase extends PluginBase implements GraphQ
}
/**
* Interfaces for the schema. Eg [Node, PageBanana].
* {@inheritdoc}
*/
public function getInterfaceTypeSdl(): string {
return u($this->getTypeSdl())
......
......@@ -101,6 +101,14 @@ interface GraphQLComposeEntityTypeInterface extends PluginInspectionInterface, D
*/
public function getTypeSdl(): string;
/**
* The interface type for the schema. EG NodeInterface.
*
* @return string
* The string used for the interface type.
*/
public function getInterfaceTypeSdl(): string;
/**
* Get common union name between entity bundles.
*
......
......@@ -45,4 +45,29 @@ class LanguageType extends GraphQLComposeSchemaTypeBase {
return $types;
}
/**
* {@inheritdoc}
*/
public function getExtensions(): array {
$extensions = parent::getExtensions();
if (!$this->languageManager->isMultilingual()) {
return $extensions;
}
$extensions[] = new ObjectType([
'name' => 'SchemaInformation',
'fields' => function () {
return [
'languages' => [
'type' => Type::nonNull(Type::listOf(Type::nonNull(static::type('Language')))),
'description' => (string) $this->t('List of languages available.'),
],
];
},
]);
return $extensions;
}
}
<?php
declare(strict_types=1);
namespace Drupal\graphql_compose\Plugin\GraphQLCompose\SchemaType;
use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeSchemaTypeBase;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
/**
* {@inheritdoc}
*
* @GraphQLComposeSchemaType(
* id = "Translation",
* )
*/
class TranslationType extends GraphQLComposeSchemaTypeBase {
/**
* {@inheritdoc}
*/
public function getTypes(): array {
$types = [];
if (!$this->languageManager->isMultilingual()) {
return $types;
}
$types[] = new ObjectType([
'name' => $this->getPluginId(),
'description' => (string) $this->t('Available translations for content.'),
'fields' => fn() => [
'title' => [
'type' => Type::string(),
'description' => (string) $this->t('The title of the translation.'),
],
'langcode' => [
'type' => Type::nonNull(static::type('Language')),
'description' => (string) $this->t('The language of the translation.'),
],
'url' => [
'type' => Type::string(),
'description' => (string) $this->t('The language name.'),
],
],
]);
return $types;
}
/**
* {@inheritdoc}
*/
public function getExtensions(): array {
$extensions = parent::getExtensions();
if (!$this->languageManager->isMultilingual()) {
return $extensions;
}
foreach ($this->gqlEntityTypeManager->getPluginInstances() as $entity_type) {
foreach ($entity_type->getBundles() as $bundle) {
if (!$bundle->isTranslatableContent()) {
continue;
}
$extensions[] = new ObjectType([
'name' => $bundle->getTypeSdl(),
'fields' => function () {
return [
'translations' => [
'type' => Type::nonNull(Type::listOf(Type::nonNull(static::type($this->getPluginId())))),
'description' => (string) $this->t('Available translations for content.'),
],
];
},
]);
}
}
return $extensions;
}
}
......@@ -9,10 +9,13 @@ use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\graphql_compose\LanguageInflector;
use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeEntityTypeInterface;
use Drupal\graphql_compose\Plugin\GraphQLComposeFieldTypeManager;
use Drupal\language\Entity\ContentLanguageSettings;
use function Symfony\Component\String\u;
/**
......@@ -57,6 +60,13 @@ class EntityTypeWrapper {
*/
public ConfigFactoryInterface $configFactory;
/**
* Drupal language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
public LanguageManagerInterface $languageManager;
/**
* Constructs a EntityTypeWrapper object.
*
......@@ -74,6 +84,7 @@ class EntityTypeWrapper {
$this->entityFieldManager = \Drupal::service('entity_field.manager');
$this->entityTypeManager = \Drupal::service('entity_type.manager');
$this->configFactory = \Drupal::service('config.factory');
$this->languageManager = \Drupal::service('language_manager');
}
/**
......@@ -157,8 +168,8 @@ class EntityTypeWrapper {
*/
public function getDescription(): ?string {
return method_exists($this->entity, 'getDescription')
? $this->entity->getDescription()
: NULL;
? $this->entity->getDescription()
: NULL;
}
/**
......@@ -185,6 +196,36 @@ class EntityTypeWrapper {
return (bool) $this->getSetting('query_load_enabled') ?: FALSE;
}
/**
* Check if the bundle has multiple and enabled translations.
*
* @return bool
* True if we would want to use this as a translation.
*/
public function isTranslatableContent(): bool {
if (!array_key_exists('path', $this->entityTypePlugin->getBaseFields())) {
return FALSE;
}
if (!$this->languageManager->isMultilingual()) {
return FALSE;
}
$entity_type = $base_type = $this->entity;
if ($entity_type instanceof ConfigEntityInterface) {
if ($bundle_of = $entity_type->getEntityType()->getBundleOf()) {
$base_type = $this->entityTypeManager->getDefinition($bundle_of);
}
}
$config = ContentLanguageSettings::loadByEntityTypeBundle(
$base_type->id(),
$entity_type->id()
);
return $config->isLanguageAlterable();
}
/**
* Get a config setting.
*
......
<?php
declare(strict_types=1);
namespace Drupal\Tests\graphql_compose\Functional\Core;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\node\NodeInterface;
use Drupal\system\Entity\Menu;
use Drupal\system\MenuInterface;
use Drupal\Tests\graphql_compose\Functional\GraphQLComposeBrowserTestBase;
use Drupal\Tests\language\Traits\LanguageTestTrait;
/**
* Test the entity languages are loading as expected.
*
* @group graphql_compose
*/
class EntityLanguageTest extends GraphQLComposeBrowserTestBase {
use LanguageTestTrait;
/**
* The test node.
*
* @var \Drupal\node\NodeInterface
*/
protected NodeInterface $node;
/**
* The test menu.
*
* @var \Drupal\system\MenuInterface
*/
protected MenuInterface $menu;
/**
* {@inheritdoc}
*/
protected static $modules = [
'graphql_compose_menus',
'graphql_compose_routes',
'content_translation',
'config_translation',
'menu_link_content',
'language',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType([
'type' => 'test',
'name' => 'Test node type',
]);
$this->createLanguageFromLangcode('ja');
$this->createLanguageFromLangcode('de');
$this->enableBundleTranslation('node', 'test');
$this->menu = Menu::create([
'id' => 'test',
'label' => 'Test Menu',
]);
$this->menu->save();
$this->node = $this->createNode([
'type' => 'test',
'title' => 'Test',
'status' => 1,
'promote' => 1,
'sticky' => 0,
'langcode' => 'en',
'path' => [
'alias' => '/test',
],
]);
$this->node->addTranslation('ja', [
'title' => 'テスト',
'path' => [
'alias' => '/test',
],
])->save();
$this->node->addTranslation('de', [
'title' => 'Testen',
'path' => [
'alias' => '/test',
],
])->save();
$this->setEntityConfig('menu', 'test', [
'enabled' => TRUE,
]);
$this->setEntityConfig('node', 'test', [
'enabled' => TRUE,
'query_load_enabled' => TRUE,
'routes_enabled' => TRUE,
]);
}
/**
* Test load entity by id.
*/
public function testNodeLoadByUuid(): void {
$query = <<<GQL
query {
default: node(id: "{$this->node->uuid()}") {
... on NodeInterface {
title
langcode {
id
}
}
}
en: node(id: "{$this->node->uuid()}", langcode: "en") {
... on NodeInterface {
title
langcode {
id
}
}
}
ja: node(id: "{$this->node->uuid()}", langcode: "ja") {
... on NodeInterface {
title
langcode {
id
}
}
}
de: node(id: "{$this->node->uuid()}", langcode: "de") {
... on NodeInterface {
title
langcode {
id
}
}
}
}
GQL;
$content = $this->executeQuery($query);
$default = $content['data']['default'];
$this->assertEquals('Test', $default['title']);
$this->assertEquals('en', $default['langcode']['id']);
$en = $content['data']['en'];
$this->assertEquals('Test', $en['title']);
$this->assertEquals('en', $en['langcode']['id']);
$ja = $content['data']['ja'];
$this->assertEquals('テスト', $ja['title']);
$this->assertEquals('ja', $ja['langcode']['id']);
$de = $content['data']['de'];
$this->assertEquals('Testen', $de['title']);
$this->assertEquals('de', $de['langcode']['id']);
}
/**
* Test load entity by route (language).
*/
public function testRouteLoadWithLangcode(): void {
$query = <<<GQL
query {
en: route(path: "/test", langcode: "en") {
... on RouteInternal {
entity {
... on NodeInterface {
title
langcode {
id
}
}
}
}
}
ja: route(path: "/test", langcode: "ja") {
... on RouteInternal {
entity {
... on NodeInterface {
title
langcode {
id
}
}
}
}
}
}
GQL;
$content = $this->executeQuery($query);
$this->assertEquals('Test', $content['data']['en']['entity']['title']);
$this->assertEquals('en', $content['data']['en']['entity']['langcode']['id']);
$this->assertEquals('テスト', $content['data']['ja']['entity']['title']);
$this->assertEquals('ja', $content['data']['ja']['entity']['langcode']['id']);
}
/**
* Test load a menu by name with langcode.
*/
public function testMenuLoadWithLangcode(): void {
$link = MenuLinkContent::create([
'title' => 'Test link',
'link' => ['uri' => 'entity:node/' . $this->node->id()],
'menu_name' => 'test',
'langcode' => 'en',
'default_langcode' => TRUE,
]);
$link->save();
$link->addTranslation('ja', [
'title' => 'テストリンク',
])->save();
$link->addTranslation('de', [
'title' => 'Testen Link',
])->save();
// Langcode on menu will change the entire response.
// Each menu needs to be requested separately.
$query = <<<GQL
query {
menu(name: TEST, langcode: "en") {
items {
title
url
}
}
}
GQL;
$content = $this->executeQuery($query);
$this->assertEquals('Test link', $content['data']['menu']['items'][0]['title']);
// JP.
$query = <<<GQL
query {
menu(name: TEST, langcode: "ja") {
items {
title
url
}
}
}
GQL;
$content = $this->executeQuery($query);
$this->assertEquals('テストリンク', $content['data']['menu']['items'][0]['title']);
// DE.
$query = <<<GQL
query {
menu(name: TEST, langcode: "de") {
items {
title
url
}
}
}
GQL;
$content = $this->executeQuery($query);
$this->assertEquals('Testen Link', $content['data']['menu']['items'][0]['title']);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment