diff --git a/drush.services.yml b/drush.services.yml new file mode 100644 index 000000000..e1e332e07 --- /dev/null +++ b/drush.services.yml @@ -0,0 +1,16 @@ +services: + delete_orphaned_paragraphs_test.commands: + class: Drupal\tide_core\Commands\TideDeleteOrphanedParagraphsTest + arguments: ['@keyvalue'] + tags: + - { name: drush.command } + tide_core.batch_command: + class: \Drupal\tide_core\Commands\TideParagraphRevisionsCleanUp + tags: + - { name: drush.command } + arguments: + - '@entity_type.manager' + - '@logger.factory' + - '@tide_core.batch' + + diff --git a/src/Batch/BatchService.php b/src/Batch/BatchService.php new file mode 100644 index 000000000..cd3e3c380 --- /dev/null +++ b/src/Batch/BatchService.php @@ -0,0 +1,187 @@ +entityTypeManager = $entityTypeManager; + $this->loggerChannel = $loggerFactory->get('tide_core'); + } + + /** + * {@inheritdoc} + */ + public function create(int $batchSize = 100, array $data = []): void { + if (empty($data)) { + $this->loggerChannel->notice('There is no data to process.'); + } + else { + $batch = new BatchBuilder(); + $batch->setTitle($this->t('Running batch process.')) + ->setFinishCallback([self::class, 'batchFinished']) + ->setInitMessage('Commencing') + ->setProgressMessage('Processing...') + ->setErrorMessage('An error occurred during processing.'); + + // Create chunks of all items. + $chunks = array_chunk($data, $batchSize); + + // Process each chunk in the array. + foreach ($chunks as $id => $chunk) { + $args = [ + $id, + $chunk, + ]; + $batch->addOperation([BatchService::class, 'batchProcess'], $args); + } + batch_set($batch->toArray()); + + $this->loggerChannel->notice('Batch created.'); + drush_backend_batch_process(); + + // Finish. + $this->loggerChannel->notice('Batch operations end.'); + } + } + + /** + * {@inheritdoc} + */ + public static function batchProcess(int $batchId, array $chunk, array &$context): void { + if (!isset($context['sandbox']['progress'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['max'] = 1000; + } + if (!isset($context['results']['updated'])) { + $context['results']['updated'] = 0; + $context['results']['skipped'] = 0; + $context['results']['failed'] = 0; + $context['results']['progress'] = 0; + $context['results']['process'] = 'Batch processing completed'; + } + + // Keep track of progress. + $context['results']['progress'] += count($chunk); + + // Message above progress bar. + $context['message'] = t('Processing batch #@batch_id, batch size @batch_size for total @count items.', [ + '@batch_id' => number_format($batchId), + '@batch_size' => number_format(count($chunk)), + '@count' => number_format($context['sandbox']['max']), + ]); + + foreach ($chunk as $dataProcessed) { + $result = self::cleanRevisions($dataProcessed['revision_list']); + switch ($result) { + case 1: + $context['results']['updated']++; + break; + + case 0: + $context['results']['skipped']++; + break; + } + } + } + + /** + * {@inheritdoc} + */ + public static function batchFinished(bool $success, array $results, array $operations, string $elapsed): void { + // Grab the messenger service, this will be needed if the batch was a + // success or a failure. + $messenger = \Drupal::messenger(); + if ($success) { + // The success variable was true, which indicates that the batch process + // was successful (i.e. no errors occurred). + // Show success message to the user. + $messenger->addMessage(t('@process processed @count, skipped @skipped, updated @updated, failed @failed in @elapsed.', [ + '@process' => $results['process'], + '@count' => $results['progress'], + '@skipped' => $results['skipped'], + '@updated' => $results['updated'], + '@failed' => $results['failed'], + '@elapsed' => $elapsed, + ])); + // Log the batch success. + \Drupal::logger('batch_form_example')->info( + '@process processed @count, skipped @skipped, updated @updated, failed @failed in @elapsed.', + [ + '@process' => $results['process'], + '@count' => $results['progress'], + '@skipped' => $results['skipped'], + '@updated' => $results['updated'], + '@failed' => $results['failed'], + '@elapsed' => $elapsed, + ] + ); + } else { + // An error occurred. $operations contains the operations that remained + // unprocessed. Pick the last operation and report on what happened. + $error_operation = reset($operations); + if ($error_operation) { + $message = t('An error occurred while processing %error_operation with arguments: @arguments', [ + '%error_operation' => print_r($error_operation[0]), + '@arguments' => print_r($error_operation[1], TRUE), + ]); + $messenger->addError($message); + } + } + } + + /** + * {@inheritdoc} + */ + public static function cleanRevisions(array $revisionIds): int { + $storage = \Drupal::entityTypeManager()->getStorage('node'); + /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */ + if (!empty($revisionIds)) { + if (count($revisionIds)) { + return 2; + } + + foreach ($revisionIds as $revisionId) { + $storage->deleteRevision($revisionId); + } + + return 1; + } + else { + return 0; + } + } +} diff --git a/src/Batch/BatchServiceInterface.php b/src/Batch/BatchServiceInterface.php new file mode 100644 index 000000000..425f3b8e9 --- /dev/null +++ b/src/Batch/BatchServiceInterface.php @@ -0,0 +1,53 @@ +keyValue = $keyValue; + } + + /** + * Delete orphaned paragraphs. + * + * @param int $limit + * The maximum number of paragraphs to process. + * + * @param string $number_days_ago + * The number of days to purge the data. + * @command tide_core:delete-orphaned-paragraphs-test + * @aliases dop + * @usage tide_core:delete-orphaned-paragraphs-test 5 + */ + public function deleteOrphanedParagraphsTest($limit = 5, $number_days_ago = '-90 days') + { + $kv = $this->keyValue->get('delete-orphaned-paragraphs-test'); + $last_checked_p_id = $kv->get('last_checked_p_id', 0); + $thirty_days_ago = strtotime($number_days_ago); + $database = \Drupal::database() + ->select('paragraphs_item_field_data', 'p') + ->fields('p', ['id']) + ->where("p.id > :last_checked_p_id", [':last_checked_p_id' => $last_checked_p_id]) + ->where("p.created < :thirty_days_ago", [':thirty_days_ago' => $thirty_days_ago]) + ->orderBy("p.id") + ->range(0, $limit); + $results = $database->execute(); + $paragraph_ids = $results->fetchCol(); + if (empty($paragraph_ids)) { + $this->output()->writeln('No paragraphs found.'); + return; + } + + foreach ($paragraph_ids as $paragraph_id) { + $paragraph_entity = Paragraph::load($paragraph_id); + if ($paragraph_entity instanceof Paragraph && $paragraph_entity->getParentEntity() === NULL) { + $paragraph_entity->delete(); + $this->output()->writeln('Paragraphs deleted.'); + } + } + $kv->set('last_checked_p_id', max($paragraph_ids)); + } +} diff --git a/src/Commands/TideParagraphRevisionsCleanUp.php b/src/Commands/TideParagraphRevisionsCleanUp.php new file mode 100644 index 000000000..9f590d560 --- /dev/null +++ b/src/Commands/TideParagraphRevisionsCleanUp.php @@ -0,0 +1,113 @@ +entityTypeManager = $entityTypeManager; + $this->loggerChannel = $loggerFactory->get('tide_core'); + $this->batch = $batch; + } + + /** + * Get node revisions per node. + * + * @return array + * A list of nodes with their corresponding node revisions. + */ + public function getNodeRevisions(string $timeAgo) { + $nodeRevisions = []; + $storage = $this->entityTypeManager->getStorage('node'); + + try { + $query = $storage->getQuery() + ->condition('status', '1') + ->condition('created', $timeAgo, '<') + ->range(0, 500) + ->accessCheck(FALSE); + $nids = $query->execute(); + + if (!empty($nids)) { + foreach($nids as $vid => $nid) { + $node = \Drupal::entityTypeManager()->getStorage('node')->load($nid); + $revisionIds = $storage->revisionIds($node); + $latestRevisionId = $storage->getLatestRevisionId($nid); + $nodeRevisions[$nid] = [ + 'revision_list' => $revisionIds, + 'current_revision' => $latestRevisionId, + ]; + } + } + } catch (\Exception $e) { + $this->output()->writeln($e); + $this->loggerChannel->warning('Error found @e', ['@e' => $e]); + } + + return $nodeRevisions; + } + + /** + * Delete paragraphs revisions. + * + * @command tide_core:paragraph-revisions-cleanup + * @aliases dpr + * + * @usage tide_core:paragraph-revisions-cleanup --batch=150 --older='30 days' + */ + public function deleteParagraphRevision(array $options = ['batch' => 10, 'older' => '30 days']) { + $timeAgo = strtotime('-' . $options['older']); + $data = $this->getNodeRevisions($timeAgo); + if (!empty($data)) { + foreach ($data as $key => $value) { + foreach($value['revision_list'] as $krid => $rid) { + if ($value['current_revision'] == $rid) { + unset($data[$key]['revision_list'][$krid]); + } + } + unset($data[$key]['current_revision']); + } + } + + $this->batch->create($options['batch'], $data); + } +} diff --git a/tide_core.services.yml b/tide_core.services.yml index c332995ba..6923ec32d 100644 --- a/tide_core.services.yml +++ b/tide_core.services.yml @@ -35,3 +35,8 @@ services: - '@logger.factory' - '@file_system' - '@monitoring.sensor_runner' + tide_core.batch: + class: 'Drupal\tide_core\Batch\BatchService' + arguments: + - '@entity_type.manager' + - '@logger.factory'