Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UHF-7324: Support deleting old revisions #133

Merged
merged 7 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config/schema/helfi_api_base.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,11 @@ helfi_api_base.environment_resolver.settings:
project_name:
type: string
label: 'The currently active project name'

helfi_api_base.delete_revisions:
type: config_entity
mapping:
entity_types:
type: sequence
sequence:
type: string
8 changes: 8 additions & 0 deletions drush.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,11 @@ services:
arguments: ['@language_manager', '@file_system', '@string_translation', '@extension.list.module']
tags:
- { name: drush.command }
helfi_api_base.revision_commands:
class: \Drupal\helfi_api_base\Commands\RevisionCommands
arguments:
- '@helfi_api_base.revision_manager'
- '@entity_type.manager'
- '@database'
tags:
- { name: drush.command }
23 changes: 23 additions & 0 deletions helfi_api_base.module
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

declare(strict_types = 1);

use Drupal\Core\Entity\EntityInterface;
use Drupal\helfi_api_base\Link\LinkProcessor;

/**
Expand Down Expand Up @@ -65,3 +66,25 @@ function helfi_api_base_mail_alter(&$message) : void {
catch (\InvalidArgumentException) {
}
}

/**
* Implements hook_entity_update().
*/
function helfi_api_base_entity_update(EntityInterface $entity) : void {
/** @var \Drupal\helfi_api_base\Entity\Revision\RevisionManager $revisionManager */
$revisionManager = \Drupal::service('helfi_api_base.revision_manager');

if (!$revisionManager->entityTypeIsSupported($entity->getEntityTypeId())) {
return;
}
$revisions = $revisionManager->getRevisions($entity->getEntityTypeId(), $entity->id());

// Queue entity revisions for deletion.
if ($revisions) {
$queue = \Drupal::queue('helfi_api_base_revision');
$queue->createItem([
'entity_id' => $entity->id(),
'entity_type' => $entity->getEntityTypeId(),
]);
}
}
7 changes: 7 additions & 0 deletions helfi_api_base.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,10 @@ services:
- '@cache_tags.invalidator'
tags:
- { name: event_subscriber }

helfi_api_base.revision_manager:
class: Drupal\helfi_api_base\Entity\Revision\RevisionManager
arguments:
- '@entity_type.manager'
- '@config.factory'
- '@database'
81 changes: 81 additions & 0 deletions src/Commands/RevisionCommands.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Drupal\helfi_api_base\Commands;

use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\helfi_api_base\Entity\Revision\RevisionManager;
use Drush\Attributes\Command;
use Drush\Attributes\Option;
use Drush\Commands\DrushCommands;

/**
* A drush command file to manage revisions.
*/
final class RevisionCommands extends DrushCommands {

/**
* Constructs a new instance.
*/
public function __construct(
private readonly RevisionManager $revisionManager,
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly Connection $connection,
) {
}

/**
* Deletes the old revisions.
*
* @param string $entityType
* The entity type.
* @param array $options
* The options.
*
* @return int
* The exit code.
*/
#[Command(name: 'helfi:revision:delete')]
#[Option(name: 'keep', description: 'Number of revisions to keep')]
public function delete(string $entityType, array $options = ['keep' => RevisionManager::KEEP_REVISIONS]) : int {
if (!$this->revisionManager->entityTypeIsSupported($entityType)) {
$this->io()->writeln('Given entity type is not supported.');
return DrushCommands::EXIT_SUCCESS;
}

$definition = $this->entityTypeManager->getDefinition($entityType);
$entityIds = $this->connection->select($definition->getBaseTable(), 't')
->fields('t', [$definition->getKey('id')])
->execute()
->fetchCol();

$totalEntities = $remainingEntities = count($entityIds);
$this->io()->writeln(new FormattableMarkup('Found @count @type entities', [
'@count' => $totalEntities,
'@type' => $entityType,
]));

foreach ($entityIds as $id) {
$revisions = $this->revisionManager->getRevisions($entityType, $id, $options['keep']);
$revisionCount = count($revisions);

$message = sprintf('Entity has less than %s revisions. Skipping', $options['keep']);

if ($revisionCount > 0) {
$message = new FormattableMarkup('Deleting @count revisions', ['@count' => $revisionCount]);
}
$this->io()->writeln(new FormattableMarkup('[@current/@entities] @message ...', [
'@current' => $remainingEntities--,
'@entities' => $totalEntities,
'@message' => $message,
]));
$this->revisionManager->deleteRevisions($entityType, $revisions);
}

return DrushCommands::EXIT_SUCCESS;
}

}
188 changes: 188 additions & 0 deletions src/Entity/Revision/RevisionManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?php

declare(strict_types = 1);

namespace Drupal\helfi_api_base\Entity\Revision;

use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;

/**
* A class to manage revisions.
*/
final class RevisionManager {

public const KEEP_REVISIONS = 5;

/**
* Constructs a new instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly ConfigFactoryInterface $configFactory,
private readonly Connection $connection,
) {
}

/**
* Gets the supported entity types.
*
* @return array
* An array of entity types.
*/
public function getSupportedEntityTypes() : array {
return $this->configFactory
->get('helfi_api_base.delete_revisions')
->get('entity_types') ?? [];
}

/**
* Asserts that entity type is supported.
*
* @param string $entityType
* The entity type to check.
*/
private function assertEntityType(string $entityType) : void {
if (!in_array($entityType, $this->getSupportedEntityTypes())) {
throw new \InvalidArgumentException('Entity type is not supported.');
}
try {
$definition = $this->entityTypeManager->getDefinition($entityType);

if (!$definition->isRevisionable()) {
throw new \InvalidArgumentException('Entity type does not support revisions.');
}
}
catch (PluginNotFoundException $e) {
throw new \InvalidArgumentException('Invalid entity type.', previous: $e);
}
}

/**
* Checks whether the given entity type is supported or not.
*
* @param string $entityType
* The entity type to check.
*
* @return bool
* TRUE if the entity type is supported.
*/
public function entityTypeIsSupported(string $entityType) : bool {
try {
$this->assertEntityType($entityType);

return TRUE;
}
catch (\InvalidArgumentException) {
}
return FALSE;
}

/**
* Deletes the previous revisions for the given entity type and ids.
*
* @param string $entityType
* The entity type.
* @param array $ids
* The version ids.
*/
public function deleteRevisions(string $entityType, array $ids) : void {
$this->assertEntityType($entityType);

$storage = $this->entityTypeManager
->getStorage($entityType);

foreach ($ids as $id) {
$storage->deleteRevision($id);
}
}

/**
* Gets the revisions for given entity type and id.
*
* Grouped by language to make testing easier.
*
* @param string $entityType
* The entity type.
* @param string|int $id
* The entity id.
* @param int $keep
* The number of revisions to keep.
*
* @return array
* An array of revision IDs.
*/
public function getRevisionsPerLanguage(
string $entityType,
string|int $id,
int $keep = self::KEEP_REVISIONS
) : array {
$this->assertEntityType($entityType);

$storage = $this->entityTypeManager->getStorage($entityType);
$definition = $this->entityTypeManager->getDefinition($entityType);

$revision_ids = $this->connection->query(
(string) new FormattableMarkup('SELECT [@vid] FROM {@table} WHERE [@id] = :id ORDER BY [@vid]', [
'@vid' => $definition->getKey('revision'),
'@table' => $definition->getRevisionTable(),
'@id' => $definition->getKey('id'),
]),
[':id' => $id]
)->fetchCol();

$revisions = [];

if (count($revision_ids) === 0) {
return [];
}
krsort($revision_ids);

foreach ($revision_ids as $vid) {
/** @var \Drupal\Core\Entity\RevisionableInterface $revision */
$revision = $storage->loadRevision($vid);

foreach ($revision->getTranslationLanguages() as $langcode => $language) {
if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) {
$revisions[$langcode][] = $revision->getLoadedRevisionId();
}
}
}

foreach ($revisions as $langcode => $items) {
$revisions[$langcode] = array_slice($items, $keep);
}

return $revisions;
}

/**
* Gets revisions for given entity type and id.
*
* @param string $entityType
* The entity type.
* @param string|int $id
* The entity ID.
* @param int $keep
* The number of revisions to keep.
*
* @return array
* An array of revision ids.
*/
public function getRevisions(string $entityType, string|int $id, int $keep = self::KEEP_REVISIONS) : array {
$revisions = $this->getRevisionsPerLanguage($entityType, $id, $keep);

return array_unique(array_merge(...array_values($revisions)));
}

}
58 changes: 58 additions & 0 deletions src/Plugin/QueueWorker/RevisionQueue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types = 1);

namespace Drupal\helfi_api_base\Plugin\QueueWorker;

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\helfi_api_base\Entity\Revision\RevisionManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Handles old revision deletion.
*
* @QueueWorker(
* id = "helfi_api_base_revision",
* title = @Translation("Queue worker for deleting old revisions"),
* cron = {"time" = 180}
* )
*/
final class RevisionQueue extends QueueWorkerBase implements ContainerFactoryPluginInterface {

/**
* The revision manager.
*
* @var \Drupal\helfi_api_base\Entity\Revision\RevisionManager
*/
private RevisionManager $revisionManager;

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) : self {
$instance = new self($configuration, $plugin_id, $plugin_definition);
$instance->revisionManager = $container->get('helfi_api_base.revision_manager');
return $instance;
}

/**
* Process queue item.
*
* @param array|mixed $data
* The queue data. Should contain 'entity_id' and 'entity_type'.
*/
public function processItem($data) : void {
if (!isset($data['entity_id'], $data['entity_type'])) {
return;
}
['entity_id' => $id, 'entity_type' => $type] = $data;

$revisions = $this->revisionManager->getRevisions($type, $id);

if ($revisions) {
$this->revisionManager->deleteRevisions($type, $revisions);
}
}

}
Loading