graphql_compose-1.0.0-beta20/src/Plugin/GraphQLComposeFieldTypeManager.php
src/Plugin/GraphQLComposeFieldTypeManager.php
<?php
declare(strict_types=1);
namespace Drupal\graphql_compose\Plugin;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\graphql_compose\Annotation\GraphQLComposeFieldType;
use Drupal\graphql_compose\Attribute\FieldType;
use Drupal\graphql_compose\Utility\ComposeConfig;
use Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeFieldTypeInterface;
/**
* Manager that collects and exposes GraphQL compose field type plugins.
*
* A field type is a plugin that defines how to resolve a Drupal Field Type.
*/
class GraphQLComposeFieldTypeManager extends DefaultPluginManager {
use StringTranslationTrait;
/**
* Private field plugin storage.
*
* @var array
*/
private array $fields = [];
/**
* Private field plugin storage.
*
* @var array
*/
private array $interfaceFields = [];
/**
* Constructs a GraphQLComposeFieldTypeManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
* @param array $config
* The configuration service parameter.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The configuration factory.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
* Entity field manager service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* Entity type manager service.
* @param \Drupal\graphql_compose\Plugin\GraphQLComposeEntityTypeManager $gqlEntityTypeManager
* Entity type plugin manager.
*/
public function __construct(
\Traversable $namespaces,
ModuleHandlerInterface $module_handler,
CacheBackendInterface $cache_backend,
array $config,
protected ConfigFactoryInterface $configFactory,
protected EntityFieldManagerInterface $entityFieldManager,
protected EntityTypeManagerInterface $entityTypeManager,
protected GraphQLComposeEntityTypeManager $gqlEntityTypeManager,
) {
parent::__construct(
'Plugin/GraphQLCompose/FieldType',
$namespaces,
$module_handler,
GraphQLComposeFieldTypeInterface::class,
FieldType::class,
GraphQLComposeFieldType::class,
);
$this->alterInfo('graphql_compose_field_type');
$this->useCaches(empty($config['development']));
$this->setCacheBackend($cache_backend, 'graphql_compose_field_type', [
'graphql_compose_field_type',
'config:field_config_list',
]);
}
/**
* Create a field plugin instances for an entity type field.
*
* @param array $config
* Configuration for the plugin.
*
* @return \Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeFieldTypeInterface|null
* The field plugin instance.
*
* @throws \Exception
* If the field config is missing the field_definition.
*
* @todo Check how efficient this is.
*/
public function getFieldInstance(array $config = []): ?GraphQLComposeFieldTypeInterface {
if (!isset($config['field_definition'])) {
throw new \Exception('Field config missing field_definition');
}
/** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
$field_definition = $config['field_definition'];
// Set required field config if not set yet.
$config = array_merge([
'description' => strval($field_definition->getDescription() ?: $field_definition->getLabel()),
'field_name' => $field_definition->getName(),
'field_type' => $field_definition->getType(),
'name_sdl' => $field_definition->getName(),
'required' => $field_definition->isRequired(),
'multiple' => $field_definition->getFieldStorageDefinition()->isMultiple(),
], $config);
return $this->createFieldInstance($config['field_type'], $config);
}
/**
* Create a field plugin instance for an entity type property.
*
* @param array $config
* Configuration for the plugin.
*
* @return \Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeFieldTypeInterface|null
* The field plugin instance.
*
* @throws \Exception
* If the field config is missing the field_name.
*
* @todo Check how efficient this is.
*/
public function getPropertyInstance(array $config = []): ?GraphQLComposeFieldTypeInterface {
if (!isset($config['field_name'])) {
throw new \Exception('Property config missing field_name');
}
// Set required property config is not set yet.
$config = array_merge([
'field_type' => 'property',
'name_sdl' => $config['field_name'],
'type_sdl' => 'String',
], $config);
return $this->createFieldInstance($config['field_type'], $config);
}
/**
* Create a field type plugin with config.
*
* @param string $field_type_plugin_id
* The plugin ID for a custom field type.
* @param array $config
* Configuration for the plugin.
*
* @return \Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeFieldTypeInterface|null
* The field plugin instance.
*/
protected function createFieldInstance(string $field_type_plugin_id, array $config): ?GraphQLComposeFieldTypeInterface {
if (!$plugin_definition = $this->getDefinition($field_type_plugin_id, FALSE)) {
// This type may be referenced by an entity and
// not have a defined plugin in GraphqlCompose/FieldType.
return NULL;
}
// Set missing config defaults if not set yet.
$config = array_merge([
'field_type' => 'property',
'name_sdl' => $plugin_definition['name_sdl'] ?? NULL,
'type_sdl' => $plugin_definition['type_sdl'] ?? NULL,
'description' => $plugin_definition['description'] ?? NULL,
], $config);
return $this->createInstance($field_type_plugin_id, $config);
}
/**
* All defined fields that have been created at time of invocation.
*
* @return array
* An array of fields that have been initialized,
* keyed by entity type and bundle.
*/
public function getFields(): array {
return $this->fields;
}
/**
* Get fields for a bundle.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle_id
* The bundle ID.
*
* @return \Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeFieldTypeInterface[]
* An array of fields for the chosen entity and bundle.
*/
public function getBundleFields(string $entity_type_id, string $bundle_id): array {
if (isset($this->fields[$entity_type_id][$bundle_id])) {
return $this->fields[$entity_type_id][$bundle_id];
}
// Get user config for fieldable fields.
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$entity_plugin_type = $this->gqlEntityTypeManager->getPluginInstance($entity_type_id);
if (!$entity_plugin_type) {
return [];
}
$bundle = $entity_plugin_type->getBundle($bundle_id);
if (!$bundle) {
return [];
}
// Hydrate fields with interface fields.
$fields = $this->getInterfaceFields($entity_type_id);
if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
$base_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
$field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle_id);
$settings = ComposeConfig::config();
foreach ($field_definitions as $field_name => $field_definition) {
// Check if field is a base-field.
if (array_key_exists($field_name, $base_definitions) || array_key_exists($field_name, $fields)) {
continue;
}
// Allow overriding field plugin config via config.
$config_base = 'field_config.' . $entity_type_id . '.' . $bundle_id . '.' . $field_name;
// Strip null values from config.
$config = array_filter(
$settings->get($config_base) ?: [],
fn ($item) => !is_null($item)
);
// Bind the field definition to the config.
$config['field_definition'] ??= $field_definition;
// User config or false or null... is false.
$enabled = $config['enabled'] ?? FALSE ?: FALSE;
// Add field to result.
if ($enabled) {
if ($instance = $this->getFieldInstance($config)) {
$fields[$field_name] = $instance;
}
}
}
}
// Bind each field (and interface field) to the bundle entity.
foreach ($fields as $field) {
$field->setEntityWrapper($bundle);
}
// Sort the fields, totally necessary.
$this->sortFields($fields);
$this->fields[$entity_type_id][$bundle_id] = $fields;
return $this->fields[$entity_type_id][$bundle_id];
}
/**
* Return fields for usage in interface.
*
* @return \Drupal\graphql_compose\Plugin\GraphQLCompose\GraphQLComposeFieldTypeInterface[]
* Field instances.
*/
public function getInterfaceFields(string $entity_type_id): array {
if (isset($this->interfaceFields[$entity_type_id])) {
return $this->interfaceFields[$entity_type_id];
}
$fields = [];
// Get base fields defined by the entity type plugin.
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$entity_plugin_type = $this->gqlEntityTypeManager->getPluginInstance($entity_type_id);
if (!$entity_plugin_type) {
return [];
}
$base_fields = $entity_plugin_type->getBaseFields();
// If it's not fieldable, it's probably a plugin entity.
if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
$base_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
foreach ($base_definitions as $field_name => $field_definition) {
// Only enable fields defined in config.
if (!array_key_exists($field_name, $base_fields)) {
continue;
}
$config = array_merge(
['field_definition' => $field_definition],
// Default to required true.
['required' => TRUE],
// Allow entity types to override field config.
$base_fields[$field_name],
);
if ($instance = $this->getFieldInstance($config)) {
$fields[$field_name] = $instance;
}
}
}
else {
// Add base fields verbatim.
foreach ($base_fields as $field_name => $config) {
$config = array_merge(
['field_name' => $field_name],
// Default to required true.
['required' => TRUE],
// Allow entity types to override field config.
$config,
);
if ($instance = $this->getPropertyInstance($config)) {
$fields[$field_name] = $instance;
}
}
}
// Combine fields with identifying fields, giving our definition priority.
$fields = array_merge(
$fields,
$this->getIdentifyingFields($entity_type),
);
// Sort the fields, totally necessary.
$this->sortFields($fields);
$this->interfaceFields[$entity_type_id] = $fields;
return $this->interfaceFields[$entity_type_id];
}
/**
* Get identifying fields for the entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return array
* The fields to use as IDs.
*/
protected function getIdentifyingFields(EntityTypeInterface $entity_type): array {
$fields = [];
// Users select how they want to load entities.
$expose_entity_ids = ComposeConfig::get('settings.expose_entity_ids', FALSE);
// Check the base definitions for the entity type.
$field_definitions = $entity_type->entityClassImplements(FieldableEntityInterface::class)
? $this->entityFieldManager->getBaseFieldDefinitions($entity_type->id())
: [];
// If loading by UUID, do not add entity ID automatically.
if ($expose_entity_ids && $entity_type->hasKey('id')) {
$id_key = $entity_type->getKey('id');
$fields[$id_key] = $this->getPropertyInstance([
'field_name' => $id_key,
'field_type' => 'entity_id',
'name_sdl' => 'id',
'type_sdl' => 'ID',
'required' => TRUE,
'description' => (string) $this->t('The entity ID.'),
'field_definition' => $field_definitions[$id_key] ?? NULL,
]);
}
// We always add UUID if available.
// If expose_entity_ids, set name_sdl to id.
// If not expose_entity_ids, set name_sdl to uuid.
if ($entity_type->hasKey('uuid')) {
$uuid_key = $entity_type->getKey('uuid');
$fields['uuid'] = $this->getPropertyInstance([
'field_name' => $uuid_key,
'field_type' => 'uuid',
'name_sdl' => $expose_entity_ids ? 'uuid' : 'id',
'type_sdl' => 'ID',
'required' => TRUE,
'description' => (string) $this->t('The Universally Unique IDentifier (UUID).'),
'field_definition' => $field_definitions[$uuid_key] ?? NULL,
]);
}
return $fields;
}
/**
* Utility function to order the fields with IDs first then alphabetically.
*
* @param array $fields
* The fields to order.
*/
protected function sortFields(array &$fields) {
// Sort fields by name.
uasort($fields, fn ($a, $b) => strnatcmp($a->getNameSdl(), $b->getNameSdl()));
$fields = array_merge(
// Put the id field first.
array_filter($fields, fn ($field) => $field->getNameSdl() === 'id'),
// Put UUID field second.
array_filter($fields, fn ($field) => $field->getNameSdl() === 'uuid'),
// Merge in alphabetical order.
$fields,
);
}
}
