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;
}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc