cached_moderation_state-1.0.0-alpha2/src/BatchUpdateHandler.php
src/BatchUpdateHandler.php
<?php
namespace Drupal\cached_moderation_state;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\RevisionableStorageInterface;
use Drupal\Core\Entity\SynchronizableInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Utility\Error;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Facilitates batch updates to 'cached_moderation_state' for existing content.
*
* Copyright (C) 2022 Library Solutions, LLC (et al.).
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*/
class BatchUpdateHandler implements BatchUpdateHandlerInterface, ContainerInjectionInterface {
use DependencySerializationTrait;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Tracks whether an entity update caused by this service is in progress.
*
* This flag cannot be an instance property because of an unexpected issue
* that occurs when accessing it while running via Drush; the property would
* seem to always be FALSE from the perspective of `hook_entity_presave()`,
* even after being set to TRUE before updating an entity.
*
* @var bool
*/
protected static $entityUpdateInProgress = FALSE;
/**
* The logger for this class.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger = NULL;
/**
* The logger factory service.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/**
* Constructs a BatchUpdateHandler object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface|null $logger_factory
* The logger factory service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ?LoggerChannelFactoryInterface $logger_factory = NULL) {
if (!isset($logger_factory)) {
@\trigger_error('Calling ' . __METHOD__ . '() without the $logger_factory argument is deprecated in cached_moderation_state:1.1.0 and will be required in cached_moderation_state:2.0.0. See https://www.drupal.org/node/3431365', \E_USER_DEPRECATED);
/**
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
// @phpcs:disable DrupalPractice.Objects.GlobalDrupal.GlobalDrupal
// @phpstan-ignore-next-line
$logger_factory = \Drupal::service('logger.factory');
// @phpcs:enable
}
$this->entityTypeManager = $entity_type_manager;
$this->loggerFactory = $logger_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('logger.factory'),
);
}
/**
* {@inheritdoc}
*/
public function getEntitiesForEntityType(string $entity_type_id, array $bundles = [], bool $only_uninitialized = FALSE) {
$results = [];
try {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$query = $storage->getQuery();
$query->accessCheck(FALSE);
// If the entity type is bundleable, filter by the supplied bundles.
if (!empty($bundles) && $bundle_key = $entity_type->getKey('bundle')) {
$query->condition($bundle_key, $bundles, 'IN');
}
// If the entity type is revisionable, query all entity revisions.
if ($entity_type->isRevisionable()) {
// The query results will now represent revision identifiers instead of
// plain entity identifiers. A companion condition in ::loadEntities()
// will ensure that the appropriate method is used to load the
// corresponding entities.
$query->allRevisions();
}
// Filter the result set to only contain results with uninitialized
// 'cached_moderation_state' fields if desired.
if ($only_uninitialized) {
$query->notExists('cached_moderation_state.updated');
}
$results = $query->execute();
// The entity query will produce array elements whose value represents a
// plain entity identifier, and whose key represents a revision
// identifier. The next line extracts the appropriate identifiers.
$results = $entity_type->isRevisionable() ? \array_keys($results) : \array_values($results);
// Prefix all of the resulting identifiers with their entity type.
$results = \array_map(fn ($id) => "{$entity_type_id}:{$id}", $results);
\sort($results, \SORT_NATURAL);
}
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
}
return $results;
}
/**
* {@inheritdoc}
*/
public function isEntityUpdateInProgress(): bool {
return self::$entityUpdateInProgress;
}
/**
* {@inheritdoc}
*/
public function loadEntities(array $entity_ids) {
$entities_by_entity_type_id = [];
foreach ($entity_ids as $entity_id) {
// Ignore invalid entity identifiers.
if (!\is_string($entity_id) || \strpos($entity_id, ':') === FALSE) {
continue;
}
// Extract the entity type and entity identifier components.
[$entity_type_id, $entity_id] = \explode(':', $entity_id, 2);
// Group the entity identifier by its entity type.
$entities_by_entity_type_id[$entity_type_id][$entity_id] = $entity_id;
}
$results = [];
foreach ($entities_by_entity_type_id as $entity_type_id => $entity_ids) {
try {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$storage = $this->entityTypeManager->getStorage($entity_type_id);
if ($entity_type->isRevisionable() && $storage instanceof RevisionableStorageInterface) {
// We're working with revision identifiers. Load the entities by their
// revision identifiers.
$entities = $storage->loadMultipleRevisions($entity_ids);
}
else {
// Load the entities by their regular identifiers.
$entities = $storage->loadMultiple($entity_ids);
}
foreach ($entities as $entity) {
// Add this entity to the resulting list of loaded entities.
$results[] = $entity;
}
}
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
}
}
return $results;
}
/**
* Get the logger for this class.
*
* @return \Drupal\Core\Logger\LoggerChannelInterface
* The logger for this class.
*/
protected function logger(): LoggerChannelInterface {
if (!isset($this->logger)) {
$this->logger = $this->loggerFactory->get('cached_moderation_state');
}
return $this->logger;
}
/**
* {@inheritdoc}
*/
public function updateEntities(array &$entity_ids, int $batch_size = 0): int {
if ($batch_size < 0) {
throw new \InvalidArgumentException('$batch_size is invalid');
}
// Batch the supplied list of entity identifiers.
$entity_ids_batch = $batch_size ? \array_slice($entity_ids, 0, $batch_size) : $entity_ids;
// Attempt to load the batch of entities, then advance the full entity list.
$entities = $this->loadEntities($entity_ids_batch);
$entity_ids = $batch_size ? \array_slice($entity_ids, $batch_size) : [];
// Process the loaded batch of entities.
foreach ($entities as $entity) {
$this->updateEntity($entity);
}
return \count($entity_ids_batch);
}
/**
* {@inheritdoc}
*/
public function updateEntity(EntityInterface $entity): void {
try {
self::$entityUpdateInProgress = TRUE;
// Update the entity's synchronization flag if applicable.
//
// Setting this flag to TRUE may prevent some entity CRUD hook
// implementations from running, which is ideal to avoid producing
// potentially unwanted side effects.
if ($entity instanceof SynchronizableInterface) {
$entity->setSyncing(TRUE);
}
$entity->save();
}
catch (\Exception $e) {
Error::logException($this->logger(), $e);
}
finally {
self::$entityUpdateInProgress = FALSE;
if ($entity instanceof SynchronizableInterface) {
$entity->setSyncing(FALSE);
}
}
}
}
