You are here

Server.php in GraphQL 8.4

File

src/Entity/Server.php
View source
<?php

namespace Drupal\graphql\Entity;

use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\graphql\GraphQL\Execution\ExecutionResult as CacheableExecutionResult;
use Drupal\graphql\GraphQL\Execution\FieldContext;
use Drupal\graphql\Plugin\PersistedQueryPluginInterface;
use GraphQL\Error\DebugFlag;
use GraphQL\Server\OperationParams;
use Drupal\graphql\GraphQL\Execution\ResolveContext;
use GraphQL\Server\ServerConfig;
use Drupal\graphql\GraphQL\ResolverRegistryInterface;
use Drupal\graphql\GraphQL\Utility\DeferredUtility;
use Drupal\graphql\Plugin\SchemaPluginInterface;
use GraphQL\Error\Error;
use GraphQL\Error\FormattedError;
use GraphQL\Executor\Executor;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Server\Helper;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Validator\DocumentValidator;

/**
 * The main GraphQL configuration and request entry point.
 *
 * Multiple GraphQL servers can be defined on different routing paths with
 * different GraphQL schemas.
 *
 * @ConfigEntityType(
 *   id = "graphql_server",
 *   label = @Translation("Server"),
 *   handlers = {
 *     "list_builder" = "Drupal\graphql\Controller\ServerListBuilder",
 *     "form" = {
 *       "edit" = "Drupal\graphql\Form\ServerForm",
 *       "create" = "Drupal\graphql\Form\ServerForm",
 *       "delete" = "Drupal\Core\Entity\EntityDeleteForm",
 *       "persisted_queries" = "Drupal\graphql\Form\PersistedQueriesForm"
 *     }
 *   },
 *   config_prefix = "graphql_servers",
 *   admin_permission = "administer graphql configuration",
 *   entity_keys = {
 *     "id" = "name",
 *     "label" = "label"
 *   },
 *   config_export = {
 *     "name",
 *     "label",
 *     "schema",
 *     "schema_configuration",
 *     "persisted_queries_settings",
 *     "endpoint",
 *     "debug_flag",
 *     "caching",
 *     "batching"
 *   },
 *   links = {
 *     "collection" = "/admin/config/graphql/servers",
 *     "create-form" = "/admin/config/graphql/servers/create",
 *     "edit-form" = "/admin/config/graphql/servers/manage/{graphql_server}",
 *     "delete-form" = "/admin/config/graphql/servers/manage/{graphql_server}/delete",
 *     "persisted_queries-form" = "/admin/config/graphql/servers/manage/{graphql_server}/persisted_queries",
 *   }
 * )
 */
class Server extends ConfigEntityBase implements ServerInterface {
  use DependencySerializationTrait;

  /**
   * The server's machine-readable name.
   *
   * @var string
   */
  public $name;

  /**
   * The server's human-readable name.
   *
   * @var string
   */
  public $label;

  /**
   * The server's schema.
   *
   * @var string
   */
  public $schema;

  /**
   * Schema configuration.
   *
   * @var array
   */
  public $schema_configuration = [];

  /**
   * The debug settings for this server.
   *
   * @var int
   * @see \GraphQL\Error\DebugFlag
   */
  public $debug_flag = DebugFlag::NONE;

  /**
   * Whether the server should cache its results.
   *
   * @var bool
   */
  public $caching = TRUE;

  /**
   * Whether the server allows query batching.
   *
   * @var bool
   */
  public $batching = TRUE;

  /**
   * The server's endpoint.
   *
   * @var string
   */
  public $endpoint;

  /**
   * Persisted query plugins configuration.
   *
   * @var array
   */
  public $persisted_queries_settings = [];

  /**
   * Persisted query plugin instances available on this server.
   *
   * @var array|null
   */
  protected $persisted_query_instances = NULL;

  /**
   * The sorted persisted query plugin instances available on this server.
   *
   * @var array|null
   */
  protected $sorted_persisted_query_instances = NULL;

  /**
   * {@inheritdoc}
   */
  public function id() {
    return $this->name;
  }

  /**
   * {@inheritdoc}
   */
  public function executeOperation(OperationParams $operation) {
    $previous = Executor::getImplementationFactory();
    Executor::setImplementationFactory([
      \Drupal::service('graphql.executor'),
      'create',
    ]);
    try {
      $config = $this
        ->configuration();
      $result = (new Helper())
        ->executeOperation($config, $operation);

      // In case execution fails before the execution stage, we have to wrap the
      // result object here.
      if (!$result instanceof CacheableExecutionResult) {
        $result = new CacheableExecutionResult($result->data, $result->errors, $result->extensions);
        $result
          ->mergeCacheMaxAge(0);
      }
    } finally {
      Executor::setImplementationFactory($previous);
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function executeBatch($operations) {

    // We can't leverage parallel processing of batched queries because of the
    // contextual properties of Drupal (e.g. language manager, current user).
    return array_map(function (OperationParams $operation) {
      return $this
        ->executeOperation($operation);
    }, $operations);
  }

  /**
   * {@inheritdoc}
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  public function configuration() {
    $params = \Drupal::getContainer()
      ->getParameter('graphql.config');

    /** @var \Drupal\graphql\Plugin\SchemaPluginManager $manager */
    $manager = \Drupal::service('plugin.manager.graphql.schema');
    $schema = $this
      ->get('schema');

    /** @var \Drupal\graphql\Plugin\SchemaPluginInterface $plugin */
    $plugin = $manager
      ->createInstance($schema);
    if ($plugin instanceof ConfigurableInterface && ($config = $this
      ->get('schema_configuration'))) {
      $plugin
        ->setConfiguration($config[$schema] ?? []);
    }

    // Create the server config.
    $registry = $plugin
      ->getResolverRegistry();
    $server = ServerConfig::create();
    $server
      ->setDebugFlag($this
      ->get('debug_flag'));
    $server
      ->setQueryBatching(!!$this
      ->get('batching'));
    $server
      ->setValidationRules($this
      ->getValidationRules());
    $server
      ->setPersistentQueryLoader($this
      ->getPersistedQueryLoader());
    $server
      ->setContext($this
      ->getContext($plugin, $params));
    $server
      ->setFieldResolver($this
      ->getFieldResolver($registry));
    $server
      ->setSchema($plugin
      ->getSchema($registry));
    $server
      ->setPromiseAdapter(new SyncPromiseAdapter());
    return $server;
  }

  /**
   * Returns to root value to use when resolving queries against the schema.
   *
   * @todo Handle this through configuration (e.g. a context value).
   *
   * May return a callable to resolve the root value at run-time based on the
   * provided query parameters / operation.
   *
   * @code
   *
   * public function getRootValue() {
   *   return function (OperationParams $params, DocumentNode $document, $operation) {
   *     // Dynamically return a root value based on the current query.
   *   };
   * }
   *
   * @endcode
   *
   * @return mixed|callable
   *   The root value for query execution or a callable factory.
   */
  protected function getRootValue() {
    return NULL;
  }

  /**
   * Returns the context object to use during query execution.
   *
   * May return a callable to instantiate a context object for each individual
   * query instead of a shared context. This may be useful e.g. when running
   * batched queries where each query operation within the same request should
   * use a separate context object.
   *
   * The returned value will be passed as an argument to every type and field
   * resolver during execution.
   *
   * @code
   *
   * public function getContext() {
   *   $shared = ['foo' => 'bar'];
   *
   *   return function (OperationParams $params, DocumentNode $document, $operation) use ($shared) {
   *     $private = ['bar' => 'baz'];
   *
   *     return new MyContext($shared, $private);
   *   };
   * }
   *
   * @endcode
   *
   * @param \Drupal\graphql\Plugin\SchemaPluginInterface $schema
   *   The schema plugin instance.
   * @param array $config
   *
   * @return mixed|callable
   *   The context object for query execution or a callable factory.
   */
  protected function getContext(SchemaPluginInterface $schema, array $config) {

    // Each document (e.g. in a batch query) gets its own resolve context. This
    // allows us to collect the cache metadata and contextual values (e.g.
    // inheritance for language) for each query separately.
    return function (OperationParams $params, DocumentNode $document, $type) use ($schema, $config) {
      $context = new ResolveContext($this, $params, $document, $type, $config);
      $context
        ->addCacheTags([
        'graphql_response',
      ]);
      if ($this instanceof CacheableDependencyInterface) {
        $context
          ->addCacheableDependency($this);
      }
      if ($schema instanceof CacheableDependencyInterface) {
        $context
          ->addCacheableDependency($schema);
      }
      return $context;
    };
  }

  /**
   * Returns the default field resolver.
   *
   * @todo Handle this through configuration on the server.
   *
   * Fields that don't explicitly declare a field resolver will use this one
   * as a fallback.
   *
   * @param \Drupal\graphql\GraphQL\ResolverRegistryInterface $registry
   *   The resolver registry.
   *
   * @return null|callable
   *   The default field resolver.
   */
  protected function getFieldResolver(ResolverRegistryInterface $registry) {
    return function ($value, $args, ResolveContext $context, ResolveInfo $info) use ($registry) {
      $field = new FieldContext($context, $info);
      $result = $registry
        ->resolveField($value, $args, $context, $info, $field);
      return DeferredUtility::applyFinally($result, function ($result) use ($field, $context) {
        if ($result instanceof CacheableDependencyInterface) {
          $field
            ->addCacheableDependency($result);
        }
        $context
          ->addCacheableDependency($field);
      });
    };
  }

  /**
   * Returns the error formatter.
   *
   * Allows to replace the default error formatter with a custom one. It is
   * essential when there is a need to adjust error format, for instance
   * to add an additional fields or remove some of the default ones.
   *
   * @return mixed|callable
   *   The error formatter.
   *
   * @see \GraphQL\Error\FormattedError::prepareFormatter
   */
  protected function getErrorFormatter() {
    return function (Error $error) {
      return FormattedError::createFromException($error);
    };
  }

  /**
   * Returns the error handler.
   *
   * @todo Handle this through configurable plugins on the server.
   *
   * Allows to replace the default error handler with a custom one. For example
   * when there is a need to handle specific errors differently.
   *
   * @return mixed|callable
   *   The error handler.
   *
   * @see \GraphQL\Executor\ExecutionResult::toArray
   */
  protected function getErrorHandler() {
    return function (array $errors, callable $formatter) {
      return array_map($formatter, $errors);
    };
  }

  /**
   * {@inheritDoc}
   */
  public function addPersistedQueryInstance(PersistedQueryPluginInterface $queryPlugin) : void {

    // Make sure the persistedQueryInstances are loaded before trying to add a
    // plugin to them.
    if (is_null($this->persisted_query_instances)) {
      $this
        ->getPersistedQueryInstances();
    }
    $this->persisted_query_instances[$queryPlugin
      ->getPluginId()] = $queryPlugin;
  }

  /**
   * {@inheritdoc}
   */
  public function removePersistedQueryInstance($queryPluginId) : void {

    // Make sure the persistedQueryInstances are loaded before trying to remove
    // a plugin from them.
    if (is_null($this->persisted_query_instances)) {
      $this
        ->getPersistedQueryInstances();
    }
    unset($this->persisted_query_instances[$queryPluginId]);
  }

  /**
   * {@inheritDoc}
   */
  public function removeAllPersistedQueryInstances() : void {
    $this->persisted_query_instances = NULL;
    $this->sorted_persisted_query_instances = NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getPersistedQueryInstances() {
    if (!is_null($this->persisted_query_instances)) {
      return $this->persisted_query_instances;
    }

    /** @var \Drupal\graphql\Plugin\PersistedQueryPluginManager $plugin_manager */
    $plugin_manager = \Drupal::service('plugin.manager.graphql.persisted_query');
    $definitions = $plugin_manager
      ->getDefinitions();
    $persisted_queries_settings = $this
      ->get('persisted_queries_settings');
    foreach ($definitions as $id => $definition) {
      if (isset($persisted_queries_settings[$id])) {
        $configuration = !empty($persisted_queries_settings[$id]) ? $persisted_queries_settings[$id] : [];
        $this->persisted_query_instances[$id] = $plugin_manager
          ->createInstance($id, $configuration);
      }
    }
    return $this->persisted_query_instances;
  }

  /**
   * {@inheritDoc}
   */
  public function getSortedPersistedQueryInstances() {
    if (!is_null($this->sorted_persisted_query_instances)) {
      return $this->sorted_persisted_query_instances;
    }
    $this->sorted_persisted_query_instances = $this
      ->getPersistedQueryInstances();
    if (!empty($this->sorted_persisted_query_instances)) {
      uasort($this->sorted_persisted_query_instances, function ($a, $b) {
        return $a
          ->getWeight() <= $b
          ->getWeight() ? -1 : 1;
      });
    }
    return $this->sorted_persisted_query_instances;
  }

  /**
   * Returns a callable for loading persisted queries.
   *
   * @return callable
   *   The persisted query loader.
   */
  protected function getPersistedQueryLoader() {
    return function ($id, OperationParams $params) {
      $sortedPersistedQueryInstances = $this
        ->getSortedPersistedQueryInstances();
      if (!empty($sortedPersistedQueryInstances)) {
        foreach ($sortedPersistedQueryInstances as $persistedQueryInstance) {
          $query = $persistedQueryInstance
            ->getQuery($id, $params);
          if (!is_null($query)) {
            return $query;
          }
        }
      }
    };
  }

  /**
   * Returns the validation rules to use for the query.
   *
   * @todo Handle this through configurable plugins on the server.
   *
   * May return a callable to allow the server to decide the validation rules
   * independently for each query operation.
   *
   * @code
   *
   * public function getValidationRules() {
   *   return function (OperationParams $params, DocumentNode $document, $operation) {
   *     if (isset($params->queryId)) {
   *       // Assume that pre-parsed documents are already validated. This allows
   *       // us to store pre-validated query documents e.g. for persisted queries
   *       // effectively improving performance by skipping run-time validation.
   *       return [];
   *     }
   *
   *     return array_values(DocumentValidator::defaultRules());
   *   };
   * }
   *
   * @endcode
   *
   * @return array|callable
   *   The validation rules or a callable factory.
   */
  protected function getValidationRules() {
    return function (OperationParams $params, DocumentNode $document, $operation) {
      if (isset($params->queryId)) {

        // Assume that pre-parsed documents are already validated. This allows
        // us to store pre-validated query documents e.g. for persisted queries
        // effectively improving performance by skipping run-time validation.
        return [];
      }
      return array_values(DocumentValidator::defaultRules());
    };
  }

  /**
   * {@inheritDoc}
   */
  public function preSave(EntityStorageInterface $storage) : void {

    // Write all the persisted queries configuration.
    $persistedQueryInstances = $this
      ->getPersistedQueryInstances();

    // Reset settings array after getting instances as it might be used when
    // obtaining them. This would break a config import containing persisted
    // queries settings as it would end up empty.
    $this->persisted_queries_settings = [];
    if (!empty($persistedQueryInstances)) {
      foreach ($persistedQueryInstances as $plugin_id => $plugin) {
        $this->persisted_queries_settings[$plugin_id] = $plugin
          ->getConfiguration();
      }
    }
    parent::preSave($storage);
  }

  /**
   * {@inheritdoc}
   *
   * @codeCoverageIgnore
   */
  public function postSave(EntityStorageInterface $storage, $update = TRUE) : void {
    parent::postSave($storage, $update);
    \Drupal::service('router.builder')
      ->setRebuildNeeded();
  }

  /**
   * {@inheritdoc}
   *
   * @codeCoverageIgnore
   */
  public static function postDelete(EntityStorageInterface $storage, array $entities) : void {
    parent::postDelete($storage, $entities);
    \Drupal::service('router.builder')
      ->setRebuildNeeded();
  }

}

Classes

Namesort descending Description
Server The main GraphQL configuration and request entry point.