sparql_entity_storage-8.x-1.0-alpha8/src/SparqlEntityStorage.php

src/SparqlEntityStorage.php
<?php

declare(strict_types=1);

namespace Drupal\sparql_entity_storage;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\sparql_entity_storage\Driver\Database\sparql\ConnectionInterface;
use Drupal\sparql_entity_storage\Entity\Query\Sparql\SparqlArg;
use Drupal\sparql_entity_storage\Entity\SparqlGraph;
use Drupal\sparql_entity_storage\Entity\SparqlMapping;
use Drupal\sparql_entity_storage\Exception\DuplicatedIdException;
use Drupal\sparql_entity_storage\Exception\UnsupportedCharactersException;
use EasyRdf\Graph;
use EasyRdf\Literal;
use EasyRdf\Sparql\Result;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Defines an entity storage backend that uses a Sparql endpoint.
 */
class SparqlEntityStorage extends ContentEntityStorageBase implements SparqlEntityStorageInterface {

  /**
   * Sparql database connection.
   */
  protected ConnectionInterface $sparql;

  /**
   * Language manager.
   */
  protected LanguageManagerInterface $languageManager;

  /**
   * Entity type manager.
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * The default bundle predicate.
   *
   * @var string[]
   */
  protected array $bundlePredicate = ['http://www.w3.org/1999/02/22-rdf-syntax-ns#type'];

  /**
   * The SPARQL graph helper service object.
   */
  protected SparqlEntityStorageGraphHandlerInterface $graphHandler;

  /**
   * The SPARQL field mapping service.
   */
  protected SparqlEntityStorageFieldHandlerInterface $fieldHandler;

  /**
   * The entity ID generator plugin manager.
   */
  protected SparqlEntityStorageEntityIdPluginManager $entityIdPluginManager;

  /**
   * Initialize the storage backend.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type this storage is about.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   The entity field manager.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   The cache backend to be used.
   * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
   *   The memory cache backend.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
   *   The entity type bundle info.
   * @param \Drupal\sparql_entity_storage\Driver\Database\sparql\ConnectionInterface $sparql
   *   The connection object.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler service.
   * @param \Drupal\sparql_entity_storage\SparqlEntityStorageGraphHandlerInterface $sparql_graph_handler
   *   The sPARQL graph helper service.
   * @param \Drupal\sparql_entity_storage\SparqlEntityStorageFieldHandlerInterface $sparql_field_handler
   *   The SPARQL field mapping service.
   * @param \Drupal\sparql_entity_storage\SparqlEntityStorageEntityIdPluginManager $entity_id_plugin_manager
   *   The entity ID generator plugin manager.
   */
  public function __construct(
    EntityTypeInterface $entity_type,
    EntityFieldManagerInterface $entity_field_manager,
    CacheBackendInterface $cache,
    MemoryCacheInterface $memory_cache,
    EntityTypeBundleInfoInterface $entity_type_bundle_info,
    ConnectionInterface $sparql,
    EntityTypeManagerInterface $entity_type_manager,
    LanguageManagerInterface $language_manager,
    ModuleHandlerInterface $module_handler,
    SparqlEntityStorageGraphHandlerInterface $sparql_graph_handler,
    SparqlEntityStorageFieldHandlerInterface $sparql_field_handler,
    SparqlEntityStorageEntityIdPluginManager $entity_id_plugin_manager,
  ) {
    parent::__construct($entity_type, $entity_field_manager, $cache, $memory_cache, $entity_type_bundle_info);
    $this->sparql = $sparql;
    $this->entityTypeManager = $entity_type_manager;
    $this->languageManager = $language_manager;
    $this->moduleHandler = $module_handler;
    $this->graphHandler = $sparql_graph_handler;
    $this->fieldHandler = $sparql_field_handler;
    $this->entityIdPluginManager = $entity_id_plugin_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type): self {
    return new static(
      $entity_type,
      $container->get('entity_field.manager'),
      $container->get('cache.entity'),
      $container->get('entity.memory_cache'),
      $container->get('entity_type.bundle.info'),
      $container->get('sparql.endpoint'),
      $container->get('entity_type.manager'),
      $container->get('language_manager'),
      $container->get('module_handler'),
      $container->get('sparql.graph_handler'),
      $container->get('sparql.field_handler'),
      $container->get('plugin.manager.sparql_entity_id')
    );
  }

  /**
   * Builds a new graph (list of triples).
   *
   * @param string $graph_uri
   *   The URI of the graph.
   *
   * @return \EasyRdf\Graph
   *   The EasyRdf graph object.
   */
  protected static function getGraph($graph_uri): Graph {
    return new Graph($graph_uri);
  }

  /**
   * {@inheritdoc}
   */
  public function create(array $values = []): ContentEntityInterface {
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = parent::create($values);
    // Ensure the default graph if no explicit graph has been set.
    if ($entity->get('graph')->isEmpty()) {
      $entity->set('graph', SparqlGraph::DEFAULT);
    }
    return $entity;
  }

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

  /**
   * {@inheritdoc}
   */
  public function getGraphHandler(): SparqlEntityStorageGraphHandlerInterface {
    return $this->graphHandler;
  }

  /**
   * {@inheritdoc}
   */
  public function getGraphDefinitions(): array {
    return $this->getGraphHandler()->getGraphDefinitions($this->entityTypeId);
  }

  /**
   * {@inheritdoc}
   */
  protected function doLoadMultiple(?array $ids = NULL, array $graph_ids = []) {
    // Attempt to load entities from the persistent cache. This will remove IDs
    // that were loaded from $ids.
    $entities_from_cache = $this->getFromPersistentCache($ids, $graph_ids);
    // Load any remaining entities from the database.
    $entities_from_storage = $this->getFromStorage($ids, $graph_ids);

    return $entities_from_cache + $entities_from_storage;
  }

  /**
   * Gets entities from the storage.
   *
   * @param array|null $ids
   *   If not empty, return entities that match these IDs. Return all entities
   *   when NULL.
   * @param array $graph_ids
   *   A list of graph IDs.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface[]
   *   Array of entities from the storage.
   *
   * @throws \Drupal\sparql_entity_storage\Exception\SparqlQueryException
   *   If the SPARQL query fails.
   * @throws \Exception
   *   The query fails with no specific reason.
   */
  protected function getFromStorage(?array $ids = NULL, array $graph_ids = []): array {
    if (empty($ids)) {
      return [];
    }
    $remaining_ids = $ids;
    $entities = [];
    while (count($remaining_ids)) {
      $operation_ids = array_slice($remaining_ids, 0, 50, TRUE);
      foreach ($operation_ids as $k => $v) {
        unset($remaining_ids[$k]);
      }
      $entities_values = $this->loadFromStorage($operation_ids, $graph_ids);
      if ($entities_values) {
        foreach ($entities_values as $id => $entity_values) {
          $bundle = $this->bundleKey ? $entity_values[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] : NULL;
          $langcode_key = $this->getEntityType()->getKey('langcode');
          $translations = [];
          if (!empty($entities_values[$id][$langcode_key])) {
            foreach ($entities_values[$id][$langcode_key] as $data) {
              if (!empty(reset($data)['value'])) {
                $translations[] = reset($data)['value'];
              }
            }
          }
          $entity_class = $this->getEntityClass($bundle);
          $entity = new $entity_class($entity_values, $this->entityTypeId, $bundle, $translations);
          $this->trackOriginalGraph($entity);
          $entities[$id] = $entity;
        }
        $this->invokeStorageLoadHook($entities);
        $this->setPersistentCache($entities);
      }
    }
    return $entities;
  }

  /**
   * Retrieves the entity data from the SPARQL endpoint.
   *
   * @param string[] $ids
   *   A list of entity IDs.
   * @param string[]|null $graph_ids
   *   An ordered list of candidate graph IDs.
   *
   * @return array|null
   *   The entity values indexed by the field mapping ID or NULL in there are no
   *   results.
   *
   * @throws \Drupal\sparql_entity_storage\Exception\SparqlQueryException
   *   If the SPARQL query fails.
   * @throws \Exception
   *   The query fails with no specific reason.
   */
  protected function loadFromStorage(array $ids, array $graph_ids): ?array {
    if (empty($ids)) {
      return [];
    }

    // @todo We should filter per entity per graph and not load the whole
    // database only to filter later on.
    // @see https://github.com/ec-europa/sparql_entity_storage/issues/2
    $ids_string = SparqlArg::serializeUris($ids, ' ');
    $graphs = $this->getGraphHandler()->getEntityTypeGraphUrisFlatList($this->getEntityTypeId());
    $named_graph = '';
    foreach ($graphs as $graph) {
      $named_graph .= 'FROM NAMED ' . SparqlArg::uri($graph) . "\n";
    }

    // @todo Get rid of the language filter. It's here because of eurovoc:
    // \Drupal\taxonomy\Form\OverviewTerms::buildForm loads full entities
    // of the whole tree: 7000+ terms in 24 languages is just too much.
    // @see https://github.com/ec-europa/sparql_entity_storage/issues/2
    $query = <<<QUERY
SELECT ?graph ?entity_id ?predicate ?field_value
{$named_graph}
WHERE{
  GRAPH ?graph {
    ?entity_id ?predicate ?field_value .
    VALUES ?entity_id { {$ids_string} } .
  }
}
QUERY;

    $entity_values = $this->sparql->query($query);
    return $this->processGraphResults($entity_values, $graph_ids);
  }

  /**
   * Processes results from the load query and returns a list of values.
   *
   * When an entity is loaded, the values might derive from multiple graph. This
   * function will process the results and attempt to load a published version
   * of the entity. If there is no published version available, then it will
   * fallback to the rest of the graphs.
   *
   * If the graph parameter can be used to restrict the available graphs to load
   * from.
   *
   * @param \EasyRdf\Sparql\Result|\EasyRdf\Graph $results
   *   A set of query results indexed per graph and entity id.
   * @param string[] $graph_ids
   *   Graph IDs.
   *
   * @return array|null
   *   The entity values indexed by the field mapping ID or NULL in there are no
   *   results.
   *
   * @throws \Exception
   *    Thrown when the entity graph is empty.
   *
   * @see https://github.com/ec-europa/sparql_entity_storage/issues/2
   *
   * @todo Reduce the cyclomatic complexity of this function in #19.
   */
  protected function processGraphResults($results, array $graph_ids): ?array {
    $values_per_entity = $this->deserializeGraphResults($results);
    if (empty($values_per_entity)) {
      return NULL;
    }

    $default_language = $this->languageManager->getDefaultLanguage()->getId();
    $inbound_map = $this->fieldHandler->getInboundMap($this->entityTypeId);
    $return = [];
    foreach ($values_per_entity as $entity_id => $values_per_graph) {
      $graph_uris = $this->getGraphHandler()->getEntityTypeGraphUris($this->getEntityTypeId());
      foreach ($graph_ids as $priority_graph_id) {
        foreach ($values_per_graph as $graph_uri => $entity_values) {
          // If the entity has been processed or the backend didn't returned
          // anything for this graph, jump to the next graph retrieved from the
          // SPARQL backend.
          if (isset($return[$entity_id]) || array_search($graph_uri, array_column($graph_uris, $priority_graph_id)) === FALSE) {
            continue;
          }

          $bundle = $this->getActiveBundle($entity_values);
          if (!$bundle) {
            continue;
          }

          // Check if the graph checked is in the request graphs. If there are
          // multiple graphs set, probably the default is requested with the
          // rest as fallback or it is a neutral call. If the default is
          // requested, it is going to be first in line so in any case, use the
          // first one.
          if (!$graph_id = $this->getGraphHandler()->getBundleGraphId($this->getEntityTypeId(), $bundle, $graph_uri)) {
            continue;
          }

          // Map bundle, entity ID and graph.
          if ($this->getEntityType()->hasKey('bundle')) {
            $return[$entity_id][$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] = $bundle;
          }
          $return[$entity_id][$this->idKey][LanguageInterface::LANGCODE_DEFAULT] = $entity_id;
          $return[$entity_id]['graph'][LanguageInterface::LANGCODE_DEFAULT] = $graph_id;

          foreach ($entity_values as $predicate => $field) {
            $field_name = $inbound_map['fields'][$predicate][$bundle]['field_name'] ?? NULL;
            if (empty($field_name)) {
              continue;
            }

            /** @var string  $field_name */
            $column = $inbound_map['fields'][$predicate][$bundle]['column'];
            foreach ($field as $lang => $items) {
              $langcode_key = ($lang === $default_language) ? LanguageInterface::LANGCODE_DEFAULT : $lang;
              foreach ($items as $delta => $item) {
                $item = $this->fieldHandler->getInboundValue($this->getEntityTypeId(), $field_name, $item, $langcode_key, $column, $bundle);

                if (!isset($return[$entity_id][$field_name][$langcode_key]) || !is_string($return[$entity_id][$field_name][$langcode_key])) {
                  $return[$entity_id][$field_name][$langcode_key][$delta][$column] = $item;
                }
              }
              if (is_array($return[$entity_id][$field_name][$langcode_key])) {
                $this->applyFieldDefaults($inbound_map['fields'][$predicate][$bundle]['type'], $return[$entity_id][$field_name][$langcode_key]);
              }
            }
          }
        }
      }
    }
    return $return;
  }

  /**
   * Deserializes a list of graph results to an array.
   *
   * The results array is an array of loaded entity values from different
   * graphs.
   * @code
   *    $results = [
   *      'http://entity_id.uri' => [
   *        'http://field.mapping.uri' => [
   *          'x-default' => [
   *            0 => 'actual value'
   *          ]
   *        ]
   *      ];
   * @endcode
   *
   * @param \EasyRdf\Sparql\Result $results
   *   A set of query results indexed per graph and entity id.
   *
   * @return array
   *   The entity values indexed by the field mapping id.
   */
  protected function deserializeGraphResults(Result $results): array {
    $values_per_entity = [];
    foreach ($results as $result) {
      $entity_id = (string) $result->entity_id;

      $lang = LanguageInterface::LANGCODE_DEFAULT;
      if ($result->field_value instanceof Literal) {
        $lang_temp = $result->field_value->getLang();
        if ($lang_temp) {
          $lang = $lang_temp;
        }
      }
      $values_per_entity[$entity_id][(string) $result->graph][(string) $result->predicate][$lang][] = (string) $result->field_value;
    }

    return $values_per_entity;
  }

  /**
   * Derives the bundle from the rdf:type.
   *
   * @param array $entity_values
   *   Entity in a raw formatted array.
   *
   * @return string
   *   The bundle ID string.
   *
   * @throws \Exception
   *    Thrown when the bundle is not found.
   */
  protected function getActiveBundle(array $entity_values): ?string {
    $bundles = [];
    foreach ($this->bundlePredicate as $bundle_predicate) {
      if (isset($entity_values[$bundle_predicate])) {
        $bundle_data = $entity_values[$bundle_predicate];
        $bundles += $this->fieldHandler->getInboundBundleValue($this->entityTypeId, $bundle_data[LanguageInterface::LANGCODE_DEFAULT][0]);
      }
    }
    if (empty($bundles)) {
      return NULL;
    }

    // Since it is possible to map more than one bundles to the same URI, allow
    // modules to handle this.
    $this->moduleHandler->alter('sparql_bundle_load', $entity_values, $bundles);
    if (count($bundles) > 1) {
      throw new \Exception('More than one bundles are defined for this uri.');
    }
    return reset($bundles);
  }

  /**
   * {@inheritdoc}
   */
  public function load($id, ?array $graph_ids = NULL): ?ContentEntityInterface {
    $entities = $this->loadMultiple([$id], $graph_ids);
    return array_shift($entities);
  }

  /**
   * {@inheritdoc}
   */
  public function loadMultiple(?array $ids = NULL, ?array $graph_ids = NULL): array {
    $this->checkGraphs($graph_ids);

    // We copy this part from parent::loadMultiple(), otherwise we cannot pass
    // the $graph_ids to self::getFromStaticCache() and self::doLoadMultiple().
    // START parent::loadMultiple() fork.
    $entities = [];

    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
    if ($ids) {
      foreach ($ids as $id) {
        if ($unsupported_characters = SparqlArg::getUnsupportedUriCharacters($id)) {
          throw new UnsupportedCharactersException("The entity ID '$id' contains unsupported characters: " . implode(', ', $unsupported_characters));
        }
      }

      $entities += $this->getFromStaticCache($ids, $graph_ids);
      if ($passed_ids) {
        $ids = array_keys(array_diff_key($passed_ids, $entities));
      }
    }
    if ($ids === NULL || $ids) {
      $queried_entities = $this->doLoadMultiple($ids, $graph_ids);
    }
    if (!empty($queried_entities)) {
      $this->postLoad($queried_entities);
      $entities += $queried_entities;

      // Add the entities that were loaded from storage to the memory cache.
      $this->setStaticCache($queried_entities);
    }
    if ($passed_ids) {
      $passed_ids = array_intersect_key($passed_ids, $entities);
      foreach ($entities as $entity) {
        $passed_ids[$entity->id()] = $entity;
      }
      $entities = $passed_ids;
    }
    // END parent::loadMultiple() fork.
    return $entities;
  }

  /**
   * {@inheritdoc}
   */
  protected function doPreSave(EntityInterface $entity) {
    // The code bellow is forked from EntityStorageBase::doPreSave() and
    // ContentEntityStorageBase::doPreSave(). We are not using the original
    // methods in order to be able to pass an additional list of graphs
    // parameter to ::loadUnchanged() method.
    // START forking from ContentEntityStorageBase::doPreSave().
    /** @var \Drupal\Core\Entity\ContentEntityBase $entity */
    $entity->updateOriginalValues();
    if ($entity->getEntityType()->isRevisionable() && !$entity->isNew() && empty($entity->getLoadedRevisionId())) {
      $entity->updateLoadedRevisionId();
    }

    // START forking from EntityStorageBase::doPreSave().
    $id = $entity->id();
    if ($entity->getOriginalId() !== NULL) {
      $id = $entity->getOriginalId();
    }
    $id_exists = $this->has($id, $entity);
    if ($id_exists && $entity->isNew()) {
      throw new EntityStorageException("'{$this->entityTypeId}' entity with ID '$id' already exists.");
    }
    if ($id_exists && !isset($entity->original)) {
      // In the case when the entity graph has been changed before saving, we
      // need the original graph, so that we load the original/unchanged entity
      // from the backend. This property was set in during entity load, in
      // ::trackOriginalGraph(). We can rely on this property also when the
      // entity us saved via UI, as this value persists in entity over an entity
      // form submit, because the entity is stored in the form state.
      // @see \Drupal\sparql_entity_storage\SparqlEntityStorage::trackOriginalGraph()
      $entity->original = $this->loadUnchanged($id, [$entity->sparqlEntityOriginalGraph]);
    }
    $entity->preSave($this);
    $this->invokeHook('presave', $entity);
    // END forking from EntityStorageBase::doPreSave().
    if (!$entity->isNew()) {
      if (empty($entity->original) || $entity->id() != $entity->original->id()) {
        throw new EntityStorageException("Update existing '{$this->entityTypeId}' entity while changing the ID is not supported.");
      }
      if (!$entity->isNewRevision() && $entity->getRevisionId() != $entity->getLoadedRevisionId()) {
        throw new EntityStorageException("Update existing '{$this->entityTypeId}' entity revision while changing the revision ID is not supported.");
      }
    }
    // END forking from ContentEntityStorageBase::doPreSave().
    // Finally reset the entity original graph property so that that its updated
    // value is available for the rest of this request.
    $this->trackOriginalGraph($entity);

    return $id;
  }

  /**
   * {@inheritdoc}
   */
  public function loadUnchanged($id, ?array $graph_ids = NULL): ?ContentEntityInterface {
    $this->checkGraphs($graph_ids);

    // START: Code forked from parent::loadUnchanged() and adapted to accept
    // graph candidates.
    $ids = [$id];
    $this->resetCache($ids);

    $entities = $this->getFromPersistentCache($ids, $graph_ids);
    if (!$entities) {
      $entities[$id] = $this->load($id, $graph_ids);
    }
    else {
      $this->postLoad($entities);
      $this->setStaticCache($entities);
    }

    return $entities[$id];
    // END: Code forked from parent::loadUnchanged().
  }

  /**
   * {@inheritdoc}
   */
  public function loadRevision($revision_id) {
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function deleteRevision($revision_id) {
  }

  /**
   * {@inheritdoc}
   */
  public function deleteFromGraph(array $entities, string $graph_id): void {
    if (!empty($entities)) {
      $ids = array_map(function (ContentEntityInterface $entity): string {
        return $entity->id();
      }, $entities);
      // Make sure that passed entities are keyed by entity ID and are loaded
      // only from the requested graph.
      $entities = $this->loadMultiple($ids, [$graph_id]);
      $this->doDelete($entities);
      $this->resetCache(array_keys($entities));
    }
  }

  /**
   * {@inheritdoc}
   */
  public function hasGraph(EntityInterface $entity, string $graph_id): bool {
    $graph_uri = $this->getGraphHandler()->getBundleGraphUri($entity->getEntityTypeId(), $entity->bundle(), $graph_id);
    return $this->idExists($entity->id(), $graph_uri);
  }

  /**
   * {@inheritdoc}
   */
  public function loadByProperties(array $values = [], ?array $graph_ids = NULL): array {
    $this->checkGraphs($graph_ids);

    /** @var \Drupal\sparql_entity_storage\Entity\Query\Sparql\SparqlQueryInterface $query */
    $query = $this->getQuery()
      ->graphs($graph_ids)
      ->accessCheck(FALSE);
    $this->buildPropertyQuery($query, $values);
    $result = $query->execute();

    return $result ? $this->loadMultiple($result, $graph_ids) : [];
  }

  /**
   * {@inheritdoc}
   */
  public function delete(array $entities) {
    if (!$entities) {
      // If no entities were passed, do nothing.
      return;
    }

    // Ensure that the entities are keyed by ID.
    $keyed_entities = [];
    foreach ($entities as $entity) {
      $keyed_entities[$entity->id()] = $entity;
    }

    $entities_by_class = $this->getEntitiesByClass($entities);

    // Allow code to run before deleting.
    foreach ($entities_by_class as $entity_class => &$items) {
      $entity_class::preDelete($this, $items);
      foreach ($items as $entity) {
        $this->invokeHook('predelete', $entity);
      }
    }

    $entities_by_graph = [];
    /** @var \Drupal\Core\Entity\EntityInterface $keyed_entity */
    foreach ($keyed_entities as $keyed_entity) {
      // Determine all possible graphs for the entity.
      $graphs_by_bundle = $this->getGraphHandler()->getEntityTypeGraphUris($this->getEntityTypeId());
      $graphs = $graphs_by_bundle[$keyed_entity->bundle()];
      foreach ($graphs as $graph_uri) {
        $entities_by_graph[$graph_uri][$keyed_entity->id()] = $keyed_entity;
      }
    }
    /** @var string $id */
    foreach ($entities_by_graph as $graph => $entities_to_delete) {
      $this->doDeleteFromGraph($entities_to_delete, $graph);
    }
    $this->resetCache(array_keys($keyed_entities), array_keys($graphs));

    // Allow code to run after deleting.
    foreach ($entities_by_class as $entity_class => &$items) {
      $entity_class::postDelete($this, $items);
      foreach ($items as $entity) {
        $this->invokeHook('delete', $entity);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function doDelete($entities) {
    $entities_by_graph = [];
    /** @var string $id */
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    foreach ($entities as $id => $entity) {
      $graph_uri = $this->getGraphHandler()->getBundleGraphUri($entity->getEntityTypeId(), $entity->bundle(), (string) $entity->get('graph')->target_id);
      $entities_by_graph[$graph_uri][$id] = $entity;
    }
    foreach ($entities_by_graph as $graph_uri => $entities_to_delete) {
      $this->doDeleteFromGraph($entities, $graph_uri);
    }
  }

  /**
   * Constructs and execute the delete query.
   *
   * @param array $entities
   *   An array of entity objects to delete.
   * @param string $graph_uri
   *   The graph URI to delete from.
   *
   * @throws \Exception
   *   The query fails with no specific reason.
   */
  protected function doDeleteFromGraph(array $entities, string $graph_uri): void {
    $entity_list = SparqlArg::serializeUris(array_keys($entities));

    $query = <<<QUERY
DELETE FROM <{$graph_uri}>
{
  ?entity ?field ?value
}
WHERE
{
  ?entity ?field ?value
  FILTER(
    ?entity IN ( {$entity_list} )
  )
}
QUERY;
    $this->sparql->query($query);
  }

  /**
   * {@inheritdoc}
   */
  protected function getQueryServiceName() {
    return 'entity.query.sparql';
  }

  /**
   * {@inheritdoc}
   */
  protected function doLoadRevisionFieldItems($revision_id) {
  }

  /**
   * {@inheritdoc}
   */
  protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
  }

  /**
   * {@inheritdoc}
   */
  protected function doDeleteFieldItems($entities) {
  }

  /**
   * {@inheritdoc}
   */
  protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
  }

  /**
   * {@inheritdoc}
   */
  protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definition, $batch_size) {
    return [];
  }

  /**
   * {@inheritdoc}
   */
  protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefinitionInterface $field_definition) {
  }

  /**
   * {@inheritdoc}
   */
  protected function doSave($id, EntityInterface $entity) {
    $bundle = $entity->bundle();
    // Generate an ID before saving, if none is available. If the ID generation
    // occurs earlier in the process (like on EntityInterface::create()), the
    // entity might be considered not new by modules that don't strictly use the
    // EntityInterface::isNew() method.
    if (empty($id)) {
      $id = $this->entityIdPluginManager->getPlugin($entity)->generate();
      $entity->{$this->idKey} = $id;
    }
    elseif ($unsupported_characters = SparqlArg::getUnsupportedUriCharacters($id)) {
      // Double quotes are not supported.
      throw new UnsupportedCharactersException("The entity ID '$id' contains unsupported characters: " . implode(', ', $unsupported_characters));
    }
    elseif ($entity->isNew() && $this->idExists($id)) {
      throw new DuplicatedIdException("Attempting to create a new entity with the ID '$id' already taken.");
    }

    // If the graph is not specified, fallback to the default one for the entity
    // type.
    if ($entity->get('graph')->isEmpty()) {
      $entity->set('graph', $this->getGraphHandler()->getDefaultGraphId($this->getEntityTypeId()));
    }

    $graph_id = $entity->get('graph')->target_id;
    $graph_uri = $this->getGraphHandler()->getBundleGraphUri($entity->getEntityTypeId(), $entity->bundle(), $graph_id);
    $graph = self::getGraph($graph_uri);
    $lang_array = $this->toLangArray($entity);
    foreach ($lang_array as $field_name => $langcode_data) {
      foreach ($langcode_data as $langcode => $field_item) {
        foreach ($field_item as $column_data) {
          foreach ($column_data as $column => $value) {
            // Filter out empty values or non mapped fields. The id is also
            // excluded as it is not mapped.
            if ($value === NULL || $value === '' || !$this->fieldHandler->hasFieldPredicate($this->getEntityTypeId(), $bundle, $field_name, $column)) {
              continue;
            }
            $predicate = $this->fieldHandler->getFieldPredicates($this->getEntityTypeId(), $field_name, $column, $bundle);
            $predicate = reset($predicate);
            $value = $this->fieldHandler->getOutboundValue($this->getEntityTypeId(), $field_name, $value, $langcode, $column, $bundle);
            $graph->add((string) $id, $predicate, $value);
          }
        }
      }
    }

    // Entities without bundles should add the rdf:type too.
    if (!$this->getEntityType()->hasKey('bundle')) {
      $predicate = reset($this->bundlePredicate);
      $mapping = SparqlMapping::loadByName($this->getEntityTypeId(), $bundle);
      $graph->add((string) $id, $predicate, $mapping->getRdfType());
    }

    // Give implementations a chance to alter the graph right before is saved.
    $this->alterGraph($graph, $entity);

    try {
      $this->update($graph, $graph_uri, $entity);
      return $entity->isNew() ? SAVED_NEW : SAVED_UPDATED;
    }
    catch (\Exception $e) {
      return FALSE;
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function doPostSave(EntityInterface $entity, $update) {
    parent::doPostSave($entity, $update);

    // After saving, this is now the "original entity", but subsequent saves
    // must be able to reference the original graph.
    // @see \Drupal\Core\Entity\EntityStorageBase::doPostSave()
    $this->trackOriginalGraph($entity);
  }

  /**
   * In this method the latest values have to be applied to the entity.
   *
   * The end array should have an index with the x-default language which should
   * be the default language to save and one index for each other translation.
   *
   * Since the user can be presented with non translatable fields in the
   * translation form, the process has to give priority to the values of the
   * current language over the default language.
   *
   * So, the process is:
   * - If the current language is the default one, add all fields to the
   *   x-default index.
   * - If the current language is not the default language, then the default
   * - language will only provide the translatable fields as default and the
   *   non-translatable will be filled by the current language.
   * - All the other languages, will only provide the translatable fields.
   *
   * Only t_literal fields should be translatable.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity to convert to an array of values.
   *
   * @return array
   *   The array of values including the translations.
   */
  protected function toLangArray(ContentEntityInterface $entity): array {
    $values = [];
    $languages = array_keys(array_filter($entity->getTranslationLanguages(), function (LanguageInterface $language) {
      return !$language->isLocked();
    }));
    $translatable_fields = array_keys($entity->getTranslatableFields());
    $fields = array_keys($entity->getFields());
    $non_translatable_fields = array_diff($fields, $translatable_fields);

    $current_langcode = $entity->language()->getId();
    if ($entity->isDefaultTranslation()) {
      foreach ($entity->getFields(FALSE) as $name => $field_item_list) {
        if (!$field_item_list->isEmpty()) {
          $values[$name][$current_langcode] = $field_item_list->getValue();
        }
      }
      $processed = [$entity->language()->getId()];
    }
    else {
      // Fill in the translatable fields of the default language and then all
      // the fields from the current language.
      $default_translation = $entity->getUntranslated();
      $default_langcode = $default_translation->language()->getId();
      foreach ($translatable_fields as $name) {
        $values[$name][$default_langcode] = $default_translation->get($name)->getValue();
      }
      // For the current language, add the translatable fields as a translation
      // and the non translatable fields as default.
      foreach ($non_translatable_fields as $name) {
        $values[$name][$default_langcode] = $entity->get($name)->getValue();
      }
      // The current language is not included in the translations if it is a
      // new translation and is outdated if it is not a new translation.
      // Thus, the handling occurs here, instead of the generic handling below.
      foreach ($translatable_fields as $name) {
        $values[$name][$current_langcode] = $entity->get($name)->getValue();
      }

      $processed = [$current_langcode, $default_langcode];
    }

    // For the rest of the languages not computed above, simply add the
    // translatable fields. This will prevent data loss from the database.
    foreach (array_diff($languages, $processed) as $langcode) {
      if (!$entity->hasTranslation($langcode)) {
        continue;
      }
      $translation = $entity->getTranslation($langcode);
      foreach ($translatable_fields as $name) {
        $item_list = $translation->get($name);
        if (!$item_list->isEmpty()) {
          $values[$name][$langcode] = $item_list->getValue();
        }
      }
    }
    return $values;
  }

  /**
   * Resolves the language based on entity and current site language.
   *
   * @param string $entity_type_id
   *   The entity type id.
   * @param string $field_name
   *   The field name for which to resolve the language.
   * @param string $langcode
   *   A default langcode or the fields detected langcode.
   *
   * @return string|null
   *   A language code or NULL, if the field has no language.
   *
   * @throws \Exception
   *   Thrown when a non existing field is requested.
   */
  protected function resolveFieldLangcode($entity_type_id, $field_name, $langcode = NULL): ?string {
    $format = $this->fieldHandler->getFieldFormat($entity_type_id, $field_name);
    $non_languages = [
      LanguageInterface::LANGCODE_NOT_SPECIFIED,
      LanguageInterface::LANGCODE_DEFAULT,
      LanguageInterface::LANGCODE_NOT_APPLICABLE,
      LanguageInterface::LANGCODE_SITE_DEFAULT,
      LanguageInterface::LANGCODE_SYSTEM,
    ];

    if ($format == SparqlEntityStorageFieldHandlerInterface::TRANSLATABLE_LITERAL && !empty($langcode) && !in_array($langcode, $non_languages)) {
      return $langcode;
    }

    $langcode = $this->languageManager->getCurrentLanguage()->getId();
    if (in_array($langcode, $non_languages)) {
      return NULL;
    }
    return $langcode;
  }

  /**
   * Alters the graph before saving the entity.
   *
   * Implementations are able to change, delete or add items to the graph before
   * this is saved to SPARQL backend.
   *
   * @param \EasyRdf\Graph $graph
   *   The graph to be altered.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity being saved.
   */
  protected function alterGraph(Graph &$graph, EntityInterface $entity): void {}

  /**
   * Insert a graph of triples.
   *
   * @param \EasyRdf\Graph $graph
   *   The graph to insert.
   * @param string $graph_uri
   *   Graph to save to.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity being saved.
   *
   * @return \EasyRdf\Sparql\Result
   *   Response.
   *
   * @throws \Drupal\sparql_entity_storage\Exception\SparqlQueryException
   *   If the SPARQL query fails.
   * @throws \Exception
   *   The query fails with no specific reason.
   */
  protected function update(Graph $graph, string $graph_uri, EntityInterface $entity): Result {
    $triples = $graph->serialise('ntriples');
    $id = SparqlArg::uri($entity->id());
    $graph_uri = SparqlArg::uri($graph_uri);

    $delete_clause = $where_clause = '';
    if (!$entity->isNew() && $this->idExists($entity->id(), $graph_uri)) {
      $property_list = $this->fieldHandler->getPropertyListToArray($this->getEntityTypeId());
      $serialized = SparqlArg::serializeUris($property_list);
      $delete_clause = "DELETE { {$id} ?field ?value }";
      $where_clause = "WHERE { {$id} ?field ?value . FILTER (?field IN ( {$serialized} )) }";
    }

    $query = <<<QUERY
      WITH {$graph_uri}
      {$delete_clause}
      INSERT {
        {$triples}
      }
      {$where_clause}
      QUERY;

    return $this->sparql->update($query);
  }

  /**
   * {@inheritdoc}
   */
  protected function has($id, EntityInterface $entity) {
    return !$entity->isNew();
  }

  /**
   * {@inheritdoc}
   */
  public function countFieldData($storage_definition, $as_bool = FALSE) {
    return $as_bool ? FALSE : 0;
  }

  /**
   * {@inheritdoc}
   */
  public function hasData() {
    return FALSE;
  }

  /**
   * Allow overrides for some field types.
   *
   * @param string $type
   *   The field type.
   * @param array $values
   *   The field values.
   *
   * @todo To be removed when columns will be supported. No need to manually
   * set this.
   */
  protected function applyFieldDefaults($type, array &$values): void {
    if (empty($values)) {
      return;
    }
    foreach ($values as &$value) {
      // Textfield: provide default filter when filter not mapped.
      switch ($type) {
        case 'text_long':
          if (!isset($value['format'])) {
            $value['format'] = 'full_html';
          }
          break;

        // Strip timezone part in dates.
        // @todo Move in InboundOutboundValueSubscriber::massageInboundValue()
        case 'datetime':
          $time_stamp = (int) $value['value'];
          // $time_stamp = strtotime($value['value']);.
          $date = date('o-m-d', $time_stamp) . "T" . date('H:i:s', $time_stamp);
          $value['value'] = $date;
          break;
      }
    }
    $this->moduleHandler->alter('sparql_apply_default_fields', $type, $values);
  }

  /**
   * {@inheritdoc}
   */
  protected function getFromStaticCache(array $ids, array $graph_ids = []) {
    $entities = [];
    if (!$this->entityType->isStaticallyCacheable()) {
      return $entities;
    }

    // If there are more than one graphs in the request, return only the first
    // one, if exists. If the first candidate doesn't exist in the static
    // cache, we don't pick up the following because the first might be
    // available later in the persistent cache or in the storage.
    $graph_id = reset($graph_ids);

    // Load any available entities from the internal cache.
    foreach ($ids as $id) {
      if ($cached = $this->memoryCache->get($this->buildCacheId($id, $graph_id))) {
        $entities[$id] = $cached->data;
      }
    }
    return $entities;
  }

  /**
   * {@inheritdoc}
   */
  protected function setStaticCache(array $entities) {
    // The parent method is overridden to account for the entity graph.
    if ($this->entityType->isStaticallyCacheable()) {
      foreach ($entities as $id => $entity) {
        $cid = $this->buildCacheId($id, $entity->get('graph')->target_id);
        $this->memoryCache->set($cid, $entity, MemoryCacheInterface::CACHE_PERMANENT, [$this->memoryCacheTag]);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function getFromPersistentCache(?array &$ids = NULL, array $graph_ids = []) {
    if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) {
      return [];
    }

    $graph_id = reset($graph_ids);

    $entities = [];
    // Build the list of cache entries to retrieve.
    $cid_map = [];
    foreach ($ids as $id) {
      $cid_map[$id] = $this->buildCacheId($id, $graph_id);
    }
    $cids = array_values($cid_map);
    if ($cache = $this->cacheBackend->getMultiple($cids)) {
      // Get the entities that were found in the cache.
      foreach ($ids as $index => $id) {
        $cid = $cid_map[$id];
        if (isset($cache[$cid])) {
          $entities[$id] = $cache[$cid]->data;
          unset($ids[$index]);
        }
      }
    }
    return $entities;
  }

  /**
   * {@inheritdoc}
   */
  protected function setPersistentCache($entities) {
    if (!$this->entityType->isPersistentlyCacheable()) {
      return;
    }

    $cache_tags = [
      $this->entityTypeId . '_values',
      'entity_field_info',
    ];
    foreach ($entities as $id => $entity) {
      $cid = $this->buildCacheId($id, $entity->get('graph')->target_id);
      $this->cacheBackend->set($cid, $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function resetCache(?array $ids = NULL, ?array $graph_ids = NULL): void {
    if ($graph_ids && !$ids) {
      throw new \InvalidArgumentException('Passing a value in $graphs_ids works only when used with non-null $ids.');
    }

    $this->checkGraphs($graph_ids, TRUE);
    if ($ids) {
      $cids = [];
      foreach ($graph_ids as $graph) {
        foreach ($ids as $id) {
          $cids[] = $this->buildCacheId($id, $graph);
        }
      }

      if ($this->entityType->isStaticallyCacheable() && $cids) {
        $this->memoryCache->deleteMultiple($cids);
      }
      if ($this->entityType->isPersistentlyCacheable()) {
        $this->cacheBackend->deleteMultiple($cids);
      }
      return;
    }
    parent::resetCache();
  }

  /**
   * {@inheritdoc}
   */
  protected function buildCacheId($id, ?string $graph_id = NULL): string {
    if (empty($graph_id)) {
      // We need to have the graph ID nullable because of the parent method
      // signature. However, the cache ID must always include the graph ID.
      throw new \InvalidArgumentException('The graph ID is required to build the cache ID.');
    }
    return "values:{$this->entityTypeId}:$id:$graph_id";
  }

  /**
   * {@inheritdoc}
   */
  public function idExists(string $id, ?string $graph = NULL): bool {
    $id = SparqlArg::uri($id);
    $predicates = SparqlArg::serializeUris($this->bundlePredicate, ' ');
    if ($graph) {
      $graph = SparqlArg::uri($graph);
      $query = "ASK WHERE { GRAPH $graph { $id ?type ?o . VALUES ?type { $predicates } } }";
    }
    else {
      $query = "ASK { $id ?type ?value . VALUES ?type { $predicates } }";
    }

    return $this->sparql->query($query)->isTrue();
  }

  /**
   * Validates a list of graphs and provide defaults.
   *
   * @param string[]|null $graph_ids
   *   An ordered list of candidate graph IDs.
   * @param bool $check_all_graphs
   *   (optional) If to check all graphs. By default, only the default graphs
   *   are checked.
   *
   * @throws \InvalidArgumentException
   *   If at least one of passed graphs doesn't exist for this entity type.
   */
  protected function checkGraphs(?array &$graph_ids = NULL, bool $check_all_graphs = FALSE): void {
    if (!$graph_ids) {
      if ($check_all_graphs) {
        // No passed graph means "all graphs for this entity type".
        $graph_ids = $this->getGraphHandler()->getEntityTypeGraphIds($this->getEntityTypeId());
      }
      else {
        // No passed graph means "all default graphs for this entity type".
        $graph_ids = $this->getGraphHandler()->getEntityTypeDefaultGraphIds($this->getEntityTypeId());
      }
      return;
    }

    $entity_type_graph_ids = $this->getGraphHandler()->getEntityTypeGraphIds($this->getEntityTypeId());

    // Validate each passed graph.
    array_walk($graph_ids, function (string $graph_id) use ($entity_type_graph_ids): void {
      if (!in_array($graph_id, $entity_type_graph_ids)) {
        throw new \InvalidArgumentException("Graph '$graph_id' doesn't exist for entity type '{$this->getEntityTypeId()}'.");
      }
    });
  }

  /**
   * Keep track of the originating graph of an entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
   */
  protected function trackOriginalGraph(EntityInterface $entity): void {
    // Store the graph ID of the loaded entity to be, eventually, used when this
    // entity gets saved. During the saving process, this value is passed to
    // SparqlEntityStorage::loadUnchanged() to correctly determine the
    // original entity graph. This value persists in entity over an entity form
    // submit, as the entity is stored in the form state, so that the entity
    // save can rely on it.
    // @see \Drupal\sparql_entity_storage\SparqlEntityStorage::doPreSave()
    // @see \Drupal\Core\Entity\EntityForm
    $entity->sparqlEntityOriginalGraph = $entity->get('graph')->target_id;
  }

  /**
   * {@inheritdoc}
   */
  protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
    return [];
  }

}

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

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