diff --git a/regcode.install b/regcode.install
index 385d9f0..3106f8e 100644
--- a/regcode.install
+++ b/regcode.install
@@ -1,5 +1,11 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\Component\Utility\DeprecationHelper;
+
 /**
  * @file
  * Install, uninstall and scheme functions for the regcode module.
@@ -103,7 +109,7 @@ function regcode_update_10200() {
     && $config->get($key_value) === 'You have not created any registration codes.') {
     $config->set($key_format, 'plain_text');
     $config->set($key_value, 'No registration codes have been created.');
-    $config->save(TRUE);
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $config->save(), fn() => $config->save(TRUE));
   }
 
 }
diff --git a/regcode.module b/regcode.module
index 4116fcc..e5f075c 100644
--- a/regcode.module
+++ b/regcode.module
@@ -7,83 +7,33 @@
 
 // phpcs:ignore Drupal.Commenting.InlineComment.SpacingAfter
 // cspell:ignore lastused hexadec alphanum abcdefghijklmnopqrstuvwqyz
-
-use Drupal\Component\Utility\Html;
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\regcode\Hook\RegcodeHooks;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\regcode\RegistrationCodeInterface;
 
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function regcode_help($path, $arg) {
-  $output = '';
-  switch ($path) {
-    case 'view.regcode.page_admin':
-      $output = '<p>' . t('View and manage created registration codes.') . '</p>';
-      break;
-
-    case 'regcode.admin_create':
-      $output = '<p>' . t('Create manually or generate new registration codes.') . '</p>';
-      break;
-
-    case 'regcode.admin_manage':
-      $output = '<p>' . t('Provides bulk management features for created registration codes.') . '</p>';
-      break;
-
-    case 'regcode.admin_settings':
-      $output = '<p>' . t('Configure the registration code module.') . '</p>';
-      break;
-  }
-  return $output;
+  return \Drupal::service(RegcodeHooks::class)->help($path, $arg);
 }
 
 /**
  * Implements hook_entity_extra_field_info().
  */
+#[LegacyHook]
 function regcode_entity_extra_field_info() {
-  $extra = [];
-
-  $config = \Drupal::config('regcode.settings');
-
-  $extra['user']['user']['form']['regcode'] = [
-    'label' => $config->get('regcode_field_title'),
-    'description' => $config->get('regcode_field_description'),
-    'weight' => 10,
-    'visible' => TRUE,
-  ];
-
-  return $extra;
+  return \Drupal::service(RegcodeHooks::class)->entityExtraFieldInfo();
 }
 
 /**
  * Implements hook_form_FORM_ID_alter() for user_register_form.
  */
+#[LegacyHook]
 function regcode_form_user_register_form_alter(&$form, FormStateInterface $form_state) {
-  // Only display the regcode field when it's attached to the form display.
-  if ($form_state->get('form_display')->getComponent('regcode')) {
-    $config = \Drupal::config('regcode.settings');
-
-    $form['regcode'] = [
-      '#type' => 'textfield',
-      '#title' => Html::escape($config->get('regcode_field_title')),
-      '#description' => Html::escape($config->get('regcode_field_description')),
-      '#required' => !($config->get('regcode_optional') || \Drupal::currentUser()->hasPermission('administer users')),
-      '#element_validate' => ['regcode_code_element_validate'],
-    ];
-
-    $form['actions']['submit']['#submit'][] = 'regcode_user_register_form_submit_handler';
-
-    // Capture the code from the URL, if present, and inject it into the
-    // registration form.
-    if (\Drupal::request()->query->has('regcode')) {
-      // The Form API can handle potentially unsafe characters as long as they
-      // are not printed directly. This code gets trimmed in
-      // \Drupal::service('registration_code')->validateCode($regcode).
-      $form['regcode']['#value'] = \Drupal::request()->query->get('regcode');
-      $form['regcode']['#description'] = NULL;
-      $form['regcode']['#disabled'] = TRUE;
-    }
-  }
+  \Drupal::service(RegcodeHooks::class)->formUserRegisterFormAlter($form, $form_state);
 }
 
 /**
diff --git a/regcode.services.yml b/regcode.services.yml
index e4f16d0..49b1283 100644
--- a/regcode.services.yml
+++ b/regcode.services.yml
@@ -3,3 +3,15 @@ services:
     class: \Drupal\regcode\RegistrationCode
     autowire: true
   Drupal\regcode\RegistrationCodeInterface: '@registration_code'
+
+  Drupal\regcode\Hook\RegcodeHooks:
+    class: Drupal\regcode\Hook\RegcodeHooks
+    autowire: true
+
+  Drupal\regcode\Hook\RegcodeTokensHooks:
+    class: Drupal\regcode\Hook\RegcodeTokensHooks
+    autowire: true
+
+  Drupal\regcode\Hook\RegcodeViewsHooks:
+    class: Drupal\regcode\Hook\RegcodeViewsHooks
+    autowire: true
diff --git a/regcode.tokens.inc b/regcode.tokens.inc
index 55c4707..3361f6d 100644
--- a/regcode.tokens.inc
+++ b/regcode.tokens.inc
@@ -5,148 +5,24 @@
  * Token support for the regcode module.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\regcode\Hook\RegcodeTokensHooks;
 use Drupal\Core\Render\BubbleableMetadata;
-use Drupal\Core\Url;
 
 // cspell:ignore lastused regurl
 
 /**
  * Implements hook_token_info().
  */
+#[LegacyHook]
 function regcode_token_info() {
-  $type = [
-    'name' => t('Registration Codes'),
-    'description' => t('Replacement tokens for registration codes'),
-    'needs-data' => 'regcode',
-  ];
-
-  $regcode['created'] = [
-    'name' => t("Code creation time"),
-    'description' => t("The date the regcode was created."),
-    'type' => 'date',
-  ];
-
-  $regcode['lastused'] = [
-    'name' => t("Code last used time"),
-    'description' => t("The date the regcode was used last."),
-    'type' => 'date',
-  ];
-
-  $regcode['begins'] = [
-    'name' => t("Code activation date"),
-    'description' => t("The date the regcode gets activated."),
-    'type' => 'date',
-  ];
-
-  $regcode['expires'] = [
-    'name' => t("Code expiry date"),
-    'description' => t("The date the regcode expires."),
-    'type' => 'date',
-  ];
-
-  $regcode['code'] = [
-    'name' => t("Registration code"),
-    'description' => t("One single registration code"),
-  ];
-
-  $regcode['regurl'] = [
-    'name' => t("Registration URL"),
-    'description' => t("Link to the registration page with regcode included"),
-  ];
-
-  $regcode['is_active'] = [
-    'name' => t("State of the regcode"),
-    'description' => t("Whether the code is active"),
-  ];
-
-  $regcode['maxuses'] = [
-    'name' => t("Maximum usage count"),
-    'description' => t("Maximum times a code can be used"),
-  ];
-
-  $regcode['uses'] = [
-    'name' => t("Current usage count"),
-    'description' => t("Number of times the code already has been used"),
-  ];
-
-  return [
-    'types' => ['regcode' => $type],
-    'tokens' => ['regcode' => $regcode],
-  ];
+  return \Drupal::service(RegcodeTokensHooks::class)->tokenInfo();
 }
 
 /**
  * Implements hook_tokens().
  */
+#[LegacyHook]
 function regcode_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
-  if (isset($options['language'])) {
-    $language_code = $options['language']->language;
-  }
-  else {
-    $language_code = NULL;
-  }
-
-  $replacements = [];
-
-  if ($type == 'regcode' && !empty($data['regcode'])) {
-    $regcode = $data['regcode'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'created':
-          $replacements[$original] = \Drupal::service('date.formatter')->format($regcode->created, 'medium', '', NULL, $language_code);
-          break;
-
-        case 'lastused':
-          $replacements[$original] = \Drupal::service('date.formatter')->format($regcode->created, 'medium', '', NULL, $language_code);
-          break;
-
-        case 'begins':
-          $replacements[$original] = \Drupal::service('date.formatter')->format($regcode->created, 'medium', '', NULL, $language_code);
-          break;
-
-        case 'expires':
-          $replacements[$original] = \Drupal::service('date.formatter')->format($regcode->created, 'medium', '', NULL, $language_code);
-          break;
-
-        case 'code':
-          $replacements[$original] = $regcode->code;
-          break;
-
-        case 'regurl':
-          $replacements[$original] = Url::fromRoute('user.register');
-          break;
-
-        case 'is_active':
-          $replacements[$original] = $regcode->is_active ? t('active') : t('inactive');
-          break;
-
-        case 'maxuses':
-          $replacements[$original] = $regcode->maxuses;
-          break;
-
-        case 'uses':
-          $replacements[$original] = $regcode->uses;
-          break;
-      }
-    }
-
-    if ($created_tokens = \Drupal::token()->findWithPrefix($tokens, 'created')) {
-      $replacements += \Drupal::token()->generate('date', $created_tokens, ['date' => $regcode->created], $options, $bubbleable_metadata);
-    }
-
-    if ($lastused_tokens = \Drupal::token()->findWithPrefix($tokens, 'lastused')) {
-      $replacements += \Drupal::token()->generate('date', $lastused_tokens, ['date' => $regcode->lastused], $options, $bubbleable_metadata);
-    }
-
-    if ($begins_tokens = \Drupal::token()->findWithPrefix($tokens, 'begins')) {
-      $replacements += \Drupal::token()->generate('date', $begins_tokens, ['date' => $regcode->begins], $options, $bubbleable_metadata);
-    }
-
-    if ($expires_tokens = \Drupal::token()->findWithPrefix($tokens, 'expires')) {
-      $replacements += \Drupal::token()->generate('date', $expires_tokens, ['date' => $regcode->expires], $options, $bubbleable_metadata);
-    }
-  }
-
-  return $replacements;
+  return \Drupal::service(RegcodeTokensHooks::class)->tokens($type, $tokens, $data, $options, $bubbleable_metadata);
 }
diff --git a/regcode.views.inc b/regcode.views.inc
index a5e2b1a..67b4e72 100644
--- a/regcode.views.inc
+++ b/regcode.views.inc
@@ -1,5 +1,12 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\regcode\Hook\RegcodeViewsHooks;
+
 /**
  * @file
  * Views integration for regcode module.
@@ -28,178 +35,9 @@
 /**
  * Implements hook_views_data().
  */
+#[LegacyHook]
 function regcode_views_data() {
-  $data = [];
-
-  $data['regcode']['table']['group'] = t('Regcode');
-
-  $data['regcode']['table']['base'] = [
-    'field' => 'rid',
-    'title' => t('Registration code'),
-    'help' => t('Registration codes created by the regcode module.'),
-    'weight' => -10,
-  ];
-
-  $data['regcode']['table']['join'] = [
-    'users' => [
-      'left_field' => 'uid',
-      'field' => 'uid',
-    ],
-  ];
-
-  $data['regcode']['rid'] = [
-    'title' => t('ID'),
-    'help' => t('The unique index for the regcode'),
-    'field' => [
-      'id' => 'numeric',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-    'filter' => [
-      'id' => 'numeric',
-    ],
-    'argument' => [
-      'id' => 'numeric',
-    ],
-  ];
-
-  $data['regcode']['uid'] = [
-    'title' => t('User'),
-    'help' => t('The last user to use this registration code.'),
-    'relationship' => [
-      'base' => 'users_field_data',
-      'base field' => 'uid',
-      'id' => 'standard',
-      'label' => t('Last user'),
-    ],
-  ];
-
-  $data['regcode']['code'] = [
-    'title' => t('Code'),
-    'help' => t('The registration code.'),
-    'field' => [
-      'id' => 'standard',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-    'filter' => [
-      'id' => 'string',
-    ],
-    'argument' => [
-      'id' => 'string',
-    ],
-  ];
-
-  $data['regcode']['created'] = [
-    'title' => t('Created'),
-    'help' => t('Date the registration code was created.'),
-    'field' => [
-      'id' => 'date',
-    ],
-    'sort' => [
-      'id' => 'date',
-    ],
-    'filter' => [
-      'id' => 'date',
-    ],
-  ];
-
-  $data['regcode']['lastused'] = [
-    'title' => t('Last used'),
-    'help' => t('Date the registration code was last used.'),
-    'field' => [
-      'id' => 'date',
-    ],
-    'sort' => [
-      'id' => 'date',
-    ],
-    'filter' => [
-      'id' => 'date',
-    ],
-  ];
-
-  $data['regcode']['begins'] = [
-    'title' => t('Begins'),
-    'help' => t('Date the registration code becomes available for use.'),
-    'field' => [
-      'id' => 'date',
-    ],
-    'sort' => [
-      'id' => 'date',
-    ],
-    'filter' => [
-      'id' => 'date',
-    ],
-  ];
-
-  $data['regcode']['expires'] = [
-    'title' => t('Expires'),
-    'help' => t('Date the registration code expires.'),
-    'field' => [
-      'id' => 'date',
-    ],
-    'sort' => [
-      'id' => 'date',
-    ],
-    'filter' => [
-      'id' => 'date',
-    ],
-  ];
-
-  $data['regcode']['is_active'] = [
-    'title' => t('Active'),
-    'help' => t('Whether the registration code is active.'),
-    'field' => [
-      'id' => 'boolean',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-    'filter' => [
-      'id' => 'boolean',
-      'label' => t('Active'),
-      'type' => 'yes-no',
-      'use_equal' => TRUE,
-    ],
-  ];
-
-  $data['regcode']['maxuses'] = [
-    'title' => t('Maximum uses'),
-    'help' => t('The maximum times this code can be used.'),
-    'field' => [
-      'id' => 'numeric',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-    'filter' => [
-      'id' => 'numeric',
-    ],
-    'argument' => [
-      'id' => 'numeric',
-    ],
-  ];
-
-  $data['regcode']['uses'] = [
-    'title' => t('Uses'),
-    'help' => t('The number of times this code has been used.'),
-    'field' => [
-      'id' => 'numeric',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-    'filter' => [
-      'id' => 'numeric',
-    ],
-    'argument' => [
-      'id' => 'numeric',
-    ],
-  ];
-
-  return $data;
+  return \Drupal::service(RegcodeViewsHooks::class)->viewsData();
 }
 
 /**
diff --git a/src/Hook/RegcodeHooks.php b/src/Hook/RegcodeHooks.php
new file mode 100644
index 0000000..022226a
--- /dev/null
+++ b/src/Hook/RegcodeHooks.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\regcode\Hook;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for regcode.
+ */
+class RegcodeHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($path, $arg) {
+    $output = '';
+    switch ($path) {
+      case 'view.regcode.page_admin':
+        $output = '<p>' . $this->t('View and manage created registration codes.') . '</p>';
+        break;
+
+      case 'regcode.admin_create':
+        $output = '<p>' . $this->t('Create manually or generate new registration codes.') . '</p>';
+        break;
+
+      case 'regcode.admin_manage':
+        $output = '<p>' . $this->t('Provides bulk management features for created registration codes.') . '</p>';
+        break;
+
+      case 'regcode.admin_settings':
+        $output = '<p>' . $this->t('Configure the registration code module.') . '</p>';
+        break;
+    }
+    return $output;
+  }
+
+  /**
+   * Implements hook_entity_extra_field_info().
+   */
+  #[Hook('entity_extra_field_info')]
+  public static function entityExtraFieldInfo() {
+    $extra = [];
+    $config = \Drupal::config('regcode.settings');
+    $extra['user']['user']['form']['regcode'] = [
+      'label' => $config->get('regcode_field_title'),
+      'description' => $config->get('regcode_field_description'),
+      'weight' => 10,
+      'visible' => TRUE,
+    ];
+    return $extra;
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for user_register_form.
+   */
+  #[Hook('form_user_register_form_alter')]
+  public static function formUserRegisterFormAlter(&$form, FormStateInterface $form_state) {
+    // Only display the regcode field when it's attached to the form display.
+    if ($form_state->get('form_display')->getComponent('regcode')) {
+      $config = \Drupal::config('regcode.settings');
+      $form['regcode'] = [
+        '#type' => 'textfield',
+        '#title' => Html::escape($config->get('regcode_field_title')),
+        '#description' => Html::escape($config->get('regcode_field_description')),
+        '#required' => !($config->get('regcode_optional') || \Drupal::currentUser()->hasPermission('administer users')),
+        '#element_validate' => [
+          'regcode_code_element_validate',
+        ],
+      ];
+      $form['actions']['submit']['#submit'][] = 'regcode_user_register_form_submit_handler';
+      // Capture the code from the URL, if present, and inject it into the
+      // registration form.
+      if (\Drupal::request()->query->has('regcode')) {
+        // The Form API can handle potentially unsafe characters as long as they
+        // are not printed directly. This code gets trimmed in
+        // \Drupal::service('registration_code')->validateCode($regcode).
+        $form['regcode']['#value'] = \Drupal::request()->query->get('regcode');
+        $form['regcode']['#description'] = NULL;
+        $form['regcode']['#disabled'] = TRUE;
+      }
+    }
+  }
+
+}
diff --git a/src/Hook/RegcodeTokensHooks.php b/src/Hook/RegcodeTokensHooks.php
new file mode 100644
index 0000000..6246c1a
--- /dev/null
+++ b/src/Hook/RegcodeTokensHooks.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Drupal\regcode\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for regcode.
+ */
+class RegcodeTokensHooks {
+  use StringTranslationTrait;
+  // cspell:ignore lastused regurl
+
+  /**
+   * Implements hook_token_info().
+   */
+  #[Hook('token_info')]
+  public function tokenInfo() {
+    $type = [
+      'name' => $this->t('Registration Codes'),
+      'description' => $this->t('Replacement tokens for registration codes'),
+      'needs-data' => 'regcode',
+    ];
+    $regcode['created'] = [
+      'name' => $this->t("Code creation time"),
+      'description' => $this->t("The date the regcode was created."),
+      'type' => 'date',
+    ];
+    $regcode['lastused'] = [
+      'name' => $this->t("Code last used time"),
+      'description' => $this->t("The date the regcode was used last."),
+      'type' => 'date',
+    ];
+    $regcode['begins'] = [
+      'name' => $this->t("Code activation date"),
+      'description' => $this->t("The date the regcode gets activated."),
+      'type' => 'date',
+    ];
+    $regcode['expires'] = [
+      'name' => $this->t("Code expiry date"),
+      'description' => $this->t("The date the regcode expires."),
+      'type' => 'date',
+    ];
+    $regcode['code'] = [
+      'name' => $this->t("Registration code"),
+      'description' => $this->t("One single registration code"),
+    ];
+    $regcode['regurl'] = [
+      'name' => $this->t("Registration URL"),
+      'description' => $this->t("Link to the registration page with regcode included"),
+    ];
+    $regcode['is_active'] = [
+      'name' => $this->t("State of the regcode"),
+      'description' => $this->t("Whether the code is active"),
+    ];
+    $regcode['maxuses'] = [
+      'name' => $this->t("Maximum usage count"),
+      'description' => $this->t("Maximum times a code can be used"),
+    ];
+    $regcode['uses'] = [
+      'name' => $this->t("Current usage count"),
+      'description' => $this->t("Number of times the code already has been used"),
+    ];
+    return [
+      'types' => [
+        'regcode' => $type,
+      ],
+      'tokens' => [
+        'regcode' => $regcode,
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_tokens().
+   */
+  #[Hook('tokens')]
+  public function tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+    if (isset($options['language'])) {
+      $language_code = $options['language']->language;
+    }
+    else {
+      $language_code = NULL;
+    }
+    $replacements = [];
+    if ($type == 'regcode' && !empty($data['regcode'])) {
+      $regcode = $data['regcode'];
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          case 'created':
+            $replacements[$original] = \Drupal::service('date.formatter')->format($regcode->created, 'medium', '', NULL, $language_code);
+            break;
+
+          case 'lastused':
+            $replacements[$original] = \Drupal::service('date.formatter')->format($regcode->created, 'medium', '', NULL, $language_code);
+            break;
+
+          case 'begins':
+            $replacements[$original] = \Drupal::service('date.formatter')->format($regcode->created, 'medium', '', NULL, $language_code);
+            break;
+
+          case 'expires':
+            $replacements[$original] = \Drupal::service('date.formatter')->format($regcode->created, 'medium', '', NULL, $language_code);
+            break;
+
+          case 'code':
+            $replacements[$original] = $regcode->code;
+            break;
+
+          case 'regurl':
+            $replacements[$original] = Url::fromRoute('user.register');
+            break;
+
+          case 'is_active':
+            $replacements[$original] = $regcode->is_active ? $this->t('active') : $this->t('inactive');
+            break;
+
+          case 'maxuses':
+            $replacements[$original] = $regcode->maxuses;
+            break;
+
+          case 'uses':
+            $replacements[$original] = $regcode->uses;
+            break;
+        }
+      }
+      if ($created_tokens = \Drupal::token()->findWithPrefix($tokens, 'created')) {
+        $replacements += \Drupal::token()->generate('date', $created_tokens, [
+          'date' => $regcode->created,
+        ], $options, $bubbleable_metadata);
+      }
+      if ($lastused_tokens = \Drupal::token()->findWithPrefix($tokens, 'lastused')) {
+        $replacements += \Drupal::token()->generate('date', $lastused_tokens, [
+          'date' => $regcode->lastused,
+        ], $options, $bubbleable_metadata);
+      }
+      if ($begins_tokens = \Drupal::token()->findWithPrefix($tokens, 'begins')) {
+        $replacements += \Drupal::token()->generate('date', $begins_tokens, [
+          'date' => $regcode->begins,
+        ], $options, $bubbleable_metadata);
+      }
+      if ($expires_tokens = \Drupal::token()->findWithPrefix($tokens, 'expires')) {
+        $replacements += \Drupal::token()->generate('date', $expires_tokens, [
+          'date' => $regcode->expires,
+        ], $options, $bubbleable_metadata);
+      }
+    }
+    return $replacements;
+  }
+
+}
diff --git a/src/Hook/RegcodeViewsHooks.php b/src/Hook/RegcodeViewsHooks.php
new file mode 100644
index 0000000..7dc8d60
--- /dev/null
+++ b/src/Hook/RegcodeViewsHooks.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace Drupal\regcode\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for regcode.
+ */
+class RegcodeViewsHooks {
+  use StringTranslationTrait;
+  /**
+   * @file
+   * Views integration for regcode module.
+   */
+  // cspell:ignore lastused newfields
+  /**
+   * Implements hook_views_handlers().
+   */
+    // phpcs:disable
+    /*function regcode_views_handlers() {
+      return [
+        'info' => [
+          'path' => drupal_get_path('module', 'regcode') . '/includes',
+        ),
+        'handlers' => [
+          'views_handler_field_term_regcode_tid' => [
+            'parent' => 'views_handler_field_prerender_list',
+          ),
+        ),
+      );
+    }*/
+    // phpcs:enable
+
+  /**
+   * Implements hook_views_data().
+   */
+  #[Hook('views_data')]
+  public function viewsData() {
+    $data = [];
+    $data['regcode']['table']['group'] = $this->t('Regcode');
+    $data['regcode']['table']['base'] = [
+      'field' => 'rid',
+      'title' => $this->t('Registration code'),
+      'help' => $this->t('Registration codes created by the regcode module.'),
+      'weight' => -10,
+    ];
+    $data['regcode']['table']['join'] = [
+      'users' => [
+        'left_field' => 'uid',
+        'field' => 'uid',
+      ],
+    ];
+    $data['regcode']['rid'] = [
+      'title' => $this->t('ID'),
+      'help' => $this->t('The unique index for the regcode'),
+      'field' => [
+        'id' => 'numeric',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+      'filter' => [
+        'id' => 'numeric',
+      ],
+      'argument' => [
+        'id' => 'numeric',
+      ],
+    ];
+    $data['regcode']['uid'] = [
+      'title' => $this->t('User'),
+      'help' => $this->t('The last user to use this registration code.'),
+      'relationship' => [
+        'base' => 'users_field_data',
+        'base field' => 'uid',
+        'id' => 'standard',
+        'label' => $this->t('Last user'),
+      ],
+    ];
+    $data['regcode']['code'] = [
+      'title' => $this->t('Code'),
+      'help' => $this->t('The registration code.'),
+      'field' => [
+        'id' => 'standard',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+      'filter' => [
+        'id' => 'string',
+      ],
+      'argument' => [
+        'id' => 'string',
+      ],
+    ];
+    $data['regcode']['created'] = [
+      'title' => $this->t('Created'),
+      'help' => $this->t('Date the registration code was created.'),
+      'field' => [
+        'id' => 'date',
+      ],
+      'sort' => [
+        'id' => 'date',
+      ],
+      'filter' => [
+        'id' => 'date',
+      ],
+    ];
+    $data['regcode']['lastused'] = [
+      'title' => $this->t('Last used'),
+      'help' => $this->t('Date the registration code was last used.'),
+      'field' => [
+        'id' => 'date',
+      ],
+      'sort' => [
+        'id' => 'date',
+      ],
+      'filter' => [
+        'id' => 'date',
+      ],
+    ];
+    $data['regcode']['begins'] = [
+      'title' => $this->t('Begins'),
+      'help' => $this->t('Date the registration code becomes available for use.'),
+      'field' => [
+        'id' => 'date',
+      ],
+      'sort' => [
+        'id' => 'date',
+      ],
+      'filter' => [
+        'id' => 'date',
+      ],
+    ];
+    $data['regcode']['expires'] = [
+      'title' => $this->t('Expires'),
+      'help' => $this->t('Date the registration code expires.'),
+      'field' => [
+        'id' => 'date',
+      ],
+      'sort' => [
+        'id' => 'date',
+      ],
+      'filter' => [
+        'id' => 'date',
+      ],
+    ];
+    $data['regcode']['is_active'] = [
+      'title' => $this->t('Active'),
+      'help' => $this->t('Whether the registration code is active.'),
+      'field' => [
+        'id' => 'boolean',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+      'filter' => [
+        'id' => 'boolean',
+        'label' => $this->t('Active'),
+        'type' => 'yes-no',
+        'use_equal' => TRUE,
+      ],
+    ];
+    $data['regcode']['maxuses'] = [
+      'title' => $this->t('Maximum uses'),
+      'help' => $this->t('The maximum times this code can be used.'),
+      'field' => [
+        'id' => 'numeric',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+      'filter' => [
+        'id' => 'numeric',
+      ],
+      'argument' => [
+        'id' => 'numeric',
+      ],
+    ];
+    $data['regcode']['uses'] = [
+      'title' => $this->t('Uses'),
+      'help' => $this->t('The number of times this code has been used.'),
+      'field' => [
+        'id' => 'numeric',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+      'filter' => [
+        'id' => 'numeric',
+      ],
+      'argument' => [
+        'id' => 'numeric',
+      ],
+    ];
+    return $data;
+  }
+
+}
diff --git a/tests/modules/regcode_alterer/regcode_alterer.module b/tests/modules/regcode_alterer/regcode_alterer.module
index ede8a10..6e171bb 100644
--- a/tests/modules/regcode_alterer/regcode_alterer.module
+++ b/tests/modules/regcode_alterer/regcode_alterer.module
@@ -8,6 +8,8 @@
  * are expected to work.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\regcode_alterer\Hook\RegcodeAltererHooks;
 use Drupal\user\UserInterface;
 
 /**
@@ -15,10 +17,9 @@ use Drupal\user\UserInterface;
  *
  * Called when a registration code is used.
  */
+#[LegacyHook]
 function regcode_alterer_regcode_used(object $code, UserInterface $account): void {
-  if (is_object($code)) {
-    \Drupal::messenger()->addStatus("Thanks {$account->getAccountName()}, the code '$code->rid' was used.");
-  }
+  \Drupal::service(RegcodeAltererHooks::class)->regcodeUsed($code, $account);
 }
 
 /**
@@ -26,12 +27,9 @@ function regcode_alterer_regcode_used(object $code, UserInterface $account): voi
  *
  * Called when a new registration code is created.
  */
+#[LegacyHook]
 function regcode_alterer_regcode_presave(object $code): void {
-  if ($code) {
-    // $code->rid is not valid until AFTER the save is complete, so we can't
-    // use it in this presave hook.
-    \Drupal::messenger()->addStatus("The code '$code->code' was created.");
-  }
+  \Drupal::service(RegcodeAltererHooks::class)->regcodePresave($code);
 }
 
 /**
@@ -39,8 +37,7 @@ function regcode_alterer_regcode_presave(object $code): void {
  *
  * Changes the regcode data that is being loaded.
  */
+#[LegacyHook]
 function regcode_alterer_regcode_load(array $code): void {
-  foreach ($code as $rid => $regcode) {
-    \Drupal::messenger()->addStatus("The code '$rid' was loaded.");
-  }
+  \Drupal::service(RegcodeAltererHooks::class)->regcodeLoad($code);
 }
diff --git a/tests/modules/regcode_alterer/regcode_alterer.services.yml b/tests/modules/regcode_alterer/regcode_alterer.services.yml
new file mode 100644
index 0000000..025b930
--- /dev/null
+++ b/tests/modules/regcode_alterer/regcode_alterer.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\regcode_alterer\Hook\RegcodeAltererHooks:
+    class: Drupal\regcode_alterer\Hook\RegcodeAltererHooks
+    autowire: true
diff --git a/tests/modules/regcode_alterer/src/Hook/RegcodeAltererHooks.php b/tests/modules/regcode_alterer/src/Hook/RegcodeAltererHooks.php
new file mode 100644
index 0000000..6e46c79
--- /dev/null
+++ b/tests/modules/regcode_alterer/src/Hook/RegcodeAltererHooks.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\regcode_alterer\Hook;
+
+use Drupal\user\UserInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for regcode_alterer.
+ */
+class RegcodeAltererHooks {
+
+  /**
+   * Implements hook_regcode_used().
+   *
+   * Called when a registration code is used.
+   */
+  #[Hook('regcode_used')]
+  public static function regcodeUsed(object $code, UserInterface $account): void {
+    if (is_object($code)) {
+      \Drupal::messenger()->addStatus("Thanks {$account->getAccountName()}, the code '{$code->rid}' was used.");
+    }
+  }
+
+  /**
+   * Implements hook_regcode_presave().
+   *
+   * Called when a new registration code is created.
+   */
+  #[Hook('regcode_presave')]
+  public static function regcodePresave(object $code): void {
+    if ($code) {
+      // $code->rid is not valid until AFTER the save is complete, so we can't
+      // use it in this presave hook.
+      \Drupal::messenger()->addStatus("The code '{$code->code}' was created.");
+    }
+  }
+
+  /**
+   * Implements hook_regcode_load().
+   *
+   * Changes the regcode data that is being loaded.
+   */
+  #[Hook('regcode_load')]
+  public static function regcodeLoad(array $code): void {
+    foreach ($code as $rid => $regcode) {
+      \Drupal::messenger()->addStatus("The code '{$rid}' was loaded.");
+    }
+  }
+
+}
diff --git a/tests/src/Functional/Update/RegcodeUpdateTest.php b/tests/src/Functional/Update/RegcodeUpdateTest.php
index 27da142..6236817 100644
--- a/tests/src/Functional/Update/RegcodeUpdateTest.php
+++ b/tests/src/Functional/Update/RegcodeUpdateTest.php
@@ -4,14 +4,15 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\regcode\Functional\Update;
 
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
 use Drupal\FunctionalTests\Update\UpdatePathTestBase;
 
 /**
  * Tests that the regcode View is properly updated during database updates.
  *
  * @group regcode
- * @group legacy
  */
+#[IgnoreDeprecations]
 class RegcodeUpdateTest extends UpdatePathTestBase {
 
   /**
diff --git a/tests/src/Kernel/RegcodeLegacyTest.php b/tests/src/Kernel/RegcodeLegacyTest.php
index 177a418..b5952aa 100644
--- a/tests/src/Kernel/RegcodeLegacyTest.php
+++ b/tests/src/Kernel/RegcodeLegacyTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\regcode\Kernel;
 
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
+use Drupal\Component\Utility\DeprecationHelper;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\regcode\RegistrationCodeInterface;
 
@@ -11,8 +13,8 @@ use Drupal\regcode\RegistrationCodeInterface;
  * Tests legacy regcode functionality.
  *
  * @group regcode
- * @group legacy
  */
+#[IgnoreDeprecations]
 class RegcodeLegacyTest extends KernelTestBase {
 
   /**
@@ -41,7 +43,7 @@ class RegcodeLegacyTest extends KernelTestBase {
    * Tests the deprecation message for regcode_load_single().
    */
   public function testLoadSingle(): void {
-    $this->expectDeprecation("regcode_load_single() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->load() instead. See https://www.drupal.org/node/3471716");
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessage("regcode_load_single() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->load() instead. See https://www.drupal.org/node/3471716"), fn() => $this->expectDeprecation("regcode_load_single() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->load() instead. See https://www.drupal.org/node/3471716"));
     $id = 1337;
     $conditions = [];
     $this->assertFalse(regcode_load_single($id, $conditions));
@@ -51,7 +53,7 @@ class RegcodeLegacyTest extends KernelTestBase {
    * Tests the deprecation message for regcode_code_validate().
    */
   public function testCodeValidate(): void {
-    $this->expectDeprecation("regcode_get_vocab_terms() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->getVocabTerms() instead. See https://www.drupal.org/node/3471716");
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessage("regcode_get_vocab_terms() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->getVocabTerms() instead. See https://www.drupal.org/node/3471716"), fn() => $this->expectDeprecation("regcode_get_vocab_terms() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->getVocabTerms() instead. See https://www.drupal.org/node/3471716"));
     $regcode = "DoesNotExist";
     $this->assertIsInt(regcode_code_validate($regcode));
   }
@@ -60,7 +62,7 @@ class RegcodeLegacyTest extends KernelTestBase {
    * Tests the deprecation message for regcode_code_consume().
    */
   public function testCodeConsume(): void {
-    $this->expectDeprecation("regcode_code_consume() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->consumeCode() instead. See https://www.drupal.org/node/3471716");
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessage("regcode_code_consume() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->consumeCode() instead. See https://www.drupal.org/node/3471716"), fn() => $this->expectDeprecation("regcode_code_consume() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->consumeCode() instead. See https://www.drupal.org/node/3471716"));
     $regcode = "DoesNotExist";
     $uid = 1337;
     $this->assertIsInt(regcode_code_consume($regcode, $uid));
@@ -70,7 +72,7 @@ class RegcodeLegacyTest extends KernelTestBase {
    * Tests the deprecation message for regcode_save().
    */
   public function testSave(): void {
-    $this->expectDeprecation("regcode_save() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->save() instead. See https://www.drupal.org/node/3471716");
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessage("regcode_save() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->save() instead. See https://www.drupal.org/node/3471716"), fn() => $this->expectDeprecation("regcode_save() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->save() instead. See https://www.drupal.org/node/3471716"));
     $code = new \stdClass();
     $action = RegistrationCodeInterface::MODE_REPLACE;
     $this->assertFalse(regcode_save($code, $action));
@@ -80,7 +82,7 @@ class RegcodeLegacyTest extends KernelTestBase {
    * Tests the deprecation message for regcode_clean().
    */
   public function testClean(): void {
-    $this->expectDeprecation("regcode_get_vocab_terms() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->getVocabTerms() instead. See https://www.drupal.org/node/3471716");
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessage("regcode_get_vocab_terms() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->getVocabTerms() instead. See https://www.drupal.org/node/3471716"), fn() => $this->expectDeprecation("regcode_get_vocab_terms() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->getVocabTerms() instead. See https://www.drupal.org/node/3471716"));
     $op = RegistrationCodeInterface::CLEAN_TRUNCATE;
     $this->assertTrue(regcode_clean($op));
   }
@@ -89,7 +91,7 @@ class RegcodeLegacyTest extends KernelTestBase {
    * Tests the deprecation message for regcode_generate().
    */
   public function testGenerate(): void {
-    $this->expectDeprecation("regcode_get_vocab_terms() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->getVocabTerms() instead. See https://www.drupal.org/node/3471716");
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessage("regcode_get_vocab_terms() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->getVocabTerms() instead. See https://www.drupal.org/node/3471716"), fn() => $this->expectDeprecation("regcode_get_vocab_terms() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->getVocabTerms() instead. See https://www.drupal.org/node/3471716"));
     $length = 12;
     $output = 'alpha';
     $case = FALSE;
@@ -100,7 +102,7 @@ class RegcodeLegacyTest extends KernelTestBase {
    * Tests the deprecation message for regcode_delete_action().
    */
   public function testDeleteAction(): void {
-    $this->expectDeprecation("regcode_delete_action() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->deleteAction() instead. See https://www.drupal.org/node/3471716");
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessage("regcode_delete_action() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->deleteAction() instead. See https://www.drupal.org/node/3471716"), fn() => $this->expectDeprecation("regcode_delete_action() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->deleteAction() instead. See https://www.drupal.org/node/3471716"));
     $object = new \stdClass();
     $object->rid = 2;
     $context = [];
@@ -111,7 +113,7 @@ class RegcodeLegacyTest extends KernelTestBase {
    * Tests the deprecation message for regcode_activate_action().
    */
   public function testActivateAction(): void {
-    $this->expectDeprecation("regcode_activate_action() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->activateAction() instead. See https://www.drupal.org/node/3471716");
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessage("regcode_activate_action() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->activateAction() instead. See https://www.drupal.org/node/3471716"), fn() => $this->expectDeprecation("regcode_activate_action() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->activateAction() instead. See https://www.drupal.org/node/3471716"));
     $object = new \stdClass();
     $object->rid = 2;
     $context = [];
@@ -122,7 +124,7 @@ class RegcodeLegacyTest extends KernelTestBase {
    * Tests the deprecation message for regcode_deactivate_action().
    */
   public function testDeactivateAction(): void {
-    $this->expectDeprecation("regcode_deactivate_action() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->deactivateAction() instead. See https://www.drupal.org/node/3471716");
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessage("regcode_deactivate_action() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->deactivateAction() instead. See https://www.drupal.org/node/3471716"), fn() => $this->expectDeprecation("regcode_deactivate_action() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->deactivateAction() instead. See https://www.drupal.org/node/3471716"));
     $object = new \stdClass();
     $object->rid = 2;
     $context = [];
@@ -133,7 +135,7 @@ class RegcodeLegacyTest extends KernelTestBase {
    * Tests the deprecation message for regcode_get_vocab_terms().
    */
   public function testGetVocabTerms(): void {
-    $this->expectDeprecation("regcode_get_vocab_terms() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->getVocabTerms() instead. See https://www.drupal.org/node/3471716");
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $this->expectUserDeprecationMessage("regcode_get_vocab_terms() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->getVocabTerms() instead. See https://www.drupal.org/node/3471716"), fn() => $this->expectDeprecation("regcode_get_vocab_terms() is deprecated in regcode:2.0.0 and is removed from regcode:3.0.0. Use \Drupal::service('registration_code')->getVocabTerms() instead. See https://www.drupal.org/node/3471716"));
     regcode_get_vocab_terms();
   }
 
