external_entity-1.0.x-dev/src/Entity/ExternalEntity.php
src/Entity/ExternalEntity.php
<?php
declare(strict_types=1);
namespace Drupal\external_entity\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Utility\Error;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Component\Utility\DeprecationHelper;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\external_entity\Contracts\ExternalEntityInterface;
use Drupal\external_entity\Contracts\ExternalEntityTypeInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
/**
* Define the external entity.
*
* @EntityType(
* id = "external_entity",
* label = @Translation("External Entity"),
* bundle_entity_type = "external_entity_type",
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid",
* "label" = "label"
* },
* handlers = {
* "storage" = "\Drupal\external_entity\Entity\ExternalEntityStorage",
* "access" = "\Drupal\external_entity\Entity\ExternalEntityAccessHandler",
* "views_data" = "\Drupal\external_entity\Entity\ExternalEntityViewsData",
* "view_builder" = "\Drupal\external_entity\Entity\ExternalEntityViewBuilder"
* }
* )
*/
class ExternalEntity extends ExternalEntityBase implements ExternalEntityInterface {
/**
* @var string|null
*/
protected ?string $type = null;
/**
* @var string|null
*/
protected ?string $uuid = null;
/**
* @var string|null
*/
protected ?string $label = null;
/**
* @var string|null
*/
protected ?string $path = null;
/**
* @var string|null
*/
protected ?string $resource = null;
/**
* @var string|null
*/
protected ?string $variation = null;
/**
* @var array
*/
protected array $fields = [];
/**
* @var array
*/
protected array $properties = [];
/**
* @var array
*/
protected array $fieldDefinitions = [];
/**
* @var array
*/
protected array $processedProperties = [];
/**
* Define the external ID delimiter.
*/
public const string ID_DELIMITER = '__';
/**
* {@inheritDoc}
*/
public function id(): ?string {
$uuid = $this->uuid();
if (!isset($uuid)) {
return NULL;
}
$delimiter = static::ID_DELIMITER;
return "{$this->bundle()}{$delimiter}{$uuid}";
}
/**
* {@inheritDoc}
*/
public function bundle(): ?string {
return $this->type;
}
/**
* {@inheritDoc}
*/
public function getPath(): ?string {
return $this->path ?? NULL;
}
/**
* {@inheritDoc}
*/
public function getResource(): string {
return $this->resource;
}
/**
* {@inheritDoc}
*/
public function getVariation(): string {
return $this->variation;
}
/**
* {@inheritDoc}
*/
public function isNew(): bool {
return FALSE;
}
/**
* {@inheritDoc}
*/
public function getProperties(bool $include_alteration = TRUE): array {
if (empty($this->processedProperties)) {
$this->processedProperties = $this->processProperties(
$this->getPropertyDefinitions()
);
}
return $this->processedProperties;
}
/**
* {@inheritDoc}
*/
public function getPropertyDefinitions(bool $include_alteration = TRUE): array {
$properties = $this->properties;
if ($include_alteration) {
$this->alterResourceProperties($properties);
}
return $properties;
}
/**
* {@inheritDoc}
*/
public function getResourceProperties(): array {
return $this->getBundleEntityType()->getResourceProperties(
$this->getResource(),
$this->getVariation()
);
}
/**
* {@inheritDoc}
*/
public function getBundleEntityTypeId(): ?string {
return $this->getEntityType()->getBundleEntityType();
}
/**
* {@inheritDoc}
*/
public function getBundleEntityType(): ?ExternalEntityTypeInterface {
$bundle = $this->bundle();
if (!isset($bundle)) {
return NULL;
}
return $this->getBundleEntityTypeStorage()->load($bundle);
}
/**
* {@inheritDoc}
*/
public static function baseFieldDefinitions(
EntityTypeInterface $entity_type,
): array {
$fields = [];
if ($entity_type->hasKey('id')) {
$fields[$entity_type->getKey('id')] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('ID'))
->setReadOnly(TRUE)
->setSetting('unsigned', TRUE);
}
if ($entity_type->hasKey('uuid')) {
$fields[$entity_type->getKey('uuid')] = BaseFieldDefinition::create('uuid')
->setLabel(new TranslatableMarkup('UUID'))
->setReadOnly(TRUE);
}
return $fields;
}
/**
* {@inheritDoc}
*/
public function getFieldDefinition($name): ?FieldDefinitionInterface {
return $this->getFieldDefinitions()[$name] ?? NULL;
}
/**
* {@inheritDoc}
*/
public function getFieldDefinitions(): array {
if (empty($this->fieldDefinitions)) {
/** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
foreach ($this->getResourceProperties() as $name => $info) {
if (
!isset($info['type'])
|| !$field_type_manager->hasDefinition($info['type'])
) {
continue;
}
$field_definition = BaseFieldDefinition::create($info['type']);
$field_definition->setName($name);
if ($label = $info['label'] ?? NULL) {
$field_definition->setLabel($label);
}
if ($settings = $info['settings'] ?? []) {
$field_definition->setSettings($settings);
}
$this->fieldDefinitions[$name] = $field_definition;
}
}
return $this->fieldDefinitions;
}
/**
* {@inheritDoc}
*/
public function get($field_name): FieldItemListInterface {
$fields = $this->getFields();
if (!isset($fields[$field_name])) {
throw new \InvalidArgumentException(sprintf('
The %s field name is invalid.', $field_name
));
}
return $fields[$field_name];
}
/**
* {@inheritDoc}
*/
public function set($field_name, $value, $notify = TRUE): self {
// Setting external entity field value is not supported at this time.
return $this;
}
/**
* {@inheritDoc}
*/
public function getFields($include_computed = TRUE): array {
if (!isset($this->fields) || empty($this->fields)) {
$entity_adapter = $this->getTypedDataEntityAdapter();
foreach ($this->getProperties() as $name => $info) {
$definition = $this->getFieldDefinition($name);
$item_class = $info['class'] ?? FieldItemList::class;
if (
!isset($definition, $info['value'])
|| !class_exists($item_class)
) {
continue;
}
$item = $item_class::createInstance(
$definition,
$name,
$entity_adapter
);
$item->setValue($info['value']);
$this->fields[$name] = $item;
}
}
return $this->fields;
}
/**
* {@inheritDoc}
*/
public function hasField($field_name): bool {
return isset($this->getFields()[$field_name]);
}
/**
* {@inheritDoc}
*/
public function getCacheTagsToInvalidate(): array {
$tags = [];
try {
$bundle = $this->getBundleEntityType();
$display = $bundle->getResourceDisplay(
$this->getResource(),
$this->getVariation()
);
$tags = Cache::mergeTags(
$bundle->getCacheTags(), $display->getCacheTags()
);
}
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 Cache::mergeTags([
"{$this->entityTypeId}:{$this->uuid()}",
"{$this->entityTypeId}:bundle:{$this->bundle()}",
"{$this->entityTypeId}:resource:{$this->getResource()}",
], $tags);
}
/**
* Alter the resource properties.
*
* @param array $properties
* An array of resource properties.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function alterResourceProperties(array &$properties): void {
foreach ($this->getResourcePropertyAlterations() as $resource) {
if (!isset($properties[$resource->getProperty()])) {
continue;
}
$property_value = &$properties[$resource->getProperty()]['value'];
if (is_array($property_value)) {
foreach ($property_value as &$value) {
foreach ($resource->getAlterations() as $key => $altered_value) {
$value[$key] = $altered_value;
}
}
}
else {
foreach ($resource->getAlterations() as $key => $altered_value) {
$property_value[$key] = $altered_value;
}
}
}
}
/**
* Get current resource property alterations.
*
* @return \Drupal\external_entity\Entity\ExternalEntityResourceAlter[]
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getResourcePropertyAlterations(): array {
return $this->getBundleEntityType()->getResourceAlterations(
$this->getResource(),
$this->getVariation()
);
}
/**
* Process the external entity properties.
*
* @param array $properties
* An array of property definitions.
*
* @return array
* An array of the processed properties.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\external_entity\Exception\NotFoundResourceDisplayException
*/
protected function processProperties(array $properties): array {
foreach ($properties as &$property) {
if (
!isset($property['value'], $property['type'])
|| empty($property['value'])
) {
continue;
}
$this->preparePropertyValue($property['type'], $property['value']);
}
return $properties;
}
/**
* Prepare the external entity property value.
*
* @param string $type
* The external entity property type.
* @param array $value
* An array of the external entity property values.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\external_entity\Exception\NotFoundResourceDisplayException
*/
protected function preparePropertyValue(
string $type,
array &$value,
): void {
if ($this->isFieldReference($type)) {
foreach ($value as &$data) {
if (!isset($data['target_uuid'])) {
continue;
}
$target_uuid = $data['target_uuid'];
$data['target_id'] = NULL;
$data['target_type'] = 'external_entity';
if (isset($data['target_revision_id'])) {
$data['target_revision_id'] = NULL;
}
$identifier = "{$this->bundle()}__{$target_uuid}";
unset($data['target_uuid']);
if ($external_entity = $this->loadEntity($identifier)) {
/** @var \Drupal\external_entity\Entity\ExternalEntityViewBuilder $view_builder */
$view_builder = $this->entityTypeManager()->getViewBuilder(
$external_entity->getEntityTypeId()
);
$data['entity'] = $view_builder->createProxyEntity($external_entity);
}
}
}
}
/**
* Load an external entity.
*
* @param string $identifier
* The external entity identifier.
*
* @return \Drupal\Core\Entity\EntityInterface|null
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function loadEntity(string $identifier): ?EntityInterface {
$storage = $this->entityTypeManager()->getStorage('external_entity');
return $storage->load($identifier);
}
/**
* Check if a field type is a reference entity.
*
* @param string $type
* An entity field type.
*
* @return bool
* Return TRUE if the field type is a reference; otherwise FALSE.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function isFieldReference(string $type): bool {
if (!$this->fieldTypeManager()->hasDefinition($type)) {
return FALSE;
}
$base = EntityReferenceItem::class;
$class = $this->fieldTypeManager()->getPluginClass($type);
return $class === $base || is_subclass_of($class, $base);
}
/**
* Get the typed data entity adapter.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The typed data entity adapter.
*/
protected function getTypedDataEntityAdapter(): TypedDataInterface {
$typed_data_manager = $this->typedDataManager();
return $typed_data_manager->create(
$typed_data_manager->createDataDefinition('entity'),
$this,
'entity'
);
}
/**
* Get the bundle entity type storage.
*
* @return \Drupal\Core\Entity\EntityStorageInterface
* The bundle entity type storage instance.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getBundleEntityTypeStorage(): EntityStorageInterface {
return $this->entityTypeManager()->getStorage($this->getBundleEntityTypeId());
}
/**
* Typed data manager service.
*
* @return \Drupal\Core\TypedData\TypedDataManagerInterface
* The typed data manager instance.
*/
protected function typedDataManager(): TypedDataManagerInterface {
return \Drupal::service('typed_data_manager');
}
/**
* Field type manager.
*
* @return \Drupal\Core\Field\FieldTypePluginManagerInterface
* The field type manager.
*/
protected function fieldTypeManager(): FieldTypePluginManagerInterface {
return \Drupal::service('plugin.manager.field.field_type');
}
}
