From d6a061ae30f0a149da90c285e522c1d41d785ae8 Mon Sep 17 00:00:00 2001 From: Sander Van Dooren Date: Fri, 8 Mar 2019 17:43:16 +0100 Subject: [PATCH 1/4] Refactor graph loader for readability. --- rdf_entity.services.yml | 3 + src/Entity/RdfEntitySparqlStorage.php | 182 ++++++++++------------- src/RawEntity.php | 164 +++++++++++++++++++++ src/RawEntityRepository.php | 200 ++++++++++++++++++++++++++ src/SparqlResultFilter.php | 28 ++++ 5 files changed, 470 insertions(+), 107 deletions(-) create mode 100644 src/RawEntity.php create mode 100644 src/RawEntityRepository.php create mode 100644 src/SparqlResultFilter.php diff --git a/rdf_entity.services.yml b/rdf_entity.services.yml index 7b36dfb1..14b9a70c 100644 --- a/rdf_entity.services.yml +++ b/rdf_entity.services.yml @@ -55,3 +55,6 @@ services: arguments: ['@typed_data_manager'] tags: - { name: event_subscriber } + rdf_entity.sparql_result.filter: + class: Drupal\rdf_entity\SparqlResultFilter + arguments: ['@sparql.graph_handler'] \ No newline at end of file diff --git a/src/Entity/RdfEntitySparqlStorage.php b/src/Entity/RdfEntitySparqlStorage.php index c3d14b65..c893eedb 100644 --- a/src/Entity/RdfEntitySparqlStorage.php +++ b/src/Entity/RdfEntitySparqlStorage.php @@ -25,6 +25,10 @@ use Drupal\rdf_entity\RdfEntitySparqlStorageInterface; use Drupal\rdf_entity\RdfFieldHandlerInterface; use Drupal\rdf_entity\RdfGraphHandlerInterface; +use Drupal\rdf_entity\RdfInterface; +use Drupal\rdf_entity\RawEntity; +use Drupal\rdf_entity\SparqlResultFilter; +use Drupal\rdf_entity\RawEntityRepository; use EasyRdf\Graph; use EasyRdf\Literal; use EasyRdf\Sparql\Result; @@ -97,6 +101,8 @@ class RdfEntitySparqlStorage extends ContentEntityStorageBase implements RdfEnti */ protected $entityIdPluginManager; + protected $filter; + /** * Initialize the storage backend. * @@ -123,7 +129,7 @@ class RdfEntitySparqlStorage extends ContentEntityStorageBase implements RdfEnti * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache * The memory cache backend. */ - public function __construct(EntityTypeInterface $entity_type, ConnectionInterface $sparql, EntityManagerInterface $entity_manager, EntityTypeManagerInterface $entity_type_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, RdfGraphHandlerInterface $rdf_graph_handler, RdfFieldHandlerInterface $rdf_field_handler, RdfEntityIdPluginManager $entity_id_plugin_manager, MemoryCacheInterface $memory_cache = NULL) { + public function __construct(EntityTypeInterface $entity_type, ConnectionInterface $sparql, EntityManagerInterface $entity_manager, EntityTypeManagerInterface $entity_type_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, RdfGraphHandlerInterface $rdf_graph_handler, RdfFieldHandlerInterface $rdf_field_handler, RdfEntityIdPluginManager $entity_id_plugin_manager, SparqlResultFilter $filter, MemoryCacheInterface $memory_cache = NULL) { parent::__construct($entity_type, $entity_manager, $cache, $memory_cache); $this->sparql = $sparql; $this->languageManager = $language_manager; @@ -132,6 +138,7 @@ public function __construct(EntityTypeInterface $entity_type, ConnectionInterfac $this->graphHandler = $rdf_graph_handler; $this->fieldHandler = $rdf_field_handler; $this->entityIdPluginManager = $entity_id_plugin_manager; + $this->filter = $filter; } /** @@ -149,6 +156,7 @@ public static function createInstance(ContainerInterface $container, EntityTypeI $container->get('sparql.graph_handler'), $container->get('sparql.field_handler'), $container->get('plugin.manager.rdf_entity.id'), + $container->get('rdf_entity.sparql_result.filter'), // We support also Drupal 8.5.x. $container->has('entity.memory_cache') ? $container->get('entity.memory_cache') : NULL ); @@ -291,12 +299,12 @@ protected function loadFromStorage(array $ids, array $graph_ids): ?array { // of the whole tree: 7000+ terms in 24 languages is just too much. // @see https://github.com/ec-europa/rdf_entity/issues/19 $query = <<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 and entity id. - $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; - - $rdf_type = NULL; - foreach ($entity_values as $predicate => $field) { - $field_name = isset($inbound_map['fields'][$predicate][$bundle]['field_name']) ? $inbound_map['fields'][$predicate][$bundle]['field_name'] : NULL; - if (empty($field_name)) { - continue; - } - - $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]); - } - } - } - } + $entity_repo = new RawEntityRepository(); + $entity_repo->createFromResult($results); + $fileredResultSet = $this->filter->filter($entity_repo, $graph_ids, $this->getEntityTypeId()); + + $entity_list = []; + foreach ($fileredResultSet as $raw_entity) { + if ($entity = $this->hydrateEntity($raw_entity)){ + $entity_list[$raw_entity->getSubject()] = $entity; } } - return $return; + return $entity_list; } /** - * 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' - * ] - * ] - * ]; - * @code - * - * @param \EasyRdf\Sparql\Result|\EasyRdf\Result $results - * A set of query results indexed per graph and entity id. + * @param \Drupal\rdf_entity\RawEntity $raw_entity * * @return array - * The entity values indexed by the field mapping id. + * @throws \Exception */ - protected function deserializeGraphResults(Result $results): array { - $values_per_entity = []; - foreach ($results as $result) { - $entity_id = (string) $result->entity_id; - $entity_graphs[$entity_id] = (string) $result->graph; - - $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; + protected function hydrateEntity(RawEntity $raw_entity): array { + $entity_array = []; + $bundle = $this->getActiveBundle($raw_entity); + if (!$bundle) { + return NULL; } + // Map bundle and entity_array id. + $entity_array[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] = $bundle; + $entity_array[$this->idKey][LanguageInterface::LANGCODE_DEFAULT] = $raw_entity->getSubject(); + $entity_array['graph'][LanguageInterface::LANGCODE_DEFAULT] = $raw_entity->getGraphUri(); - return $values_per_entity; + foreach ($raw_entity as $predicate => $field) { + $entity_array = $this->attachField($predicate, $bundle, $field, $entity_array); + } + return $entity_array; } /** * Derives the bundle from the rdf:type. * - * @param array $entity_values + * @param RawEntity $entity_values * Entity in a raw formatted array. * * @return string @@ -452,12 +385,12 @@ protected function deserializeGraphResults(Result $results): array { * @throws \Exception * Thrown when the bundle is not found. */ - protected function getActiveBundle(array $entity_values): ?string { + protected function getActiveBundle(RawEntity $entity_values): ?string { $bundle_predicates = $this->bundlePredicate; $bundles = []; foreach ($bundle_predicates as $bundle_predicate) { - if (isset($entity_values[$bundle_predicate])) { - $bundle_data = $entity_values[$bundle_predicate]; + if ($entity_values->hasPredicate($bundle_predicate)) { + $bundle_data = $entity_values->getObjectDataByPredicate($bundle_predicate); $bundles += $this->fieldHandler->getInboundBundleValue($this->entityTypeId, $bundle_data[LanguageInterface::LANGCODE_DEFAULT][0]); } } @@ -1328,4 +1261,39 @@ protected function trackOriginalGraph(EntityInterface $entity): void { $entity->rdfEntityOriginalGraph = $entity->get('graph')->target_id; } + /** + * Hydrates a field and attaches it to the entity structure. + * @param $predicate + * The field predicate. + * @param string $bundle + * @param array $field + * @param array $entity + * + * @return array + */ + protected function attachField($predicate, string $bundle, array $field, array $entity): array { + $inbound_map = $this->fieldHandler->getInboundMap($this->entityTypeId); + $field_name = isset($inbound_map['fields'][$predicate][$bundle]['field_name']) ? $inbound_map['fields'][$predicate][$bundle]['field_name'] : NULL; + if (empty($field_name)) { + return $entity; + } + assert(is_string($field_name), 'Field name must be string'); + $default_language = $this->languageManager->getDefaultLanguage()->getId(); + $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($entity[$field_name][$langcode_key]) || !is_string($entity[$field_name][$langcode_key])) { + $entity[$field_name][$langcode_key][$delta][$column] = $item; + } + } + if (is_array($entity[$field_name][$langcode_key])) { + $this->applyFieldDefaults($inbound_map['fields'][$predicate][$bundle]['type'], $entity[$field_name][$langcode_key]); + } + } + return $entity; + } + } diff --git a/src/RawEntity.php b/src/RawEntity.php new file mode 100644 index 00000000..9f97bec9 --- /dev/null +++ b/src/RawEntity.php @@ -0,0 +1,164 @@ +graph = $graph; + $this->subject = $subject; + } + + /** + * Gets the subject of the entity. + * + * @return string + * The subject URI. + */ + public function getSubject() { + return $this->subject; + } + + /** + * Gets the originating graph of the entity. + * @return string + * The graph URI. + */ + public function getGraphUri() { + return $this->graph; + } + + /** + * Adds a predicate-object pair to the entity. + * + * @todo Add support for blank nodes. + * + * @param string $predicate + * The URI of the predicate. + * @param $object + * A literal or resource. + */ + public function add(string $predicate, $object) { + $language = $this->getLanguage($object); + $this->objects[$predicate][$language][] = (string) $object; + } + + /** + * Checks if an object with a given predicate exists in the set. + * + * @param $predicate + * The URI of the predicate. + * + * @return bool + * True if an object with said predicate exits in the set. + */ + public function hasPredicate($predicate) { + return isset($this->objects[$predicate]); + } + + /** + * Returns the language/object structure for given predicate. + * + * @param $predicate + * The URI of the predicate. + * + * @return mixed + */ + public function getObjectDataByPredicate($predicate) { + return $this->objects[$predicate]; + } + + /** + * Determine the language of an object. + * + * @param \EasyRdf\Resource|\EasyRdf\Literal $object + * The object for which the language will be determined. + * + * @return string + * The language code of the object. + */ + protected function getLanguage($object): string { + $language = LanguageInterface::LANGCODE_DEFAULT; + if ($object instanceof Literal) { + $object_language = $object->getLang(); + if ($object_language) { + $language = $object_language; + } + } + return $language; + } + + /** + * {@inheritdoc} + */ + public function rewind() { + reset($this->objects); + } + + /** + * {@inheritdoc} + */ + public function current() : array { + return current($this->objects); + } + + /** + * {@inheritdoc} + */ + public function key() :string { + return key($this->objects); + } + + /** + * {@inheritdoc} + */ + public function next() { + next($this->objects); + } + + /** + * {@inheritdoc} + */ + public function valid() { + return key($this->objects) !== NULL; + } + +} diff --git a/src/RawEntityRepository.php b/src/RawEntityRepository.php new file mode 100644 index 00000000..bc0c886f --- /dev/null +++ b/src/RawEntityRepository.php @@ -0,0 +1,200 @@ +graph; + $subject = (string) $result->entity_subject; + $predicate = (string) $result->predicate; + $object = $result->field_value; + + $this->addResult($graph, $subject, $predicate, $object); + } + } + + /** + * Adds a value to the repo. Creates new EntityValue object if needed. + * + * @param string $graph + * The graph URI. + * @param string $subject + * The subject URI. + * @param string $predicate + * The predicate URI. + * @param $object + * The object. + */ + protected function addResult(string $graph, string $subject, string $predicate, $object) { + $entity_values = $this->loadCreateRawEntity($graph, $subject); + $entity_values->add($predicate, $object); + } + + /** + * Loads a raw entity from the repo if it exists, or creates one if not. + * + * @param $graph + * The graph URI. + * @param $subject + * The subject URI. + * + * @return \Drupal\rdf_entity\RawEntity + */ + protected function loadCreateRawEntity($graph, $subject) : RawEntity { + if (isset($this->repoBySubject[$subject][$graph])) { + return $this->repoBySubject[$subject][$graph]; + } + return $this->createRawEntity($graph, $subject); + } + + /** + * Create a new raw entity, and add it to the repo. + * + * @param $graph + * The graph URI. + * @param $subject + * The subject URI. + * + * @return \Drupal\rdf_entity\RawEntity + * The entity values object. + */ + protected function createRawEntity($graph, $subject) : RawEntity { + $sparql_result = new RawEntity($graph, $subject); + + $this->trackEntity($sparql_result); + return $sparql_result; + } + + /** + * Associates a raw entity with the repo. + * + * @param \Drupal\rdf_entity\RawEntity $raw_entity + * The raw entity to keep track of. + */ + public function trackEntity(RawEntity $raw_entity) { + // Add the same object the lookup tables. + $this->repoBySubject[$raw_entity->getSubject()][$raw_entity->getGraphUri()] = $raw_entity; + $this->repoFlat[] = $raw_entity; + } + + /** + * Create new immutable entity repo, filtered by graph. + * + * @param $uris The graph uri to filter on. + * + * @return \Drupal\rdf_entity\RawEntityRepository + */ + public function newRepoFromGraphUris($uris) { + $filtered_entity_repo = new RawEntityRepository(); + foreach ($this as $raw_entity) { + if (in_array($raw_entity->getGraphUri(), $uris)) { + $filtered_entity_repo->trackEntity($raw_entity); + } + } + return $filtered_entity_repo; + } + + /** + * Merges two entity repos into a new one. + * + * Only results with a subject that is not present in the set get merged in. + * + * @param \Drupal\rdf_entity\RawEntityRepository $repo_to_merge + * + * @return \Drupal\rdf_entity\RawEntityRepository + */ + public function merge(RawEntityRepository $repo_to_merge) { + $merged_repo = clone $this; + foreach ($repo_to_merge as $entity_to_merge) { + if (!$merged_repo->hasSubject($entity_to_merge->getSubject())) { + $merged_repo->trackEntity($entity_to_merge); + } + } + return $merged_repo; + } + + /** + * A raw entity with the given subject is present in the repo. + * + * @param $subject + * The URI of the subject. + * + * @return bool + * True if the raw entity was found. + */ + public function hasSubject($subject) { + return isset($this->repoBySubject[$subject]); + } + + /** + * {@inheritdoc} + */ + public function rewind() { + $this->position = 0; + } + + /** + * {@inheritdoc} + */ + public function current() : RawEntity{ + return $this->repoFlat[$this->position]; + } + + /** + * {@inheritdoc} + */ + public function key() :string { + return $this->position; + } + + /** + * {@inheritdoc} + */ + public function next() { + ++$this->position; + } + + /** + * {@inheritdoc} + */ + public function valid() { + return isset($this->repoFlat[$this->position]); + } + +} \ No newline at end of file diff --git a/src/SparqlResultFilter.php b/src/SparqlResultFilter.php new file mode 100644 index 00000000..643b97c7 --- /dev/null +++ b/src/SparqlResultFilter.php @@ -0,0 +1,28 @@ +graphHandler = $graph_handler; + } + + public function filter(RawEntityRepository $entity_repo, $graph_priorities, $entity_type_id) : RawEntityRepository { + $filtered_set = new RawEntityRepository(); + + foreach ($graph_priorities as $graph_priority) { + $uris = $this->graphUriFromPrio($graph_priority, $entity_type_id); + $priority_resultset = $entity_repo->newRepoFromGraphUris($uris); + $filtered_set = $filtered_set->merge($priority_resultset); + } + return $filtered_set; + } + + protected function graphUriFromPrio(string $prio, $entity_type_id) { + $graph_uris = $this->graphHandler->getEntityTypeGraphUris($entity_type_id); + return array_column($graph_uris, $prio); + } +} \ No newline at end of file From 63561212e6ec29f562598188f0e6158b6bc7ce10 Mon Sep 17 00:00:00 2001 From: Sander Van Dooren Date: Mon, 11 Mar 2019 09:22:35 +0100 Subject: [PATCH 2/4] Get graph handler from storage. --- rdf_entity.services.yml | 2 +- src/SparqlResultFilter.php | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/rdf_entity.services.yml b/rdf_entity.services.yml index 14b9a70c..394c78e7 100644 --- a/rdf_entity.services.yml +++ b/rdf_entity.services.yml @@ -57,4 +57,4 @@ services: - { name: event_subscriber } rdf_entity.sparql_result.filter: class: Drupal\rdf_entity\SparqlResultFilter - arguments: ['@sparql.graph_handler'] \ No newline at end of file + arguments: ['@entity_type.manager'] \ No newline at end of file diff --git a/src/SparqlResultFilter.php b/src/SparqlResultFilter.php index 643b97c7..a4a68259 100644 --- a/src/SparqlResultFilter.php +++ b/src/SparqlResultFilter.php @@ -2,12 +2,14 @@ namespace Drupal\rdf_entity; +use Drupal\Core\Entity\EntityTypeManagerInterface; + class SparqlResultFilter { - protected $graphHandler; + protected $entityTypeManager; - public function __construct(RdfGraphHandlerInterface $graph_handler) { - $this->graphHandler = $graph_handler; + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; } public function filter(RawEntityRepository $entity_repo, $graph_priorities, $entity_type_id) : RawEntityRepository { @@ -22,7 +24,11 @@ public function filter(RawEntityRepository $entity_repo, $graph_priorities, $ent } protected function graphUriFromPrio(string $prio, $entity_type_id) { - $graph_uris = $this->graphHandler->getEntityTypeGraphUris($entity_type_id); + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if (!$storage instanceof RdfEntitySparqlStorageInterface) { + throw new \Exception('Storage must implement RDF Storage interface.'); + } + $graph_uris = $storage->getGraphHandler()->getEntityTypeGraphUris($entity_type_id); return array_column($graph_uris, $prio); } } \ No newline at end of file From 7193c0876aa0853479e00b8019173dacbc49dd45 Mon Sep 17 00:00:00 2001 From: Sander Van Dooren Date: Mon, 11 Mar 2019 15:22:36 +0100 Subject: [PATCH 3/4] Rename filter class to more meaningful name. --- rdf_entity.services.yml | 4 +- src/Entity/RdfEntitySparqlStorage.php | 74 ++++++++++++++++----------- src/LayeredGraphPriorityFilter.php | 49 ++++++++++++++++++ src/SparqlResultFilter.php | 34 ------------ 4 files changed, 95 insertions(+), 66 deletions(-) create mode 100644 src/LayeredGraphPriorityFilter.php delete mode 100644 src/SparqlResultFilter.php diff --git a/rdf_entity.services.yml b/rdf_entity.services.yml index 394c78e7..cde3c9c6 100644 --- a/rdf_entity.services.yml +++ b/rdf_entity.services.yml @@ -55,6 +55,6 @@ services: arguments: ['@typed_data_manager'] tags: - { name: event_subscriber } - rdf_entity.sparql_result.filter: - class: Drupal\rdf_entity\SparqlResultFilter + rdf_entity.graph_priority.filter: + class: Drupal\rdf_entity\LayeredGraphPriorityFilter arguments: ['@entity_type.manager'] \ No newline at end of file diff --git a/src/Entity/RdfEntitySparqlStorage.php b/src/Entity/RdfEntitySparqlStorage.php index c893eedb..73a273ec 100644 --- a/src/Entity/RdfEntitySparqlStorage.php +++ b/src/Entity/RdfEntitySparqlStorage.php @@ -27,7 +27,7 @@ use Drupal\rdf_entity\RdfGraphHandlerInterface; use Drupal\rdf_entity\RdfInterface; use Drupal\rdf_entity\RawEntity; -use Drupal\rdf_entity\SparqlResultFilter; +use Drupal\rdf_entity\LayeredGraphPriorityFilter; use Drupal\rdf_entity\RawEntityRepository; use EasyRdf\Graph; use EasyRdf\Literal; @@ -101,7 +101,7 @@ class RdfEntitySparqlStorage extends ContentEntityStorageBase implements RdfEnti */ protected $entityIdPluginManager; - protected $filter; + protected $graphPriorityFilter; /** * Initialize the storage backend. @@ -126,10 +126,12 @@ class RdfEntitySparqlStorage extends ContentEntityStorageBase implements RdfEnti * The rdf mapping helper service. * @param \Drupal\rdf_entity\RdfEntityIdPluginManager $entity_id_plugin_manager * The RDF entity ID generator plugin manager. + * @param \Drupal\rdf_entity\LayeredGraphPriorityFilter + * The entity entity repository filter the * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache * The memory cache backend. */ - public function __construct(EntityTypeInterface $entity_type, ConnectionInterface $sparql, EntityManagerInterface $entity_manager, EntityTypeManagerInterface $entity_type_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, RdfGraphHandlerInterface $rdf_graph_handler, RdfFieldHandlerInterface $rdf_field_handler, RdfEntityIdPluginManager $entity_id_plugin_manager, SparqlResultFilter $filter, MemoryCacheInterface $memory_cache = NULL) { + public function __construct(EntityTypeInterface $entity_type, ConnectionInterface $sparql, EntityManagerInterface $entity_manager, EntityTypeManagerInterface $entity_type_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, RdfGraphHandlerInterface $rdf_graph_handler, RdfFieldHandlerInterface $rdf_field_handler, RdfEntityIdPluginManager $entity_id_plugin_manager, LayeredGraphPriorityFilter $filter, MemoryCacheInterface $memory_cache = NULL) { parent::__construct($entity_type, $entity_manager, $cache, $memory_cache); $this->sparql = $sparql; $this->languageManager = $language_manager; @@ -138,7 +140,7 @@ public function __construct(EntityTypeInterface $entity_type, ConnectionInterfac $this->graphHandler = $rdf_graph_handler; $this->fieldHandler = $rdf_field_handler; $this->entityIdPluginManager = $entity_id_plugin_manager; - $this->filter = $filter; + $this->graphPriorityFilter = $filter; } /** @@ -156,12 +158,45 @@ public static function createInstance(ContainerInterface $container, EntityTypeI $container->get('sparql.graph_handler'), $container->get('sparql.field_handler'), $container->get('plugin.manager.rdf_entity.id'), - $container->get('rdf_entity.sparql_result.filter'), + $container->get('rdf_entity.graph_priority.filter'), // We support also Drupal 8.5.x. $container->has('entity.memory_cache') ? $container->get('entity.memory_cache') : NULL ); } + /** + * Format the entity select SPARQL query. + * + * @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/rdf_entity/issues/19 + * + * @param array $ids + * @param array $graphs + * + * @return string + */ + protected static function formatEntitySelectQuery(array $ids, array $graphs): string { + $ids_string = SparqlArg::serializeUris($ids, ' '); + $named_graph = ''; + foreach ($graphs as $graph) { + $named_graph .= 'FROM NAMED ' . SparqlArg::uri($graph) . "\n"; + } + + // @todo https://github.com/ec-europa/rdf_entity/issues/19 + $query = <<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/rdf_entity/issues/19 - $query = <<sparql->query($query); return $this->processGraphResults($entity_values, $graph_ids); @@ -339,10 +352,11 @@ protected function loadFromStorage(array $ids, array $graph_ids): ?array { protected function processGraphResults($results, array $graph_ids): ?array { $entity_repo = new RawEntityRepository(); $entity_repo->createFromResult($results); - $fileredResultSet = $this->filter->filter($entity_repo, $graph_ids, $this->getEntityTypeId()); + // Filter down to one result per subject (graph with highest priority). + $filtered_result_set = $this->graphPriorityFilter->filter($entity_repo, $graph_ids, $this->getEntityTypeId()); $entity_list = []; - foreach ($fileredResultSet as $raw_entity) { + foreach ($filtered_result_set as $raw_entity) { if ($entity = $this->hydrateEntity($raw_entity)){ $entity_list[$raw_entity->getSubject()] = $entity; } diff --git a/src/LayeredGraphPriorityFilter.php b/src/LayeredGraphPriorityFilter.php new file mode 100644 index 00000000..e694e846 --- /dev/null +++ b/src/LayeredGraphPriorityFilter.php @@ -0,0 +1,49 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * Created a filtered entity repository. + * + * The new will contain one entry for each subject. If an entity is + * present in multiple graphs, the entity from the graph with the highest + * priority will be selected. + * + * @param \Drupal\rdf_entity\RawEntityRepository $entity_repository + * @param $graph_priorities + * @param $entity_type_id + * + * @return \Drupal\rdf_entity\RawEntityRepository + * @throws \Exception + */ + public function filter(RawEntityRepository $entity_repository, $graph_priorities, $entity_type_id) : RawEntityRepository { + $filtered_set = new RawEntityRepository(); + + foreach ($graph_priorities as $graph_priority) { + $uris = $this->graphUrisFromPriority($graph_priority, $entity_type_id); + $priority_result_set = $entity_repository->newRepoFromGraphUris($uris); + $filtered_set = $filtered_set->merge($priority_result_set); + } + return $filtered_set; + } + + + protected function graphUrisFromPriority(string $priority, $entity_type_id) { + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if (!$storage instanceof RdfEntitySparqlStorageInterface) { + throw new \Exception('Storage must implement RDF Storage interface.'); + } + $graph_uris = $storage->getGraphHandler()->getEntityTypeGraphUris($entity_type_id); + return array_column($graph_uris, $priority); + } +} \ No newline at end of file diff --git a/src/SparqlResultFilter.php b/src/SparqlResultFilter.php deleted file mode 100644 index a4a68259..00000000 --- a/src/SparqlResultFilter.php +++ /dev/null @@ -1,34 +0,0 @@ -entityTypeManager = $entity_type_manager; - } - - public function filter(RawEntityRepository $entity_repo, $graph_priorities, $entity_type_id) : RawEntityRepository { - $filtered_set = new RawEntityRepository(); - - foreach ($graph_priorities as $graph_priority) { - $uris = $this->graphUriFromPrio($graph_priority, $entity_type_id); - $priority_resultset = $entity_repo->newRepoFromGraphUris($uris); - $filtered_set = $filtered_set->merge($priority_resultset); - } - return $filtered_set; - } - - protected function graphUriFromPrio(string $prio, $entity_type_id) { - $storage = $this->entityTypeManager->getStorage($entity_type_id); - if (!$storage instanceof RdfEntitySparqlStorageInterface) { - throw new \Exception('Storage must implement RDF Storage interface.'); - } - $graph_uris = $storage->getGraphHandler()->getEntityTypeGraphUris($entity_type_id); - return array_column($graph_uris, $prio); - } -} \ No newline at end of file From a5627072075b760dd2374c8c82673019d23e16aa Mon Sep 17 00:00:00 2001 From: Sander Van Dooren Date: Tue, 26 Mar 2019 09:53:28 +0100 Subject: [PATCH 4/4] Revision support. --- rdf_entity.links.task.yml | 6 + rdf_entity.routing.yml | 63 +++++ src/Controller/RdfController.php | 225 +++++++++++++++++- src/Entity/Query/Sparql/Query.php | 8 + src/Entity/Query/Sparql/SparqlCondition.php | 10 + .../Query/Sparql/SparqlQueryInterface.php | 3 + src/Entity/Rdf.php | 25 +- src/Entity/RdfEntitySparqlStorage.php | 224 ++++++++++++----- src/Form/RdfRevisionDeleteForm.php | 55 +++++ src/Form/RdfRevisionRevertForm.php | 177 ++++++++++++++ src/Form/RdfRevisionRevertTranslationForm.php | 42 ++++ src/RawEntityRepository.php | 6 +- src/RdfInterface.php | 3 +- src/RouteProcessor/RouteProcessorRdf.php | 3 +- 14 files changed, 784 insertions(+), 66 deletions(-) create mode 100644 src/Form/RdfRevisionDeleteForm.php create mode 100644 src/Form/RdfRevisionRevertForm.php create mode 100644 src/Form/RdfRevisionRevertTranslationForm.php diff --git a/rdf_entity.links.task.yml b/rdf_entity.links.task.yml index a782c7e0..0f3a89f8 100755 --- a/rdf_entity.links.task.yml +++ b/rdf_entity.links.task.yml @@ -25,3 +25,9 @@ entity.rdf_entity.collection: title: RDF route_name: entity.rdf_entity.collection base_route: system.admin_content + +entity.rdf_entity.version_history: + route_name: entity.rdf_entity.version_history + base_route: entity.rdf_entity.canonical + title: 'Revisions' + weight: 20 \ No newline at end of file diff --git a/rdf_entity.routing.yml b/rdf_entity.routing.yml index 748b1fba..2266b8b0 100755 --- a/rdf_entity.routing.yml +++ b/rdf_entity.routing.yml @@ -11,6 +11,18 @@ entity.rdf_entity.canonical: # Calls the access controller of the entity, $operation 'view'. _entity_access: 'rdf_entity.view' +entity.rdf_entity.version_history: + path: '/rdf_entity/{rdf_entity}/revisions' + defaults: + _title: 'Revisions' + _controller: '\Drupal\rdf_entity\Controller\RdfController::revisionOverview' + requirements: + _entity_access: 'rdf_entity.edit' + options: + parameters: + rdf_entity: + type: entity:rdf_entity + entity.rdf_entity.collection: path: 'admin/content/rdf' defaults: @@ -148,3 +160,54 @@ entity.rdf_entity_graph.disable: toggle_operation: disable requirements: _custom_access: 'Drupal\rdf_entity\Controller\RdfEntityGraphToggle::access' + +entity.rdf_entity.revision: + path: '/rdf_entity/{rdf_entity}/revisions/{rdf_revision}/view' + defaults: + _controller: 'Drupal\rdf_entity\Controller\RdfController::revisionShow' + options: + parameters: + rdf_entity: + type: entity:rdf_entity + rdf_revision: + type: entity:rdf_entity + requirements: + # Calls the access controller of the entity, $operation 'view'. + _entity_access: 'rdf_entity.view' + +rdf_entity.revision_revert_confirm: + path: '/rdf_entity/{rdf_entity}/revisions/{rdf_revision}/revert' + defaults: + _form: '\Drupal\rdf_entity\Form\RdfRevisionRevertForm' + _title: 'Revert to earlier revision' + options: + parameters: + rdf_entity: + type: entity:rdf_entity + rdf_revision: + type: entity:rdf_entity + requirements: + _entity_access: 'rdf_entity.edit' +# T +rdf_entity.revision_revert_translation_confirm: + path: '/rdf_entity/{rdf_entity}/revisions/{rdf_revision}/revert/{langcode}' + defaults: + _form: '\Drupal\rdf_entity\Form\RdfRevisionRevertTranslationForm' + _title: 'Revert to earlier revision of a translation' + parameters: + rdf_entity: + type: entity:rdf_entity + rdf_revision: + type: entity:rdf_entity + +rdf_entity.revision_delete_confirm: + path: '/rdf_entity/{rdf_entity}/revisions/{rdf_revision}/delete' + defaults: + _form: '\Drupal\rdf_entity\Form\RdfRevisionDeleteForm' + _title: 'Delete earlier revision' + options: + parameters: + rdf_entity: + type: entity:rdf_entity + rdf_revision: + type: entity:rdf_entity \ No newline at end of file diff --git a/src/Controller/RdfController.php b/src/Controller/RdfController.php index f089ec37..48f1d547 100644 --- a/src/Controller/RdfController.php +++ b/src/Controller/RdfController.php @@ -4,13 +4,57 @@ use Drupal\Component\Utility\Xss; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Datetime\DateFormatterInterface; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\Controller\EntityViewController; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Url; +use Drupal\rdf_entity\RdfEntitySparqlStorageInterface; use Drupal\rdf_entity\RdfEntityTypeInterface; use Drupal\rdf_entity\RdfInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides route responses for rdf_entity.module. */ -class RdfController extends ControllerBase { +class RdfController extends ControllerBase implements ContainerInjectionInterface { + + /** + * The date formatter service. + * + * @var \Drupal\Core\Datetime\DateFormatterInterface + */ + protected $dateFormatter; + + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * Constructs a RdfController object. + * + * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter + * The date formatter service. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + */ + public function __construct(DateFormatterInterface $date_formatter, RendererInterface $renderer) { + $this->dateFormatter = $date_formatter; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('date.formatter'), + $container->get('renderer') + ); + } /** * Route title callback. @@ -108,4 +152,183 @@ public function addPage() { return $build; } + /** + * Generates an overview table of older revisions of a entity. + * + * @param \Drupal\rdf_entity\RdfInterface $rdf_entity + * A rdf object. + * + * @return array + * An array as expected by \Drupal\Core\Render\RendererInterface::render(). + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function revisionOverview(RdfInterface $rdf_entity) { + $account = $this->currentUser(); + $langcode = $rdf_entity->language()->getId(); + $langname = $rdf_entity->language()->getName(); + $languages = $rdf_entity->getTranslationLanguages(); + $has_translations = (count($languages) > 1); + $rdf_storage = $this->entityManager()->getStorage('rdf_entity'); + $type = $rdf_entity->getType(); + + $build['#title'] = $has_translations ? $this->t('@langname revisions for %title', ['@langname' => $langname, '%title' => $rdf_entity->label()]) : $this->t('Revisions for %title', ['%title' => $rdf_entity->label()]); + $header = [$this->t('Revision'), $this->t('Operations')]; + + $revert_permission = (($account->hasPermission("revert $type revisions") || $account->hasPermission('revert all revisions') || $account->hasPermission('administer nodes')) && $rdf_entity->access('update')); + $delete_permission = (($account->hasPermission("delete $type revisions") || $account->hasPermission('delete all revisions') || $account->hasPermission('administer nodes')) && $rdf_entity->access('delete')); + + $rows = []; + $default_revision = $rdf_entity->getRevisionId(); + $current_revision_displayed = FALSE; + + $revisions = $this->getRevisionIds($rdf_entity, $rdf_storage); + + foreach ($revisions as $vid) { + /** @var \Drupal\rdf_entity\RdfInterface $revision */ + $revision = $rdf_storage->loadRevision($vid); + // Only show revisions that are affected by the language that is being + // displayed. + // @todo && $revision->getTranslation($langcode)->isRevisionTranslationAffected() + if ($revision->hasTranslation($langcode) ) { + $username = [ + '#theme' => 'username', + '#account' => $revision->getRevisionUser(), + ]; + + // Use revision link to link to revisions that are not active. + $date = $this->dateFormatter->format($revision->getChangedTime(), 'short'); + + // We treat also the latest translation-affecting revision as current + // revision, if it was the default revision, as its values for the + // current language will be the same of the current default revision in + // this case. + $is_current_revision = $vid == $default_revision || (!$current_revision_displayed && $revision->wasDefaultRevision()); + if (!$is_current_revision) { + $link = $this->l($date, new Url('entity.rdf_entity.revision', ['rdf_entity' => $rdf_entity->id(), 'rdf_revision' => $vid])); + } + else { + $link = $rdf_entity->link($date); + $current_revision_displayed = TRUE; + } + + $row = []; + $column = [ + 'data' => [ + '#type' => 'inline_template', + '#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}

{{ message }}

{% endif %}', + '#context' => [ + 'date' => $link, + 'username' => $this->renderer->renderPlain($username), + 'message' => ['#markup' => $revision->revision_log->value, '#allowed_tags' => Xss::getHtmlTagList()], + ], + ], + ]; + // @todo Simplify once https://www.drupal.org/node/2334319 lands. + $this->renderer->addCacheableDependency($column['data'], $username); + $row[] = $column; + + if ($is_current_revision) { + $row[] = [ + 'data' => [ + '#prefix' => '', + '#markup' => $this->t('Current revision'), + '#suffix' => '', + ], + ]; + + $rows[] = [ + 'data' => $row, + 'class' => ['revision-current'], + ]; + } + else { + $links = []; + if ($revert_permission) { + $links['revert'] = [ + 'title' => $vid < $rdf_entity->getRevisionId() ? $this->t('Revert') : $this->t('Set as current revision'), + 'url' => $has_translations ? + Url::fromRoute('rdf_entity.revision_revert_translation_confirm', ['rdf_rntity' => $rdf_entity->id(), 'rdf_revision' => $vid, 'langcode' => $langcode]) : + Url::fromRoute('rdf_entity.revision_revert_confirm', ['rdf_entity' => $rdf_entity->id(), 'rdf_revision' => $vid]), + ]; + } + + if ($delete_permission) { + $links['delete'] = [ + 'title' => $this->t('Delete'), + 'url' => Url::fromRoute('rdf_entity.revision_delete_confirm', ['rdf_entity' => $rdf_entity->id(), 'rdf_revision' => $vid]), + ]; + } + + $row[] = [ + 'data' => [ + '#type' => 'operations', + '#links' => $links, + ], + ]; + + $rows[] = $row; + } + } + } + + $build['node_revisions_table'] = [ + '#theme' => 'table', + '#rows' => $rows, + '#header' => $header, + '#attached' => [ + 'library' => ['node/drupal.node.admin'], + ], + '#attributes' => ['class' => 'node-revision-table'], + ]; + + $build['pager'] = ['#type' => 'pager']; + + // Bypass cache for development + $build['#cache']['max-age'] = 0; + + return $build; + } + + /** + * Gets a list of rdf revision IDs for a specific entity. + * + * @param \Drupal\rdf_entity\RdfInterface $rdf_entity + * The rdf entity. + * @param \Drupal\rdf_entity\RdfEntitySparqlStorageInterface $rdf_storage + * The rdf storage handler. + * + * @return int[] + * Node revision IDs (in descending order). + */ + protected function getRevisionIds(RdfInterface $rdf_entity, RdfEntitySparqlStorageInterface $rdf_storage) { + $query = $rdf_storage->getQuery() + ->allRevisions() + //->condition($rdf_entity->getEntityType()->getKey('bundle'), 'event') + ->condition($rdf_entity->getEntityType()->getKey('id'), $rdf_entity->id()) + ->sort($rdf_entity->getEntityType()->getKey('revision'), 'DESC') + ->pager(50); + $result= $query->execute(); + return array_keys($result); + } + + + /** + * Displays a rdf revision. + * + * @param \Drupal\rdf_entity\RdfInterface $rdf_revision + * The rdf revision. + * + * @return array + * An array suitable for \Drupal\Core\Render\RendererInterface::render(). + */ + public function revisionShow(RdfInterface $rdf_revision) { + $rdf_entity = $this->entityManager()->getTranslationFromContext($rdf_revision); + $view_controller = new EntityViewController($this->entityManager, $this->renderer); + $page = $view_controller->view($rdf_entity); + unset($page['#cache']); + return $page; + } + } diff --git a/src/Entity/Query/Sparql/Query.php b/src/Entity/Query/Sparql/Query.php index 32939fd9..12c25f3f 100644 --- a/src/Entity/Query/Sparql/Query.php +++ b/src/Entity/Query/Sparql/Query.php @@ -352,6 +352,14 @@ protected function conditionGroupFactory($conjunction = 'AND') { return new $class($conjunction, $this, $this->namespaces, $this->graphHandler, $this->fieldHandler); } + public function getLatestRevision() { + return $this->latestRevision; + } + + public function getAllRevisions() { + return $this->allRevisions; + } + /** * Return the query string for debugging help. * diff --git a/src/Entity/Query/Sparql/SparqlCondition.php b/src/Entity/Query/Sparql/SparqlCondition.php index 13acedd8..bc2e4c12 100644 --- a/src/Entity/Query/Sparql/SparqlCondition.php +++ b/src/Entity/Query/Sparql/SparqlCondition.php @@ -295,6 +295,16 @@ public function condition($field = NULL, $value = NULL, $operator = NULL, $lang * Thrown if the value is NULL or the operator is not allowed. */ public function keyCondition($field, $value, $operator) { + // Revision handling. + $query = $this->query; + if ($query instanceof SparqlQueryInterface) { + if ($field === 'id' && $query->getAllRevisions()) { + $field = 'vid'; + } + } + + + // @todo: Add support for loadMultiple with empty Id (load all). if ($value == NULL) { throw new \Exception('The value cannot be NULL for conditions related to the Id and bundle keys.'); diff --git a/src/Entity/Query/Sparql/SparqlQueryInterface.php b/src/Entity/Query/Sparql/SparqlQueryInterface.php index 12913688..129a46e1 100644 --- a/src/Entity/Query/Sparql/SparqlQueryInterface.php +++ b/src/Entity/Query/Sparql/SparqlQueryInterface.php @@ -44,4 +44,7 @@ public function getEntityType(): EntityTypeInterface; */ public function getEntityStorage(): RdfEntitySparqlStorageInterface; + public function getLatestRevision(); + + public function getAllRevisions(); } diff --git a/src/Entity/Rdf.php b/src/Entity/Rdf.php index aaa4c6e2..8a8d491f 100755 --- a/src/Entity/Rdf.php +++ b/src/Entity/Rdf.php @@ -3,6 +3,7 @@ namespace Drupal\rdf_entity\Entity; use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\RevisionLogEntityTrait; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityTypeInterface; @@ -32,25 +33,35 @@ * }, * "access" = "Drupal\rdf_entity\RdfAccessControlHandler", * }, + * show_revision_ui = TRUE, * list_cache_contexts = { "user" }, * base_table = null, + * revision_table = null, * admin_permission = "administer rdf entity", * fieldable = TRUE, * translatable = TRUE, * entity_keys = { * "id" = "id", + * "revision" = "vid", * "uid" = "uid", * "bundle" = "rid", * "langcode" = "langcode", * "label" = "label", * "uuid" = "uuid", * }, + * revision_metadata_keys = { + * "revision_user" = "revision_uid", + * "revision_created" = "revision_timestamp", + * "revision_log_message" = "revision_log" + * }, * bundle_entity_type = "rdf_type", * links = { * "canonical" = "/rdf_entity/{rdf_entity}", * "edit-form" = "/rdf_entity/{rdf_entity}/edit", * "delete-form" = "/rdf_entity/{rdf_entity}/delete", - * "collection" = "/rdf_entity/list" + * "collection" = "/rdf_entity/list", + * "version-history" = "/rdf_entity/{rdf_entity}/revisions", + * "revision" = "/rdf_entity/{rdf_entity}/revisions/{rdf_revision}/view", * }, * field_ui_base_route = "entity.rdf_type.edit_form", * permission_granularity = "bundle", @@ -60,6 +71,7 @@ class Rdf extends ContentEntityBase implements RdfInterface { use EntityChangedTrait; + use RevisionLogEntityTrait; /** * Entity bundle. @@ -68,6 +80,8 @@ class Rdf extends ContentEntityBase implements RdfInterface { */ protected $rid; + protected $vid; + /** * {@inheritdoc} * @@ -140,6 +154,12 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('ID')) ->setTranslatable(FALSE); + if ($entity_type->hasKey('revision')) { + $fields[$entity_type->getKey('revision')] = BaseFieldDefinition::create('uri') + ->setLabel(new TranslatableMarkup('Revision ID')) + ->setReadOnly(TRUE); + } + $fields['rid'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('Rdf Type')) ->setDescription(t('The Rdf type of this entity.')) @@ -212,6 +232,9 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { 'weight' => 2, ]); + // Add the revision metadata fields. + $fields += static::revisionLogBaseFieldDefinitions($entity_type); + return $fields; } diff --git a/src/Entity/RdfEntitySparqlStorage.php b/src/Entity/RdfEntitySparqlStorage.php index 73a273ec..87cb33b5 100644 --- a/src/Entity/RdfEntitySparqlStorage.php +++ b/src/Entity/RdfEntitySparqlStorage.php @@ -21,6 +21,8 @@ use Drupal\rdf_entity\Database\Driver\sparql\ConnectionInterface; use Drupal\rdf_entity\Entity\Query\Sparql\SparqlArg; use Drupal\rdf_entity\Exception\DuplicatedIdException; +use Drupal\rdf_entity\HydratedEntity; +use Drupal\rdf_entity\HydratedEntityList; use Drupal\rdf_entity\RdfEntityIdPluginManager; use Drupal\rdf_entity\RdfEntitySparqlStorageInterface; use Drupal\rdf_entity\RdfFieldHandlerInterface; @@ -133,6 +135,7 @@ class RdfEntitySparqlStorage extends ContentEntityStorageBase implements RdfEnti */ public function __construct(EntityTypeInterface $entity_type, ConnectionInterface $sparql, EntityManagerInterface $entity_manager, EntityTypeManagerInterface $entity_type_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, RdfGraphHandlerInterface $rdf_graph_handler, RdfFieldHandlerInterface $rdf_field_handler, RdfEntityIdPluginManager $entity_id_plugin_manager, LayeredGraphPriorityFilter $filter, MemoryCacheInterface $memory_cache = NULL) { parent::__construct($entity_type, $entity_manager, $cache, $memory_cache); + $this->revisionKey = $this->entityType->getKey('revision'); $this->sparql = $sparql; $this->languageManager = $language_manager; $this->entityTypeManager = $entity_type_manager; @@ -206,7 +209,7 @@ protected static function formatEntitySelectQuery(array $ids, array $graphs): st * @return \EasyRdf\Graph * The EasyRdf graph object. */ - protected static function getGraph($graph_uri) { + protected static function createGraph($graph_uri) { $graph = new Graph($graph_uri); return $graph; } @@ -273,23 +276,8 @@ protected function getFromStorage(array $ids = NULL, array $graph_ids = []): arr 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] : FALSE; - $langcode_key = $this->getEntityType()->getKey('langcode'); - $translations = []; - if (!empty($entities_values[$id][$langcode_key])) { - foreach ($entities_values[$id][$langcode_key] as $langcode => $data) { - if (!empty(reset($data)['value'])) { - $translations[] = reset($data)['value']; - } - } - } - $entity = new $this->entityClass($entity_values, $this->entityTypeId, $bundle, $translations); - $this->trackOriginalGraph($entity); - $entities[$id] = $entity; - } + $entities = $this->loadFromStorage($operation_ids, $graph_ids); + if ($entities) { $this->invokeStorageLoadHook($entities); $this->setPersistentCache($entities); } @@ -322,8 +310,13 @@ protected function loadFromStorage(array $ids, array $graph_ids): ?array { $graphs = $this->getGraphHandler()->getEntityTypeGraphUrisFlatList($this->getEntityTypeId()); $query = self::formatEntitySelectQuery($ids, $graphs); - $entity_values = $this->sparql->query($query); - return $this->processGraphResults($entity_values, $graph_ids); + $query_result = $this->sparql->query($query); + $entities = $this->processGraphResults($query_result, $graph_ids); + + foreach ($entities as $id => $entity) { + $this->trackOriginalGraph($entity); + } + return $entities; } /** @@ -349,19 +342,12 @@ protected function loadFromStorage(array $ids, array $graph_ids): ?array { * @throws \Exception * Thrown when the entity graph is empty. */ - protected function processGraphResults($results, array $graph_ids): ?array { - $entity_repo = new RawEntityRepository(); - $entity_repo->createFromResult($results); + protected function processGraphResults($results, array $graph_ids): array { + $entity_repo = RawEntityRepository::createFromResult($results); // Filter down to one result per subject (graph with highest priority). $filtered_result_set = $this->graphPriorityFilter->filter($entity_repo, $graph_ids, $this->getEntityTypeId()); - $entity_list = []; - foreach ($filtered_result_set as $raw_entity) { - if ($entity = $this->hydrateEntity($raw_entity)){ - $entity_list[$raw_entity->getSubject()] = $entity; - } - } - return $entity_list; + return $this->hydrateRepository($filtered_result_set); } /** @@ -370,21 +356,55 @@ protected function processGraphResults($results, array $graph_ids): ?array { * @return array * @throws \Exception */ - protected function hydrateEntity(RawEntity $raw_entity): array { + protected function hydrateEntity(RawEntity $raw_entity): ?EntityInterface { $entity_array = []; $bundle = $this->getActiveBundle($raw_entity); if (!$bundle) { return NULL; } - // Map bundle and entity_array id. - $entity_array[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] = $bundle; - $entity_array[$this->idKey][LanguageInterface::LANGCODE_DEFAULT] = $raw_entity->getSubject(); - $entity_array['graph'][LanguageInterface::LANGCODE_DEFAULT] = $raw_entity->getGraphUri(); + $entity_array = $this->attachEntityId($entity_array, $bundle, $raw_entity); + $entity_array = $this->attachBundle($bundle, $entity_array); + + $entity_array = $this->attachGraph($raw_entity, $entity_array); foreach ($raw_entity as $predicate => $field) { $entity_array = $this->attachField($predicate, $bundle, $field, $entity_array); } - return $entity_array; + $translations = $this->getTranslations($entity_array); + return new $this->entityClass($entity_array, $this->entityTypeId, $bundle, $translations); + } + + /** + * @todo document! + */ + protected function attachEntityId(array $entity_values, string $bundle, RawEntity $raw_entity) { + $entity_values[$this->idKey][LanguageInterface::LANGCODE_DEFAULT] = $raw_entity->getSubject(); + if ($this->fieldHandler->fieldIsMapped($this->entityTypeId, 'vid')) { + $rev_uris = $this->fieldHandler->getFieldPredicates($this->entityTypeId, 'vid', NULL, $bundle); + if ($rev_uris) { + $uri = array_pop($rev_uris); + // Loading a revision + if ($raw_entity->hasPredicate($uri)) { + $id = $raw_entity->getObjectDataByPredicate($uri)[LanguageInterface::LANGCODE_DEFAULT][0]; + $entity_values[$this->idKey][LanguageInterface::LANGCODE_DEFAULT] = $id; + $entity_values[$this->revisionKey][LanguageInterface::LANGCODE_DEFAULT] = $raw_entity->getSubject(); + } + } + } + return $entity_values; + } + + protected function getTranslations($entity_values) { + $langcode_key = $this->getEntityType()->getKey('langcode'); + $translations = []; + if (!empty($entity_values[$langcode_key])) { + foreach ($entity_values[$langcode_key] as $langcode => $data) { + if (!empty(reset($data)['value'])) { + $translations[] = reset($data)['value']; + } + } + } + return $translations; } /** @@ -416,7 +436,7 @@ protected function getActiveBundle(RawEntity $entity_values): ?string { // modules to handle this. $this->moduleHandler->alter('rdf_load_bundle', $entity_values, $bundles); if (count($bundles) > 1) { - throw new \Exception('More than one bundles are defined for this uri.'); + throw new \Exception('More than one bundle is defined for this uri.'); } return reset($bundles); } @@ -570,9 +590,7 @@ public function loadUnchanged($id, array $graph_ids = NULL): ?ContentEntityInter * {@inheritdoc} */ public function loadRevision($revision_id) { - list($entity_id, $graph) = explode('||', $revision_id); - - return NULL; + return $this->load($revision_id); } /** @@ -769,19 +787,27 @@ protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefiniti * {@inheritdoc} */ protected function doSave($id, EntityInterface $entity) { - $bundle = $entity->bundle(); + if (!$entity instanceof RdfInterface) { + throw new \RuntimeException("Trying to persist an unsupported entity to RDF backend."); + } + $subject = $id; // 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; + $subject = $id = $this->entityIdPluginManager->getPlugin($entity)->generate(); } elseif ($entity->isNew() && $this->idExists($id)) { throw new DuplicatedIdException("Attempting to create a new entity with the ID '$id' already taken."); } + $entity->{$this->idKey} = $id; + if ($entity->isNewRevision()) { + $subject = $this->entityIdPluginManager->getPlugin($entity)->generate(); + $entity->set('vid', $id); + } + // If the graph is not specified, fallback to the default one for the entity // type. if ($entity->get('graph')->isEmpty()) { @@ -790,8 +816,50 @@ protected function doSave($id, EntityInterface $entity) { $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); + $graph = $this->serialiseEntity( + self::createGraph($graph_uri), + $this->toLangArray($entity), + $entity->bundle(), + $subject + ); + + // Give implementations a chance to alter the graph right before is saved. + $this->alterGraph($graph, $entity); + + if (!$entity->isNew()) { + $this->deleteBeforeInsert($id, $graph_uri); + } + try { + // + if ($entity->isDefaultRevision()) { + $rev_id = $entity->getRevisionId(); + $entity->set('vid', NULL); + $graph = $this->serialiseEntity( + $graph, + $this->toLangArray($entity), + $entity->bundle(), + $rev_id + ); + } + $this->insert($graph, $graph_uri); + + return $entity->isNew() ? SAVED_NEW : SAVED_UPDATED; + } + catch (\Exception $e) { + return FALSE; + } + } + + /** + * @param \Drupal\Core\Entity\EntityInterface $entity + * @param array $lang_array + * @param string $bundle + * @param \EasyRdf\Graph $graph + * @param string $subject + * + * @throws \Drupal\rdf_entity\Exception\UnmappedFieldException + */ + protected function serialiseEntity(Graph $graph, array $lang_array, string $bundle, string $subject): Graph { foreach ($lang_array as $field_name => $langcode_data) { foreach ($langcode_data as $langcode => $field_item) { foreach ($field_item as $delta => $column_data) { @@ -804,25 +872,12 @@ protected function doSave($id, EntityInterface $entity) { $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); + $graph->add($subject, $predicate, $value); } } } } - - // Give implementations a chance to alter the graph right before is saved. - $this->alterGraph($graph, $entity); - - if (!$entity->isNew()) { - $this->deleteBeforeInsert($id, $graph_uri); - } - try { - $this->insert($graph, $graph_uri); - return $entity->isNew() ? SAVED_NEW : SAVED_UPDATED; - } - catch (\Exception $e) { - return FALSE; - } + return $graph; } /** @@ -1277,6 +1332,7 @@ protected function trackOriginalGraph(EntityInterface $entity): void { /** * Hydrates a field and attaches it to the entity structure. + * * @param $predicate * The field predicate. * @param string $bundle @@ -1310,4 +1366,52 @@ protected function attachField($predicate, string $bundle, array $field, array $ return $entity; } + /** + * @param \Drupal\rdf_entity\RawEntityRepository $filtered_result_set + * + * @return array + * @throws \Exception + */ + protected function hydrateRepository(RawEntityRepository $filtered_result_set): array { + $entity_list = []; + foreach ($filtered_result_set as $raw_entity) { + if ($entity = $this->hydrateEntity($raw_entity)) { + $entity_list[$entity->id()] = $entity; + } + } + return $entity_list; + } + + /** + * @param \Drupal\rdf_entity\RawEntity $raw_entity + * @param $entity_array + * + * @return array + */ + protected function attachGraph(RawEntity $raw_entity, $entity_array): array { + $def = $this->getGraphHandler() + ->getEntityTypeGraphUris($this->entityTypeId); + foreach ($def as $bundle => $bundle_data) { + foreach ($bundle_data as $graph_id => $graph_uri) { + if ($graph_uri === $raw_entity->getGraphUri()) { + $entity_array['graph'][LanguageInterface::LANGCODE_DEFAULT] = $graph_id; + } + } + } + return $entity_array; + } + + /** + * @param string|null $bundle + * @param array $entity_array + * + * @return array + */ + protected function attachBundle(?string $bundle, array $entity_array): array { + if ($this->bundleKey) { + $entity_array[$this->bundleKey][LanguageInterface::LANGCODE_DEFAULT] = $bundle; + } + return $entity_array; + } + } diff --git a/src/Form/RdfRevisionDeleteForm.php b/src/Form/RdfRevisionDeleteForm.php new file mode 100644 index 00000000..96c9cb3e --- /dev/null +++ b/src/Form/RdfRevisionDeleteForm.php @@ -0,0 +1,55 @@ +sparqlStorage = $sparql_storage; + $this->dateFormatter = $date_formatter; + $this->time = $time; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager')->getStorage('rdf_entity'), + $container->get('date.formatter'), + $container->get('datetime.time') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'rdf_entity_revision_revert_confirm'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + + $date_formatted = $this->dateFormatter->format($this->getRevisionTime()); + return t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $date_formatted]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.rdf_entity.version_history', ['rdf_entity' => $this->revision->id()]); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Revert'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $rdf_revision = NULL) { + $this->revision = $rdf_revision; + $form = parent::buildForm($form, $form_state); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // The revision timestamp will be updated when the revision is saved. Keep + // the original one for the confirmation message. + $original_revision_timestamp = $this->getRevisionTime(); + + $this->revision = $this->prepareRevertedRevision($this->revision, $form_state); + $this->revision->revision_log = t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)]); + $this->revision->setRevisionUserId($this->currentUser()->id()); + $this->revision->setRevisionCreationTime($this->time->getRequestTime()); + $this->revision->setChangedTime($this->time->getRequestTime()); + $this->revision->save(); + + $this->logger('content')->notice('@type: reverted %title revision %revision.', ['@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]); + $this->messenger() + ->addStatus($this->t('@type %title has been reverted to the revision from %revision-date.', [ + '@type' => $this->revision->bundle(), + '%title' => $this->revision->label(), + '%revision-date' => $this->dateFormatter->format($original_revision_timestamp), + ])); + $form_state->setRedirect( + 'entity.rdf_entity.version_history', + ['rdf_entity' => $this->revision->id()] + ); + } + + protected function getRevisionTime() { + $date = $this->revision->getRevisionCreationTime(); + if (!$date) { + $date = $this->revision->getCreatedTime(); + } + return $date; + } + + /** + * Prepares a revision to be reverted. + * + * @param \Drupal\node\NodeInterface $revision + * The revision to be reverted. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\node\NodeInterface + * The prepared revision ready to be stored. + */ + protected function prepareRevertedRevision(RdfInterface $revision, FormStateInterface $form_state) { + $revision->setNewRevision(); + $revision->isDefaultRevision(TRUE); + + return $revision; + } + +} diff --git a/src/Form/RdfRevisionRevertTranslationForm.php b/src/Form/RdfRevisionRevertTranslationForm.php new file mode 100644 index 00000000..e0b4be2d --- /dev/null +++ b/src/Form/RdfRevisionRevertTranslationForm.php @@ -0,0 +1,42 @@ +graph; $subject = (string) $result->entity_subject; $predicate = (string) $result->predicate; $object = $result->field_value; - $this->addResult($graph, $subject, $predicate, $object); + $repo->addResult($graph, $subject, $predicate, $object); } + return $repo; } /** diff --git a/src/RdfInterface.php b/src/RdfInterface.php index 86bee8ce..7b7d7fb4 100755 --- a/src/RdfInterface.php +++ b/src/RdfInterface.php @@ -5,6 +5,7 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityChangedInterface; use Drupal\Core\Entity\EntityPublishedInterface; +use Drupal\Core\Entity\RevisionLogInterface; use Drupal\user\EntityOwnerInterface; /** @@ -14,7 +15,7 @@ * * @ingroup rdf_entity */ -interface RdfInterface extends ContentEntityInterface, EntityPublishedInterface, EntityOwnerInterface, EntityChangedInterface { +interface RdfInterface extends ContentEntityInterface, EntityPublishedInterface, EntityOwnerInterface, EntityChangedInterface, RevisionLogInterface { /** * Gets the name of the rdf entity. diff --git a/src/RouteProcessor/RouteProcessorRdf.php b/src/RouteProcessor/RouteProcessorRdf.php index 7101344c..0d9b1696 100644 --- a/src/RouteProcessor/RouteProcessorRdf.php +++ b/src/RouteProcessor/RouteProcessorRdf.php @@ -48,7 +48,8 @@ public function __construct(RouteMatchInterface $route_match) { */ public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) { if ($route->hasOption('parameters')) { - foreach ($route->getOption('parameters') as $type => $parameter) { + $option = $route->getOption('parameters'); + foreach ($option as $type => $parameter) { // If the rdf_entity converter exists in the parameter, // then the parameter is of type rdf_entity and needs to be normalized. if (isset($parameter['converter']) && $parameter['converter'] == 'paramconverter.rdf_entity' && SparqlArg::isValidResource($parameters[$type])) {