graphql_core_schema-1.0.x-dev/src/EntitySchemaBuilder.php
src/EntitySchemaBuilder.php
<?php
namespace Drupal\graphql_core_schema;
use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem;
use Drupal\Core\Field\Plugin\Field\FieldType\EmailItem;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Field\Plugin\Field\FieldType\IntegerItem;
use Drupal\Core\Field\Plugin\Field\FieldType\LanguageItem;
use Drupal\Core\Field\Plugin\Field\FieldType\MapItem;
use Drupal\Core\Field\Plugin\Field\FieldType\NumericItemBase;
use Drupal\Core\Field\Plugin\Field\FieldType\StringItem;
use Drupal\Core\Field\Plugin\Field\FieldType\StringItemBase;
use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\Plugin\DataType\StringData;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
use Drupal\file\Plugin\Field\FieldType\FileItem;
use Drupal\graphql_core_schema\Event\AlterEntityFieldEvent;
use Drupal\graphql_core_schema\SchemaBuilder\SchemaBuilderField;
use Drupal\graphql_core_schema\SchemaBuilder\SchemaBuilderRegistry;
use Drupal\options\Plugin\Field\FieldType\ListStringItem;
use Drupal\text\Plugin\Field\FieldType\TextItemBase;
use Drupal\text\Plugin\Field\FieldType\TextWithSummaryItem;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* The EntitySchemaBuilder class.
*/
class EntitySchemaBuilder {
/**
* The placeholder name to use for unexposed bundles.
*/
public const UNEXPOSED_BUNDLE_NAME = 'unexposed';
/**
* Types that should never be generated.
*
* @var string[]
*/
const EXCLUDED_TYPES = [
'password',
'_core_config_info',
];
/**
* Fields that are not resolved by the default field resolver.
*
* These fields exist on all entities and do not need to be resolved.
*
* @var string[]
*/
const EXCLUDED_ENTITY_FIELDS = [
'id',
'uuid',
'label',
'langcode',
];
/**
* The entity type manager.
*/
protected EntityTypeManager|NULL $entityTypeManager = NULL;
/**
* The entity field manager.
*/
protected EntityFieldManagerInterface|NULL $entityFieldManager = NULL;
/**
* The entity type bundle info service.
*/
protected EntityTypeBundleInfoInterface|NULL $entityTypeBundleInfo = NULL;
/**
* The type data manager.
*/
protected TypedDataManagerInterface|NULL $typedDataManager = NULL;
/**
* The typed config manager.
*/
protected TypedConfigManagerInterface|NULL $typedConfigManager = NULL;
public function __construct(
protected SchemaBuilderRegistry $registry,
protected CoreComposableConfig $config,
protected ModuleHandlerInterface $moduleHandler,
protected array $schemaConfiguration,
protected EventDispatcherInterface $dispatcher,
) {}
/**
* Get the entity type manager.
*
* @return EntityTypeManager
* The entity type manager.
*/
private function getEntityTypeManager(): EntityTypeManager {
if (empty($this->entityTypeManager)) {
$this->entityTypeManager = \Drupal::service('entity_type.manager');
}
return $this->entityTypeManager;
}
/**
* Get the entity field manager.
*
* @return EntityFieldManagerInterface
* The entity field manager.
*/
private function getEntityFieldManager(): EntityFieldManagerInterface {
if (empty($this->entityFieldManager)) {
$this->entityFieldManager = \Drupal::service('entity_field.manager');
}
return $this->entityFieldManager;
}
/**
* Get the entity type bundle info service.
*
* @return EntityTypeBundleInfoInterface
* The entity type bundle info service.
*/
private function getEntityTypeBundleInfo(): EntityTypeBundleInfoInterface {
if (empty($this->entityTypeBundleInfo)) {
$this->entityTypeBundleInfo = \Drupal::service('entity_type.bundle.info');
}
return $this->entityTypeBundleInfo;
}
/**
* Get the typed data manager.
*
* @return \Drupal\Core\TypedData\TypedDataManagerInterface
* The typed data manager.
*/
private function getTypedDataManager(): TypedDataManagerInterface {
if (empty($this->typedDataManager)) {
$this->typedDataManager = \Drupal::service('typed_data_manager');
}
return $this->typedDataManager;
}
/**
* Get the typed config manager.
*
* @return \Drupal\Core\Config\TypedConfigManagerInterface
* The typed config manager.
*/
private function getTypedConfigManager(): TypedConfigManagerInterface {
if (empty($this->typedConfigManager)) {
$this->typedConfigManager = \Drupal::service('config.typed');
}
return $this->typedConfigManager;
}
/**
* Get the schema mapping for a config entity type.
*
* @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $type
* The config entity type.
*
* @return array
* The schema mapping.
*/
private function getConfigEntityMapping(ConfigEntityTypeInterface $type): array {
$configPrefix = $type->getConfigPrefix();
$typedConfigDefinition = $this->getTypedConfigManager()->getDefinition($configPrefix . '.*');
$mapping = $typedConfigDefinition['mapping'] ?? [];
if (empty($mapping)) {
$typedConfigDefinition = $this->getTypedConfigManager()->getDefinition($configPrefix . '.*.*');
$mapping = $typedConfigDefinition['mapping'] ?? [];
}
if (empty($mapping)) {
$typedConfigDefinition = $this->getTypedConfigManager()->getDefinition($configPrefix . '.*.*.*');
$mapping = $typedConfigDefinition['mapping'] ?? [];
}
return $mapping;
}
/**
* Create a field.
*/
public function createField(string $name): SchemaBuilderField {
return new SchemaBuilderField($name);
}
/**
* Get the GraphQL field definition for an entity value field.
*
* This will return a type for value fields, where instead of the entire
* field the direct field value is resolved. For example a simple text
* field will directly resolve to a string scalar.
*
* If no appropriate scalar is found the type for the field item is returned.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $fieldDefinition
* The field definition.
*
* @return \Drupal\graphql_core_schema\SchemaBuilder\SchemaBuilderField|null
* The GraphQL type if found.
*/
private function buildGraphqlValueField(FieldDefinitionInterface $fieldDefinition): SchemaBuilderField|NULL {
$description = (string) $fieldDefinition->getDescription();
$fieldName = $fieldDefinition->getName();
if (!$description) {
$description = (string) $fieldDefinition->getFieldStorageDefinition()->getDescription();
}
$storageDefinition = $fieldDefinition->getFieldStorageDefinition();
$isMultiple = $storageDefinition->isMultiple();
// Create a field item we can use to determine what scalar this should
// resolve to.
$itemDefinition = $fieldDefinition->getItemDefinition();
$typedData = $this->getTypedDataManager()->create($itemDefinition);
if ($fieldName === 'metatag' && $typedData instanceof MapItem) {
return NULL;
}
$valueFieldName = EntitySchemaHelper::toCamelCase($fieldName);
$field = $this->createField($valueFieldName)
->description($description)
->valueField()
->machineName($fieldName);
if ($isMultiple) {
$field->list();
}
$fieldType = $fieldDefinition->getType();
if (
$typedData instanceof StringItem ||
$typedData instanceof StringItemBase ||
$typedData instanceof EmailItem ||
$typedData instanceof ListStringItem ||
$fieldType === 'telephone'
) {
return $field->type('String');
}
elseif ($typedData instanceof LanguageItem) {
return $field->type('LanguageInterface');
}
elseif ($typedData instanceof IntegerItem) {
return $field->type('Int');
}
elseif ($typedData instanceof NumericItemBase) {
return $field->type('Float');
}
elseif ($typedData instanceof BooleanItem) {
return $field->type('Boolean');
}
elseif ($typedData instanceof EntityReferenceItem && !$typedData instanceof FileItem) {
$type = $this->getTypeForEntityReferenceFieldItem($itemDefinition, $isMultiple);
if ($type) {
return $field->type($type);
}
// The entity type that is referenced is not enabled, so we don't output
// this field at all.
return NULL;
}
elseif ($fieldType === 'dynamic_entity_reference') {
return $field->type('Entity');
}
elseif ($typedData instanceof TextWithSummaryItem) {
$summary = $this->createField('summary')->type('Boolean');
return $field->type('String')->argument($summary);
}
elseif ($typedData instanceof TextItemBase) {
return $field->type('String');
}
elseif ($typedData instanceof TimestampItem) {
return $field->type('String');
}
elseif ($typedData instanceof MapItem) {
return $field->type('MapData');
}
// The field type is not scalar, try to get the GraphQL type for this item
// definition.
$itemType = $this->getFieldItemType($itemDefinition);
if ($itemType) {
return $field->type($itemType);
}
return NULL;
}
/**
* Generate the GraphQL type for an entity reference field item.
*
* @param \Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface $itemDefinition
* The field definition.
*
* @return string|null
* The name of the referenced type.
*/
private function getTypeForEntityReferenceFieldItem(FieldItemDataDefinitionInterface $itemDefinition): ?string {
$targetType = $itemDefinition->getSetting('target_type');
$dataType = $itemDefinition->getDataType();
// Special handling for the "dynamic_entity_reference" contrib module that
// allows referencing multiple different entity types in a field.
// Because we don't (yet) support union types, we just type these fields
// using the generic Entity interface.
if ($dataType === 'field_item:dynamic_entity_reference') {
return 'Entity';
}
// Check if the target entity type is enabled.
if (!$targetType || !$this->config->isEntityTypeEnabled($targetType)) {
return NULL;
}
// Get the target bundles that can be referenced. This value is a bit
// random, either a string or an array.
$handlerSettings = $itemDefinition->getSetting('handler_settings') ?? [];
$targetBundles = $handlerSettings['target_bundles'] ?? [];
if (is_string($targetBundles)) {
$targetBundles = [$targetBundles];
}
$targetBundles = array_values($targetBundles);
// Try to return the GraphQL type of a specific bundle (e.g.
// TaxonomyTermTag). This is only possible if:
// - Only one bundle can be referenced
// - The single bundle's name is different than the entity type name.
// This is the case for entity types without bundles, for which we don't
// create an interface (e.g. "User" exists only as a type, not an
// interface).
// - The bundle is enabled in the schema configuration.
if (
count($targetBundles) === 1 &&
$targetType !== $targetBundles[0] &&
$this->config->isBundleEnabled($targetType, $targetBundles[0])
) {
return $this->config->getBundleTypeName($targetType, $targetBundles[0]);
}
// If no specific bundle type could be returned, we use just the
// entity type (e.g. "TaxonomyTerm").
return EntitySchemaHelper::toPascalCase([$targetType]);
}
/**
* Build the FieldItemList type for a field type.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $fieldDefinition
* The field definition.
*
* @return string|null
* The GraphQL type.
*/
private function getFieldItemListType(FieldDefinitionInterface $fieldDefinition): string|NULL {
// e.g. string, boolean, email, string_long, text_with_summary.
$fieldTypeName = $fieldDefinition->getType();
// e.g. FieldItemListEmail.
$graphqlTypeName = EntitySchemaHelper::toPascalCase([
'field_item_list_',
$fieldTypeName,
]);
// Type was already generated.
if ($this->registry->typeWillExist($graphqlTypeName)) {
return $graphqlTypeName;
}
$type = $this->registry
->createType($graphqlTypeName)
->description((string) $fieldDefinition->getLabel())
->addInterface('FieldItemList');
$itemDefinition = $fieldDefinition->getItemDefinition();
if ($itemDefinition instanceof FieldItemDataDefinition) {
$fieldItemType = $this->getFieldItemType($itemDefinition);
if ($fieldItemType) {
$list = $this->createField('list')->type($fieldItemType)->description('Array of field items.')->list();
$type->addField($list);
$first = $this->createField('first')->type($fieldItemType)->description('The first field item.');
$type->addField($first);
}
}
return $graphqlTypeName;
}
/**
* Check if the given entity reference field should be added.
*/
private function shouldAddField(string $fieldName, FieldDefinitionInterface $definition) {
if (in_array($fieldName, self::EXCLUDED_ENTITY_FIELDS)) {
return FALSE;
}
$fieldType = $definition->getType();
if (in_array($fieldType, self::EXCLUDED_TYPES)) {
return FALSE;
}
if ($fieldType === 'entity_reference' || $fieldType === 'entity_reference_revisions') {
$targetType = $definition->getSetting('target_type');
return $this->config->isEntityTypeEnabled($targetType);
}
return TRUE;
}
/**
* Build the type for a field item definition.
*
* @param \Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface $itemDefinition
* The field item data definition.
*
* @return string|null
* The GraphQL type if available.
*/
private function getFieldItemType(FieldItemDataDefinitionInterface $itemDefinition): string|NULL {
$fieldDefinition = $itemDefinition->getFieldDefinition();
// The type, e.g. string, text_with_summary, email, telephone.
$fieldType = $fieldDefinition->getType();
// e.g. FielditemTypeTextWithSummary.
$graphqlDataTypeName = EntitySchemaHelper::toPascalCase(
['field_item_type_', $fieldType]
);
// Type has already been generated.
if ($this->registry->typeWillExist($graphqlDataTypeName)) {
return $graphqlDataTypeName;
}
$type = $this->registry
->createType($graphqlDataTypeName)
->addInterface('FieldItemType')
->description((string) $itemDefinition->getLabel());
$propertyDefinitions = $itemDefinition->getPropertyDefinitions();
// Is set to TRUE if there is a "value" property of type "string".
$hasStringValue = FALSE;
// Is set to TRUE if there is a "value" property of type "integer".
$hasIntegerValue = FALSE;
foreach ($propertyDefinitions as $name => $propertyDefinition) {
if ($propertyDefinition instanceof DataDefinition) {
$propertyFieldType = $this->getDataPropertyType($propertyDefinition->toArray());
// We have to explicitly override the GraphQL type for these two field
// types, because the determined type can be random. It depends on
// which entity field first triggers generating the type for this
// field. For example, it could be the "uid" base field on nodes,
// which would make the type of the "entity" property be "User", which
// is obviously wrong. This is why we explicitly override it to be
// "Entity".
if (
$name === 'entity' &&
($fieldType === 'entity_reference' || $fieldType === 'entity_reference_revisions')
) {
$propertyFieldType = 'Entity';
}
// Field item types that share the same value field with the same type
// all get an additional interface.
if ($name === 'value') {
if ($propertyFieldType === 'String') {
$hasStringValue = TRUE;
}
elseif ($propertyFieldType === 'Int') {
$hasIntegerValue = TRUE;
}
}
if ($propertyFieldType) {
$propertyFieldName = EntitySchemaHelper::toCamelCase($name);
$description = (string) $propertyDefinition->getLabel();
$field = $this
->createField($propertyFieldName)
->type($propertyFieldType)
->machineName($name)
->description($description);
$type->addField($field);
}
}
}
// Field item types are always generated, even if no fields have been
// derived. This is so that schema extensions can easily extend these types
// by implementing missing fields.
$typedData = $this->getTypedDataManager()->create($itemDefinition);
// Add additional interfaces for certain field item types.
// Interface for timestamp/date field items.
if ($typedData instanceof TimestampItem || $typedData instanceof DateTimeItem) {
$type->addInterface('FieldItemTypeTimestampInterface');
}
// Interface for field item types whose "value" field is a string.
if ($hasStringValue) {
$type->addInterface('FieldItemTypeStringInterface');
}
// Interface for field item types whose "value" field is an integer.
if ($hasIntegerValue) {
$type->addInterface('FieldItemTypeIntegerInterface');
}
return $graphqlDataTypeName;
}
/**
* Add types for the entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entityType
* The entity type.
* @param \Drupal\Core\Field\FieldDefinitionInterface[] $fieldDefinitions
* The base field definitions of the entity type.
*
* @return string
* The name of the generated type or interface.
*/
public function addContentEntityType(EntityTypeInterface $entityType, array $fieldDefinitions): string {
$typeName = EntitySchemaHelper::toPascalCase([$entityType->id()]);
if ($this->registry->typeWillExist($typeName)) {
return $typeName;
}
$this->registry->addGeneratedTypeName($typeName);
$hasBundles = $entityType->hasKey('bundle');
$description = (string) $entityType->getLabel();
$interfaces = $this->getInterfacesForEntityType($entityType);
$fields = $this->createEntityFields($entityType, $fieldDefinitions);
if ($hasBundles) {
$this->registry->createOrExtendInterface($typeName, $description, $fields, $interfaces);
return $typeName;
}
$type = $this->registry->createType($typeName)->description($description);
foreach ($interfaces as $interface) {
$type->addInterface($interface);
}
foreach ($fields as $field) {
$type->addField($field);
}
return $typeName;
}
/**
* Generate a GraphQL type for an entity type.
*
* @param string $entityTypeId
* The entity type ID.
*
* @return string|null
* The generated GraphQL type.
*/
public function generateTypeForEntityType(string $entityTypeId): string|NULL {
// Don't generate types for disabled entity types.
if (!$this->config->isEntityTypeEnabled($entityTypeId)) {
return NULL;
}
$graphqlTypeName = EntitySchemaHelper::toPascalCase($entityTypeId);
if ($this->registry->typeWillExist($graphqlTypeName)) {
return $graphqlTypeName;
}
$entityType = $this->getEntityTypeManager()->getDefinition($entityTypeId);
if (!$entityType) {
return NULL;
}
if ($entityType instanceof ConfigEntityTypeInterface) {
$mapping = $this->getConfigEntityMapping($entityType);
return $this->addConfigEntityType($entityType, $mapping);
}
elseif ($entityType instanceof ContentEntityTypeInterface) {
$hasBundles = $entityType->hasKey('bundle');
$fieldDefinitions = $hasBundles
? $this->getEntityFieldManager()->getBaseFieldDefinitions($entityTypeId)
: $this->getEntityFieldManager()->getFieldDefinitions($entityTypeId, $entityTypeId);
$generatedType = $this->addContentEntityType($entityType, $fieldDefinitions);
if ($generatedType && $hasBundles) {
$bundles = $this->getEntityTypeBundleInfo()->getBundleInfo($entityTypeId);
foreach (array_keys($bundles) as $bundleId) {
$bundleFieldDefinitions = $this->getEntityFieldManager()->getFieldDefinitions($entityTypeId, $bundleId);
$this->addContentEntityBundleType($entityType, $bundleId, $bundles[$bundleId], $bundleFieldDefinitions);
}
}
return $generatedType;
}
return NULL;
}
/**
* Add types for the configuration entity type.
*
* @param \Drupal\Core\Config\Entity\ConfigEntityType $entityType
* The config entity type ID.
* @param array $mapping
* The schema mapping for the config entity.
*
* @return string|null
* The name of the generated GraphQL type.
*/
public function addConfigEntityType(ConfigEntityType $entityType, array $mapping): string {
$entityTypeId = $entityType->id();
$typeName = EntitySchemaHelper::toPascalCase([$entityTypeId]);
if ($this->registry->typeWillExist($typeName)) {
return $typeName;
}
$type = $this->registry->createType($typeName)->description($entityType->getLabel());
$type->addInterface('Entity');
$fields = [];
foreach ($mapping as $propertyName => $definition) {
$graphqlFieldName = EntitySchemaHelper::toCamelCase($propertyName);
if (in_array($propertyName, self::EXCLUDED_ENTITY_FIELDS)) {
continue;
}
if (!$this->config->fieldIsEnabled($entityTypeId, $propertyName)) {
continue;
}
$propertyType = $this->getDataPropertyType($definition);
if ($propertyType) {
if (!empty($fields[$graphqlFieldName])) {
$graphqlFieldName = $propertyName;
}
// Allow altering the schema.
$event = new AlterEntityFieldEvent(
gqlFieldMachineName: $propertyName,
gqlFieldSchemaType: $propertyType,
gqlFieldName: $graphqlFieldName,
entityType: $entityType,
schemaConfiguration: $this->schemaConfiguration,
fieldDefinition: $definition,
);
$this->dispatcher->dispatch(
$event,
AlterEntityFieldEvent::EVENT_NAME,
);
$fields[] = $event->getGqlFieldName();
$description = $event->getGqlFieldDescription();
$field = $this
->createField($event->getGqlFieldName())
->machineName($event->getGqlFieldMachineName())
->description($description ?? '')
->type($event->getGqlFieldSchemaType());
$type->addField($field);
}
}
if ($entityTypeId === 'configurable_language') {
$type->addInterface('LanguageInterface');
}
return $typeName;
}
/**
* Add types for the entity bundle type.
*
* @param \Drupal\Core\Entity\ContentEntityTypeInterface $entityType
* The entity type.
* @param string $bundleId
* The bundle ID.
* @param array $bundleInfo
* The bundle info.
* @param \Drupal\Core\Field\FieldDefinitionInterface[] $fieldDefinitions
* The field definitions of the bundle.
*/
public function addContentEntityBundleType(ContentEntityTypeInterface $entityType, string $bundleId, array $bundleInfo, array $fieldDefinitions) {
$entityTypeId = $entityType->id();
$entityTypeName = EntitySchemaHelper::toPascalCase([$entityTypeId]);
$bundleTypeName = $this->config->getBundleTypeName($entityTypeId, $bundleId);
if ($this->registry->typeWillExist($bundleTypeName)) {
return;
}
// If the bundle is not enabled, we want to expose a dummy type so that
// references to this type can be resolved.
$enabled = $this->config->isBundleEnabled($entityTypeId, $bundleId);
$descriptionBundle = $enabled ? $bundleId : self::UNEXPOSED_BUNDLE_NAME;
$description = [
(string) $bundleInfo['label'] ?? $entityTypeId,
"{entity_type: $entityTypeId, bundle: $descriptionBundle}",
];
$type = $this->registry->createType($bundleTypeName)->description(implode(' ', $description));
$type->addInterface($entityTypeName);
foreach ($this->getInterfacesForEntityType($entityType) as $interface) {
$type->addInterface($interface);
}
// If the bundle is not enabled, we do not need to expose the fields.
if (!$enabled) {
return;
}
foreach ($this->createEntityFields($entityType, $fieldDefinitions) as $field) {
$type->addField($field);
}
// Add the interface and fields for a translatable entity.
// We do this separately so that we can use the entity's
// type as the type for both fields.
if (!empty($bundleInfo['translatable'])) {
$type->addInterface('EntityTranslatable');
}
}
/**
* Get interfaces for the entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entityType
* The entity type.
*
* @return string[]
* The interfaces.
*/
public function getInterfacesForEntityType(EntityTypeInterface $entityType): array {
$pairs = [
'\Drupal\Core\Entity\EntityDescriptionInterface' => 'EntityDescribable',
];
$interfaces = [
'Entity',
];
foreach ($pairs as $dependency => $interface) {
if ($entityType->entityClassImplements($dependency)) {
$interfaces[] = $interface;
}
}
$isLinkable = !empty($entityType->getLinkTemplates());
if ($isLinkable) {
$interfaces[] = 'EntityLinkable';
}
if ($entityType->isRevisionable()) {
$interfaces[] = 'EntityRevisionable';
}
return $interfaces;
}
/**
* Merge fields from the given interfaces with the base fields.
*
* @param array $fields
* The type fields.
* @param \GraphQL\Type\Definition\InterfaceType[] $interfaces
* The interfaces.
*
* @return array
* The type fields merged with the interface fields.
*/
public function mergeInterfaceFields(array $fields, array $interfaces): array {
$mergedFields = $fields;
foreach ($interfaces as $interface) {
$mergedFields = array_merge($mergedFields, $interface->getFields());
}
return $mergedFields;
}
/**
* Create GraphQL fields given the entity field definitions.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entityType
* The entity type the fields belong to.
* @param \Drupal\Core\Field\FieldDefinitionInterface[] $fieldDefinitions
* The field definitions.
*
* @return SchemaBuilderField[]
* The array of GraphQL fields.
*/
private function createEntityFields(EntityTypeInterface $entityType, array $fieldDefinitions): array {
$fields = [];
foreach ($fieldDefinitions as $fieldName => $definition) {
if (!$this->config->fieldIsEnabled($entityType->id(), $fieldName)) {
continue;
}
if (!$this->shouldAddField($fieldName, $definition)) {
continue;
}
$type = $this->getFieldItemListType($definition);
if ($type) {
$graphqlFieldName = EntitySchemaHelper::toCamelCase(
[$fieldName, '_raw_field']
);
if (!empty($fields[$graphqlFieldName])) {
$graphqlFieldName = $fieldName . 'RawField';
}
$description = (string) $definition->getDescription();
// Allow altering the schema.
$event = new AlterEntityFieldEvent(
gqlFieldMachineName: $fieldName,
gqlFieldSchemaType: $type,
gqlFieldName: $graphqlFieldName,
entityType: $entityType,
schemaConfiguration: $this->schemaConfiguration,
dataDefinition: $definition,
gqlFieldDescription: $description
);
$this->dispatcher->dispatch(
$event,
AlterEntityFieldEvent::EVENT_NAME
);
$fields[$event->getGqlFieldName()] = $this->createField($event->getGqlFieldName())
->type($event->getGqlFieldSchemaType())
->description($event->getGqlFieldDescription())
->machineName($event->getGqlFieldMachineName());
}
if ($this->config->shouldGeneratedValueFields()) {
$valueField = $this->buildGraphqlValueField($definition);
if ($valueField) {
$valueFieldName = $valueField->getName();
// If there has already been a field created with this name there is
// a conflict between field names that have been camel cased. In
// this rare case we generate the field name using the actual Drupal
// machine name.
if (!empty($fields[$valueFieldName])) {
$valueFieldName = $fieldName;
}
// Allow altering the schema.
$event = new AlterEntityFieldEvent(
gqlFieldMachineName: $valueField->getMachineName(),
gqlFieldSchemaType: $valueField->type,
gqlFieldName: $valueFieldName,
entityType: $entityType,
schemaConfiguration: $this->schemaConfiguration,
dataDefinition: $definition,
gqlFieldDescription: $valueField->description,
);
$this->dispatcher->dispatch(
$event,
AlterEntityFieldEvent::EVENT_NAME
);
$description = $event->getGqlFieldDescription();
$valueField
->machineName($event->getGqlFieldMachineName())
->name($event->getGqlFieldName())
->type($event->getGqlFieldSchemaType())
->description($description ?? '');
$fields[$event->getGqlFieldName()] = $valueField;
}
}
}
return $fields;
}
/**
* Get the GraphQL type for a data property definition.
*
* This is the lowest possible leaf in the entity schema. It usually resolves
* to a scalar, but special handling is implemented for sequence types and
* entity reference types.
*
* @param array $definition
* The property definition.
*
* @return string|null
* The name of the GraphQL type.
*/
protected function getDataPropertyType(array $definition): string|NULL {
$type = $definition['type'];
if (in_array($type, self::EXCLUDED_TYPES)) {
return NULL;
}
// Basic types.
switch ($type) {
case 'string':
case 'email':
case 'text':
case 'label':
case 'path':
case 'color_hex':
case 'date_form':
case 'filter_format':
case 'datetime_iso8601':
case 'timestamp':
case 'required_label':
case 'machine_name':
return 'String';
case 'boolean':
return 'Boolean';
case 'integer':
return 'Int';
case 'float':
return 'Float';
case 'uri':
return 'Url';
case 'config_dependencies':
return 'MapData';
case 'dynamic_entity_reference':
return 'Entity';
case '_core_config_info':
return NULL;
case 'entity_reference':
case 'entity_revision_reference':
$targetEntityType = $definition['constraints']['EntityType'] ?? NULL;
if ($targetEntityType) {
$generatedType = $this->generateTypeForEntityType($targetEntityType);
if ($generatedType) {
return $generatedType;
}
}
return 'Entity';
case 'language_reference':
return 'LanguageInterface';
}
// Try to find a matching data definition for this type.
if ($this->getTypedDataManager()->hasDefinition($type)) {
$dataDefinition = $this->getTypedDataManager()->getDefinition($type);
$instance = $this->getTypedDataManager()->createDataDefinition($type);
$typedData = $this->getTypedDataManager()->create($instance);
if ($typedData instanceof StringData) {
return 'String';
}
if ($instance instanceof ComplexDataDefinitionInterface) {
$propertyDefinitions = $instance->getPropertyDefinitions();
return $this->getComplexDataType($type, $propertyDefinitions);
}
}
elseif ($this->getTypedConfigManager()->hasDefinition($type)) {
$dataDefinition = $this->getTypedConfigManager()->getDefinition($type);
$instance = $this->getTypedConfigManager()->createDataDefinition($type);
// A mapping is basically a type that references another type.
// The method being called here will eventually call this method again. If
// the referenced map type again references a map type, it might end up
// here a third time and so on. In the end we have eventually resolved to
// a scalar type being returned above.
// This allows us to fully resolve config schema types down to the last
// property, if supported.
if ($dataDefinition && $instance) {
if (!empty($dataDefinition['mapping'])) {
return $this->getTypeForMapping($type, $dataDefinition['mapping']);
}
}
}
return NULL;
}
/**
* Try to infer the type for a mapping property.
*
* @param string $mappingName
* The name of the mapping.
* @param array $mapping
* The mapping configuration.
*
* @return string|null
* The GraphQL type if found.
*/
private function getTypeForMapping(string $mappingName, array $mapping): string|NULL {
// E.g. DataTypeLinkitMatcher.
$graphqlTypeName = $this->getGraphqlTypeNameForMapping($mappingName);
// Type already generated.
if ($this->registry->typeWillExist($graphqlTypeName)) {
return $graphqlTypeName;
}
$type = $this->registry->createType($graphqlTypeName)->description("The $mappingName schema mapping.");
foreach ($mapping as $mappingProperty => $mappingDefinition) {
$mappingType = $this->getDataPropertyType($mappingDefinition);
if ($mappingType) {
$field = $this->createField($mappingProperty)->type($mappingType);
$type->addField($field);
}
}
return $graphqlTypeName;
}
/**
* Try to generate a type for ComplexDataDefinition with properties.
*
* @param string $typeName
* The name of the complex data.
* @param \Drupal\Core\TypedData\DataDefinition[] $propertyDefinitions
* The property definitions.
*
* @return string|null
* The GraphQL type.
*/
private function getComplexDataType(string $typeName, array $propertyDefinitions): string|NULL {
// e.g. DataTypeShipmentItem.
$graphqlDataTypeName = EntitySchemaHelper::toPascalCase(
['data_type_', $typeName]
);
// Type has already been generated.
if ($this->registry->typeWillExist($graphqlDataTypeName)) {
return $graphqlDataTypeName;
}
$fields = [];
foreach ($propertyDefinitions as $name => $propertyDefinition) {
if ($propertyDefinition instanceof DataDefinition) {
$propertyFieldType = $this->getDataPropertyType($propertyDefinition->toArray());
if ($propertyFieldType) {
$propertyFieldName = EntitySchemaHelper::toCamelCase($name);
$field = $this->createField($propertyFieldName)->type($propertyFieldType)->description((string) $propertyDefinition->getLabel());
$fields[] = $field;
}
}
}
if (empty($fields)) {
return 'MapData';
}
$type = $this->registry->createType($graphqlDataTypeName);
foreach ($fields as $field) {
$type->addField($field);
}
return $graphqlDataTypeName;
}
/**
* Get the GraphQL type name for a schema type mapping.
*
* @param string $mappingName
* The name of the mapping.
*
* @return string
* The GraphQL type name.
*/
protected function getGraphqlTypeNameForMapping(string $mappingName) {
return EntitySchemaHelper::toPascalCase([
'data_type_',
$mappingName,
]);
}
}
