diff --git a/node_read_time.info.yml b/node_read_time.info.yml
index 4013a57..2f40004 100644
--- a/node_read_time.info.yml
+++ b/node_read_time.info.yml
@@ -1,6 +1,6 @@
 type: module
 name: Node Read Time
 description: 'Displays reading time field on a node.'
-core_version_requirement: ^8 || ^9 || ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 package: Content
 configure: system.admin_config_reading_time
diff --git a/node_read_time.install b/node_read_time.install
index ae6105d..79a7417 100644
--- a/node_read_time.install
+++ b/node_read_time.install
@@ -5,6 +5,7 @@
  * Install/update/uninstall hooks for node_read_time module.
  */
 
+use Drupal\Component\Utility\DeprecationHelper;
 use Drupal\Core\Field\BaseFieldDefinition;
 
 /**
@@ -44,7 +45,7 @@ function node_read_time_update_10001() {
       $role->grantPermission('administer node read time');
 
       try {
-        $role->trustData()->save();
+        DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => $role, fn() => $role->trustData())->save();
       }
       catch (\Exception $e) {
       }
diff --git a/node_read_time.module b/node_read_time.module
index 380c39e..15d4b0e 100644
--- a/node_read_time.module
+++ b/node_read_time.module
@@ -5,6 +5,8 @@
  * Reading time module.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\node_read_time\Hook\NodeReadTimeHooks;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -15,129 +17,49 @@ use Drupal\node_read_time\Plugin\Field\NodeReadTime;
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function node_read_time_help($route_name, RouteMatchInterface $route_match) {
-  if ($route_name === 'help.page.node_read_time') {
-    $readme_content = file_get_contents(dirname(__FILE__) . '/README.md');
-    if (\Drupal::moduleHandler()->moduleExists('markdown')) {
-      // Use the Markdown filter to render the README.
-      $filter_manager = \Drupal::service('plugin.manager.filter');
-      $markdown_config = \Drupal::configFactory()->get('markdown.settings')->getRawData();
-      $markdown_settings = ['settings' => $markdown_config];
-      $filter = $filter_manager->createInstance('markdown', $markdown_settings);
-      $readme_content = $filter->process($readme_content, 'en');
-    }
-    else {
-      $readme_content = '<pre>' . $readme_content . '</pre>';
-    }
-    return $readme_content;
-  }
+  return \Drupal::service(NodeReadTimeHooks::class)->help($route_name, $route_match);
 }
 
 /**
  * Implements hook_entity_extra_field_info().
  */
+#[LegacyHook]
 function node_read_time_entity_extra_field_info(): ?array {
-
-  $config = \Drupal::config('node_read_time.settings');
-  $bundles = $config->get('reading_time')['container'];
-  $extra = NULL;
-
-  if ($bundles) {
-    foreach ($bundles as $machine_name => $bundle) {
-      if ($bundle['is_activated']) {
-        $extra['node'][$machine_name]['display']['reading_time'] = [
-          'label' => t('Reading time'),
-          'description' => t('This field is used to display the reading time of a node.'),
-          'weight' => 100,
-          'visible' => FALSE,
-        ];
-      }
-    }
-  }
-
-  return $extra;
+  return \Drupal::service(NodeReadTimeHooks::class)->entityExtraFieldInfo();
 }
 
 /**
  * Implements hook_ENTITY_TYPE_view().
  */
+#[LegacyHook]
 function node_read_time_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode): void {
-  if ($display->getComponent('reading_time')) {
-    $config = \Drupal::config('node_read_time.settings')
-      ->get('reading_time')['container'];
-
-    if ($config[$entity->getType()]['is_activated']) {
-      if ($entity->hasField('node_read_time_override') && !$entity->get('node_read_time_override')->isEmpty()) {
-        $reading_time = $entity->get('node_read_time_override')->getString();
-      }
-      else {
-        // If words per minute is not set, give an average of 225.
-        $words_per_minute = \Drupal::config('node_read_time.settings')->get('reading_time')['words_per_minute'] ?: 225;
-        $reading_time_service = \Drupal::service('node_read_time.reading_time');
-        $reading_time = $reading_time_service
-          ->setWordsPerMinute($words_per_minute)
-          ->collectWords($entity)
-          ->calculateReadingTime()
-          ->getReadingTime();
-
-        // Clear the words variable.
-        $reading_time_service->setWords(0);
-      }
-
-      $build['reading_time'] = [
-        '#theme' => 'reading_time',
-        '#reading_time' => $reading_time,
-      ];
-    }
-
-  }
+  \Drupal::service(NodeReadTimeHooks::class)->nodeView($build, $entity, $display, $view_mode);
 }
 
 /**
  * Implements hook_theme().
  */
+#[LegacyHook]
 function node_read_time_theme(): array {
-  return [
-    'reading_time' => [
-      'variables' => [
-        'reading_time' => NULL,
-        'template' => 'reading-time',
-      ],
-    ],
-  ];
+  return \Drupal::service(NodeReadTimeHooks::class)->theme();
 }
 
 /**
  * Implements hook_views_data_alter().
  */
+#[LegacyHook]
 function node_read_time_views_data_alter(array &$data): void {
-  $data['node_field_data']['node_read_time'] = [
-    'title' => t('Node read time'),
-    'field' => [
-      'title' => t('Node read time'),
-      'help' => t('Node read time, if it is enabled for the node type.'),
-      'id' => 'node_read_time',
-    ],
-  ];
+  \Drupal::service(NodeReadTimeHooks::class)->viewsDataAlter($data);
 }
 
 /**
  * Implements hook_entity_base_field_info().
  */
+#[LegacyHook]
 function node_read_time_entity_base_field_info(EntityTypeInterface $entity_type) {
-  $fields = [];
-
-  if ($entity_type->id() == 'node') {
-    $fields['node_read_time_override'] = BaseFieldDefinition::create('string')
-      ->setLabel(t('Override reading time'))
-      ->setDisplayOptions('form', [
-        'type' => 'string_textfield',
-      ])
-      ->setDisplayConfigurable('form', TRUE)
-      ->setDisplayConfigurable('view', FALSE);
-  }
-
-  return $fields;
+  return \Drupal::service(NodeReadTimeHooks::class)->entityBaseFieldInfo($entity_type);
 }
 
 /**
diff --git a/node_read_time.services.yml b/node_read_time.services.yml
index e4c19ed..c0df542 100644
--- a/node_read_time.services.yml
+++ b/node_read_time.services.yml
@@ -3,3 +3,7 @@ services:
   node_read_time.reading_time:
     class: Drupal\node_read_time\Calculate\ReadingTime
     arguments: ['@entity_type.manager', '@config.factory']
+
+  Drupal\node_read_time\Hook\NodeReadTimeHooks:
+    class: Drupal\node_read_time\Hook\NodeReadTimeHooks
+    autowire: true
diff --git a/src/Hook/NodeReadTimeHooks.php b/src/Hook/NodeReadTimeHooks.php
new file mode 100644
index 0000000..a9c3c98
--- /dev/null
+++ b/src/Hook/NodeReadTimeHooks.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Drupal\node_read_time\Hook;
+
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for node_read_time.
+ */
+class NodeReadTimeHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public static function help($route_name, RouteMatchInterface $route_match) {
+    if ($route_name === 'help.page.node_read_time') {
+      $readme_content = file_get_contents(dirname(__FILE__) . '/README.md');
+      if (\Drupal::moduleHandler()->moduleExists('markdown')) {
+        // Use the Markdown filter to render the README.
+        $filter_manager = \Drupal::service('plugin.manager.filter');
+        $markdown_config = \Drupal::configFactory()->get('markdown.settings')->getRawData();
+        $markdown_settings = [
+          'settings' => $markdown_config,
+        ];
+        $filter = $filter_manager->createInstance('markdown', $markdown_settings);
+        $readme_content = $filter->process($readme_content, 'en');
+      }
+      else {
+        $readme_content = '<pre>' . $readme_content . '</pre>';
+      }
+      return $readme_content;
+    }
+  }
+
+  /**
+   * Implements hook_entity_extra_field_info().
+   */
+  #[Hook('entity_extra_field_info')]
+  public function entityExtraFieldInfo(): ?array {
+    $config = \Drupal::config('node_read_time.settings');
+    $bundles = $config->get('reading_time')['container'];
+    $extra = NULL;
+    if ($bundles) {
+      foreach ($bundles as $machine_name => $bundle) {
+        if ($bundle['is_activated']) {
+          $extra['node'][$machine_name]['display']['reading_time'] = [
+            'label' => $this->t('Reading time'),
+            'description' => $this->t('This field is used to display the reading time of a node.'),
+            'weight' => 100,
+            'visible' => FALSE,
+          ];
+        }
+      }
+    }
+    return $extra;
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_view().
+   */
+  #[Hook('node_view')]
+  public static function nodeView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode): void {
+    if ($display->getComponent('reading_time')) {
+      $config = \Drupal::config('node_read_time.settings')->get('reading_time')['container'];
+      if ($config[$entity->getType()]['is_activated']) {
+        if ($entity->hasField('node_read_time_override') && !$entity->get('node_read_time_override')->isEmpty()) {
+          $reading_time = $entity->get('node_read_time_override')->getString();
+        }
+        else {
+          // If words per minute is not set, give an average of 225.
+          $words_per_minute = \Drupal::config('node_read_time.settings')->get('reading_time')['words_per_minute'] ?: 225;
+          $reading_time_service = \Drupal::service('node_read_time.reading_time');
+          $reading_time = $reading_time_service->setWordsPerMinute($words_per_minute)->collectWords($entity)->calculateReadingTime()->getReadingTime();
+          // Clear the words variable.
+          $reading_time_service->setWords(0);
+        }
+        $build['reading_time'] = [
+          '#theme' => 'reading_time',
+          '#reading_time' => $reading_time,
+        ];
+      }
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public static function theme(): array {
+    return [
+      'reading_time' => [
+        'variables' => [
+          'reading_time' => NULL,
+          'template' => 'reading-time',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_views_data_alter().
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(array &$data): void {
+    $data['node_field_data']['node_read_time'] = [
+      'title' => $this->t('Node read time'),
+      'field' => [
+        'title' => $this->t('Node read time'),
+        'help' => $this->t('Node read time, if it is enabled for the node type.'),
+        'id' => 'node_read_time',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    $fields = [];
+    if ($entity_type->id() == 'node') {
+      $fields['node_read_time_override'] = BaseFieldDefinition::create('string')->setLabel($this->t('Override reading time'))->setDisplayOptions('form', [
+        'type' => 'string_textfield',
+      ])->setDisplayConfigurable('form', TRUE)->setDisplayConfigurable('view', FALSE);
+    }
+    return $fields;
+  }
+
+}
diff --git a/tests/src/Functional/LoadTest.php b/tests/src/Functional/LoadTest.php
index 54b528f..b1af73b 100644
--- a/tests/src/Functional/LoadTest.php
+++ b/tests/src/Functional/LoadTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\node_read_time\Functional;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\Core\Url;
 use Drupal\Tests\BrowserTestBase;
 
@@ -10,6 +12,8 @@ use Drupal\Tests\BrowserTestBase;
  *
  * @group node_read_time
  */
+#[Group('node_read_time')]
+#[RunTestsInSeparateProcesses]
 class LoadTest extends BrowserTestBase {
 
   /**
