diff --git a/src/Hook/ViewsStringAggregationHooks.php b/src/Hook/ViewsStringAggregationHooks.php
new file mode 100644
index 0000000..34a8d8c
--- /dev/null
+++ b/src/Hook/ViewsStringAggregationHooks.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\views_string_aggregation\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for views_string_aggregation.
+ */
+class ViewsStringAggregationHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.views_string_aggregation':
+        return '<p>' . $this->t('The Views String Aggregation module provides enhanced string aggregation functionality
+         to Views SQL queries.
+         It supports MySQL/MariaDB GROUP_CONCAT, SQLite GROUP_CONCAT
+         and PostgreSQL STRING_AGG functions,
+         allowing users to concatenate string values from multiple rows
+         into a single field in Views results.') . '</p>';
+    }
+  }
+
+  /**
+   * Implements hook_views_plugins_query_alter().
+   *
+   * If supported by the database, this module replaces the default views_query
+   * plugin with an extended version that adds string aggregation. It builds on
+   * the core Sql plugin, preserving all existing functionality while enabling
+   * string aggregation support.
+   *
+   * Note for developers:
+   * To disable this behavior, set the config value 'extend_views_query_plugin'
+   * to FALSE in views_string_aggregation.settings and clear cache.
+   * As per Views, you can then assign a specific query plugin per table
+   * in hook_views_data() or hook_views_data_alter():
+   *
+   *   $views_data['table']['base']['query_id'] = 'vsa_views_query_mysql';
+   *   $views_data['table']['base']['query_id'] = 'vsa_views_query_pgsql';
+   */
+  #[Hook('views_plugins_query_alter')]
+  public static function viewsPluginsQueryAlter(array &$plugins) {
+    // Check configuration setting to see if we
+    // should extend the default views_query plugin.
+    $config = \Drupal::config('views_string_aggregation.settings');
+    if (!$config->get('extend_views_query_plugin')) {
+      \Drupal::logger('views_string_aggregation')->debug('The setting "extend_views_query_plugin" is disabled by configuration.');
+      return;
+    }
+    if (!empty($plugins['views_query'])) {
+      // Add class handler to extend the default views_query plugin,
+      // based on database type.
+      $database_type = \Drupal::database()->databaseType();
+      if (in_array($database_type, [
+        'mysql',
+        'mariadb',
+      ])) {
+        $plugins['views_query']['class'] = 'Drupal\views_string_aggregation\Plugin\views\query\MySql';
+        $plugins['views_query']['provider'] = 'views_string_aggregation';
+      }
+      elseif (in_array($database_type, [
+        'pgsql',
+      ])) {
+        $plugins['views_query']['class'] = 'Drupal\views_string_aggregation\Plugin\views\query\PgSql';
+        $plugins['views_query']['provider'] = 'views_string_aggregation';
+      }
+      elseif (in_array($database_type, [
+        'sqlite',
+      ])) {
+        $plugins['views_query']['class'] = 'Drupal\views_string_aggregation\Plugin\views\query\Sqlite';
+        $plugins['views_query']['provider'] = 'views_string_aggregation';
+      }
+      else {
+        \Drupal::logger('views_string_aggregation')->notice('The database type "@type" is not yet supported.', [
+          '@type' => $database_type,
+        ]);
+      }
+      // @todo other databases with string aggregation support.
+    }
+  }
+
+  /**
+   * Implements hook_config_schema_info_alter().
+   */
+  #[Hook('config_schema_info_alter')]
+  public static function configSchemaInfoAlter(&$definitions) {
+    // Extend the views query options schema with our custom options.
+    if (isset($definitions['views.query.views_query'])) {
+      $definitions['views.query.views_query']['mapping']['vsa_separator'] = [
+        'type' => 'string',
+        'label' => 'Views String Aggregation separator',
+        'nullable' => TRUE,
+      ];
+      $definitions['views.query.views_query']['mapping']['vsa_order_by'] = [
+        'type' => 'string',
+        'label' => 'Views String Aggregation order by field',
+        'nullable' => TRUE,
+      ];
+      $definitions['views.query.views_query']['mapping']['vsa_order_direction'] = [
+        'type' => 'string',
+        'label' => 'Views String Aggregation order direction',
+        'nullable' => TRUE,
+      ];
+      $definitions['views.query.views_query']['mapping']['vsa_max_length'] = [
+        'type' => 'string',
+        'label' => 'Views String Aggregation maximum string length (e.g group_concat_max_len)',
+        'nullable' => TRUE,
+      ];
+    }
+  }
+
+}
diff --git a/views_string_aggregation.info.yml b/views_string_aggregation.info.yml
index 73d57d9..0334168 100644
--- a/views_string_aggregation.info.yml
+++ b/views_string_aggregation.info.yml
@@ -2,6 +2,6 @@ name: Views String Aggregation
 type: module
 description: 'Provides Views database specific SQL level string aggregation support.'
 package: Views
-core_version_requirement: ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 dependencies:
   - drupal:views
diff --git a/views_string_aggregation.module b/views_string_aggregation.module
index 168bd22..475ef93 100644
--- a/views_string_aggregation.module
+++ b/views_string_aggregation.module
@@ -7,24 +7,16 @@
  * Adds enhanced string aggregation functionality to Views SQL.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\views_string_aggregation\Hook\ViewsStringAggregationHooks;
 use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function views_string_aggregation_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.views_string_aggregation':
-      return '<p>' .
-      t(
-        'The Views String Aggregation module provides enhanced string aggregation functionality
-         to Views SQL queries.
-         It supports MySQL/MariaDB GROUP_CONCAT, SQLite GROUP_CONCAT
-         and PostgreSQL STRING_AGG functions,
-         allowing users to concatenate string values from multiple rows
-         into a single field in Views results.'
-      ) . '</p>';
-  }
+  return \Drupal::service(ViewsStringAggregationHooks::class)->help($route_name, $route_match);
 }
 
 /**
@@ -44,67 +36,15 @@ function views_string_aggregation_help($route_name, RouteMatchInterface $route_m
  *   $views_data['table']['base']['query_id'] = 'vsa_views_query_mysql';
  *   $views_data['table']['base']['query_id'] = 'vsa_views_query_pgsql';
  */
+#[LegacyHook]
 function views_string_aggregation_views_plugins_query_alter(array &$plugins) {
-  // Check configuration setting to see if we
-  // should extend the default views_query plugin.
-  $config = \Drupal::config('views_string_aggregation.settings');
-  if (!$config->get('extend_views_query_plugin')) {
-    \Drupal::logger('views_string_aggregation')->debug(
-      'The setting "extend_views_query_plugin" is disabled by configuration.'
-    );
-    return;
-  }
-  if (!empty($plugins['views_query'])) {
-    // Add class handler to extend the default views_query plugin,
-    // based on database type.
-    $database_type = \Drupal::database()->databaseType();
-    if (in_array($database_type, ['mysql', 'mariadb'])) {
-      $plugins['views_query']['class'] = 'Drupal\views_string_aggregation\Plugin\views\query\MySql';
-      $plugins['views_query']['provider'] = 'views_string_aggregation';
-    }
-    elseif (in_array($database_type, ['pgsql'])) {
-      $plugins['views_query']['class'] = 'Drupal\views_string_aggregation\Plugin\views\query\PgSql';
-      $plugins['views_query']['provider'] = 'views_string_aggregation';
-    }
-    elseif (in_array($database_type, ['sqlite'])) {
-      $plugins['views_query']['class'] = 'Drupal\views_string_aggregation\Plugin\views\query\Sqlite';
-      $plugins['views_query']['provider'] = 'views_string_aggregation';
-    }
-    else {
-      \Drupal::logger('views_string_aggregation')->notice(
-        'The database type "@type" is not yet supported.',
-        ['@type' => $database_type]
-      );
-    }
-    // @todo other databases with string aggregation support.
-  }
+  \Drupal::service(ViewsStringAggregationHooks::class)->viewsPluginsQueryAlter($plugins);
 }
 
 /**
  * Implements hook_config_schema_info_alter().
  */
+#[LegacyHook]
 function views_string_aggregation_config_schema_info_alter(&$definitions) {
-  // Extend the views query options schema with our custom options.
-  if (isset($definitions['views.query.views_query'])) {
-    $definitions['views.query.views_query']['mapping']['vsa_separator'] = [
-      'type' => 'string',
-      'label' => 'Views String Aggregation separator',
-      'nullable' => TRUE,
-    ];
-    $definitions['views.query.views_query']['mapping']['vsa_order_by'] = [
-      'type' => 'string',
-      'label' => 'Views String Aggregation order by field',
-      'nullable' => TRUE,
-    ];
-    $definitions['views.query.views_query']['mapping']['vsa_order_direction'] = [
-      'type' => 'string',
-      'label' => 'Views String Aggregation order direction',
-      'nullable' => TRUE,
-    ];
-    $definitions['views.query.views_query']['mapping']['vsa_max_length'] = [
-      'type' => 'string',
-      'label' => 'Views String Aggregation maximum string length (e.g group_concat_max_len)',
-      'nullable' => TRUE,
-    ];
-  }
+  \Drupal::service(ViewsStringAggregationHooks::class)->configSchemaInfoAlter($definitions);
 }
diff --git a/views_string_aggregation.services.yml b/views_string_aggregation.services.yml
new file mode 100644
index 0000000..21a919b
--- /dev/null
+++ b/views_string_aggregation.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\views_string_aggregation\Hook\ViewsStringAggregationHooks:
+    class: Drupal\views_string_aggregation\Hook\ViewsStringAggregationHooks
+    autowire: true
