external_entity-1.0.x-dev/src/Entity/ExternalEntityStorage.php
src/Entity/ExternalEntityStorage.php
<?php
declare(strict_types=1);
namespace Drupal\external_entity\Entity;
use Drupal\Core\Utility\Error;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Component\Utility\DeprecationHelper;
use Drupal\Core\Entity\Sql\DefaultTableMapping;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\external_entity\Contracts\ExternalEntityInterface;
use Drupal\external_entity\Contracts\ExternalEntityTypeInterface;
use Drupal\external_entity\Contracts\ExternalEntityStorageInterface;
use Drupal\external_entity\Definition\ExternalEntityDefaultDefinition;
/**
* Define the external entity storage class.
*/
class ExternalEntityStorage extends EntityStorageBase implements ExternalEntityStorageInterface {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\external_entity\Contracts\ExternalEntityTypeInterface
*/
protected $externalEntityType;
/**
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $externalEntityCache;
/**
* The remote entity storage constructor.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $memory_cache
* The memory cache.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $external_entity_cache
* The external entity cache.
*/
public function __construct(
EntityTypeInterface $entity_type,
MemoryCacheInterface $memory_cache,
EntityTypeManagerInterface $entity_type_manager,
CacheBackendInterface $external_entity_cache,
) {
parent::__construct($entity_type, $memory_cache);
$this->entityTypeManager = $entity_type_manager;
$this->externalEntityCache = $external_entity_cache;
}
/**
* {@inheritDoc}
*/
public static function createInstance(
ContainerInterface $container,
EntityTypeInterface $entity_type,
) {
return new static(
$entity_type,
$container->get('entity.memory_cache'),
$container->get('entity_type.manager'),
$container->get('cache.external_entity')
);
}
/**
* {@inheritDoc}
*/
public function loadRevision($revision_id): void {}
/**
* {@inheritDoc}
*/
public function deleteRevision($revision_id): void {}
/**
* {@inheritDoc}
*/
public function getResourceQuery(
string $resource,
string $entity_type_id,
string $conjunction = 'AND',
): QueryInterface {
return \Drupal::service($this->getQueryServiceName())->get(
$resource,
$this->entityType,
$entity_type_id,
$conjunction
);
}
/**
* {@inheritDoc}
*/
public function createEntityFromDefinition(
string $external_entity_type,
ExternalEntityDefaultDefinition $definition,
): ExternalEntityInterface {
return $this->create([
'uuid' => $definition->uuid(),
'label' => $definition->label(),
'path' => $definition->getPath(),
'type' => $external_entity_type,
'resource' => $definition->getResource(),
'variation' => $definition->getVariation(),
'properties' => $definition->getProperties(),
]);
}
/**
* {@inheritDoc}
*/
public function getQuery($conjunction = 'AND'): QueryInterface {
/** @var \Drupal\Core\Entity\Query\QueryFactoryInterface $factory */
$factory = \Drupal::service($this->getQueryStubServiceName());
return $factory->get(
$this->entityType,
$conjunction
);
}
/**
* {@inheritDoc}
*/
public function getAggregateQuery($conjunction = 'AND'): QueryInterface {
/** @var \Drupal\Core\Entity\Query\QueryFactoryInterface $factory */
$factory = \Drupal::service($this->getQueryStubServiceName());
return $factory->getAggregate(
$this->entityType,
$conjunction
);
}
/**
* {@inheritDoc}
*/
protected function setStaticCache(array $entities): void {
if ($this->entityType->isStaticallyCacheable()) {
foreach ($entities as $id => $entity) {
$this->memoryCache->set($this->buildCacheId($id), $entity, MemoryCacheInterface::CACHE_PERMANENT, [$this->memoryCacheTag]);
}
}
}
/**
* {@inheritDoc}
*/
protected function doLoadMultiple(?array $ids = NULL): array {
$entities = [];
/** @var \Drupal\external_entity\Definition\ExternalEntityDefaultDefinition $definition */
foreach ($this->loadExternalDefinitions($ids) as $external_entity_type_id => $definitions) {
foreach ($definitions as $uuid => $definition) {
$cid = "external_entity:entity:{$uuid}";
$cache = $this->externalEntityCache->get($cid);
if (
is_object($cache)
&& isset($cache->data)
&& $cache->data instanceof ExternalEntityInterface
) {
$entity = $cache->data;
}
else {
$entity = $this->createEntityFromDefinition(
$external_entity_type_id,
$definition
);
$this->externalEntityCache->set(
$cid,
$entity,
CacheBackendInterface::CACHE_PERMANENT,
$entity->getCacheTags()
);
}
$entities[$entity->id()] = $entity;
}
}
return $entities;
}
/**
* Define the table mapping.
*
* This is a workaround due to an issue when having an
* \Drupal\Core\Entity\FieldableEntityInterface implemented by the external
* entity object. When \Drupal\views\ViewsConfigUpdater::getMultivalueBaseFieldUpdateTableInfo
* is invoked when the view is saved, it will try to update the SQL table,
* but since the External Entity doesn't use an SQL table, since data is
* retrieved using an API connection.
*
* @return \Drupal\Core\Entity\Sql\DefaultTableMapping|null
*/
public function getTableMapping(): ?DefaultTableMapping {
return NULL;
}
/**
* Conduct the loading of the external definitions.
*
* @param \Drupal\external_entity\Contracts\ExternalEntityTypeInterface $external_entity_type
* The external entity type.
* @param array $ids
* An array of external entity ids.
*
* @return array
* An array of external definitions.
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function doLoadExternalDefinitions(
ExternalEntityTypeInterface $external_entity_type,
array $ids = [],
): array {
try {
if ($connection = $external_entity_type->getStorageConnection()) {
return $connection->connectionTypeInstance()->lookupDefinitions($ids);
}
}
catch (\Exception $exception) {
DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.1.0', fn() => Error::logException(\Drupal::logger('external_entity'), $exception), fn() => watchdog_exception('external_entity', $exception));
}
return [];
}
/**
* Get external entity type instance.
*
* @param string $name
* The external entity type name.
*
* @return \Drupal\external_entity\Contracts\ExternalEntityTypeInterface
* The external entity type instance.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getExternalEntityType(
string $name,
): ?ExternalEntityTypeInterface {
if (!isset($this->externalEntityType[$name])) {
$storage = $this->externalEntityTypeStorage();
$this->externalEntityType[$name] = $storage->load(
$name
);
}
return $this->externalEntityType[$name];
}
/**
* {@inheritDoc}
*/
protected function getQueryServiceName(): string {
return 'entity.query.api';
}
/**
* The query stub service name.
*
* @return string
* The key for the query stub service.
*/
protected function getQueryStubServiceName(): string {
return 'entity.query.stub';
}
/**
* {@inheritDoc}
*/
protected function doDelete($entities): void {
throw new \RuntimeException(
'Remote entity does not support delete.'
);
}
/**
* {@inheritDoc}
*/
protected function doSave($id, EntityInterface $entity): void {
throw new \RuntimeException(
'Remote entity does not support save.'
);
}
/**
* {@inheritDoc}
*/
protected function has($id, EntityInterface $entity): bool {
return TRUE;
}
/**
* Load external entity lookup definitions.
*
* @param array $ids
* An array of external entity ids.
*
* @return \Drupal\external_entity\Definition\ExternalEntityDefaultDefinition[]
* An array of external entity lookup definitions.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected function loadExternalDefinitions(array $ids): array {
$definitions = [];
foreach ($this->parseExternalEntityIds($ids) as $external_entity_type_id => $uuids) {
if ($external_entity_type = $this->getExternalEntityType($external_entity_type_id)) {
$definitions[$external_entity_type_id] = $this->doLoadExternalDefinitions(
$external_entity_type,
$uuids
);
}
}
return $definitions;
}
/**
* Parse external entity IDs.
*
* @param array $ids
* An array of external entity IDs concatenated with the external entity
* type and external UUID.
*
* @return array
* An array of parsed external entity IDs.
*/
protected function parseExternalEntityIds(array $ids): array {
$entity_ids = [];
foreach ($ids as $id) {
[$entity_external_type, $uuid] = explode(
ExternalEntity::ID_DELIMITER,
$id
);
if (!isset($entity_external_type, $uuid)) {
continue;
}
$entity_ids[$entity_external_type][] = $uuid;
}
return $entity_ids;
}
/**
* External entity type storage.
*
* @return \Drupal\Core\Entity\EntityStorageInterface
* The entity storage instance.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function externalEntityTypeStorage(): EntityStorageInterface {
return $this->entityTypeManager->getStorage(
'external_entity_type'
);
}
/**
* External entity connection storage.
*
* @return \Drupal\Core\Entity\EntityStorageInterface
* The entity storage instance.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function externalEntityConnectionStorage(): EntityStorageInterface {
return $this->entityTypeManager->getStorage(
'external_entity_connection'
);
}
}
