You are here

Server.php in Search API 8

File

src/Entity/Server.php
View source
<?php

namespace Drupal\search_api\Entity;

use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\search_api\Event\DeterminingServerFeaturesEvent;
use Drupal\search_api\Event\SearchApiEvents;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\LoggerTrait;
use Drupal\search_api\Query\QueryInterface;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\ServerInterface;
use Drupal\search_api\Utility\Utility;

/**
 * Defines the search server configuration entity.
 *
 * @ConfigEntityType(
 *   id = "search_api_server",
 *   label = @Translation("Search server"),
 *   label_collection = @Translation("Search servers"),
 *   label_singular = @Translation("search server"),
 *   label_plural = @Translation("search servers"),
 *   label_count = @PluralTranslation(
 *     singular = "@count search server",
 *     plural = "@count search servers",
 *   ),
 *   handlers = {
 *     "storage" = "Drupal\search_api\Entity\SearchApiConfigEntityStorage",
 *     "form" = {
 *       "default" = "Drupal\search_api\Form\ServerForm",
 *       "edit" = "Drupal\search_api\Form\ServerForm",
 *       "delete" = "Drupal\search_api\Form\ServerDeleteConfirmForm",
 *       "disable" = "Drupal\search_api\Form\ServerDisableConfirmForm",
 *       "clear" = "Drupal\search_api\Form\ServerClearConfirmForm",
 *     },
 *   },
 *   admin_permission = "administer search_api",
 *   config_prefix = "server",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "name",
 *     "uuid" = "uuid",
 *     "status" = "status",
 *   },
 *   config_export = {
 *     "id",
 *     "name",
 *     "description",
 *     "backend",
 *     "backend_config",
 *   },
 *   links = {
 *     "canonical" = "/admin/config/search/search-api/server/{search_api_server}",
 *     "add-form" = "/admin/config/search/search-api/add-server",
 *     "edit-form" = "/admin/config/search/search-api/server/{search_api_server}/edit",
 *     "delete-form" = "/admin/config/search/search-api/server/{search_api_server}/delete",
 *     "disable" = "/admin/config/search/search-api/server/{search_api_server}/disable",
 *     "enable" = "/admin/config/search/search-api/server/{search_api_server}/enable",
 *   }
 * )
 */
class Server extends ConfigEntityBase implements ServerInterface {
  use InstallingTrait;
  use LoggerTrait;

  /**
   * The ID of the server.
   *
   * @var string
   */
  protected $id;

  /**
   * The displayed name of the server.
   *
   * @var string
   */
  protected $name;

  /**
   * The displayed description of the server.
   *
   * @var string
   */
  protected $description = '';

  /**
   * The ID of the backend plugin.
   *
   * @var string
   */
  protected $backend;

  /**
   * The backend plugin configuration.
   *
   * @var array
   */
  protected $backend_config = [];

  /**
   * The backend plugin instance.
   *
   * @var \Drupal\search_api\Backend\BackendInterface
   */
  protected $backendPlugin;

  /**
   * The features this server supports.
   *
   * @var string[]|null
   */
  protected $features;

  /**
   * {@inheritdoc}
   */
  public function getDescription() {
    return $this->description;
  }

  /**
   * {@inheritdoc}
   */
  public function hasValidBackend() {
    $backend_plugin_definition = \Drupal::service('plugin.manager.search_api.backend')
      ->getDefinition($this
      ->getBackendId(), FALSE);
    return !empty($backend_plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public function getBackendId() {
    return $this->backend;
  }

  /**
   * {@inheritdoc}
   */
  public function getBackend() {
    if (!$this->backendPlugin) {
      $backend_plugin_manager = \Drupal::service('plugin.manager.search_api.backend');
      $config = $this->backend_config;
      $config['#server'] = $this;
      if (!($this->backendPlugin = $backend_plugin_manager
        ->createInstance($this
        ->getBackendId(), $config))) {
        $backend_id = $this
          ->getBackendId();
        $label = $this
          ->label();
        throw new SearchApiException("The backend with ID '{$backend_id}' could not be retrieved for server '{$label}'.");
      }
    }
    return $this->backendPlugin;
  }

  /**
   * {@inheritdoc}
   */
  public function getBackendConfig() {
    return $this->backend_config;
  }

  /**
   * {@inheritdoc}
   */
  public function setBackendConfig(array $backend_config) {
    $this->backend_config = $backend_config;

    // In case the backend plugin is already loaded, make sure the configuration
    // stays in sync.
    if ($this->backendPlugin && $this
      ->getBackend()
      ->getConfiguration() !== $backend_config) {
      $this
        ->getBackend()
        ->setConfiguration($backend_config);
    }
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getIndexes(array $properties = []) {
    $storage = \Drupal::entityTypeManager()
      ->getStorage('search_api_index');
    return $storage
      ->loadByProperties([
      'server' => $this
        ->id(),
    ] + $properties);
  }

  /**
   * {@inheritdoc}
   */
  public function viewSettings() {
    return $this
      ->hasValidBackend() ? $this
      ->getBackend()
      ->viewSettings() : [];
  }

  /**
   * {@inheritdoc}
   */
  public function isAvailable() {
    return $this
      ->hasValidBackend() ? $this
      ->getBackend()
      ->isAvailable() : FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function supportsFeature($feature) {
    return in_array($feature, $this
      ->getSupportedFeatures());
  }

  /**
   * {@inheritdoc}
   */
  public function getSupportedFeatures() {
    if (!isset($this->features)) {
      $this->features = [];
      if ($this
        ->hasValidBackend()) {
        $this->features = $this
          ->getBackend()
          ->getSupportedFeatures();
      }
      $description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.determining_server_features" event instead. See https://www.drupal.org/node/3059866';
      \Drupal::moduleHandler()
        ->alterDeprecated($description, 'search_api_server_features', $this->features, $this);

      /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher */
      $eventDispatcher = \Drupal::getContainer()
        ->get('event_dispatcher');
      $eventDispatcher
        ->dispatch(SearchApiEvents::DETERMINING_SERVER_FEATURES, new DeterminingServerFeaturesEvent($this->features, $this));
    }
    return $this->features;
  }

  /**
   * {@inheritdoc}
   */
  public function supportsDataType($type) {
    if ($this
      ->hasValidBackend()) {
      return $this
        ->getBackend()
        ->supportsDataType($type);
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function getDiscouragedProcessors() {
    if ($this
      ->hasValidBackend()) {
      return $this
        ->getBackend()
        ->getDiscouragedProcessors();
    }
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function getBackendDefinedFields(IndexInterface $index) {
    if ($this
      ->hasValidBackend()) {
      return $this
        ->getBackend()
        ->getBackendDefinedFields($index);
    }
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function addIndex(IndexInterface $index) {
    $server_task_manager = \Drupal::getContainer()
      ->get('search_api.server_task_manager');

    // When freshly adding an index to a server, it doesn't make any sense to
    // execute possible other tasks for that server/index combination.
    // (removeIndex() is implicit when adding an index which was already added.)
    $server_task_manager
      ->delete($this, $index);
    try {
      if ($server_task_manager
        ->execute($this)) {
        $this
          ->getBackend()
          ->addIndex($index);
        return;
      }
    } catch (SearchApiException $e) {
      $vars = [
        '%server' => $this
          ->label(),
        '%index' => $index
          ->label(),
      ];
      $this
        ->logException($e, '%type while adding index %index to server %server: @message in %function (line %line of %file).', $vars);
    }
    $task_manager = \Drupal::getContainer()
      ->get('search_api.task_manager');
    $task_manager
      ->addTask(__FUNCTION__, $this, $index);
  }

  /**
   * {@inheritdoc}
   */
  public function updateIndex(IndexInterface $index) {
    $server_task_manager = \Drupal::getContainer()
      ->get('search_api.server_task_manager');
    try {
      if ($server_task_manager
        ->execute($this)) {
        $this
          ->getBackend()
          ->updateIndex($index);
        return;
      }
    } catch (SearchApiException $e) {
      $vars = [
        '%server' => $this
          ->label(),
        '%index' => $index
          ->label(),
      ];
      $this
        ->logException($e, '%type while updating the fields of index %index on server %server: @message in %function (line %line of %file).', $vars);
    }
    $task_manager = \Drupal::getContainer()
      ->get('search_api.task_manager');
    $task_manager
      ->addTask(__FUNCTION__, $this, $index, $index->original ?? NULL);
  }

  /**
   * {@inheritdoc}
   */
  public function removeIndex($index) {
    $server_task_manager = \Drupal::getContainer()
      ->get('search_api.server_task_manager');

    // When removing an index from a server, it doesn't make any sense anymore
    // to delete items from it, or react to other changes.
    $server_task_manager
      ->delete($this, $index);
    try {
      if ($server_task_manager
        ->execute($this)) {
        $this
          ->getBackend()
          ->removeIndex($index);
        return;
      }
    } catch (SearchApiException $e) {
      $vars = [
        '%server' => $this
          ->label(),
        '%index' => is_object($index) ? $index
          ->label() : $index,
      ];
      $this
        ->logException($e, '%type while removing index %index from server %server: @message in %function (line %line of %file).', $vars);
    }
    $task_manager = \Drupal::getContainer()
      ->get('search_api.task_manager');
    $data = NULL;
    if (!is_object($index)) {
      $data = $index;
      $index = NULL;
    }
    $task_manager
      ->addTask(__FUNCTION__, $this, $index, $data);
  }

  /**
   * {@inheritdoc}
   */
  public function indexItems(IndexInterface $index, array $items) {
    $server_task_manager = \Drupal::getContainer()
      ->get('search_api.server_task_manager');
    if ($server_task_manager
      ->execute($this)) {
      return $this
        ->getBackend()
        ->indexItems($index, $items);
    }
    $index_label = $index
      ->label();
    throw new SearchApiException("Could not index items on index '{$index_label}' because pending server tasks could not be executed.");
  }

  /**
   * {@inheritdoc}
   */
  public function deleteItems(IndexInterface $index, array $item_ids) {
    if ($index
      ->isReadOnly()) {
      $vars = [
        '%index' => $index
          ->label(),
      ];
      $this
        ->getLogger()
        ->warning('Trying to delete items from index %index which is marked as read-only.', $vars);
      return;
    }
    $server_task_manager = \Drupal::getContainer()
      ->get('search_api.server_task_manager');
    try {
      if ($server_task_manager
        ->execute($this)) {
        $this
          ->getBackend()
          ->deleteItems($index, $item_ids);

        // Clear search api list caches.
        Cache::invalidateTags([
          'search_api_list:' . $index
            ->id(),
        ]);
        return;
      }
    } catch (SearchApiException $e) {
      $vars = [
        '%server' => $this
          ->label(),
      ];
      $this
        ->logException($e, '%type while deleting items from server %server: @message in %function (line %line of %file).', $vars);
    }
    $task_manager = \Drupal::getContainer()
      ->get('search_api.task_manager');
    $task_manager
      ->addTask(__FUNCTION__, $this, $index, $item_ids);
  }

  /**
   * {@inheritdoc}
   */
  public function deleteAllIndexItems(IndexInterface $index, $datasource_id = NULL) {
    if ($index
      ->isReadOnly()) {
      $vars = [
        '%index' => $index
          ->label(),
      ];
      $this
        ->getLogger()
        ->warning('Trying to delete items from index %index which is marked as read-only.', $vars);
      return;
    }
    $server_task_manager = \Drupal::getContainer()
      ->get('search_api.server_task_manager');
    if (!$datasource_id) {

      // If we're deleting all items of the index, there's no point in keeping
      // any other "delete items" tasks.
      $types = [
        'deleteItems',
        'deleteAllIndexItems',
      ];
      $server_task_manager
        ->delete($this, $index, $types);
    }
    try {
      if ($server_task_manager
        ->execute($this)) {
        $this
          ->getBackend()
          ->deleteAllIndexItems($index, $datasource_id);

        // Clear search api list caches.
        Cache::invalidateTags([
          'search_api_list:' . $index
            ->id(),
        ]);
        return;
      }
    } catch (SearchApiException $e) {
      $vars = [
        '%server' => $this
          ->label(),
        '%index' => $index
          ->label(),
      ];
      $this
        ->logException($e, '%type while deleting items of index %index from server %server: @message in %function (line %line of %file).', $vars);
    }
    $task_manager = \Drupal::getContainer()
      ->get('search_api.task_manager');
    $task_manager
      ->addTask(__FUNCTION__, $this, $index, $datasource_id);
  }

  /**
   * {@inheritdoc}
   */
  public function deleteAllItems() {
    $failed = [];
    $properties['status'] = TRUE;
    $properties['read_only'] = FALSE;
    foreach ($this
      ->getIndexes($properties) as $index) {
      try {
        $this
          ->getBackend()
          ->deleteAllIndexItems($index);
        Cache::invalidateTags([
          'search_api_list:' . $index
            ->id(),
        ]);
      } catch (SearchApiException $e) {
        $args = [
          '%index' => $index
            ->label(),
        ];
        $this
          ->logException($e, '%type while deleting all items from index %index: @message in %function (line %line of %file).', $args);
        $failed[] = $index
          ->label();
      }
    }
    if (!empty($e)) {
      $server_name = $this
        ->label();
      $failed = implode(', ', $failed);
      throw new SearchApiException("Deleting all items from server '{$server_name}' failed for the following (write-enabled) indexes: {$failed}.", 0, $e);
    }
    $types = [
      'deleteItems',
      'deleteAllIndexItems',
    ];
    \Drupal::getContainer()
      ->get('search_api.server_task_manager')
      ->delete($this, NULL, $types);
  }

  /**
   * {@inheritdoc}
   */
  public function search(QueryInterface $query) {
    $this
      ->getBackend()
      ->search($query);
  }

  /**
   * {@inheritdoc}
   */
  public function preSave(EntityStorageInterface $storage) {
    parent::preSave($storage);

    // The rest of the code only applies to updates.
    if (!isset($this->original)) {
      return;
    }

    // Retrieve active config overrides for this server.
    $overrides = Utility::getConfigOverrides($this);

    // If there are overrides for the backend or its configuration, attempt to
    // apply them for the preUpdate() call.
    if (isset($overrides['backend']) || isset($overrides['backend_config'])) {
      $backend_config = $this
        ->getBackendConfig();
      if (isset($overrides['backend_config'])) {
        $backend_config = $overrides['backend_config'];
      }
      $backend_id = $this
        ->getBackendId();
      if (isset($overrides['backend'])) {
        $backend_id = $overrides['backend'];
      }
      $backend_plugin_manager = \Drupal::service('plugin.manager.search_api.backend');
      $backend_config['#server'] = $this;
      if (!($backend = $backend_plugin_manager
        ->createInstance($backend_id, $backend_config))) {
        $label = $this
          ->label();
        throw new SearchApiException("The backend with ID '{$backend_id}' could not be retrieved for server '{$label}'.");
      }
    }
    else {
      $backend = $this
        ->getBackend();
    }
    $backend
      ->preUpdate();

    // If the server is being disabled, also disable all its indexes.
    if (!$this
      ->isSyncing() && !$this
      ->isInstallingFromExtension() && !isset($overrides['status']) && !$this
      ->status() && $this->original
      ->status()) {
      foreach ($this
        ->getIndexes([
        'status' => TRUE,
      ]) as $index) {

        /** @var \Drupal\search_api\IndexInterface $index */
        $index
          ->setStatus(FALSE)
          ->save();
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
    if ($this
      ->hasValidBackend()) {
      if ($update) {
        $reindexing_necessary = $this
          ->getBackend()
          ->postUpdate();
        if ($reindexing_necessary) {
          foreach ($this
            ->getIndexes() as $index) {
            $index
              ->reindex();
          }
        }
      }
      else {
        $this
          ->getBackend()
          ->postInsert();
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function preDelete(EntityStorageInterface $storage, array $entities) {

    // @todo This will, via Index::onDependencyRemoval(), remove all indexes
    //   from this server, triggering the server's removeIndex() method. This
    //   is, at best, wasted performance and could at worst lead to a bug if
    //   removeIndex() saves the server. We should try what happens when this is
    //   the case, whether there really is a bug, and try to fix it somehow –
    //   maybe clever detection of this case in removeIndex() or
    //   Index::postSave(). $server->isUninstalling() might help?
    parent::preDelete($storage, $entities);

    // Iterate through the servers, executing the backends' preDelete() methods
    // and removing all their pending server tasks.
    foreach ($entities as $server) {

      /** @var \Drupal\search_api\ServerInterface $server */
      if ($server
        ->hasValidBackend()) {
        $server
          ->getBackend()
          ->preDelete();
      }
      \Drupal::getContainer()
        ->get('search_api.server_task_manager')
        ->delete($server);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function calculateDependencies() {
    parent::calculateDependencies();

    // Add the backend's dependencies.
    if ($this
      ->hasValidBackend()) {
      $this
        ->calculatePluginDependencies($this
        ->getBackend());
    }
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function onDependencyRemoval(array $dependencies) {
    $changed = parent::onDependencyRemoval($dependencies);
    if ($this
      ->hasValidBackend()) {
      $removed_backend_dependencies = [];
      $backend = $this
        ->getBackend();
      foreach ($backend
        ->calculateDependencies() as $dependency_type => $list) {
        if (isset($dependencies[$dependency_type])) {
          $removed_backend_dependencies[$dependency_type] = array_intersect_key($dependencies[$dependency_type], array_flip($list));
        }
      }
      $removed_backend_dependencies = array_filter($removed_backend_dependencies);
      if ($removed_backend_dependencies) {
        if ($backend
          ->onDependencyRemoval($removed_backend_dependencies)) {
          $this->backend_config = $backend
            ->getConfiguration();
          $changed = TRUE;
        }
      }
    }
    return $changed;
  }

  /**
   * Implements the magic __clone() method.
   *
   * Prevents the backend plugin instance from being cloned.
   */
  public function __clone() {
    $this->backendPlugin = NULL;
  }

}

Classes

Namesort descending Description
Server Defines the search server configuration entity.