diff --git a/quick_node_clone.module b/quick_node_clone.module
index 7665c6f..dcc6f78 100644
--- a/quick_node_clone.module
+++ b/quick_node_clone.module
@@ -5,17 +5,12 @@
  * Contains quick_node_clone.module.
  */
 
-use Drupal\Component\Utility\Html;
-use Drupal\Core\Entity\ContentEntityFormInterface;
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\quick_node_clone\Hook\QuickNodeCloneHooks;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
-use Drupal\group\Entity\GroupContent;
-use Drupal\group\Entity\GroupRelationship;
-use Drupal\group\Plugin\Group\Relation\GroupRelationTypeManagerInterface;
 use Drupal\node\NodeInterface;
-use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Session\AccountInterface;
 
 if (class_exists('\Drupal\node\Form\NodeForm')) {
@@ -28,143 +23,39 @@ else {
 /**
  * Implements hook_entity_type_build().
  */
+#[LegacyHook]
 function quick_node_clone_entity_type_build(array &$entity_types) {
-  if (isset($entity_types['node'])) {
-    $entity_types['node']->setFormClass('quick_node_clone', 'Drupal\quick_node_clone\Form\QuickNodeCloneNodeForm');
-  }
+  \Drupal::service(QuickNodeCloneHooks::class)->entityTypeBuild($entity_types);
 }
 
 /**
  * Implements hook_entity_operation().
  */
+#[LegacyHook]
 function quick_node_clone_entity_operation(EntityInterface $entity) {
-  $operations = [];
-
-  // Only add an operation for node entities.
-  if ($entity->getEntityTypeId() !== 'node') {
-    return $operations;
-  }
-
-  if ($entity->access('clone')) {
-    $operations['quick_clone'] = [
-      'title' => t('Clone'),
-      'weight' => '100',
-      'url' => Url::fromRoute('quick_node_clone.node.quick_clone', ['node' => $entity->id()]),
-    ];
-  }
-
-  return $operations;
+  return \Drupal::service(QuickNodeCloneHooks::class)->entityOperation($entity);
 }
 
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function quick_node_clone_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.quick_node_clone':
-      $text = file_get_contents(dirname(__FILE__) . '/README.md');
-      if (!\Drupal::moduleHandler()->moduleExists('markdown')) {
-        return '<pre>' . Html::escape($text) . '</pre>';
-      }
-      else {
-        // Use the Markdown filter to render the README.
-        $filter_manager = \Drupal::service('plugin.manager.filter');
-        $settings = \Drupal::configFactory()
-          ->get('markdown.settings')
-          ->getRawData();
-        $config = ['settings' => $settings];
-        $filter = $filter_manager->createInstance('markdown', $config);
-        return $filter->process($text, 'en');
-      }
-  }
-  return NULL;
+  return \Drupal::service(QuickNodeCloneHooks::class)->help($route_name, $route_match);
 }
 
 /**
  * Implements hook_form_alter().
  */
+#[LegacyHook]
 function quick_node_clone_form_alter(&$form, FormStateInterface $form_state, $form_id) {
-  if (!array_key_exists('footer', $form)) {
-    return;
-  }
-
-  // Check the operation set on the form is 'quick_node_clone'.
-  $form_object = $form_state->getFormObject();
-  if (!$form_object instanceof ContentEntityFormInterface) {
-    return;
-  }
-  if ($form_object->getOperation() !== 'quick_node_clone') {
-    return;
-  }
-
-  // Check that the content entity being cloned is moderated.
-  if (!\Drupal::moduleHandler()->moduleExists('content_moderation')) {
-    return;
-  }
-  $moderation_info = \Drupal::service('content_moderation.moderation_information');
-  if (!$moderation_info->isModeratedEntity($form_object->getEntity())) {
-    return;
-  }
-
-  $form['moderation_state']['#group'] = 'footer';
+  \Drupal::service(QuickNodeCloneHooks::class)->formAlter($form, $form_state, $form_id);
 }
 
 /**
  * Implements hook_node_access().
  */
+#[LegacyHook]
 function quick_node_clone_node_access(NodeInterface $node, $operation, AccountInterface $account) {
-  // Check if the operation is 'clone'.
-  if ($operation != 'clone') {
-    return AccessResult::neutral();
-  }
-
-  $bundle = $node->bundle();
-  $permissions = ["clone $bundle content"];
-
-  $is_owner = $node->getOwnerId() == $account->id();
-  if ($is_owner) {
-    $permissions[] = "clone own $bundle content";
-  }
-
-  $access = AccessResult::allowedIfHasPermissions($account, $permissions, 'OR');
-  if (!$access->isAllowed()) {
-    return AccessResult::neutral();
-  }
-
-  if (\Drupal::moduleHandler()->moduleExists('gnode')) {
-
-    // Check that user has permission to create a relationship.
-    // Support for group module version 1.x.
-    if (class_exists(GroupContent::class)) {
-      $group_relationships = GroupContent::loadByEntity($node);
-      foreach ($group_relationships as $group_relationship) {
-        $access = $group_relationship->getContentPlugin()->createEntityAccess($group_relationship->getGroup(), $account);
-        if ($access->isAllowed()) {
-          return AccessResult::allowed();
-        }
-      }
-    }
-    // Support for group module version 2.x and 3.x.
-    else {
-      $group_relationships = GroupRelationship::loadByEntity($node);
-      $relation_type_manager = \Drupal::service('group_relation_type.manager');
-      assert($relation_type_manager instanceof GroupRelationTypeManagerInterface);
-      foreach ($group_relationships as $group_relationship) {
-        $access_handler = $relation_type_manager->getAccessControlHandler($group_relationship->getPluginId());
-        $access = $access_handler->entityCreateAccess($group_relationship->getGroup(), $account);
-        if ($access) {
-          return AccessResult::allowed();
-        }
-      }
-    }
-  }
-
-  // Only check global access if we there is no group module enabled, or
-  // content does not have group(s).
-  if (empty($group_relationships) && $node->access('create')) {
-    return AccessResult::allowed();
-  }
-
-  // Default to Drupal's normal access handling.
-  return AccessResult::neutral();
+  return \Drupal::service(QuickNodeCloneHooks::class)->nodeAccess($node, $operation, $account);
 }
diff --git a/quick_node_clone.services.yml b/quick_node_clone.services.yml
index 2e1cf1e..85080dd 100644
--- a/quick_node_clone.services.yml
+++ b/quick_node_clone.services.yml
@@ -25,3 +25,11 @@ services:
       - '@request_stack'
       - '@entity_type.manager'
       - '@?path_alias.manager'
+
+  Drupal\quick_node_clone\Hook\QuickNodeCloneHooks:
+    class: Drupal\quick_node_clone\Hook\QuickNodeCloneHooks
+    autowire: true
+
+  Drupal\quick_node_clone\Hook\QuickNodeCloneViewsHooks:
+    class: Drupal\quick_node_clone\Hook\QuickNodeCloneViewsHooks
+    autowire: true
diff --git a/quick_node_clone.views.inc b/quick_node_clone.views.inc
index 5a35c10..5b7f2f3 100644
--- a/quick_node_clone.views.inc
+++ b/quick_node_clone.views.inc
@@ -1,5 +1,12 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\quick_node_clone\Hook\QuickNodeCloneViewsHooks;
+
 /**
  * @file
  * Provide views data for quick_node_clone.module.
@@ -8,12 +15,7 @@
 /**
  * Implements hook_views_data_alter().
  */
+#[LegacyHook]
 function quick_node_clone_views_data_alter(&$data) {
-  $data['node']['clone_link'] = [
-    'field' => [
-      'title' => t('Add clone link'),
-      'help' => t('Provide a clone link to the Content.'),
-      'id' => 'quick_node_clone_link',
-    ],
-  ];
+  \Drupal::service(QuickNodeCloneViewsHooks::class)->viewsDataAlter($data);
 }
diff --git a/src/EventSubscriber/AddressEventSubscriber.php b/src/EventSubscriber/AddressEventSubscriber.php
index 88f600a..f130556 100644
--- a/src/EventSubscriber/AddressEventSubscriber.php
+++ b/src/EventSubscriber/AddressEventSubscriber.php
@@ -59,7 +59,7 @@ class AddressEventSubscriber implements EventSubscriberInterface {
   /**
    * {@inheritdoc}
    */
-  public static function getSubscribedEvents() {
+  public static function getSubscribedEvents(): array {
 
     $events = [];
 
diff --git a/src/Form/QuickNodeCloneNodeForm.php b/src/Form/QuickNodeCloneNodeForm.php
index 2715f48..e4ddece 100644
--- a/src/Form/QuickNodeCloneNodeForm.php
+++ b/src/Form/QuickNodeCloneNodeForm.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\quick_node_clone\Form;
 
+use Drupal\Component\Utility\DeprecationHelper;
 use Drupal\Core\Form\FormStateInterface;
 
 /**
@@ -45,7 +46,7 @@ class QuickNodeCloneNodeForm extends NodeForm {
       'link' => $node_link,
     ];
     $t_args = [
-      '@type' => node_get_type_label($node),
+      '@type' => DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => $node->getBundleEntity()->label(), fn() => node_get_type_label($node)),
       '%title' => $node->label(),
     ];
 
diff --git a/src/Hook/QuickNodeCloneHooks.php b/src/Hook/QuickNodeCloneHooks.php
new file mode 100644
index 0000000..25c036d
--- /dev/null
+++ b/src/Hook/QuickNodeCloneHooks.php
@@ -0,0 +1,165 @@
+<?php
+
+namespace Drupal\quick_node_clone\Hook;
+
+use Drupal\group\Plugin\Group\Relation\GroupRelationTypeManagerInterface;
+use Drupal\group\Entity\GroupRelationship;
+use Drupal\group\Entity\GroupContent;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\node\NodeInterface;
+use Drupal\Core\Entity\ContentEntityFormInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for quick_node_clone.
+ */
+class QuickNodeCloneHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public static function entityTypeBuild(array &$entity_types) {
+    if (isset($entity_types['node'])) {
+      $entity_types['node']->setFormClass('quick_node_clone', 'Drupal\quick_node_clone\Form\QuickNodeCloneNodeForm');
+    }
+  }
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $entity) {
+    $operations = [];
+    // Only add an operation for node entities.
+    if ($entity->getEntityTypeId() !== 'node') {
+      return $operations;
+    }
+    if ($entity->access('clone')) {
+      $operations['quick_clone'] = [
+        'title' => $this->t('Clone'),
+        'weight' => '100',
+        'url' => Url::fromRoute('quick_node_clone.node.quick_clone', [
+          'node' => $entity->id(),
+        ]),
+      ];
+    }
+    return $operations;
+  }
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public static function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.quick_node_clone':
+        $text = file_get_contents(dirname(__FILE__) . '/README.md');
+        if (!\Drupal::moduleHandler()->moduleExists('markdown')) {
+          return '<pre>' . Html::escape($text) . '</pre>';
+        }
+        else {
+          // Use the Markdown filter to render the README.
+          $filter_manager = \Drupal::service('plugin.manager.filter');
+          $settings = \Drupal::configFactory()->get('markdown.settings')->getRawData();
+          $config = [
+            'settings' => $settings,
+          ];
+          $filter = $filter_manager->createInstance('markdown', $config);
+          return $filter->process($text, 'en');
+        }
+    }
+    return NULL;
+  }
+
+  /**
+   * Implements hook_form_alter().
+   */
+  #[Hook('form_alter')]
+  public static function formAlter(&$form, FormStateInterface $form_state, $form_id) {
+    if (!array_key_exists('footer', $form)) {
+      return;
+    }
+    // Check the operation set on the form is 'quick_node_clone'.
+    $form_object = $form_state->getFormObject();
+    if (!$form_object instanceof ContentEntityFormInterface) {
+      return;
+    }
+    if ($form_object->getOperation() !== 'quick_node_clone') {
+      return;
+    }
+    // Check that the content entity being cloned is moderated.
+    if (!\Drupal::moduleHandler()->moduleExists('content_moderation')) {
+      return;
+    }
+    $moderation_info = \Drupal::service('content_moderation.moderation_information');
+    if (!$moderation_info->isModeratedEntity($form_object->getEntity())) {
+      return;
+    }
+    $form['moderation_state']['#group'] = 'footer';
+  }
+
+  /**
+   * Implements hook_node_access().
+   */
+  #[Hook('node_access')]
+  public static function nodeAccess(NodeInterface $node, $operation, AccountInterface $account) {
+    // Check if the operation is 'clone'.
+    if ($operation != 'clone') {
+      return AccessResult::neutral();
+    }
+    $bundle = $node->bundle();
+    $permissions = [
+      "clone {$bundle} content",
+    ];
+    $is_owner = $node->getOwnerId() == $account->id();
+    if ($is_owner) {
+      $permissions[] = "clone own {$bundle} content";
+    }
+    $access = AccessResult::allowedIfHasPermissions($account, $permissions, 'OR');
+    if (!$access->isAllowed()) {
+      return AccessResult::neutral();
+    }
+    if (\Drupal::moduleHandler()->moduleExists('gnode')) {
+      // Check that user has permission to create a relationship.
+      // Support for group module version 1.x.
+      if (class_exists(GroupContent::class)) {
+        $group_relationships = GroupContent::loadByEntity($node);
+        foreach ($group_relationships as $group_relationship) {
+          $access = $group_relationship->getContentPlugin()->createEntityAccess($group_relationship->getGroup(), $account);
+          if ($access->isAllowed()) {
+            return AccessResult::allowed();
+          }
+        }
+      }
+      else {
+        $group_relationships = GroupRelationship::loadByEntity($node);
+        $relation_type_manager = \Drupal::service('group_relation_type.manager');
+        assert($relation_type_manager instanceof GroupRelationTypeManagerInterface);
+        foreach ($group_relationships as $group_relationship) {
+          $access_handler = $relation_type_manager->getAccessControlHandler($group_relationship->getPluginId());
+          $access = $access_handler->entityCreateAccess($group_relationship->getGroup(), $account);
+          if ($access) {
+            return AccessResult::allowed();
+          }
+        }
+      }
+    }
+    // Only check global access if we there is no group module enabled, or
+    // content does not have group(s).
+    if (empty($group_relationships) && $node->access('create')) {
+      return AccessResult::allowed();
+    }
+    // Default to Drupal's normal access handling.
+    return AccessResult::neutral();
+  }
+
+}
diff --git a/src/Hook/QuickNodeCloneViewsHooks.php b/src/Hook/QuickNodeCloneViewsHooks.php
new file mode 100644
index 0000000..22e228a
--- /dev/null
+++ b/src/Hook/QuickNodeCloneViewsHooks.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\quick_node_clone\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for quick_node_clone.
+ */
+class QuickNodeCloneViewsHooks {
+  use StringTranslationTrait;
+  /**
+   * @file
+   * Provide views data for quick_node_clone.module.
+   */
+
+  /**
+   * Implements hook_views_data_alter().
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(&$data) {
+    $data['node']['clone_link'] = [
+      'field' => [
+        'title' => $this->t('Add clone link'),
+        'help' => $this->t('Provide a clone link to the Content.'),
+        'id' => 'quick_node_clone_link',
+      ],
+    ];
+  }
+
+}
diff --git a/tests/src/Functional/QuickNodeCloneExcludeNodeFieldsTest.php b/tests/src/Functional/QuickNodeCloneExcludeNodeFieldsTest.php
index 4480356..562233e 100644
--- a/tests/src/Functional/QuickNodeCloneExcludeNodeFieldsTest.php
+++ b/tests/src/Functional/QuickNodeCloneExcludeNodeFieldsTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\quick_node_clone\Functional;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\Tests\BrowserTestBase;
 
 /**
@@ -9,6 +11,8 @@ use Drupal\Tests\BrowserTestBase;
  *
  * @group Quick Node Clone
  */
+#[Group('Quick Node Clone')]
+#[RunTestsInSeparateProcesses]
 class QuickNodeCloneExcludeNodeFieldsTest extends BrowserTestBase {
 
   /**
diff --git a/tests/src/Functional/QuickNodeCloneGroupIntegrationTest.php b/tests/src/Functional/QuickNodeCloneGroupIntegrationTest.php
index 9780cbd..74918a1 100644
--- a/tests/src/Functional/QuickNodeCloneGroupIntegrationTest.php
+++ b/tests/src/Functional/QuickNodeCloneGroupIntegrationTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\quick_node_clone\Functional;
 
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\Tests\node\Traits\NodeCreationTrait;
 use Drupal\group\Entity\Group;
@@ -18,6 +19,7 @@ use Drupal\group\PermissionScopeInterface;
  *
  * @group group_integration
  */
+#[RunTestsInSeparateProcesses]
 class QuickNodeCloneGroupIntegrationTest extends BrowserTestBase {
 
   use NodeCreationTrait;
diff --git a/tests/src/Functional/QuickNodeCloneTest.php b/tests/src/Functional/QuickNodeCloneTest.php
index f18cb66..7182388 100644
--- a/tests/src/Functional/QuickNodeCloneTest.php
+++ b/tests/src/Functional/QuickNodeCloneTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\quick_node_clone\Functional;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\Tests\BrowserTestBase;
 
 /**
@@ -9,6 +11,8 @@ use Drupal\Tests\BrowserTestBase;
  *
  * @group Quick Node Clone
  */
+#[Group('Quick Node Clone')]
+#[RunTestsInSeparateProcesses]
 class QuickNodeCloneTest extends BrowserTestBase {
 
   /**
