Testor can be configured to execute a sanitization command before creating a backup.

For example:

sanitize:
  command: 'drush sql:sanitize'

Drush by default do some basic sanitization and also can be extended.

Let write a custom command that deletes all users except user 1.

First, generate a custom command with Drush:

drush generate drush:command-file

It interactively asks for input. Input module name "my_sanitizer" and leave all other inputs default. Output should be like:


 Welcome to dcf generator!
–––––––––––––––––––––––––––
 Module machine name:
 ➤ my_sanitizer
 Module name [My sanitizer]:
 ➤ 
 Class [MySanitizerCommands]:
 ➤ 
 Would you like to inject dependencies? [No]:
 ➤ 
 The following directories and files have been created or updated:
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
 • /var/www/html/web/modules/custom/my_sanitizer/src/Drush/Commands/MySanitizerCommands.php

Now let make the generated command a sanitize plugin. Make it extend SanitizePluginInterface (to help IDE generate the method stubs) and inject Connection to the constructor:

/**
 * A Drush commandfile.
 */
final class MySanitizerCommands extends DrushCommands implements SanitizePluginInterface
{

  use AutowireTrait;

  /**
   * Constructs a MySanitizerCommands object.
   */
  public function __construct(
    private readonly Token $token,
    protected Connection   $database,
  )
  {
    parent::__construct();
  }

}

Now implement methods and add corresponding annotations. Finally, class looks like below:

/**
 * A Drush commandfile.
 */
final class MySanitizerCommands extends DrushCommands implements SanitizePluginInterface
{

  use AutowireTrait;

  /**
   * Constructs a MySanitizerCommands object.
   */
  public function __construct(
    private readonly Token $token,
    protected Connection   $database,
  )
  {
    parent::__construct();
  }

  #[CLI\Hook(type: HookManager::POST_COMMAND_HOOK, target: SanitizeCommands::SANITIZE)]
  public function sanitize($result, CommandData $commandData)
  {
    // TODO: Implement sanitize() method.
    $result = $this->database->query('select uid from users where uid > 1')->fetchAll();

    foreach ($result as $item) {
      $this->deleteUser($item->uid);
    }
  }

  #[CLI\Hook(type: HookManager::ON_EVENT, target: SanitizeCommands::CONFIRMS)]
  public function messages(array &$messages, InputInterface $input)
  {
    $messages[] = 'Remove all users but user with id=1.';
  }

  /**
   * Deletes a user programmatically.
   *
   * @param int $uid
   *   The user ID of the user to delete.
   */
  protected function deleteUser($uid)
  {
    // Load the user entity by user ID.
    $user = User::load($uid);

    if ($user) {
      // Delete the user entity directly.
      $user->delete();

      \Drupal::logger('custom_module')->notice('User account with UID @uid has been deleted.', ['@uid' => $uid]);
    } else {
      \Drupal::logger('custom_module')->error('User account with UID @uid does not exist.', ['@uid' => $uid]);
    }
  }
}

To enable our custom command, first enable the newly created module:

drush en my_sanitizer

Now, when executing

drush sql:sanitize

it should show the added message, and after confirmation perform sanitize.

Same is before creating a snapshot via testor. E.g:

testor snapshot:create --name check-sanitization