external_entities-8.x-2.x-dev/external_entities.module
external_entities.module
<?php
/**
* @file
* Allows using remote entities, for example through a REST interface.
*/
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Entity\ContentEntityFormInterface;
use Drupal\Core\Entity\ContentEntityType;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\external_entities\Entity\ConfigurableExternalEntityTypeInterface;
use Drupal\external_entities\Entity\ExternalEntityInterface;
use Drupal\external_entities\Entity\ExternalEntityTypeInterface;
use Drupal\external_entities\Plugin\ExternalEntities\StorageClient\RestClientInterface;
use Drupal\external_entities\Plugin\Field\AnnotationTitleFieldItemList;
/**
* Implements hook_entity_type_build().
*/
function external_entities_entity_type_build(array &$entity_types) {
// Check for the external_entity_type config entity.
if (!empty($entity_types['external_entity_type'])) {
$external_entity_type_config = $entity_types['external_entity_type'];
$lock_status = \Drupal::state()->get('external_entities.type.locked');
// Get the existing external entity type configurations.
/** @var \Drupal\external_entities\Entity\ExternalEntityTypeInterface[] $external_entity_types */
$external_entity_types = \Drupal::entityTypeManager()->createHandlerInstance(
$external_entity_type_config->getHandlerClass('storage'),
$external_entity_type_config
)->loadMultiple();
// Add custom particular definitions for each external entity type.
foreach ($external_entity_types as $external_entity_type) {
// Definition override for the external entity type.
$definition = [
'id' => $external_entity_type->id(),
// phpcs:disable
'label' => t($external_entity_type->getLabel()),
'label_plural' => t($external_entity_type->getPluralLabel()),
'label_collection' => t($external_entity_type->getPluralLabel()),
// phpcs:enable
'links' => [
'collection' => "/{$external_entity_type->getBasePath()}",
'canonical' => "/{$external_entity_type->getBasePath()}/{{$external_entity_type->id()}}",
],
'field_ui_base_route' => 'entity.external_entity_type.' . $external_entity_type->id() . '.edit_form',
'permission_granularity' => 'entity_type',
'persistent_cache' => (bool) $external_entity_type->getPersistentCacheMaxAge(),
];
if (!$external_entity_type->isReadOnly() || $external_entity_type->isAnnotatable()) {
$definition['links']['add-form'] = "/{$external_entity_type->getBasePath()}/add";
$definition['links']['edit-form'] = "/{$external_entity_type->getBasePath()}/{{$external_entity_type->id()}}/edit";
$definition['links']['delete-form'] = "/{$external_entity_type->getBasePath()}/{{$external_entity_type->id()}}/delete";
}
// Base definition for the external entity type.
$definition = $external_entity_type
->get('content_class')::getBaseDefinition(
$external_entity_type,
$definition
);
// Fully disables field editing if display is locked.
if (!empty($lock_status[$external_entity_type->id()]['lock_display'])) {
unset($definition['field_ui_base_route']);
}
// Make sure the entity type has not been defined already.
// Normally, the creation of an already existing content type through the
// external entity type UI is forbidden, but external entity types created
// by config/code might bypass this protection so we should check to
// avoid problems.
if (!empty($entity_types[$definition['id']])) {
// Log warning.
\Drupal::logger('external_entities')->warning(
"Conflicting content type detected. Cannot use external entity type '"
. $definition['id']
. "' as it has already been defined."
);
}
else {
// Add the new content entity to the entity types.
$entity_types[$definition['id']] = new ContentEntityType($definition);
}
}
}
}
/**
* Implements hook_entity_operation().
*
* We need to generate Field UI operations (manage fields and displays) manually
* because the Field UI module only provides them for entity bundles, not entity
* types.
*
* @see field_ui_entity_operation()
*/
function external_entities_entity_operation(EntityInterface $entity) {
$operations = [];
if ($entity instanceof ExternalEntityTypeInterface
&& \Drupal::service('module_handler')->moduleExists('field_ui')
) {
// Get edition restrictions.
$lock_status = \Drupal::state()->get('external_entities.type.locked');
/** @var \Drupal\external_entities\ExternalEntityTypeInterface $entity */
$derived_entity_type = $entity->getDerivedEntityType();
$account = \Drupal::currentUser();
if ($account->hasPermission('administer ' . $derived_entity_type->id() . ' fields')
&& empty($lock_status[$derived_entity_type->id()]['lock_fields'])
&& empty($lock_status[$derived_entity_type->id()]['lock_display'])
) {
$operations['manage-fields'] = [
'title' => t('Manage fields'),
'weight' => 15,
'url' => Url::fromRoute("entity.{$derived_entity_type->id()}.field_ui_fields"),
];
}
if ($account->hasPermission('administer ' . $derived_entity_type->id() . ' form display')
&& empty($lock_status[$derived_entity_type->id()]['lock_display'])
) {
$operations['manage-form-display'] = [
'title' => t('Manage form display'),
'weight' => 20,
'url' => Url::fromRoute("entity.entity_form_display.{$derived_entity_type->id()}.default"),
];
}
if ($account->hasPermission('administer ' . $derived_entity_type->id() . ' display')
&& empty($lock_status[$derived_entity_type->id()]['lock_display'])
) {
$operations['manage-display'] = [
'title' => t('Manage display'),
'weight' => 25,
'url' => Url::fromRoute("entity.entity_view_display.{$derived_entity_type->id()}.default"),
];
}
}
return $operations;
}
/**
* Implements hook_entity_operation_alter().
*/
function external_entities_entity_operation_alter(array &$operations, EntityInterface $entity) {
if ($entity instanceof ExternalEntityTypeInterface) {
$lock_status = \Drupal::state()->get('external_entities.type.locked');
/** @var \Drupal\external_entities\ExternalEntityTypeInterface $entity */
$derived_entity_type = $entity->getDerivedEntityType();
if (!empty($lock_status[$derived_entity_type->id()]['lock_edit'])) {
unset($operations['edit']);
}
if (!empty($lock_status[$derived_entity_type->id()]['lock_delete'])) {
unset($operations['delete']);
}
}
}
/**
* Implements hook_local_tasks_alter().
*/
#[\Drupal\Core\Attribute\ReOrderHook('field_ui')]
function external_entities_local_tasks_alter(&$local_tasks) {
// Loop on external entity types to manage field ui locks.
$lock_status = \Drupal::state()->get('external_entities.type.locked');
if (empty($lock_status)) {
return;
}
$xntt_storage = \Drupal::entityTypeManager()->getStorage('external_entity_type');
$xntt_types = $xntt_storage
->getQuery()
->accessCheck(FALSE)
->execute();
foreach ($xntt_types as $xntt_type) {
if (!empty($lock_status[$xntt_type]['lock_display'])) {
unset($local_tasks['field_ui.fields:overview_' . $xntt_type]);
unset($local_tasks['field_ui.fields:field_edit_' . $xntt_type]);
unset($local_tasks['field_ui.fields:display_overview_' . $xntt_type]);
unset($local_tasks['field_ui.fields:field_display_default_' . $xntt_type]);
unset($local_tasks['field_ui.fields:form_display_overview_' . $xntt_type]);
unset($local_tasks['field_ui.fields:field_form_display_default_' . $xntt_type]);
}
elseif (!empty($lock_status[$xntt_type]['lock_fields'])) {
unset($local_tasks['field_ui.fields:overview_' . $xntt_type]);
unset($local_tasks['field_ui.fields:field_edit_' . $xntt_type]);
}
}
}
/**
* Implements hook_module_implements_alter().
*/
#[LegacyModuleImplementsAlter]
function external_entities_module_implements_alter(&$implementations, $hook) {
if ($hook == 'local_tasks_alter') {
// Append our implementation of 'hook_local_tasks_alter' after the field_ui
// one as we need to alter what field_ui does.
$field_ui_pos = array_search('field_ui', array_keys($implementations));
if (FALSE !== $field_ui_pos) {
$group = $implementations['external_entities'];
unset($implementations['external_entities']);
++$field_ui_pos;
$implementations =
array_slice($implementations, 0, $field_ui_pos, TRUE)
+ ['external_entities' => $group]
+ array_slice($implementations, $field_ui_pos, NULL, TRUE);
}
}
}
/**
* Implements hook_form_FORM_ID_alter() for 'field_storage_config_edit_form'.
*
* Replace the default cardinality form validation. External entity field values
* reside in an external storage making the higher delta checks unnecessary.
*/
function external_entities_form_field_storage_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if (!empty($form['cardinality_container']['#element_validate'])) {
$entity_type = \Drupal::entityTypeManager()->getDefinition($form_state->get('entity_type_id'));
if ($entity_type && $entity_type->getProvider() === 'external_entities') {
foreach ($form['cardinality_container']['#element_validate'] as $i => $callback) {
if ((is_string($callback) && $callback == '::validateCardinality')
|| (is_array($callback) && $callback[1] == 'validateCardinality')) {
$key = $i;
break;
}
}
if (isset($key)) {
$form['cardinality_container']['#element_validate'][$key] = 'external_entities_field_storage_config_edit_form_validate_cardinality';
}
}
}
}
/**
* Validates the cardinality form for external entities.
*
* This validates a subset of what the core cardinality validation validates.
*
* @param array $element
* The cardinality form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @see \Drupal\field_ui\Form\FieldStorageConfigEditForm::validateCardinality()
*/
function external_entities_field_storage_config_edit_form_validate_cardinality(array &$element, FormStateInterface $form_state) {
if ($form_state->getValue('cardinality') === 'number' && !$form_state->getValue('cardinality_number')) {
$form_state->setError($element['cardinality_number'], t('Number of values is required.'));
}
}
/**
* Implements hook_entity_bundle_field_info_alter().
*/
function external_entities_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {
if (!is_a($entity_type, ExternalEntityTypeInterface::class)) {
return;
}
$label_key = $entity_type->getKey('label');
if (empty($label_key)) {
return;
}
$count = \Drupal::entityQuery('external_entity_type')
->accessCheck(FALSE)
->condition('annotation_entity_type_id', $entity_type->getDerivedEntityTypeId())
->condition('annotation_bundle_id', $bundle)
->count()
->execute();
if (!$count) {
return;
}
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $base_field_definitions */
$base_field_definitions = call_user_func($entity_type->getClass() . '::baseFieldDefinitions', $entity_type);
if (!empty($base_field_definitions[$label_key])) {
$fields[$label_key] = clone $base_field_definitions[$label_key]
->setName($label_key)
->setTargetEntityTypeId($entity_type->getDerivedEntityTypeId())
->setTargetBundle($bundle)
->setClass(AnnotationTitleFieldItemList::class)
->setComputed(TRUE)
->setReadOnly(TRUE)
->setDisplayOptions('form', [
'region' => 'hidden',
])
->setDisplayConfigurable('form', FALSE);
}
}
/**
* Implements hook_help().
*/
function external_entities_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// For help overview pages we use the route help.page.$moduleName.
case 'help.page.external_entities':
return [
'external_entities_help' => [
'#theme' => 'external_entities_help',
'#title' => t('External Entities Help'),
],
];
}
}
/**
* Implements hook_theme().
*/
function external_entities_theme() {
return [
'external_entity' => [
'render element' => 'elements',
],
'external_entities_help' => [
'template' => 'external-entities-help',
'variables' => [],
],
'file_client_base_pattern_help' => [
'template' => 'file-client-base-pattern-help',
'variables' => [
'file_ext' => '',
],
],
];
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function external_entities_theme_suggestions_external_entity(array $variables) {
/** @var \Drupal\external_entities\Entity\ExternalEntityInterface $entity */
$entity = $variables['elements']['#entity'];
$sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
$suggestions[] = 'external_entity__' . $sanitized_view_mode;
$suggestions[] = 'external_entity__' . $entity->getEntityTypeId();
$suggestions[] = 'external_entity__' . $entity->getEntityTypeId() . '__' . $sanitized_view_mode;
$suggestions[] = 'external_entity__' . $entity->getEntityTypeId() . '__' . $entity->bundle();
$suggestions[] = 'external_entity__' . $entity->getEntityTypeId() . '__' . $entity->bundle() . '__' . $sanitized_view_mode;
$suggestions[] = 'external_entity__' . $entity->id();
$suggestions[] = 'external_entity__' . $entity->id() . '__' . $sanitized_view_mode;
return $suggestions;
}
/**
* Implements hook_entity_view_alter().
*/
function external_entities_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
if ($entity instanceof ExternalEntityInterface) {
$build['#theme'] = 'external_entity';
$build['#entity'] = $entity;
}
}
/**
* Implements hook_entity_insert().
*/
function external_entities_entity_insert(EntityInterface $entity) {
_external_entities_save_annotated_external_entity($entity);
}
/**
* Implements hook_entity_update().
*/
function external_entities_entity_update(EntityInterface $entity) {
_external_entities_save_annotated_external_entity($entity);
}
/**
* Implements hook_entity_delete().
*/
function external_entities_entity_delete(EntityInterface $entity) {
_external_entities_save_annotated_external_entity($entity);
}
/**
* Save the annotated external entity.
*
* Saves the external entity (if any) that the given entity is annotating. We do
* this because annotation changes can have indirect changes to an external
* entity.
*
* An example use case: an annotatable external entity is displayed (entity
* display) along with various inherited annotation fields. Saving
* the external entity on annotation change will make sure the render cache
* for the external entity is invalidated and the annotation changes become
* visible.
*
* Another example use case: a pathauto pattern is configured for an annotatable
* external entity, and the pattern uses an inherited annotation field. Saving
* the external entity on annotation change will make sure the generated path
* is updated.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* An entity object.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
function _external_entities_save_annotated_external_entity(EntityInterface $entity) {
if (!empty($entity->{ExternalEntityInterface::ANNOTATION_AUTO_SAVE_INDUCED_BY_EXTERNAL_ENTITY_CHANGE_PROPERTY})) {
return;
}
if (!$entity instanceof FieldableEntityInterface) {
return;
}
/** @var \Drupal\external_entities\Entity\ExternalEntityTypeInterface[] $external_entity_types */
$external_entity_types = \Drupal::entityTypeManager()
->getStorage('external_entity_type')
->loadMultiple();
foreach ($external_entity_types as $external_entity_type) {
if (!$external_entity_type->isAnnotatable()
|| $external_entity_type->getAnnotationEntityTypeId() !== $entity->getEntityTypeId()
|| $external_entity_type->getAnnotationBundleId() !== $entity->bundle()
|| $entity->get($external_entity_type->getAnnotationFieldName())->isEmpty()
) {
continue;
}
/** @var \Drupal\Core\Entity\EntityInterface[] $referenced_entities */
$referenced_entities = $entity
->get($external_entity_type->getAnnotationFieldName())
->referencedEntities();
foreach ($referenced_entities as $referenced_entity) {
if (!$referenced_entity instanceof ExternalEntityInterface) {
continue;
}
if (!empty($entity->original) && !empty($external_entity_type->getInheritedAnnotationFields())) {
$referenced_entity->original = clone $referenced_entity;
$referenced_entity->original->mapAnnotationFields($entity->original);
}
$referenced_entity->{ExternalEntityInterface::EXTERNAL_ENTITY_AUTO_SAVE_INDUCED_BY_ANNOTATION_CHANGE_PROPERTY} = TRUE;
$referenced_entity->save();
}
}
}
/**
* Implements template_preprocess_HOOK().
*/
function template_preprocess_external_entity(&$variables) {
$variables['external_entity'] = $variables['elements']['#entity'];
$variables['entity_type'] = $variables['external_entity']->getEntityTypeId();
$variables['bundle'] = $variables['external_entity']->bundle();
// Build the $content variable for templates.
$variables += ['content' => []];
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
}
/**
* Implements hook_field_config_presave().
*/
function external_entities_field_config_presave(EntityInterface $entity): void {
try {
$external_entity_types = \Drupal::entityTypeManager()
->getStorage('external_entity_type')
->loadMultiple();
}
catch (InvalidPluginDefinitionException | PluginNotFoundException $exception) {
\Drupal::logger('external_entities')->error($exception);
return;
}
if (array_key_exists($entity->get('entity_type'), $external_entity_types)) {
external_entities_set_config_dependencies($entity);
}
}
/**
* Set config dependency for entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* Defines a common interface for all entity objects.
*
* @return void
* Returns nothing.
*/
function external_entities_set_config_dependencies(EntityInterface $entity): void {
$dependencies = $entity->get('dependencies');
$parent_entity_config_id = 'external_entities.external_entity_type.' . $entity->getTargetEntityTypeId();
if (!in_array($parent_entity_config_id, $dependencies['config'] ?? [], TRUE)) {
$dependencies['config'][] = $parent_entity_config_id;
}
$entity->set('dependencies', $dependencies);
}
/**
* Implements hook_cron().
*/
function external_entities_cron() {
// Cleans up REST connection limitations.
$xntt_storage = \Drupal::entityTypeManager()->getStorage('external_entity_type');
$xntt_types = $xntt_storage
->getQuery()
->accessCheck(FALSE)
->execute();
// Loop on external entity types to check the ones using REST clients and list
// endpoints.
$endpoints = [];
foreach ($xntt_types as $xntt_type) {
$xntt_type_instance = $xntt_storage->load($xntt_type);
if ($xntt_type_instance instanceof ConfigurableExternalEntityTypeInterface) {
foreach ($xntt_type_instance->getDataAggregator()->getStorageClients() as $storage_client) {
if ($storage_client instanceof RestClientInterface) {
$config = $storage_client->getConfiguration();
$endpoints[$config['endpoint']] = TRUE;
}
}
}
}
$db_connection = \Drupal::database();
if (!empty($endpoints)) {
// Remove all entries not in the $endpoints list.
$num_deleted = $db_connection->delete('xntt_rest_queries')
->condition('endpoint', array_keys($endpoints), 'NOT IN')
->execute();
}
else {
// Remove all entries.
$num_deleted = $db_connection->delete('xntt_rest_queries')
->execute();
}
if ($num_deleted) {
\Drupal::logger('external_entities')->info("Removed $num_deleted (unused) endpoint trackings for query limitations.");
}
}
/**
* Regenerate the xntt-file index file for file storage client.
*
* This is a callback function for bacth processing.
*
* @param array $params
* An array of parameters.
* @param string $mode
* Processing mode. One of 'update' or 'regenerate'.
* 'update' will remove unexisting entities/files and add missing ones while
* 'regenerate' will recreate the whole file from scratch.
* @param array $context
* Drupal Batch API context variable.
*/
function external_entities_regenerate_index_file_process($params, $mode, &$context) {
// Check current status.
if (!array_key_exists('input_stack', $context['sandbox'])) {
// Init.
$context['finished'] = 0.05;
$context['message'] = t(
'Index file initialized, listing entity files in @dir...',
['@dir' => $params['config']['root']]
);
$context['sandbox']['input_stack'] = [$params['config']['root']];
$context['sandbox']['data_file_keys'] = [];
$context['sandbox']['data_file_stack'] = [];
$context['sandbox']['entity_file_index'] = [];
$context['sandbox']['processed_directories'] = 0;
$context['sandbox']['processed_files'] = 0;
$context['sandbox']['loaded_entities'] = 0;
$index_file = $context['results']['index_file'] =
$params['config']['performances']['index_file'];
if ('update' == $mode) {
$context['sandbox']['new_entries'] = 0;
if (file_exists($index_file)) {
// Read current index.
$config =
$params['config']
+ [
ExternalEntityTypeInterface::XNTT_TYPE_PROP => $params['xntt'],
'debug_level' => 0,
];
$storage_client =
\Drupal::service('plugin.manager.external_entities.storage_client')
->createInstance($params['plugin'], $config);
$context['sandbox']['entity_file_index'] =
$storage_client->getEntityFileIndex();
$context['sandbox']['cleanup'] = TRUE;
$context['results']['loaded_entries'] =
count($context['sandbox']['entity_file_index']);
}
else {
// Or create a new one if missing.
if ($fh = fopen($index_file, 'w')) {
fclose($fh);
}
else {
// Set an error.
$context['results']['error'] = t(
'Failed to create missing index file "@index_file".',
['@index_file' => $index_file]
);
$context['finished'] = 1;
}
}
}
elseif ('regenerate' == $mode) {
// Clear existing file.
if ($fh = fopen($index_file, 'w')) {
fclose($fh);
}
else {
// Set an error.
$context['results']['error'] = t(
'Failed to create index file "@index_file".',
['@index_file' => $index_file]
);
$context['finished'] = 1;
}
}
else {
// Unknown/unsupported mode, set an error.
$context['results']['error'] = t(
'Unsupported index modification mode: "@mode".',
['@mode' => $mode]
);
$context['finished'] = 1;
}
}
elseif (!empty($context['sandbox']['cleanup'])) {
$context['message'] = t('Index entries with missing files removed...');
$context['finished'] = 0.15;
unset($context['sandbox']['cleanup']);
$entity_file_index = $context['sandbox']['entity_file_index'];
$removed_entries = 0;
foreach ($entity_file_index as $id => $path) {
if (!file_exists($path)) {
unset($context['sandbox']['entity_file_index'][$id]);
++$removed_entries;
}
}
$context['results']['removed_entries'] = $removed_entries;
}
elseif (!empty($context['sandbox']['input_stack'])) {
// @todo Optimization: make sure 'structure' specification allows to dig
// deeper.
// Process input path stack to get all matching files.
// Each operation works on a single directory.
$dir = array_shift($context['sandbox']['input_stack']);
$context['message'] = t('Processed @path...', ['@path' => $dir]);
$context['finished'] = 0.30;
$context['sandbox']['processed_directories']++;
// Scan directory.
$root_length = strlen($params['config']['root']) + 1;
$files = scandir($dir, SCANDIR_SORT_NONE);
if ($files) {
foreach ($files as $file) {
$file_path = "$dir/$file";
// Remove leading root path.
$path_to_filter = substr($file_path, $root_length);
if (is_file($file_path)
&& (preg_match($params['regex'], $path_to_filter))
) {
// File matches, add to the list.
$context['sandbox']['data_file_keys'][$file_path] = TRUE;
}
elseif (is_dir($file_path) && !str_starts_with($file, '.')) {
// Add new directory for a next scan.
$context['sandbox']['input_stack'][] = $file_path;
}
}
}
}
elseif (!empty($context['sandbox']['data_file_keys'])) {
$context['message'] = t('Data files listed, listing related entities...');
$context['finished'] = 0.50;
// Get the list of unique files.
$context['sandbox']['data_file_stack'] =
array_keys($context['sandbox']['data_file_keys']);
// Clear keys.
$context['sandbox']['data_file_keys'] = [];
$context['results']['processed_directories'] =
$context['sandbox']['processed_directories'];
}
elseif (!empty($context['sandbox']['data_file_stack'])) {
// Process data files.
$file = array_shift($context['sandbox']['data_file_stack']);
$context['message'] = t(
'Loading entities from @file...',
['@file' => $file]
);
$context['finished'] = 0.65;
$context['sandbox']['processed_files']++;
$xntt_type = \Drupal::entityTypeManager()->getStorage('external_entity_type')->load($params['xntt']);
$config =
$params['config']
+ [
ExternalEntityTypeInterface::XNTT_TYPE_PROP => $xntt_type,
'debug_level' => 0,
];
$storage_client =
\Drupal::service('plugin.manager.external_entities.storage_client')
->createInstance($params['plugin'], $config);
$entities = $storage_client->loadFile($file, FALSE, TRUE);
$context['sandbox']['loaded_entities'] += count($entities);
$entity_file_path = substr($file, strlen($params['config']['root']) + 1);
// Process each entity.
if ('update' == $mode) {
foreach (array_keys($entities) as $id) {
if (empty($context['sandbox']['entity_file_index'][$id])) {
$context['sandbox']['new_entries']++;
$context['sandbox']['entity_file_index'][$id] = $entity_file_path;
}
}
}
else {
foreach (array_keys($entities) as $id) {
$context['sandbox']['entity_file_index'][$id] = $entity_file_path;
}
}
}
elseif (!empty($context['sandbox']['entity_file_index'])) {
// Finally, save the new index file.
$context['message'] = t('New entity-file index file saved.');
$context['finished'] = 1;
$xntt_type = \Drupal::entityTypeManager()->getStorage('external_entity_type')->load($params['xntt']);
$config =
$params['config']
+ [
ExternalEntityTypeInterface::XNTT_TYPE_PROP => $xntt_type,
'debug_level' => 0,
];
$storage_client =
\Drupal::service('plugin.manager.external_entities.storage_client')
->createInstance($params['plugin'], $config);
$storage_client->setEntityFileIndex(
$context['sandbox']['entity_file_index'],
TRUE
);
$context['results']['processed_files'] =
$context['sandbox']['processed_files'];
$context['results']['loaded_entities'] =
$context['sandbox']['loaded_entities'];
$context['results']['indexed_entities'] =
count($context['sandbox']['entity_file_index']);
if ('update' == $mode) {
$context['results']['new_entries'] =
$context['sandbox']['new_entries'];
}
}
else {
// Done.
$context['finished'] = 1;
}
}
/**
* Callback function for file indexing batch process end (file storage client).
*
* @param bool $success
* A boolean indicating whether the batch has completed successfully.
* @param array $results
* The value set in $context['results'] by callback_batch_operation().
* @param array $operations
* If $success is FALSE, contains the operations that remained unprocessed.
* @param string $elapsed
* A string representing the elapsed time for the batch process, e.g.,
* '1 min 30 secs'.
*/
function external_entities_regenerate_index_file_finished($success, $results, $operations, $elapsed) {
if ($success && empty($results['error'])) {
$message = '';
if (!empty($results['loaded_entries'])) {
$message .= \Drupal::translation()->formatPlural(
$results['loaded_entries'],
'Loaded 1 entry from index file.',
'Loaded @count entries from index file.'
) . "\n";
}
if (!empty($results['removed_entries'])) {
$message .= \Drupal::translation()->formatPlural(
$results['removed_entries'],
'Removed 1 obsolete entry from index file.',
'Removed @count obsolete entries from index file.'
) . "\n";
}
if (!empty($results['processed_directories'])) {
$message .= \Drupal::translation()->formatPlural(
$results['processed_directories'],
'Processed 1 directory.',
'Processed @count directories.'
) . "\n";
}
if (!empty($results['processed_files'])) {
$message .= \Drupal::translation()->formatPlural(
$results['processed_files'],
'Processed 1 file.',
'Processed @count files.'
) . "\n";
}
if (!empty($results['loaded_entities'])) {
$message .= \Drupal::translation()->formatPlural(
$results['loaded_entities'],
'Loaded 1 external entity.',
'Loaded @count external entities.'
) . "\n";
}
if (array_key_exists('new_entries', $results)) {
$message .= t(
'Successfully updated entity-file index file @file with @count new records.',
['@file' => $results['index_file'], '@count' => $results['new_entries']]
);
}
else {
$message .= t(
'Successfully regenerated entity-file index file @file with @count records.',
['@file' => $results['index_file'], '@count' => $results['indexed_entities'] ?? 0]
);
}
\Drupal::logger('external_entities')->notice($message);
\Drupal::messenger()->addStatus($message);
}
else {
$message = t('Finished with an error: @error',
['@error' => ($results['error'] ?? t('unexpected error'))]
);
\Drupal::logger('external_entities')->error($message);
\Drupal::messenger()->addError($message);
}
}
/**
* Implements hook_form_alter().
*/
function external_entities_form_alter(array &$form, FormStateInterface $form_state, string $form_id) {
$form_object = $form_state->getFormObject();
if (!$form_object instanceof ContentEntityFormInterface) {
return;
}
$entity = $form_object->getEntity();
if (!$entity instanceof ExternalEntityInterface) {
return;
}
elseif (in_array(
$form_id,
[
$entity->getEntityTypeId() . '_delete_form',
// @todo Not sure we need the confirm form.
$entity->getEntityTypeId() . '_confirm_form',
]
)
) {
// Do not display ID field on delete forms.
return;
}
// Provide an ID field as it might be needed depending on the external
// storage client, but since it might also be auto-generated, it is not set to
// required.
$form['id'] = [
'#type' => 'textfield',
'#title' => t('ID'),
'#default_value' => $entity->id() ?? '',
'#description' => $entity->isNew() ? t('Enter the ID for this external entity if needed.') : NULL,
'#maxlength' => 4096,
'#disabled' => !$entity->isNew(),
'#weight' => -10,
];
// Hide untranslatable fields on non-default translation forms. This is
// done by Drupal core for regular fields, but since our annotation fields
// are computed we have to do it ourselves.
// @see \Drupal\content_translation\ContentTranslationHandler::entityFormSharedElements()
// @see \Drupal\Core\Entity\EntityChangesDetectionTrait::getFieldsToSkipFromTranslationChangesCheck()
// @todo Is this a sign that our annotation fields should be marked as having
// a custom storage instead of being computed?
if ($entity->getExternalEntityType()->isAnnotatable()
&& $entity->isDefaultTranslationAffectedOnly()
&& !$entity->isDefaultTranslation()
) {
$inherited_annotation_fields = $entity
->getExternalEntityType()
->getInheritedAnnotationFields();
foreach ($inherited_annotation_fields as $field_definition) {
$inherited_field_name = ExternalEntityInterface::ANNOTATION_FIELD_PREFIX . $field_definition->getName();
if (empty($form[$inherited_field_name])) {
continue;
}
$inherited_field_definition = $entity
->get($inherited_field_name)
->getFieldDefinition();
if (!$inherited_field_definition->isTranslatable()) {
$form[$inherited_field_name]['#access'] = FALSE;
}
}
}
// Put action buttons at the end of the form.
$form['actions']['#weight'] = 5000;
}
