image_to_media_swapper-2.x-dev/src/MediaSwapFormService.php
src/MediaSwapFormService.php
<?php
declare(strict_types=1);
namespace Drupal\image_to_media_swapper;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\image_to_media_swapper\Entity\MediaSwapRecordInterface;
/**
* Service for shared functionality between media swap forms.
*/
class MediaSwapFormService {
use StringTranslationTrait;
use LoggerChannelTrait;
/**
* Constructs a MediaSwapFormService object.
*
* @param \Drupal\image_to_media_swapper\BatchProcessorService $batchProcessorService
* The batch processor service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser
* The current user service.
* @param \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter
* The date formatter service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
*/
public function __construct(
protected BatchProcessorService $batchProcessorService,
protected EntityTypeManagerInterface $entityTypeManager,
protected AccountProxyInterface $currentUser,
protected DateFormatterInterface $dateFormatter,
protected TimeInterface $time,
) {}
/**
* Gets text fields that contain image tags.
*
* @return array
* An associative array of field keys and their labels with image counts.
*/
public function getFieldsWithImages(): array {
$options = [];
$allFields = $this->batchProcessorService->getEligibleTextFields();
foreach ($allFields as $fieldKey => $fieldLabel) {
$entities = $this->batchProcessorService->getEntitiesWithFiles($fieldKey, ['images']);
if (!empty($entities)) {
$count = count($entities);
$options[$fieldKey] = $fieldLabel . ' (' . $count . ' entities with images)';
}
}
return $options;
}
/**
* Gets text fields that contain file links.
*
* @return array
* An associative array of field keys and their labels with link counts.
*/
public function getFieldsWithFileLinks(): array {
$options = [];
$allFields = $this->batchProcessorService->getEligibleTextFields();
foreach ($allFields as $fieldKey => $fieldLabel) {
$entities = $this->batchProcessorService->getEntitiesWithFiles($fieldKey, ['links']);
if (!empty($entities)) {
$count = count($entities);
$options[$fieldKey] = $fieldLabel . ' (' . $count . ' entities with file links)';
}
}
return $options;
}
/**
* Gets the count of pending items.
*
* @return int
* The number of pending items.
*/
public function getPendingItemsCount(): int {
$query = $this->entityTypeManager->getStorage('media_swap_record')
->getQuery()
->accessCheck()
->condition('processing_status', 'pending')
->count();
return $query->execute();
}
/**
* Creates pending MediaSwapRecords for entities with a specific field.
*
* @param string $fieldSelector
* The field selector in the format 'entity_type.bundle.field_name'.
* @param string $category
* The category ('images', 'links', or 'mixed').
*
* @return int
* The number of records created.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function createPendingRecordsForField(string $fieldSelector, string $category): int {
$fieldSelectorParts = explode('.', $fieldSelector);
if (count($fieldSelectorParts) !== 3) {
throw new \InvalidArgumentException('Field selector must be in the format "entity_type.bundle.field_name".');
}
$entityTypeId = $fieldSelectorParts[0];
$bundle = $fieldSelectorParts[1];
// Get entities that have content to process.
$entities = $this->batchProcessorService->getEntitiesWithFiles($fieldSelector, [$category]);
if (empty($entities)) {
return 0;
}
$count = 0;
try {
$swapRecordStorage = $this->entityTypeManager->getStorage('media_swap_record');
// Check for existing pending records to avoid duplicates.
$existingRecords = $swapRecordStorage->getQuery()
->condition('field_selector', $fieldSelector)
->condition('processing_status', 'pending')
->accessCheck()
->execute();
$existingEntityIds = [];
if (!empty($existingRecords)) {
/** @var \Drupal\image_to_media_swapper\Entity\MediaSwapRecordInterface[] $existingEntities */
$existingEntities = $swapRecordStorage->loadMultiple($existingRecords);
foreach ($existingEntities as $record) {
$existingEntityIds[$record->getTargetEntityId()] = TRUE;
}
}
// Create pending records for each entity.
foreach ($entities as $entity) {
// Skip if there's already a pending record for this entity and field.
if (isset($existingEntityIds[$entity->id()])) {
continue;
}
/** @var \Drupal\image_to_media_swapper\Entity\MediaSwapRecordInterface $swapRecord */
$swapRecord = $swapRecordStorage->create([
'field_selector' => $fieldSelector,
'target_entity_type' => $entityTypeId,
'target_bundle' => $bundle,
'target_entity_id' => $entity->id(),
'batch_category' => $category,
'processing_status' => 'pending',
'processed_time' => $this->dateFormatter->format($this->time
->getRequestTime(), 'custom', 'U'),
'uid' => $this->currentUser->id(),
]);
$swapRecord->save();
$count++;
}
}
catch (InvalidPluginDefinitionException | PluginNotFoundException $e) {
$this->loggerFactory->get('media_swap_record_service')
->error($e->getMessage());
}
return $count;
}
/**
* Gets all pending records.
*
* @return \Drupal\image_to_media_swapper\Entity\MediaSwapRecordInterface[]
* An array of pending MediaSwapRecord entities.
*/
public function getPendingRecords(): array {
try {
$query = $this->entityTypeManager->getStorage('media_swap_record')
->getQuery()
->accessCheck()
->condition('processing_status', 'pending')
->sort('id', 'ASC');
$results = $query->execute();
}
catch (InvalidPluginDefinitionException | PluginNotFoundException $e) {
$this->loggerFactory->get('media_swap_record_service')
->error($e->getMessage());
}
if (empty($results)) {
return [];
}
try {
return $this->entityTypeManager->getStorage('media_swap_record')
->loadMultiple($results);
}
catch (InvalidPluginDefinitionException | PluginNotFoundException $e) {
$this->loggerFactory->get('media_swap_record_service')
->error($e->getMessage());
}
return [];
}
/**
* Rechecks processed items to update their status.
*
* @return int
* The number of items rechecked.
*/
public function recheckProcessedItems(): int {
// This will check the status of pending records and mark them as completed
// if they've been manually processed.
$count = 0;
$pendingRecords = $this->getPendingRecords();
foreach ($pendingRecords as $record) {
// Get the target entity.
try {
$entityTypeId = $record->getTargetEntityType();
$entityId = $record->getTargetEntityId();
$entity = $this->entityTypeManager->getStorage($entityTypeId)
->load($entityId);
if (!$entity) {
// Entity doesn't exist anymore.
$record->setProcessingStatus('failed');
$record->setErrorMessage('Target entity no longer exists.');
$record->save();
$count++;
continue;
}
// Get the field selector parts.
$needsProcessing = $this->isProcessing($record, $entity);
// If it doesn't need processing anymore, mark as completed.
if (!$needsProcessing) {
$record->setProcessingStatus('completed');
$record->save();
$count++;
}
}
catch (\Exception $e) {
$this->loggerFactory->get('image_to_media_swapper')
->error('Error rechecking media swap record @id: @message', [
'@id' => $record->id(),
'@message' => $e->getMessage(),
]);
}
}
return $count;
}
/**
* Rechecks a single media swap record.
*
* @param \Drupal\image_to_media_swapper\Entity\MediaSwapRecordInterface $media_swap_record
* The ID of the MediaSwapRecord to recheck.
*
* @return bool
* TRUE if the record was successfully rechecked, FALSE otherwise.
*/
public function recheckSingleRecord(MediaSwapRecordInterface $media_swap_record): bool {
try {
// Only recheck pending records.
if ($media_swap_record->getProcessingStatus() !== 'pending') {
return FALSE;
}
// Get the target entity.
$entityTypeId = $media_swap_record->getTargetEntityType();
$entityId = $media_swap_record->getTargetEntityId();
$entity = $this->entityTypeManager->getStorage($entityTypeId)
->load($entityId);
if (!$entity) {
// Entity doesn't exist anymore.
$media_swap_record->setProcessingStatus('failed');
$media_swap_record->setErrorMessage('Target entity no longer exists.');
$media_swap_record->save();
return TRUE;
}
// Get the field selector parts.
$needsProcessing = $this->isProcessing($media_swap_record, $entity);
// If it doesn't need processing anymore, mark as completed.
if (!$needsProcessing) {
$media_swap_record->setProcessingStatus('completed');
$media_swap_record->save();
}
return TRUE;
}
catch (\Exception $e) {
$this->loggerFactory->get('image_to_media_swapper')
->error('Error rechecking media swap record @id: @message', [
'@id' => $media_swap_record,
'@message' => $e->getMessage(),
]);
return FALSE;
}
}
/**
* Determines if the entity still needs processing based on its content.
*
* @param \Drupal\image_to_media_swapper\Entity\MediaSwapRecordInterface $media_swap_record
* The media swap record.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The target entity.
*
* @return bool
* TRUE if the entity still needs processing, FALSE otherwise.
*/
protected function isProcessing(MediaSwapRecordInterface $media_swap_record, EntityInterface $entity): bool {
$fieldSelector = $media_swap_record->getFieldSelector();
$fieldSelectorParts = explode('.', $fieldSelector);
$fieldName = $fieldSelectorParts[2];
// Check if the entity still has content that needs processing.
$category = $media_swap_record->getBatchCategory();
$richText = $entity->get($fieldName)->value ?? '';
$needsProcessing = FALSE;
if ($category === 'images' && str_contains($richText, '<img')) {
$needsProcessing = TRUE;
}
elseif ($category === 'links' && $this->batchProcessorService->containsFileLinks($richText)) {
$needsProcessing = TRUE;
}
elseif ($category === 'mixed' && (str_contains($richText, '<img') || $this->batchProcessorService->containsFileLinks($richText))) {
$needsProcessing = TRUE;
}
return $needsProcessing;
}
}
