sparql_entity_storage-8.x-1.0-alpha8/src/Driver/Database/sparql/Connection.php

src/Driver/Database/sparql/Connection.php
<?php

declare(strict_types=1);

namespace Drupal\sparql_entity_storage\Driver\Database\sparql;

use Drupal\Core\Database\Event\StatementExecutionEndEvent;
use Drupal\Core\Database\Log;
use Drupal\Core\Database\StatementInterface;
use Drupal\sparql_entity_storage\Exception\SparqlQueryException;
use EasyRdf\Graph;
use EasyRdf\Http\Exception as EasyRdfException;
use EasyRdf\Sparql\Client;
use EasyRdf\Sparql\Result;

/**
 * @addtogroup database
 * @{
 */

/**
 * SPARQL connection service.
 */
class Connection implements ConnectionInterface {

  /**
   * The client instance object that performs requests to the SPARQL endpoint.
   */
  protected Client $easyRdfClient;

  /**
   * The connection information for this connection object.
   *
   * @var array
   */
  protected array $connectionOptions;

  /**
   * The static cache of a DB statement stub object.
   */
  protected StatementInterface $statementStub;

  /**
   * The database target this connection is for.
   *
   * We need this information for later auditing and logging.
   */
  protected ?string $target = NULL;

  /**
   * The key representing this connection.
   *
   * The key is a unique string which identifies a database connection. A
   * connection can be a single server or a cluster of primary and replicas
   * (use target to pick between primary and replica).
   */
  protected ?string $key = NULL;

  /**
   * The current database logging object for this connection.
   */
  protected ?Log $logger = NULL;

  /**
   * Tracks the database API events to be dispatched.
   *
   * For performance reasons, database API events are not yielded by default.
   * Call ::enableEvents() to enable them.
   */
  private array $enabledEvents = [];

  /**
   * Constructs a new connection instance.
   *
   * @param \EasyRdf\Sparql\Client $easy_rdf_client
   *   Object of \EasyRdf\Sparql\Client which is a database connection.
   * @param array $connection_options
   *   An associative array of connection options. See the "Database settings"
   *   section from 'sites/default/settings.php' a for a detailed description of
   *   the structure of this array.
   */
  public function __construct(Client $easy_rdf_client, array $connection_options) {
    $this->easyRdfClient = $easy_rdf_client;
    $this->connectionOptions = $connection_options;
  }

  /**
   * {@inheritdoc}
   */
  public function getSparqlClient(): Client {
    return $this->easyRdfClient;
  }

  /**
   * {@inheritdoc}
   */
  public function query(string $query, array $args = [], array $options = []): Result {
    return $this->doQuery($query, $args, $options);
  }

  /**
   * {@inheritdoc}
   */
  public function constructQuery(string $query, array $args = [], array $options = []): Graph {
    return $this->doQuery($query, $args, $options);
  }

  /**
   * Execute the query against the endpoint.
   *
   * @param string $query
   *   The string query to execute.
   * @param array $args
   *   An array of arguments for the query.
   * @param array $options
   *   An associative array of options to control how the query is run.
   *
   * @return \EasyRdf\Sparql\Result|\EasyRdf\Graph
   *   The query result.
   *
   * @throws \InvalidArgumentException
   *   If $args value is passed but arguments replacement is not yet
   *   supported. To be removed in #55.
   * @throws \Drupal\sparql_entity_storage\Exception\SparqlQueryException
   *   Exception during query execution, e.g. timeout.
   *
   * @see https://github.com/ec-europa/sparql_entity_storage/issues/1
   */
  protected function doQuery(string $query, array $args = [], array $options = []) {
    // @todo Remove this in #1.
    // @see https://github.com/ec-europa/sparql_entity_storage/issues/1
    if ($args) {
      throw new \InvalidArgumentException('Replacement arguments are not yet supported.');
    }

    if ($this->logger) {
      $query_start = microtime(TRUE);
    }

    try {
      // @todo Implement argument replacement in #1.
      // @see https://github.com/ec-europa/sparql_entity_storage/issues/1
      $results = $this->easyRdfClient->query($query);
    }
    catch (EasyRdfException $exception) {
      // Re-throw the exception, but with the query as message.
      throw new SparqlQueryException("Execution of query failed with message '{$exception->getBody()}'. Query: " . $query, $exception->getCode(), $exception);
    }

    if ($this->logger) {
      $query_end = microtime(TRUE);
      $this->log($query, $args, $query_end - $query_start);
    }

    return $results;
  }

  /**
   * {@inheritdoc}
   */
  public function update(string $query, array $args = [], array $options = []): Result {
    // @todo Remove this in #1.
    // @see https://github.com/ec-europa/sparql_entity_storage/issues/1
    if ($args) {
      throw new \InvalidArgumentException('Replacement arguments are not yet supported.');
    }

    if ($this->logger) {
      $query_start = microtime(TRUE);
    }

    try {
      // @todo Implement argument replacement in #1.
      // @see https://github.com/ec-europa/sparql_entity_storage/issues/1
      $result = $this->easyRdfClient->update($query);
    }
    catch (EasyRdfException $exception) {
      // Re-throw the exception, but with the query as message.
      throw new SparqlQueryException("Execution of query failed with message '{$exception->getBody()}'. Query: " . $query, $exception->getCode(), $exception);
    }

    if ($this->logger) {
      $query_end = microtime(TRUE);
      $this->log($query, $args, $query_end - $query_start);
    }

    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function getQueryUri(): string {
    return $this->easyRdfClient->getQueryUri();
  }

  /**
   * {@inheritdoc}
   */
  public function setLogger(Log $logger): void {
    $this->logger = $logger;
  }

  /**
   * {@inheritdoc}
   */
  public function getLogger(): ?Log {
    return $this->logger;
  }

  /**
   * {@inheritdoc}
   */
  public static function open(array &$connection_options = []): Client {
    $endpoint_path = !empty($connection_options['database']) ? trim($connection_options['database'], ' /') : '';
    // After trimming the value might be ''. Testing again.
    $endpoint_path = $endpoint_path ?: 'sparql';
    $protocol = empty($connection_options['https']) ? 'http' : 'https';

    $connect_string = "{$protocol}://{$connection_options['host']}:{$connection_options['port']}/{$endpoint_path}";

    return new Client($connect_string);
  }

  /**
   * {@inheritdoc}
   */
  public function setTarget(?string $target = NULL): void {
    if (!isset($this->target)) {
      $this->target = $target;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getTarget(): ?string {
    return $this->target;
  }

  /**
   * {@inheritdoc}
   */
  public function setKey(string $key): void {
    if (!isset($this->key)) {
      $this->key = $key;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getKey(): ?string {
    return $this->key;
  }

  /**
   * {@inheritdoc}
   */
  public function getConnectionOptions(): array {
    return $this->connectionOptions;
  }

  /**
   * {@inheritdoc}
   */
  public function destroy(): void {}

  /**
   * Logs a query duration in the DB logger.
   *
   * @param string $query
   *   The query to be logged.
   * @param array $args
   *   Arguments passed to the query.
   * @param float $duration
   *   The duration of the query run.
   *
   * @throws \RuntimeException
   *   If an attempt to log was made but the logger is not started.
   */
  protected function log(string $query, array $args, float $duration): void {
    if (!$this->logger) {
      throw new \RuntimeException('Cannot log query as the logger is not started.');
    }
    $statement = $this->getStatement()->setQuery($query);
    $event = new StatementExecutionEndEvent(
      spl_object_id($statement),
      $this->getKey(),
      $this->getTarget(),
      $query,
      $args,
      $this->findCallerFromDebugBacktrace(),
      microtime(TRUE)
    );
    $this->logger->logFromEvent($event);
  }

  /**
   * Returns and statically caches a DB statement stub used to log a query.
   *
   * The Drupal core database logger cannot be swapped because, instead of being
   * injected, is hardcoded in \Drupal\Core\Database\Database::startLog(). But
   * the \Drupal\Core\Database\Log::log() is expecting a database statement of
   * type \Drupal\Core\Database\StatementInterface as first argument and the
   * SPARQL database driver uses no StatementInterface class. Workaround this
   * limitation by faking a database statement object just to honour the logger
   * requirement. We use a statement stub that only stores the connection and
   * the query to be used when logging the event.
   *
   * @return \Drupal\sparql_entity_storage\Driver\Database\sparql\StatementStub
   *   A faked statement object.
   *
   * @see \Drupal\Core\Database\Database::startLog()
   * @see \Drupal\Core\Database\Log
   * @see \Drupal\Core\Database\StatementInterface
   * @see \Drupal\sparql_entity_storage\Driver\Database\sparql\StatementStub
   * @see \Drupal\sparql_entity_storage\Driver\Database\sparql\Connection::log()
   */
  protected function getStatement(): StatementStub {
    if (!isset($this->statementStub)) {
      $this->statementStub = (new StatementStub())->setDatabaseConnection($this);
    }
    return $this->statementStub;
  }

  /**
   * Creates an array of database connection options from a URL.
   *
   * @param string $url
   *   The URL.
   * @param string $root
   *   The root directory of the Drupal installation. Some database drivers,
   *   like for example SQLite, need this information.
   *
   * @return array
   *   The connection options.
   *
   * @throws \InvalidArgumentException
   *   Exception thrown when the provided URL does not meet the minimum
   *   requirements.
   *
   * @see \Drupal\Core\Database\Database::convertDbUrlToConnectionInfo()
   *
   * @internal
   *   This method should not be called. Use
   *   \Drupal\Core\Database\Database::convertDbUrlToConnectionInfo() instead.
   */
  public static function createConnectionOptionsFromUrl($url, $root) {
    $url_components = parse_url($url);
    if (!isset($url_components['scheme'], $url_components['host'])) {
      throw new \InvalidArgumentException('Minimum requirement: driver://host');
    }

    $url_components += [
      'user' => '',
      'pass' => '',
    ];

    // Use reflection to get the namespace of the class being called.
    $reflector = new \ReflectionClass(get_called_class());

    $database = [
      'host' => $url_components['host'],
      'namespace' => $reflector->getNamespaceName(),
      'driver' => $url_components['scheme'],
    ];

    if (isset($url_components['port'])) {
      $database['port'] = $url_components['port'];
    }

    return $database;
  }

  /**
   * Creates a URL from an array of database connection options.
   *
   * @param array $connection_options
   *   The array of connection options for a database connection.
   *
   * @return string
   *   The connection info as a URL.
   *
   * @throws \InvalidArgumentException
   *   Exception thrown when the provided array of connection options does not
   *   meet the minimum requirements.
   *
   * @see \Drupal\Core\Database\Database::getConnectionInfoAsUrl()
   *
   * @internal
   *   This method should not be called. Use
   *   \Drupal\Core\Database\Database::getConnectionInfoAsUrl() instead.
   */
  public static function createUrlFromConnectionOptions(array $connection_options) {
    if (!isset($connection_options['driver'], $connection_options['database'])) {
      throw new \InvalidArgumentException("As a minimum, the connection options array must contain at least the 'driver' and 'database' keys");
    }

    $user = '';
    if (isset($connection_options['username'])) {
      $user = $connection_options['username'];
      if (isset($connection_options['password'])) {
        $user .= ':' . $connection_options['password'];
      }
      $user .= '@';
    }

    $host = empty($connection_options['host']) ? 'localhost' : $connection_options['host'];

    $db_url = $connection_options['driver'] . '://' . $user . $host;

    if (isset($connection_options['port'])) {
      $db_url .= ':' . $connection_options['port'];
    }

    $db_url .= '/' . $connection_options['database'];

    if (isset($connection_options['prefix']['default']) && $connection_options['prefix']['default'] !== '') {
      $db_url .= '#' . $connection_options['prefix']['default'];
    }

    return $db_url;
  }

  /**
   * Returns the status of a database API event toggle.
   *
   * @param string $eventName
   *   The name of the event to check.
   *
   * @return bool
   *   TRUE if the event is going to be fired by the database API, FALSE
   *   otherwise.
   */
  public function isEventEnabled(string $eventName): bool {
    return $this->enabledEvents[$eventName] ?? FALSE;
  }

  /**
   * Enables database API events dispatching.
   *
   * @param string[] $eventNames
   *   A list of database events to be enabled.
   *
   * @return $this
   */
  public function enableEvents(array $eventNames): ConnectionInterface {
    foreach ($eventNames as $eventName) {
      assert(class_exists($eventName), "Event class {$eventName} does not exist");
      $this->enabledEvents[$eventName] = TRUE;
    }
    return $this;
  }

  /**
   * Disables database API events dispatching.
   *
   * @param string[] $eventNames
   *   A list of database events to be disabled.
   *
   * @return $this
   */
  public function disableEvents(array $eventNames): ConnectionInterface {
    foreach ($eventNames as $eventName) {
      assert(class_exists($eventName), "Event class {$eventName} does not exist");
      $this->enabledEvents[$eventName] = FALSE;
    }
    return $this;
  }

  /**
   * Determine the last non-database method that called the database API.
   *
   * Traversing the call stack from the very first call made during the
   * request, we define "the routine that called this query" as the last entry
   * in the call stack that is not any method called from the namespace of the
   * database driver, is not inside the Drupal\Core\Database namespace and does
   * have a file (which excludes call_user_func_array(), anonymous functions
   * and similar). That makes the climbing logic very simple, and handles the
   * variable stack depth caused by the query builders.
   *
   * See the @link http://php.net/debug_backtrace debug_backtrace() @endlink
   * function.
   *
   * @return array
   *   This method returns a stack trace entry similar to that generated by
   *   debug_backtrace(). However, it flattens the trace entry and the trace
   *   entry before it so that we get the function and args of the function that
   *   called into the database system, not the function and args of the
   *   database call itself.
   */
  public function findCallerFromDebugBacktrace(): array {
    $stack = $this->removeDatabaseEntriesFromDebugBacktrace($this->getDebugBacktrace(), $this->getConnectionOptions()['namespace']);
    // Return the first function call whose stack entry has a 'file' key, that
    // is, it is not a callback or a closure.
    for ($i = 0; $i < count($stack); $i++) {
      if (!empty($stack[$i]['file'])) {
        return [
          'file' => $stack[$i]['file'],
          'line' => $stack[$i]['line'],
          'function' => $stack[$i + 1]['function'],
          'class' => $stack[$i + 1]['class'] ?? NULL,
          'type' => $stack[$i + 1]['type'] ?? NULL,
          'args' => $stack[$i + 1]['args'] ?? [],
        ];
      }
    }

    return [];
  }

  /**
   * Removes database related calls from a backtrace array.
   *
   * @param array $backtrace
   *   A standard PHP backtrace. Passed by reference.
   * @param string $driver_namespace
   *   The PHP namespace of the database driver.
   *
   * @return array
   *   The cleaned backtrace array.
   */
  public static function removeDatabaseEntriesFromDebugBacktrace(array $backtrace, string $driver_namespace): array {
    // Starting from the very first entry processed during the request, find
    // the first function call that can be identified as a call to a
    // method/function in the database layer.
    for ($n = count($backtrace) - 1; $n >= 0; $n--) {
      // If the call was made from a function, 'class' will be empty. We give
      // it a default empty string value in that case.
      $class = $backtrace[$n]['class'] ?? '';
      if (str_starts_with($class, __NAMESPACE__) || str_starts_with($class, $driver_namespace)) {
        break;
      }
    }

    return array_values(array_slice($backtrace, $n));
  }

  /**
   * Gets the debug backtrace.
   *
   * Wraps the debug_backtrace function to allow mocking results in PHPUnit
   * tests.
   *
   * @return array[]
   *   The debug backtrace.
   */
  protected function getDebugBacktrace(): array {
    return debug_backtrace();
  }

}

/**
 * @} End of "addtogroup database".
 */

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

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