diff --git a/file_replace.info.yml b/file_replace.info.yml
index 5d4be8c..21d40de 100644
--- a/file_replace.info.yml
+++ b/file_replace.info.yml
@@ -1,7 +1,7 @@
 name: File replace
 description: 'Allows to replace files'
 type: module
-core_version_requirement: ^10.3 || ^11
+core_version_requirement: ^10.3 || ^11 || ^12
 package: Media
 dependencies:
   - drupal:file
diff --git a/file_replace.module b/file_replace.module
index e4ac1ae..e8b6f89 100644
--- a/file_replace.module
+++ b/file_replace.module
@@ -5,15 +5,17 @@
  * File replace functionality.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\file_replace\Hook\FileReplaceHooks;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\file\FileInterface;
 
 /**
  * Implements hook_entity_type_build().
  */
+#[LegacyHook]
 function file_replace_entity_type_build(array &$entity_types) {
-  $entity_types['file']->setFormClass('replace', 'Drupal\file_replace\Form\FileReplaceForm');
-  $entity_types['file']->setLinkTemplate('replace-form', '/admin/content/files/replace/{file}');
+  \Drupal::service(FileReplaceHooks::class)->entityTypeBuild($entity_types);
 }
 
 /**
@@ -21,48 +23,23 @@ function file_replace_entity_type_build(array &$entity_types) {
  *
  * Views integration of file_replace.
  */
+#[LegacyHook]
 function file_replace_views_data() {
-  $data['file_managed']['file_replace_link'] = [
-    'field' => [
-      'title' => t('Link to replace file'),
-      'help' => t('Provide a link to replace the file.'),
-      'id' => 'file_replace_link',
-    ],
-  ];
-
-  return $data;
+  return \Drupal::service(FileReplaceHooks::class)->viewsData();
 }
 
 /**
  * Implements hook_file_replace().
  */
+#[LegacyHook]
 function file_replace_file_replace(FileInterface $file) {
-  // Flush image cache.
-  if (\Drupal::moduleHandler()->moduleExists('image')) {
-    if (explode('/', $file->getMimeType())[0] == 'image') {
-      /** @var \Drupal\image\Entity\ImageStyle[] $image_styles */
-      $image_styles = \Drupal::entityTypeManager()->getStorage('image_style')->loadMultiple();
-      foreach ($image_styles as $image_style) {
-        $image_style->flush($file->getFileUri());
-      }
-    }
-  }
+  \Drupal::service(FileReplaceHooks::class)->fileReplace($file);
 }
 
 /**
  * Implements hook_entity_operation().
  */
+#[LegacyHook]
 function file_replace_entity_operation(EntityInterface $entity): array {
-  $operations = [];
-  if ($entity instanceof FileInterface) {
-    $replace_url = $entity->toUrl('replace-form');
-    if ($replace_url->access()) {
-      $operations['replace'] = [
-        'title' => t('Replace'),
-        'url' => $replace_url,
-        'weight' => 100,
-      ];
-    }
-  }
-  return $operations;
+  return \Drupal::service(FileReplaceHooks::class)->entityOperation($entity);
 }
diff --git a/file_replace.services.yml b/file_replace.services.yml
index e6b92c9..3bab40d 100644
--- a/file_replace.services.yml
+++ b/file_replace.services.yml
@@ -4,3 +4,7 @@ services:
     arguments: ['@file.usage']
     tags:
       - { name: access_check, applies_to: _file_replace_access }
+
+  Drupal\file_replace\Hook\FileReplaceHooks:
+    class: Drupal\file_replace\Hook\FileReplaceHooks
+    autowire: true
diff --git a/modules/file_replace_shell_exec/file_replace_shell_exec.info.yml b/modules/file_replace_shell_exec/file_replace_shell_exec.info.yml
index 27f21da..926a45f 100644
--- a/modules/file_replace_shell_exec/file_replace_shell_exec.info.yml
+++ b/modules/file_replace_shell_exec/file_replace_shell_exec.info.yml
@@ -1,6 +1,6 @@
 name: File replace shell exec
 description: 'Execute a shell command when a file is replaced'
 type: module
-core_version_requirement: ^8 || ^9 || ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 dependencies:
   - drupal:file_replace
diff --git a/modules/file_replace_shell_exec/file_replace_shell_exec.module b/modules/file_replace_shell_exec/file_replace_shell_exec.module
index 1ae878b..34de5ef 100644
--- a/modules/file_replace_shell_exec/file_replace_shell_exec.module
+++ b/modules/file_replace_shell_exec/file_replace_shell_exec.module
@@ -5,27 +5,14 @@
  * Contains file_replace_shell_exec functionality.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\file_replace_shell_exec\Hook\FileReplaceShellExecHooks;
 use Drupal\file\FileInterface;
 
 /**
  * Implements hook_file_replace().
  */
+#[LegacyHook]
 function file_replace_shell_exec_file_replace(FileInterface $file) {
-  $command = \Drupal::config('file_replace_shell_exec.settings')->get('command');
-  if (!$command) {
-    return;
-  }
-
-  $shell_exec = shell_exec($command);
-  if ($shell_exec) {
-    \Drupal::logger('file_replace_shell_exec')->info('The command %command was executed with message:' . PHP_EOL . '<pre>%message</pre>', [
-      '%command' => $command,
-      '%message' => $shell_exec,
-    ]);
-  }
-  else {
-    \Drupal::logger('file_replace_shell_exec')->error('The command %command could not be executed.', [
-      '%command' => $command,
-    ]);
-  }
+  \Drupal::service(FileReplaceShellExecHooks::class)->fileReplace($file);
 }
diff --git a/modules/file_replace_shell_exec/file_replace_shell_exec.services.yml b/modules/file_replace_shell_exec/file_replace_shell_exec.services.yml
new file mode 100644
index 0000000..db51ff8
--- /dev/null
+++ b/modules/file_replace_shell_exec/file_replace_shell_exec.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\file_replace_shell_exec\Hook\FileReplaceShellExecHooks:
+    class: Drupal\file_replace_shell_exec\Hook\FileReplaceShellExecHooks
+    autowire: true
diff --git a/modules/file_replace_shell_exec/src/Hook/FileReplaceShellExecHooks.php b/modules/file_replace_shell_exec/src/Hook/FileReplaceShellExecHooks.php
new file mode 100644
index 0000000..a1c2c0f
--- /dev/null
+++ b/modules/file_replace_shell_exec/src/Hook/FileReplaceShellExecHooks.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\file_replace_shell_exec\Hook;
+
+use Drupal\file\FileInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for file_replace_shell_exec.
+ */
+class FileReplaceShellExecHooks {
+
+  /**
+   * Implements hook_file_replace().
+   */
+  #[Hook('file_replace')]
+  public static function fileReplace(FileInterface $file) {
+    $command = \Drupal::config('file_replace_shell_exec.settings')->get('command');
+    if (!$command) {
+      return;
+    }
+    $shell_exec = shell_exec($command);
+    if ($shell_exec) {
+      \Drupal::logger('file_replace_shell_exec')->info('The command %command was executed with message:' . PHP_EOL . '<pre>%message</pre>', [
+        '%command' => $command,
+        '%message' => $shell_exec,
+      ]);
+    }
+    else {
+      \Drupal::logger('file_replace_shell_exec')->error('The command %command could not be executed.', [
+        '%command' => $command,
+      ]);
+    }
+  }
+
+}
diff --git a/src/Hook/FileReplaceHooks.php b/src/Hook/FileReplaceHooks.php
new file mode 100644
index 0000000..ae9635e
--- /dev/null
+++ b/src/Hook/FileReplaceHooks.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\file_replace\Hook;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\file\FileInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for file_replace.
+ */
+class FileReplaceHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public static function entityTypeBuild(array &$entity_types) {
+    $entity_types['file']->setFormClass('replace', 'Drupal\file_replace\Form\FileReplaceForm');
+    $entity_types['file']->setLinkTemplate('replace-form', '/admin/content/files/replace/{file}');
+  }
+
+  /**
+   * Implements hook_views_data().
+   *
+   * Views integration of file_replace.
+   */
+  #[Hook('views_data')]
+  public function viewsData() {
+    $data['file_managed']['file_replace_link'] = [
+      'field' => [
+        'title' => $this->t('Link to replace file'),
+        'help' => $this->t('Provide a link to replace the file.'),
+        'id' => 'file_replace_link',
+      ],
+    ];
+    return $data;
+  }
+
+  /**
+   * Implements hook_file_replace().
+   */
+  #[Hook('file_replace')]
+  public static function fileReplace(FileInterface $file) {
+    // Flush image cache.
+    if (\Drupal::moduleHandler()->moduleExists('image')) {
+      if (explode('/', $file->getMimeType())[0] == 'image') {
+        /** @var \Drupal\image\Entity\ImageStyle[] $image_styles */
+        $image_styles = \Drupal::entityTypeManager()->getStorage('image_style')->loadMultiple();
+        foreach ($image_styles as $image_style) {
+          $image_style->flush($file->getFileUri());
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $entity): array {
+    $operations = [];
+    if ($entity instanceof FileInterface) {
+      $replace_url = $entity->toUrl('replace-form');
+      if ($replace_url->access()) {
+        $operations['replace'] = [
+          'title' => $this->t('Replace'),
+          'url' => $replace_url,
+          'weight' => 100,
+        ];
+      }
+    }
+    return $operations;
+  }
+
+}
