sparql_entity_storage-8.x-1.0-alpha8/src/SparqlEntityStorageFieldHandler.php
src/SparqlEntityStorageFieldHandler.php
<?php
declare(strict_types=1);
namespace Drupal\sparql_entity_storage;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\sparql_entity_storage\Entity\Query\Sparql\SparqlArg;
use Drupal\sparql_entity_storage\Entity\SparqlMapping;
use Drupal\sparql_entity_storage\Event\InboundValueEvent;
use Drupal\sparql_entity_storage\Event\OutboundValueEvent;
use Drupal\sparql_entity_storage\Event\SparqlEntityStorageEvents;
use Drupal\sparql_entity_storage\Exception\NonExistingFieldPropertyException;
use Drupal\sparql_entity_storage\Exception\UnmappedFieldException;
use EasyRdf\Literal;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Contains helper methods that help with the URI mappings of Drupal elements.
*
* Mainly, two field maps are created and statically cached:
*
* The OUTBOUND MAP, witch is a Drupal oriented property mapping array. A YAML
* representation of this array would look like:
* @codingStandardsIgnoreStart
* sparql_entity:
* bundle_key: rid
* bundles:
* catalog: http://www.w3.org/ns/dcat#Catalog
* other_bundle: ...
* fields:
* label:
* main_property: value
* columns:
* value:
* catalog:
* predicate: http://purl.org/dc/terms/title
* format: t_literal
* serialize: false
* data_type: string
* other_bundle:
* predicate: ...
* ...
* other_column:
* catalog:
* ...
* other_field: ...
* other_entity_type:
* bundle_key: ...
* ...
* @codingStandardsIgnoreEnd
*
* The INBOUND MAP, witch is a SPARQL oriented property mapping array. A YAML
* representation of this array would look like:
* @codingStandardsIgnoreStart
* sparql_entity:
* bundle_key: rid
* bundles:
* http://www.w3.org/ns/dcat#Catalog:
* - catalog
* - collection
* http://example.com:
* - other_bundle
* fields:
* http://purl.org/dc/terms/title:
* catalog:
* field_name: label
* column: value
* serialize: false
* type: string
* data_type: string
* other_field:
* field_name: ...
* ...
* http://example.com/field_mapping:
* ....
* other_entity_type:
* bundle_key: ...
* ...
* @codingStandardsIgnoreEnd
*/
class SparqlEntityStorageFieldHandler implements SparqlEntityStorageFieldHandlerInterface {
/**
* The static cache of outbound map.
*
* @var array
*/
protected array $outboundMap;
/**
* The static cache of inbound map.
*
* @var array
*/
protected array $inboundMap;
/**
* The entity type manager service.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The entity field manager service.
*/
protected EntityFieldManagerInterface $entityFieldManager;
/**
* The event dispatcher service.
*/
protected EventDispatcherInterface $eventDispatcher;
/**
* The entity type bundle info service.
*/
protected EntityTypeBundleInfoInterface $entityTypeBundleInfo;
/**
* Constructs a QueryFactory object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, EventDispatcherInterface $event_dispatcher, EntityTypeBundleInfoInterface $entity_type_bundle_info) {
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->eventDispatcher = $event_dispatcher;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
}
/**
* Prepares the property mappings for the given entity type ID.
*
* This is the central point where the field maps SPARQL-to-Drupal (inbound)
* and Drupal-to-SPARQL (outbound) are build. The parsed results are
* statically cached.
*
* @param string $entity_type_id
* The entity type ID.
*
* @throws \Exception
* Thrown when a bundle does not have the mapped bundle.
*/
protected function buildEntityTypeProperties($entity_type_id) {
if (empty($this->outboundMap[$entity_type_id]) && empty($this->inboundMap[$entity_type_id])) {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$this->outboundMap[$entity_type_id] = $this->inboundMap[$entity_type_id] = [];
$this->outboundMap[$entity_type_id]['bundle_key'] = $this->inboundMap[$entity_type_id]['bundle_key'] = $entity_type->getKey('bundle') ?: NULL;
foreach ($this->entityTypeBundleInfo->getBundleInfo($entity_type_id) as $bundle_id => $bundle_info) {
$field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle_id);
$mapping = SparqlMapping::loadByName($entity_type_id, $bundle_id);
if (!$bundle_mapping = $mapping->getRdfType()) {
throw new \Exception("The {$bundle_info['label']} SPARQL entity does not have an rdf_type set.");
}
$this->outboundMap[$entity_type_id]['bundles'][$bundle_id] = $bundle_mapping;
// More than one Drupal bundle can share the same mapped URI.
$this->inboundMap[$entity_type_id]['bundles'][$bundle_mapping][] = $bundle_id;
$base_fields_mapping = $mapping->getMappings();
foreach ($field_definitions as $field_name => $field_definition) {
$field_storage_definition = $field_definition->getFieldStorageDefinition();
// @todo Unify field mappings in #3.
// @see https://github.com/ec-europa/sparql_entity_storage/issues/3
if ($field_storage_definition instanceof BaseFieldDefinition) {
$field_mapping = $base_fields_mapping[$field_name] ?? NULL;
}
else {
$field_mapping = $field_storage_definition->getThirdPartySetting('sparql_entity_storage', 'mapping');
}
// This field is not mapped.
if (!$field_mapping) {
continue;
}
$this->outboundMap[$entity_type_id]['fields'][$field_name]['main_property'] = $field_storage_definition->getMainPropertyName();
foreach ($field_mapping as $column_name => $column_mapping) {
if (empty($column_mapping['predicate'])) {
continue;
}
// Handle the serialized values.
$serialize = FALSE;
$field_storage_schema = $field_storage_definition->getSchema()['columns'];
// Inflate value back into a normal item.
if (!empty($field_storage_schema[$column_name]['serialize'])) {
$serialize = TRUE;
}
// Retrieve the property definition primitive data type.
$property_definition = $field_storage_definition->getPropertyDefinition($column_name);
if (empty($property_definition)) {
throw new NonExistingFieldPropertyException("Field '$field_name' of type '{$field_storage_definition->getType()}' has no property '$column_name'.");
}
$data_type = $property_definition->getDataType();
$this->outboundMap[$entity_type_id]['fields'][$field_name]['columns'][$column_name][$bundle_id] = [
'predicate' => $column_mapping['predicate'],
'format' => $column_mapping['format'],
'serialize' => $serialize,
'data_type' => $data_type,
];
$this->inboundMap[$entity_type_id]['fields'][$column_mapping['predicate']][$bundle_id] = [
'field_name' => $field_name,
'column' => $column_name,
'serialize' => $serialize,
'type' => $field_storage_definition->getType(),
'data_type' => $data_type,
];
}
}
}
}
}
/**
* {@inheritdoc}
*/
public function getInboundMap(string $entity_type_id): array {
if (!isset($this->inboundMap[$entity_type_id])) {
$this->buildEntityTypeProperties($entity_type_id);
}
return $this->inboundMap[$entity_type_id];
}
/**
* {@inheritdoc}
*/
public function getFieldPredicates(string $entity_type_id, string $field_name, ?string $column_name = NULL, ?string $bundle = NULL): array {
$drupal_to_sparql = $this->getOutboundMap($entity_type_id);
if (!isset($drupal_to_sparql['fields'][$field_name])) {
throw new UnmappedFieldException("You are requesting the mapping for a non mapped field: $field_name (entity type: $entity_type_id).");
}
$field_mapping = $drupal_to_sparql['fields'][$field_name];
$column_name = $column_name ?: $field_mapping['main_property'];
$bundles = $bundle ? [$bundle] : array_keys($drupal_to_sparql['bundles']);
$return = [];
foreach ($bundles as $bundle) {
if (isset($field_mapping['columns'][$column_name][$bundle]['predicate'])) {
$return[$bundle] = $field_mapping['columns'][$column_name][$bundle]['predicate'];
}
}
return array_filter($return);
}
/**
* {@inheritdoc}
*/
public function getFieldFormat(string $entity_type_id, string $field_name, ?string $column_name = NULL, ?string $bundle = NULL): array {
$drupal_to_sparql = $this->getOutboundMap($entity_type_id);
if (!isset($drupal_to_sparql['fields'][$field_name])) {
throw new \Exception("You are requesting the mapping for a non mapped field: $field_name.");
}
$field_mapping = $drupal_to_sparql['fields'][$field_name];
$column_name = $column_name ?: $field_mapping['main_property'];
if (!empty($bundle)) {
return [$field_mapping['columns'][$column_name][$bundle]['format']];
}
return array_values(array_column($field_mapping['columns'][$column_name], 'format'));
}
/**
* {@inheritdoc}
*/
public function getFieldMainProperty(string $entity_type_id, string $field_name): string {
$outbound_data = $this->getOutboundMap($entity_type_id);
return $outbound_data['fields'][$field_name]['main_property'];
}
/**
* {@inheritdoc}
*/
public function getPropertyListToArray(string $entity_type_id): array {
$inbound_map = $this->getInboundMap($entity_type_id);
return array_unique(array_keys($inbound_map['fields']));
}
/**
* {@inheritdoc}
*/
public function hasFieldPredicate(string $entity_type_id, string $bundle, string $field_name, string $column_name): bool {
$drupal_to_sparql = $this->getOutboundMap($entity_type_id);
return isset($drupal_to_sparql['fields'][$field_name]['columns'][$column_name][$bundle]);
}
/**
* {@inheritdoc}
*/
public function isBundleMapped(string $entity_type_id, string $bundle): bool {
$outbound_map = $this->getOutboundMap($entity_type_id);
return isset($outbound_map['bundles'][$bundle]);
}
/**
* {@inheritdoc}
*/
public function bundlesToUris(string $entity_type_id, array $bundles, bool $to_resource_uris = FALSE): array {
if (SparqlArg::isValidResources($bundles)) {
return $bundles;
}
foreach ($bundles as $index => $bundle) {
$value = $this->getOutboundBundleValue($entity_type_id, $bundle);
if (empty($value)) {
throw new \Exception("The $bundle bundle does not have a mapping.");
}
$bundles[$index] = $to_resource_uris ? SparqlArg::uri($value) : $value;
}
return $bundles;
}
/**
* {@inheritdoc}
*/
public function getOutboundValue(string $entity_type_id, string $field_name, $value, ?string $langcode = NULL, ?string $column_name = NULL, ?string $bundle = NULL) {
$outbound_map = $this->getOutboundMap($entity_type_id);
$format = $this->getFieldFormat($entity_type_id, $field_name, $column_name, $bundle);
$format = reset($format);
$field_mapping_info = $this->getFieldInfoFromOutboundMap($entity_type_id, $field_name, $column_name, $bundle);
$field_mapping_info = reset($field_mapping_info);
$event = new OutboundValueEvent($entity_type_id, $field_name, $value, $field_mapping_info, $langcode, $column_name, $bundle, $format);
$this->eventDispatcher->dispatch($event, SparqlEntityStorageEvents::OUTBOUND_VALUE);
$value = $event->getValue();
$format = $event->getFormat();
$serialize = $this->isFieldSerializable($entity_type_id, $field_name, $column_name);
if ($serialize) {
$value = serialize($value);
}
if ($field_name === $outbound_map['bundle_key']) {
$value = $this->getOutboundBundleValue($entity_type_id, $value);
}
switch ($format) {
case static::RESOURCE:
return [
'type' => substr($value, 0, 2) == '_:' ? 'bnode' : 'uri',
'value' => $value,
];
case static::NON_TYPE:
return new Literal($value);
case static::TRANSLATABLE_LITERAL:
return Literal::create($value, $langcode);
default:
return Literal::create($value, NULL, $format);
}
}
/**
* {@inheritdoc}
*/
public function getInboundBundleValue(string $entity_type_id, string $bundle_uri): array {
$inbound_map = $this->getInboundMap($entity_type_id);
return $inbound_map['bundles'][$bundle_uri] ?? [];
}
/**
* {@inheritdoc}
*/
public function getInboundValue(string $entity_type_id, string $field_name, $value, ?string $langcode = NULL, ?string $column_name = NULL, ?string $bundle = NULL) {
// The outbound map contains the same information as the inbound map: the
// only difference is how the data is structured. It's safe to retrieve the
// field information from the outbound map.
// @see self::buildEntityTypeProperties()
$field_mapping_info = $this->getFieldInfoFromOutboundMap($entity_type_id, $field_name, $column_name, $bundle);
$field_mapping_info = reset($field_mapping_info);
$event = new InboundValueEvent($entity_type_id, $field_name, $value, $field_mapping_info, $langcode, $column_name, $bundle);
$this->eventDispatcher->dispatch($event, SparqlEntityStorageEvents::INBOUND_VALUE);
$value = $event->getValue();
if ($this->isFieldSerializable($entity_type_id, $field_name, $column_name)) {
$value = unserialize($value, ['allowed_classes' => FALSE]);
}
return $value;
}
/**
* {@inheritdoc}
*/
public static function getSupportedDataTypes(): array {
return [
static::RESOURCE => t('Resource'),
static::TRANSLATABLE_LITERAL => t('Translatable literal'),
static::NON_TYPE => t('String (No type)'),
'xsd:string' => t('Literal'),
'xsd:boolean' => t('Boolean'),
'xsd:date' => t('Date'),
'xsd:dateTime' => t('Datetime'),
'xsd:decimal' => t('Decimal'),
'xsd:integer' => t('Integer'),
'xsd:anyURI' => t('URI (xsd:anyURI)'),
];
}
/**
* {@inheritdoc}
*/
public function fieldIsMapped(string $entity_type_id, string $field_name): bool {
$outbound_map = $this->getOutboundMap($entity_type_id);
return isset($outbound_map['fields'][$field_name]);
}
/**
* {@inheritdoc}
*/
public function clearCache(): void {
unset($this->outboundMap);
unset($this->inboundMap);
}
/**
* Returns the Drupal-to-SPARQL mapping array.
*
* @param string $entity_type_id
* The entity type id.
*
* @return array
* The drupal-to-sparql array.
*/
protected function getOutboundMap(string $entity_type_id): array {
if (!isset($this->outboundMap[$entity_type_id])) {
$this->buildEntityTypeProperties($entity_type_id);
}
return $this->outboundMap[$entity_type_id];
}
/**
* Returns whether the field is serializable.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $field_name
* The field name.
* @param string|null $column_name
* (optional) The column name. If omitted, the main property will be used.
*
* @return bool
* Whether the field is serializable.
*
* @throws \Exception
* Thrown when a non existing field is requested.
*/
protected function isFieldSerializable(string $entity_type_id, string $field_name, ?string $column_name = NULL): bool {
$drupal_to_sparql = $this->getOutboundMap($entity_type_id);
if (!isset($drupal_to_sparql['fields'][$field_name])) {
throw new \Exception("You are requesting the mapping for a non mapped field: $field_name.");
}
$field_mapping = $drupal_to_sparql['fields'][$field_name];
$column_name = $column_name ?: $field_mapping['main_property'];
$serialize_array = array_column($field_mapping['columns'][$column_name], 'serialize');
if (empty($serialize_array)) {
return FALSE;
}
$serialize = reset($serialize_array);
return $serialize;
}
/**
* Returns the outbound bundle mapping.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle ID.
*
* @return string
* The bundle mapping.
*
* @throws \Exception
* Thrown when the bundle is not found.
*/
protected function getOutboundBundleValue(string $entity_type_id, string $bundle): string {
$outbound_map = $this->getOutboundMap($entity_type_id);
if (empty($outbound_map['bundles'][$bundle])) {
throw new \Exception("The $bundle bundle does not have a mapped id.");
}
return $outbound_map['bundles'][$bundle];
}
/**
* Retrieves information about the mapping of a certain field.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $field_name
* The field name.
* @param string|null $column_name
* (optional) The column name. If omitted, the field main property is used.
* @param string|null $bundle
* (optional) If passed, filter the final array by bundle.
*
* @return array
* An associative array with the information about the field mappings.
* When no bundle is specified, an array of arrays is returned, where the
* first level keys are all the bundles with that field.
*
* @throws \Exception
* Thrown when the field is not found.
*/
protected function getFieldInfoFromOutboundMap(string $entity_type_id, string $field_name, ?string $column_name = NULL, ?string $bundle = NULL): array {
$mapping = $this->getOutboundMap($entity_type_id);
if (!isset($mapping['fields'][$field_name])) {
throw new \Exception("You are requesting the mapping info for a non mapped field: $field_name.");
}
$field_mapping = $mapping['fields'][$field_name];
$column_name = $column_name ?: $field_mapping['main_property'];
if (!empty($bundle)) {
return [$field_mapping['columns'][$column_name][$bundle]];
}
return array_values($field_mapping['columns'][$column_name]);
}
}
