graphql_core_schema-1.0.x-dev/src/Plugin/GraphQL/Schema/CoreComposableSchema.php

src/Plugin/GraphQL/Schema/CoreComposableSchema.php
<?php

namespace Drupal\graphql_core_schema\Plugin\GraphQL\Schema;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\Checkboxes;
use Drupal\Core\TypedData\TypedDataTrait;
use Drupal\graphql\GraphQL\ResolverBuilder;
use Drupal\graphql\GraphQL\ResolverRegistry;
use Drupal\graphql\GraphQL\ResolverRegistryInterface;
use Drupal\graphql\Plugin\GraphQL\Schema\ComposableSchema;
use Drupal\graphql\Plugin\SchemaExtensionPluginInterface;
use Drupal\graphql\Plugin\SchemaExtensionPluginManager;
use Drupal\graphql_core_schema\CoreComposableConfig;
use Drupal\graphql_core_schema\CoreComposableResolver;
use Drupal\graphql_core_schema\CoreSchemaExtensionInterface;
use Drupal\graphql_core_schema\CoreSchemaInterfaceExtensionInterface;
use Drupal\graphql_core_schema\EntitySchemaBuilder;
use Drupal\graphql_core_schema\Form\CoreComposableSchemaFormHelper;
use Drupal\graphql_core_schema\GraphQL\Enums\DrupalDateFormatEnum;
use Drupal\graphql_core_schema\GraphQL\Enums\EntityTypeEnum;
use Drupal\graphql_core_schema\GraphQL\Enums\LangcodeEnum;
use Drupal\graphql_core_schema\SchemaBuilder\SchemaBuilderGenerator;
use Drupal\graphql_core_schema\SchemaBuilder\SchemaBuilderRegistry;
use Drupal\graphql_core_schema\TypeAwareSchemaExtensionInterface;
use Drupal\typed_data\DataFetcherTrait;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\Parser;
use GraphQL\Type\Schema;
use GraphQL\Utils\BuildSchema;
use GraphQL\Utils\SchemaExtender;
use GraphQL\Utils\SchemaPrinter;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Drupal\graphql_core_schema\GraphQL\Enums\LanguageDirectionEnum;

/**
 * Extendable core schema.
 *
 * @Schema(
 *   id = "core_composable",
 *   name = "Core Composable Schema"
 * )
 */
class CoreComposableSchema extends ComposableSchema {

  use TypedDataTrait;
  use DataFetcherTrait;
  use DependencySerializationTrait;

  /**
   * Array of generated GraphQL types.
   *
   * The types are only present if the schema is being generated. In a
   * normal production environment this is empty, because it's only needed
   * when the schema is extended.
   *
   * @var string[]
   */
  protected $generatedTypes;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('cache.graphql.ast'),
      $container->get('module_handler'),
      $container->get('plugin.manager.graphql.schema_extension'),
      $container->getParameter('graphql.config'),
      $container->get('entity_type.manager'),
      $container->get('file_system'),
      $container->get('event_dispatcher'),
    );
  }

  public function __construct(
    array $configuration,
    $pluginId,
    array $pluginDefinition,
    CacheBackendInterface $astCache,
    ModuleHandlerInterface $moduleHandler,
    SchemaExtensionPluginManager $extensionManager,
    array $config,
    protected EntityTypeManagerInterface $entityTypeManager,
    protected FileSystemInterface $fileSystem,
    protected EventDispatcherInterface $dispatcher,
  ) {
    parent::__construct($configuration, $pluginId, $pluginDefinition, $astCache, $moduleHandler, $extensionManager, $config);
  }

  /**
   * Get the core schema definitions.
   *
   * @param string[] $extensionBaseDefinitions
   *   The base definition files from enabled extensions.
   *
   * @return \GraphQL\Language\AST\DefinitionNode[]
   *   The core schema definitions.
   */
  protected function getCoreSchemaDefinition(array $extensionBaseDefinitions): array {
    $module = $this->moduleHandler->getModule('graphql_core_schema');
    $folder = $module->getPath() . '/graphql/core';
    $files = $this->fileSystem->scanDirectory($folder, '/.*\.graphqls$/');

    $coreFiles = array_map(function ($file) {
      return file_get_contents($file->uri);
    }, $files);

    $sdl = implode("\n\n", array_merge($coreFiles, $extensionBaseDefinitions));
    $parsed = Parser::parse($sdl);
    return iterator_to_array($parsed->definitions);
  }

  /**
   * {@inheritdoc}
   */
  protected function getSchemaDefinition(): string {
    $config = CoreComposableConfig::fromConfiguration($this->configuration);

    // Throw an exception here because the schema is broken if no entity type
    // is enabled.
    if (empty($config->getEnabledEntityTypes())) {
      throw new \Exception('At least one entity type must be enabled for the schema to work properly.');
    }

    $extensions = $this->getExtensions();
    $extensionBaseDefinitions = [];

    foreach ($extensions as $extension) {
      $extensionBaseDefinitions[] = $extension->getBaseDefinition();
      if ($extension instanceof CoreSchemaInterfaceExtensionInterface) {
        $extensionId = $extension->getPluginId();
        throw new \Exception("Extending interfaces using getInterfaceExtender() has been removed. You can now directly define the interface in $extensionId.base.graphqls, it will be used as the base for generating the interface. More information: https://graphql-core-schema.netlify.app/advanced/extending-interfaces.html#defining-the-interface-in-an-extension-base-graphqls-file");
      }
    }

    $coreSchemaDefinitions = $this->getCoreSchemaDefinition($extensionBaseDefinitions);
    $entityTypeDefinitions = $this->entityTypeManager->getDefinitions();
    $schemaBuilderRegistry = new SchemaBuilderRegistry();
    $schemaBuilder = new EntitySchemaBuilder(
      $schemaBuilderRegistry,
      $config,
      $this->moduleHandler,
      $this->getConfiguration(),
      $this->dispatcher,
    );

    foreach (array_keys($entityTypeDefinitions) as $typeId) {
      $schemaBuilder->generateTypeForEntityType($typeId);
    }

    $generator = new SchemaBuilderGenerator();
    $generator
      ->addType(new LangcodeEnum())
      ->addType(new LanguageDirectionEnum())
      ->addType(new DrupalDateFormatEnum())
      ->addType(new EntityTypeEnum($config->getEnabledEntityTypes()));

    $schema = $generator->getGeneratedSchema($schemaBuilderRegistry, $config, $coreSchemaDefinitions);
    $this->generatedTypes = $generator->getGeneratedTypeNames();
    return $schema;
  }

  /**
   * {@inheritdoc}
   */
  protected function getExtensions() {
    $coreComposableConfig = CoreComposableConfig::fromConfiguration($this->configuration);
    $extensions = array_map(function ($id) use ($coreComposableConfig) {
      // Expose the extension configuration if it exists.
      $extensionConfiguration = $this->configuration['extension_' . $id] ?? [];
      // Expose the general configuration of the endpoint so the extensions can
      // make decisions based on which entity types / bundles / fields are
      // enabled.
      $extensionConfiguration += ['core_composable' => $coreComposableConfig];
      if ($this->extensionManager->hasDefinition($id)) {
        return $this->extensionManager->createInstance($id, $extensionConfiguration);
      }
    }, array_filter($this->getConfiguration()['extensions'] ?? []));
    // Order the extensions by priority so that higher priority extensions are
    // processed first.
    return $this->extensionManager->sortByPriority(array_filter($extensions));
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);
    $config = CoreComposableConfig::fromConfiguration($this->configuration);
    $formHelper = new CoreComposableSchemaFormHelper();
    $formHelper->buildConfigurationForm(
      $form,
      $form_state,
      $this->configuration,
      $this->getExtensions(),
      [$this, 'reloadFieldsAndBundles']
    );
    $formHelper->buildEntityFieldForm($form, $form_state, $this->configuration, $config->getEnabledEntityTypes());
    $formHelper->buildEntityBundleForm($form, $form_state, $this->configuration, $config->getEnabledEntityTypes());

    $form['#attached']['library'][] = 'graphql_core_schema/tweaks';
    return $form;
  }

  /**
   * Ajax Callback for form reload.
   *
   * @param array $form
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   */
  public function reloadFieldsAndBundles(array &$form, FormStateInterface $form_state): AjaxResponse {
    $response = new AjaxResponse();

    $fields = $form['schema_configuration']['core_composable']['fields'];
    $response->addCommand(new ReplaceCommand('#field-wrapper', $fields));

    $bundles = $form['schema_configuration']['core_composable']['bundles'];
    $response->addCommand(new ReplaceCommand('#bundle-wrapper', $bundles));

    return $response;
  }

  /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $formState): void {
    $values = $formState->getValues();
    $extensions = array_filter(array_values($formState->getValue('extensions')));
    $entityTypesArray = is_array($values['enabled_entity_types']) ? $values['enabled_entity_types'] : [];
    $entityTypes = Checkboxes::getCheckedCheckboxes($entityTypesArray);

    foreach ($extensions as $extensionId) {
      $instance = $this->extensionManager->createInstance($extensionId);
      if ($instance instanceof CoreSchemaExtensionInterface) {
        $requiredEntityIds = $instance->getEntityTypeDependencies();
        foreach ($requiredEntityIds as $entityId) {
          if (!in_array($entityId, $entityTypes)) {
            $element = $form['enabled_entity_types'][$entityId];
            $formState->setError(
              $element,
              $this->t('Extension "@extension" requires entity type "@type" to be enabled', [
                '@extension' => $instance->getBaseId(),
                '@type' => $entityId,
              ]
            ));
          }
        }

        $requiredExtensions = $instance->getExtensionDependencies();
        foreach ($requiredExtensions as $requiredExtensionId) {
          if (!in_array($requiredExtensionId, $extensions)) {
            $formState->setErrorByName(
              $requiredExtensionId . '_' . $extensionId,
              $this->t('Extension "@extension" requires extension "@dependency" to be enabled', [
                '@extension' => $instance->getBaseId(),
                '@dependency' => $requiredExtensionId,
              ]
            ));
          }
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getResolverRegistry() {
    // Create the registry and provide our default field and type resolvers.
    // As the name suggests these are called if no other field or type resolver
    // matched. This means that, to "override" the behavior of a field you can
    // just register your own resolver for this specific field.
    $registry = new ResolverRegistry(
      [CoreComposableResolver::class, 'resolveFieldDefault'],
      [CoreComposableResolver::class, 'resolveTypeDefault'],
    );

    $builder = new ResolverBuilder();
    CoreComposableResolver::registerPingResolvers($registry, $builder);
    CoreComposableResolver::registerEntityResolvers($registry, $builder);
    CoreComposableResolver::registerFieldListResolvers($registry, $builder);
    CoreComposableResolver::registerLanguageResolvers($registry, $builder);
    CoreComposableResolver::registerUrlResolvers($registry, $builder);
    return $registry;
  }

  /**
   * {@inheritdoc}
   */
  public function getSchema(ResolverRegistryInterface $registry) {
    $extensions = $this->getExtensions();
    $document = $this->getSchemaDocument($extensions);
    $schema = $this->buildSchemaOverride($document, $registry);

    if (empty($extensions)) {
      return $schema;
    }

    foreach ($extensions as $extension) {
      $extension->registerResolvers($registry);
    }

    $extendedDocument = $this->getFullSchemaDocumentOverride($schema, $extensions);
    if (empty($extendedDocument)) {
      return $schema;
    }

    return $this->buildSchemaOverride($extendedDocument, $registry);
  }

  /**
   * Create a GraphQL schema object from the given AST document.
   *
   * Waiting for https://github.com/drupal-graphql/graphql/pull/1379 to be part
   * of the graphql module.
   */
  protected function buildSchemaOverride(DocumentNode $astDocument, ResolverRegistryInterface $registry): Schema {
    $resolver = [$registry, 'resolveType'];
    // Performance optimization.
    // Do not validate the schema on every request by passing the option:
    // ['assumeValid' => true] to the build function.
    $options = ['assumeValid' => TRUE];
    $schema = BuildSchema::build($astDocument, function ($config, TypeDefinitionNode $type) use ($resolver) {
      if ($type instanceof InterfaceTypeDefinitionNode || $type instanceof UnionTypeDefinitionNode) {
        $config['resolveType'] = $resolver;
      }

      return $config;
    }, $options);
    return $schema;
  }

  /**
   * Returns the full AST combination of parsed schema with extensions, cached.
   *
   * Waiting for https://github.com/drupal-graphql/graphql/pull/1379 to be part
   * of the graphql module.
   */
  protected function getFullSchemaDocumentOverride(Schema $schema, array $extensions): ?DocumentNode {
    // Only use caching of the parsed document if we aren't in development mode.
    $cid = "full:{$this->getPluginId()}";
    if (empty($this->inDevelopment) && $cache = $this->astCache->get($cid)) {
      return $cache->data;
    }

    $ast = NULL;
    if ($extendAst = $this->getExtensionDocument($extensions)) {
      $fullSchema = SchemaExtender::extend($schema, $extendAst);
      // Performance: export the full schema as string and parse it again. That
      // way we can cache the full AST.
      $fullSchemaString = SchemaPrinter::doPrint($fullSchema);
      $ast = Parser::parse($fullSchemaString, ['noLocation' => TRUE]);
    }

    if (empty($this->inDevelopment)) {
      $this->astCache->set($cid, $ast, CacheBackendInterface::CACHE_PERMANENT, ['graphql']);
    }
    return $ast;
  }

  /**
   * {@inheritdoc}
   */
  public function getSchemaDocument(array $extensions = []) {
    // @todo Remove this function as soon as
    // https://github.com/drupal-graphql/graphql/pull/1314
    // is merged.
    $cid = "schema:{$this->getPluginId()}";
    if (empty($this->inDevelopment) && $cache = $this->astCache->get($cid)) {
      return $cache->data;
    }

    $schema = [$this->getSchemaDefinition()];

    // This option avoids WSOD / recursion issues.
    $options = ['noLocation' => TRUE];
    $ast = Parser::parse(implode("\n\n", $schema), $options);
    if (empty($this->inDevelopment)) {
      $this->astCache->set($cid, $ast, CacheBackendInterface::CACHE_PERMANENT, ['graphql']);
    }

    return $ast;
  }

  /**
   * {@inheritdoc}
   */
  protected function getExtensionDocument(array $extensions = []) {
    // Only use caching of the parsed document if we aren't in development mode.
    $cid = "extension:{$this->getPluginId()}";
    if (empty($this->inDevelopment) && $cache = $this->astCache->get($cid)) {
      return $cache->data;
    }

    $extensions = array_filter(array_map(function (SchemaExtensionPluginInterface $extension) {
      $extensionSchema = $extension->getExtensionDefinition();

      // Extensions implementing this interface can additionally extend the
      // schema conditionally. They get an array of all generated GraphQL types
      // as the first argument.
      if ($extension instanceof TypeAwareSchemaExtensionInterface) {
        $typeExtensionSchema = $extension->getTypeExtensionDefinition($this->generatedTypes ?? []);
        if ($typeExtensionSchema) {
          $extensionSchema .= "\n\n" . $typeExtensionSchema;
        }
      }

      return $extensionSchema;
    }, $extensions), function ($definition) {
      return !empty($definition);
    });

    $ast = !empty($extensions) ? Parser::parse(implode("\n\n", $extensions)) : NULL;
    if (empty($this->inDevelopment)) {
      $this->astCache->set($cid, $ast, CacheBackendInterface::CACHE_PERMANENT, ['graphql']);
    }

    return $ast;
  }

  /**
   * Get the AST from an extension.
   *
   * @param \GraphQL\Type\Schema $schema
   *   The base schema.
   * @param \GraphQL\Language\AST\DocumentNode $extendSchema
   *   The extension schema.
   *
   * @return \GraphQL\Language\AST\DocumentNode
   *   The AST of the schema.
   *
   * @throws \GraphQL\Error\Error
   * @throws \GraphQL\Error\SyntaxError
   */
  public function getExtensionSchemaAst(Schema $schema, DocumentNode $extendSchema) {
    $cid = "schema_extension:{$this->getPluginId()}";
    if (empty($this->inDevelopment) && $cache = $this->astCache->get($cid)) {
      return $cache->data;
    }

    $schema = SchemaExtender::extend($schema, $extendSchema);
    $schema_string = SchemaPrinter::doPrint($schema);
    $options = ['noLocation' => TRUE];
    $ast = Parser::parse($schema_string, $options);
    if (empty($this->inDevelopment)) {
      $this->astCache->set($cid, $ast, CacheBackendInterface::CACHE_PERMANENT, ['graphql']);
    }

    return $ast;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc