diff --git a/node_view_permissions.info.yml b/node_view_permissions.info.yml
index 83c7200..0d69dc5 100644
--- a/node_view_permissions.info.yml
+++ b/node_view_permissions.info.yml
@@ -2,6 +2,6 @@ name: Node view permissions
 description: Enables permissions "View own content" and "View any content" for each content type.
 package: Access control
 type: module
-core_version_requirement: ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 dependencies:
 - drupal:node
diff --git a/node_view_permissions.install b/node_view_permissions.install
index 79f1b29..0fcc678 100644
--- a/node_view_permissions.install
+++ b/node_view_permissions.install
@@ -5,6 +5,8 @@
  * Contains install and update functions for node_view_permissions.
  */
 
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\node\NodeAccessRebuild;
 use Drupal\node\Entity\NodeType;
 use Drupal\user\Entity\Role;
 use Drupal\user\RoleInterface;
@@ -13,7 +15,7 @@ use Drupal\user\RoleInterface;
  * Implements hook_install().
  */
 function node_view_permissions_install() {
-  node_access_needs_rebuild(TRUE);
+  DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->setNeedsRebuild(TRUE), fn() => node_access_needs_rebuild(TRUE));
 
   // Add 'view any [node_type] content' permission
   // for anonymous and connected users.
@@ -35,12 +37,12 @@ function node_view_permissions_install() {
  * Implements hook_uninstall().
  */
 function node_view_permissions_uninstall() {
-  node_access_needs_rebuild(TRUE);
+  DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->setNeedsRebuild(TRUE), fn() => node_access_needs_rebuild(TRUE));
 }
 
 /**
  * Implements hook_update_N().
  */
 function node_view_permissions_update_8001() {
-  node_access_needs_rebuild(TRUE);
+  DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->setNeedsRebuild(TRUE), fn() => node_access_needs_rebuild(TRUE));
 }
diff --git a/node_view_permissions.module b/node_view_permissions.module
index 224485e..b51c4e9 100644
--- a/node_view_permissions.module
+++ b/node_view_permissions.module
@@ -7,162 +7,23 @@
  * @see \Drupal\node\Tests\NodeAccessBaseTableTest
  */
 
-use Drupal\Core\Language\Language;
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\node_view_permissions\Hook\NodeViewPermissionsHooks;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\node\Entity\NodeType;
 use Drupal\node\NodeInterface;
 
 /**
  * Implements hook_node_access_records().
  */
+#[LegacyHook]
 function node_view_permissions_node_access_records(NodeInterface $node) {
-  $grants = [];
-  // We don't want to override view published permissions.
-  $languages = $node->getTranslationLanguages();
-  foreach ($languages as $langcode => $language) {
-    if ($langcode === Language::LANGCODE_NOT_APPLICABLE || $langcode === Language::LANGCODE_NOT_SPECIFIED) {
-      // If node is not translatable, check the publish status of it.
-      if ($node->isPublished()) {
-        $grants[] = [
-          'realm' => "view_any_{$node->getType()}_content",
-          'gid' => 1,
-          'grant_view' => 1,
-          'grant_update' => 0,
-          'grant_delete' => 0,
-          'priority' => 0,
-        ];
-        $grants[] = [
-          'realm' => "view_own_{$node->getType()}_content",
-          'gid' => $node->getOwnerId(),
-          'grant_view' => 1,
-          'grant_update' => 0,
-          'grant_delete' => 0,
-          'priority' => 0,
-        ];
-      } else {
-        $grants[] = [
-          'realm' => "view_any_unpublished_{$node->getType()}_content",
-          'gid' => 1,
-          'grant_view' => 1,
-          'grant_update' => 0,
-          'grant_delete' => 0,
-          'priority' => 0,
-        ];
-        $grants[] = [
-          'realm' => "view_own_unpublished_{$node->getType()}_content",
-          'gid' => $node->getOwnerId(),
-          'grant_view' => 1,
-          'grant_update' => 0,
-          'grant_delete' => 0,
-          'priority' => 0,
-        ];
-      }
-    }
-    else {
-      // If node is translated, check the publish status of the translation
-      // and create separate realm for it.
-      $translation = $node->getTranslation($langcode);
-      if ($translation->isPublished()) {
-        $grants[] = [
-          'realm' => "view_any_{$node->getType()}_{$langcode}_content",
-          'langcode' => $langcode,
-          'gid' => 1,
-          'grant_view' => 1,
-          'grant_update' => 0,
-          'grant_delete' => 0,
-          'priority' => 0,
-        ];
-        $grants[] = [
-          'realm' => "view_own_{$node->getType()}_{$langcode}_content",
-          'langcode' => $langcode,
-          'gid' => $node->getOwnerId(),
-          'grant_view' => 1,
-          'grant_update' => 0,
-          'grant_delete' => 0,
-          'priority' => 0,
-        ];
-      } else {
-        $grants[] = [
-          'realm' => "view_any_unpublished_{$node->getType()}_{$langcode}_content",
-          'langcode' => $langcode,
-          'gid' => 1,
-          'grant_view' => 1,
-          'grant_update' => 0,
-          'grant_delete' => 0,
-          'priority' => 0,
-        ];
-        $grants[] = [
-          'realm' => "view_own_unpublished_{$node->getType()}_{$langcode}_content",
-          'langcode' => $langcode,
-          'gid' => $node->getOwnerId(),
-          'grant_view' => 1,
-          'grant_update' => 0,
-          'grant_delete' => 0,
-          'priority' => 0,
-        ];
-      }
-    }
-  }
-
-  return $grants;
+  return \Drupal::service(NodeViewPermissionsHooks::class)->nodeAccessRecords($node);
 }
 
 /**
  * Implements hook_node_grants().
  */
+#[LegacyHook]
 function node_view_permissions_node_grants(AccountInterface $account, $op) {
-  $static_cache = &drupal_static(__FUNCTION__, []);
-  $cache_key = $account->id() . ':' . $op;
-  if (isset($static_cache[$cache_key])) {
-    return $static_cache[$cache_key];
-  }
-
-  $grants = [];
-  if ($op == 'view') {
-    $languages = \Drupal::languageManager()->getLanguages();
-    $node_types = NodeType::loadMultiple();
-    $account_id = $account->id();
-    $is_authenticated = ($account_id != 0);
-    $view_any_unpublished = $account->hasPermission("view any unpublished content");
-    $view_own_unpublished = $account->hasPermission("view own unpublished content");
-
-    foreach ($node_types as $type) {
-      $type_id = $type->id();
-      $view_any = $account->hasPermission("view any $type_id content");
-      $view_own = $account->hasPermission("view own $type_id content") && $is_authenticated;
-
-      // Language-independent grants (set once per type).
-      if ($view_any) {
-        $grants["view_any_{$type_id}_content"] = [1];
-      }
-      if ($view_own) {
-        $grants["view_own_{$type_id}_content"] = [$account_id];
-      }
-      if ($view_any_unpublished && $view_any) {
-        $grants["view_any_unpublished_{$type_id}_content"] = [1];
-      }
-      if ($view_own_unpublished && $view_own) {
-        $grants["view_own_unpublished_{$type_id}_content"] = [$account_id];
-      }
-
-      // Language-specific grants (reuse cached permission results).
-      foreach ($languages as $langcode => $language) {
-        if ($view_any) {
-          $grants["view_any_{$type_id}_{$langcode}_content"] = [1];
-        }
-        if ($view_own) {
-          $grants["view_own_{$type_id}_{$langcode}_content"] = [$account_id];
-        }
-        if ($view_any_unpublished && $view_any) {
-          $grants["view_any_unpublished_{$type_id}_{$langcode}_content"] = [1];
-        }
-        if ($view_own_unpublished && $view_own) {
-          $grants["view_own_unpublished_{$type_id}_{$langcode}_content"] = [$account_id];
-        }
-      }
-    }
-  }
-
-  $static_cache[$cache_key] = $grants;
-  return $grants;
+  return \Drupal::service(NodeViewPermissionsHooks::class)->nodeGrants($account, $op);
 }
diff --git a/node_view_permissions.services.yml b/node_view_permissions.services.yml
new file mode 100644
index 0000000..4054c2d
--- /dev/null
+++ b/node_view_permissions.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\node_view_permissions\Hook\NodeViewPermissionsHooks:
+    class: Drupal\node_view_permissions\Hook\NodeViewPermissionsHooks
+    autowire: true
diff --git a/src/Hook/NodeViewPermissionsHooks.php b/src/Hook/NodeViewPermissionsHooks.php
new file mode 100644
index 0000000..6d850fe
--- /dev/null
+++ b/src/Hook/NodeViewPermissionsHooks.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace Drupal\node_view_permissions\Hook;
+
+use Drupal\node\Entity\NodeType;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Language\Language;
+use Drupal\node\NodeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for node_view_permissions.
+ */
+class NodeViewPermissionsHooks {
+
+  /**
+   * Implements hook_node_access_records().
+   */
+  #[Hook('node_access_records')]
+  public static function nodeAccessRecords(NodeInterface $node) {
+    $grants = [];
+    // We don't want to override view published permissions.
+    $languages = $node->getTranslationLanguages();
+    foreach ($languages as $langcode => $language) {
+      if ($langcode === Language::LANGCODE_NOT_APPLICABLE || $langcode === Language::LANGCODE_NOT_SPECIFIED) {
+        // If node is not translatable, check the publish status of it.
+        if ($node->isPublished()) {
+          $grants[] = [
+            'realm' => "view_any_{$node->getType()}_content",
+            'gid' => 1,
+            'grant_view' => 1,
+            'grant_update' => 0,
+            'grant_delete' => 0,
+            'priority' => 0,
+          ];
+          $grants[] = [
+            'realm' => "view_own_{$node->getType()}_content",
+            'gid' => $node->getOwnerId(),
+            'grant_view' => 1,
+            'grant_update' => 0,
+            'grant_delete' => 0,
+            'priority' => 0,
+          ];
+        }
+        else {
+          $grants[] = [
+            'realm' => "view_any_unpublished_{$node->getType()}_content",
+            'gid' => 1,
+            'grant_view' => 1,
+            'grant_update' => 0,
+            'grant_delete' => 0,
+            'priority' => 0,
+          ];
+          $grants[] = [
+            'realm' => "view_own_unpublished_{$node->getType()}_content",
+            'gid' => $node->getOwnerId(),
+            'grant_view' => 1,
+            'grant_update' => 0,
+            'grant_delete' => 0,
+            'priority' => 0,
+          ];
+        }
+      }
+      else {
+        // If node is translated, check the publish status of the translation
+        // and create separate realm for it.
+        $translation = $node->getTranslation($langcode);
+        if ($translation->isPublished()) {
+          $grants[] = [
+            'realm' => "view_any_{$node->getType()}_{$langcode}_content",
+            'langcode' => $langcode,
+            'gid' => 1,
+            'grant_view' => 1,
+            'grant_update' => 0,
+            'grant_delete' => 0,
+            'priority' => 0,
+          ];
+          $grants[] = [
+            'realm' => "view_own_{$node->getType()}_{$langcode}_content",
+            'langcode' => $langcode,
+            'gid' => $node->getOwnerId(),
+            'grant_view' => 1,
+            'grant_update' => 0,
+            'grant_delete' => 0,
+            'priority' => 0,
+          ];
+        }
+        else {
+          $grants[] = [
+            'realm' => "view_any_unpublished_{$node->getType()}_{$langcode}_content",
+            'langcode' => $langcode,
+            'gid' => 1,
+            'grant_view' => 1,
+            'grant_update' => 0,
+            'grant_delete' => 0,
+            'priority' => 0,
+          ];
+          $grants[] = [
+            'realm' => "view_own_unpublished_{$node->getType()}_{$langcode}_content",
+            'langcode' => $langcode,
+            'gid' => $node->getOwnerId(),
+            'grant_view' => 1,
+            'grant_update' => 0,
+            'grant_delete' => 0,
+            'priority' => 0,
+          ];
+        }
+      }
+    }
+    return $grants;
+  }
+
+  /**
+   * Implements hook_node_grants().
+   */
+  #[Hook('node_grants')]
+  public static function nodeGrants(AccountInterface $account, $op) {
+    $static_cache =& drupal_static('node_view_permissions_node_grants', []);
+    $cache_key = $account->id() . ':' . $op;
+    if (isset($static_cache[$cache_key])) {
+      return $static_cache[$cache_key];
+    }
+    $grants = [];
+    if ($op == 'view') {
+      $languages = \Drupal::languageManager()->getLanguages();
+      $node_types = NodeType::loadMultiple();
+      $account_id = $account->id();
+      $is_authenticated = $account_id != 0;
+      $view_any_unpublished = $account->hasPermission("view any unpublished content");
+      $view_own_unpublished = $account->hasPermission("view own unpublished content");
+      foreach ($node_types as $type) {
+        $type_id = $type->id();
+        $view_any = $account->hasPermission("view any {$type_id} content");
+        $view_own = $account->hasPermission("view own {$type_id} content") && $is_authenticated;
+        // Language-independent grants (set once per type).
+        if ($view_any) {
+          $grants["view_any_{$type_id}_content"] = [
+            1,
+          ];
+        }
+        if ($view_own) {
+          $grants["view_own_{$type_id}_content"] = [
+            $account_id,
+          ];
+        }
+        if ($view_any_unpublished && $view_any) {
+          $grants["view_any_unpublished_{$type_id}_content"] = [
+            1,
+          ];
+        }
+        if ($view_own_unpublished && $view_own) {
+          $grants["view_own_unpublished_{$type_id}_content"] = [
+            $account_id,
+          ];
+        }
+        // Language-specific grants (reuse cached permission results).
+        foreach ($languages as $langcode => $language) {
+          if ($view_any) {
+            $grants["view_any_{$type_id}_{$langcode}_content"] = [
+              1,
+            ];
+          }
+          if ($view_own) {
+            $grants["view_own_{$type_id}_{$langcode}_content"] = [
+              $account_id,
+            ];
+          }
+          if ($view_any_unpublished && $view_any) {
+            $grants["view_any_unpublished_{$type_id}_{$langcode}_content"] = [
+              1,
+            ];
+          }
+          if ($view_own_unpublished && $view_own) {
+            $grants["view_own_unpublished_{$type_id}_{$langcode}_content"] = [
+              $account_id,
+            ];
+          }
+        }
+      }
+    }
+    $static_cache[$cache_key] = $grants;
+    return $grants;
+  }
+
+}
diff --git a/tests/src/Functional/NodeViewPermissionsTest.php b/tests/src/Functional/NodeViewPermissionsTest.php
index d5c496e..3067c55 100644
--- a/tests/src/Functional/NodeViewPermissionsTest.php
+++ b/tests/src/Functional/NodeViewPermissionsTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\node_view_permissions\Functional;
 
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\node\NodeAccessRebuild;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Url;
 use Drupal\Tests\BrowserTestBase;
@@ -35,7 +37,7 @@ class NodeViewPermissionsTest extends BrowserTestBase {
       'name' => 'Article',
     ]);
 
-    node_access_rebuild();
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->rebuild(), fn() => node_access_rebuild());
   }
 
   /**
@@ -114,7 +116,7 @@ class NodeViewPermissionsTest extends BrowserTestBase {
       'uid' => 0,
     ]);
 
-    node_access_rebuild();
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->rebuild(), fn() => node_access_rebuild());
 
     $this->drupalGet(Url::fromRoute('entity.node.canonical', [
       'node' => $node->id(),
