diff --git a/samples.info.yml b/samples.info.yml
index 09342bc..78bdbd8 100644
--- a/samples.info.yml
+++ b/samples.info.yml
@@ -2,7 +2,7 @@ name: Sample content
 description: Create restricted content in production for demonstration and development purposes.
 
 type: module
-core_version_requirement: ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 
 dependencies:
   - drupal:node
diff --git a/samples.install b/samples.install
index 11f6e02..abaf09f 100644
--- a/samples.install
+++ b/samples.install
@@ -4,7 +4,8 @@
  * @file
  * Installs the sample content module.
  */
-
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\node\NodeAccessRebuild;
 use Drupal\Core\Url;
 
 /**
@@ -18,7 +19,7 @@ function samples_install($is_syncing) {
   // nodes access can take a very long time.
   $node_count = (int) \Drupal::entityQuery('node')->accessCheck(FALSE)->count()->execute();
   if ($node_count === 0) {
-    node_access_rebuild();
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->rebuild(), fn() => node_access_rebuild());
   }
   else {
     \Drupal::messenger()->addWarning(t("You must now rebuild your site's node access permissions. <a href=\":rebuild_url\">Rebuild permissions.</a>", [
diff --git a/samples.module b/samples.module
index ef252a7..7fa77e1 100644
--- a/samples.module
+++ b/samples.module
@@ -7,6 +7,8 @@
 
 declare(strict_types=1);
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\samples\Hook\SamplesHooks;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Access\AccessResultReasonInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
@@ -19,47 +21,35 @@ use Drupal\samples\EntityDecorator\Node;
 /**
  * Implements hook_node_access_records().
  */
-function samples_node_access_records(NodeInterface $node): ?array {
-  $samples_node_access = \Drupal::service('samples.node_access');
-  assert($samples_node_access instanceof NodeAccess);
-  return $samples_node_access->getAccessRecords($node);
+#[LegacyHook]
+function samples_node_access_records(NodeInterface $node): ?array
+{
+    return \Drupal::service(SamplesHooks::class)->nodeAccessRecords($node);
 }
 
 /**
  * Implements hook_node_grants().
  */
-function samples_node_grants(AccountInterface $account, $op): array {
-  $samples_node_access = \Drupal::service('samples.node_access');
-  assert($samples_node_access instanceof NodeAccess);
-  return $samples_node_access->getGrants($account, $op);
+#[LegacyHook]
+function samples_node_grants(AccountInterface $account, $op): array
+{
+    return \Drupal::service(SamplesHooks::class)->nodeGrants($account, $op);
 }
 
 /**
  * Implements hook_entity_base_field_info().
  */
-function samples_entity_base_field_info(EntityTypeInterface $entity_type): array {
-  $fields = [];
-  if ($entity_type->id() === 'node') {
-    $fields['samples_status'] = Node::getSamplesStatusBaseFieldDefinition();
-  }
-  return $fields;
+#[LegacyHook]
+function samples_entity_base_field_info(EntityTypeInterface $entity_type): array
+{
+    return \Drupal::service(SamplesHooks::class)->entityBaseFieldInfo($entity_type);
 }
 
 /**
  * Implements hook_entity_field_access().
  */
-function samples_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account): AccessResult {
-  // Determines if this hook applies to the given field.
-  $concerned = $operation === 'edit' && $field_definition->getName() === 'samples_status' && $field_definition->getTargetEntityTypeId() === 'node';
-  $permission = 'administer content samples';
-  // If this module is concerned with the field and the user does not have
-  // permission to edit it, then forbid access to the field. Otherwise, the
-  // result will be neutral so that access will be determined by the entity
-  // access handler or other field access hooks.
-  $concerned_and_missing_permission = $concerned && !$account->hasPermission($permission);
-  $access_result = AccessResult::forbiddenIf($concerned_and_missing_permission)->addCacheContexts(['user.permissions']);
-  if ($access_result->isForbidden() && $access_result instanceof AccessResultReasonInterface) {
-    $access_result->setReason("The '$permission' permission is required.");
-  }
-  return $access_result;
+#[LegacyHook]
+function samples_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account): AccessResult
+{
+    return \Drupal::service(SamplesHooks::class)->entityFieldAccess($operation, $field_definition, $account);
 }
diff --git a/samples.services.yml b/samples.services.yml
index 661802a..73b5068 100644
--- a/samples.services.yml
+++ b/samples.services.yml
@@ -5,3 +5,7 @@ services:
     arguments:
       - '@entity_type.manager'
       - '@entity_type.bundle.info'
+
+  Drupal\samples\Hook\SamplesHooks:
+    class: Drupal\samples\Hook\SamplesHooks
+    autowire: true
diff --git a/src/Hook/SamplesHooks.php b/src/Hook/SamplesHooks.php
new file mode 100644
index 0000000..2dd49ad
--- /dev/null
+++ b/src/Hook/SamplesHooks.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Drupal\samples\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\AccessResultReasonInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\node\NodeInterface;
+use Drupal\samples\Access\NodeAccess;
+use Drupal\samples\EntityDecorator\Node;
+use Drupal\Core\Hook\Attribute\Hook;
+/**
+ * Hook implementations for samples.
+ */
+class SamplesHooks
+{
+    /**
+     * Implements hook_node_access_records().
+     */
+    #[Hook('node_access_records')]
+    public static function nodeAccessRecords(\Drupal\node\NodeInterface $node): ?array
+    {
+        $samples_node_access = \Drupal::service('samples.node_access');
+        assert($samples_node_access instanceof \Drupal\samples\Access\NodeAccess);
+        return $samples_node_access->getAccessRecords($node);
+    }
+    /**
+     * Implements hook_node_grants().
+     */
+    #[Hook('node_grants')]
+    public static function nodeGrants(\Drupal\Core\Session\AccountInterface $account, $op): array
+    {
+        $samples_node_access = \Drupal::service('samples.node_access');
+        assert($samples_node_access instanceof \Drupal\samples\Access\NodeAccess);
+        return $samples_node_access->getGrants($account, $op);
+    }
+    /**
+     * Implements hook_entity_base_field_info().
+     */
+    #[Hook('entity_base_field_info')]
+    public static function entityBaseFieldInfo(\Drupal\Core\Entity\EntityTypeInterface $entity_type): array
+    {
+        $fields = [
+        ];
+        if ($entity_type->id() === 'node') {
+            $fields['samples_status'] = \Drupal\samples\EntityDecorator\Node::getSamplesStatusBaseFieldDefinition();
+        }
+        return $fields;
+    }
+    /**
+     * Implements hook_entity_field_access().
+     */
+    #[Hook('entity_field_access')]
+    public static function entityFieldAccess($operation, \Drupal\Core\Field\FieldDefinitionInterface $field_definition, \Drupal\Core\Session\AccountInterface $account): \Drupal\Core\Access\AccessResult
+    {
+        // Determines if this hook applies to the given field.
+        $concerned = $operation === 'edit' && $field_definition->getName() === 'samples_status' && $field_definition->getTargetEntityTypeId() === 'node';
+        $permission = 'administer content samples';
+        // If this module is concerned with the field and the user does not have
+        // permission to edit it, then forbid access to the field. Otherwise, the
+        // result will be neutral so that access will be determined by the entity
+        // access handler or other field access hooks.
+        $concerned_and_missing_permission = $concerned && !$account->hasPermission($permission);
+        $access_result = \Drupal\Core\Access\AccessResult::forbiddenIf($concerned_and_missing_permission)->addCacheContexts([
+            'user.permissions',
+        ]);
+        if ($access_result->isForbidden() && $access_result instanceof \Drupal\Core\Access\AccessResultReasonInterface) {
+            $access_result->setReason("The '{$permission}' permission is required.");
+        }
+        return $access_result;
+    }
+}
diff --git a/tests/src/Functional/SampleContentTest.php b/tests/src/Functional/SampleContentTest.php
index 021c814..aae72d1 100644
--- a/tests/src/Functional/SampleContentTest.php
+++ b/tests/src/Functional/SampleContentTest.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
 
 namespace Drupal\Tests\samples\Functional;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\node\Entity\NodeType;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\Tests\user\Traits\UserCreationTrait;
@@ -14,6 +16,8 @@ use Drupal\user\UserInterface;
  *
  * @group samples
  */
+#[Group('samples')]
+#[RunTestsInSeparateProcesses]
 class SampleContentTest extends BrowserTestBase {
 
   use UserCreationTrait;
