external_entities-8.x-2.x-dev/src/Entity/ExternalEntity.php
src/Entity/ExternalEntity.php
<?php
namespace Drupal\external_entities\Entity;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityPublishedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\RevisionableEntityBundleInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Entity\TranslatableInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\external_entities\Event\ExternalEntitiesEvents;
use Drupal\external_entities\Event\ExternalEntityBaseDefinitionEvent;
use Drupal\external_entities\Event\ExternalEntityExtractRawDataEvent;
use Drupal\user\EntityOwnerTrait;
/**
* Defines the external entity class.
*
* @see external_entities_entity_type_build()
*/
class ExternalEntity extends ContentEntityBase implements ExternalEntityInterface {
use EntityOwnerTrait;
use EntityPublishedTrait;
/**
* Source raw data provided by the storage client.
*
* @var array
*/
protected $rawData;
/**
* Returns external entity content entity base definition.
*
* @param \Drupal\external_entities\Entity\ExternalEntityTypeInterface $external_entity_type
* The external entity type.
* @param array $definition
* Definition overrides.
*
* @return array
* Base content entity definition.
*/
public static function getBaseDefinition(
ExternalEntityTypeInterface $external_entity_type,
array $definition = [],
) :array {
$base_definition = [
'id' => 'external_entity',
'label' => t('External Entity'),
'label_plural' => t('External Entities'),
'label_collection' => t('External Entities'),
'provider' => 'external_entities',
'class' => 'Drupal\external_entities\Entity\ExternalEntity',
'group' => 'content',
'group_label' => t('Content'),
'translatable' => TRUE,
'admin_permission' => 'administer external entity types',
'permission_granularity' => 'entity_type',
'persistent_cache' => FALSE,
'handlers' => [
'storage' => 'Drupal\external_entities\ExternalEntityStorage',
'view_builder' => 'Drupal\Core\Entity\EntityViewBuilder',
'form' => [
'default' => 'Drupal\external_entities\Form\ExternalEntityForm',
'edit' => 'Drupal\external_entities\Form\ExternalEntityForm',
'delete' => 'Drupal\external_entities\Form\ExternalEntityDeleteForm',
],
'list_builder' => 'Drupal\external_entities\ExternalEntityListBuilder',
'translation' => 'Drupal\external_entities\ExternalEntityTranslationHandler',
'access' => 'Drupal\external_entities\ExternalEntityAccessControlHandler',
'route_provider' => [
'html' => 'Drupal\external_entities\Routing\ExternalEntityHtmlRouteProvider',
],
],
'links' => [],
'entity_keys' => [
'id' => 'id',
'uuid' => 'uuid',
'label' => 'title',
'owner' => 'uid',
'langcode' => 'langcode',
'published' => 'status',
],
];
// Apply the definition overrides.
$definition = NestedArray::mergeDeep($base_definition, $definition);
$event = new ExternalEntityBaseDefinitionEvent(
$definition,
$external_entity_type
);
\Drupal::service('event_dispatcher')->dispatch(
$event,
ExternalEntitiesEvents::EXTERNAL_ENTITY_BASE_DEFINITION
);
return $event->getBaseDefinition();
}
/**
* {@inheritdoc}
*/
public function __construct(
array $values,
$entity_type,
$bundle = FALSE,
$translations = [],
) {
parent::__construct($values, $entity_type, $bundle, $translations);
$this->rawData = [];
}
/**
* {@inheritdoc}
*/
public function getExternalEntityType() :ExternalEntityTypeInterface {
return $this
->entityTypeManager()
->getStorage('external_entity_type')
->load($this->getEntityTypeId());
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
// Base fields (id, uuid, langcode).
$fields = parent::baseFieldDefinitions($entity_type);
// Owner field (uid).
$fields += static::ownerBaseFieldDefinitions($entity_type);
// Replace current field definition to allow non-numeric identifiers.
$fields[$entity_type->getKey('id')] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('ID'))
->setReadOnly(TRUE);
// Title field.
$fields[$entity_type->getKey('label')] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('Title'))
->setRequired(TRUE)
->setTranslatable(TRUE)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
])
->setDisplayConfigurable('view', TRUE)
->setDisplayOptions('form', [
'type' => 'string_textfield',
])
->setDisplayConfigurable('form', TRUE);
$fields[$entity_type->getKey('published')] = BaseFieldDefinition::create('boolean')
->setLabel(t('Publishing status'))
->setDescription(t('A boolean indicating whether the content is published.'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setDefaultValue(TRUE)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
])
->setDisplayConfigurable('view', TRUE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
'settings' => [
'display_label' => TRUE,
],
'weight' => 120,
])
->setDisplayConfigurable('form', TRUE);
// Note: translation fields are added automatically when external entity
// type is set to "translatable" in the "Content language and translation"
// administration interface.
return $fields;
}
/**
* {@inheritdoc}
*/
public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
$fields = parent::bundleFieldDefinitions($entity_type, $bundle, $base_field_definitions);
// Here $entity_type is an instance of
// \Drupal\Core\Entity\ContentEntityTypeInterface and not a
// \Drupal\external_entities\Entity\ExternalEntityTypeInterface instance. We
// need to load the corresponding ExternalEntityTypeInterface instance to
// work with it.
/** @var \Drupal\external_entities\Entity\ExternalEntityTypeInterface $external_entity_type */
$external_entity_type = \Drupal::entityTypeManager()
->getStorage('external_entity_type')
->load($entity_type->id());
if ($external_entity_type && $external_entity_type->isAnnotatable()) {
// Add the annotation reference field.
$fields[ExternalEntityInterface::ANNOTATION_FIELD] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Annotation'))
->setComputed(TRUE)
->setDescription(t('The annotation entity.'))
->setSetting('target_type', $external_entity_type->getAnnotationEntityTypeId())
->setSetting('handler', 'default')
->setSetting('handler_settings', [
'target_bundles' => [$external_entity_type->getAnnotationBundleId()],
]);
// Have the external entity inherit annotation fields.
$inherited_fields = $external_entity_type->getInheritedAnnotationFields();
foreach ($inherited_fields as $field) {
$field_definition = BaseFieldDefinition::createFromFieldStorageDefinition($field->getFieldStorageDefinition())
->setName(ExternalEntityInterface::ANNOTATION_FIELD_PREFIX . $field->getName())
->setReadOnly(TRUE)
->setComputed(TRUE)
->setLabel($field->getLabel())
->setDescription($field->getDescription())
->setSettings($field->getSettings())
->setRequired($field->isRequired())
->setTranslatable($field->isTranslatable())
->setDefaultValue($field->getDefaultValueLiteral())
->setDefaultValueCallback($field->getDefaultValueCallback())
->setDisplayConfigurable('form', $field->isDisplayConfigurable('form'))
->setDisplayOptions('form', $field->getDisplayOptions('form') ?: [])
->setDisplayConfigurable('view', $field->isDisplayConfigurable('view'))
->setDisplayOptions('view', $field->getDisplayOptions('view') ?: []);
$fields[$field_definition->getName()] = $field_definition;
}
}
return $fields;
}
/**
* {@inheritdoc}
*/
public function toRawData() :array {
// Not using $this->toArray() here because we don't want computed values.
$entity_values = [];
foreach ($this->getFields(FALSE) as $name => $property) {
$entity_values[$name] = $property->getValue();
}
// Get entity langcode.
$storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
$raw_data = $storage->createRawDataFromEntityValues(
$entity_values,
$this->getOriginalRawData(),
$this->language()->getId()
);
// Allow other modules to perform custom extraction logic.
$event = new ExternalEntityExtractRawDataEvent($this, $raw_data);
\Drupal::service('event_dispatcher')->dispatch(
$event,
ExternalEntitiesEvents::EXTRACT_RAW_DATA
);
return $event->getRawData();
}
/**
* {@inheritdoc}
*/
public function setOriginalRawData(array $raw_data) :self {
$this->rawData = $raw_data;
return $this;
}
/**
* {@inheritdoc}
*/
public function getOriginalRawData() :array {
return $this->rawData;
}
/**
* {@inheritdoc}
*/
public function createAnnotation(array $values = []) :ContentEntityInterface {
$external_entity_type = $this->getExternalEntityType();
$annotation_entity_type = $this
->entityTypeManager()
->getDefinition($external_entity_type->getAnnotationEntityTypeId());
$bundle_key = $annotation_entity_type->getKey('bundle');
if ($bundle_key) {
$values[$bundle_key] = $external_entity_type->getAnnotationBundleId();
}
$langcode_key = $annotation_entity_type->getKey('langcode');
if ($langcode_key) {
$values[$langcode_key] = $this->getUntranslated()->language()->getId();
}
return $this
->entityTypeManager()
->getStorage($external_entity_type->getAnnotationEntityTypeId())
->create($values + [
// @todo What do we do if the external entity doesn't have an ID?
$external_entity_type->getAnnotationFieldName() => $this->id(),
]);
}
/**
* {@inheritdoc}
*/
public function getAnnotation() :ContentEntityInterface|null {
if (!$this->id()) {
return NULL;
}
$annotation = current(
$this
->entityTypeManager()
->getStorage($this->getEntityTypeId())
->getAnnotations([$this->id()])
);
return $annotation ?: NULL;
}
/**
* {@inheritdoc}
*/
public function mapAnnotationFields(?ContentEntityInterface $annotation = NULL) :ExternalEntityInterface {
$external_entity_type = $this->getExternalEntityType();
if (!$external_entity_type->isAnnotatable()) {
return $this;
}
if (!$annotation) {
$annotation = $this->getAnnotation();
}
if (!$annotation) {
return $this;
}
$this->set(ExternalEntityInterface::ANNOTATION_FIELD, $annotation);
$inherited_fields = $external_entity_type->getInheritedAnnotationFields();
foreach (array_keys($this->getTranslationLanguages()) as $langcode) {
if ($annotation instanceof TranslatableInterface && $annotation->isTranslatable()) {
if (!$annotation->hasTranslation($langcode)) {
continue;
}
$annotation_translation = $annotation->getTranslation($langcode);
}
else {
if ($this->getUntranslated()->language()->getId() !== $langcode) {
continue;
}
$annotation_translation = $annotation;
}
foreach ($inherited_fields as $field_name => $inherited_field) {
$this
->getTranslation($langcode)
->set(
ExternalEntityInterface::ANNOTATION_FIELD_PREFIX . $field_name,
$annotation_translation->get($field_name)->getValue(),
);
}
}
return $this;
}
/**
* Gets the fields that can be inherited by the external entity.
*
* @param \Drupal\external_entities\Entity\ExternalEntityTypeInterface $type
* The type of the external entity.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
* An array of field definitions, keyed by field name.
*
* @see \Drupal\Core\Entity\EntityManagerInterface::getFieldDefinitions()
*/
public static function getInheritedAnnotationFields(ExternalEntityTypeInterface $type) {
$inherited_fields = [];
$field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($type->getAnnotationEntityTypeId(), $type->getAnnotationBundleId());
foreach ($field_definitions as $field_name => $field_definition) {
if ($field_name !== $type->getAnnotationFieldName()) {
$inherited_fields[$field_name] = $field_definition;
}
}
return $inherited_fields;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return Cache::mergeTags(
Cache::mergeTags(parent::getCacheTags(), $this->getCacheTagsToInvalidate()),
$this->getExternalEntityType()->getCacheTags()
);
}
/**
* {@inheritdoc}
*/
public function getCacheTagsToInvalidate() {
return Cache::mergeTags(
parent::getCacheTagsToInvalidate(),
$this->getExternalEntityType()->getCacheTagsToInvalidate()
);
}
/**
* {@inheritDoc}
*/
public function getCacheMaxAge() {
return $this->getExternalEntityType()->getPersistentCacheMaxAge();
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) :void {
parent::postSave($storage, $update);
$this->saveAnnotationAfterExternalEntitySave($storage);
}
/**
* Saves the annotation after the external entity is saved.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage object.
*
* @see ExternalEntity::postSave()
*/
protected function saveAnnotationAfterExternalEntitySave(EntityStorageInterface $storage) :void {
if (!$this->hasField(ExternalEntityInterface::ANNOTATION_FIELD)) {
return;
}
// If this external entity was automatically saved because of a change in
// the annotation, there's no need to save the annotation again.
if (!empty($this->{ExternalEntityInterface::EXTERNAL_ENTITY_AUTO_SAVE_INDUCED_BY_ANNOTATION_CHANGE_PROPERTY})) {
return;
}
/** @var \Drupal\Core\Entity\ContentEntityInterface|null $annotation */
$annotation = $this
->get(ExternalEntityInterface::ANNOTATION_FIELD)
->entity;
if (!$annotation) {
$annotation = $this->createAnnotation();
$this->set(ExternalEntityInterface::ANNOTATION_FIELD, $annotation);
}
if (!$annotation) {
return;
}
// Check if a new revision needs to be created or not.
if ($annotation instanceof RevisionableInterface) {
$bundle_entity_type = $annotation
->getEntityType()
->getBundleEntityType();
if ($bundle_entity_type) {
$bundle_entity = $this
->entityTypeManager()
->getStorage($bundle_entity_type)
->load($annotation->bundle());
if ($bundle_entity instanceof RevisionableEntityBundleInterface && $bundle_entity->shouldCreateNewRevision()) {
$annotation->setNewRevision();
}
}
}
// If the annotation is translatable we make sure all necessary translations
// are present on the annotation.
$new_translations = [];
if ($annotation instanceof TranslatableInterface && $annotation->isTranslatable()) {
foreach (array_keys($this->getTranslationLanguages()) as $langcode) {
if (!$annotation->hasTranslation($langcode)) {
$annotation->addTranslation($langcode);
$new_translations[] = $langcode;
}
}
}
$save_annotation = FALSE;
foreach (array_keys($this->getTranslationLanguages()) as $langcode) {
if ($annotation instanceof TranslatableInterface && $annotation->isTranslatable()) {
$annotation_translation = $annotation->getTranslation($langcode);
}
else {
if ($this->getUntranslated()->language()->getId() !== $langcode) {
continue;
}
$annotation_translation = $annotation;
}
foreach ($this->getExternalEntityType()->getInheritedAnnotationFields() as $field_definition) {
$inherited_field_name = ExternalEntityInterface::ANNOTATION_FIELD_PREFIX . $field_definition->getName();
if (!$this->hasField($inherited_field_name) || !$annotation->hasField($field_definition->getName())) {
continue;
}
if (!$annotation_translation->get($field_definition->getName())->equals($this->getTranslation($langcode)->get($inherited_field_name))) {
if (!$annotation->isNew() || !$this->getTranslation($langcode)->get($inherited_field_name)->isEmpty()) {
$annotation_translation->set($field_definition->getName(), $this->getTranslation($langcode)->get($inherited_field_name)->getValue());
}
else {
$this->getTranslation($langcode)->set($inherited_field_name, $annotation_translation->get($field_definition->getName())->getValue());
}
$save_annotation = TRUE;
}
elseif ($annotation->isNew() || in_array($langcode, $new_translations, TRUE)) {
if (!$this->getTranslation($langcode)->get($inherited_field_name)->isEmpty()) {
$annotation_translation->set($field_definition->getName(), $this->getTranslation($langcode)->get($inherited_field_name)->getValue());
$save_annotation = TRUE;
}
}
}
}
if ($save_annotation) {
$annotation->{ExternalEntityInterface::ANNOTATION_AUTO_SAVE_INDUCED_BY_EXTERNAL_ENTITY_CHANGE_PROPERTY} = TRUE;
$annotation->save();
$storage->resetCache([$this->id()]);
}
}
/**
* {@inheritdoc}
*/
public function getTranslation($langcode) {
// Load translation data if needed and available.
if (($langcode != LanguageInterface::LANGCODE_DEFAULT)
&& (!isset($this->translations[$langcode]['entity']))
) {
$this->getExternalTranslation($langcode);
}
return parent::getTranslation($langcode);
}
/**
* Returns the corresponding external translation if available.
*
* @param string|null $langcode
* The language code. If null, set default.
*
* @return \Drupal\external_entities\Entity\ExternalEntityInterface|null
* The translated external entity.
*/
public function getExternalTranslation(?string $langcode) :?ExternalEntityInterface {
$translation = NULL;
// Ensure we always use the default language code when dealing with the
// original entity language.
if (empty($langcode)
|| ($this->languageManager()->getDefaultLanguage()->getId() == $langcode)
) {
$langcode = LanguageInterface::LANGCODE_DEFAULT;
}
if ($langcode != LanguageInterface::LANGCODE_DEFAULT) {
// Check if the requested translation has already been loaded and if not,
// check if we could load it from remote source and load it if possible.
if (empty($this->translations[$langcode]['entity'])) {
$storage = $this->entityTypeManager()->getStorage($this->getEntityTypeId());
// Check if external entity type uses specific settings for the data
// aggregator, meaning we need to load new data from storage(s).
if ($this->getExternalEntityType()->isDataAggregatorOverridden($langcode)) {
// We must use different data aggregation settings and load from an
// external source.
// @todo Maybe we could provide a field mapping on the default
// language to pre-get the list of available languages and avoid
// loading from an external source when we know it is not available.
$new_raw_data = $storage->getRawDataFromExternalStorage([$this->id()], $langcode);
if (!empty($new_raw_data[$this->id()])) {
// Map the data into entity objects and according fields.
$translation = $storage->mapRawDataToExternalEntities($new_raw_data, $langcode)[$this->id()] ?? NULL;
}
}
elseif ($this->getExternalEntityType()->isFieldMappingOverridden($langcode)) {
// We use current data but the mapping changes.
$translation = $storage->mapRawDataToExternalEntities(
[$this->id() => $this->translations[LanguageInterface::LANGCODE_DEFAULT]['entity']->getOriginalRawData()],
$langcode
)[$this->id()] ?? NULL;
// @todo The problem here is that we will always have something as it
// the id would be mapped to the same field as the original one, even
// if we don't have translated data. The idea would be to include a
// new data processor on the id field to only fill it if some
// translated fields, like the title, are not missing/empty.
}
if (!empty($translation)) {
// Initialize translation.
$translation->langcode = $langcode;
$translation->activeLangcode = $langcode;
// Need to be set to have the default translation displayed on the
// list of translations.
$translation->defaultLangcode =& $this->defaultLangcode;
// Remap fields and values according to langcode.
foreach ($translation->values as $key => $value) {
if (isset($value[LanguageInterface::LANGCODE_DEFAULT])) {
$this->values[$key][$langcode] ??= $value[LanguageInterface::LANGCODE_DEFAULT];
}
}
$translation->values = &$this->values;
foreach ($translation->fields as $key => $value) {
if (isset($value[LanguageInterface::LANGCODE_DEFAULT])) {
$this->fields[$key][$langcode] ??= $value[LanguageInterface::LANGCODE_DEFAULT];
}
}
$translation->fields = &$this->fields;
$translation->translations = &$this->translations;
$translation->enforceIsNew = &$this->enforceIsNew;
$translation->newRevision = &$this->newRevision;
$translation->entityKeys = &$this->entityKeys;
$translation->translatableEntityKeys = &$this->translatableEntityKeys;
$translation->translationInitialize = FALSE;
$translation->typedData = NULL;
$translation->loadedRevisionId = &$this->loadedRevisionId;
$translation->isDefaultRevision = &$this->isDefaultRevision;
$translation->enforceRevisionTranslationAffected = &$this->enforceRevisionTranslationAffected;
$translation->isSyncing = &$this->isSyncing;
$this->translations[$langcode]['entity'] = $translation;
$this->translations[$langcode]['status'] = static::TRANSLATION_EXISTING;
}
}
elseif (static::TRANSLATION_EXISTING == $this->translations[$langcode]['status']) {
$translation = $this->translations[$langcode]['entity'];
}
}
else {
$translation = $this;
}
return $translation;
}
/**
* {@inheritdoc}
*/
public function hasTranslation($langcode) {
if ($langcode == $this->defaultLangcode) {
$langcode = LanguageInterface::LANGCODE_DEFAULT;
}
// To know if there is a translation, we must try to load it from the
// external source.
return !empty($this->getExternalTranslation($langcode));
}
/**
* {@inheritdoc}
*/
public function getTranslationLanguages($include_default = TRUE) {
// If new, or not multilingual, assume to only have default site language.
if ($this->getUntranslated()->isNew()
|| !$this->languageManager()->isMultilingual()
) {
$defaul_langcode = $this->languageManager()->getDefaultLanguage()->getId();
return $include_default
? [$defaul_langcode => new Language(['id' => $defaul_langcode])]
: [];
}
// To know what languages are available, we need to try to load them in the
// "translations" structure and then use parent implementation that uses
// that structure.
$xntt_type = $this->getExternalEntityType();
$language_settings = $xntt_type->getLanguageSettings();
// Only loop on languages that have been set for this external entity.
foreach (($language_settings['overrides'] ?? []) as $langcode => $lang_settings) {
// Try to load the corresponding language, which will initialize the
// "translations" structure.
$this->getExternalTranslation($langcode);
}
$translation_languages = parent::getTranslationLanguages($include_default);
return $translation_languages;
}
/**
* {@inheritdoc}
*/
public function isTranslatable() {
$bundles = $this->entityTypeBundleInfo()->getBundleInfo($this->entityTypeId);
$language_settings = $this->getExternalEntityType()->getLanguageSettings();
// To be translatable, an external entity needs to have at least one
// translation override, otherwise, only the default language is available.
return !empty($bundles[$this->bundle()]['translatable'])
&& !empty($language_settings['overrides'])
&& !$this->getUntranslated()->language()->isLocked()
&& $this->languageManager()->isMultilingual();
}
/**
* {@inheritdoc}
*/
public function removeTranslation($langcode) {
if (isset($this->translations[$langcode])
&& ($langcode != LanguageInterface::LANGCODE_DEFAULT)
&& ($langcode != $this->defaultLangcode)
) {
foreach ($this->getFieldDefinitions() as $name => $definition) {
if ($definition->isTranslatable()) {
unset($this->values[$name][$langcode]);
unset($this->fields[$name][$langcode]);
}
}
}
else {
throw new \InvalidArgumentException("The specified translation ($langcode) cannot be removed.");
}
}
}
