diff --git a/core/.phpstan-baseline.php b/core/.phpstan-baseline.php
index c77e49e08ca8cb667d8ba66d4b86d44b8f83c3a4..d6d75205e43477fdf91b9514e6e583a5045934e6 100644
--- a/core/.phpstan-baseline.php
+++ b/core/.phpstan-baseline.php
@@ -12150,39 +12150,39 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function announcements_feed_cron\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\announcements_feed\\\\AnnounceRenderer\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/announcements_feed/announcements_feed.module',
+	'path' => __DIR__ . '/modules/announcements_feed/src/AnnounceRenderer.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function announcements_feed_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\announcements_feed\\\\AnnounceRenderer\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/announcements_feed/announcements_feed.module',
+	'path' => __DIR__ . '/modules/announcements_feed/src/AnnounceRenderer.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function announcements_feed_toolbar\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\announcements_feed\\\\Hook\\\\AnnouncementsFeedHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/announcements_feed/announcements_feed.module',
+	'path' => __DIR__ . '/modules/announcements_feed/src/Hook/AnnouncementsFeedHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function announcements_feed_toolbar_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\announcements_feed\\\\Hook\\\\AnnouncementsFeedHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/announcements_feed/announcements_feed.module',
+	'path' => __DIR__ . '/modules/announcements_feed/src/Hook/AnnouncementsFeedHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\announcements_feed\\\\AnnounceRenderer\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\announcements_feed\\\\Hook\\\\AnnouncementsFeedHooks\\:\\:toolbar\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/announcements_feed/src/AnnounceRenderer.php',
+	'path' => __DIR__ . '/modules/announcements_feed/src/Hook/AnnouncementsFeedHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\announcements_feed\\\\AnnounceRenderer\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\announcements_feed\\\\Hook\\\\AnnouncementsFeedHooks\\:\\:toolbarAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/announcements_feed/src/AnnounceRenderer.php',
+	'path' => __DIR__ . '/modules/announcements_feed/src/Hook/AnnouncementsFeedHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -12196,12 +12196,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/announcements_feed/tests/src/Kernel/AnnounceFetcherUserTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function automated_cron_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/automated_cron/automated_cron.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function automated_cron_settings_submit\\(\\) has no return type specified\\.$#',
@@ -12216,15 +12210,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function ban_schema\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\automated_cron\\\\Hook\\\\AutomatedCronHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/ban/ban.install',
+	'path' => __DIR__ . '/modules/automated_cron/src/Hook/AutomatedCronHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function ban_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function ban_schema\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/ban/ban.module',
+	'path' => __DIR__ . '/modules/ban/ban.install',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -12280,6 +12274,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/ban/src/Form/BanDelete.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\ban\\\\Hook\\\\BanHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/ban/src/Hook/BanHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\Tests\\\\ban\\\\Kernel\\\\Migrate\\\\d7\\\\MigrateBlockedIpsTest\\:\\:assertConfigSchema\\(\\) has no return type specified\\.$#',
@@ -12300,9 +12300,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function basic_auth_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\basic_auth\\\\Hook\\\\BasicAuthHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/basic_auth/basic_auth.module',
+	'path' => __DIR__ . '/modules/basic_auth/src/Hook/BasicAuthHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -12318,39 +12318,39 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function big_pipe_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function big_pipe_theme_suggestions_big_pipe_interface_preview\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/big_pipe/big_pipe.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function big_pipe_page_attachments\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\big_pipe\\\\EventSubscriber\\\\HtmlResponseBigPipeSubscriber\\:\\:onRespond\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/big_pipe/big_pipe.module',
+	'path' => __DIR__ . '/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function big_pipe_theme_suggestions_big_pipe_interface_preview\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\big_pipe\\\\EventSubscriber\\\\HtmlResponseBigPipeSubscriber\\:\\:onRespondEarly\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/big_pipe/big_pipe.module',
+	'path' => __DIR__ . '/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\big_pipe\\\\EventSubscriber\\\\HtmlResponseBigPipeSubscriber\\:\\:onRespond\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\big_pipe\\\\EventSubscriber\\\\NoBigPipeRouteAlterSubscriber\\:\\:onRoutingRouteAlterSetNoBigPipe\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php',
+	'path' => __DIR__ . '/modules/big_pipe/src/EventSubscriber/NoBigPipeRouteAlterSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\big_pipe\\\\EventSubscriber\\\\HtmlResponseBigPipeSubscriber\\:\\:onRespondEarly\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\big_pipe\\\\Hook\\\\BigPipeHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php',
+	'path' => __DIR__ . '/modules/big_pipe/src/Hook/BigPipeHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\big_pipe\\\\EventSubscriber\\\\NoBigPipeRouteAlterSubscriber\\:\\:onRoutingRouteAlterSetNoBigPipe\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\big_pipe\\\\Hook\\\\BigPipeHooks\\:\\:pageAttachments\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/big_pipe/src/EventSubscriber/NoBigPipeRouteAlterSubscriber.php',
+	'path' => __DIR__ . '/modules/big_pipe/src/Hook/BigPipeHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -12408,9 +12408,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function big_pipe_bypass_js_library_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\big_pipe_bypass_js\\\\Hook\\\\BigPipeBypassJsHooks\\:\\:libraryInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/big_pipe/tests/modules/big_pipe_bypass_js/big_pipe_bypass_js.module',
+	'path' => __DIR__ . '/modules/big_pipe/tests/modules/big_pipe_bypass_js/src/Hook/BigPipeBypassJsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -12442,12 +12442,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/big_pipe/tests/modules/big_pipe_regression_test/src/BigPipeRegressionTestController.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function big_pipe_test_page_top\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\big_pipe_test\\\\BigPipeTestController\\:\\:exception\\(\\) has no return type specified\\.$#',
@@ -12472,6 +12466,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/big_pipe/tests/modules/big_pipe_test/src/Form/BigPipeTestForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\big_pipe_test\\\\Hook\\\\BigPipeTestHooks\\:\\:pageTop\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/big_pipe/tests/modules/big_pipe_test/src/Hook/BigPipeTestHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\Tests\\\\big_pipe\\\\Unit\\\\Render\\\\BigPipeResponseAttachmentsProcessorTest\\:\\:attachmentsProvider\\(\\) has no return type specified\\.$#',
@@ -12496,48 +12496,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/big_pipe/tests/src/Unit/StackMiddleware/ContentLengthTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function block_block_build_local_actions_block_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/block/block.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function block_configurable_language_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/block/block.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function block_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/block/block.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function block_menu_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/block/block.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function block_modules_installed\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/block/block.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function block_page_top\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/block/block.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function block_rebuild\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/block/block.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function block_theme_initialize\\(\\) has no return type specified\\.$#',
@@ -12556,12 +12514,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/block/block.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function block_user_role_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/block/block.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function template_preprocess_block\\(\\) has no return type specified\\.$#',
@@ -12690,21 +12642,51 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function block_test_block_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\block\\\\Hook\\\\BlockHooks\\:\\:blockBuildLocalActionsBlockAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/block/tests/modules/block_test/block_test.module',
+	'path' => __DIR__ . '/modules/block/src/Hook/BlockHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function block_test_block_build_test_cache_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\block\\\\Hook\\\\BlockHooks\\:\\:configurableLanguageDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/block/tests/modules/block_test/block_test.module',
+	'path' => __DIR__ . '/modules/block/src/Hook/BlockHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function block_test_block_view_test_cache_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\block\\\\Hook\\\\BlockHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/block/tests/modules/block_test/block_test.module',
+	'path' => __DIR__ . '/modules/block/src/Hook/BlockHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\block\\\\Hook\\\\BlockHooks\\:\\:menuDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/block/src/Hook/BlockHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\block\\\\Hook\\\\BlockHooks\\:\\:modulesInstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/block/src/Hook/BlockHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\block\\\\Hook\\\\BlockHooks\\:\\:pageTop\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/block/src/Hook/BlockHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\block\\\\Hook\\\\BlockHooks\\:\\:rebuild\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/block/src/Hook/BlockHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\block\\\\Hook\\\\BlockHooks\\:\\:userRoleDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/block/src/Hook/BlockHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -12736,6 +12718,24 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/block/tests/modules/block_test/src/Form/TestForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\block_test\\\\Hook\\\\BlockTestHooks\\:\\:blockAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/block/tests/modules/block_test/src/Hook/BlockTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\block_test\\\\Hook\\\\BlockTestHooks\\:\\:blockBuildTestCacheAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/block/tests/modules/block_test/src/Hook/BlockTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\block_test\\\\Hook\\\\BlockTestHooks\\:\\:blockViewTestCacheAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/block/tests/modules/block_test/src/Hook/BlockTestHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\block_test\\\\Plugin\\\\Block\\\\TestBlockInstantiation\\:\\:blockSubmit\\(\\) has no return type specified\\.$#',
@@ -12946,6 +12946,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/block/tests/src/Kernel/BlockConfigSchemaTest.php',
 ];
+$ignoreErrors[] = [
+	// identifier: phpunit.coversMethod
+	'message' => '#^@covers value \\:\\:block_rebuild references an invalid method\\.$#',
+	'count' => 3,
+	'path' => __DIR__ . '/modules/block/tests/src/Kernel/BlockRebuildTest.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\Tests\\\\block\\\\Kernel\\\\Plugin\\\\migrate\\\\source\\\\BlockTest\\:\\:providerSource\\(\\) has no return type specified\\.$#',
@@ -12988,24 +12994,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/block/tests/src/Unit/Plugin/migrate/process/BlockSettingsTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function block_content_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/block_content/block_content.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function block_content_query_entity_reference_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/block_content/block_content.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function block_content_theme_suggestions_block_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/block_content/block_content.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function template_preprocess_block_content_add_list\\(\\) has no return type specified\\.$#',
@@ -13120,6 +13108,24 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/block_content/src/Event/BlockContentGetDependencyEvent.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\block_content\\\\Hook\\\\BlockContentHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/block_content/src/Hook/BlockContentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\block_content\\\\Hook\\\\BlockContentHooks\\:\\:queryEntityReferenceAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/block_content/src/Hook/BlockContentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\block_content\\\\Hook\\\\BlockContentHooks\\:\\:themeSuggestionsBlockAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/block_content/src/Hook/BlockContentHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\block_content\\\\Plugin\\\\Block\\\\BlockContentBlock\\:\\:blockSubmit\\(\\) has no return type specified\\.$#',
@@ -13134,27 +13140,27 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function block_content_test_block_content_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\block_content_test\\\\Hook\\\\BlockContentTestHooks\\:\\:blockContentInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/block_content/tests/modules/block_content_test/block_content_test.module',
+	'path' => __DIR__ . '/modules/block_content/tests/modules/block_content_test/src/Hook/BlockContentTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function block_content_test_block_content_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\block_content_test\\\\Hook\\\\BlockContentTestHooks\\:\\:blockContentPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/block_content/tests/modules/block_content_test/block_content_test.module',
+	'path' => __DIR__ . '/modules/block_content/tests/modules/block_content_test/src/Hook/BlockContentTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function block_content_test_block_content_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\block_content_test\\\\Hook\\\\BlockContentTestHooks\\:\\:blockContentUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/block_content/tests/modules/block_content_test/block_content_test.module',
+	'path' => __DIR__ . '/modules/block_content/tests/modules/block_content_test/src/Hook/BlockContentTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function block_content_test_block_content_view\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\block_content_test\\\\Hook\\\\BlockContentTestHooks\\:\\:blockContentView\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/block_content/tests/modules/block_content_test/block_content_test.module',
+	'path' => __DIR__ . '/modules/block_content/tests/modules/block_content_test/src/Hook/BlockContentTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -13500,45 +13506,45 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function breakpoint_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\breakpoint\\\\BreakpointManager\\:\\:clearCachedDefinitions\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/breakpoint/breakpoint.module',
+	'path' => __DIR__ . '/modules/breakpoint/src/BreakpointManager.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function breakpoint_themes_installed\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\breakpoint\\\\BreakpointManager\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/breakpoint/breakpoint.module',
+	'path' => __DIR__ . '/modules/breakpoint/src/BreakpointManager.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function breakpoint_themes_uninstalled\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\breakpoint\\\\BreakpointManager\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/breakpoint/breakpoint.module',
+	'path' => __DIR__ . '/modules/breakpoint/src/BreakpointManager.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\breakpoint\\\\BreakpointManager\\:\\:clearCachedDefinitions\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\breakpoint\\\\BreakpointManager\\:\\:processDefinition\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/breakpoint/src/BreakpointManager.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\breakpoint\\\\BreakpointManager\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\breakpoint\\\\Hook\\\\BreakpointHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/breakpoint/src/BreakpointManager.php',
+	'path' => __DIR__ . '/modules/breakpoint/src/Hook/BreakpointHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\breakpoint\\\\BreakpointManager\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\breakpoint\\\\Hook\\\\BreakpointHooks\\:\\:themesInstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/breakpoint/src/BreakpointManager.php',
+	'path' => __DIR__ . '/modules/breakpoint/src/Hook/BreakpointHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\breakpoint\\\\BreakpointManager\\:\\:processDefinition\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\breakpoint\\\\Hook\\\\BreakpointHooks\\:\\:themesUninstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/breakpoint/src/BreakpointManager.php',
+	'path' => __DIR__ . '/modules/breakpoint/src/Hook/BreakpointHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -13548,51 +13554,51 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function ckeditor5_config_schema_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function ckeditor5_filter_format_edit_form_submit\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/ckeditor5/ckeditor5.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function ckeditor5_filter_format_edit_form_submit\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function ckeditor5_module_implements_alter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/ckeditor5/ckeditor5.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function ckeditor5_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\ckeditor5\\\\Annotation\\\\CKEditor5Plugin\\:\\:setClass\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/ckeditor5/ckeditor5.module',
+	'path' => __DIR__ . '/modules/ckeditor5/src/Annotation/CKEditor5Plugin.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function ckeditor5_js_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\ckeditor5\\\\EventSubscriber\\\\CKEditor5CacheTag\\:\\:onSave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/ckeditor5/ckeditor5.module',
+	'path' => __DIR__ . '/modules/ckeditor5/src/EventSubscriber/CKEditor5CacheTag.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function ckeditor5_library_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\ckeditor5\\\\Hook\\\\Ckeditor5Hooks\\:\\:configSchemaInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/ckeditor5/ckeditor5.module',
+	'path' => __DIR__ . '/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function ckeditor5_module_implements_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\ckeditor5\\\\Hook\\\\Ckeditor5Hooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/ckeditor5/ckeditor5.module',
+	'path' => __DIR__ . '/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\ckeditor5\\\\Annotation\\\\CKEditor5Plugin\\:\\:setClass\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\ckeditor5\\\\Hook\\\\Ckeditor5Hooks\\:\\:jsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/ckeditor5/src/Annotation/CKEditor5Plugin.php',
+	'path' => __DIR__ . '/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\ckeditor5\\\\EventSubscriber\\\\CKEditor5CacheTag\\:\\:onSave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\ckeditor5\\\\Hook\\\\Ckeditor5Hooks\\:\\:libraryInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/ckeditor5/src/EventSubscriber/CKEditor5CacheTag.php',
+	'path' => __DIR__ . '/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -13914,9 +13920,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function ckeditor_test_editor_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\ckeditor_test\\\\Hook\\\\CkeditorTestHooks\\:\\:editorInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/ckeditor5/tests/modules/ckeditor_test/ckeditor_test.module',
+	'path' => __DIR__ . '/modules/ckeditor5/tests/modules/ckeditor_test/src/Hook/CkeditorTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -14296,114 +14302,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/comment/comment.install',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_cron\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_entity_extra_field_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_entity_insert\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_entity_predelete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_entity_storage_load\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_entity_view\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_entity_view_display_presave\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_field_config_create\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_field_config_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_field_config_update\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_field_info_entity_type_ui_definitions_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_field_storage_config_insert\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_field_type_category_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_node_links_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_node_search_result\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_node_update_index\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_node_view_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function comment_preprocess_block\\(\\) has no return type specified\\.$#',
@@ -14416,54 +14314,18 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/comment/comment.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_ranking\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function comment_uri\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/comment/comment.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_user_cancel\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_user_predelete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function template_preprocess_comment\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/comment/comment.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_token_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.tokens.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_tokens\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.tokens.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function comment_views_data_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/comment.views.inc',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\comment\\\\CommentBreadcrumbBuilder\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
@@ -14692,6 +14554,150 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/comment/src/Form/DeleteForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:entityExtraFieldInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:entityInsert\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:entityPredelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:entityStorageLoad\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:entityView\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:entityViewDisplayPresave\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:fieldConfigCreate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:fieldConfigDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:fieldConfigUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:fieldInfoEntityTypeUiDefinitionsAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:fieldStorageConfigInsert\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:fieldTypeCategoryInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:nodeLinksAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:nodeSearchResult\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:nodeUpdateIndex\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:nodeViewAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:ranking\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:userCancel\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentHooks\\:\\:userPredelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentTokensHooks\\:\\:tokenInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentTokensHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentTokensHooks\\:\\:tokens\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentTokensHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\comment\\\\Hook\\\\CommentViewsHooks\\:\\:viewsDataAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/comment/src/Hook/CommentViewsHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\comment\\\\Plugin\\\\EntityReferenceSelection\\\\CommentSelection\\:\\:entityQueryAlter\\(\\) has no return type specified\\.$#',
@@ -14886,15 +14892,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function comment_display_configurable_test_entity_base_field_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\comment_display_configurable_test\\\\Hook\\\\CommentDisplayConfigurableTestHooks\\:\\:entityBaseFieldInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/tests/modules/comment_display_configurable_test/comment_display_configurable_test.module',
+	'path' => __DIR__ . '/modules/comment/tests/modules/comment_display_configurable_test/src/Hook/CommentDisplayConfigurableTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function comment_display_configurable_test_entity_type_build\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\comment_display_configurable_test\\\\Hook\\\\CommentDisplayConfigurableTestHooks\\:\\:entityTypeBuild\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/tests/modules/comment_display_configurable_test/comment_display_configurable_test.module',
+	'path' => __DIR__ . '/modules/comment/tests/modules/comment_display_configurable_test/src/Hook/CommentDisplayConfigurableTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -14904,15 +14910,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function comment_test_comment_links_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\comment_test\\\\Controller\\\\CommentTestController\\:\\:commentReport\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/tests/modules/comment_test/comment_test.module',
+	'path' => __DIR__ . '/modules/comment/tests/modules/comment_test/src/Controller/CommentTestController.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\comment_test\\\\Controller\\\\CommentTestController\\:\\:commentReport\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\comment_test\\\\Hook\\\\CommentTestHooks\\:\\:commentLinksAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/comment/tests/modules/comment_test/src/Controller/CommentTestController.php',
+	'path' => __DIR__ . '/modules/comment/tests/modules/comment_test/src/Hook/CommentTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -15472,18 +15478,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/comment/tests/src/Unit/CommentLinkBuilderTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_file_download\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config/config.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config/config.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\config\\\\ConfigSubscriber\\:\\:onConfigImporterValidate\\(\\) has no return type specified\\.$#',
@@ -15604,6 +15598,18 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/config/src/Form/ConfigSync.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config\\\\Hook\\\\ConfigHooks\\:\\:fileDownload\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config/src/Hook/ConfigHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config\\\\Hook\\\\ConfigHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config/src/Hook/ConfigHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\config_collection_install_test\\\\EventSubscriber\\:\\:addCollections\\(\\) has no return type specified\\.$#',
@@ -15612,9 +15618,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function config_entity_static_cache_test_config_test_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\config_entity_static_cache_test\\\\Hook\\\\ConfigEntityStaticCacheTestHooks\\:\\:configTestLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_entity_static_cache_test/config_entity_static_cache_test.module',
+	'path' => __DIR__ . '/modules/config/tests/config_entity_static_cache_test/src/Hook/ConfigEntityStaticCacheTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -15628,12 +15634,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/config/tests/config_import_test/config_import_test.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_import_test_config_import_steps_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_import_test/config_import_test.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\config_import_test\\\\EventSubscriber\\:\\:onConfigDelete\\(\\) has no return type specified\\.$#',
@@ -15666,69 +15666,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function config_install_dependency_test_config_test_create\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_install_dependency_test/config_install_dependency_test.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_schema_test_config_schema_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_schema_test/config_schema_test.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function _config_test_update_is_syncing_store\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_test/config_test.hooks.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_test_config_test_create\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_test/config_test.hooks.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_test_config_test_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_test/config_test.hooks.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_test_config_test_insert\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_test/config_test.hooks.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_test_config_test_load\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_test/config_test.hooks.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_test_config_test_predelete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_test/config_test.hooks.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_test_config_test_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\config_import_test\\\\Hook\\\\ConfigImportTestHooks\\:\\:configImportStepsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_test/config_test.hooks.inc',
+	'path' => __DIR__ . '/modules/config/tests/config_import_test/src/Hook/ConfigImportTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function config_test_config_test_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\config_install_dependency_test\\\\Hook\\\\ConfigInstallDependencyTestHooks\\:\\:configTestCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_test/config_test.hooks.inc',
+	'path' => __DIR__ . '/modules/config/tests/config_install_dependency_test/src/Hook/ConfigInstallDependencyTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function config_test_cache_flush\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\config_schema_test\\\\Hook\\\\ConfigSchemaTestHooks\\:\\:configSchemaInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/config/tests/config_test/config_test.module',
+	'path' => __DIR__ . '/modules/config/tests/config_schema_test/src/Hook/ConfigSchemaTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -15838,6 +15790,60 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/config/tests/config_test/src/Entity/ConfigTest.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_test\\\\Hook\\\\ConfigTestHooks\\:\\:cacheFlush\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config/tests/config_test/src/Hook/ConfigTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_test\\\\Hook\\\\ConfigTestHooksHooks\\:\\:configTestCreate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config/tests/config_test/src/Hook/ConfigTestHooksHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_test\\\\Hook\\\\ConfigTestHooksHooks\\:\\:configTestDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config/tests/config_test/src/Hook/ConfigTestHooksHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_test\\\\Hook\\\\ConfigTestHooksHooks\\:\\:configTestInsert\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config/tests/config_test/src/Hook/ConfigTestHooksHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_test\\\\Hook\\\\ConfigTestHooksHooks\\:\\:configTestLoad\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config/tests/config_test/src/Hook/ConfigTestHooksHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_test\\\\Hook\\\\ConfigTestHooksHooks\\:\\:configTestPredelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config/tests/config_test/src/Hook/ConfigTestHooksHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_test\\\\Hook\\\\ConfigTestHooksHooks\\:\\:configTestPresave\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config/tests/config_test/src/Hook/ConfigTestHooksHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_test\\\\Hook\\\\ConfigTestHooksHooks\\:\\:configTestUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config/tests/config_test/src/Hook/ConfigTestHooksHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_test\\\\Hook\\\\ConfigTestHooksHooks\\:\\:updateIsSyncingStore\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config/tests/config_test/src/Hook/ConfigTestHooksHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\config_test\\\\SchemaListenerController\\:\\:test\\(\\) has no return type specified\\.$#',
@@ -16030,42 +16036,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/config/tests/src/Unit/Menu/ConfigLocalTasksTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_translation_config_schema_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config_translation/config_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_translation_config_translation_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config_translation/config_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_translation_entity_operation\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config_translation/config_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_translation_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config_translation/config_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_translation_themes_installed\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config_translation/config_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function config_translation_themes_uninstalled\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/config_translation/config_translation.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\config_translation\\\\ConfigEntityMapper\\:\\:populateFromRouteMatch\\(\\) has no return type specified\\.$#',
@@ -16234,6 +16204,42 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/config_translation/src/FormElement/PluralVariants.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_translation\\\\Hook\\\\ConfigTranslationHooks\\:\\:configSchemaInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config_translation/src/Hook/ConfigTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_translation\\\\Hook\\\\ConfigTranslationHooks\\:\\:configTranslationInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config_translation/src/Hook/ConfigTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_translation\\\\Hook\\\\ConfigTranslationHooks\\:\\:entityOperation\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config_translation/src/Hook/ConfigTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_translation\\\\Hook\\\\ConfigTranslationHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config_translation/src/Hook/ConfigTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_translation\\\\Hook\\\\ConfigTranslationHooks\\:\\:themesInstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config_translation/src/Hook/ConfigTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\config_translation\\\\Hook\\\\ConfigTranslationHooks\\:\\:themesUninstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/config_translation/src/Hook/ConfigTranslationHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\config_translation\\\\Plugin\\\\Menu\\\\ContextualLink\\\\ConfigTranslationContextualLink\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
@@ -16266,21 +16272,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function config_translation_test_config_translation_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\config_translation_test\\\\EventSubscriber\\\\ConfigTranslationTestSubscriber\\:\\:addConfigNames\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module',
+	'path' => __DIR__ . '/modules/config_translation/tests/modules/config_translation_test/src/EventSubscriber/ConfigTranslationTestSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function config_translation_test_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\config_translation_test\\\\Hook\\\\ConfigTranslationTestHooks\\:\\:configTranslationInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module',
+	'path' => __DIR__ . '/modules/config_translation/tests/modules/config_translation_test/src/Hook/ConfigTranslationTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\config_translation_test\\\\EventSubscriber\\\\ConfigTranslationTestSubscriber\\:\\:addConfigNames\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\config_translation_test\\\\Hook\\\\ConfigTranslationTestHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/config_translation/tests/modules/config_translation_test/src/EventSubscriber/ConfigTranslationTestSubscriber.php',
+	'path' => __DIR__ . '/modules/config_translation/tests/modules/config_translation_test/src/Hook/ConfigTranslationTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -16336,54 +16342,18 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/config_translation/tests/src/Kernel/Plugin/migrate/source/d6/ProfileFieldTranslationTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function contact_entity_extra_field_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/contact/contact.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function contact_form_user_admin_settings_submit\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/contact/contact.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function contact_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/contact/contact.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function contact_mail\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/contact/contact.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function contact_menu_local_tasks_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/contact/contact.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function contact_rest_resource_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/contact/contact.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function contact_user_profile_form_submit\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/contact/contact.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function contact_views_data_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/contact/contact.views.inc',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\contact\\\\ContactFormEditForm\\:\\:create\\(\\) has no return type specified\\.$#',
@@ -16450,6 +16420,42 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/contact/src/Entity/Message.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\contact\\\\Hook\\\\ContactHooks\\:\\:entityExtraFieldInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/contact/src/Hook/ContactHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\contact\\\\Hook\\\\ContactHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/contact/src/Hook/ContactHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\contact\\\\Hook\\\\ContactHooks\\:\\:mail\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/contact/src/Hook/ContactHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\contact\\\\Hook\\\\ContactHooks\\:\\:menuLocalTasksAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/contact/src/Hook/ContactHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\contact\\\\Hook\\\\ContactHooks\\:\\:restResourceAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/contact/src/Hook/ContactHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\contact\\\\Hook\\\\ContactViewsHooks\\:\\:viewsDataAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/contact/src/Hook/ContactViewsHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\contact\\\\MailHandler\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
@@ -16542,9 +16548,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function contact_storage_test_entity_base_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\contact_storage_test\\\\Hook\\\\ContactStorageTestHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module',
+	'path' => __DIR__ . '/modules/contact/tests/modules/contact_storage_test/src/Hook/ContactStorageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -16854,387 +16860,387 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_action_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function content_moderation_preprocess_node\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\ContentPreprocess\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/ContentPreprocess.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_base_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\ContentPreprocess\\:\\:preprocessNode\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/ContentPreprocess.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_bundle_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\ContentModerationState\\:\\:updateOrCreateFromEntity\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/ContentModerationState.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_bundle_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\BlockContentModerationHandler\\:\\:enforceRevisionsBundleFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/BlockContentModerationHandler.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_bundle_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\BlockContentModerationHandler\\:\\:enforceRevisionsEntityFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/BlockContentModerationHandler.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandler\\:\\:enforceRevisionsBundleFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandler.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_extra_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandler\\:\\:enforceRevisionsEntityFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandler.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_field_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandler\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandler.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_form_display_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandler\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandler.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandler\\:\\:onPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandler.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_prepare_form\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandlerInterface\\:\\:enforceRevisionsBundleFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandlerInterface.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandlerInterface\\:\\:enforceRevisionsEntityFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandlerInterface.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_revision_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandlerInterface\\:\\:onPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandlerInterface.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_translation_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\NodeModerationHandler\\:\\:enforceRevisionsBundleFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\NodeModerationHandler\\:\\:enforceRevisionsEntityFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_entity_view\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_preprocess_node\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_views_post_execute\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_workflow_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityRevisionDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_workflow_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityTranslationDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.module',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_views_data\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.views.inc',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_views_query_substitutions\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityView\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/content_moderation.views_execution.inc',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\ContentPreprocess\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:updateOrCreateFromEntity\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/ContentPreprocess.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\ContentPreprocess\\:\\:preprocessNode\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:bundleFormRedirect\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/ContentPreprocess.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\ContentModerationState\\:\\:updateOrCreateFromEntity\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/ContentModerationState.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\BlockContentModerationHandler\\:\\:enforceRevisionsBundleFormAlter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:entityPrepareForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/BlockContentModerationHandler.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\BlockContentModerationHandler\\:\\:enforceRevisionsEntityFormAlter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:entityTypeAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/BlockContentModerationHandler.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandler\\:\\:enforceRevisionsBundleFormAlter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:formAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandler.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandler\\:\\:enforceRevisionsEntityFormAlter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandler.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandler\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandler.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandler\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\EventSubscriber\\\\ConfigImportSubscriber\\:\\:onConfigImporterValidate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandler.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/EventSubscriber/ConfigImportSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandler\\:\\:onPresave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\ContentModerationConfigureEntityTypesForm\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandler.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Form/ContentModerationConfigureEntityTypesForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandlerInterface\\:\\:enforceRevisionsBundleFormAlter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\ContentModerationConfigureEntityTypesForm\\:\\:getTitle\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandlerInterface.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Form/ContentModerationConfigureEntityTypesForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandlerInterface\\:\\:enforceRevisionsEntityFormAlter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\ContentModerationConfigureEntityTypesForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandlerInterface.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Form/ContentModerationConfigureEntityTypesForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\ModerationHandlerInterface\\:\\:onPresave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\ContentModerationConfigureForm\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/ModerationHandlerInterface.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Form/ContentModerationConfigureForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\NodeModerationHandler\\:\\:enforceRevisionsBundleFormAlter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\ContentModerationConfigureForm\\:\\:submitConfigurationForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Form/ContentModerationConfigureForm.php',
 ];
 $ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Entity\\\\Handler\\\\NodeModerationHandler\\:\\:enforceRevisionsEntityFormAlter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php',
+	// identifier: isset.variable
+	'message' => '#^Variable \\$state in isset\\(\\) always exists and is not nullable\\.$#',
+	'count' => 3,
+	'path' => __DIR__ . '/modules/content_moderation/src/Form/ContentModerationStateForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\EntityModerationForm\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Form/EntityModerationForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityDelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\EntityModerationForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Form/EntityModerationForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityInsert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:actionInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityPresave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityRevisionDelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityTranslationDelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityBundleDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityUpdate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityBundleFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:entityView\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityBundleInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityOperations\\:\\:updateOrCreateFromEntity\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityOperations.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:bundleFormRedirect\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityExtraFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityFieldAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:entityPrepareForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityFormDisplayAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:entityTypeAlter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:formAlter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityPrepareForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EntityTypeInfo\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityRevisionDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EntityTypeInfo.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\EventSubscriber\\\\ConfigImportSubscriber\\:\\:onConfigImporterValidate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityTranslationDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/EventSubscriber/ConfigImportSubscriber.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\ContentModerationConfigureEntityTypesForm\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Form/ContentModerationConfigureEntityTypesForm.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\ContentModerationConfigureEntityTypesForm\\:\\:getTitle\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:entityView\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Form/ContentModerationConfigureEntityTypesForm.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\ContentModerationConfigureEntityTypesForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Form/ContentModerationConfigureEntityTypesForm.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\ContentModerationConfigureForm\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:viewsPostExecute\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Form/ContentModerationConfigureForm.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\ContentModerationConfigureForm\\:\\:submitConfigurationForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:workflowInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Form/ContentModerationConfigureForm.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
-	// identifier: isset.variable
-	'message' => '#^Variable \\$state in isset\\(\\) always exists and is not nullable\\.$#',
-	'count' => 3,
-	'path' => __DIR__ . '/modules/content_moderation/src/Form/ContentModerationStateForm.php',
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationHooks\\:\\:workflowUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\EntityModerationForm\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationViewsExecutionHooks\\:\\:viewsQuerySubstitutions\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Form/EntityModerationForm.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationViewsExecutionHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\content_moderation\\\\Form\\\\EntityModerationForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation\\\\Hook\\\\ContentModerationViewsHooks\\:\\:viewsData\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/src/Form/EntityModerationForm.php',
+	'path' => __DIR__ . '/modules/content_moderation/src/Hook/ContentModerationViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -17448,21 +17454,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_test_resave_entity_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation_test_resave\\\\Hook\\\\ContentModerationTestResaveHooks\\:\\:entityInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.module',
+	'path' => __DIR__ . '/modules/content_moderation/tests/modules/content_moderation_test_resave/src/Hook/ContentModerationTestResaveHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_test_views_views_data_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation_test_views\\\\Hook\\\\ContentModerationTestViewsHooks\\:\\:viewsDataAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.module',
+	'path' => __DIR__ . '/modules/content_moderation/tests/modules/content_moderation_test_views/src/Hook/ContentModerationTestViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_moderation_test_views_views_query_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_moderation_test_views\\\\Hook\\\\ContentModerationTestViewsHooks\\:\\:viewsQueryAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.module',
+	'path' => __DIR__ . '/modules/content_moderation/tests/modules/content_moderation_test_views/src/Hook/ContentModerationTestViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -18058,60 +18064,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_element_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function content_translation_enable_widget\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_entity_base_field_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_entity_bundle_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_entity_extra_field_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_entity_operation\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_entity_presave\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_field_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function content_translation_language_configuration_element_submit\\(\\) has no return type specified\\.$#',
@@ -18124,60 +18082,18 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_language_content_settings_insert\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_language_content_settings_update\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_language_fallback_candidates_entity_view_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_language_types_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_menu_links_discovered_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function content_translation_module_implements_alter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_page_attachments\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function content_translation_preprocess_language_content_settings_table\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function content_translation_views_data_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/content_translation.module',
-];
 $ignoreErrors[] = [
 	// identifier: variable.undefined
 	'message' => '#^Variable \\$locked_languages might not be defined\\.$#',
@@ -18352,6 +18268,96 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/content_translation/src/FieldTranslationSynchronizerInterface.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:elementInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:entityBundleInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:entityExtraFieldInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:entityOperation\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:entityPresave\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:fieldInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:languageContentSettingsInsert\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:languageContentSettingsUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:languageFallbackCandidatesEntityViewAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:languageTypesInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:menuLinksDiscoveredAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:pageAttachments\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\content_translation\\\\Hook\\\\ContentTranslationHooks\\:\\:viewsDataAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/content_translation/src/Hook/ContentTranslationHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\content_translation\\\\Plugin\\\\Derivative\\\\ContentTranslationContextualLinks\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
@@ -18390,27 +18396,27 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_translation_test_entity_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function content_translation_test_form_node_form_submit\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_translation_test_entity_bundle_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_translation_test\\\\Hook\\\\ContentTranslationTestHooks\\:\\:entityAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module',
+	'path' => __DIR__ . '/modules/content_translation/tests/modules/content_translation_test/src/Hook/ContentTranslationTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_translation_test_entity_translation_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_translation_test\\\\Hook\\\\ContentTranslationTestHooks\\:\\:entityBundleInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module',
+	'path' => __DIR__ . '/modules/content_translation/tests/modules/content_translation_test/src/Hook/ContentTranslationTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function content_translation_test_form_node_form_submit\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\content_translation_test\\\\Hook\\\\ContentTranslationTestHooks\\:\\:entityTranslationDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module',
+	'path' => __DIR__ . '/modules/content_translation/tests/modules/content_translation_test/src/Hook/ContentTranslationTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -18708,45 +18714,45 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function contextual_contextual_links_view_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function contextual_preprocess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/contextual/contextual.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function contextual_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\contextual\\\\ContextualController\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/contextual/contextual.module',
+	'path' => __DIR__ . '/modules/contextual/src/ContextualController.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function contextual_page_attachments\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\contextual\\\\Hook\\\\ContextualHooks\\:\\:contextualLinksViewAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/contextual/contextual.module',
+	'path' => __DIR__ . '/modules/contextual/src/Hook/ContextualHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function contextual_preprocess\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\contextual\\\\Hook\\\\ContextualHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/contextual/contextual.module',
+	'path' => __DIR__ . '/modules/contextual/src/Hook/ContextualHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function contextual_toolbar\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\contextual\\\\Hook\\\\ContextualHooks\\:\\:pageAttachments\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/contextual/contextual.module',
+	'path' => __DIR__ . '/modules/contextual/src/Hook/ContextualHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function contextual_views_data_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\contextual\\\\Hook\\\\ContextualHooks\\:\\:toolbar\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/contextual/contextual.views.inc',
+	'path' => __DIR__ . '/modules/contextual/src/Hook/ContextualHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\contextual\\\\ContextualController\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\contextual\\\\Hook\\\\ContextualViewsHooks\\:\\:viewsDataAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/contextual/src/ContextualController.php',
+	'path' => __DIR__ . '/modules/contextual/src/Hook/ContextualViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -18774,21 +18780,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function contextual_test_block_view_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\contextual_test\\\\Hook\\\\ContextualTestHooks\\:\\:blockViewAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/contextual/tests/modules/contextual_test/contextual_test.module',
+	'path' => __DIR__ . '/modules/contextual/tests/modules/contextual_test/src/Hook/ContextualTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function contextual_test_contextual_links_view_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\contextual_test\\\\Hook\\\\ContextualTestHooks\\:\\:contextualLinksViewAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/contextual/tests/modules/contextual_test/contextual_test.module',
+	'path' => __DIR__ . '/modules/contextual/tests/modules/contextual_test/src/Hook/ContextualTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function contextual_test_page_attachments_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\contextual_test\\\\Hook\\\\ContextualTestHooks\\:\\:pageAttachmentsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/contextual/tests/modules/contextual_test/contextual_test.module',
+	'path' => __DIR__ . '/modules/contextual/tests/modules/contextual_test/src/Hook/ContextualTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -18810,21 +18816,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function datetime_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\datetime\\\\DateTimeComputed\\:\\:setValue\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/datetime/datetime.module',
+	'path' => __DIR__ . '/modules/datetime/src/DateTimeComputed.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function datetime_field_views_data\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\datetime\\\\Hook\\\\DatetimeHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/datetime/datetime.views.inc',
+	'path' => __DIR__ . '/modules/datetime/src/Hook/DatetimeHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\datetime\\\\DateTimeComputed\\:\\:setValue\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\datetime\\\\Hook\\\\DatetimeViewsHooks\\:\\:fieldViewsData\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/datetime/src/DateTimeComputed.php',
+	'path' => __DIR__ . '/modules/datetime/src/Hook/DatetimeViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -18960,15 +18966,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function datetime_range_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\datetime_range\\\\Hook\\\\DatetimeRangeHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/datetime_range/datetime_range.module',
+	'path' => __DIR__ . '/modules/datetime_range/src/Hook/DatetimeRangeHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function datetime_range_field_views_data\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\datetime_range\\\\Hook\\\\DatetimeRangeViewsHooks\\:\\:fieldViewsData\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/datetime_range/datetime_range.views.inc',
+	'path' => __DIR__ . '/modules/datetime_range/src/Hook/DatetimeRangeViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: return.missing
@@ -19026,69 +19032,69 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function dblog_cron\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dblog\\\\Form\\\\DblogClearLogConfirmForm\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dblog/dblog.module',
+	'path' => __DIR__ . '/modules/dblog/src/Form/DblogClearLogConfirmForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function dblog_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dblog\\\\Form\\\\DblogClearLogConfirmForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dblog/dblog.module',
+	'path' => __DIR__ . '/modules/dblog/src/Form/DblogClearLogConfirmForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function dblog_menu_links_discovered_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dblog\\\\Form\\\\DblogFilterForm\\:\\:resetForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dblog/dblog.module',
+	'path' => __DIR__ . '/modules/dblog/src/Form/DblogFilterForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function dblog_views_pre_render\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dblog\\\\Form\\\\DblogFilterForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dblog/dblog.module',
+	'path' => __DIR__ . '/modules/dblog/src/Form/DblogFilterForm.php',
 ];
 $ignoreErrors[] = [
-	// identifier: isset.variable
-	'message' => '#^Variable \\$view in isset\\(\\) always exists and is not nullable\\.$#',
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\dblog\\\\Form\\\\DblogFilterForm\\:\\:validateForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dblog/dblog.module',
+	'path' => __DIR__ . '/modules/dblog/src/Form/DblogFilterForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function dblog_views_data\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dblog\\\\Hook\\\\DblogHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dblog/dblog.views.inc',
+	'path' => __DIR__ . '/modules/dblog/src/Hook/DblogHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\dblog\\\\Form\\\\DblogClearLogConfirmForm\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dblog\\\\Hook\\\\DblogHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dblog/src/Form/DblogClearLogConfirmForm.php',
+	'path' => __DIR__ . '/modules/dblog/src/Hook/DblogHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\dblog\\\\Form\\\\DblogClearLogConfirmForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dblog\\\\Hook\\\\DblogHooks\\:\\:menuLinksDiscoveredAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dblog/src/Form/DblogClearLogConfirmForm.php',
+	'path' => __DIR__ . '/modules/dblog/src/Hook/DblogHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\dblog\\\\Form\\\\DblogFilterForm\\:\\:resetForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dblog\\\\Hook\\\\DblogHooks\\:\\:viewsPreRender\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dblog/src/Form/DblogFilterForm.php',
+	'path' => __DIR__ . '/modules/dblog/src/Hook/DblogHooks.php',
 ];
 $ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\dblog\\\\Form\\\\DblogFilterForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	// identifier: isset.variable
+	'message' => '#^Variable \\$view in isset\\(\\) always exists and is not nullable\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dblog/src/Form/DblogFilterForm.php',
+	'path' => __DIR__ . '/modules/dblog/src/Hook/DblogHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\dblog\\\\Form\\\\DblogFilterForm\\:\\:validateForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dblog\\\\Hook\\\\DblogViewsHooks\\:\\:viewsData\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dblog/src/Form/DblogFilterForm.php',
+	'path' => __DIR__ . '/modules/dblog/src/Hook/DblogViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -19182,21 +19188,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function dynamic_page_cache_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dynamic_page_cache\\\\EventSubscriber\\\\DynamicPageCacheSubscriber\\:\\:onRequest\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dynamic_page_cache/dynamic_page_cache.module',
+	'path' => __DIR__ . '/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\dynamic_page_cache\\\\EventSubscriber\\\\DynamicPageCacheSubscriber\\:\\:onRequest\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dynamic_page_cache\\\\EventSubscriber\\\\DynamicPageCacheSubscriber\\:\\:onResponse\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\dynamic_page_cache\\\\EventSubscriber\\\\DynamicPageCacheSubscriber\\:\\:onResponse\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dynamic_page_cache\\\\Hook\\\\DynamicPageCacheHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php',
+	'path' => __DIR__ . '/modules/dynamic_page_cache/src/Hook/DynamicPageCacheHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -19224,111 +19230,111 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_element_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function editor_form_filter_admin_form_ajax\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/editor/editor.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_entity_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function editor_form_filter_admin_format_editor_configure\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/editor/editor.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_entity_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function editor_form_filter_admin_format_submit\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/editor/editor.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_entity_revision_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function editor_form_filter_admin_format_validate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/editor/editor.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_entity_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\Ajax\\\\EditorDialogSave\\:\\:render\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/editor.module',
+	'path' => __DIR__ . '/modules/editor/src/Ajax/EditorDialogSave.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_file_download\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\EditorInterface\\:\\:setEditor\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/editor.module',
+	'path' => __DIR__ . '/modules/editor/src/EditorInterface.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_filter_format_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\Element\\:\\:preRenderTextFormat\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/editor.module',
+	'path' => __DIR__ . '/modules/editor/src/Element.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_form_filter_admin_form_ajax\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\Entity\\\\Editor\\:\\:setEditor\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/editor.module',
+	'path' => __DIR__ . '/modules/editor/src/Entity/Editor.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_form_filter_admin_format_editor_configure\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\EventSubscriber\\\\EditorConfigTranslationSubscriber\\:\\:addConfigNames\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/editor.module',
+	'path' => __DIR__ . '/modules/editor/src/EventSubscriber/EditorConfigTranslationSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_form_filter_admin_format_submit\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\Hook\\\\EditorHooks\\:\\:elementInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/editor.module',
+	'path' => __DIR__ . '/modules/editor/src/Hook/EditorHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_form_filter_admin_format_validate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\Hook\\\\EditorHooks\\:\\:entityDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/editor.module',
+	'path' => __DIR__ . '/modules/editor/src/Hook/EditorHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\Hook\\\\EditorHooks\\:\\:entityInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/editor.module',
+	'path' => __DIR__ . '/modules/editor/src/Hook/EditorHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_menu_links_discovered_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\Hook\\\\EditorHooks\\:\\:entityRevisionDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/editor.module',
+	'path' => __DIR__ . '/modules/editor/src/Hook/EditorHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\editor\\\\Ajax\\\\EditorDialogSave\\:\\:render\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\Hook\\\\EditorHooks\\:\\:entityUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/src/Ajax/EditorDialogSave.php',
+	'path' => __DIR__ . '/modules/editor/src/Hook/EditorHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\editor\\\\EditorInterface\\:\\:setEditor\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\Hook\\\\EditorHooks\\:\\:fileDownload\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/src/EditorInterface.php',
+	'path' => __DIR__ . '/modules/editor/src/Hook/EditorHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\editor\\\\Element\\:\\:preRenderTextFormat\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\Hook\\\\EditorHooks\\:\\:filterFormatPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/src/Element.php',
+	'path' => __DIR__ . '/modules/editor/src/Hook/EditorHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\editor\\\\Entity\\\\Editor\\:\\:setEditor\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\Hook\\\\EditorHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/src/Entity/Editor.php',
+	'path' => __DIR__ . '/modules/editor/src/Hook/EditorHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\editor\\\\EventSubscriber\\\\EditorConfigTranslationSubscriber\\:\\:addConfigNames\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor\\\\Hook\\\\EditorHooks\\:\\:menuLinksDiscoveredAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/src/EventSubscriber/EditorConfigTranslationSubscriber.php',
+	'path' => __DIR__ . '/modules/editor/src/Hook/EditorHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -19344,33 +19350,33 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_test_editor_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor_test\\\\Hook\\\\EditorTestHooks\\:\\:editorInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/tests/modules/editor_test/editor_test.module',
+	'path' => __DIR__ . '/modules/editor/tests/modules/editor_test/src/Hook/EditorTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_test_editor_js_settings_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor_test\\\\Hook\\\\EditorTestHooks\\:\\:editorJsSettingsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/tests/modules/editor_test/editor_test.module',
+	'path' => __DIR__ . '/modules/editor/tests/modules/editor_test/src/Hook/EditorTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_test_editor_xss_filter_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor_test\\\\Hook\\\\EditorTestHooks\\:\\:editorXssFilterAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/tests/modules/editor_test/editor_test.module',
+	'path' => __DIR__ . '/modules/editor/tests/modules/editor_test/src/Hook/EditorTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_test_entity_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor_test\\\\Hook\\\\EditorTestHooks\\:\\:entityUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/tests/modules/editor_test/editor_test.module',
+	'path' => __DIR__ . '/modules/editor/tests/modules/editor_test/src/Hook/EditorTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function editor_test_file_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\editor_test\\\\Hook\\\\EditorTestHooks\\:\\:filePresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/editor/tests/modules/editor_test/editor_test.module',
+	'path' => __DIR__ . '/modules/editor/tests/modules/editor_test/src/Hook/EditorTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -19474,72 +19480,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/editor/tests/src/Unit/EditorXssFilter/StandardTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_config_import_steps_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field/field.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_cron\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field/field.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_entity_bundle_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field/field.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_entity_bundle_field_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field/field.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_entity_field_storage_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field/field.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_field_config_create\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field/field.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_field_config_insert\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field/field.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_field_config_presave\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field/field.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_field_storage_config_update\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field/field.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function field_form_field_config_edit_form_entity_builder\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/field/field.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field/field.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function field_purge_batch\\(\\) has no return type specified\\.$#',
@@ -19674,195 +19620,255 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\field\\\\Plugin\\\\migrate\\\\field\\\\Email\\:\\:defineValueProcessPipeline\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field\\\\Hook\\\\FieldHooks\\:\\:configImportStepsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/src/Plugin/migrate/field/Email.php',
+	'path' => __DIR__ . '/modules/field/src/Hook/FieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_entity_info_translatable\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field\\\\Hook\\\\FieldHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.entity.inc',
+	'path' => __DIR__ . '/modules/field/src/Hook/FieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_default_value\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field\\\\Hook\\\\FieldHooks\\:\\:entityBundleDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.field.inc',
+	'path' => __DIR__ . '/modules/field/src/Hook/FieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_entity_field_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field\\\\Hook\\\\FieldHooks\\:\\:entityBundleFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.field.inc',
+	'path' => __DIR__ . '/modules/field/src/Hook/FieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_field_storage_config_update_forbid\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field\\\\Hook\\\\FieldHooks\\:\\:entityFieldStorageInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.field.inc',
+	'path' => __DIR__ . '/modules/field/src/Hook/FieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_field_widget_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field\\\\Hook\\\\FieldHooks\\:\\:fieldConfigCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.field.inc',
+	'path' => __DIR__ . '/modules/field/src/Hook/FieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function _field_test_alter_widget\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field\\\\Hook\\\\FieldHooks\\:\\:fieldConfigInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
+	'path' => __DIR__ . '/modules/field/src/Hook/FieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_entity_bundle_field_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field\\\\Hook\\\\FieldHooks\\:\\:fieldConfigPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
+	'path' => __DIR__ . '/modules/field/src/Hook/FieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_entity_display_build_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field\\\\Hook\\\\FieldHooks\\:\\:fieldStorageConfigUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
+	'path' => __DIR__ . '/modules/field/src/Hook/FieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_entity_extra_field_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field\\\\Hook\\\\FieldHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
+	'path' => __DIR__ . '/modules/field/src/Hook/FieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_field_info_entity_type_ui_definitions_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field\\\\Plugin\\\\migrate\\\\field\\\\Email\\:\\:defineValueProcessPipeline\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
+	'path' => __DIR__ . '/modules/field/src/Plugin/migrate/field/Email.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_field_storage_config_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function field_test_entity_info_translatable\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.entity.inc',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_field_ui_preconfigured_options_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function field_test_default_value\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.field.inc',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_field_widget_complete_form_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function _field_test_alter_widget\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_field_widget_complete_test_field_widget_multiple_form_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function field_test_field_storage_config_create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_field_widget_complete_test_field_widget_multiple_single_value_form_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\FieldDefaultValueCallbackProvider\\:\\:calculateDefaultValue\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/FieldDefaultValueCallbackProvider.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_field_widget_single_element_form_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Form\\\\NestedEntityTestForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Form/NestedEntityTestForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_query_efq_metadata_test_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Form\\\\NestedEntityTestForm\\:\\:validateForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Form/NestedEntityTestForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_query_efq_table_prefixing_test_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestFieldHooks\\:\\:entityFieldAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/field_test.module',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestFieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\field_test\\\\FieldDefaultValueCallbackProvider\\:\\:calculateDefaultValue\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestFieldHooks\\:\\:fieldStorageConfigUpdateForbid\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/FieldDefaultValueCallbackProvider.php',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestFieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\field_test\\\\Form\\\\NestedEntityTestForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestFieldHooks\\:\\:fieldWidgetInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Form/NestedEntityTestForm.php',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestFieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\field_test\\\\Form\\\\NestedEntityTestForm\\:\\:validateForm\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Form/NestedEntityTestForm.php',
-];
-$ignoreErrors[] = [
-	// identifier: empty.variable
-	'message' => '#^Variable \\$items in empty\\(\\) always exists and is not falsy\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Plugin/Field/FieldFormatter/TestFieldEmptySettingFormatter.php',
-];
-$ignoreErrors[] = [
-	// identifier: empty.variable
-	'message' => '#^Variable \\$items in empty\\(\\) always exists and is not falsy\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestHooks\\:\\:entityBundleFieldInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Plugin/Field/FieldFormatter/TestFieldMultipleFormatter.php',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\field_test\\\\Plugin\\\\Field\\\\FieldFormatter\\\\TestFieldPrepareViewFormatter\\:\\:prepareView\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestHooks\\:\\:entityDisplayBuildAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Plugin/Field/FieldFormatter/TestFieldPrepareViewFormatter.php',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\field_test\\\\Plugin\\\\Field\\\\FieldType\\\\TestItem\\:\\:delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestHooks\\:\\:entityExtraFieldInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestItem.php',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\field_test\\\\Plugin\\\\Field\\\\FieldWidget\\\\TestFieldWidgetMultiple\\:\\:multipleValidate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestHooks\\:\\:fieldInfoEntityTypeUiDefinitionsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidgetMultiple.php',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_test_boolean_access_denied_entity_field_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestHooks\\:\\:fieldUiPreconfiguredOptionsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_test_boolean_access_denied/field_test_boolean_access_denied.module',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_third_party_test_field_formatter_settings_summary_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestHooks\\:\\:fieldWidgetCompleteFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_third_party_test/field_third_party_test.module',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_third_party_test_field_formatter_third_party_settings_form\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestHooks\\:\\:fieldWidgetCompleteTestFieldWidgetMultipleFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_third_party_test/field_third_party_test.module',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_third_party_test_field_widget_settings_summary_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestHooks\\:\\:fieldWidgetCompleteTestFieldWidgetMultipleSingleValueFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_third_party_test/field_third_party_test.module',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_third_party_test_field_widget_third_party_settings_form\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestHooks\\:\\:fieldWidgetSingleElementFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field/tests/modules/field_third_party_test/field_third_party_test.module',
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestHooks\\:\\:queryEfqMetadataTestAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_test\\\\Hook\\\\FieldTestHooks\\:\\:queryEfqTablePrefixingTestAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: empty.variable
+	'message' => '#^Variable \\$items in empty\\(\\) always exists and is not falsy\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Plugin/Field/FieldFormatter/TestFieldEmptySettingFormatter.php',
+];
+$ignoreErrors[] = [
+	// identifier: empty.variable
+	'message' => '#^Variable \\$items in empty\\(\\) always exists and is not falsy\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Plugin/Field/FieldFormatter/TestFieldMultipleFormatter.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_test\\\\Plugin\\\\Field\\\\FieldFormatter\\\\TestFieldPrepareViewFormatter\\:\\:prepareView\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Plugin/Field/FieldFormatter/TestFieldPrepareViewFormatter.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_test\\\\Plugin\\\\Field\\\\FieldType\\\\TestItem\\:\\:delete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Plugin/Field/FieldType/TestItem.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_test\\\\Plugin\\\\Field\\\\FieldWidget\\\\TestFieldWidgetMultiple\\:\\:multipleValidate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidgetMultiple.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_test_boolean_access_denied\\\\Hook\\\\FieldTestBooleanAccessDeniedHooks\\:\\:entityFieldAccess\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field/tests/modules/field_test_boolean_access_denied/src/Hook/FieldTestBooleanAccessDeniedHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_third_party_test\\\\Hook\\\\FieldThirdPartyTestHooks\\:\\:fieldFormatterSettingsSummaryAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_third_party_test\\\\Hook\\\\FieldThirdPartyTestHooks\\:\\:fieldFormatterThirdPartySettingsForm\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_third_party_test\\\\Hook\\\\FieldThirdPartyTestHooks\\:\\:fieldWidgetSettingsSummaryAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_third_party_test\\\\Hook\\\\FieldThirdPartyTestHooks\\:\\:fieldWidgetThirdPartySettingsForm\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -20566,18 +20572,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/field/tests/src/Unit/Plugin/migrate/process/d6/FieldSettingsTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_layout_entity_view_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field_layout/field_layout.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_layout_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field_layout/field_layout.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\field_layout\\\\Entity\\\\FieldLayoutEntityFormDisplay\\:\\:init\\(\\) has no return type specified\\.$#',
@@ -20694,57 +20688,27 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_layout_test_layout_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_layout\\\\Hook\\\\FieldLayoutHooks\\:\\:entityViewAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field_layout/tests/modules/field_layout_test/field_layout_test.module',
+	'path' => __DIR__ . '/modules/field_layout/src/Hook/FieldLayoutHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\field_layout_test\\\\Form\\\\EmbeddedForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_layout\\\\Hook\\\\FieldLayoutHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field_layout/tests/modules/field_layout_test/src/Form/EmbeddedForm.php',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_ui_entity_bundle_create\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field_ui/field_ui.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_ui_entity_form_mode_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field_ui/field_ui.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_ui_entity_form_mode_presave\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field_ui/field_ui.module',
+	'path' => __DIR__ . '/modules/field_layout/src/Hook/FieldLayoutHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_ui_entity_operation\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field_ui/field_ui.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_ui_entity_type_build\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field_ui/field_ui.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_ui_entity_view_mode_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_layout_test\\\\Form\\\\EmbeddedForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field_ui/field_ui.module',
+	'path' => __DIR__ . '/modules/field_layout/tests/modules/field_layout_test/src/Form/EmbeddedForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_ui_entity_view_mode_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_layout_test\\\\Hook\\\\FieldLayoutTestHooks\\:\\:layoutAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field_ui/field_ui.module',
+	'path' => __DIR__ . '/modules/field_layout/tests/modules/field_layout_test/src/Hook/FieldLayoutTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -20752,18 +20716,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/field_ui/field_ui.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_ui_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field_ui/field_ui.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function field_ui_local_tasks_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/field_ui/field_ui.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function field_ui_preprocess_form_element__new_storage_type\\(\\) has no return type specified\\.$#',
@@ -21088,6 +21040,60 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/field_ui/src/Form/FieldStorageReuseForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_ui\\\\Hook\\\\FieldUiHooks\\:\\:entityBundleCreate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field_ui/src/Hook/FieldUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_ui\\\\Hook\\\\FieldUiHooks\\:\\:entityFormModeDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field_ui/src/Hook/FieldUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_ui\\\\Hook\\\\FieldUiHooks\\:\\:entityFormModePresave\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field_ui/src/Hook/FieldUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_ui\\\\Hook\\\\FieldUiHooks\\:\\:entityOperation\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field_ui/src/Hook/FieldUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_ui\\\\Hook\\\\FieldUiHooks\\:\\:entityTypeBuild\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field_ui/src/Hook/FieldUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_ui\\\\Hook\\\\FieldUiHooks\\:\\:entityViewModeDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field_ui/src/Hook/FieldUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_ui\\\\Hook\\\\FieldUiHooks\\:\\:entityViewModePresave\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field_ui/src/Hook/FieldUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_ui\\\\Hook\\\\FieldUiHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field_ui/src/Hook/FieldUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\field_ui\\\\Hook\\\\FieldUiHooks\\:\\:localTasksAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/field_ui/src/Hook/FieldUiHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\field_ui\\\\Plugin\\\\Derivative\\\\FieldUiLocalAction\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
@@ -21126,15 +21132,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_ui_test_field_config_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function field_ui_test_region_callback\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/field_ui/tests/modules/field_ui_test/field_ui_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function field_ui_test_region_callback\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\field_ui_test\\\\Hook\\\\FieldUiTestHooks\\:\\:fieldConfigAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/field_ui/tests/modules/field_ui_test/field_ui_test.module',
+	'path' => __DIR__ . '/modules/field_ui/tests/modules/field_ui_test/src/Hook/FieldUiTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -21346,36 +21352,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/file/file.install',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function file_cron\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/file/file.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function file_field_widget_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/file/file.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function file_file_download\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/file/file.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function file_file_predelete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/file/file.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function file_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/file/file.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function file_managed_file_submit\\(\\) has no return type specified\\.$#',
@@ -21388,18 +21364,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/file/file.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function file_token_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/file/file.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function file_tokens\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/file/file.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function template_preprocess_file_link\\(\\) has no return type specified\\.$#',
@@ -21430,18 +21394,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/file/file.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function file_field_views_data\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/file/file.views.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function file_field_views_data_views_data_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/file/file.views.inc',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\file\\\\ComputedFileUrl\\:\\:setValue\\(\\) has no return type specified\\.$#',
@@ -21628,6 +21580,60 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/file/src/FileUsage/FileUsageInterface.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/file/src/Hook/FileHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileHooks\\:\\:fieldWidgetInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/file/src/Hook/FileHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileHooks\\:\\:fileDownload\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/file/src/Hook/FileHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileHooks\\:\\:filePredelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/file/src/Hook/FileHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/file/src/Hook/FileHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileHooks\\:\\:tokenInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/file/src/Hook/FileHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileHooks\\:\\:tokens\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/file/src/Hook/FileHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileViewsHooks\\:\\:fieldViewsData\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/file/src/Hook/FileViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\file\\\\Hook\\\\FileViewsHooks\\:\\:fieldViewsDataViewsDataAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/file/src/Hook/FileViewsHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: empty.variable
 	'message' => '#^Variable \\$rows in empty\\(\\) always exists and is not falsy\\.$#',
@@ -21810,111 +21816,111 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_file_copy\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function file_test_file_scan_callback_reset\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_file_download\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function file_test_reset\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_file_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function file_test_set_return\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_file_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function file_test_validator\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_file_mimetype_mapping_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Form\\\\FileTestForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Form/FileTestForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_file_move\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Form\\\\FileTestForm\\:\\:validateForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Form/FileTestForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_file_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Form\\\\FileTestSaveUploadFromForm\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_file_scan_callback_reset\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Form\\\\FileTestSaveUploadFromForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_file_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Form\\\\FileTestSaveUploadFromForm\\:\\:validateForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_file_url_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Hook\\\\FileTestHooks\\:\\:fileCopy\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Hook/FileTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_reset\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Hook\\\\FileTestHooks\\:\\:fileDownload\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Hook/FileTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_set_return\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Hook\\\\FileTestHooks\\:\\:fileInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Hook/FileTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function file_test_validator\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Hook\\\\FileTestHooks\\:\\:fileLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/file_test.module',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Hook/FileTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\file_test\\\\Form\\\\FileTestForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Hook\\\\FileTestHooks\\:\\:fileMimetypeMappingAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/src/Form/FileTestForm.php',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Hook/FileTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\file_test\\\\Form\\\\FileTestForm\\:\\:validateForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Hook\\\\FileTestHooks\\:\\:fileMove\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/src/Form/FileTestForm.php',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Hook/FileTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\file_test\\\\Form\\\\FileTestSaveUploadFromForm\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Hook\\\\FileTestHooks\\:\\:filePredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Hook/FileTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\file_test\\\\Form\\\\FileTestSaveUploadFromForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Hook\\\\FileTestHooks\\:\\:fileUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Hook/FileTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\file_test\\\\Form\\\\FileTestSaveUploadFromForm\\:\\:validateForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\file_test\\\\Hook\\\\FileTestHooks\\:\\:fileUrlAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/file/tests/file_test/src/Form/FileTestSaveUploadFromForm.php',
+	'path' => __DIR__ . '/modules/file/tests/file_test/src/Hook/FileTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -22588,24 +22594,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/filter/filter.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function filter_filter_secure_image_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/filter/filter.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function filter_formats_reset\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/filter/filter.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function filter_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/filter/filter.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function template_preprocess_filter_guidelines\\(\\) has no return type specified\\.$#',
@@ -22780,6 +22774,18 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/filter/src/Form/FilterDisableForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\filter\\\\Hook\\\\FilterHooks\\:\\:filterSecureImageAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/filter/src/Hook/FilterHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\filter\\\\Hook\\\\FilterHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/filter/src/Hook/FilterHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\filter\\\\Plugin\\\\Filter\\\\FilterHtml\\:\\:filterElementAttributes\\(\\) has no return type specified\\.$#',
@@ -22812,27 +22818,27 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function filter_test_filter_format_disable\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\filter_test\\\\Form\\\\FilterTestFormatForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/filter/tests/filter_test/filter_test.module',
+	'path' => __DIR__ . '/modules/filter/tests/filter_test/src/Form/FilterTestFormatForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function filter_test_filter_format_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\filter_test\\\\Hook\\\\FilterTestHooks\\:\\:filterFormatDisable\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/filter/tests/filter_test/filter_test.module',
+	'path' => __DIR__ . '/modules/filter/tests/filter_test/src/Hook/FilterTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function filter_test_filter_format_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\filter_test\\\\Hook\\\\FilterTestHooks\\:\\:filterFormatInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/filter/tests/filter_test/filter_test.module',
+	'path' => __DIR__ . '/modules/filter/tests/filter_test/src/Hook/FilterTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\filter_test\\\\Form\\\\FilterTestFormatForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\filter_test\\\\Hook\\\\FilterTestHooks\\:\\:filterFormatUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/filter/tests/filter_test/src/Form/FilterTestFormatForm.php',
+	'path' => __DIR__ . '/modules/filter/tests/filter_test/src/Hook/FilterTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -22962,81 +22968,81 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function help_block_view_help_block_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function help_preprocess_block\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/help/help.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function help_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help\\\\HelpSectionManager\\:\\:clearCachedDefinitions\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/help.module',
+	'path' => __DIR__ . '/modules/help/src/HelpSectionManager.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function help_modules_installed\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help\\\\HelpSectionManager\\:\\:setSearchManager\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/help.module',
+	'path' => __DIR__ . '/modules/help/src/HelpSectionManager.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function help_modules_uninstalled\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help\\\\HelpTopicPluginBase\\:\\:getProvider\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/help.module',
+	'path' => __DIR__ . '/modules/help/src/HelpTopicPluginBase.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function help_preprocess_block\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help\\\\HelpTopicTwigLoader\\:\\:addExtension\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/help.module',
+	'path' => __DIR__ . '/modules/help/src/HelpTopicTwigLoader.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function help_themes_installed\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help\\\\HelpTwigExtension\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/help.module',
+	'path' => __DIR__ . '/modules/help/src/HelpTwigExtension.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function help_themes_uninstalled\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help\\\\HelpTwigExtension\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/help.module',
+	'path' => __DIR__ . '/modules/help/src/HelpTwigExtension.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\help\\\\HelpSectionManager\\:\\:clearCachedDefinitions\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help\\\\Hook\\\\HelpHooks\\:\\:blockViewHelpBlockAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/src/HelpSectionManager.php',
+	'path' => __DIR__ . '/modules/help/src/Hook/HelpHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\help\\\\HelpSectionManager\\:\\:setSearchManager\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help\\\\Hook\\\\HelpHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/src/HelpSectionManager.php',
+	'path' => __DIR__ . '/modules/help/src/Hook/HelpHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\help\\\\HelpTopicPluginBase\\:\\:getProvider\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help\\\\Hook\\\\HelpHooks\\:\\:modulesInstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/src/HelpTopicPluginBase.php',
+	'path' => __DIR__ . '/modules/help/src/Hook/HelpHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\help\\\\HelpTopicTwigLoader\\:\\:addExtension\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help\\\\Hook\\\\HelpHooks\\:\\:modulesUninstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/src/HelpTopicTwigLoader.php',
+	'path' => __DIR__ . '/modules/help/src/Hook/HelpHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\help\\\\HelpTwigExtension\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help\\\\Hook\\\\HelpHooks\\:\\:themesInstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/src/HelpTwigExtension.php',
+	'path' => __DIR__ . '/modules/help/src/Hook/HelpHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\help\\\\HelpTwigExtension\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help\\\\Hook\\\\HelpHooks\\:\\:themesUninstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/src/HelpTwigExtension.php',
+	'path' => __DIR__ . '/modules/help/src/Hook/HelpHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -23076,27 +23082,27 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function help_page_test_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help_page_test\\\\Hook\\\\HelpPageTestHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/tests/modules/help_page_test/help_page_test.module',
+	'path' => __DIR__ . '/modules/help/tests/modules/help_page_test/src/Hook/HelpPageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function help_test_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help_test\\\\Hook\\\\HelpTestHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/tests/modules/help_test/help_test.module',
+	'path' => __DIR__ . '/modules/help/tests/modules/help_test/src/Hook/HelpTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function help_topics_test_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help_topics_test\\\\Hook\\\\HelpTopicsTestHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/tests/modules/help_topics_test/help_topics_test.module',
+	'path' => __DIR__ . '/modules/help/tests/modules/help_topics_test/src/Hook/HelpTopicsTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function help_topics_test_help_topics_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\help_topics_test\\\\Hook\\\\HelpTopicsTestHooks\\:\\:helpTopicsInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/tests/modules/help_topics_test/help_topics_test.module',
+	'path' => __DIR__ . '/modules/help/tests/modules/help_topics_test/src/Hook/HelpTopicsTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -23106,15 +23112,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function more_help_page_test_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\more_help_page_test\\\\Hook\\\\MoreHelpPageTestHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/tests/modules/more_help_page_test/more_help_page_test.module',
+	'path' => __DIR__ . '/modules/help/tests/modules/more_help_page_test/src/Hook/MoreHelpPageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function more_help_page_test_help_section_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\more_help_page_test\\\\Hook\\\\MoreHelpPageTestHooks\\:\\:helpSectionInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/help/tests/modules/more_help_page_test/more_help_page_test.module',
+	'path' => __DIR__ . '/modules/help/tests/modules/more_help_page_test/src/Hook/MoreHelpPageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -23154,57 +23160,57 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function history_cron\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function history_write\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/history/history.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function history_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\history\\\\Controller\\\\HistoryController\\:\\:readNode\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/history/history.module',
+	'path' => __DIR__ . '/modules/history/src/Controller/HistoryController.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function history_node_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\history\\\\Hook\\\\HistoryHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/history/history.module',
+	'path' => __DIR__ . '/modules/history/src/Hook/HistoryHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function history_node_view_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\history\\\\Hook\\\\HistoryHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/history/history.module',
+	'path' => __DIR__ . '/modules/history/src/Hook/HistoryHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function history_user_cancel\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\history\\\\Hook\\\\HistoryHooks\\:\\:nodeDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/history/history.module',
+	'path' => __DIR__ . '/modules/history/src/Hook/HistoryHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function history_user_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\history\\\\Hook\\\\HistoryHooks\\:\\:nodeViewAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/history/history.module',
+	'path' => __DIR__ . '/modules/history/src/Hook/HistoryHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function history_write\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\history\\\\Hook\\\\HistoryHooks\\:\\:userCancel\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/history/history.module',
+	'path' => __DIR__ . '/modules/history/src/Hook/HistoryHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function history_views_data\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\history\\\\Hook\\\\HistoryHooks\\:\\:userDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/history/history.views.inc',
+	'path' => __DIR__ . '/modules/history/src/Hook/HistoryHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\history\\\\Controller\\\\HistoryController\\:\\:readNode\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\history\\\\Hook\\\\HistoryViewsHooks\\:\\:viewsData\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/history/src/Controller/HistoryController.php',
+	'path' => __DIR__ . '/modules/history/src/Hook/HistoryViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -23332,60 +23338,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/image/image.install',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function image_entity_presave\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/image/image.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function image_field_config_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/image/image.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function image_field_config_update\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/image/image.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function image_field_storage_config_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/image/image.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function image_field_storage_config_update\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/image/image.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function image_file_download\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/image/image.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function image_file_move\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/image/image.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function image_file_predelete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/image/image.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function image_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/image/image.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function image_path_flush\\(\\) has no return type specified\\.$#',
@@ -23398,18 +23350,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/image/image.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function image_field_views_data\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/image/image.views.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function image_field_views_data_views_data_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/image/image.views.inc',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\image\\\\ConfigurableImageEffectBase\\:\\:submitConfigurationForm\\(\\) has no return type specified\\.$#',
@@ -23572,6 +23512,72 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/image/src/Form/ImageStyleFormBase.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\image\\\\Hook\\\\ImageHooks\\:\\:entityPresave\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/image/src/Hook/ImageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\image\\\\Hook\\\\ImageHooks\\:\\:fieldConfigDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/image/src/Hook/ImageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\image\\\\Hook\\\\ImageHooks\\:\\:fieldConfigUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/image/src/Hook/ImageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\image\\\\Hook\\\\ImageHooks\\:\\:fieldStorageConfigDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/image/src/Hook/ImageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\image\\\\Hook\\\\ImageHooks\\:\\:fieldStorageConfigUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/image/src/Hook/ImageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\image\\\\Hook\\\\ImageHooks\\:\\:fileDownload\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/image/src/Hook/ImageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\image\\\\Hook\\\\ImageHooks\\:\\:fileMove\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/image/src/Hook/ImageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\image\\\\Hook\\\\ImageHooks\\:\\:filePredelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/image/src/Hook/ImageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\image\\\\Hook\\\\ImageHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/image/src/Hook/ImageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\image\\\\Hook\\\\ImageViewsHooks\\:\\:fieldViewsData\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/image/src/Hook/ImageViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\image\\\\Hook\\\\ImageViewsHooks\\:\\:fieldViewsDataViewsDataAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/image/src/Hook/ImageViewsHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\image\\\\ImageEffectBase\\:\\:setConfiguration\\(\\) has no return type specified\\.$#',
@@ -23754,9 +23760,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function image_access_test_hidden_entity_field_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\image_access_test_hidden\\\\Hook\\\\ImageAccessTestHiddenHooks\\:\\:entityFieldAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/image/tests/modules/image_access_test_hidden/image_access_test_hidden.module',
+	'path' => __DIR__ . '/modules/image/tests/modules/image_access_test_hidden/src/Hook/ImageAccessTestHiddenHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -23766,21 +23772,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function image_module_test_image_effect_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\image_module_test\\\\Hook\\\\ImageModuleTestHooks\\:\\:imageEffectInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/image/tests/modules/image_module_test/image_module_test.module',
+	'path' => __DIR__ . '/modules/image/tests/modules/image_module_test/src/Hook/ImageModuleTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function image_module_test_image_style_flush\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\image_module_test\\\\Hook\\\\ImageModuleTestHooks\\:\\:imageStyleFlush\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/image/tests/modules/image_module_test/image_module_test.module',
+	'path' => __DIR__ . '/modules/image/tests/modules/image_module_test/src/Hook/ImageModuleTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function image_module_test_image_style_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\image_module_test\\\\Hook\\\\ImageModuleTestHooks\\:\\:imageStylePresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/image/tests/modules/image_module_test/image_module_test.module',
+	'path' => __DIR__ . '/modules/image/tests/modules/image_module_test/src/Hook/ImageModuleTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -24040,18 +24046,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/inline_form_errors/inline_form_errors.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function inline_form_errors_element_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/inline_form_errors/inline_form_errors.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function inline_form_errors_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/inline_form_errors/inline_form_errors.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function inline_form_errors_preprocess_datetime_wrapper\\(\\) has no return type specified\\.$#',
@@ -24094,6 +24088,18 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/inline_form_errors/src/FormErrorHandler.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\inline_form_errors\\\\Hook\\\\InlineFormErrorsHooks\\:\\:elementInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/inline_form_errors/src/Hook/InlineFormErrorsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\inline_form_errors\\\\Hook\\\\InlineFormErrorsHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/inline_form_errors/src/Hook/InlineFormErrorsHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\inline_form_errors\\\\InlineFormErrorsServiceProvider\\:\\:alter\\(\\) has no return type specified\\.$#',
@@ -24114,285 +24120,285 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_entity_bundle_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\EntityAccessChecker\\:\\:setLatestRevisionCheck\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Access/EntityAccessChecker.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_entity_bundle_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:addConditionFieldPrefix\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_entity_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:applyAccessConditions\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_entity_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:applyAccessControls\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:buildTree\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_jsonapi_block_content_filter_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:secureQuery\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_jsonapi_comment_filter_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:setFieldManager\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_jsonapi_entity_filter_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:setModuleHandler\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
 ];
 $ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function jsonapi_jsonapi_entity_test_filter_access\\(\\) has no return type specified\\.$#',
+	// identifier: empty.variable
+	'message' => '#^Variable \\$reason in empty\\(\\) always exists and is not falsy\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Context/FieldResolver.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_jsonapi_file_filter_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Controller\\\\EntityResource\\:\\:doPatchIndividualRelationship\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Controller/EntityResource.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_jsonapi_media_filter_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Controller\\\\EntityResource\\:\\:doPatchMultipleRelationship\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Controller/EntityResource.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_jsonapi_node_filter_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Controller\\\\EntityResource\\:\\:updateEntityField\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Controller/EntityResource.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_jsonapi_shortcut_filter_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Controller\\\\EntityResource\\:\\:validate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Controller/EntityResource.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_jsonapi_taxonomy_term_filter_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Controller\\\\FileUpload\\:\\:ensureFileUploadAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Controller/FileUpload.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_jsonapi_user_filter_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Controller\\\\FileUpload\\:\\:validate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/Controller/FileUpload.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_jsonapi_workspace_filter_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\DefaultExceptionSubscriber\\:\\:onException\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_modules_installed\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\DefaultExceptionSubscriber\\:\\:setEventResponse\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/jsonapi.module',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\EntityAccessChecker\\:\\:setLatestRevisionCheck\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\JsonApiRequestValidator\\:\\:onRequest\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Access/EntityAccessChecker.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/JsonApiRequestValidator.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:addConditionFieldPrefix\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\JsonApiRequestValidator\\:\\:onResponse\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/JsonApiRequestValidator.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:applyAccessConditions\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\JsonapiMaintenanceModeSubscriber\\:\\:onMaintenanceModeRequest\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/JsonapiMaintenanceModeSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:applyAccessControls\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceObjectNormalizationCacher\\:\\:onTerminate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceObjectNormalizationCacher.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:buildTree\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceObjectNormalizationCacher\\:\\:saveOnTerminate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceObjectNormalizationCacher.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:secureQuery\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceObjectNormalizationCacher\\:\\:set\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceObjectNormalizationCacher.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:setFieldManager\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceObjectNormalizationCacher\\:\\:setRequestStack\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceObjectNormalizationCacher.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Access\\\\TemporaryQueryGuard\\:\\:setModuleHandler\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceObjectNormalizationCacher\\:\\:setVariationCache\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Access/TemporaryQueryGuard.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceObjectNormalizationCacher.php',
 ];
 $ignoreErrors[] = [
-	// identifier: empty.variable
-	'message' => '#^Variable \\$reason in empty\\(\\) always exists and is not falsy\\.$#',
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceResponseSubscriber\\:\\:onResponse\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Context/FieldResolver.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Controller\\\\EntityResource\\:\\:doPatchIndividualRelationship\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceResponseSubscriber\\:\\:renderResponseBody\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Controller/EntityResource.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Controller\\\\EntityResource\\:\\:doPatchMultipleRelationship\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceResponseValidator\\:\\:onResponse\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Controller/EntityResource.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceResponseValidator.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Controller\\\\EntityResource\\:\\:updateEntityField\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceResponseValidator\\:\\:setValidator\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Controller/EntityResource.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceResponseValidator.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Controller\\\\EntityResource\\:\\:validate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Exception\\\\UnprocessableHttpEntityException\\:\\:setViolations\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Controller/EntityResource.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Exception/UnprocessableHttpEntityException.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Controller\\\\FileUpload\\:\\:ensureFileUploadAccess\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:entityBundleCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Controller/FileUpload.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Controller\\\\FileUpload\\:\\:validate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:entityBundleDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Controller/FileUpload.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\DefaultExceptionSubscriber\\:\\:onException\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:entityCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\DefaultExceptionSubscriber\\:\\:setEventResponse\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:entityDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/DefaultExceptionSubscriber.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\JsonApiRequestValidator\\:\\:onRequest\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/JsonApiRequestValidator.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\JsonApiRequestValidator\\:\\:onResponse\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:jsonapiBlockContentFilterAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/JsonApiRequestValidator.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\JsonapiMaintenanceModeSubscriber\\:\\:onMaintenanceModeRequest\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:jsonapiCommentFilterAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/JsonapiMaintenanceModeSubscriber.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceObjectNormalizationCacher\\:\\:onTerminate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:jsonapiEntityFilterAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceObjectNormalizationCacher.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceObjectNormalizationCacher\\:\\:saveOnTerminate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:jsonapiEntityTestFilterAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceObjectNormalizationCacher.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceObjectNormalizationCacher\\:\\:set\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:jsonapiFileFilterAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceObjectNormalizationCacher.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceObjectNormalizationCacher\\:\\:setRequestStack\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:jsonapiMediaFilterAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceObjectNormalizationCacher.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceObjectNormalizationCacher\\:\\:setVariationCache\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:jsonapiNodeFilterAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceObjectNormalizationCacher.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceResponseSubscriber\\:\\:onResponse\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:jsonapiShortcutFilterAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceResponseSubscriber\\:\\:renderResponseBody\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:jsonapiTaxonomyTermFilterAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceResponseSubscriber.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceResponseValidator\\:\\:onResponse\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:jsonapiUserFilterAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceResponseValidator.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\EventSubscriber\\\\ResourceResponseValidator\\:\\:setValidator\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:jsonapiWorkspaceFilterAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/EventSubscriber/ResourceResponseValidator.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\jsonapi\\\\Exception\\\\UnprocessableHttpEntityException\\:\\:setViolations\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi\\\\Hook\\\\JsonapiHooks\\:\\:modulesInstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/src/Exception/UnprocessableHttpEntityException.php',
+	'path' => __DIR__ . '/modules/jsonapi/src/Hook/JsonapiHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -24564,9 +24570,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_test_field_access_entity_field_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi_test_field_access\\\\Hook\\\\JsonapiTestFieldAccessHooks\\:\\:entityFieldAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.module',
+	'path' => __DIR__ . '/modules/jsonapi/tests/modules/jsonapi_test_field_access/src/Hook/JsonapiTestFieldAccessHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -24576,15 +24582,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_test_non_cacheable_methods_entity_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi_test_non_cacheable_methods\\\\Hook\\\\JsonapiTestNonCacheableMethodsHooks\\:\\:entityPredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/tests/modules/jsonapi_test_non_cacheable_methods/jsonapi_test_non_cacheable_methods.module',
+	'path' => __DIR__ . '/modules/jsonapi/tests/modules/jsonapi_test_non_cacheable_methods/src/Hook/JsonapiTestNonCacheableMethodsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_test_non_cacheable_methods_entity_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi_test_non_cacheable_methods\\\\Hook\\\\JsonapiTestNonCacheableMethodsHooks\\:\\:entityPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/tests/modules/jsonapi_test_non_cacheable_methods/jsonapi_test_non_cacheable_methods.module',
+	'path' => __DIR__ . '/modules/jsonapi/tests/modules/jsonapi_test_non_cacheable_methods/src/Hook/JsonapiTestNonCacheableMethodsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -24612,9 +24618,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jsonapi_test_user_user_format_name_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jsonapi_test_user\\\\Hook\\\\JsonapiTestUserHooks\\:\\:userFormatNameAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/jsonapi/tests/modules/jsonapi_test_user/jsonapi_test_user.module',
+	'path' => __DIR__ . '/modules/jsonapi/tests/modules/jsonapi_test_user/src/Hook/JsonapiTestUserHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -25114,60 +25120,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/language/language.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function language_element_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/language/language.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function language_entity_base_field_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/language/language.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function language_entity_bundle_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/language/language.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function language_entity_field_access\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/language/language.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function language_field_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/language/language.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function language_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/language/language.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function language_language_types_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/language/language.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function language_modules_installed\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/language/language.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function language_modules_uninstalled\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/language/language.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function language_negotiation_url_prefixes_update\\(\\) has no return type specified\\.$#',
@@ -25180,12 +25132,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/language/language.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function language_tour_tips_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/language/language.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\language\\\\Config\\\\LanguageConfigFactoryOverride\\:\\:addCollections\\(\\) has no return type specified\\.$#',
@@ -25510,6 +25456,60 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/language/src/Form/NegotiationUrlForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\language\\\\Hook\\\\LanguageHooks\\:\\:elementInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/language/src/Hook/LanguageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\language\\\\Hook\\\\LanguageHooks\\:\\:entityBaseFieldInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/language/src/Hook/LanguageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\language\\\\Hook\\\\LanguageHooks\\:\\:entityBundleDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/language/src/Hook/LanguageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\language\\\\Hook\\\\LanguageHooks\\:\\:entityFieldAccess\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/language/src/Hook/LanguageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\language\\\\Hook\\\\LanguageHooks\\:\\:fieldInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/language/src/Hook/LanguageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\language\\\\Hook\\\\LanguageHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/language/src/Hook/LanguageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\language\\\\Hook\\\\LanguageHooks\\:\\:languageTypesInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/language/src/Hook/LanguageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\language\\\\Hook\\\\LanguageHooks\\:\\:modulesInstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/language/src/Hook/LanguageHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\language\\\\Hook\\\\LanguageHooks\\:\\:tourTipsAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/language/src/Hook/LanguageHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\language\\\\HttpKernel\\\\PathProcessorLanguage\\:\\:initConfigSubscriber\\(\\) has no return type specified\\.$#',
@@ -25722,87 +25722,87 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function language_entity_field_access_test_entity_field_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_entity_field_access_test\\\\Hook\\\\LanguageEntityFieldAccessTestHooks\\:\\:entityFieldAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_entity_field_access_test/language_entity_field_access_test.module',
+	'path' => __DIR__ . '/modules/language/tests/language_entity_field_access_test/src/Hook/LanguageEntityFieldAccessTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function language_test_language_fallback_candidates_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function language_test_store_language_negotiation\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/language/tests/language_test/language_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function language_test_language_fallback_candidates_test_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_test\\\\Controller\\\\LanguageTestController\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_test/language_test.module',
+	'path' => __DIR__ . '/modules/language/tests/language_test/src/Controller/LanguageTestController.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function language_test_language_negotiation_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_test\\\\Controller\\\\LanguageTestController\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_test/language_test.module',
+	'path' => __DIR__ . '/modules/language/tests/language_test/src/Controller/LanguageTestController.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function language_test_language_switch_links_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_test\\\\Controller\\\\LanguageTestController\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_test/language_test.module',
+	'path' => __DIR__ . '/modules/language/tests/language_test/src/Controller/LanguageTestController.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function language_test_language_types_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_test\\\\Controller\\\\LanguageTestController\\:\\:typeLinkActiveClass\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_test/language_test.module',
+	'path' => __DIR__ . '/modules/language/tests/language_test/src/Controller/LanguageTestController.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function language_test_language_types_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_test\\\\Hook\\\\LanguageTestHooks\\:\\:languageFallbackCandidatesAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_test/language_test.module',
+	'path' => __DIR__ . '/modules/language/tests/language_test/src/Hook/LanguageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function language_test_module_preinstall\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_test\\\\Hook\\\\LanguageTestHooks\\:\\:languageFallbackCandidatesTestAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_test/language_test.module',
+	'path' => __DIR__ . '/modules/language/tests/language_test/src/Hook/LanguageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function language_test_page_top\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_test\\\\Hook\\\\LanguageTestHooks\\:\\:languageNegotiationInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_test/language_test.module',
+	'path' => __DIR__ . '/modules/language/tests/language_test/src/Hook/LanguageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function language_test_store_language_negotiation\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_test\\\\Hook\\\\LanguageTestHooks\\:\\:languageSwitchLinksAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_test/language_test.module',
+	'path' => __DIR__ . '/modules/language/tests/language_test/src/Hook/LanguageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\language_test\\\\Controller\\\\LanguageTestController\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_test\\\\Hook\\\\LanguageTestHooks\\:\\:languageTypesInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_test/src/Controller/LanguageTestController.php',
+	'path' => __DIR__ . '/modules/language/tests/language_test/src/Hook/LanguageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\language_test\\\\Controller\\\\LanguageTestController\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_test\\\\Hook\\\\LanguageTestHooks\\:\\:languageTypesInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_test/src/Controller/LanguageTestController.php',
+	'path' => __DIR__ . '/modules/language/tests/language_test/src/Hook/LanguageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\language_test\\\\Controller\\\\LanguageTestController\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_test\\\\Hook\\\\LanguageTestHooks\\:\\:modulePreinstall\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_test/src/Controller/LanguageTestController.php',
+	'path' => __DIR__ . '/modules/language/tests/language_test/src/Hook/LanguageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\language_test\\\\Controller\\\\LanguageTestController\\:\\:typeLinkActiveClass\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\language_test\\\\Hook\\\\LanguageTestHooks\\:\\:pageTop\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/language/tests/language_test/src/Controller/LanguageTestController.php',
+	'path' => __DIR__ . '/modules/language/tests/language_test/src/Hook/LanguageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -26116,102 +26116,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/layout_builder/layout_builder.install',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_block_content_access\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_cron\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_entity_build_defaults_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_entity_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_entity_presave\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_entity_translation_create\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_entity_view_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_field_config_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_field_config_insert\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function layout_builder_module_implements_alter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_plugin_filter_block__block_ui_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_plugin_filter_block__layout_builder_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_plugin_filter_block_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_plugin_filter_layout__layout_builder_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_plugin_filter_layout_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function layout_builder_preprocess_language_content_settings_table\\(\\) has no return type specified\\.$#',
@@ -26220,27 +26130,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function layout_builder_system_breadcrumb_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_theme_registry_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\layout_builder_expose_all_field_blocks\\\\Hook\\\\LayoutBuilderExposeAllFieldBlocksHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_theme_suggestions_field_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/layout_builder.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function layout_builder_expose_all_field_blocks_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/modules/layout_builder_expose_all_field_blocks/layout_builder_expose_all_field_blocks.module',
+	'path' => __DIR__ . '/modules/layout_builder/modules/layout_builder_expose_all_field_blocks/src/Hook/LayoutBuilderExposeAllFieldBlocksHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -26614,6 +26506,114 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/layout_builder/src/Form/RevertOverridesForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:blockContentAccess\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:entityBuildDefaultsAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:entityDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:entityPresave\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:entityTranslationCreate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:entityViewAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:fieldConfigDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:fieldConfigInsert\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:pluginFilterBlockAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:pluginFilterBlockBlockUiAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:pluginFilterBlockLayoutBuilderAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:pluginFilterLayoutAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:pluginFilterLayoutLayoutBuilderAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:systemBreadcrumbAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:themeRegistryAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\layout_builder\\\\Hook\\\\LayoutBuilderHooks\\:\\:themeSuggestionsFieldAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/src/Hook/LayoutBuilderHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\layout_builder\\\\InlineBlockEntityOperations\\:\\:create\\(\\) has no return type specified\\.$#',
@@ -26910,15 +26910,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function layout_builder_extra_field_test_entity_extra_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function layout_builder_extra_field_test_node_view\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_extra_field_test/layout_builder_extra_field_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function layout_builder_extra_field_test_node_view\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\layout_builder_extra_field_test\\\\Hook\\\\LayoutBuilderExtraFieldTestHooks\\:\\:entityExtraFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_extra_field_test/layout_builder_extra_field_test.module',
+	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_extra_field_test/src/Hook/LayoutBuilderExtraFieldTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -26934,51 +26934,51 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function layout_builder_entity_form_display_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function layout_builder_test_module_implements_alter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function layout_builder_test_entity_extra_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function layout_builder_test_node_view\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function layout_builder_test_module_implements_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function layout_builder_test_preprocess_layout__onecol\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function layout_builder_test_node_view\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function layout_builder_test_preprocess_layout__twocol_section\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function layout_builder_test_plugin_filter_block__layout_builder_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\layout_builder_test\\\\Hook\\\\LayoutBuilderTestHooks\\:\\:entityExtraFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module',
+	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function layout_builder_test_preprocess_layout__onecol\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\layout_builder_test\\\\Hook\\\\LayoutBuilderTestHooks\\:\\:layoutBuilderEntityFormDisplayAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module',
+	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function layout_builder_test_preprocess_layout__twocol_section\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\layout_builder_test\\\\Hook\\\\LayoutBuilderTestHooks\\:\\:pluginFilterBlockLayoutBuilderAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module',
+	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function layout_builder_test_system_breadcrumb_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\layout_builder_test\\\\Hook\\\\LayoutBuilderTestHooks\\:\\:systemBreadcrumbAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module',
+	'path' => __DIR__ . '/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -27454,6 +27454,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/layout_builder/tests/src/Kernel/DefaultsSectionStorageTest.php',
 ];
+$ignoreErrors[] = [
+	// identifier: phpunit.covers
+	'message' => '#^@covers value layout_builder_entity_view_alter references an invalid class or function\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/layout_builder/tests/src/Kernel/EntityViewAlterTest.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\Tests\\\\layout_builder\\\\Kernel\\\\FieldBlockTest\\:\\:providerTestBlockAccessEntityAllowedFieldHasValue\\(\\) has no return type specified\\.$#',
@@ -27606,15 +27612,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function layout_discovery_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function template_preprocess_layout\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/layout_discovery/layout_discovery.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function template_preprocess_layout\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\layout_discovery\\\\Hook\\\\LayoutDiscoveryHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/layout_discovery/layout_discovery.module',
+	'path' => __DIR__ . '/modules/layout_discovery/src/Hook/LayoutDiscoveryHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -27630,21 +27636,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function link_field_type_category_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function template_preprocess_link_formatter_link_separate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/link/link.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function link_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\link\\\\Hook\\\\LinkHooks\\:\\:fieldTypeCategoryInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/link/link.module',
+	'path' => __DIR__ . '/modules/link/src/Hook/LinkHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function template_preprocess_link_formatter_link_separate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\link\\\\Hook\\\\LinkHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/link/link.module',
+	'path' => __DIR__ . '/modules/link/src/Hook/LinkHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -27702,9 +27708,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function link_test_base_field_entity_base_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\link_test_base_field\\\\Hook\\\\LinkTestBaseFieldHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/link/tests/modules/link_test_base_field/link_test_base_field.module',
+	'path' => __DIR__ . '/modules/link/tests/modules/link_test_base_field/src/Hook/LinkTestBaseFieldHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -27922,36 +27928,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/locale/locale.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function locale_cache_flush\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/locale.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function locale_configurable_language_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/locale.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function locale_configurable_language_insert\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/locale.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function locale_configurable_language_update\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/locale.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function locale_cron\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/locale.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function locale_form_language_admin_add_form_alter_submit\\(\\) has no return type specified\\.$#',
@@ -27964,36 +27940,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/locale/locale.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function locale_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/locale.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function locale_js_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/locale.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function locale_library_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/locale.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function locale_module_preuninstall\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/locale.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function locale_modules_installed\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/locale.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function locale_preprocess_node\\(\\) has no return type specified\\.$#',
@@ -28030,18 +27976,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/locale/locale.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function locale_themes_installed\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/locale.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function locale_themes_uninstalled\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/locale.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function locale_translation_clear_status\\(\\) has no return type specified\\.$#',
@@ -28216,6 +28150,78 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/locale/src/Form/TranslationStatusForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\locale\\\\Hook\\\\LocaleHooks\\:\\:cacheFlush\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/locale/src/Hook/LocaleHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\locale\\\\Hook\\\\LocaleHooks\\:\\:configurableLanguageDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/locale/src/Hook/LocaleHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\locale\\\\Hook\\\\LocaleHooks\\:\\:configurableLanguageInsert\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/locale/src/Hook/LocaleHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\locale\\\\Hook\\\\LocaleHooks\\:\\:configurableLanguageUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/locale/src/Hook/LocaleHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\locale\\\\Hook\\\\LocaleHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/locale/src/Hook/LocaleHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\locale\\\\Hook\\\\LocaleHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/locale/src/Hook/LocaleHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\locale\\\\Hook\\\\LocaleHooks\\:\\:jsAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/locale/src/Hook/LocaleHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\locale\\\\Hook\\\\LocaleHooks\\:\\:libraryInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/locale/src/Hook/LocaleHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\locale\\\\Hook\\\\LocaleHooks\\:\\:modulePreuninstall\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/locale/src/Hook/LocaleHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\locale\\\\Hook\\\\LocaleHooks\\:\\:modulesInstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/locale/src/Hook/LocaleHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\locale\\\\Hook\\\\LocaleHooks\\:\\:themesInstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/locale/src/Hook/LocaleHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\locale\\\\Hook\\\\LocaleHooks\\:\\:themesUninstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/locale/src/Hook/LocaleHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\locale\\\\LocaleConfigManager\\:\\:deleteLanguageTranslations\\(\\) has no return type specified\\.$#',
@@ -28542,63 +28548,63 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function locale_test_countries_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\locale_test\\\\Hook\\\\LocaleTestHooks\\:\\:countriesAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test/locale_test.module',
+	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test/src/Hook/LocaleTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function locale_test_language_fallback_candidates_locale_lookup_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\locale_test\\\\Hook\\\\LocaleTestHooks\\:\\:languageFallbackCandidatesLocaleLookupAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test/locale_test.module',
+	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test/src/Hook/LocaleTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function locale_test_locale_translation_projects_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\locale_test\\\\Hook\\\\LocaleTestHooks\\:\\:localeTranslationProjectsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test/locale_test.module',
+	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test/src/Hook/LocaleTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function locale_test_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\locale_test\\\\Hook\\\\LocaleTestHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test/locale_test.module',
+	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test/src/Hook/LocaleTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function locale_test_token_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\locale_test\\\\Hook\\\\LocaleTestHooks\\:\\:tokenInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test/locale_test.module',
+	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test/src/Hook/LocaleTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function locale_test_tokens\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\locale_test\\\\Hook\\\\LocaleTestHooks\\:\\:tokens\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test/locale_test.module',
+	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test/src/Hook/LocaleTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function locale_test_development_release_locale_translation_projects_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\locale_test_development_release\\\\Hook\\\\LocaleTestDevelopmentReleaseHooks\\:\\:localeTranslationProjectsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test_development_release/locale_test_development_release.module',
+	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test_development_release/src/Hook/LocaleTestDevelopmentReleaseHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function locale_test_development_release_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\locale_test_development_release\\\\Hook\\\\LocaleTestDevelopmentReleaseHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test_development_release/locale_test_development_release.module',
+	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test_development_release/src/Hook/LocaleTestDevelopmentReleaseHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function locale_test_translate_modules_installed\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\locale_test_translate\\\\Hook\\\\LocaleTestTranslateHooks\\:\\:modulesInstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test_translate/locale_test_translate.module',
+	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test_translate/src/Hook/LocaleTestTranslateHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function locale_test_translate_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\locale_test_translate\\\\Hook\\\\LocaleTestTranslateHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test_translate/locale_test_translate.module',
+	'path' => __DIR__ . '/modules/locale/tests/modules/locale_test_translate/src/Hook/LocaleTestTranslateHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -28846,48 +28852,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/media/media.install',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_entity_access\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media/media.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_field_type_category_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media/media.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_field_ui_preconfigured_options_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media/media.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_field_widget_complete_form_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media/media.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_field_widget_single_element_form_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media/media.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function media_filter_format_edit_form_validate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/media/media.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media/media.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function media_preprocess_media_reference_help\\(\\) has no return type specified\\.$#',
@@ -28900,12 +28870,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/media/media.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_views_query_substitutions\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media/media.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function template_preprocess_media\\(\\) has no return type specified\\.$#',
@@ -28996,6 +28960,48 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/media/src/Form/MediaTypeDeleteConfirmForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media\\\\Hook\\\\MediaHooks\\:\\:entityAccess\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media/src/Hook/MediaHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media\\\\Hook\\\\MediaHooks\\:\\:fieldTypeCategoryInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media/src/Hook/MediaHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media\\\\Hook\\\\MediaHooks\\:\\:fieldUiPreconfiguredOptionsAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media/src/Hook/MediaHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media\\\\Hook\\\\MediaHooks\\:\\:fieldWidgetCompleteFormAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media/src/Hook/MediaHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media\\\\Hook\\\\MediaHooks\\:\\:fieldWidgetSingleElementFormAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media/src/Hook/MediaHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media\\\\Hook\\\\MediaHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media/src/Hook/MediaHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media\\\\Hook\\\\MediaHooks\\:\\:viewsQuerySubstitutions\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media/src/Hook/MediaHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\media\\\\MediaForm\\:\\:form\\(\\) has no return type specified\\.$#',
@@ -29256,21 +29262,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function media_test_embed_entity_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function media_test_embed_preprocess_media_embed_error\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/media/tests/modules/media_test_embed/media_test_embed.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function media_test_embed_entity_view_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\media_test_embed\\\\Hook\\\\MediaTestEmbedHooks\\:\\:entityAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/media/tests/modules/media_test_embed/media_test_embed.module',
+	'path' => __DIR__ . '/modules/media/tests/modules/media_test_embed/src/Hook/MediaTestEmbedHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function media_test_embed_preprocess_media_embed_error\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\media_test_embed\\\\Hook\\\\MediaTestEmbedHooks\\:\\:entityViewAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/media/tests/modules/media_test_embed/media_test_embed.module',
+	'path' => __DIR__ . '/modules/media/tests/modules/media_test_embed/src/Hook/MediaTestEmbedHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -29278,12 +29284,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/media/tests/modules/media_test_embed/src/Routing/RouteSubscriber.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_test_oembed_oembed_resource_url_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media/tests/modules/media_test_oembed/media_test_oembed.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function media_test_oembed_preprocess_media_oembed_iframe\\(\\) has no return type specified\\.$#',
@@ -29302,6 +29302,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/media/tests/modules/media_test_oembed/src/Controller/ResourceController.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media_test_oembed\\\\Hook\\\\MediaTestOembedHooks\\:\\:oembedResourceUrlAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media/tests/modules/media_test_oembed/src/Hook/MediaTestOembedHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\media_test_oembed\\\\MediaTestOembedServiceProvider\\:\\:alter\\(\\) has no return type specified\\.$#',
@@ -29878,36 +29884,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/media_library/media_library.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_library_field_ui_preconfigured_options_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media_library/media_library.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_library_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media_library/media_library.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_library_image_style_access\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media_library/media_library.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_library_local_tasks_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media_library/media_library.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_library_media_source_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media_library/media_library.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function media_library_preprocess_media\\(\\) has no return type specified\\.$#',
@@ -29926,18 +29902,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/media_library/media_library.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_library_views_post_render\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media_library/media_library.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_library_views_pre_render\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media_library/media_library.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function template_preprocess_media_library_item\\(\\) has no return type specified\\.$#',
@@ -29950,12 +29914,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/media_library/media_library.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function media_library_views_data\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/media_library/media_library.views.inc',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\media_library\\\\Ajax\\\\UpdateSelectionCommand\\:\\:render\\(\\) has no return type specified\\.$#',
@@ -30052,6 +30010,54 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/media_library/src/Form/SettingsForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media_library\\\\Hook\\\\MediaLibraryHooks\\:\\:fieldUiPreconfiguredOptionsAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media_library/src/Hook/MediaLibraryHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media_library\\\\Hook\\\\MediaLibraryHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media_library/src/Hook/MediaLibraryHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media_library\\\\Hook\\\\MediaLibraryHooks\\:\\:imageStyleAccess\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media_library/src/Hook/MediaLibraryHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media_library\\\\Hook\\\\MediaLibraryHooks\\:\\:localTasksAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media_library/src/Hook/MediaLibraryHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media_library\\\\Hook\\\\MediaLibraryHooks\\:\\:mediaSourceInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media_library/src/Hook/MediaLibraryHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media_library\\\\Hook\\\\MediaLibraryHooks\\:\\:viewsPostRender\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media_library/src/Hook/MediaLibraryHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media_library\\\\Hook\\\\MediaLibraryHooks\\:\\:viewsPreRender\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media_library/src/Hook/MediaLibraryHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\media_library\\\\Hook\\\\MediaLibraryViewsHooks\\:\\:viewsData\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/media_library/src/Hook/MediaLibraryViewsHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\media_library\\\\MediaLibraryServiceProvider\\:\\:register\\(\\) has no return type specified\\.$#',
@@ -30138,27 +30144,27 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function media_library_test_entity_field_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\media_library_test\\\\Form\\\\TestNodeFormOverride\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/media_library/tests/modules/media_library_test/media_library_test.module',
+	'path' => __DIR__ . '/modules/media_library/tests/modules/media_library_test/src/Form/TestNodeFormOverride.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function media_library_test_field_widget_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\media_library_test\\\\Hook\\\\MediaLibraryTestHooks\\:\\:entityFieldAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/media_library/tests/modules/media_library_test/media_library_test.module',
+	'path' => __DIR__ . '/modules/media_library/tests/modules/media_library_test/src/Hook/MediaLibraryTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function media_library_test_media_create_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\media_library_test\\\\Hook\\\\MediaLibraryTestHooks\\:\\:fieldWidgetInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/media_library/tests/modules/media_library_test/media_library_test.module',
+	'path' => __DIR__ . '/modules/media_library/tests/modules/media_library_test/src/Hook/MediaLibraryTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\media_library_test\\\\Form\\\\TestNodeFormOverride\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\media_library_test\\\\Hook\\\\MediaLibraryTestHooks\\:\\:mediaCreateAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/media_library/tests/modules/media_library_test/src/Form/TestNodeFormOverride.php',
+	'path' => __DIR__ . '/modules/media_library/tests/modules/media_library_test/src/Hook/MediaLibraryTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -30372,93 +30378,93 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function menu_link_content_entity_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Entity\\\\MenuLinkContent\\:\\:postSave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/menu_link_content.module',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Entity/MenuLinkContent.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function menu_link_content_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Entity\\\\MenuLinkContent\\:\\:preCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/menu_link_content.module',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Entity/MenuLinkContent.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function menu_link_content_menu_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Entity\\\\MenuLinkContent\\:\\:preDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/menu_link_content.module',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Entity/MenuLinkContent.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function menu_link_content_path_alias_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Entity\\\\MenuLinkContent\\:\\:preSave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/menu_link_content.module',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Entity/MenuLinkContent.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function menu_link_content_path_alias_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Entity\\\\MenuLinkContent\\:\\:setInsidePlugin\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/menu_link_content.module',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Entity/MenuLinkContent.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function menu_link_content_path_alias_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Form\\\\MenuLinkContentDeleteForm\\:\\:getDeletionMessage\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/menu_link_content.module',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Form/MenuLinkContentDeleteForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\menu_link_content\\\\Entity\\\\MenuLinkContent\\:\\:postSave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Form\\\\MenuLinkContentForm\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/src/Entity/MenuLinkContent.php',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Form/MenuLinkContentForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\menu_link_content\\\\Entity\\\\MenuLinkContent\\:\\:preCreate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Form\\\\MenuLinkContentForm\\:\\:form\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/src/Entity/MenuLinkContent.php',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Form/MenuLinkContentForm.php',
 ];
 $ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\menu_link_content\\\\Entity\\\\MenuLinkContent\\:\\:preDelete\\(\\) has no return type specified\\.$#',
+	// identifier: return.missing
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Form\\\\MenuLinkContentForm\\:\\:save\\(\\) should return int but return statement is missing\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/src/Entity/MenuLinkContent.php',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Form/MenuLinkContentForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\menu_link_content\\\\Entity\\\\MenuLinkContent\\:\\:preSave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Hook\\\\MenuLinkContentHooks\\:\\:entityPredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/src/Entity/MenuLinkContent.php',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Hook/MenuLinkContentHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\menu_link_content\\\\Entity\\\\MenuLinkContent\\:\\:setInsidePlugin\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Hook\\\\MenuLinkContentHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/src/Entity/MenuLinkContent.php',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Hook/MenuLinkContentHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\menu_link_content\\\\Form\\\\MenuLinkContentDeleteForm\\:\\:getDeletionMessage\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Hook\\\\MenuLinkContentHooks\\:\\:menuDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/src/Form/MenuLinkContentDeleteForm.php',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Hook/MenuLinkContentHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\menu_link_content\\\\Form\\\\MenuLinkContentForm\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Hook\\\\MenuLinkContentHooks\\:\\:pathAliasDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/src/Form/MenuLinkContentForm.php',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Hook/MenuLinkContentHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\menu_link_content\\\\Form\\\\MenuLinkContentForm\\:\\:form\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Hook\\\\MenuLinkContentHooks\\:\\:pathAliasInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/src/Form/MenuLinkContentForm.php',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Hook/MenuLinkContentHooks.php',
 ];
 $ignoreErrors[] = [
-	// identifier: return.missing
-	'message' => '#^Method Drupal\\\\menu_link_content\\\\Form\\\\MenuLinkContentForm\\:\\:save\\(\\) should return int but return statement is missing\\.$#',
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\menu_link_content\\\\Hook\\\\MenuLinkContentHooks\\:\\:pathAliasUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/src/Form/MenuLinkContentForm.php',
+	'path' => __DIR__ . '/modules/menu_link_content/src/Hook/MenuLinkContentHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -30492,15 +30498,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function menu_operations_link_test_entity_operation\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_operations_link_test\\\\Hook\\\\MenuOperationsLinkTestHooks\\:\\:entityOperation\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/tests/menu_operations_link_test/menu_operations_link_test.module',
+	'path' => __DIR__ . '/modules/menu_link_content/tests/menu_operations_link_test/src/Hook/MenuOperationsLinkTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function menu_operations_link_test_entity_operation_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_operations_link_test\\\\Hook\\\\MenuOperationsLinkTestHooks\\:\\:entityOperationAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_link_content/tests/menu_operations_link_test/menu_operations_link_test.module',
+	'path' => __DIR__ . '/modules/menu_link_content/tests/menu_operations_link_test/src/Hook/MenuOperationsLinkTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -30718,18 +30724,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/menu_ui/menu_ui.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function menu_ui_block_view_system_menu_block_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_ui/menu_ui.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function menu_ui_entity_type_build\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_ui/menu_ui.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function menu_ui_form_node_form_submit\\(\\) has no return type specified\\.$#',
@@ -30748,12 +30742,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/menu_ui/menu_ui.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function menu_ui_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_ui/menu_ui.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function menu_ui_node_builder\\(\\) has no return type specified\\.$#',
@@ -30766,12 +30754,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/menu_ui/menu_ui.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function menu_ui_system_breadcrumb_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/menu_ui/menu_ui.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\menu_ui\\\\Form\\\\MenuDeleteForm\\:\\:create\\(\\) has no return type specified\\.$#',
@@ -30820,6 +30802,30 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/menu_ui/src/Form/MenuLinkResetForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\menu_ui\\\\Hook\\\\MenuUiHooks\\:\\:blockViewSystemMenuBlockAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/menu_ui/src/Hook/MenuUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\menu_ui\\\\Hook\\\\MenuUiHooks\\:\\:entityTypeBuild\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/menu_ui/src/Hook/MenuUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\menu_ui\\\\Hook\\\\MenuUiHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/menu_ui/src/Hook/MenuUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\menu_ui\\\\Hook\\\\MenuUiHooks\\:\\:systemBreadcrumbAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/menu_ui/src/Hook/MenuUiHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\menu_ui\\\\MenuForm\\:\\:buildOverviewForm\\(\\) has no return type specified\\.$#',
@@ -30904,12 +30910,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/menu_ui/tests/src/Kernel/MenuBlockTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function migrate_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/migrate/migrate.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\migrate\\\\Annotation\\\\MigrateSource\\:\\:setProviders\\(\\) has no return type specified\\.$#',
@@ -30976,6 +30976,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/migrate/src/Form/MessageForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\migrate\\\\Hook\\\\MigrateHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/migrate/src/Hook/MigrateHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: constructor.unusedParameter
 	'message' => '#^Constructor of class Drupal\\\\migrate\\\\MigrateException has an unused parameter \\$code\\.$#',
@@ -31606,15 +31612,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function migrate_prepare_row_test_migrate_prepare_row\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\migrate_prepare_row_test\\\\Hook\\\\MigratePrepareRowTestHooks\\:\\:migratePrepareRow\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/migrate/tests/modules/migrate_prepare_row_test/migrate_prepare_row_test.module',
+	'path' => __DIR__ . '/modules/migrate/tests/modules/migrate_prepare_row_test/src/Hook/MigratePrepareRowTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function migrate_skip_all_rows_test_migrate_prepare_row\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\migrate_skip_all_rows_test\\\\Hook\\\\MigrateSkipAllRowsTestHooks\\:\\:migratePrepareRow\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/migrate/tests/modules/migrate_skip_all_rows_test/migrate_skip_all_rows_test.module',
+	'path' => __DIR__ . '/modules/migrate/tests/modules/migrate_skip_all_rows_test/src/Hook/MigrateSkipAllRowsTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -32030,18 +32036,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/migrate/tests/src/Unit/process/SubstrTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function migrate_drupal_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/migrate_drupal/migrate_drupal.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function migrate_drupal_migration_plugins_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/migrate_drupal/migrate_drupal.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\migrate_drupal\\\\FieldDiscovery\\:\\:addAllFieldProcesses\\(\\) has no return type specified\\.$#',
@@ -32078,6 +32072,18 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/migrate_drupal/src/FieldDiscoveryInterface.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\migrate_drupal\\\\Hook\\\\MigrateDrupalHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/migrate_drupal/src/Hook/MigrateDrupalHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\migrate_drupal\\\\Hook\\\\MigrateDrupalHooks\\:\\:migrationPluginsAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/migrate_drupal/src/Hook/MigrateDrupalHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\migrate_drupal\\\\MigrateDrupalServiceProvider\\:\\:alter\\(\\) has no return type specified\\.$#',
@@ -32664,12 +32670,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/migrate_drupal/tests/src/Unit/source/d6/Drupal6SqlBaseTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function migrate_drupal_ui_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/migrate_drupal_ui/migrate_drupal_ui.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\migrate_drupal_ui\\\\Batch\\\\MigrateMessageCapture\\:\\:clear\\(\\) has no return type specified\\.$#',
@@ -32820,6 +32820,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/migrate_drupal_ui/src/Form/ReviewForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\migrate_drupal_ui\\\\Hook\\\\MigrateDrupalUiHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/migrate_drupal_ui/src/Hook/MigrateDrupalUiHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\migrate_drupal_ui\\\\Routing\\\\MigrateDrupalUiRouteSubscriber\\:\\:alterRoutes\\(\\) has no return type specified\\.$#',
@@ -33072,12 +33078,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/mysql/mysql.install',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function mysql_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/mysql/mysql.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\mysql\\\\Driver\\\\Database\\\\mysql\\\\Connection\\:\\:createDatabase\\(\\) has no return type specified\\.$#',
@@ -33168,6 +33168,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/mysql/src/Driver/Database/mysql/Schema.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\mysql\\\\Hook\\\\MysqlHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/mysql/src/Hook/MysqlHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\Tests\\\\mysql\\\\Kernel\\\\mysql\\\\DbDumpTest\\:\\:assertPathAliasExists\\(\\) has no return type specified\\.$#',
@@ -33194,9 +33200,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function navigation_top_bar_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\navigation_top_bar\\\\Hook\\\\NavigationTopBarHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/modules/navigation_top_bar/navigation_top_bar.module',
+	'path' => __DIR__ . '/modules/navigation/modules/navigation_top_bar/src/Hook/NavigationTopBarHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -33206,81 +33212,81 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function navigation_block_build_local_tasks_block_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function navigation_module_implements_alter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/navigation/navigation.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function navigation_element_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function navigation_post_update_set_logo_dimensions_default\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/navigation.module',
+	'path' => __DIR__ . '/modules/navigation/navigation.post_update.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function navigation_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function navigation_post_update_update_permissions\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/navigation.module',
+	'path' => __DIR__ . '/modules/navigation/navigation.post_update.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function navigation_menu_links_discovered_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\navigation\\\\Form\\\\LayoutForm\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/navigation.module',
+	'path' => __DIR__ . '/modules/navigation/src/Form/LayoutForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function navigation_module_implements_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\navigation\\\\Form\\\\LayoutForm\\:\\:redirectOnSubmit\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/navigation.module',
+	'path' => __DIR__ . '/modules/navigation/src/Form/LayoutForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function navigation_page_top\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\navigation\\\\Form\\\\SettingsForm\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/navigation.module',
+	'path' => __DIR__ . '/modules/navigation/src/Form/SettingsForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function navigation_plugin_filter_block__layout_builder_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\navigation\\\\Hook\\\\NavigationHooks\\:\\:blockBuildLocalTasksBlockAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/navigation.module',
+	'path' => __DIR__ . '/modules/navigation/src/Hook/NavigationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function navigation_plugin_filter_layout__layout_builder_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\navigation\\\\Hook\\\\NavigationHooks\\:\\:elementInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/navigation.module',
+	'path' => __DIR__ . '/modules/navigation/src/Hook/NavigationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function navigation_post_update_set_logo_dimensions_default\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\navigation\\\\Hook\\\\NavigationHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/navigation.post_update.php',
+	'path' => __DIR__ . '/modules/navigation/src/Hook/NavigationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function navigation_post_update_update_permissions\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\navigation\\\\Hook\\\\NavigationHooks\\:\\:menuLinksDiscoveredAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/navigation.post_update.php',
+	'path' => __DIR__ . '/modules/navigation/src/Hook/NavigationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\navigation\\\\Form\\\\LayoutForm\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\navigation\\\\Hook\\\\NavigationHooks\\:\\:pageTop\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/src/Form/LayoutForm.php',
+	'path' => __DIR__ . '/modules/navigation/src/Hook/NavigationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\navigation\\\\Form\\\\LayoutForm\\:\\:redirectOnSubmit\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\navigation\\\\Hook\\\\NavigationHooks\\:\\:pluginFilterBlockLayoutBuilderAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/src/Form/LayoutForm.php',
+	'path' => __DIR__ . '/modules/navigation/src/Hook/NavigationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\navigation\\\\Form\\\\SettingsForm\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\navigation\\\\Hook\\\\NavigationHooks\\:\\:pluginFilterLayoutLayoutBuilderAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/navigation/src/Form/SettingsForm.php',
+	'path' => __DIR__ . '/modules/navigation/src/Hook/NavigationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -33452,351 +33458,351 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_comment_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function node_form_system_themes_admin_form_submit\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/node/node.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_comment_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function node_preprocess_block\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/node/node.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_comment_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function node_preprocess_field__node\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/node/node.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_config_translation_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function node_preprocess_html\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/node/node.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_configurable_language_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function node_reindex_node_search\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/node/node.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_cron\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function node_theme_suggestions_node\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/node/node.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_entity_extra_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function template_preprocess_node\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/node/node.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_entity_view_display_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function template_preprocess_node_add_list\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/node/node.module',
 ];
 $ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function node_form_system_themes_admin_form_submit\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	// identifier: return.missing
+	'message' => '#^Method Drupal\\\\node\\\\ConfigTranslation\\\\NodeTypeMapper\\:\\:setEntity\\(\\) should return bool but return statement is missing\\.$#',
+	'count' => 2,
+	'path' => __DIR__ . '/modules/node/src/ConfigTranslation/NodeTypeMapper.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\ContextProvider\\\\NodeRouteContext\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/ContextProvider/NodeRouteContext.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_modules_installed\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\ContextProvider\\\\NodeRouteContext\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/ContextProvider/NodeRouteContext.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_modules_uninstalled\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Controller\\\\NodePreviewController\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/Controller/NodePreviewController.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_node_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Controller\\\\NodeViewController\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/Controller/NodeViewController.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_page_top\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\Node\\:\\:postDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/Entity/Node.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_preprocess_block\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\Node\\:\\:postSave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/Entity/Node.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_preprocess_field__node\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\Node\\:\\:preDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/Entity/Node.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_preprocess_html\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\Node\\:\\:preSave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/Entity/Node.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_query_node_access_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\Node\\:\\:preSaveRevision\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/Entity/Node.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_ranking\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\NodeType\\:\\:postDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/Entity/NodeType.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_reindex_node_search\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\NodeType\\:\\:postSave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/Entity/NodeType.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_theme_suggestions_node\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\NodeType\\:\\:setDisplaySubmitted\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/Entity/NodeType.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_user_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\NodeType\\:\\:setNewRevision\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/Entity/NodeType.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function template_preprocess_node\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\NodeType\\:\\:setPreviewMode\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/Entity/NodeType.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function template_preprocess_node_add_list\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\EventSubscriber\\\\NodeAdminRouteSubscriber\\:\\:alterRoutes\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.module',
+	'path' => __DIR__ . '/modules/node/src/EventSubscriber/NodeAdminRouteSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_token_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\EventSubscriber\\\\NodeAdminRouteSubscriber\\:\\:onConfigSave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.tokens.inc',
+	'path' => __DIR__ . '/modules/node/src/EventSubscriber/NodeAdminRouteSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_tokens\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\EventSubscriber\\\\NodeTranslationExceptionSubscriber\\:\\:onException\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.tokens.inc',
+	'path' => __DIR__ . '/modules/node/src/EventSubscriber/NodeTranslationExceptionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_views_analyze\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\EventSubscriber\\\\NodeTranslationMigrateSubscriber\\:\\:onPostImport\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.views.inc',
+	'path' => __DIR__ . '/modules/node/src/EventSubscriber/NodeTranslationMigrateSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_views_query_substitutions\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\EventSubscriber\\\\NodeTranslationMigrateSubscriber\\:\\:onPostRowSave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/node.views_execution.inc',
+	'path' => __DIR__ . '/modules/node/src/EventSubscriber/NodeTranslationMigrateSubscriber.php',
 ];
 $ignoreErrors[] = [
-	// identifier: return.missing
-	'message' => '#^Method Drupal\\\\node\\\\ConfigTranslation\\\\NodeTypeMapper\\:\\:setEntity\\(\\) should return bool but return statement is missing\\.$#',
-	'count' => 2,
-	'path' => __DIR__ . '/modules/node/src/ConfigTranslation/NodeTypeMapper.php',
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeDeleteForm\\:\\:getDeletionMessage\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/node/src/Form/NodeDeleteForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\ContextProvider\\\\NodeRouteContext\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeDeleteForm\\:\\:logDeletionMessage\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/ContextProvider/NodeRouteContext.php',
+	'path' => __DIR__ . '/modules/node/src/Form/NodeDeleteForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\ContextProvider\\\\NodeRouteContext\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodePreviewForm\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/ContextProvider/NodeRouteContext.php',
+	'path' => __DIR__ . '/modules/node/src/Form/NodePreviewForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Controller\\\\NodePreviewController\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodePreviewForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Controller/NodePreviewController.php',
+	'path' => __DIR__ . '/modules/node/src/Form/NodePreviewForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Controller\\\\NodeViewController\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeRevisionDeleteForm\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Controller/NodeViewController.php',
+	'path' => __DIR__ . '/modules/node/src/Form/NodeRevisionDeleteForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\Node\\:\\:postDelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeRevisionDeleteForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Entity/Node.php',
+	'path' => __DIR__ . '/modules/node/src/Form/NodeRevisionDeleteForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\Node\\:\\:postSave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeRevisionRevertForm\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Entity/Node.php',
+	'path' => __DIR__ . '/modules/node/src/Form/NodeRevisionRevertForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\Node\\:\\:preDelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeRevisionRevertForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Entity/Node.php',
+	'path' => __DIR__ . '/modules/node/src/Form/NodeRevisionRevertForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\Node\\:\\:preSave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeRevisionRevertTranslationForm\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Entity/Node.php',
+	'path' => __DIR__ . '/modules/node/src/Form/NodeRevisionRevertTranslationForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\Node\\:\\:preSaveRevision\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Form\\\\RebuildPermissionsForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Entity/Node.php',
+	'path' => __DIR__ . '/modules/node/src/Form/RebuildPermissionsForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\NodeType\\:\\:postDelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:commentDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Entity/NodeType.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\NodeType\\:\\:postSave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:commentInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Entity/NodeType.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\NodeType\\:\\:setDisplaySubmitted\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:commentUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Entity/NodeType.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\NodeType\\:\\:setNewRevision\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:configTranslationInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Entity/NodeType.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Entity\\\\NodeType\\:\\:setPreviewMode\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:configurableLanguageDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Entity/NodeType.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\EventSubscriber\\\\NodeAdminRouteSubscriber\\:\\:alterRoutes\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:cron\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/EventSubscriber/NodeAdminRouteSubscriber.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\EventSubscriber\\\\NodeAdminRouteSubscriber\\:\\:onConfigSave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:entityExtraFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/EventSubscriber/NodeAdminRouteSubscriber.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\EventSubscriber\\\\NodeTranslationExceptionSubscriber\\:\\:onException\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:entityViewDisplayAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/EventSubscriber/NodeTranslationExceptionSubscriber.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\EventSubscriber\\\\NodeTranslationMigrateSubscriber\\:\\:onPostImport\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/EventSubscriber/NodeTranslationMigrateSubscriber.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\EventSubscriber\\\\NodeTranslationMigrateSubscriber\\:\\:onPostRowSave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:modulesInstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/EventSubscriber/NodeTranslationMigrateSubscriber.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeDeleteForm\\:\\:getDeletionMessage\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:modulesUninstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Form/NodeDeleteForm.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeDeleteForm\\:\\:logDeletionMessage\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:nodeAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Form/NodeDeleteForm.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodePreviewForm\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:pageTop\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Form/NodePreviewForm.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodePreviewForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:queryNodeAccessAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Form/NodePreviewForm.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeRevisionDeleteForm\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:ranking\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Form/NodeRevisionDeleteForm.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeRevisionDeleteForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeHooks1\\:\\:userPredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Form/NodeRevisionDeleteForm.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeHooks1.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeRevisionRevertForm\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeTokensHooks\\:\\:tokenInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Form/NodeRevisionRevertForm.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeTokensHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeRevisionRevertForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeTokensHooks\\:\\:tokens\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Form/NodeRevisionRevertForm.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeTokensHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Form\\\\NodeRevisionRevertTranslationForm\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeViewsExecutionHooks\\:\\:viewsQuerySubstitutions\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Form/NodeRevisionRevertTranslationForm.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeViewsExecutionHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\node\\\\Form\\\\RebuildPermissionsForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node\\\\Hook\\\\NodeViewsHooks\\:\\:viewsAnalyze\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/src/Form/RebuildPermissionsForm.php',
+	'path' => __DIR__ . '/modules/node/src/Hook/NodeViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -34220,129 +34226,129 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_access_test_node_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_access_test\\\\Hook\\\\NodeAccessTestHooks\\:\\:nodeAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test/node_access_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test/src/Hook/NodeAccessTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_access_test_node_access_records\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_access_test\\\\Hook\\\\NodeAccessTestHooks\\:\\:nodeAccessRecords\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test/node_access_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test/src/Hook/NodeAccessTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_access_test_node_grants\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_access_test\\\\Hook\\\\NodeAccessTestHooks\\:\\:nodeGrants\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test/node_access_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test/src/Hook/NodeAccessTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_access_test_empty_node_access_records\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_access_test_empty\\\\Hook\\\\NodeAccessTestEmptyHooks\\:\\:nodeAccessRecords\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test_empty/node_access_test_empty.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test_empty/src/Hook/NodeAccessTestEmptyHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_access_test_empty_node_grants\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_access_test_empty\\\\Hook\\\\NodeAccessTestEmptyHooks\\:\\:nodeGrants\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test_empty/node_access_test_empty.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test_empty/src/Hook/NodeAccessTestEmptyHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_access_test_language_node_access_records\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_access_test_language\\\\Hook\\\\NodeAccessTestLanguageHooks\\:\\:nodeAccessRecords\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test_language/node_access_test_language.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test_language/src/Hook/NodeAccessTestLanguageHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_access_test_language_node_grants\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_access_test_language\\\\Hook\\\\NodeAccessTestLanguageHooks\\:\\:nodeGrants\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test_language/node_access_test_language.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_access_test_language/src/Hook/NodeAccessTestLanguageHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_display_configurable_test_entity_base_field_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_display_configurable_test\\\\Hook\\\\NodeDisplayConfigurableTestHooks\\:\\:entityBaseFieldInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_display_configurable_test/node_display_configurable_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_display_configurable_test/src/Hook/NodeDisplayConfigurableTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_display_configurable_test_entity_type_build\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_display_configurable_test\\\\Hook\\\\NodeDisplayConfigurableTestHooks\\:\\:entityTypeBuild\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_display_configurable_test/node_display_configurable_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_display_configurable_test/src/Hook/NodeDisplayConfigurableTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_test_entity_view_mode_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_test\\\\Hook\\\\NodeTestHooks\\:\\:entityViewModeAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_test/node_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_test_node_access_records\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_test\\\\Hook\\\\NodeTestHooks\\:\\:nodeAccessRecords\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_test/node_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_test_node_access_records_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_test\\\\Hook\\\\NodeTestHooks\\:\\:nodeAccessRecordsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_test/node_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_test_node_build_defaults_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_test\\\\Hook\\\\NodeTestHooks\\:\\:nodeBuildDefaultsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_test/node_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_test_node_grants\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_test\\\\Hook\\\\NodeTestHooks\\:\\:nodeGrants\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_test/node_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_test_node_grants_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_test\\\\Hook\\\\NodeTestHooks\\:\\:nodeGrantsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_test/node_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_test_node_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_test\\\\Hook\\\\NodeTestHooks\\:\\:nodeInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_test/node_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_test_node_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_test\\\\Hook\\\\NodeTestHooks\\:\\:nodePresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_test/node_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_test_node_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_test\\\\Hook\\\\NodeTestHooks\\:\\:nodeUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_test/node_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_test_node_view\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_test\\\\Hook\\\\NodeTestHooks\\:\\:nodeView\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_test/node_test.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_test_exception_node_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_test_exception\\\\Hook\\\\NodeTestExceptionHooks\\:\\:nodeInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_test_exception/node_test_exception.module',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_test_exception/src/Hook/NodeTestExceptionHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function node_test_views_views_data_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\node_test_views\\\\Hook\\\\NodeTestViewsViewsHooks\\:\\:viewsDataAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/node/tests/modules/node_test_views/node_test_views.views.inc',
+	'path' => __DIR__ . '/modules/node/tests/modules/node_test_views/src/Hook/NodeTestViewsViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -34434,6 +34440,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/node/tests/src/Functional/NodeBlockFunctionalTest.php',
 ];
+$ignoreErrors[] = [
+	// identifier: phpunit.covers
+	'message' => '#^@covers value node_local_tasks_alter references an invalid class or function\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/node/tests/src/Functional/NodeRevisionsUiTest.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\Tests\\\\node\\\\Functional\\\\NodeTestBase\\:\\:assertNodeAccess\\(\\) has no return type specified\\.$#',
@@ -35108,33 +35120,33 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function options_field_storage_config_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\options\\\\Hook\\\\OptionsHooks\\:\\:fieldStorageConfigDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/options/options.module',
+	'path' => __DIR__ . '/modules/options/src/Hook/OptionsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function options_field_storage_config_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\options\\\\Hook\\\\OptionsHooks\\:\\:fieldStorageConfigUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/options/options.module',
+	'path' => __DIR__ . '/modules/options/src/Hook/OptionsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function options_field_storage_config_update_forbid\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\options\\\\Hook\\\\OptionsHooks\\:\\:fieldStorageConfigUpdateForbid\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/options/options.module',
+	'path' => __DIR__ . '/modules/options/src/Hook/OptionsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function options_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\options\\\\Hook\\\\OptionsHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/options/options.module',
+	'path' => __DIR__ . '/modules/options/src/Hook/OptionsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function options_field_views_data\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\options\\\\Hook\\\\OptionsViewsHooks\\:\\:fieldViewsData\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/options/options.views.inc',
+	'path' => __DIR__ . '/modules/options/src/Hook/OptionsViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: return.missing
@@ -35258,9 +35270,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function options_test_options_list_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\options_test\\\\Hook\\\\OptionsTestHooks\\:\\:optionsListAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/options/tests/options_test/options_test.module',
+	'path' => __DIR__ . '/modules/options/tests/options_test/src/Hook/OptionsTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -35624,9 +35636,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function page_cache_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\page_cache\\\\Hook\\\\PageCacheHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/page_cache/page_cache.module',
+	'path' => __DIR__ . '/modules/page_cache/src/Hook/PageCacheHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -35714,45 +35726,45 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function path_entity_base_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\path\\\\Form\\\\PathFilterForm\\:\\:resetForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/path/path.module',
+	'path' => __DIR__ . '/modules/path/src/Form/PathFilterForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function path_entity_base_field_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\path\\\\Form\\\\PathFilterForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/path/path.module',
+	'path' => __DIR__ . '/modules/path/src/Form/PathFilterForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function path_entity_translation_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\path\\\\Hook\\\\PathHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/path/path.module',
+	'path' => __DIR__ . '/modules/path/src/Hook/PathHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function path_field_widget_single_element_form_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\path\\\\Hook\\\\PathHooks\\:\\:entityBaseFieldInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/path/path.module',
+	'path' => __DIR__ . '/modules/path/src/Hook/PathHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function path_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\path\\\\Hook\\\\PathHooks\\:\\:entityTranslationCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/path/path.module',
+	'path' => __DIR__ . '/modules/path/src/Hook/PathHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\path\\\\Form\\\\PathFilterForm\\:\\:resetForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\path\\\\Hook\\\\PathHooks\\:\\:fieldWidgetSingleElementFormAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/path/src/Form/PathFilterForm.php',
+	'path' => __DIR__ . '/modules/path/src/Hook/PathHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\path\\\\Form\\\\PathFilterForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\path\\\\Hook\\\\PathHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/path/src/Form/PathFilterForm.php',
+	'path' => __DIR__ . '/modules/path/src/Hook/PathHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: return.missing
@@ -36126,12 +36138,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/pgsql/pgsql.install',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function pgsql_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/pgsql/pgsql.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\pgsql\\\\Driver\\\\Database\\\\pgsql\\\\Connection\\:\\:addSavepoint\\(\\) has no return type specified\\.$#',
@@ -36300,6 +36306,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/pgsql/src/Driver/Database/pgsql/Upsert.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\pgsql\\\\Hook\\\\PgsqlHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/pgsql/src/Hook/PgsqlHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\Tests\\\\pgsql\\\\Unit\\\\SchemaTest\\:\\:providerComputedConstraintName\\(\\) has no return type specified\\.$#',
@@ -36308,9 +36320,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function phpass_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\phpass\\\\Hook\\\\PhpassHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/phpass/phpass.module',
+	'path' => __DIR__ . '/modules/phpass/src/Hook/PhpassHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -36326,27 +36338,27 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function responsive_image_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function template_preprocess_responsive_image\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/responsive_image/responsive_image.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function responsive_image_library_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function template_preprocess_responsive_image_formatter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/responsive_image/responsive_image.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function template_preprocess_responsive_image\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\responsive_image\\\\Hook\\\\ResponsiveImageHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/responsive_image/responsive_image.module',
+	'path' => __DIR__ . '/modules/responsive_image/src/Hook/ResponsiveImageHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function template_preprocess_responsive_image_formatter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\responsive_image\\\\Hook\\\\ResponsiveImageHooks\\:\\:libraryInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/responsive_image/responsive_image.module',
+	'path' => __DIR__ . '/modules/responsive_image/src/Hook/ResponsiveImageHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: empty.variable
@@ -36474,12 +36486,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/responsive_image/tests/src/Kernel/Plugin/migrate/source/d7/ResponsiveImageStylesTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function rest_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/rest/rest.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\rest\\\\Entity\\\\ConfigDependencies\\:\\:create\\(\\) has no return type specified\\.$#',
@@ -36516,6 +36522,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\rest\\\\Hook\\\\RestHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/rest/src/Hook/RestHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\rest\\\\Plugin\\\\rest\\\\resource\\\\EntityResource\\:\\:addLinkHeaders\\(\\) has no return type specified\\.$#',
@@ -36692,27 +36704,27 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function config_test_rest_config_test_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\config_test_rest\\\\Hook\\\\ConfigTestRestHooks\\:\\:configTestAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/rest/tests/modules/config_test_rest/config_test_rest.module',
+	'path' => __DIR__ . '/modules/rest/tests/modules/config_test_rest/src/Hook/ConfigTestRestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function rest_test_entity_base_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\rest_test\\\\Hook\\\\RestTestHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/rest/tests/modules/rest_test/rest_test.module',
+	'path' => __DIR__ . '/modules/rest/tests/modules/rest_test/src/Hook/RestTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function rest_test_entity_field_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\rest_test\\\\Hook\\\\RestTestHooks\\:\\:entityFieldAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/rest/tests/modules/rest_test/rest_test.module',
+	'path' => __DIR__ . '/modules/rest/tests/modules/rest_test/src/Hook/RestTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function rest_test_views_views_post_execute\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\rest_test_views\\\\Hook\\\\RestTestViewsHooks\\:\\:viewsPostExecute\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/rest/tests/modules/rest_test_views/rest_test_views.module',
+	'path' => __DIR__ . '/modules/rest/tests/modules/rest_test_views/src/Hook/RestTestViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -37164,24 +37176,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/search/search.install',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function search_block_presave\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/search/search.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function search_cron\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/search/search.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function search_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/search/search.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function search_preprocess_block\\(\\) has no return type specified\\.$#',
@@ -37308,6 +37302,24 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/search/src/Form/SearchPageFormBase.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\search\\\\Hook\\\\SearchHooks\\:\\:blockPresave\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/search/src/Hook/SearchHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\search\\\\Hook\\\\SearchHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/search/src/Hook/SearchHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\search\\\\Hook\\\\SearchHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/search/src/Hook/SearchHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\search\\\\Plugin\\\\Block\\\\SearchBlock\\:\\:blockSubmit\\(\\) has no return type specified\\.$#',
@@ -37562,9 +37574,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function search_date_query_alter_query_search_node_search_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\search_date_query_alter\\\\Hook\\\\SearchDateQueryAlterHooks\\:\\:querySearchNodeSearchAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/search/tests/modules/search_date_query_alter/search_date_query_alter.module',
+	'path' => __DIR__ . '/modules/search/tests/modules/search_date_query_alter/src/Hook/SearchDateQueryAlterHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -37586,15 +37598,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function search_langcode_test_search_preprocess\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\search_langcode_test\\\\Hook\\\\SearchLangcodeTestHooks\\:\\:searchPreprocess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/search/tests/modules/search_langcode_test/search_langcode_test.module',
+	'path' => __DIR__ . '/modules/search/tests/modules/search_langcode_test/src/Hook/SearchLangcodeTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function search_query_alter_query_search_node_search_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\search_query_alter\\\\Hook\\\\SearchQueryAlterHooks\\:\\:querySearchNodeSearchAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/search/tests/modules/search_query_alter/search_query_alter.module',
+	'path' => __DIR__ . '/modules/search/tests/modules/search_query_alter/src/Hook/SearchQueryAlterHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -37866,12 +37878,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/search/tests/src/Kernel/SearchTokenizerTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function serialization_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/serialization/serialization.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\serialization\\\\Encoder\\\\XmlEncoder\\:\\:setBaseEncoder\\(\\) has no return type specified\\.$#',
@@ -37902,6 +37908,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\serialization\\\\Hook\\\\SerializationHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/serialization/src/Hook/SerializationHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: method.notFound
 	'message' => '#^Call to an undefined method Drupal\\\\serialization\\\\Normalizer\\\\EntityNormalizer\\:\\:getCustomSerializedPropertyNames\\(\\)\\.$#',
@@ -37952,9 +37964,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_serialization_test_entity_field_access_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_serialization_test\\\\Hook\\\\EntitySerializationTestHooks\\:\\:entityFieldAccessAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/serialization/tests/modules/entity_serialization_test/entity_serialization_test.module',
+	'path' => __DIR__ . '/modules/serialization/tests/modules/entity_serialization_test/src/Hook/EntitySerializationTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -38000,75 +38012,75 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function settings_tray_block_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function settings_tray_preprocess_block\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/settings_tray/settings_tray.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function settings_tray_block_view_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\settings_tray\\\\Block\\\\BlockEntitySettingTrayForm\\:\\:form\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/settings_tray/settings_tray.module',
+	'path' => __DIR__ . '/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function settings_tray_contextual_links_view_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\settings_tray\\\\Block\\\\BlockEntitySettingTrayForm\\:\\:submitVisibility\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/settings_tray/settings_tray.module',
+	'path' => __DIR__ . '/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function settings_tray_css_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\settings_tray\\\\Block\\\\BlockEntitySettingTrayForm\\:\\:successfulAjaxSubmit\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/settings_tray/settings_tray.module',
+	'path' => __DIR__ . '/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function settings_tray_entity_type_build\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\settings_tray\\\\Block\\\\BlockEntitySettingTrayForm\\:\\:validateVisibility\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/settings_tray/settings_tray.module',
+	'path' => __DIR__ . '/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function settings_tray_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\settings_tray\\\\Hook\\\\SettingsTrayHooks\\:\\:blockAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/settings_tray/settings_tray.module',
+	'path' => __DIR__ . '/modules/settings_tray/src/Hook/SettingsTrayHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function settings_tray_preprocess_block\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\settings_tray\\\\Hook\\\\SettingsTrayHooks\\:\\:blockViewAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/settings_tray/settings_tray.module',
+	'path' => __DIR__ . '/modules/settings_tray/src/Hook/SettingsTrayHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function settings_tray_toolbar_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\settings_tray\\\\Hook\\\\SettingsTrayHooks\\:\\:contextualLinksViewAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/settings_tray/settings_tray.module',
+	'path' => __DIR__ . '/modules/settings_tray/src/Hook/SettingsTrayHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\settings_tray\\\\Block\\\\BlockEntitySettingTrayForm\\:\\:form\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\settings_tray\\\\Hook\\\\SettingsTrayHooks\\:\\:cssAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php',
+	'path' => __DIR__ . '/modules/settings_tray/src/Hook/SettingsTrayHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\settings_tray\\\\Block\\\\BlockEntitySettingTrayForm\\:\\:submitVisibility\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\settings_tray\\\\Hook\\\\SettingsTrayHooks\\:\\:entityTypeBuild\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php',
+	'path' => __DIR__ . '/modules/settings_tray/src/Hook/SettingsTrayHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\settings_tray\\\\Block\\\\BlockEntitySettingTrayForm\\:\\:successfulAjaxSubmit\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\settings_tray\\\\Hook\\\\SettingsTrayHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php',
+	'path' => __DIR__ . '/modules/settings_tray/src/Hook/SettingsTrayHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\settings_tray\\\\Block\\\\BlockEntitySettingTrayForm\\:\\:validateVisibility\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\settings_tray\\\\Hook\\\\SettingsTrayHooks\\:\\:toolbarAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/settings_tray/src/Block/BlockEntitySettingTrayForm.php',
+	'path' => __DIR__ . '/modules/settings_tray/src/Hook/SettingsTrayHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -38084,9 +38096,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function settings_tray_test_css_page_attachments\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\settings_tray_test_css\\\\Hook\\\\SettingsTrayTestCssHooks\\:\\:pageAttachments\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/settings_tray/tests/modules/settings_tray_test_css/settings_tray_test_css.module',
+	'path' => __DIR__ . '/modules/settings_tray/tests/modules/settings_tray_test_css/src/Hook/SettingsTrayTestCssHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -38160,12 +38172,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/shortcut/shortcut.install',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function shortcut_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/shortcut/shortcut.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function shortcut_preprocess_block\\(\\) has no return type specified\\.$#',
@@ -38178,24 +38184,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/shortcut/shortcut.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function shortcut_themes_installed\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/shortcut/shortcut.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function shortcut_toolbar\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/shortcut/shortcut.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function shortcut_user_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/shortcut/shortcut.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\shortcut\\\\Entity\\\\Shortcut\\:\\:postSave\\(\\) has no return type specified\\.$#',
@@ -38250,6 +38238,30 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/shortcut/src/Form/SwitchShortcutSet.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\shortcut\\\\Hook\\\\ShortcutHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/shortcut/src/Hook/ShortcutHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\shortcut\\\\Hook\\\\ShortcutHooks\\:\\:themesInstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/shortcut/src/Hook/ShortcutHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\shortcut\\\\Hook\\\\ShortcutHooks\\:\\:toolbar\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/shortcut/src/Hook/ShortcutHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\shortcut\\\\Hook\\\\ShortcutHooks\\:\\:userDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/shortcut/src/Hook/ShortcutHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\shortcut\\\\ShortcutForm\\:\\:form\\(\\) has no return type specified\\.$#',
@@ -38580,12 +38592,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/shortcut/tests/src/Unit/Menu/ShortcutLocalTasksTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function sqlite_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/sqlite/sqlite.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\sqlite\\\\Driver\\\\Database\\\\sqlite\\\\Connection\\:\\:createDatabase\\(\\) has no return type specified\\.$#',
@@ -38744,21 +38750,27 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\syslog\\\\Logger\\\\SysLog\\:\\:openConnection\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\sqlite\\\\Hook\\\\SqliteHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/syslog/src/Logger/SysLog.php',
+	'path' => __DIR__ . '/modules/sqlite/src/Hook/SqliteHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\syslog\\\\Logger\\\\SysLog\\:\\:syslogWrapper\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\syslog\\\\Hook\\\\SyslogHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/syslog/src/Hook/SyslogHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\syslog\\\\Logger\\\\SysLog\\:\\:openConnection\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/syslog/src/Logger/SysLog.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function syslog_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\syslog\\\\Logger\\\\SysLog\\:\\:syslogWrapper\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/syslog/syslog.module',
+	'path' => __DIR__ . '/modules/syslog/src/Logger/SysLog.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -39342,6 +39354,132 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/src/Form/ThemeSettingsForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:archiverInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:blockViewSystemMainBlockAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:elementInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:entityTypeBuild\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:fileDownload\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:filetransferInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:jsSettingsAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:jsSettingsBuild\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:libraryInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:mail\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:modulesUninstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:pageAttachments\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:pageTop\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:queryEntityReferenceAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:themeRegistryAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemHooks\\:\\:updaterInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemTokensHooks\\:\\:tokenInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemTokensHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\system\\\\Hook\\\\SystemTokensHooks\\:\\:tokens\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/src/Hook/SystemTokensHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\system\\\\PathBasedBreadcrumbBuilder\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
@@ -39602,7 +39740,7 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function system_archiver_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function _system_page_attachments\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/system.module',
 ];
@@ -39624,102 +39762,18 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/system.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_block_view_system_main_block_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function system_check_directory\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/system.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_cron\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_element_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_entity_type_build\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_file_download\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_filetransfer_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function system_hook_info\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/system.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_js_settings_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_js_settings_build\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_library_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_mail\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_modules_uninstalled\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_page_attachments\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_page_top\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function system_preprocess_block\\(\\) has no return type specified\\.$#',
@@ -39732,30 +39786,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/system.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_query_entity_reference_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function system_sort_themes\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/system.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_system_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_theme_registry_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function system_theme_suggestions_field\\(\\) has no return type specified\\.$#',
@@ -39786,12 +39822,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/system.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_updater_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function template_preprocess_entity_add_list\\(\\) has no return type specified\\.$#',
@@ -39804,18 +39834,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/system.post_update.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_token_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.tokens.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function system_tokens\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/system.tokens.inc',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\accept_header_routing_test\\\\AcceptHeaderRoutingTestServiceProvider\\:\\:alter\\(\\) has no return type specified\\.$#',
@@ -39836,15 +39854,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function advisory_feed_test_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\advisory_feed_test\\\\AdvisoryTestClientMiddleware\\:\\:__invoke\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/advisory_feed_test/advisory_feed_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/advisory_feed_test/src/AdvisoryTestClientMiddleware.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\advisory_feed_test\\\\AdvisoryTestClientMiddleware\\:\\:__invoke\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\advisory_feed_test\\\\Hook\\\\AdvisoryFeedTestHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/advisory_feed_test/src/AdvisoryTestClientMiddleware.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/advisory_feed_test/src/Hook/AdvisoryFeedTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -40418,93 +40436,93 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function block_drupal_alter_foo_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function common_test_module_implements_alter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function common_test_cron\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function common_test_preprocess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function common_test_drupal_alter_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function common_test_preprocess_common_test_render_element\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function common_test_js_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function olivero_drupal_alter_alter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function common_test_js_settings_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\common_test\\\\Controller\\\\CommonTestController\\:\\:typeLinkActiveClass\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/common_test/src/Controller/CommonTestController.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function common_test_library_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\common_test\\\\Hook\\\\CommonTestHooks\\:\\:blockDrupalAlterFooAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function common_test_library_info_build\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\common_test\\\\Hook\\\\CommonTestHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function common_test_module_implements_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\common_test\\\\Hook\\\\CommonTestHooks\\:\\:drupalAlterAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function common_test_page_attachments\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\common_test\\\\Hook\\\\CommonTestHooks\\:\\:jsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function common_test_page_attachments_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\common_test\\\\Hook\\\\CommonTestHooks\\:\\:jsSettingsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function common_test_preprocess\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\common_test\\\\Hook\\\\CommonTestHooks\\:\\:libraryInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function common_test_preprocess_common_test_render_element\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\common_test\\\\Hook\\\\CommonTestHooks\\:\\:libraryInfoBuild\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function olivero_drupal_alter_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\common_test\\\\Hook\\\\CommonTestHooks\\:\\:pageAttachments\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/common_test/common_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\common_test\\\\Controller\\\\CommonTestController\\:\\:typeLinkActiveClass\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\common_test\\\\Hook\\\\CommonTestHooks\\:\\:pageAttachmentsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/common_test/src/Controller/CommonTestController.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function common_test_cron_helper_cron\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\common_test_cron_helper\\\\Hook\\\\CommonTestCronHelperHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/common_test_cron_helper/common_test_cron_helper.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/common_test_cron_helper/src/Hook/CommonTestCronHelperHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -40598,9 +40616,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function css_disable_transitions_test_page_attachments\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\css_disable_transitions_test\\\\Hook\\\\CssDisableTransitionsTestHooks\\:\\:pageAttachments\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/css_disable_transitions_test/css_disable_transitions_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/css_disable_transitions_test/src/Hook/CssDisableTransitionsTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -40610,27 +40628,27 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function database_test_query_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\database_test\\\\Form\\\\DatabaseTestForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/database_test/database_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/database_test/src/Form/DatabaseTestForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function database_test_query_database_test_alter_remove_range_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\database_test\\\\Hook\\\\DatabaseTestHooks\\:\\:queryAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/database_test/database_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/database_test/src/Hook/DatabaseTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\database_test\\\\Form\\\\DatabaseTestForm\\:\\:submitForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\database_test\\\\Hook\\\\DatabaseTestHooks\\:\\:queryDatabaseTestAlterRemoveRangeAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/database_test/src/Form/DatabaseTestForm.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/database_test/src/Hook/DatabaseTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function decoupled_menus_test_menu_links_discovered_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\decoupled_menus_test\\\\Hook\\\\DecoupledMenusTestHooks\\:\\:menuLinksDiscoveredAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/decoupled_menus_test/decoupled_menus_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/decoupled_menus_test/src/Hook/DecoupledMenusTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -40640,27 +40658,27 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function delay_cache_tags_invalidation_entity_test_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\delay_cache_tags_invalidation\\\\Hook\\\\DelayCacheTagsInvalidationHooks\\:\\:entityTestInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/delay_cache_tags_invalidation/delay_cache_tags_invalidation.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/delay_cache_tags_invalidation/src/Hook/DelayCacheTagsInvalidationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function delay_cache_tags_invalidation_user_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\delay_cache_tags_invalidation\\\\Hook\\\\DelayCacheTagsInvalidationHooks\\:\\:userInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/delay_cache_tags_invalidation/delay_cache_tags_invalidation.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/delay_cache_tags_invalidation/src/Hook/DelayCacheTagsInvalidationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function dependency_version_test_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\dependency_version_test\\\\Hook\\\\DependencyVersionTestHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/dependency_version_test/dependency_version_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/dependency_version_test/src/Hook/DependencyVersionTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function deprecated_module_test_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\deprecated_module_test\\\\Hook\\\\DeprecatedModuleTestHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/deprecated_module_test/deprecated_module_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/deprecated_module_test/src/Hook/DeprecatedModuleTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -40810,399 +40828,399 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function element_info_test_element_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\element_info_test\\\\Hook\\\\ElementInfoTestHooks\\:\\:elementInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/element_info_test/element_info_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function element_info_test_element_plugin_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\element_info_test\\\\Hook\\\\ElementInfoTestHooks\\:\\:elementPluginAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/element_info_test/element_info_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_block_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:blockCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_block_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:blockDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_block_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:blockInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_block_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:blockLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_block_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:blockPredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_block_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:blockPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_block_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:blockUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_comment_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:commentCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_comment_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:commentDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_comment_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:commentInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_comment_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:commentLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_comment_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:commentPredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_comment_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:commentPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_comment_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:commentUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_entity_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:entityCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_entity_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:entityDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_entity_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:entityInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_entity_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:entityLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_entity_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:entityPredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_entity_preload\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:entityPreload\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_entity_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:entityPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_entity_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:entityUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_file_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:fileCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_file_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:fileDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_file_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:fileInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_file_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:fileLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_file_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:filePredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_file_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:filePresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_file_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:fileUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_node_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:nodeCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_node_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:nodeDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_node_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:nodeInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_node_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:nodeLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_node_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:nodePredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_node_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:nodePresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_node_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:nodeUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_term_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyTermCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_term_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyTermDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_term_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyTermInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_term_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyTermLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_term_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyTermPredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_term_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyTermPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_term_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyTermUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_vocabulary_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyVocabularyCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_vocabulary_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyVocabularyDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_vocabulary_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyVocabularyInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_vocabulary_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyVocabularyLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_vocabulary_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyVocabularyPredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_vocabulary_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyVocabularyPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_taxonomy_vocabulary_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:taxonomyVocabularyUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_user_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:userCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_user_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:userDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_user_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:userInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_user_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:userLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_user_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:userPredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_user_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:userPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_crud_hook_test_user_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_crud_hook_test\\\\Hook\\\\EntityCrudHookTestHooks\\:\\:userUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_reference_test_entity_base_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_reference_test\\\\Hook\\\\EntityReferenceTestHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_reference_test/entity_reference_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_reference_test/src/Hook/EntityReferenceTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_reference_test_entity_base_field_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_reference_test\\\\Hook\\\\EntityReferenceTestHooks\\:\\:entityBaseFieldInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_reference_test/entity_reference_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_reference_test/src/Hook/EntityReferenceTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_schema_test_entity_base_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_schema_test\\\\Hook\\\\EntitySchemaTestHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_schema_test/entity_schema_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_schema_test/src/Hook/EntitySchemaTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_schema_test_entity_bundle_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_schema_test\\\\Hook\\\\EntitySchemaTestHooks\\:\\:entityBundleCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_schema_test/entity_schema_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_schema_test/src/Hook/EntitySchemaTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_schema_test_entity_bundle_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_schema_test\\\\Hook\\\\EntitySchemaTestHooks\\:\\:entityBundleDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_schema_test/entity_schema_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_schema_test/src/Hook/EntitySchemaTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_schema_test_entity_bundle_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_schema_test\\\\Hook\\\\EntitySchemaTestHooks\\:\\:entityBundleFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_schema_test/entity_schema_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_schema_test/src/Hook/EntitySchemaTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_schema_test_entity_field_storage_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_schema_test\\\\Hook\\\\EntitySchemaTestHooks\\:\\:entityFieldStorageInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_schema_test/entity_schema_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_schema_test/src/Hook/EntitySchemaTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -41230,411 +41248,411 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function entity_test_form_entity_test_form_validate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_base_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function entity_test_form_entity_test_form_validate_check\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_base_field_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Controller\\\\EntityTestController\\:\\:testAdmin\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_bundle_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Entity\\\\EntityTest\\:\\:preCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_bundle_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:enableEventTracking\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_create_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:enableLiveDefinitionUpdates\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_display_build_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onEntityTypeCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_extra_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onEntityTypeDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_field_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onEntityTypeEvent\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_field_access_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onEntityTypeUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_form_display_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onFieldStorageDefinitionCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_form_mode_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onFieldStorageDefinitionDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_form_mode_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onFieldStorageDefinitionEvent\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onFieldStorageDefinitionUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_operation_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onFieldableEntityTypeCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_predelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onFieldableEntityTypeUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_prepare_view\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:storeDefinitionUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:storeEvent\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_revision_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestForm\\:\\:form\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestForm\\:\\:prepareEntity\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestForm.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_create_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestPermissions\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestPermissions.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestPermissions\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestPermissions.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mul_changed_translation_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestViewBuilder\\:\\:buildComponents\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestViewBuilder.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mul_changed_translation_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mul_changed_translation_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mul_langcode_key_translation_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityBaseFieldInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mul_langcode_key_translation_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityBundleInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mul_langcode_key_translation_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityBundleInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mul_translation_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityCreateAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mul_translation_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityDisplayBuildAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mul_translation_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityExtraFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mulrev_changed_translation_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityFieldAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mulrev_changed_translation_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityFieldAccessAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mulrev_changed_translation_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityFormDisplayAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mulrev_revision_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityFormModeAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mulrev_translation_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityFormModeInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mulrev_translation_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_test_mulrev_translation_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityOperationAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_translation_create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityPredelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_translation_delete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityPrepareView\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_translation_insert\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_update\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityRevisionCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_entity_view_mode_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_form_entity_test_form_validate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestCreateAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_form_entity_test_form_validate_check\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_query_entity_test_access_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulChangedTranslationCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_views_data_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulChangedTranslationDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/entity_test.views.inc',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\Controller\\\\EntityTestController\\:\\:testAdmin\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulChangedTranslationInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Controller/EntityTestController.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\Entity\\\\EntityTest\\:\\:preCreate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulLangcodeKeyTranslationCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:enableEventTracking\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulLangcodeKeyTranslationDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:enableLiveDefinitionUpdates\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulLangcodeKeyTranslationInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onEntityTypeCreate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulTranslationCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onEntityTypeDelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulTranslationDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onEntityTypeEvent\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulTranslationInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onEntityTypeUpdate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulrevChangedTranslationCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onFieldStorageDefinitionCreate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulrevChangedTranslationDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onFieldStorageDefinitionDelete\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulrevChangedTranslationInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onFieldStorageDefinitionEvent\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulrevRevisionCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onFieldStorageDefinitionUpdate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulrevTranslationCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onFieldableEntityTypeCreate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulrevTranslationDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:onFieldableEntityTypeUpdate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTestMulrevTranslationInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:storeDefinitionUpdate\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTranslationCreate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestDefinitionSubscriber\\:\\:storeEvent\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTranslationDelete\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestDefinitionSubscriber.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestForm\\:\\:form\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityTranslationInsert\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestForm.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestForm\\:\\:prepareEntity\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityUpdate\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestForm.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestPermissions\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:entityViewModeInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestPermissions.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestPermissions\\:\\:getNumberOfPlurals\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestHooks\\:\\:queryEntityTestAccessAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestPermissions.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\entity_test\\\\EntityTestViewBuilder\\:\\:buildComponents\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test\\\\Hook\\\\EntityTestViewsHooks\\:\\:viewsDataAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/EntityTestViewBuilder.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/src/Hook/EntityTestViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -42290,12 +42308,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/entity_test/update/status_report_8002.inc',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function entity_test_bundle_class_entity_bundle_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_bundle_class/entity_test_bundle_class.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\entity_test_bundle_class\\\\Entity\\\\EntityTestBundleClass\\:\\:postCreate\\(\\) has no return type specified\\.$#',
@@ -42328,51 +42340,39 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_constraints_entity_type_build\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_constraints/entity_test_constraints.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function entity_test_extra_entity_base_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test_bundle_class\\\\Hook\\\\EntityTestBundleClassHooks\\:\\:entityBundleInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_extra/entity_test_extra.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_bundle_class/src/Hook/EntityTestBundleClassHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_extra_entity_bundle_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test_constraints\\\\Hook\\\\EntityTestConstraintsHooks\\:\\:entityTypeBuild\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_extra/entity_test_extra.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_constraints/src/Hook/EntityTestConstraintsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_extra_entity_field_storage_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test_extra\\\\Hook\\\\EntityTestExtraHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_extra/entity_test_extra.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_extra/src/Hook/EntityTestExtraHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_operation_entity_operation\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test_extra\\\\Hook\\\\EntityTestExtraHooks\\:\\:entityBundleFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_operation/entity_test_operation.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_extra/src/Hook/EntityTestExtraHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_update_entity_base_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test_extra\\\\Hook\\\\EntityTestExtraHooks\\:\\:entityFieldStorageInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_update/entity_test_update.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_extra/src/Hook/EntityTestExtraHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function entity_test_update_entity_field_storage_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\entity_test_operation\\\\Hook\\\\EntityTestOperationHooks\\:\\:entityOperation\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_update/entity_test_update.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function entity_test_update_view_presave\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_update/entity_test_update.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_operation/src/Hook/EntityTestOperationHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -42422,6 +42422,24 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_update/src/EventSubscriber/EntitySchemaSubscriber.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\entity_test_update\\\\Hook\\\\EntityTestUpdateHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_update/src/Hook/EntityTestUpdateHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\entity_test_update\\\\Hook\\\\EntityTestUpdateHooks\\:\\:entityFieldStorageInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_update/src/Hook/EntityTestUpdateHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\entity_test_update\\\\Hook\\\\EntityTestUpdateHooks\\:\\:viewPresave\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/tests/modules/entity_test_update/src/Hook/EntityTestUpdateHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function equivalent_update_test_update_100000\\(\\) has no return type specified\\.$#',
@@ -42532,15 +42550,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function experimental_module_requirements_test_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\experimental_module_requirements_test\\\\Hook\\\\ExperimentalModuleRequirementsTestHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/experimental_module_requirements_test/experimental_module_requirements_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/experimental_module_requirements_test/src/Hook/ExperimentalModuleRequirementsTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function experimental_module_test_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\experimental_module_test\\\\Hook\\\\ExperimentalModuleTestHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/experimental_module_test/experimental_module_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/experimental_module_test/src/Hook/ExperimentalModuleTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -43186,15 +43204,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function jquery_keyevent_polyfill_test_library_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\jquery_keyevent_polyfill_test\\\\Hook\\\\JqueryKeyeventPolyfillTestHooks\\:\\:libraryInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/jquery_keyevent_polyfill_test/jquery_keyevent_polyfill_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/jquery_keyevent_polyfill_test/src/Hook/JqueryKeyeventPolyfillTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function js_deprecation_test_js_settings_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\js_deprecation_test\\\\Hook\\\\JsDeprecationTestHooks\\:\\:jsSettingsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/js_deprecation_test/src/Hook/JsDeprecationTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -43210,21 +43228,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function js_testing_ajax_request_test_page_attachments\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\js_testing_ajax_request_test\\\\Hook\\\\JsTestingAjaxRequestTestHooks\\:\\:pageAttachments\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/js_testing_ajax_request_test/js_testing_ajax_request_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/js_testing_ajax_request_test/src/Hook/JsTestingAjaxRequestTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function js_testing_log_test_js_settings_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\js_testing_log_test\\\\Hook\\\\JsTestingLogTestHooks\\:\\:jsSettingsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/js_testing_log_test/js_testing_log_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/js_testing_log_test/src/Hook/JsTestingLogTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function js_testing_log_test_page_attachments\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\js_testing_log_test\\\\Hook\\\\JsTestingLogTestHooks\\:\\:pageAttachments\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/js_testing_log_test/js_testing_log_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/js_testing_log_test/src/Hook/JsTestingLogTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -43294,9 +43312,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function lazy_route_provider_install_test_menu_links_discovered_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\lazy_route_provider_install_test\\\\Hook\\\\LazyRouteProviderInstallTestHooks\\:\\:menuLinksDiscoveredAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/lazy_route_provider_install_test/lazy_route_provider_install_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/lazy_route_provider_install_test/src/Hook/LazyRouteProviderInstallTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: constructor.unusedParameter
@@ -43317,45 +43335,45 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function link_generation_test_link_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\link_generation_test\\\\Hook\\\\LinkGenerationTestHooks\\:\\:linkAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/link_generation_test/link_generation_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/link_generation_test/src/Hook/LinkGenerationTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function mail_cancel_test_mail_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\mail_cancel_test\\\\Hook\\\\MailCancelTestHooks\\:\\:mailAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/mail_cancel_test/mail_cancel_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/mail_cancel_test/src/Hook/MailCancelTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function mail_html_test_mail\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\mail_html_test\\\\Hook\\\\MailHtmlTestHooks\\:\\:mail\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/mail_html_test/mail_html_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/mail_html_test/src/Hook/MailHtmlTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function many_assets_test_library_info_build\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\many_assets_test\\\\Hook\\\\ManyAssetsTestHooks\\:\\:libraryInfoBuild\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/many_assets_test/many_assets_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/many_assets_test/src/Hook/ManyAssetsTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function menu_test_menu_links_discovered_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_test\\\\Access\\\\AccessCheck\\:\\:create\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/menu_test/menu_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/menu_test/src/Access/AccessCheck.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function menu_test_menu_local_tasks_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_test\\\\Hook\\\\MenuTestHooks\\:\\:menuLinksDiscoveredAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/menu_test/menu_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/menu_test/src/Hook/MenuTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\menu_test\\\\Access\\\\AccessCheck\\:\\:create\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\menu_test\\\\Hook\\\\MenuTestHooks\\:\\:menuLocalTasksAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/menu_test/src/Access/AccessCheck.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/menu_test/src/Hook/MenuTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -43419,9 +43437,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function module_required_test_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\module_required_test\\\\Hook\\\\ModuleRequiredTestHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/module_required_test/module_required_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/module_required_test/src/Hook/ModuleRequiredTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -43508,9 +43526,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function off_canvas_test_page_attachments\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\off_canvas_test\\\\Hook\\\\OffCanvasTestHooks\\:\\:pageAttachments\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/off_canvas_test/off_canvas_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/off_canvas_test/src/Hook/OffCanvasTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -43664,15 +43682,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function plugin_test_plugin_test_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\plugin_test\\\\Hook\\\\PluginTestHooks\\:\\:pluginTestAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/plugin_test/plugin_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/plugin_test/src/Hook/PluginTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function plugin_test_test_plugin_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\plugin_test\\\\Hook\\\\PluginTestHooks\\:\\:testPluginInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/plugin_test/plugin_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/plugin_test/src/Hook/PluginTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -43859,9 +43877,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function router_installer_test_modules_installed\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\router_installer_test\\\\Hook\\\\RouterInstallerTestHooks\\:\\:modulesInstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/router_installer_test/router_installer_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/router_installer_test/src/Hook/RouterInstallerTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -44075,15 +44093,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function session_exists_cache_context_test_page_top\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\session_exists_cache_context_test\\\\Hook\\\\SessionExistsCacheContextTestHooks\\:\\:pageTop\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/session_exists_cache_context_test/session_exists_cache_context_test.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function session_test_user_login\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/session_test/session_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/session_exists_cache_context_test/src/Hook/SessionExistsCacheContextTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -44109,6 +44121,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/session_test/src/Form/SessionTestForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\session_test\\\\Hook\\\\SessionTestHooks\\:\\:userLogin\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/tests/modules/session_test/src/Hook/SessionTestHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\session_test\\\\Session\\\\TestSessionBag\\:\\:clearFlag\\(\\) has no return type specified\\.$#',
@@ -44123,9 +44141,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function system_module_test_page_attachments_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\system_module_test\\\\Hook\\\\SystemModuleTestHooks\\:\\:pageAttachmentsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/system_module_test/system_module_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/system_module_test/src/Hook/SystemModuleTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -44213,67 +44231,67 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\system_test\\\\MockFileTransfer\\:\\:getSettingsForm\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\system_test\\\\Hook\\\\SystemTestHooks\\:\\:filetransferInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/system_test/src/MockFileTransfer.php',
+	'path' => __DIR__ . '/modules/system/tests/modules/system_test/src/Hook/SystemTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function _system_test_first_shutdown_function\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\system_test\\\\Hook\\\\SystemTestHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/system_test/system_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/system_test/src/Hook/SystemTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function _system_test_second_shutdown_function\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\system_test\\\\Hook\\\\SystemTestHooks\\:\\:modulePreinstall\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/system_test/system_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/system_test/src/Hook/SystemTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function system_test_filetransfer_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\system_test\\\\Hook\\\\SystemTestHooks\\:\\:modulePreuninstall\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/system_test/system_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/system_test/src/Hook/SystemTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function system_test_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\system_test\\\\Hook\\\\SystemTestHooks\\:\\:modulesInstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/system_test/system_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/system_test/src/Hook/SystemTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function system_test_module_preinstall\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\system_test\\\\Hook\\\\SystemTestHooks\\:\\:modulesUninstalled\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/system_test/system_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/system_test/src/Hook/SystemTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function system_test_module_preuninstall\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\system_test\\\\Hook\\\\SystemTestHooks\\:\\:pageAttachments\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/system_test/system_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/system_test/src/Hook/SystemTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function system_test_modules_installed\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\system_test\\\\Hook\\\\SystemTestHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/system_test/system_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/system_test/src/Hook/SystemTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function system_test_modules_uninstalled\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\system_test\\\\MockFileTransfer\\:\\:getSettingsForm\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/system_test/system_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/system_test/src/MockFileTransfer.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function system_test_page_attachments\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function _system_test_first_shutdown_function\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/system_test/system_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function system_test_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function _system_test_second_shutdown_function\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/system_test/system_test.module',
 ];
@@ -44357,9 +44375,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function theme_page_test_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\theme_page_test\\\\Hook\\\\ThemePageTestHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/theme_page_test/theme_page_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/theme_page_test/src/Hook/ThemePageTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -44369,21 +44387,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function theme_suggestions_test_theme_suggestions_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\theme_suggestions_test\\\\Hook\\\\ThemeSuggestionsTestHooks\\:\\:themeSuggestionsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/theme_suggestions_test/src/Hook/ThemeSuggestionsTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function theme_suggestions_test_theme_suggestions_theme_test_specific_suggestions_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\theme_suggestions_test\\\\Hook\\\\ThemeSuggestionsTestHooks\\:\\:themeSuggestionsThemeTestSpecificSuggestionsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/theme_suggestions_test/src/Hook/ThemeSuggestionsTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function theme_suggestions_test_theme_suggestions_theme_test_suggestions_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\theme_suggestions_test\\\\Hook\\\\ThemeSuggestionsTestHooks\\:\\:themeSuggestionsThemeTestSuggestionsAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/theme_suggestions_test/src/Hook/ThemeSuggestionsTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -44397,6 +44415,36 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/src/EventSubscriber/ThemeTestSubscriber.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\theme_test\\\\Hook\\\\ThemeTestHooks\\:\\:pageBottom\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/src/Hook/ThemeTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\theme_test\\\\Hook\\\\ThemeTestHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/src/Hook/ThemeTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\theme_test\\\\Hook\\\\ThemeTestHooks\\:\\:themeRegistryAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/src/Hook/ThemeTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\theme_test\\\\Hook\\\\ThemeTestHooks\\:\\:themeSuggestionsAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/src/Hook/ThemeTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\theme_test\\\\Hook\\\\ThemeTestHooks\\:\\:themeSuggestionsThemeTestSuggestionsAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/src/Hook/ThemeTestHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\theme_test\\\\ThemeTestController\\:\\:generalSuggestionAlter\\(\\) has no return type specified\\.$#',
@@ -44469,12 +44517,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/theme_test.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function theme_test_page_bottom\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/theme_test.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function theme_test_preprocess_html\\(\\) has no return type specified\\.$#',
@@ -44493,24 +44535,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/theme_test.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function theme_test_system_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/theme_test.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function theme_test_theme_registry_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/theme_test.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function theme_test_theme_suggestions_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/theme_test.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function theme_test_theme_suggestions_node\\(\\) has no return type specified\\.$#',
@@ -44529,12 +44553,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/theme_test.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function theme_test_theme_suggestions_theme_test_suggestions_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/theme_test/theme_test.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\trusted_hosts_test\\\\Controller\\\\TrustedHostsTestController\\:\\:bagType\\(\\) has no return type specified\\.$#',
@@ -44657,15 +44675,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function unique_field_constraint_test_entity_base_field_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\unique_field_constraint_test\\\\Hook\\\\UniqueFieldConstraintTestHooks\\:\\:entityBaseFieldInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/unique_field_constraint_test/unique_field_constraint_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/unique_field_constraint_test/src/Hook/UniqueFieldConstraintTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function unique_field_constraint_test_query_entity_test_access_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\unique_field_constraint_test\\\\Hook\\\\UniqueFieldConstraintTestHooks\\:\\:queryEntityTestAccessAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/unique_field_constraint_test/unique_field_constraint_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/unique_field_constraint_test/src/Hook/UniqueFieldConstraintTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -44681,39 +44699,39 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function update_script_test_requirements\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\update_script_test\\\\Hook\\\\UpdateScriptTestHooks\\:\\:cacheFlush\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/update_script_test/update_script_test.install',
+	'path' => __DIR__ . '/modules/system/tests/modules/update_script_test/src/Hook/UpdateScriptTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function update_script_test_update_7200\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\update_script_test\\\\Hook\\\\UpdateScriptTestHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/update_script_test/update_script_test.install',
+	'path' => __DIR__ . '/modules/system/tests/modules/update_script_test/src/Hook/UpdateScriptTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function update_script_test_update_7201\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function update_script_test_requirements\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/update_script_test/update_script_test.install',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function update_script_test_update_8001\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function update_script_test_update_7200\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/system/tests/modules/update_script_test/update_script_test.install',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function update_script_test_cache_flush\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function update_script_test_update_7201\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/update_script_test/update_script_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/update_script_test/update_script_test.install',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function update_script_test_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function update_script_test_update_8001\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/modules/update_script_test/update_script_test.module',
+	'path' => __DIR__ . '/modules/system/tests/modules/update_script_test/update_script_test.install',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -47003,9 +47021,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function test_module_required_by_theme_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\test_module_required_by_theme\\\\Hook\\\\TestModuleRequiredByThemeHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/test_module_required_by_theme.module',
+	'path' => __DIR__ . '/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/src/Hook/TestModuleRequiredByThemeHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -47115,6 +47133,72 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/taxonomy/src/Form/VocabularyResetForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\taxonomy\\\\Hook\\\\TaxonomyHooks\\:\\:entityOperation\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/taxonomy/src/Hook/TaxonomyHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\taxonomy\\\\Hook\\\\TaxonomyHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/taxonomy/src/Hook/TaxonomyHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\taxonomy\\\\Hook\\\\TaxonomyHooks\\:\\:localTasksAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/taxonomy/src/Hook/TaxonomyHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\taxonomy\\\\Hook\\\\TaxonomyHooks\\:\\:nodeInsert\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/taxonomy/src/Hook/TaxonomyHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\taxonomy\\\\Hook\\\\TaxonomyHooks\\:\\:nodePredelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/taxonomy/src/Hook/TaxonomyHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\taxonomy\\\\Hook\\\\TaxonomyHooks\\:\\:nodeUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/taxonomy/src/Hook/TaxonomyHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\taxonomy\\\\Hook\\\\TaxonomyHooks\\:\\:taxonomyTermDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/taxonomy/src/Hook/TaxonomyHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\taxonomy\\\\Hook\\\\TaxonomyTokensHooks\\:\\:tokenInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\taxonomy\\\\Hook\\\\TaxonomyTokensHooks\\:\\:tokens\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\taxonomy\\\\Hook\\\\TaxonomyViewsHooks\\:\\:fieldViewsDataAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/taxonomy/src/Hook/TaxonomyViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\taxonomy\\\\Hook\\\\TaxonomyViewsHooks\\:\\:viewsDataAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/taxonomy/src/Hook/TaxonomyViewsHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\taxonomy\\\\Plugin\\\\Validation\\\\Constraint\\\\TaxonomyTermHierarchyConstraintValidator\\:\\:create\\(\\) has no return type specified\\.$#',
@@ -47499,48 +47583,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/taxonomy/taxonomy.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function taxonomy_entity_operation\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/taxonomy.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function taxonomy_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/taxonomy.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function taxonomy_local_tasks_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/taxonomy.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function taxonomy_node_insert\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/taxonomy.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function taxonomy_node_predelete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/taxonomy.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function taxonomy_node_update\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/taxonomy.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function taxonomy_taxonomy_term_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/taxonomy.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function taxonomy_term_is_page\\(\\) has no return type specified\\.$#',
@@ -47561,69 +47603,45 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function taxonomy_token_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/taxonomy.tokens.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function taxonomy_tokens\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/taxonomy.tokens.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function taxonomy_field_views_data_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/taxonomy.views.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function taxonomy_views_data_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/taxonomy.views.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function taxonomy_crud_taxonomy_vocabulary_presave\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\taxonomy_crud\\\\Hook\\\\TaxonomyCrudHooks\\:\\:taxonomyVocabularyPresave\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_crud/taxonomy_crud.module',
+	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_crud/src/Hook/TaxonomyCrudHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function taxonomy_term_display_configurable_test_entity_base_field_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\taxonomy_term_display_configurable_test\\\\Hook\\\\TaxonomyTermDisplayConfigurableTestHooks\\:\\:entityBaseFieldInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_term_display_configurable_test/taxonomy_term_display_configurable_test.module',
+	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_term_display_configurable_test/src/Hook/TaxonomyTermDisplayConfigurableTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function taxonomy_term_display_configurable_test_entity_type_build\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\taxonomy_term_display_configurable_test\\\\Hook\\\\TaxonomyTermDisplayConfigurableTestHooks\\:\\:entityTypeBuild\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_term_display_configurable_test/taxonomy_term_display_configurable_test.module',
+	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_term_display_configurable_test/src/Hook/TaxonomyTermDisplayConfigurableTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function taxonomy_test_query_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\taxonomy_test\\\\Hook\\\\TaxonomyTestHooks\\:\\:queryAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_test/taxonomy_test.module',
+	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_test/src/Hook/TaxonomyTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function taxonomy_test_query_taxonomy_term_access_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\taxonomy_test\\\\Hook\\\\TaxonomyTestHooks\\:\\:queryTaxonomyTermAccessAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_test/taxonomy_test.module',
+	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_test/src/Hook/TaxonomyTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function taxonomy_test_query_term_access_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\taxonomy_test\\\\Hook\\\\TaxonomyTestHooks\\:\\:queryTermAccessAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_test/taxonomy_test.module',
+	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_test/src/Hook/TaxonomyTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function taxonomy_test_taxonomy_term_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\taxonomy_test\\\\Hook\\\\TaxonomyTestHooks\\:\\:taxonomyTermLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_test/taxonomy_test.module',
+	'path' => __DIR__ . '/modules/taxonomy/tests/modules/taxonomy_test/src/Hook/TaxonomyTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -48197,21 +48215,27 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function telephone_field_formatter_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\telephone\\\\Hook\\\\TelephoneHooks\\:\\:fieldFormatterInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/telephone/telephone.module',
+	'path' => __DIR__ . '/modules/telephone/src/Hook/TelephoneHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function telephone_field_type_category_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\telephone\\\\Hook\\\\TelephoneHooks\\:\\:fieldTypeCategoryInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/telephone/telephone.module',
+	'path' => __DIR__ . '/modules/telephone/src/Hook/TelephoneHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function telephone_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\telephone\\\\Hook\\\\TelephoneHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/telephone/telephone.module',
+	'path' => __DIR__ . '/modules/telephone/src/Hook/TelephoneHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\text\\\\Hook\\\\TextHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/text/src/Hook/TextHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -48285,12 +48309,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/text/tests/src/Unit/Plugin/migrate/field/d7/TextFieldTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function text_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/text/text.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\toolbar\\\\Ajax\\\\SetSubtreesCommand\\:\\:render\\(\\) has no return type specified\\.$#',
@@ -48305,85 +48323,85 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function toolbar_disable_user_toolbar_toolbar_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\toolbar\\\\Hook\\\\ToolbarHooks\\:\\:help\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/toolbar_disable_user_toolbar.module',
+	'path' => __DIR__ . '/modules/toolbar/src/Hook/ToolbarHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function toolbar_test_preprocess_menu\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\toolbar\\\\Hook\\\\ToolbarHooks\\:\\:pageTop\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module',
+	'path' => __DIR__ . '/modules/toolbar/src/Hook/ToolbarHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function toolbar_test_toolbar\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\toolbar\\\\Hook\\\\ToolbarHooks\\:\\:toolbar\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module',
+	'path' => __DIR__ . '/modules/toolbar/src/Hook/ToolbarHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\Tests\\\\toolbar\\\\Functional\\\\ToolbarCacheContextsTest\\:\\:assertCacheContext\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\toolbar_disable_user_toolbar\\\\Hook\\\\ToolbarDisableUserToolbarHooks\\:\\:toolbarAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php',
+	'path' => __DIR__ . '/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/src/Hook/ToolbarDisableUserToolbarHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\Tests\\\\toolbar\\\\Functional\\\\ToolbarCacheContextsTest\\:\\:assertCacheMaxAge\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\toolbar_test\\\\Hook\\\\ToolbarTestHooks\\:\\:toolbar\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php',
+	'path' => __DIR__ . '/modules/toolbar/tests/modules/toolbar_test/src/Hook/ToolbarTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\Tests\\\\toolbar\\\\Functional\\\\ToolbarCacheContextsTest\\:\\:assertCacheTags\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function toolbar_test_preprocess_menu\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php',
+	'path' => __DIR__ . '/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\Tests\\\\toolbar\\\\Functional\\\\ToolbarCacheContextsTest\\:\\:assertNoCacheContext\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\Tests\\\\toolbar\\\\Functional\\\\ToolbarCacheContextsTest\\:\\:assertCacheContext\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\Tests\\\\toolbar\\\\Functional\\\\ToolbarCacheContextsTest\\:\\:assertPageCacheContextsAndTags\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\Tests\\\\toolbar\\\\Functional\\\\ToolbarCacheContextsTest\\:\\:assertCacheMaxAge\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Method Drupal\\\\Tests\\\\toolbar\\\\Functional\\\\ToolbarCacheContextsTest\\:\\:enablePageCaching\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\Tests\\\\toolbar\\\\Functional\\\\ToolbarCacheContextsTest\\:\\:assertCacheTags\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function template_preprocess_toolbar\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\Tests\\\\toolbar\\\\Functional\\\\ToolbarCacheContextsTest\\:\\:assertNoCacheContext\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/toolbar/toolbar.module',
+	'path' => __DIR__ . '/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function toolbar_help\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\Tests\\\\toolbar\\\\Functional\\\\ToolbarCacheContextsTest\\:\\:assertPageCacheContextsAndTags\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/toolbar/toolbar.module',
+	'path' => __DIR__ . '/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function toolbar_page_top\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\Tests\\\\toolbar\\\\Functional\\\\ToolbarCacheContextsTest\\:\\:enablePageCaching\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/toolbar/toolbar.module',
+	'path' => __DIR__ . '/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function toolbar_preprocess_html\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function template_preprocess_toolbar\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/toolbar/toolbar.module',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function toolbar_toolbar\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function toolbar_preprocess_html\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/toolbar/toolbar.module',
 ];
@@ -48429,6 +48447,60 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/update/src/Form/UpdateReady.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\update\\\\Hook\\\\UpdateHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/update/src/Hook/UpdateHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\update\\\\Hook\\\\UpdateHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/update/src/Hook/UpdateHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\update\\\\Hook\\\\UpdateHooks\\:\\:mail\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/update/src/Hook/UpdateHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\update\\\\Hook\\\\UpdateHooks\\:\\:modulesInstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/update/src/Hook/UpdateHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\update\\\\Hook\\\\UpdateHooks\\:\\:modulesUninstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/update/src/Hook/UpdateHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\update\\\\Hook\\\\UpdateHooks\\:\\:pageTop\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/update/src/Hook/UpdateHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\update\\\\Hook\\\\UpdateHooks\\:\\:themesInstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/update/src/Hook/UpdateHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\update\\\\Hook\\\\UpdateHooks\\:\\:themesUninstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/update/src/Hook/UpdateHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\update\\\\Hook\\\\UpdateHooks\\:\\:verifyUpdateArchive\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/update/src/Hook/UpdateHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\update\\\\ProjectCoreCompatibility\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
@@ -48551,21 +48623,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function update_test_filetransfer_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\update_test\\\\Hook\\\\UpdateTestHooks\\:\\:filetransferInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/update/tests/modules/update_test/update_test.module',
+	'path' => __DIR__ . '/modules/update/tests/modules/update_test/src/Hook/UpdateTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function update_test_system_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\update_test\\\\Hook\\\\UpdateTestHooks\\:\\:systemInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/update/tests/modules/update_test/update_test.module',
+	'path' => __DIR__ . '/modules/update/tests/modules/update_test/src/Hook/UpdateTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function update_test_update_status_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\update_test\\\\Hook\\\\UpdateTestHooks\\:\\:updateStatusAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/update/tests/modules/update_test/update_test.module',
+	'path' => __DIR__ . '/modules/update/tests/modules/update_test/src/Hook/UpdateTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -48735,6 +48807,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/update/tests/src/Unit/ProjectCoreCompatibilityTest.php',
 ];
+$ignoreErrors[] = [
+	// identifier: phpunit.covers
+	'message' => '#^@covers value \\\\update_mail references an invalid class or function\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/update/tests/src/Unit/UpdateMailTest.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function _update_authorize_clear_update_status\\(\\) has no return type specified\\.$#',
@@ -48825,12 +48903,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/update/update.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function update_cron\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/update/update.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function update_fetch_data\\(\\) has no return type specified\\.$#',
@@ -48843,36 +48915,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/update/update.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function update_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/update/update.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function update_mail\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/update/update.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function update_modules_installed\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/update/update.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function update_modules_uninstalled\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/update/update.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function update_page_top\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/update/update.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function update_refresh\\(\\) has no return type specified\\.$#',
@@ -48885,24 +48927,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/update/update.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function update_themes_installed\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/update/update.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function update_themes_uninstalled\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/update/update.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function update_verify_update_archive\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/update/update.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function update_remove_post_updates\\(\\) has no return type specified\\.$#',
@@ -49167,6 +49191,132 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/user/src/Form/UserPermissionsForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:elementInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:entityExtraFieldInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:entityOperation\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:filterFormatDisable\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:jsSettingsAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:mail\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:modulesUninstalled\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:templatePreprocessDefaultVariablesAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:toolbar\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:userLogin\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:userLogout\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:userPresave\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:userRoleDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:userRoleInsert\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:userView\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserHooks\\:\\:userViewAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserTokensHooks\\:\\:tokenInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserTokensHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserTokensHooks\\:\\:tokens\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserTokensHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserViewsExecutionHooks\\:\\:viewsQuerySubstitutions\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\user\\\\Hook\\\\UserViewsHooks\\:\\:viewsPluginsArgumentValidatorAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/user/src/Hook/UserViewsHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\user\\\\ModulePermissionsLinkHelper\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
@@ -49703,21 +49853,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function user_access_test_entity_create_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\user_access_test\\\\Hook\\\\UserAccessTestHooks\\:\\:entityCreateAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/user/tests/modules/user_access_test/user_access_test.module',
+	'path' => __DIR__ . '/modules/user/tests/modules/user_access_test/src/Hook/UserAccessTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function user_access_test_entity_field_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\user_access_test\\\\Hook\\\\UserAccessTestHooks\\:\\:entityFieldAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/user/tests/modules/user_access_test/user_access_test.module',
+	'path' => __DIR__ . '/modules/user/tests/modules/user_access_test/src/Hook/UserAccessTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function user_access_test_user_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\user_access_test\\\\Hook\\\\UserAccessTestHooks\\:\\:userAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/user/tests/modules/user_access_test/user_access_test.module',
+	'path' => __DIR__ . '/modules/user/tests/modules/user_access_test/src/Hook/UserAccessTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -50559,30 +50709,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/user/user.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_element_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_entity_extra_field_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_entity_operation\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_filter_format_disable\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function user_form_process_password_confirm\\(\\) has no return type specified\\.$#',
@@ -50595,18 +50721,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/user/user.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_js_settings_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function user_login_finalize\\(\\) has no return type specified\\.$#',
@@ -50619,24 +50733,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/user/user.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_mail\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function user_mail_tokens\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/user/user.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_modules_uninstalled\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function user_picture_enabled\\(\\) has no return type specified\\.$#',
@@ -50667,84 +50769,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/user/user.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_template_preprocess_default_variables_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_toolbar\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_user_login\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_user_logout\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_user_presave\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_user_role_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_user_role_insert\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_user_view\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_user_view_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_token_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.tokens.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_tokens\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.tokens.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_views_plugins_argument_validator_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.views.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function user_views_query_substitutions\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/user/user.views_execution.inc',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\views\\\\Ajax\\\\HighlightCommand\\:\\:render\\(\\) has no return type specified\\.$#',
@@ -51165,6 +51189,132 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/views/src/Form/ViewsFormMainForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:baseFieldOverrideDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:baseFieldOverrideInsert\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:baseFieldOverrideUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:fieldConfigDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:fieldConfigInsert\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:fieldConfigUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:localTasksAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:queryViewsAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:themeSuggestionsCommentAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:themeSuggestionsContainerAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:themeSuggestionsNodeAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:viewPresave\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsHooks\\:\\:viewsPreRender\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsTokensHooks\\:\\:tokenInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsTokensHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsTokensHooks\\:\\:tokens\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsTokensHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsViewsExecutionHooks\\:\\:viewsFormSubstitutions\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsViewsExecutionHooks\\:\\:viewsQuerySubstitutions\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsViewsHooks\\:\\:fieldViewsData\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsViewsHooks\\:\\:viewsData\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views\\\\Hook\\\\ViewsViewsHooks\\:\\:viewsDataAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/src/Hook/ViewsViewsHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\views\\\\ManyToOneHelper\\:\\:addFilter\\(\\) has no return type specified\\.$#',
@@ -57233,21 +57383,21 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function views_entity_test_entity_base_field_info\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\views_entity_test\\\\Hook\\\\ViewsEntityTestHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_entity_test/views_entity_test.module',
+	'path' => __DIR__ . '/modules/views/tests/modules/views_entity_test/src/Hook/ViewsEntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function views_entity_test_entity_field_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\views_entity_test\\\\Hook\\\\ViewsEntityTestHooks\\:\\:entityFieldAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_entity_test/views_entity_test.module',
+	'path' => __DIR__ . '/modules/views/tests/modules/views_entity_test/src/Hook/ViewsEntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function views_entity_test_entity_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\views_entity_test\\\\Hook\\\\ViewsEntityTestHooks\\:\\:entityLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_entity_test/views_entity_test.module',
+	'path' => __DIR__ . '/modules/views/tests/modules/views_entity_test/src/Hook/ViewsEntityTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -57263,15 +57413,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function views_test_config_view_load\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\views_test_config\\\\Hook\\\\ViewsTestConfigHooks\\:\\:viewLoad\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_config/views_test_config.module',
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_config/src/Hook/ViewsTestConfigHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function views_test_config_views_post_render\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\views_test_config\\\\Hook\\\\ViewsTestConfigHooks\\:\\:viewsPostRender\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_config/views_test_config.module',
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_config/src/Hook/ViewsTestConfigHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -57315,6 +57465,114 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Form/ViewsTestDataErrorForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataHooks\\:\\:viewUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsExecutionHooks\\:\\:fieldViewsData\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsExecutionHooks\\:\\:fieldViewsDataAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsExecutionHooks\\:\\:viewsFormSubstitutions\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsExecutionHooks\\:\\:viewsPostBuild\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsExecutionHooks\\:\\:viewsPostExecute\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsExecutionHooks\\:\\:viewsPostRender\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsExecutionHooks\\:\\:viewsPreBuild\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsExecutionHooks\\:\\:viewsPreExecute\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsExecutionHooks\\:\\:viewsPreRender\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsExecutionHooks\\:\\:viewsPreView\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsExecutionHooks\\:\\:viewsQueryAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsExecutionHooks\\:\\:viewsQuerySubstitutions\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: isset.variable
+	'message' => '#^Variable \\$view in isset\\(\\) always exists and is not nullable\\.$#',
+	'count' => 2,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsHooks\\:\\:viewsAnalyze\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsHooks\\:\\:viewsData\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsHooks\\:\\:viewsDataAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_test_data\\\\Hook\\\\ViewsTestDataViewsHooks\\:\\:viewsInvalidateCache\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\views_test_data\\\\Plugin\\\\views\\\\access\\\\StaticTest\\:\\:alterRouteDefinition\\(\\) has no return type specified\\.$#',
@@ -57593,117 +57851,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function views_test_data_view_update\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_analyze\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_data\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_data_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_invalidate_cache\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_field_views_data\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_field_views_data_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_form_substitutions\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_post_build\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_post_execute\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_post_render\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_pre_build\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_pre_execute\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_pre_render\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_pre_view\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_query_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_data_views_query_substitutions\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: isset.variable
-	'message' => '#^Variable \\$view in isset\\(\\) always exists and is not nullable\\.$#',
-	'count' => 2,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_test_entity_reference_views_data_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\views_test_entity_reference\\\\Hook\\\\ViewsTestEntityReferenceHooks\\:\\:viewsDataAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_entity_reference/views_test_entity_reference.module',
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_entity_reference/src/Hook/ViewsTestEntityReferenceHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -57713,19 +57863,19 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function _views_test_query_access_restrict_by_uuid\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\views_test_query_access\\\\Hook\\\\ViewsTestQueryAccessHooks\\:\\:queryBlockContentAccessAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_query_access/views_test_query_access.module',
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_query_access/src/Hook/ViewsTestQueryAccessHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function views_test_query_access_query_block_content_access_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\views_test_query_access\\\\Hook\\\\ViewsTestQueryAccessHooks\\:\\:queryMediaAccessAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/views/tests/modules/views_test_query_access/views_test_query_access.module',
+	'path' => __DIR__ . '/modules/views/tests/modules/views_test_query_access/src/Hook/ViewsTestQueryAccessHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function views_test_query_access_query_media_access_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Function _views_test_query_access_restrict_by_uuid\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/views/tests/modules/views_test_query_access/views_test_query_access.module',
 ];
@@ -59055,24 +59205,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/views/views.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_base_field_override_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_base_field_override_insert\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_base_field_override_update\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function views_disable_view\\(\\) has no return type specified\\.$#',
@@ -59091,30 +59223,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/views/views.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_field_config_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_field_config_insert\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_field_config_update\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function views_hook_info\\(\\) has no return type specified\\.$#',
@@ -59127,12 +59235,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/views/views.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_local_tasks_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function views_preprocess_comment\\(\\) has no return type specified\\.$#',
@@ -59145,42 +59247,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/views/views.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_query_views_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_theme_suggestions_comment_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_theme_suggestions_container_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_theme_suggestions_node_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_view_presave\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_views_pre_render\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function template_preprocess_views_exposed_form\\(\\) has no return type specified\\.$#',
@@ -59283,54 +59349,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/views/views.theme.inc',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_token_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.tokens.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_tokens\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.tokens.inc',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function views_entity_field_label\\(\\) has no return type specified\\.$#',
 	'count' => 1,
 	'path' => __DIR__ . '/modules/views/views.views.inc',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_field_views_data\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.views.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_views_data\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.views.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_views_data_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.views.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_views_form_substitutions\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.views_execution.inc',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_views_query_substitutions\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views/views.views_execution.inc',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function views_ui_add_ajax_trigger\\(\\) has no return type specified\\.$#',
@@ -59517,6 +59541,36 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/views_ui/src/Form/BreakLockForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_ui\\\\Hook\\\\ViewsUiHooks\\:\\:contextualLinksViewAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views_ui/src/Hook/ViewsUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_ui\\\\Hook\\\\ViewsUiHooks\\:\\:entityTypeBuild\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views_ui/src/Hook/ViewsUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_ui\\\\Hook\\\\ViewsUiHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views_ui/src/Hook/ViewsUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_ui\\\\Hook\\\\ViewsUiHooks\\:\\:viewsAnalyze\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views_ui/src/Hook/ViewsUiHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\views_ui\\\\Hook\\\\ViewsUiHooks\\:\\:viewsPluginsDisplayAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/views_ui/src/Hook/ViewsUiHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\views_ui\\\\ViewAddForm\\:\\:cancel\\(\\) has no return type specified\\.$#',
@@ -59879,15 +59933,15 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function views_ui_test_views_preview_info_alter\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\views_ui_test\\\\Hook\\\\ViewsUiTestHooks\\:\\:viewsPreviewInfoAlter\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/views_ui/tests/modules/views_ui_test/views_ui_test.module',
+	'path' => __DIR__ . '/modules/views_ui/tests/modules/views_ui_test/src/Hook/ViewsUiTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function views_ui_test_field_views_data\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\views_ui_test_field\\\\Hook\\\\ViewsUiTestFieldViewsHooks\\:\\:viewsData\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/views_ui/tests/modules/views_ui_test_field/views_ui_test_field.views.inc',
+	'path' => __DIR__ . '/modules/views_ui/tests/modules/views_ui_test_field/src/Hook/ViewsUiTestFieldViewsHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -60051,24 +60105,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/views_ui/views_ui.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_ui_contextual_links_view_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views_ui/views_ui.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_ui_entity_type_build\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views_ui/views_ui.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_ui_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views_ui/views_ui.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function views_ui_preprocess_views_view\\(\\) has no return type specified\\.$#',
@@ -60099,18 +60135,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/views_ui/views_ui.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_ui_views_analyze\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views_ui/views_ui.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function views_ui_views_plugins_display_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/views_ui/views_ui.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function template_preprocess_views_ui_build_group_filter_form\\(\\) has no return type specified\\.$#',
@@ -60345,6 +60369,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/workflows/src/Form/WorkflowTransitionEditForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workflows\\\\Hook\\\\WorkflowsHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workflows/src/Hook/WorkflowsHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\workflows\\\\Plugin\\\\WorkflowTypeBase\\:\\:setConfiguration\\(\\) has no return type specified\\.$#',
@@ -60447,6 +60477,18 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/workflows/tests/modules/workflow_type_test/src/Form/ComplexTestTypeConfigureForm.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workflow_type_test\\\\Hook\\\\WorkflowTypeTestHooks\\:\\:workflowAccess\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workflows/tests/modules/workflow_type_test/src/Hook/WorkflowTypeTestHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workflow_type_test\\\\Hook\\\\WorkflowTypeTestHooks\\:\\:workflowTypeInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workflows/tests/modules/workflow_type_test/src/Hook/WorkflowTypeTestHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\workflow_type_test\\\\Plugin\\\\WorkflowType\\\\ComplexTestType\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
@@ -60483,18 +60525,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/workflows/tests/modules/workflow_type_test/workflow_type_test.module',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workflow_type_test_workflow_access\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workflows/tests/modules/workflow_type_test/workflow_type_test.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workflow_type_test_workflow_type_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workflows/tests/modules/workflow_type_test/workflow_type_test.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\Tests\\\\workflows\\\\Functional\\\\Rest\\\\WorkflowJsonAnonTest\\:\\:assertAuthenticationEdgeCases\\(\\) has no return type specified\\.$#',
@@ -60609,12 +60639,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/workflows/tests/src/Unit/WorkflowStateTransitionOperationsAccessCheckTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workflows_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workflows/workflows.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\workspaces\\\\Controller\\\\WorkspacesHtmlEntityFormController\\:\\:formatPlural\\(\\) has no return type specified\\.$#',
@@ -60945,6 +60969,114 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/workspaces/src/FormOperations.php',
 ];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:cron\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:entityAccess\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:entityBaseFieldInfo\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:entityCreateAccess\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:entityDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:entityInsert\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:entityPredelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:entityPreload\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:entityPresave\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:entityRevisionDelete\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:entityTypeBuild\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:entityUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:fieldInfoAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:help\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:menuLinkContentUpdate\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:modulePreinstall\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:toolbar\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
+$ignoreErrors[] = [
+	// identifier: missingType.return
+	'message' => '#^Method Drupal\\\\workspaces\\\\Hook\\\\WorkspacesHooks\\:\\:viewsQueryAlter\\(\\) has no return type specified\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/src/Hook/WorkspacesHooks.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\workspaces\\\\Negotiator\\\\SessionWorkspaceNegotiator\\:\\:getActiveWorkspace\\(\\) has no return type specified\\.$#',
@@ -61205,9 +61337,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function workspace_access_test_workspace_access\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\workspace_access_test\\\\Hook\\\\WorkspaceAccessTestHooks\\:\\:workspaceAccess\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/tests/modules/workspace_access_test/workspace_access_test.module',
+	'path' => __DIR__ . '/modules/workspaces/tests/modules/workspace_access_test/src/Hook/WorkspaceAccessTestHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
@@ -62037,6 +62169,30 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceInformationTest.php',
 ];
+$ignoreErrors[] = [
+	// identifier: phpunit.coversMethod
+	'message' => '#^@covers value \\:\\:workspaces_entity_delete references an invalid method\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php',
+];
+$ignoreErrors[] = [
+	// identifier: phpunit.coversMethod
+	'message' => '#^@covers value \\:\\:workspaces_entity_insert references an invalid method\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php',
+];
+$ignoreErrors[] = [
+	// identifier: phpunit.coversMethod
+	'message' => '#^@covers value \\:\\:workspaces_entity_presave references an invalid method\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php',
+];
+$ignoreErrors[] = [
+	// identifier: phpunit.coversMethod
+	'message' => '#^@covers value \\:\\:workspaces_entity_revision_delete references an invalid method\\.$#',
+	'count' => 1,
+	'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceIntegrationTest.php',
+];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceIntegrationTest\\:\\:assertWorkspaceAssociation\\(\\) has no return type specified\\.$#',
@@ -62211,12 +62367,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspacesFileItemTest.php',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_module_preinstall\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.install',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function workspaces_requirements\\(\\) has no return type specified\\.$#',
@@ -62229,108 +62379,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/modules/workspaces/workspaces.install',
 ];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_cron\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_entity_access\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_entity_base_field_info\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_entity_create_access\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_entity_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_entity_insert\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_entity_predelete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_entity_preload\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_entity_presave\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_entity_revision_delete\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_entity_type_build\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_entity_update\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_field_info_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_help\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_menu_link_content_update\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_toolbar\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
-$ignoreErrors[] = [
-	// identifier: missingType.return
-	'message' => '#^Function workspaces_views_query_alter\\(\\) has no return type specified\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/modules/workspaces/workspaces.module',
-];
 $ignoreErrors[] = [
 	// identifier: missingType.return
 	'message' => '#^Function demo_umami_requirements\\(\\) has no return type specified\\.$#',
@@ -62363,9 +62411,9 @@
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
-	'message' => '#^Function demo_umami_content_module_preinstall\\(\\) has no return type specified\\.$#',
+	'message' => '#^Method Drupal\\\\demo_umami_content\\\\Hook\\\\DemoUmamiContentHooks\\:\\:modulePreinstall\\(\\) has no return type specified\\.$#',
 	'count' => 1,
-	'path' => __DIR__ . '/profiles/demo_umami/modules/demo_umami_content/demo_umami_content.install',
+	'path' => __DIR__ . '/profiles/demo_umami/modules/demo_umami_content/src/Hook/DemoUmamiContentHooks.php',
 ];
 $ignoreErrors[] = [
 	// identifier: missingType.return
diff --git a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
index 6c73a0d85e469bc79623bb17c7cf696cc9d5c8cd..a2c47433c50bb253a2cd31188493474f02ddb3b9 100644
--- a/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
+++ b/core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
@@ -62,7 +62,7 @@ public function renderBarePage(array $content, $title, $page_theme_property, arr
 
     // Add the bare minimum of attachments from the system module and the
     // current maintenance theme.
-    system_page_attachments($html['page']);
+    _system_page_attachments($html['page']);
     $this->renderer->renderRoot($html);
 
     $response = new HtmlResponse();
diff --git a/core/modules/announcements_feed/announcements_feed.module b/core/modules/announcements_feed/announcements_feed.module
deleted file mode 100644
index 1a5406cbc0ea5f619a05af267eea68e947a0266d..0000000000000000000000000000000000000000
--- a/core/modules/announcements_feed/announcements_feed.module
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-
-/**
- * @file
- * Fetch community announcements from www.drupal.org feed.
- */
-
-use Drupal\announcements_feed\RenderCallbacks;
-use Drupal\Core\Link;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function announcements_feed_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.announcements_feed':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Announcements module displays announcements from the Drupal community. For more information, see the <a href=":documentation">online documentation for the Announcements module</a>.', [':documentation' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/announcements-feed']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl><dt>' . t('Accessing announcements') . '</dt>';
-      $output .= '<dd>' . t('Users with the "View drupal.org announcements" permission may click on the "Announcements" item in the administration toolbar, or access @link, to see all announcements relevant to the Drupal version of your site.', [
-        '@link' => Link::createFromRoute(t('Announcements'), 'announcements_feed.announcement')->toString(),
-      ]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_toolbar().
- */
-function announcements_feed_toolbar() {
-  if (!\Drupal::currentUser()->hasPermission('access announcements')) {
-    return [
-      '#cache' => ['contexts' => ['user.permissions']],
-    ];
-  }
-  $items['announcement'] = [
-    '#type' => 'toolbar_item',
-    'tab' => [
-      '#lazy_builder' => [
-        'announcements_feed.lazy_builders:renderAnnouncements',
-        [],
-      ],
-      '#create_placeholder' => TRUE,
-      '#cache' => [
-        'tags' => [
-          'announcements_feed:feed',
-        ],
-      ],
-    ],
-    '#wrapper_attributes' => [
-      'class' => ['announce-toolbar-tab'],
-    ],
-    '#cache' => ['contexts' => ['user.permissions']],
-    '#weight' => 3399,
-  ];
-
-  // \Drupal\toolbar\Element\ToolbarItem::preRenderToolbarItem adds an
-  // #attributes property to each toolbar item's tab child automatically.
-  // Lazy builders don't support an #attributes property so we need to
-  // add another render callback to remove the #attributes property. We start by
-  // adding the defaults, and then we append our own pre render callback.
-  $items['announcement'] += \Drupal::service('plugin.manager.element_info')->getInfo('toolbar_item');
-  $items['announcement']['#pre_render'][] = [RenderCallbacks::class, 'removeTabAttributes'];
-  return $items;
-}
-
-/**
- * Implements hook_toolbar_alter().
- */
-function announcements_feed_toolbar_alter(&$items) {
-  // As the "Announcements" link is shown already in the top toolbar bar, we
-  // don't need it again in the administration menu tray, so hide it.
-  if (!empty($items['administration']['tray'])) {
-    $callable = function (array $element) {
-      unset($element['administration_menu']['#items']['announcements_feed.announcement']);
-      return $element;
-    };
-
-    $items['administration']['tray']['toolbar_administration']['#pre_render'][] = $callable;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function announcements_feed_theme($existing, $type, $theme, $path): array {
-  return [
-    'announcements_feed' => [
-      'variables' => [
-        'featured' => NULL,
-        'standard' => NULL,
-        'count' => 0,
-        'feed_link' => '',
-      ],
-    ],
-    'announcements_feed_admin' => [
-      'variables' => [
-        'featured' => NULL,
-        'standard' => NULL,
-        'count' => 0,
-        'feed_link' => '',
-      ],
-    ],
-  ];
-}
-
-/**
- * Implements hook_cron().
- */
-function announcements_feed_cron() {
-  $config = \Drupal::config('announcements_feed.settings');
-  $interval = $config->get('cron_interval');
-  $last_check = \Drupal::state()->get('announcements_feed.last_fetch', 0);
-  $time = \Drupal::time()->getRequestTime();
-  if (($time - $last_check) > $interval) {
-    \Drupal::service('announcements_feed.fetcher')->fetch(TRUE);
-    \Drupal::state()->set('announcements_feed.last_fetch', $time);
-  }
-}
diff --git a/core/modules/announcements_feed/src/Hook/AnnouncementsFeedHooks.php b/core/modules/announcements_feed/src/Hook/AnnouncementsFeedHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..0fba340a6e615c3bccebe57765d061a67784d13d
--- /dev/null
+++ b/core/modules/announcements_feed/src/Hook/AnnouncementsFeedHooks.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Drupal\announcements_feed\Hook;
+
+use Drupal\announcements_feed\RenderCallbacks;
+use Drupal\Core\Link;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for announcements_feed.
+ */
+class AnnouncementsFeedHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.announcements_feed':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Announcements module displays announcements from the Drupal community. For more information, see the <a href=":documentation">online documentation for the Announcements module</a>.', [
+          ':documentation' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/announcements-feed',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl><dt>' . t('Accessing announcements') . '</dt>';
+        $output .= '<dd>' . t('Users with the "View drupal.org announcements" permission may click on the "Announcements" item in the administration toolbar, or access @link, to see all announcements relevant to the Drupal version of your site.', [
+          '@link' => Link::createFromRoute(t('Announcements'), 'announcements_feed.announcement')->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_toolbar().
+   */
+  #[Hook('toolbar')]
+  public function toolbar() {
+    if (!\Drupal::currentUser()->hasPermission('access announcements')) {
+      return ['#cache' => ['contexts' => ['user.permissions']]];
+    }
+    $items['announcement'] = [
+      '#type' => 'toolbar_item',
+      'tab' => [
+        '#lazy_builder' => [
+          'announcements_feed.lazy_builders:renderAnnouncements',
+                  [],
+        ],
+        '#create_placeholder' => TRUE,
+        '#cache' => [
+          'tags' => [
+            'announcements_feed:feed',
+          ],
+        ],
+      ],
+      '#wrapper_attributes' => [
+        'class' => [
+          'announce-toolbar-tab',
+        ],
+      ],
+      '#cache' => [
+        'contexts' => [
+          'user.permissions',
+        ],
+      ],
+      '#weight' => 3399,
+    ];
+    // \Drupal\toolbar\Element\ToolbarItem::preRenderToolbarItem adds an
+    // #attributes property to each toolbar item's tab child automatically.
+    // Lazy builders don't support an #attributes property so we need to
+    // add another render callback to remove the #attributes property. We start by
+    // adding the defaults, and then we append our own pre render callback.
+    $items['announcement'] += \Drupal::service('plugin.manager.element_info')->getInfo('toolbar_item');
+    $items['announcement']['#pre_render'][] = [RenderCallbacks::class, 'removeTabAttributes'];
+    return $items;
+  }
+
+  /**
+   * Implements hook_toolbar_alter().
+   */
+  #[Hook('toolbar_alter')]
+  public function toolbarAlter(&$items) {
+    // As the "Announcements" link is shown already in the top toolbar bar, we
+    // don't need it again in the administration menu tray, so hide it.
+    if (!empty($items['administration']['tray'])) {
+      $callable = function (array $element) {
+        unset($element['administration_menu']['#items']['announcements_feed.announcement']);
+        return $element;
+      };
+      $items['administration']['tray']['toolbar_administration']['#pre_render'][] = $callable;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme($existing, $type, $theme, $path) : array {
+    return [
+      'announcements_feed' => [
+        'variables' => [
+          'featured' => NULL,
+          'standard' => NULL,
+          'count' => 0,
+          'feed_link' => '',
+        ],
+      ],
+      'announcements_feed_admin' => [
+        'variables' => [
+          'featured' => NULL,
+          'standard' => NULL,
+          'count' => 0,
+          'feed_link' => '',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_cron().
+   */
+  #[Hook('cron')]
+  public function cron() {
+    $config = \Drupal::config('announcements_feed.settings');
+    $interval = $config->get('cron_interval');
+    $last_check = \Drupal::state()->get('announcements_feed.last_fetch', 0);
+    $time = \Drupal::time()->getRequestTime();
+    if ($time - $last_check > $interval) {
+      \Drupal::service('announcements_feed.fetcher')->fetch(TRUE);
+      \Drupal::state()->set('announcements_feed.last_fetch', $time);
+    }
+  }
+
+}
diff --git a/core/modules/automated_cron/automated_cron.module b/core/modules/automated_cron/automated_cron.module
index 72ccc46fdcc00a10e3648883de6aa510fbfcc764..f1f2b482d3df095cf38fe0fe483f0adf7ca5de0a 100644
--- a/core/modules/automated_cron/automated_cron.module
+++ b/core/modules/automated_cron/automated_cron.module
@@ -2,55 +2,10 @@
 
 /**
  * @file
- * Provides an automated cron by executing it at the end of a response.
  */
 
-use Drupal\Core\Url;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Form\FormStateInterface;
 
-/**
- * Implements hook_help().
- */
-function automated_cron_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.automated_cron':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Automated Cron module runs cron operations for your site using normal browser/page requests instead of having to set up a separate cron job. The Automated Cron module checks at the end of each server response when cron operation was last ran and, if it has been too long since last run, it executes the cron tasks after sending a server response. For more information, see the <a href=":automated_cron-documentation">online documentation for the Automated Cron module</a>.', [':automated_cron-documentation' => 'https://www.drupal.org/documentation/modules/automated_cron']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Configuring Automated Cron') . '</dt>';
-      $output .= '<dd>' . t('On the <a href=":cron-settings">Cron page</a>, you can set the frequency (time interval) for running cron jobs.', [':cron-settings' => Url::fromRoute('system.cron_settings')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Disabling Automated Cron') . '</dt>';
-      $output .= '<dd>' . t('To disable automated cron, the recommended method is to uninstall the module, to reduce site overhead. If you only want to disable it temporarily, you can set the frequency to Never on the Cron page, and then change the frequency back when you want to start it up again.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for the system_cron_settings() form.
- */
-function automated_cron_form_system_cron_settings_alter(&$form, &$form_state): void {
-  $automated_cron_settings = \Drupal::config('automated_cron.settings');
-
-  $options = [3600, 10800, 21600, 43200, 86400, 604800];
-  $form['cron']['interval'] = [
-    '#type' => 'select',
-    '#title' => t('Run cron every'),
-    '#description' => t('More information about setting up scheduled tasks can be found by <a href=":url">reading the cron tutorial on drupal.org</a>.', [':url' => 'https://www.drupal.org/docs/8/administering-a-drupal-8-site/cron-automated-tasks']),
-    '#default_value' => $automated_cron_settings->get('interval'),
-    '#options' => [0 => t('Never')] + array_map([\Drupal::service('date.formatter'), 'formatInterval'], array_combine($options, $options)),
-  ];
-
-  // Add submit callback.
-  $form['#submit'][] = 'automated_cron_settings_submit';
-
-  // Theme this form as a config form.
-  $form['#theme'] = 'system_config_form';
-}
-
 /**
  * Form submission handler for system_cron_settings().
  */
diff --git a/core/modules/automated_cron/src/Hook/AutomatedCronHooks.php b/core/modules/automated_cron/src/Hook/AutomatedCronHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..64be8d4c2512fde5eb5ff058c268ea308351d74a
--- /dev/null
+++ b/core/modules/automated_cron/src/Hook/AutomatedCronHooks.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\automated_cron\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for automated_cron.
+ */
+class AutomatedCronHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.automated_cron':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Automated Cron module runs cron operations for your site using normal browser/page requests instead of having to set up a separate cron job. The Automated Cron module checks at the end of each server response when cron operation was last ran and, if it has been too long since last run, it executes the cron tasks after sending a server response. For more information, see the <a href=":automated_cron-documentation">online documentation for the Automated Cron module</a>.', [
+          ':automated_cron-documentation' => 'https://www.drupal.org/documentation/modules/automated_cron',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Configuring Automated Cron') . '</dt>';
+        $output .= '<dd>' . t('On the <a href=":cron-settings">Cron page</a>, you can set the frequency (time interval) for running cron jobs.', [
+          ':cron-settings' => Url::fromRoute('system.cron_settings')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Disabling Automated Cron') . '</dt>';
+        $output .= '<dd>' . t('To disable automated cron, the recommended method is to uninstall the module, to reduce site overhead. If you only want to disable it temporarily, you can set the frequency to Never on the Cron page, and then change the frequency back when you want to start it up again.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for the system_cron_settings() form.
+   */
+  #[Hook('form_system_cron_settings_alter')]
+  public function formSystemCronSettingsAlter(&$form, &$form_state) : void {
+    $automated_cron_settings = \Drupal::config('automated_cron.settings');
+    $options = [3600, 10800, 21600, 43200, 86400, 604800];
+    $form['cron']['interval'] = [
+      '#type' => 'select',
+      '#title' => t('Run cron every'),
+      '#description' => t('More information about setting up scheduled tasks can be found by <a href=":url">reading the cron tutorial on drupal.org</a>.', [
+        ':url' => 'https://www.drupal.org/docs/8/administering-a-drupal-8-site/cron-automated-tasks',
+      ]),
+      '#default_value' => $automated_cron_settings->get('interval'),
+      '#options' => [
+        0 => t('Never'),
+      ] + array_map([
+        \Drupal::service('date.formatter'),
+        'formatInterval',
+      ], array_combine($options, $options)),
+    ];
+    // Add submit callback.
+    $form['#submit'][] = 'automated_cron_settings_submit';
+    // Theme this form as a config form.
+    $form['#theme'] = 'system_config_form';
+  }
+
+}
diff --git a/core/modules/ban/ban.module b/core/modules/ban/ban.module
deleted file mode 100644
index a407fe7e2d23eb08e5a6825b574f5e3babbb3948..0000000000000000000000000000000000000000
--- a/core/modules/ban/ban.module
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-/**
- * @file
- * Allows to ban individual IP addresses.
- */
-
-use Drupal\Core\Url;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function ban_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.ban':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Ban module allows administrators to ban visits to their site from individual IP addresses. For more information, see the <a href=":url">online documentation for the Ban module</a>.', [':url' => 'https://www.drupal.org/documentation/modules/ban']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Banning IP addresses') . '</dt>';
-      $output .= '<dd>' . t('Administrators can enter IP addresses to ban on the <a href=":bans">IP address bans</a> page.', [':bans' => Url::fromRoute('ban.admin_page')->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'ban.admin_page':
-      return '<p>' . t('IP addresses listed here are banned from your site. Banned addresses are completely forbidden from accessing the site and instead see a brief message explaining the situation.') . '</p>';
-  }
-}
diff --git a/core/modules/ban/src/Hook/BanHooks.php b/core/modules/ban/src/Hook/BanHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c4966a1a41a5ab04ffa05c94bd4be772a498b77e
--- /dev/null
+++ b/core/modules/ban/src/Hook/BanHooks.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\ban\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for ban.
+ */
+class BanHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.ban':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Ban module allows administrators to ban visits to their site from individual IP addresses. For more information, see the <a href=":url">online documentation for the Ban module</a>.', [':url' => 'https://www.drupal.org/documentation/modules/ban']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Banning IP addresses') . '</dt>';
+        $output .= '<dd>' . t('Administrators can enter IP addresses to ban on the <a href=":bans">IP address bans</a> page.', [':bans' => Url::fromRoute('ban.admin_page')->toString()]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'ban.admin_page':
+        return '<p>' . t('IP addresses listed here are banned from your site. Banned addresses are completely forbidden from accessing the site and instead see a brief message explaining the situation.') . '</p>';
+    }
+  }
+
+}
diff --git a/core/modules/basic_auth/basic_auth.module b/core/modules/basic_auth/basic_auth.module
deleted file mode 100644
index 879e76355bb838895285bdf87a72958d2a7694b2..0000000000000000000000000000000000000000
--- a/core/modules/basic_auth/basic_auth.module
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides an HTTP Basic authentication provider.
- */
-
-use Drupal\Core\Url;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function basic_auth_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.basic_auth':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The HTTP Basic Authentication module supplies an <a href="http://en.wikipedia.org/wiki/Basic_access_authentication">HTTP Basic authentication</a> provider for web service requests. This authentication provider authenticates requests using the HTTP Basic Authentication username and password, as an alternative to using Drupal\'s standard cookie-based authentication system. It is only useful if your site provides web services configured to use this type of authentication (for instance, the <a href=":rest_help">RESTful Web Services module</a>). For more information, see the <a href=":hba_do">online documentation for the HTTP Basic Authentication module</a>.', [':hba_do' => 'https://www.drupal.org/documentation/modules/basic_auth', ':rest_help' => (\Drupal::moduleHandler()->moduleExists('rest')) ? Url::fromRoute('help.page', ['name' => 'rest'])->toString() : '#']) . '</p>';
-      return $output;
-  }
-}
diff --git a/core/modules/basic_auth/src/Hook/BasicAuthHooks.php b/core/modules/basic_auth/src/Hook/BasicAuthHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..2660b1ff87817836733f75c6f3d90003a031f336
--- /dev/null
+++ b/core/modules/basic_auth/src/Hook/BasicAuthHooks.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\basic_auth\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for basic_auth.
+ */
+class BasicAuthHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.basic_auth':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The HTTP Basic Authentication module supplies an <a href="http://en.wikipedia.org/wiki/Basic_access_authentication">HTTP Basic authentication</a> provider for web service requests. This authentication provider authenticates requests using the HTTP Basic Authentication username and password, as an alternative to using Drupal\'s standard cookie-based authentication system. It is only useful if your site provides web services configured to use this type of authentication (for instance, the <a href=":rest_help">RESTful Web Services module</a>). For more information, see the <a href=":hba_do">online documentation for the HTTP Basic Authentication module</a>.', [
+          ':hba_do' => 'https://www.drupal.org/documentation/modules/basic_auth',
+          ':rest_help' => \Drupal::moduleHandler()->moduleExists('rest') ? Url::fromRoute('help.page', [
+            'name' => 'rest',
+          ])->toString() : '#',
+        ]) . '</p>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/big_pipe/big_pipe.module b/core/modules/big_pipe/big_pipe.module
index aff90f945f840b26ce176c07001b5bae6dbb857b..2ac12cbd9a21b26aea0c464160ea69f3c2ce253d 100644
--- a/core/modules/big_pipe/big_pipe.module
+++ b/core/modules/big_pipe/big_pipe.module
@@ -2,96 +2,8 @@
 
 /**
  * @file
- * Adds BigPipe no-JS detection.
  */
 
-use Drupal\big_pipe\Render\Placeholder\BigPipeStrategy;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_help().
- */
-function big_pipe_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.big_pipe':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The BigPipe module sends pages with dynamic content in a way that allows browsers to show them much faster. For more information, see the <a href=":big_pipe-documentation">online documentation for the BigPipe module</a>.', [':big_pipe-documentation' => 'https://www.drupal.org/documentation/modules/big_pipe']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Speeding up your site') . '</dt>';
-      $output .= '<dd>' . t('The module requires no configuration. Every part of the page contains metadata that allows BigPipe to figure this out on its own.') . '</dd>';
-      $output .= '</dl>';
-
-      return $output;
-  }
-}
-
-/**
- * Implements hook_page_attachments().
- *
- * @see \Drupal\big_pipe\Controller\BigPipeController::setNoJsCookie()
- */
-function big_pipe_page_attachments(array &$page) {
-  // Routes that don't use BigPipe also don't need no-JS detection.
-  if (\Drupal::routeMatch()->getRouteObject()->getOption('_no_big_pipe')) {
-    return;
-  }
-
-  $request = \Drupal::request();
-  // BigPipe is only used when there is an actual session, so only add the no-JS
-  // detection when there actually is a session.
-  // @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy.
-  $session_exists = \Drupal::service('session_configuration')->hasSession($request);
-  $page['#cache']['contexts'][] = 'session.exists';
-  // Only do the no-JS detection while we don't know if there's no JS support:
-  // avoid endless redirect loops.
-  $has_big_pipe_nojs_cookie = $request->cookies->has(BigPipeStrategy::NOJS_COOKIE);
-  $page['#cache']['contexts'][] = 'cookies:' . BigPipeStrategy::NOJS_COOKIE;
-  if ($session_exists) {
-    if (!$has_big_pipe_nojs_cookie) {
-      // Let server set the BigPipe no-JS cookie.
-      $page['#attached']['html_head'][] = [
-        [
-          // Redirect through a 'Refresh' meta tag if JavaScript is disabled.
-          '#tag' => 'meta',
-          '#noscript' => TRUE,
-          '#attributes' => [
-            'http-equiv' => 'Refresh',
-            'content' => '0; URL=' . Url::fromRoute('big_pipe.nojs', [], ['query' => \Drupal::service('redirect.destination')->getAsArray()])->toString(),
-          ],
-        ],
-        'big_pipe_detect_nojs',
-      ];
-    }
-    else {
-      // Let client delete the BigPipe no-JS cookie.
-      $page['#attached']['html_head'][] = [
-        [
-          '#tag' => 'script',
-          '#value' => 'document.cookie = "' . BigPipeStrategy::NOJS_COOKIE . '=1; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"',
-        ],
-        'big_pipe_detect_js',
-      ];
-    }
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function big_pipe_theme(): array {
-  return [
-    'big_pipe_interface_preview' => [
-      'variables' => [
-        'callback' => NULL,
-        'arguments' => NULL,
-        'preview' => NULL,
-      ],
-    ],
-  ];
-}
-
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
diff --git a/core/modules/big_pipe/src/Hook/BigPipeHooks.php b/core/modules/big_pipe/src/Hook/BigPipeHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..370bfb899ccaca7f03bbe36afd28bf6eca98da99
--- /dev/null
+++ b/core/modules/big_pipe/src/Hook/BigPipeHooks.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Drupal\big_pipe\Hook;
+
+use Drupal\Core\Url;
+use Drupal\big_pipe\Render\Placeholder\BigPipeStrategy;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for big_pipe.
+ */
+class BigPipeHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.big_pipe':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The BigPipe module sends pages with dynamic content in a way that allows browsers to show them much faster. For more information, see the <a href=":big_pipe-documentation">online documentation for the BigPipe module</a>.', [
+          ':big_pipe-documentation' => 'https://www.drupal.org/documentation/modules/big_pipe',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Speeding up your site') . '</dt>';
+        $output .= '<dd>' . t('The module requires no configuration. Every part of the page contains metadata that allows BigPipe to figure this out on its own.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_page_attachments().
+   *
+   * @see \Drupal\big_pipe\Controller\BigPipeController::setNoJsCookie()
+   */
+  #[Hook('page_attachments')]
+  public function pageAttachments(array &$page) {
+    // Routes that don't use BigPipe also don't need no-JS detection.
+    if (\Drupal::routeMatch()->getRouteObject()->getOption('_no_big_pipe')) {
+      return;
+    }
+    $request = \Drupal::request();
+    // BigPipe is only used when there is an actual session, so only add the no-JS
+    // detection when there actually is a session.
+    // @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy.
+    $session_exists = \Drupal::service('session_configuration')->hasSession($request);
+    $page['#cache']['contexts'][] = 'session.exists';
+    // Only do the no-JS detection while we don't know if there's no JS support:
+    // avoid endless redirect loops.
+    $has_big_pipe_nojs_cookie = $request->cookies->has(BigPipeStrategy::NOJS_COOKIE);
+    $page['#cache']['contexts'][] = 'cookies:' . BigPipeStrategy::NOJS_COOKIE;
+    if ($session_exists) {
+      if (!$has_big_pipe_nojs_cookie) {
+        // Let server set the BigPipe no-JS cookie.
+        $page['#attached']['html_head'][] = [
+          [
+            // Redirect through a 'Refresh' meta tag if JavaScript is disabled.
+            '#tag' => 'meta',
+            '#noscript' => TRUE,
+            '#attributes' => [
+              'http-equiv' => 'Refresh',
+              'content' => '0; URL=' . Url::fromRoute('big_pipe.nojs', [], [
+                'query' => \Drupal::service('redirect.destination')->getAsArray(),
+              ])->toString(),
+            ],
+          ],
+          'big_pipe_detect_nojs',
+        ];
+      }
+      else {
+        // Let client delete the BigPipe no-JS cookie.
+        $page['#attached']['html_head'][] = [
+          [
+            '#tag' => 'script',
+            '#value' => 'document.cookie = "' . BigPipeStrategy::NOJS_COOKIE . '=1; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"',
+          ],
+          'big_pipe_detect_js',
+        ];
+      }
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'big_pipe_interface_preview' => [
+        'variables' => [
+          'callback' => NULL,
+          'arguments' => NULL,
+          'preview' => NULL,
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_bypass_js/big_pipe_bypass_js.module b/core/modules/big_pipe/tests/modules/big_pipe_bypass_js/big_pipe_bypass_js.module
deleted file mode 100644
index 8dd1204079de194afc9e97b557ac385fcdb64ad3..0000000000000000000000000000000000000000
--- a/core/modules/big_pipe/tests/modules/big_pipe_bypass_js/big_pipe_bypass_js.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides a way to bypass Big Pipe JavaScript.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_library_info_alter().
- *
- * Disables Big Pipe JavaScript by removing the js file from the library.
- */
-function big_pipe_bypass_js_library_info_alter(&$libraries, $extension) {
-  if ($extension === 'big_pipe') {
-    unset($libraries['big_pipe']['js']);
-  }
-}
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_bypass_js/src/Hook/BigPipeBypassJsHooks.php b/core/modules/big_pipe/tests/modules/big_pipe_bypass_js/src/Hook/BigPipeBypassJsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..a143ba7da717fdd4fc1272ccdaed29a63fa12286
--- /dev/null
+++ b/core/modules/big_pipe/tests/modules/big_pipe_bypass_js/src/Hook/BigPipeBypassJsHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\big_pipe_bypass_js\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for big_pipe_bypass_js.
+ */
+class BigPipeBypassJsHooks {
+
+  /**
+   * Implements hook_library_info_alter().
+   *
+   * Disables Big Pipe JavaScript by removing the js file from the library.
+   */
+  #[Hook('library_info_alter')]
+  public function libraryInfoAlter(&$libraries, $extension) {
+    if ($extension === 'big_pipe') {
+      unset($libraries['big_pipe']['js']);
+    }
+  }
+
+}
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_regression_test/big_pipe_regression_test.module b/core/modules/big_pipe/tests/modules/big_pipe_regression_test/big_pipe_regression_test.module
deleted file mode 100644
index aed08ec5f007a787f6c7d57455a749ece457af20..0000000000000000000000000000000000000000
--- a/core/modules/big_pipe/tests/modules/big_pipe_regression_test/big_pipe_regression_test.module
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-/**
- * @file
- * Support module for BigPipe testing.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_theme().
- *
- * @see \Drupal\Tests\big_pipe\FunctionalJavascript\BigPipeRegressionTest::testBigPipeLargeContent
- */
-function big_pipe_regression_test_theme(): array {
-  return [
-    'big_pipe_test_large_content' => [
-      'variables' => [],
-    ],
-  ];
-}
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_regression_test/src/Hook/BigPipeRegressionTestHooks.php b/core/modules/big_pipe/tests/modules/big_pipe_regression_test/src/Hook/BigPipeRegressionTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..d8f4ecd319c79431a7167a2fd5c4b68a38127798
--- /dev/null
+++ b/core/modules/big_pipe/tests/modules/big_pipe_regression_test/src/Hook/BigPipeRegressionTestHooks.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\big_pipe_regression_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for big_pipe_regression_test.
+ */
+class BigPipeRegressionTestHooks {
+
+  /**
+   * Implements hook_theme().
+   *
+   * @see \Drupal\Tests\big_pipe\FunctionalJavascript\BigPipeRegressionTest::testBigPipeLargeContent
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return ['big_pipe_test_large_content' => ['variables' => []]];
+  }
+
+}
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.module b/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.module
deleted file mode 100644
index 73d5a83f94cc24318df9760ab4530b54b75b459d..0000000000000000000000000000000000000000
--- a/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.module
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-/**
- * @file
- * Support module for BigPipe testing.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_page_top().
- */
-function big_pipe_test_page_top(array &$page_top) {
-  // Ensure this hook is invoked on every page load.
-  $page_top['#cache']['max-age'] = 0;
-
-  $request = \Drupal::request();
-  if ($request->query->get('trigger_session')) {
-    $request->getSession()->set('big_pipe_test', TRUE);
-  }
-}
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/src/Hook/BigPipeTestHooks.php b/core/modules/big_pipe/tests/modules/big_pipe_test/src/Hook/BigPipeTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..691ef5837c32b0d39f634c6a46e7c64216d542c8
--- /dev/null
+++ b/core/modules/big_pipe/tests/modules/big_pipe_test/src/Hook/BigPipeTestHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\big_pipe_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for big_pipe_test.
+ */
+class BigPipeTestHooks {
+
+  /**
+   * Implements hook_page_top().
+   */
+  #[Hook('page_top')]
+  public function pageTop(array &$page_top) {
+    // Ensure this hook is invoked on every page load.
+    $page_top['#cache']['max-age'] = 0;
+    $request = \Drupal::request();
+    if ($request->query->get('trigger_session')) {
+      $request->getSession()->set('big_pipe_test', TRUE);
+    }
+  }
+
+}
diff --git a/core/modules/block/block.api.php b/core/modules/block/block.api.php
index 3146346e78711404b0f57ce575adbc90d73d4225..2cfcfd44463928b83e56728c771d5ec1d1019103 100644
--- a/core/modules/block/block.api.php
+++ b/core/modules/block/block.api.php
@@ -5,6 +5,9 @@
  * Hooks provided by the Block module.
  */
 
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\block\Entity\Block;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Access\AccessResult;
 
 /**
@@ -116,7 +119,7 @@
  *
  * @ingroup block_api
  */
-function hook_block_view_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
+function hook_block_view_alter(array &$build, BlockPluginInterface $block) {
   // Remove the contextual links on all blocks that provide them.
   if (isset($build['#contextual_links'])) {
     unset($build['#contextual_links']);
@@ -146,7 +149,7 @@ function hook_block_view_alter(array &$build, \Drupal\Core\Block\BlockPluginInte
  *
  * @ingroup block_api
  */
-function hook_block_view_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
+function hook_block_view_BASE_BLOCK_ID_alter(array &$build, BlockPluginInterface $block) {
   // Change the title of the specific block.
   $build['#title'] = t('New title of the block');
 }
@@ -173,7 +176,7 @@ function hook_block_view_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\B
  *
  * @ingroup block_api
  */
-function hook_block_build_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
+function hook_block_build_alter(array &$build, BlockPluginInterface $block) {
   // Add the 'user' cache context to some blocks.
   if ($block->label() === 'some condition') {
     $build['#cache']['contexts'][] = 'user';
@@ -201,7 +204,7 @@ function hook_block_build_alter(array &$build, \Drupal\Core\Block\BlockPluginInt
  *
  * @ingroup block_api
  */
-function hook_block_build_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
+function hook_block_build_BASE_BLOCK_ID_alter(array &$build, BlockPluginInterface $block) {
   // Explicitly enable placeholdering of the specific block.
   $build['#create_placeholder'] = TRUE;
 }
@@ -230,7 +233,7 @@ function hook_block_build_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\
  * @see \Drupal\block\BlockAccessControlHandler::checkAccess()
  * @ingroup block_api
  */
-function hook_block_access(\Drupal\block\Entity\Block $block, $operation, \Drupal\Core\Session\AccountInterface $account) {
+function hook_block_access(Block $block, $operation, AccountInterface $account) {
   // Example code that would prevent displaying the 'Powered by Drupal' block in
   // a region different than the footer.
   if ($operation == 'view' && $block->getPluginId() == 'system_powered_by_block') {
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index eac594c01653b27c7aa774d6d6a02185fe526666..de17aed9fb998eb56bb6d0c4ab36adbf51ed92e3 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -2,86 +2,10 @@
 
 /**
  * @file
- * Controls the visual building blocks a page is constructed with.
  */
 
 use Drupal\Component\Utility\Html;
 use Drupal\Core\Installer\InstallerKernel;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
-use Drupal\language\ConfigurableLanguageInterface;
-use Drupal\system\Entity\Menu;
-use Drupal\Core\Block\BlockPluginInterface;
-use Drupal\block\Entity\Block;
-
-/**
- * Implements hook_help().
- */
-function block_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.block':
-      $block_content = \Drupal::moduleHandler()->moduleExists('block_content') ? Url::fromRoute('help.page', ['name' => 'block_content'])->toString() : '#';
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Block module allows you to place blocks in regions of your installed themes, and configure block settings. For more information, see the <a href=":blocks-documentation">online documentation for the Block module</a>.', [':blocks-documentation' => 'https://www.drupal.org/documentation/modules/block/']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Placing and moving blocks') . '</dt>';
-      $output .= '<dd>' . t('You can place a new block in a region by selecting <em>Place block</em> on the <a href=":blocks">Block layout page</a>. Once a block is placed, it can be moved to a different region by drag-and-drop or by using the <em>Region</em> drop-down list, and then clicking <em>Save blocks</em>.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Toggling between different themes') . '</dt>';
-      $output .= '<dd>' . t('Blocks are placed and configured specifically for each theme. The Block layout page opens with the default theme, but you can toggle to other installed themes.') . '</dd>';
-      $output .= '<dt>' . t('Demonstrating block regions for a theme') . '</dt>';
-      $output .= '<dd>' . t('You can see where the regions are for the current theme by clicking the <em>Demonstrate block regions</em> link on the <a href=":blocks">Block layout page</a>. Regions are specific to each theme.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Configuring block settings') . '</dt>';
-      $output .= '<dd>' . t('To change the settings of an individual block click on the <em>Configure</em> link on the <a href=":blocks">Block layout page</a>. The available options vary depending on the module that provides the block. For all blocks you can change the block title and toggle whether to display it.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Controlling visibility') . '</dt>';
-      $output .= '<dd>' . t('You can control the visibility of a block by restricting it to specific pages, content types, and/or roles by setting the appropriate options under <em>Visibility settings</em> of the block configuration.') . '</dd>';
-      $output .= '<dt>' . t('Adding content blocks') . '</dt>';
-      $output .= '<dd>' . t('You can add content blocks, if the <em>Block Content</em> module is installed. For more information, see the <a href=":blockcontent-help">Block Content help page</a>.', [':blockcontent-help' => $block_content]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-  if ($route_name == 'block.admin_display' || $route_name == 'block.admin_display_theme') {
-    $demo_theme = $route_match->getParameter('theme') ?: \Drupal::config('system.theme')->get('default');
-    $themes = \Drupal::service('theme_handler')->listInfo();
-    $output = '<p>' . t('Block placement is specific to each theme on your site. Changes will not be saved until you click <em>Save blocks</em> at the bottom of the page.') . '</p>';
-    $output .= '<p>' . Link::fromTextAndUrl(t('Demonstrate block regions (@theme)', ['@theme' => $themes[$demo_theme]->info['name']]), Url::fromRoute('block.admin_demo', ['theme' => $demo_theme]))->toString() . '</p>';
-    return $output;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function block_theme(): array {
-  return [
-    'block' => [
-      'render element' => 'elements',
-    ],
-  ];
-}
-
-/**
- * Implements hook_page_top().
- */
-function block_page_top(array &$page_top) {
-  if (\Drupal::routeMatch()->getRouteName() === 'block.admin_demo') {
-    $theme = \Drupal::theme()->getActiveTheme()->getName();
-    $page_top['backlink'] = [
-      '#type' => 'link',
-      '#title' => t('Exit block region demonstration'),
-      '#options' => ['attributes' => ['class' => ['block-demo-backlink']]],
-      '#weight' => -10,
-    ];
-    if (\Drupal::config('system.theme')->get('default') == $theme) {
-      $page_top['backlink']['#url'] = Url::fromRoute('block.admin_display');
-    }
-    else {
-      $page_top['backlink']['#url'] = Url::fromRoute('block.admin_display_theme', ['theme' => $theme]);
-    }
-  }
-}
 
 /**
  * Initializes blocks for installed themes.
@@ -146,53 +70,6 @@ function block_theme_initialize($theme) {
   }
 }
 
-/**
- * Implements hook_modules_installed().
- *
- * @see block_themes_installed()
- */
-function block_modules_installed($modules) {
-  // block_themes_installed() does not call block_theme_initialize() during site
-  // installation because block configuration can be optional or provided by the
-  // profile. Now, when the profile is installed, this configuration exists,
-  // call block_theme_initialize() for all installed themes.
-  $profile = \Drupal::installProfile();
-  if (in_array($profile, $modules, TRUE)) {
-    foreach (\Drupal::service('theme_handler')->listInfo() as $theme => $data) {
-      block_theme_initialize($theme);
-    }
-  }
-}
-
-/**
- * Implements hook_rebuild().
- */
-function block_rebuild() {
-  foreach (\Drupal::service('theme_handler')->listInfo() as $theme => $data) {
-    if ($data->status) {
-      $regions = system_region_list($theme);
-      /** @var \Drupal\block\BlockInterface[] $blocks */
-      $blocks = \Drupal::entityTypeManager()->getStorage('block')->loadByProperties(['theme' => $theme]);
-      foreach ($blocks as $block_id => $block) {
-        // Disable blocks in invalid regions.
-        if (!isset($regions[$block->getRegion()])) {
-          if ($block->status()) {
-            \Drupal::messenger()
-              ->addWarning(t('The block %info was assigned to the invalid region %region and has been disabled.', [
-                '%info' => $block_id,
-                '%region' => $block->getRegion(),
-              ]));
-          }
-          $block
-            ->setRegion(system_default_region($theme))
-            ->disable()
-            ->save();
-        }
-      }
-    }
-  }
-}
-
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
@@ -269,70 +146,3 @@ function template_preprocess_block(&$variables) {
   }
 
 }
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for user_role entities.
- *
- * Removes deleted role from blocks that use it.
- */
-function block_user_role_delete($role) {
-  foreach (Block::loadMultiple() as $block) {
-    /** @var \Drupal\block\BlockInterface $block */
-    $visibility = $block->getVisibility();
-    if (isset($visibility['user_role']['roles'][$role->id()])) {
-      unset($visibility['user_role']['roles'][$role->id()]);
-      $block->setVisibilityConfig('user_role', $visibility['user_role']);
-      $block->save();
-    }
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for menu entities.
- */
-function block_menu_delete(Menu $menu) {
-  if (!$menu->isSyncing()) {
-    foreach (Block::loadMultiple() as $block) {
-      if ($block->getPluginId() == 'system_menu_block:' . $menu->id()) {
-        $block->delete();
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for 'configurable_language'.
- *
- * Delete the potential block visibility settings of the deleted language.
- */
-function block_configurable_language_delete(ConfigurableLanguageInterface $language) {
-  // Remove the block visibility settings for the deleted language.
-  foreach (Block::loadMultiple() as $block) {
-    /** @var \Drupal\block\BlockInterface $block */
-    $visibility = $block->getVisibility();
-    if (isset($visibility['language']['langcodes'][$language->id()])) {
-      unset($visibility['language']['langcodes'][$language->id()]);
-      $block->setVisibilityConfig('language', $visibility['language']);
-      $block->save();
-    }
-  }
-}
-
-/**
- * Implements hook_block_build_BASE_BLOCK_ID_alter().
- */
-function block_block_build_local_actions_block_alter(array &$build, BlockPluginInterface $block) {
-  $build['#lazy_builder_preview'] = [
-    '#type' => 'container',
-    '#attributes' => [
-      'class' => ['invisible'],
-    ],
-    'actions' => [
-      '#theme' => 'menu_local_action',
-      '#link' => [
-        'title' => t('Add'),
-        'url' => Url::fromUserInput('#'),
-      ],
-    ],
-  ];
-}
diff --git a/core/modules/block/src/Hook/BlockHooks.php b/core/modules/block/src/Hook/BlockHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..1c2147fce54a700394b258287564b7742976b787
--- /dev/null
+++ b/core/modules/block/src/Hook/BlockHooks.php
@@ -0,0 +1,207 @@
+<?php
+
+namespace Drupal\block\Hook;
+
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\language\ConfigurableLanguageInterface;
+use Drupal\system\Entity\Menu;
+use Drupal\block\Entity\Block;
+use Drupal\Core\Link;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for block.
+ */
+class BlockHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.block':
+        $block_content = \Drupal::moduleHandler()->moduleExists('block_content') ? Url::fromRoute('help.page', ['name' => 'block_content'])->toString() : '#';
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Block module allows you to place blocks in regions of your installed themes, and configure block settings. For more information, see the <a href=":blocks-documentation">online documentation for the Block module</a>.', [':blocks-documentation' => 'https://www.drupal.org/documentation/modules/block/']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Placing and moving blocks') . '</dt>';
+        $output .= '<dd>' . t('You can place a new block in a region by selecting <em>Place block</em> on the <a href=":blocks">Block layout page</a>. Once a block is placed, it can be moved to a different region by drag-and-drop or by using the <em>Region</em> drop-down list, and then clicking <em>Save blocks</em>.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Toggling between different themes') . '</dt>';
+        $output .= '<dd>' . t('Blocks are placed and configured specifically for each theme. The Block layout page opens with the default theme, but you can toggle to other installed themes.') . '</dd>';
+        $output .= '<dt>' . t('Demonstrating block regions for a theme') . '</dt>';
+        $output .= '<dd>' . t('You can see where the regions are for the current theme by clicking the <em>Demonstrate block regions</em> link on the <a href=":blocks">Block layout page</a>. Regions are specific to each theme.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Configuring block settings') . '</dt>';
+        $output .= '<dd>' . t('To change the settings of an individual block click on the <em>Configure</em> link on the <a href=":blocks">Block layout page</a>. The available options vary depending on the module that provides the block. For all blocks you can change the block title and toggle whether to display it.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Controlling visibility') . '</dt>';
+        $output .= '<dd>' . t('You can control the visibility of a block by restricting it to specific pages, content types, and/or roles by setting the appropriate options under <em>Visibility settings</em> of the block configuration.') . '</dd>';
+        $output .= '<dt>' . t('Adding content blocks') . '</dt>';
+        $output .= '<dd>' . t('You can add content blocks, if the <em>Block Content</em> module is installed. For more information, see the <a href=":blockcontent-help">Block Content help page</a>.', [':blockcontent-help' => $block_content]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+    if ($route_name == 'block.admin_display' || $route_name == 'block.admin_display_theme') {
+      $demo_theme = $route_match->getParameter('theme') ?: \Drupal::config('system.theme')->get('default');
+      $themes = \Drupal::service('theme_handler')->listInfo();
+      $output = '<p>' . t('Block placement is specific to each theme on your site. Changes will not be saved until you click <em>Save blocks</em> at the bottom of the page.') . '</p>';
+      $output .= '<p>' . Link::fromTextAndUrl(t('Demonstrate block regions (@theme)', ['@theme' => $themes[$demo_theme]->info['name']]), Url::fromRoute('block.admin_demo', ['theme' => $demo_theme]))->toString() . '</p>';
+      return $output;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return ['block' => ['render element' => 'elements']];
+  }
+
+  /**
+   * Implements hook_page_top().
+   */
+  #[Hook('page_top')]
+  public function pageTop(array &$page_top) {
+    if (\Drupal::routeMatch()->getRouteName() === 'block.admin_demo') {
+      $theme = \Drupal::theme()->getActiveTheme()->getName();
+      $page_top['backlink'] = [
+        '#type' => 'link',
+        '#title' => t('Exit block region demonstration'),
+        '#options' => [
+          'attributes' => [
+            'class' => [
+              'block-demo-backlink',
+            ],
+          ],
+        ],
+        '#weight' => -10,
+      ];
+      if (\Drupal::config('system.theme')->get('default') == $theme) {
+        $page_top['backlink']['#url'] = Url::fromRoute('block.admin_display');
+      }
+      else {
+        $page_top['backlink']['#url'] = Url::fromRoute('block.admin_display_theme', ['theme' => $theme]);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_modules_installed().
+   *
+   * @see block_themes_installed()
+   */
+  #[Hook('modules_installed')]
+  public function modulesInstalled($modules) {
+    // block_themes_installed() does not call block_theme_initialize() during site
+    // installation because block configuration can be optional or provided by the
+    // profile. Now, when the profile is installed, this configuration exists,
+    // call block_theme_initialize() for all installed themes.
+    $profile = \Drupal::installProfile();
+    if (in_array($profile, $modules, TRUE)) {
+      foreach (\Drupal::service('theme_handler')->listInfo() as $theme => $data) {
+        block_theme_initialize($theme);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_rebuild().
+   */
+  #[Hook('rebuild')]
+  public function rebuild() {
+    foreach (\Drupal::service('theme_handler')->listInfo() as $theme => $data) {
+      if ($data->status) {
+        $regions = system_region_list($theme);
+        /** @var \Drupal\block\BlockInterface[] $blocks */
+        $blocks = \Drupal::entityTypeManager()->getStorage('block')->loadByProperties(['theme' => $theme]);
+        foreach ($blocks as $block_id => $block) {
+          // Disable blocks in invalid regions.
+          if (!isset($regions[$block->getRegion()])) {
+            if ($block->status()) {
+              \Drupal::messenger()->addWarning(t('The block %info was assigned to the invalid region %region and has been disabled.', ['%info' => $block_id, '%region' => $block->getRegion()]));
+            }
+            $block->setRegion(system_default_region($theme))->disable()->save();
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for user_role entities.
+   *
+   * Removes deleted role from blocks that use it.
+   */
+  #[Hook('user_role_delete')]
+  public function userRoleDelete($role) {
+    foreach (Block::loadMultiple() as $block) {
+      /** @var \Drupal\block\BlockInterface $block */
+      $visibility = $block->getVisibility();
+      if (isset($visibility['user_role']['roles'][$role->id()])) {
+        unset($visibility['user_role']['roles'][$role->id()]);
+        $block->setVisibilityConfig('user_role', $visibility['user_role']);
+        $block->save();
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for menu entities.
+   */
+  #[Hook('menu_delete')]
+  public function menuDelete(Menu $menu) {
+    if (!$menu->isSyncing()) {
+      foreach (Block::loadMultiple() as $block) {
+        if ($block->getPluginId() == 'system_menu_block:' . $menu->id()) {
+          $block->delete();
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for 'configurable_language'.
+   *
+   * Delete the potential block visibility settings of the deleted language.
+   */
+  #[Hook('configurable_language_delete')]
+  public function configurableLanguageDelete(ConfigurableLanguageInterface $language) {
+    // Remove the block visibility settings for the deleted language.
+    foreach (Block::loadMultiple() as $block) {
+      /** @var \Drupal\block\BlockInterface $block */
+      $visibility = $block->getVisibility();
+      if (isset($visibility['language']['langcodes'][$language->id()])) {
+        unset($visibility['language']['langcodes'][$language->id()]);
+        $block->setVisibilityConfig('language', $visibility['language']);
+        $block->save();
+      }
+    }
+  }
+
+  /**
+   * Implements hook_block_build_BASE_BLOCK_ID_alter().
+   */
+  #[Hook('block_build_local_actions_block_alter')]
+  public function blockBuildLocalActionsBlockAlter(array &$build, BlockPluginInterface $block) {
+    $build['#lazy_builder_preview'] = [
+      '#type' => 'container',
+      '#attributes' => [
+        'class' => [
+          'invisible',
+        ],
+      ],
+      'actions' => [
+        '#theme' => 'menu_local_action',
+        '#link' => [
+          'title' => t('Add'),
+          'url' => Url::fromUserInput('#'),
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/block/tests/modules/block_test/block_test.module b/core/modules/block/tests/modules/block_test/block_test.module
deleted file mode 100644
index 94e9ca745f657fa827f42c1135389c22a043db5f..0000000000000000000000000000000000000000
--- a/core/modules/block/tests/modules/block_test/block_test.module
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide test blocks.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Block\BlockPluginInterface;
-use Drupal\Core\Cache\Cache;
-
-/**
- * Implements hook_block_alter().
- */
-function block_test_block_alter(&$block_info) {
-  if (\Drupal::state()->get('block_test_info_alter') && isset($block_info['test_block_instantiation'])) {
-    $block_info['test_block_instantiation']['category'] = t('Custom category');
-  }
-}
-
-/**
- * Implements hook_block_view_BASE_BLOCK_ID_alter().
- */
-function block_test_block_view_test_cache_alter(array &$build, BlockPluginInterface $block) {
-  if (\Drupal::state()->get('block_test_view_alter_suffix') !== NULL) {
-    $build['#attributes']['foo'] = 'bar';
-  }
-  if (\Drupal::state()->get('block_test_view_alter_append_pre_render_prefix') !== NULL) {
-    $build['#pre_render'][] = '\Drupal\block_test\BlockRenderAlterContent::preRender';
-  }
-}
-
-/**
- * Implements hook_block_build_BASE_BLOCK_ID_alter().
- */
-function block_test_block_build_test_cache_alter(array &$build, BlockPluginInterface $block) {
-  // Test altering cache keys, contexts, tags and max-age.
-  if (\Drupal::state()->get('block_test_block_alter_cache_key') !== NULL) {
-    $build['#cache']['keys'][] = \Drupal::state()->get('block_test_block_alter_cache_key');
-  }
-  if (\Drupal::state()->get('block_test_block_alter_cache_context') !== NULL) {
-    $build['#cache']['contexts'][] = \Drupal::state()->get('block_test_block_alter_cache_context');
-  }
-  if (\Drupal::state()->get('block_test_block_alter_cache_tag') !== NULL) {
-    $build['#cache']['tags'] = Cache::mergeTags($build['#cache']['tags'], [\Drupal::state()->get('block_test_block_alter_cache_tag')]);
-  }
-  if (\Drupal::state()->get('block_test_block_alter_cache_max_age') !== NULL) {
-    $build['#cache']['max-age'] = \Drupal::state()->get('block_test_block_alter_cache_max_age');
-  }
-
-  // Test setting #create_placeholder.
-  if (\Drupal::state()->get('block_test_block_alter_create_placeholder') !== NULL) {
-    $build['#create_placeholder'] = \Drupal::state()->get('block_test_block_alter_create_placeholder');
-  }
-}
diff --git a/core/modules/block/tests/modules/block_test/src/Hook/BlockTestHooks.php b/core/modules/block/tests/modules/block_test/src/Hook/BlockTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..55b167d4946382c655c554e741ca9ed59d12fb48
--- /dev/null
+++ b/core/modules/block/tests/modules/block_test/src/Hook/BlockTestHooks.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\block_test\Hook;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for block_test.
+ */
+class BlockTestHooks {
+
+  /**
+   * Implements hook_block_alter().
+   */
+  #[Hook('block_alter')]
+  public function blockAlter(&$block_info) {
+    if (\Drupal::state()->get('block_test_info_alter') && isset($block_info['test_block_instantiation'])) {
+      $block_info['test_block_instantiation']['category'] = t('Custom category');
+    }
+  }
+
+  /**
+   * Implements hook_block_view_BASE_BLOCK_ID_alter().
+   */
+  #[Hook('block_view_test_cache_alter')]
+  public function blockViewTestCacheAlter(array &$build, BlockPluginInterface $block) {
+    if (\Drupal::state()->get('block_test_view_alter_suffix') !== NULL) {
+      $build['#attributes']['foo'] = 'bar';
+    }
+    if (\Drupal::state()->get('block_test_view_alter_append_pre_render_prefix') !== NULL) {
+      $build['#pre_render'][] = '\Drupal\block_test\BlockRenderAlterContent::preRender';
+    }
+  }
+
+  /**
+   * Implements hook_block_build_BASE_BLOCK_ID_alter().
+   */
+  #[Hook('block_build_test_cache_alter')]
+  public function blockBuildTestCacheAlter(array &$build, BlockPluginInterface $block) {
+    // Test altering cache keys, contexts, tags and max-age.
+    if (\Drupal::state()->get('block_test_block_alter_cache_key') !== NULL) {
+      $build['#cache']['keys'][] = \Drupal::state()->get('block_test_block_alter_cache_key');
+    }
+    if (\Drupal::state()->get('block_test_block_alter_cache_context') !== NULL) {
+      $build['#cache']['contexts'][] = \Drupal::state()->get('block_test_block_alter_cache_context');
+    }
+    if (\Drupal::state()->get('block_test_block_alter_cache_tag') !== NULL) {
+      $build['#cache']['tags'] = Cache::mergeTags($build['#cache']['tags'], [\Drupal::state()->get('block_test_block_alter_cache_tag')]);
+    }
+    if (\Drupal::state()->get('block_test_block_alter_cache_max_age') !== NULL) {
+      $build['#cache']['max-age'] = \Drupal::state()->get('block_test_block_alter_cache_max_age');
+    }
+    // Test setting #create_placeholder.
+    if (\Drupal::state()->get('block_test_block_alter_create_placeholder') !== NULL) {
+      $build['#create_placeholder'] = \Drupal::state()->get('block_test_block_alter_create_placeholder');
+    }
+  }
+
+}
diff --git a/core/modules/block/tests/src/Kernel/BlockRebuildTest.php b/core/modules/block/tests/src/Kernel/BlockRebuildTest.php
index dba36b79252802919d878d603e4a4e6de931e650..70daee8158acfafabf1beb2fc2e20c7e7ed34e29 100644
--- a/core/modules/block/tests/src/Kernel/BlockRebuildTest.php
+++ b/core/modules/block/tests/src/Kernel/BlockRebuildTest.php
@@ -8,6 +8,7 @@
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\Tests\block\Traits\BlockCreationTrait;
+use Drupal\block\Hook\BlockHooks;
 
 /**
  * Tests block_rebuild().
@@ -44,22 +45,12 @@ protected function setUp(): void {
     $this->container->get('config.factory')->getEditable('system.theme')->set('default', 'stark')->save();
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function setUpBeforeClass(): void {
-    parent::setUpBeforeClass();
-
-    // @todo Once block_rebuild() is refactored to auto-loadable code, remove
-    //   this require statement.
-    require_once static::getDrupalRoot() . '/core/modules/block/block.module';
-  }
-
   /**
    * @covers ::block_rebuild
    */
   public function testRebuildNoBlocks(): void {
-    block_rebuild();
+    $blockRebuild = new BlockHooks();
+    $blockRebuild->rebuild();
     $messages = \Drupal::messenger()->all();
     \Drupal::messenger()->deleteAll();
     $this->assertEquals([], $messages);
@@ -71,7 +62,8 @@ public function testRebuildNoBlocks(): void {
   public function testRebuildNoInvalidBlocks(): void {
     $this->placeBlock('system_powered_by_block', ['region' => 'content']);
 
-    block_rebuild();
+    $blockRebuild = new BlockHooks();
+    $blockRebuild->rebuild();
     $messages = \Drupal::messenger()->all();
     \Drupal::messenger()->deleteAll();
     $this->assertEquals([], $messages);
@@ -102,7 +94,8 @@ public function testRebuildInvalidBlocks(): void {
     $this->assertSame('INVALID', $block2->getRegion());
     $this->assertFalse($block2->status());
 
-    block_rebuild();
+    $blockRebuild = new BlockHooks();
+    $blockRebuild->rebuild();
 
     // Reload block entities.
     $block1 = Block::load($block1->id());
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php
index 8fd9422428dad006e2ceae662a09f67c4b630909..9304a4db20b110c2032b92a2fcfcc1368c6ca56c 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockContentTranslationTest.php
@@ -5,6 +5,7 @@
 namespace Drupal\Tests\block\Kernel\Migrate\d6;
 
 use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
+use Drupal\block\Hook\BlockHooks;
 
 /**
  * Tests migration of i18n block translations.
@@ -49,7 +50,8 @@ protected function setUp(): void {
       'd6_block',
       'd6_block_translation',
     ]);
-    block_rebuild();
+    $blockRebuild = new BlockHooks();
+    $blockRebuild->rebuild();
   }
 
   /**
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php
index c06f5226d70091c9727e77457db553b35b22a8fe..eb3ad7721e9be98e67f839dba12390e22859dbc7 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d6/MigrateBlockTest.php
@@ -6,6 +6,7 @@
 
 use Drupal\block\Entity\Block;
 use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
+use Drupal\block\Hook\BlockHooks;
 
 /**
  * Tests migration of blocks to configuration entities.
@@ -54,7 +55,8 @@ protected function setUp(): void {
       'd6_user_role',
       'd6_block',
     ]);
-    block_rebuild();
+    $blockRebuild = new BlockHooks();
+    $blockRebuild->rebuild();
   }
 
   /**
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
index d024787870f7e17833d85cbc0185bf3b7e823748..ab2072c508b4751709dd55b286051a40b264a749 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockContentTranslationTest.php
@@ -5,6 +5,7 @@
 namespace Drupal\Tests\block\Kernel\Migrate\d7;
 
 use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
+use Drupal\block\Hook\BlockHooks;
 
 /**
  * Tests migration of i18n block translations.
@@ -52,7 +53,8 @@ protected function setUp(): void {
       'd7_block',
       'd7_block_translation',
     ]);
-    block_rebuild();
+    $blockRebuild = new BlockHooks();
+    $blockRebuild->rebuild();
   }
 
   /**
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockNoBlockContentTest.php b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockNoBlockContentTest.php
index 6bd100d7d1ea2fc027e6771fcb687eee0c0a56da..54fa9605b7a53a239908c695895b4bc0f3e5beef 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockNoBlockContentTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockNoBlockContentTest.php
@@ -6,6 +6,7 @@
 
 use Drupal\block\Entity\Block;
 use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
+use Drupal\block\Hook\BlockHooks;
 
 /**
  * Tests the migration of blocks without Block Content installed.
@@ -52,7 +53,8 @@ protected function setUp(): void {
       'd7_user_role',
       'd7_block',
     ]);
-    block_rebuild();
+    $blockRebuild = new BlockHooks();
+    $blockRebuild->rebuild();
   }
 
   /**
diff --git a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockTest.php b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockTest.php
index 2ef460b60f55cc61b973176e69015824cff97923..41407b41ff6a64c08bc2b1109b2175ac59676f6b 100644
--- a/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockTest.php
+++ b/core/modules/block/tests/src/Kernel/Migrate/d7/MigrateBlockTest.php
@@ -7,6 +7,7 @@
 use Drupal\block\Entity\Block;
 use Drupal\block_content\Entity\BlockContent;
 use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
+use Drupal\block\Hook\BlockHooks;
 
 /**
  * Tests migration of blocks to configuration entities.
@@ -58,7 +59,8 @@ protected function setUp(): void {
       'd7_custom_block',
       'd7_block',
     ]);
-    block_rebuild();
+    $blockRebuild = new BlockHooks();
+    $blockRebuild->rebuild();
   }
 
   /**
diff --git a/core/modules/block_content/block_content.module b/core/modules/block_content/block_content.module
index 3b38634254dedcdf65c93dd33b32fc0fd2d58816..7b85ae6de45bab4d8de771fe0ccf4ea60f299502 100644
--- a/core/modules/block_content/block_content.module
+++ b/core/modules/block_content/block_content.module
@@ -2,73 +2,12 @@
 
 /**
  * @file
- * Allows the creation of content blocks through the user interface.
  */
 
-use Drupal\block\BlockInterface;
-use Drupal\block_content\BlockContentInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Url;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
-use Drupal\Core\Database\Query\SelectInterface;
-use Drupal\Core\Database\Query\AlterableInterface;
 use Drupal\Core\Database\Query\ConditionInterface;
 
-/**
- * Implements hook_help().
- */
-function block_content_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.block_content':
-      $field_ui = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Block Content module allows you to create and manage custom <em>block types</em> and <em>content-containing blocks</em>. For more information, see the <a href=":online-help">online documentation for the Block Content module</a>.', [':online-help' => 'https://www.drupal.org/documentation/modules/block_content']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Creating and managing block types') . '</dt>';
-      $output .= '<dd>' . t('Users with the <em>Administer blocks</em> permission can create and edit block types with fields and display settings, from the <a href=":types">Block types</a> page under the Structure menu. For more information about managing fields and display settings, see the <a href=":field-ui">Field UI module help</a> and <a href=":field">Field module help</a>.', [':types' => Url::fromRoute('entity.block_content_type.collection')->toString(), ':field-ui' => $field_ui, ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Creating content blocks') . '</dt>';
-      $output .= '<dd>' . t('Users with the <em>Administer blocks</em> permission can create, edit, and delete content blocks of each defined block type, from the <a href=":block-library">Content blocks page</a>. After creating a block, place it in a region from the <a href=":blocks">Block layout page</a>, just like blocks provided by other modules.', [':blocks' => Url::fromRoute('block.admin_display')->toString(), ':block-library' => Url::fromRoute('entity.block_content.collection')->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function block_content_theme($existing, $type, $theme, $path): array {
-  return [
-    'block_content_add_list' => [
-      'variables' => ['content' => NULL],
-      'file' => 'block_content.pages.inc',
-    ],
-  ];
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function block_content_entity_type_alter(array &$entity_types): void {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  // Add a translation handler for fields if the language module is enabled.
-  if (\Drupal::moduleHandler()->moduleExists('language')) {
-    $translation = $entity_types['block_content']->get('translation');
-    $translation['block_content'] = TRUE;
-    $entity_types['block_content']->set('translation', $translation);
-  }
-
-  // Swap out the default EntityChanged constraint with a custom one with
-  // different logic for inline blocks.
-  $constraints = $entity_types['block_content']->getConstraints();
-  unset($constraints['EntityChanged']);
-  $constraints['BlockContentEntityChanged'] = NULL;
-  $entity_types['block_content']->setConstraints($constraints);
-}
-
 /**
  * Adds the default body field to a block type.
  *
@@ -117,32 +56,6 @@ function block_content_add_body_field($block_type_id, $label = 'Body') {
   return $field;
 }
 
-/**
- * Implements hook_query_TAG_alter().
- *
- * Alters any 'entity_reference' query where the entity type is
- * 'block_content' and the query has the tag 'block_content_access'.
- *
- * These queries should only return reusable blocks unless a condition on
- * 'reusable' is explicitly set.
- *
- * Block_content entities that are not reusable should by default not be
- * selectable as entity reference values. A module can still create an instance
- * of \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface
- * that will allow selection of non-reusable blocks by explicitly setting
- * a condition on the 'reusable' field.
- *
- * @see \Drupal\block_content\BlockContentAccessControlHandler
- */
-function block_content_query_entity_reference_alter(AlterableInterface $query) {
-  if ($query instanceof SelectInterface && $query->getMetaData('entity_type') === 'block_content' && $query->hasTag('block_content_access')) {
-    $data_table = \Drupal::entityTypeManager()->getDefinition('block_content')->getDataTable();
-    if (array_key_exists($data_table, $query->getTables()) && !_block_content_has_reusable_condition($query->conditions(), $query->getTables())) {
-      $query->condition("$data_table.reusable", TRUE);
-    }
-  }
-}
-
 /**
  * Utility function to find nested conditions using the reusable field.
  *
@@ -186,58 +99,3 @@ function _block_content_has_reusable_condition(array $condition, array $tables)
   }
   return FALSE;
 }
-
-/**
- * Implements hook_theme_suggestions_HOOK_alter() for block templates.
- */
-function block_content_theme_suggestions_block_alter(array &$suggestions, array $variables) {
-  $suggestions_new = [];
-  $content = $variables['elements']['content'];
-
-  $block_content = $variables['elements']['content']['#block_content'] ?? NULL;
-
-  if ($block_content instanceof BlockContentInterface) {
-    $bundle = $content['#block_content']->bundle();
-    $view_mode = strtr($variables['elements']['#configuration']['view_mode'], '.', '_');
-
-    $suggestions_new[] = 'block__block_content__view__' . $view_mode;
-    $suggestions_new[] = 'block__block_content__type__' . $bundle;
-    $suggestions_new[] = 'block__block_content__view_type__' . $bundle . '__' . $view_mode;
-
-    if (!empty($variables['elements']['#id'])) {
-      $suggestions_new[] = 'block__block_content__id__' . $variables['elements']['#id'];
-      $suggestions_new[] = 'block__block_content__id_view__' . $variables['elements']['#id'] . '__' . $view_mode;
-    }
-
-    // Remove duplicate block__block_content.
-    $suggestions = array_unique($suggestions);
-    array_splice($suggestions, 1, 0, $suggestions_new);
-  }
-
-  return $suggestions;
-}
-
-/**
- * Implements hook_entity_operation().
- */
-function block_content_entity_operation(EntityInterface $entity): array {
-  $operations = [];
-  if ($entity instanceof BlockInterface) {
-    $plugin = $entity->getPlugin();
-    if ($plugin->getBaseId() === 'block_content') {
-      $custom_block = \Drupal::entityTypeManager()->getStorage('block_content')->loadByProperties([
-        'uuid' => $plugin->getDerivativeId(),
-      ]);
-      $custom_block = reset($custom_block);
-      if ($custom_block && $custom_block->access('update')) {
-        $operations['block-edit'] = [
-          'title' => t('Edit block'),
-          'url' => $custom_block->toUrl('edit-form')->setOptions([]),
-          'weight' => 50,
-        ];
-      }
-    }
-  }
-
-  return $operations;
-}
diff --git a/core/modules/block_content/block_content.pages.inc b/core/modules/block_content/block_content.pages.inc
index ff1528c265a765c92d44f4207d7dc4711b45cb49..b5124506590a68b90a2ca783fc85b5944c8b249f 100644
--- a/core/modules/block_content/block_content.pages.inc
+++ b/core/modules/block_content/block_content.pages.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Provides page callbacks for content blocks.
  */
 
 use Drupal\Core\Link;
diff --git a/core/modules/block_content/src/Hook/BlockContentHooks.php b/core/modules/block_content/src/Hook/BlockContentHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..155d14a5240965070295bb0f436361914eb42638
--- /dev/null
+++ b/core/modules/block_content/src/Hook/BlockContentHooks.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace Drupal\block_content\Hook;
+
+use Drupal\block\BlockInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\block_content\BlockContentInterface;
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for block_content.
+ */
+class BlockContentHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.block_content':
+        $field_ui = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Block Content module allows you to create and manage custom <em>block types</em> and <em>content-containing blocks</em>. For more information, see the <a href=":online-help">online documentation for the Block Content module</a>.', [':online-help' => 'https://www.drupal.org/documentation/modules/block_content']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Creating and managing block types') . '</dt>';
+        $output .= '<dd>' . t('Users with the <em>Administer blocks</em> permission can create and edit block types with fields and display settings, from the <a href=":types">Block types</a> page under the Structure menu. For more information about managing fields and display settings, see the <a href=":field-ui">Field UI module help</a> and <a href=":field">Field module help</a>.', [
+          ':types' => Url::fromRoute('entity.block_content_type.collection')->toString(),
+          ':field-ui' => $field_ui,
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Creating content blocks') . '</dt>';
+        $output .= '<dd>' . t('Users with the <em>Administer blocks</em> permission can create, edit, and delete content blocks of each defined block type, from the <a href=":block-library">Content blocks page</a>. After creating a block, place it in a region from the <a href=":blocks">Block layout page</a>, just like blocks provided by other modules.', [
+          ':blocks' => Url::fromRoute('block.admin_display')->toString(),
+          ':block-library' => Url::fromRoute('entity.block_content.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme($existing, $type, $theme, $path) : array {
+    return [
+      'block_content_add_list' => [
+        'variables' => [
+          'content' => NULL,
+        ],
+        'file' => 'block_content.pages.inc',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    // Add a translation handler for fields if the language module is enabled.
+    if (\Drupal::moduleHandler()->moduleExists('language')) {
+      $translation = $entity_types['block_content']->get('translation');
+      $translation['block_content'] = TRUE;
+      $entity_types['block_content']->set('translation', $translation);
+    }
+    // Swap out the default EntityChanged constraint with a custom one with
+    // different logic for inline blocks.
+    $constraints = $entity_types['block_content']->getConstraints();
+    unset($constraints['EntityChanged']);
+    $constraints['BlockContentEntityChanged'] = NULL;
+    $entity_types['block_content']->setConstraints($constraints);
+  }
+
+  /**
+   * Implements hook_query_TAG_alter().
+   *
+   * Alters any 'entity_reference' query where the entity type is
+   * 'block_content' and the query has the tag 'block_content_access'.
+   *
+   * These queries should only return reusable blocks unless a condition on
+   * 'reusable' is explicitly set.
+   *
+   * Block_content entities that are not reusable should by default not be
+   * selectable as entity reference values. A module can still create an instance
+   * of \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface
+   * that will allow selection of non-reusable blocks by explicitly setting
+   * a condition on the 'reusable' field.
+   *
+   * @see \Drupal\block_content\BlockContentAccessControlHandler
+   */
+  #[Hook('query_entity_reference_alter')]
+  public function queryEntityReferenceAlter(AlterableInterface $query) {
+    if ($query instanceof SelectInterface && $query->getMetaData('entity_type') === 'block_content' && $query->hasTag('block_content_access')) {
+      $data_table = \Drupal::entityTypeManager()->getDefinition('block_content')->getDataTable();
+      if (array_key_exists($data_table, $query->getTables()) && !_block_content_has_reusable_condition($query->conditions(), $query->getTables())) {
+        $query->condition("{$data_table}.reusable", TRUE);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK_alter() for block templates.
+   */
+  #[Hook('theme_suggestions_block_alter')]
+  public function themeSuggestionsBlockAlter(array &$suggestions, array $variables) {
+    $suggestions_new = [];
+    $content = $variables['elements']['content'];
+    $block_content = $variables['elements']['content']['#block_content'] ?? NULL;
+    if ($block_content instanceof BlockContentInterface) {
+      $bundle = $content['#block_content']->bundle();
+      $view_mode = strtr($variables['elements']['#configuration']['view_mode'], '.', '_');
+      $suggestions_new[] = 'block__block_content__view__' . $view_mode;
+      $suggestions_new[] = 'block__block_content__type__' . $bundle;
+      $suggestions_new[] = 'block__block_content__view_type__' . $bundle . '__' . $view_mode;
+      if (!empty($variables['elements']['#id'])) {
+        $suggestions_new[] = 'block__block_content__id__' . $variables['elements']['#id'];
+        $suggestions_new[] = 'block__block_content__id_view__' . $variables['elements']['#id'] . '__' . $view_mode;
+      }
+      // Remove duplicate block__block_content.
+      $suggestions = array_unique($suggestions);
+      array_splice($suggestions, 1, 0, $suggestions_new);
+    }
+    return $suggestions;
+  }
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $entity) : array {
+    $operations = [];
+    if ($entity instanceof BlockInterface) {
+      $plugin = $entity->getPlugin();
+      if ($plugin->getBaseId() === 'block_content') {
+        $custom_block = \Drupal::entityTypeManager()->getStorage('block_content')->loadByProperties(['uuid' => $plugin->getDerivativeId()]);
+        $custom_block = reset($custom_block);
+        if ($custom_block && $custom_block->access('update')) {
+          $operations['block-edit'] = [
+            'title' => t('Edit block'),
+            'url' => $custom_block->toUrl('edit-form')->setOptions([]),
+            'weight' => 50,
+          ];
+        }
+      }
+    }
+    return $operations;
+  }
+
+}
diff --git a/core/modules/block_content/tests/modules/block_content_test/block_content_test.module b/core/modules/block_content/tests/modules/block_content_test/block_content_test.module
deleted file mode 100644
index 9ab1194bd51eaaecc70fc87636174cea98510d9e..0000000000000000000000000000000000000000
--- a/core/modules/block_content/tests/modules/block_content_test/block_content_test.module
+++ /dev/null
@@ -1,71 +0,0 @@
-<?php
-
-/**
- * @file
- * A dummy module for testing content block related hooks.
- *
- * This is a dummy module that implements content block related hooks to test API
- * interaction with the block_content module.
- */
-
-declare(strict_types=1);
-
-use Drupal\block_content\Entity\BlockContent;
-
-/**
- * Implements hook_block_content_view().
- */
-function block_content_test_block_content_view(array &$build, BlockContent $block_content, $view_mode) {
-  // Add extra content.
-  $build['extra_content'] = [
-    '#markup' => '<blink>Wow</blink>',
-  ];
-}
-
-/**
- * Implements hook_block_content_presave().
- */
-function block_content_test_block_content_presave(BlockContent $block_content) {
-  if ($block_content->label() == 'testing_block_content_presave') {
-    $block_content->setInfo($block_content->label() . '_presave');
-  }
-  // Determine changes.
-  if (!empty($block_content->original) && $block_content->original->label() == 'test_changes') {
-    if ($block_content->original->label() != $block_content->label()) {
-      $block_content->setInfo($block_content->label() . '_presave');
-      // Drupal 1.0 release.
-      $block_content->changed = 979534800;
-    }
-  }
-}
-
-/**
- * Implements hook_block_content_update().
- */
-function block_content_test_block_content_update(BlockContent $block_content) {
-  // Determine changes on update.
-  if (!empty($block_content->original) && $block_content->original->label() == 'test_changes') {
-    if ($block_content->original->label() != $block_content->label()) {
-      $block_content->setInfo($block_content->label() . '_update');
-    }
-  }
-}
-
-/**
- * Implements hook_block_content_insert().
- *
- * This tests saving a block_content on block_content insert.
- *
- * @see \Drupal\block_content\Tests\BlockContentSaveTest::testBlockContentSaveOnInsert()
- */
-function block_content_test_block_content_insert(BlockContent $block_content) {
-  // Set the block_content title to the block_content ID and save.
-  if ($block_content->label() == 'new') {
-    $block_content->setInfo('BlockContent ' . $block_content->id());
-    $block_content->setNewRevision(FALSE);
-    $block_content->save();
-  }
-  if ($block_content->label() == 'fail_creation') {
-    throw new Exception('Test exception for rollback.');
-  }
-}
diff --git a/core/modules/block_content/tests/modules/block_content_test/src/Hook/BlockContentTestHooks.php b/core/modules/block_content/tests/modules/block_content_test/src/Hook/BlockContentTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..34acfa50d09df1d0074fa2676c38e635dd6f80d3
--- /dev/null
+++ b/core/modules/block_content/tests/modules/block_content_test/src/Hook/BlockContentTestHooks.php
@@ -0,0 +1,75 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\block_content_test\Hook;
+
+use Drupal\block_content\Entity\BlockContent;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for block_content_test.
+ */
+class BlockContentTestHooks {
+
+  /**
+   * Implements hook_block_content_view().
+   */
+  #[Hook('block_content_view')]
+  public function blockContentView(array &$build, BlockContent $block_content, $view_mode) {
+    // Add extra content.
+    $build['extra_content'] = ['#markup' => '<blink>Wow</blink>'];
+  }
+
+  /**
+   * Implements hook_block_content_presave().
+   */
+  #[Hook('block_content_presave')]
+  public function blockContentPresave(BlockContent $block_content) {
+    if ($block_content->label() == 'testing_block_content_presave') {
+      $block_content->setInfo($block_content->label() . '_presave');
+    }
+    // Determine changes.
+    if (!empty($block_content->original) && $block_content->original->label() == 'test_changes') {
+      if ($block_content->original->label() != $block_content->label()) {
+        $block_content->setInfo($block_content->label() . '_presave');
+        // Drupal 1.0 release.
+        $block_content->changed = 979534800;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_block_content_update().
+   */
+  #[Hook('block_content_update')]
+  public function blockContentUpdate(BlockContent $block_content) {
+    // Determine changes on update.
+    if (!empty($block_content->original) && $block_content->original->label() == 'test_changes') {
+      if ($block_content->original->label() != $block_content->label()) {
+        $block_content->setInfo($block_content->label() . '_update');
+      }
+    }
+  }
+
+  /**
+   * Implements hook_block_content_insert().
+   *
+   * This tests saving a block_content on block_content insert.
+   *
+   * @see \Drupal\block_content\Tests\BlockContentSaveTest::testBlockContentSaveOnInsert()
+   */
+  #[Hook('block_content_insert')]
+  public function blockContentInsert(BlockContent $block_content) {
+    // Set the block_content title to the block_content ID and save.
+    if ($block_content->label() == 'new') {
+      $block_content->setInfo('BlockContent ' . $block_content->id());
+      $block_content->setNewRevision(FALSE);
+      $block_content->save();
+    }
+    if ($block_content->label() == 'fail_creation') {
+      throw new \Exception('Test exception for rollback.');
+    }
+  }
+
+}
diff --git a/core/modules/block_content/tests/src/Kernel/BlockContentTest.php b/core/modules/block_content/tests/src/Kernel/BlockContentTest.php
index 9c402c586879135c31d50629fd382e7723c54b1a..e155c86f0f7bd45fa88955f39d50f32eba6fd3a3 100644
--- a/core/modules/block_content/tests/src/Kernel/BlockContentTest.php
+++ b/core/modules/block_content/tests/src/Kernel/BlockContentTest.php
@@ -11,6 +11,7 @@
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\Tests\user\Traits\UserCreationTrait;
+use Drupal\block_content\Hook\BlockContentHooks;
 
 /**
  * Tests the block content.
@@ -60,7 +61,8 @@ public function testOperationLinks(): void {
     ]);
 
     // The anonymous user doesn't have the "administer block" permission.
-    $this->assertEmpty(block_content_entity_operation($block));
+    $blockContentEntityOperation = new BlockContentHooks();
+    $this->assertEmpty($blockContentEntityOperation->entityOperation($block));
 
     $this->setUpCurrentUser(['uid' => 1], ['edit any spiffy block content', 'administer blocks']);
 
@@ -71,7 +73,7 @@ public function testOperationLinks(): void {
         'url' => $block_content->toUrl('edit-form')->setOptions([]),
         'weight' => 50,
       ],
-    ], block_content_entity_operation($block));
+    ], $blockContentEntityOperation->entityOperation($block));
   }
 
 }
diff --git a/core/modules/block_content/tests/src/Kernel/BlockTemplateSuggestionsTest.php b/core/modules/block_content/tests/src/Kernel/BlockTemplateSuggestionsTest.php
index 426f9c3ec5c8981402588716753110ef443ea72a..5076c80e457dd5ed9856bb66e9a7d90a85838321 100644
--- a/core/modules/block_content/tests/src/Kernel/BlockTemplateSuggestionsTest.php
+++ b/core/modules/block_content/tests/src/Kernel/BlockTemplateSuggestionsTest.php
@@ -8,6 +8,7 @@
 use Drupal\block_content\Entity\BlockContent;
 use Drupal\block_content\Entity\BlockContentType;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\block_content\Hook\BlockContentHooks;
 
 /**
  * Tests the block_content_theme_suggestions_block() function.
@@ -73,7 +74,8 @@ public function testBlockThemeHookSuggestions(): void {
     $variables['elements']['content']['#block_content'] = $this->blockContent;
     $suggestions_empty = [];
     $suggestions_empty[] = 'block__block_content__' . $block->uuid();
-    $suggestions = block_content_theme_suggestions_block_alter($suggestions_empty, $variables);
+    $blockTemplateSuggestionsAlter = new BlockContentHooks();
+    $suggestions = $blockTemplateSuggestionsAlter->themeSuggestionsBlockAlter($suggestions_empty, $variables);
 
     $this->assertSame([
       'block__block_content__' . $block->uuid(),
diff --git a/core/modules/breakpoint/breakpoint.module b/core/modules/breakpoint/breakpoint.module
deleted file mode 100644
index 2b14352a1ab9826f671bb690b644999218bb6d30..0000000000000000000000000000000000000000
--- a/core/modules/breakpoint/breakpoint.module
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-/**
- * @file
- * Manage breakpoints and breakpoint groups for responsive designs.
- */
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function breakpoint_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.breakpoint':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Breakpoint module keeps track of the height, width, and resolution breakpoints where a responsive design needs to change in order to respond to different devices being used to view the site. This module does not have a user interface. For more information, see the <a href=":docs">online documentation for the Breakpoint module</a>.', [':docs' => 'https://www.drupal.org/documentation/modules/breakpoint']) . '</p>';
-      $output .= '<h4>' . t('Terminology') . '</h4>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Breakpoint') . '</dt>';
-      $output .= '<dd>' . t('A breakpoint separates the height or width of viewports (screens, printers, and other media output types) into steps. For instance, a width breakpoint of 40em creates two steps: one for widths up to 40em and one for widths above 40em. Breakpoints can be used to define when layouts should shift from one form to another, when images should be resized, and other changes that need to respond to changes in viewport height or width.') . '</dd>';
-      $output .= '<dt>' . t('Media query') . '</dt>';
-      $output .= '<dd>' . t('<a href=":w3">Media  queries</a> are a formal way to encode breakpoints. For instance, a width breakpoint at 40em would be written as the media query "(min-width: 40em)". Breakpoints are really just media queries with some additional meta-data, such as a name and multiplier information.', [':w3' => 'https://www.w3.org/TR/css3-mediaqueries/']) . '</dd>';
-      $output .= '<dt>' . t('Resolution multiplier') . '</dt>';
-      $output .= '<dd>' . t('Resolution multipliers are a measure of the viewport\'s device resolution, defined to be the ratio between the physical pixel size of the active device and the <a href="http://en.wikipedia.org/wiki/Device_independent_pixel">device-independent pixel</a> size. The Breakpoint module defines multipliers of 1, 1.5, and 2; when defining breakpoints, modules and themes can define which multipliers apply to each breakpoint.') . '</dd>';
-      $output .= '<dt>' . t('Breakpoint group') . '</dt>';
-      $output .= '<dd>' . t('Breakpoints can be organized into groups. Modules and themes should use groups to separate out breakpoints that are meant to be used for different purposes, such as breakpoints for layouts or breakpoints for image sizing.') . '</dd>';
-      $output .= '</dl>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Defining breakpoints and breakpoint groups') . '</dt>';
-      $output .= '<dd>' . t('Modules and themes can use the API provided by the Breakpoint module to define breakpoints and breakpoint groups, and to assign resolution multipliers to breakpoints.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_themes_installed().
- */
-function breakpoint_themes_installed($theme_list) {
-  \Drupal::service('breakpoint.manager')->clearCachedDefinitions();
-}
-
-/**
- * Implements hook_themes_uninstalled().
- */
-function breakpoint_themes_uninstalled($theme_list) {
-  \Drupal::service('breakpoint.manager')->clearCachedDefinitions();
-}
diff --git a/core/modules/breakpoint/src/Hook/BreakpointHooks.php b/core/modules/breakpoint/src/Hook/BreakpointHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..4c6e8fa8f9aebcdbaf12bf0ed6f5d48b063862a6
--- /dev/null
+++ b/core/modules/breakpoint/src/Hook/BreakpointHooks.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\breakpoint\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for breakpoint.
+ */
+class BreakpointHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.breakpoint':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Breakpoint module keeps track of the height, width, and resolution breakpoints where a responsive design needs to change in order to respond to different devices being used to view the site. This module does not have a user interface. For more information, see the <a href=":docs">online documentation for the Breakpoint module</a>.', [':docs' => 'https://www.drupal.org/documentation/modules/breakpoint']) . '</p>';
+        $output .= '<h4>' . t('Terminology') . '</h4>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Breakpoint') . '</dt>';
+        $output .= '<dd>' . t('A breakpoint separates the height or width of viewports (screens, printers, and other media output types) into steps. For instance, a width breakpoint of 40em creates two steps: one for widths up to 40em and one for widths above 40em. Breakpoints can be used to define when layouts should shift from one form to another, when images should be resized, and other changes that need to respond to changes in viewport height or width.') . '</dd>';
+        $output .= '<dt>' . t('Media query') . '</dt>';
+        $output .= '<dd>' . t('<a href=":w3">Media  queries</a> are a formal way to encode breakpoints. For instance, a width breakpoint at 40em would be written as the media query "(min-width: 40em)". Breakpoints are really just media queries with some additional meta-data, such as a name and multiplier information.', [':w3' => 'https://www.w3.org/TR/css3-mediaqueries/']) . '</dd>';
+        $output .= '<dt>' . t('Resolution multiplier') . '</dt>';
+        $output .= '<dd>' . t('Resolution multipliers are a measure of the viewport\'s device resolution, defined to be the ratio between the physical pixel size of the active device and the <a href="http://en.wikipedia.org/wiki/Device_independent_pixel">device-independent pixel</a> size. The Breakpoint module defines multipliers of 1, 1.5, and 2; when defining breakpoints, modules and themes can define which multipliers apply to each breakpoint.') . '</dd>';
+        $output .= '<dt>' . t('Breakpoint group') . '</dt>';
+        $output .= '<dd>' . t('Breakpoints can be organized into groups. Modules and themes should use groups to separate out breakpoints that are meant to be used for different purposes, such as breakpoints for layouts or breakpoints for image sizing.') . '</dd>';
+        $output .= '</dl>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Defining breakpoints and breakpoint groups') . '</dt>';
+        $output .= '<dd>' . t('Modules and themes can use the API provided by the Breakpoint module to define breakpoints and breakpoint groups, and to assign resolution multipliers to breakpoints.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_themes_installed().
+   */
+  #[Hook('themes_installed')]
+  public function themesInstalled($theme_list) {
+    \Drupal::service('breakpoint.manager')->clearCachedDefinitions();
+  }
+
+  /**
+   * Implements hook_themes_uninstalled().
+   */
+  #[Hook('themes_uninstalled')]
+  public function themesUninstalled($theme_list) {
+    \Drupal::service('breakpoint.manager')->clearCachedDefinitions();
+  }
+
+}
diff --git a/core/modules/ckeditor5/ckeditor5.module b/core/modules/ckeditor5/ckeditor5.module
index 8ba406780c0933a2638a4e92e5c3e2dbd9327f8a..caa6f3f50dabd92c2aa11087b2b29dd9f1720417 100644
--- a/core/modules/ckeditor5/ckeditor5.module
+++ b/core/modules/ckeditor5/ckeditor5.module
@@ -8,7 +8,6 @@
 declare(strict_types = 1);
 
 use Drupal\ckeditor5\HTMLRestrictions;
-use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\InvokeCommand;
@@ -16,80 +15,7 @@
 use Drupal\Core\Ajax\PrependCommand;
 use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Ajax\RemoveCommand;
-use Drupal\Core\Asset\AttachedAssetsInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Render\Element;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_help().
- */
-function ckeditor5_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.ckeditor5':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The CKEditor 5 module provides a highly-accessible, highly-usable visual text editor and adds a toolbar to text fields. Users can use buttons to format content and to create semantically correct and valid HTML. The CKEditor module uses the framework provided by the <a href=":text_editor">Text Editor module</a>. It requires JavaScript to be enabled in the browser. For more information, see the <a href=":doc_url">online documentation for the CKEditor 5 module</a> and the <a href=":cke5_url">CKEditor 5 website</a>.', [':doc_url' => 'https://www.drupal.org/docs/contributed-modules/ckeditor-5', ':cke5_url' => 'https://ckeditor.com/ckeditor-5/', ':text_editor' => Url::fromRoute('help.page', ['name' => 'editor'])->toString()]) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Enabling CKEditor 5 for individual text formats') . '</dt>';
-      $output .= '<dd>' . t('CKEditor 5 has to be installed and configured separately for individual text formats from the <a href=":formats">Text formats and editors page</a> because the filter settings for each text format can be different. For more information, see the <a href=":text_editor">Text Editor help page</a> and <a href=":filter">Filter help page</a>.', [':formats' => Url::fromRoute('filter.admin_overview')->toString(), ':text_editor' => Url::fromRoute('help.page', ['name' => 'editor'])->toString(), ':filter' => Url::fromRoute('help.page', ['name' => 'filter'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Configuring the toolbar') . '</dt>';
-      $output .= '<dd>' . t('When CKEditor 5 is chosen from the <em>Text editor</em> drop-down menu, its toolbar configuration is displayed. You can add and remove buttons from the <em>Active toolbar</em> by dragging and dropping them. Separators and rows can be added to organize the buttons.') . '</dd>';
-      $output .= '<dt>' . t('Filtering HTML content') . '</dt>';
-      $output .= '<dd>' . t("Unlike other text editors, plugin configuration determines the tags and attributes allowed in text formats using CKEditor 5. If using the <em>Limit allowed HTML tags and correct faulty HTML</em> filter, this filter's values will be automatically set based on enabled plugins and toolbar items.");
-      $output .= '<dt>' . t('Toggling between formatted text and HTML source') . '</dt>';
-      $output .= '<dd>' . t('If the <em>Source</em> button is available in the toolbar, users can click this button to disable the visual editor and edit the HTML source directly. After toggling back, the visual editor uses the HTML tags allowed via plugin configuration (and not explicity disallowed by filters) to format the text. Tags not enabled via plugin configuration will be stripped out of the HTML source when the user toggles back to the text editor.') . '</dd>';
-      $output .= '<dt>' . t('Developing CKEditor 5 plugins in Drupal') . '</dt>';
-      $output .= '<dd>' . t('See the <a href=":dev_docs_url">online documentation</a> for detailed information on developing CKEditor 5 plugins for use in Drupal.', [':dev_docs_url' => 'https://www.drupal.org/docs/contributed-modules/ckeditor-5/plugin-and-contrib-module-development']) . '</dd>';
-      $output .= '</dd>';
-      $output .= '<dt>' . t('Accessibility features') . '</dt>';
-      $output .= '<dd>' . t('The built in WYSIWYG editor (CKEditor 5) comes with a number of accessibility features. CKEditor 5 comes with built in <a href=":shortcuts">keyboard shortcuts</a>, which can be beneficial for both power users and keyboard only users.', [':shortcuts' => 'https://ckeditor.com/docs/ckeditor5/latest/features/keyboard-support.html']) . '</dd>';
-      $output .= '<dt>' . t('Generating accessible content') . '</dt>';
-      $output .= '<dd>';
-      $output .= '<ul>';
-      $output .= '<li>' . t('HTML tables can be created with table headers and caption/summary elements.') . '</li>';
-      $output .= '<li>' . t('Alt text is required by default on images added through CKEditor (note that this can be overridden).') . '</li>';
-      $output .= '<li>' . t('Semantic HTML5 figure/figcaption are available to add captions to images.') . '</li>';
-      $output .= '<li>' . t('To support multilingual page content, CKEditor 5 can be configured to include a language button in the toolbar.') . '</li>';
-      $output .= '</ul>';
-      $output .= '</dd>';
-      $output .= '</dl>';
-      $output .= '<h3 id="migration-settings">' . t('Migrating an Existing Text Format to CKEditor 5') . '</h2>';
-      $output .= '<p>' . t('When switching an existing text format to use CKEditor 5, an automatic process is initiated that helps text formats switching to CKEditor 5 from CKEditor 4 (or no text editor) to do so with minimal effort and zero data loss.') . '</p>';
-      $output .= '<p>' . t("This process is designed for there to be no data loss risk in switching to CKEditor 5. However some of your editor's functionality may not be 100% equivalent to what was available previously. In most cases, these changes are minimal. After the process completes, status and/or warning messages will summarize any changes that occurred, and more detailed information will be available in the site's logs.") . '</p>';
-      $output .= '<p>' . t('CKEditor 5 will attempt to enable plugins that provide equivalent toolbar items to those used prior to switching to CKEditor 5. All core CKEditor 4 plugins and many popular contrib plugins already have CKEditor 5 equivalents. In some cases, functionality that required contrib modules is now built into CKEditor 5. In instances where a plugin does not have an equivalent, no data loss will occur but elements previously provided via the plugin may need to be added manually as HTML via source editing.') . '</p>';
-      $output .= '<h4>' . t('Additional migration considerations for text formats with restricted HTML') . '</h4>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('The “Allowed HTML tags" field in the “Limit allowed HTML tags and correct Faulty HTML" filter is now read-only') . '</dt>';
-      $output .= '<dd>' . t('This field accurately represents the tags/attributes allowed by a text format, but the allowed tags are based on which plugins are enabled and how they are configured. For example, enabling the Underline plugin adds the &lt;u&gt; tag to “Allowed HTML tags".') . '</dd>';
-      $output .= '<dt id="required-tags">' . t('The &lt;p&gt; and &lt;br &gt; tags will be automatically added to your text format.') . '</dt>';
-      $output .= '<dd>' . t('CKEditor 5 requires the &lt;p&gt; and &lt;br &gt; tags to achieve basic functionality. They will be automatically added to “Allowed HTML tags" on formats that previously did not allow them.') . '</dd>';
-      $output .= '<dt id="source-editing">' . t('Tags/attributes that are not explicitly supported by any plugin are supported by Source Editing') . '</dt>';
-      $output .= '<dd>' . t('When a necessary tag/attribute is not directly supported by an available plugin, the "Source Editing" plugin is enabled. This plugin is typically used for by passing the CKEditor 5 UI and editing contents as HTML source. In the settings for Source Editing, tags/attributes that aren\'t available via other plugins are added to Source Editing\'s "Manually editable HTML tags" setting so they are supported by the text format.') . '</dd>';
-      $output .= '</dl>';
-
-      return $output;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function ckeditor5_theme(): array {
-  return [
-    // The theme hook is used for rendering the CKEditor 5 toolbar settings in
-    // the Drupal admin UI. The toolbar settings UI is internal, and utilizing
-    // it outside of core usages is not supported because the UI can change at
-    // any point.
-    // @internal
-    'ckeditor5_settings_toolbar' => [
-      'render element' => 'form',
-    ],
-  ];
-}
 
 /**
  * Implements hook_module_implements_alter().
@@ -116,117 +42,6 @@ function ckeditor5_module_implements_alter(&$implementations, $hook) {
   }
 }
 
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function ckeditor5_form_filter_format_form_alter(array &$form, FormStateInterface $form_state, $form_id): void {
-  $editor = $form_state->get('editor');
-
-  // CKEditor 5 plugin config determines the available HTML tags. If an HTML
-  // restricting filter is enabled and the editor is CKEditor 5, the 'Allowed
-  // HTML tags' field is made read only and automatically populated with the
-  // values needed by CKEditor 5 plugins.
-  // @see \Drupal\ckeditor5\Plugin\Editor\CKEditor5::buildConfigurationForm()
-  if ($editor && $editor->getEditor() === 'ckeditor5') {
-    if (isset($form['filters']['settings']['filter_html']['allowed_html'])) {
-      $filter_allowed_html = &$form['filters']['settings']['filter_html']['allowed_html'];
-
-      $filter_allowed_html['#value_callback'] = [CKEditor5::class, 'getGeneratedAllowedHtmlValue'];
-      // Set readonly and add the form-disabled wrapper class as using #disabled
-      // or the disabled attribute will prevent the new values from being
-      // validated.
-      $filter_allowed_html['#attributes']['readonly'] = TRUE;
-      $filter_allowed_html['#wrapper_attributes']['class'][] = 'form-disabled';
-
-      $filter_allowed_html['#description'] = t('With CKEditor 5 this is a
-          read-only field. The allowed HTML tags and attributes are determined
-          by the CKEditor 5 configuration. Manually removing tags would break
-          enabled functionality, and any manually added tags would be removed by
-          CKEditor 5 on render.');
-
-      // The media_filter_format_edit_form_validate validator is not needed
-      // with CKEditor 5 as it exists to enforce the inclusion of specific
-      // allowed tags that are added automatically by CKEditor 5. The
-      // validator is removed so it does not conflict with the automatic
-      // addition of those allowed tags.
-      $key = array_search('media_filter_format_edit_form_validate', $form['#validate']);
-      if ($key !== FALSE) {
-        unset($form['#validate'][$key]);
-      }
-    }
-  }
-
-  // Override the AJAX callbacks for changing editors, so multiple areas of the
-  // form can be updated on change.
-  $form['editor']['editor']['#ajax'] = [
-    'callback' => '_update_ckeditor5_html_filter',
-    'trigger_as' => ['name' => 'editor_configure'],
-  ];
-  $form['editor']['configure']['#ajax'] = [
-    'callback' => '_update_ckeditor5_html_filter',
-  ];
-
-  $form['editor']['settings']['subform']['toolbar']['items']['#ajax'] = [
-    'callback' => '_update_ckeditor5_html_filter',
-    'trigger_as' => ['name' => 'editor_configure'],
-    'event' => 'change',
-    'ckeditor5_only' => 'true',
-  ];
-
-  foreach (Element::children($form['filters']['status']) as $filter_type) {
-    $form['filters']['status'][$filter_type]['#ajax'] = [
-      'callback' => '_update_ckeditor5_html_filter',
-      'trigger_as' => ['name' => 'editor_configure'],
-      'event' => 'change',
-      'ckeditor5_only' => 'true',
-    ];
-  }
-
-  /**
-   * Recursively adds AJAX listeners to plugin settings elements.
-   *
-   * These are added so allowed tags and other fields that have values
-   * dependent on plugin settings can be updated via AJAX when these settings
-   * are changed in the editor form.
-   *
-   * @param array $plugins_config_form
-   *   The plugins config subform render array.
-   */
-  $add_listener = function (array &$plugins_config_form) use (&$add_listener): void {
-    $field_types = [
-      'checkbox',
-      'select',
-      'radios',
-      'textarea',
-    ];
-    if (isset($plugins_config_form['#type']) && in_array($plugins_config_form['#type'], $field_types) && !isset($plugins_config_form['#ajax'])) {
-      $plugins_config_form['#ajax'] = [
-        'callback' => '_update_ckeditor5_html_filter',
-        'trigger_as' => ['name' => 'editor_configure'],
-        'event' => 'change',
-        'ckeditor5_only' => 'true',
-      ];
-    }
-
-    foreach ($plugins_config_form as $key => &$value) {
-      if (is_array($value) && !str_contains((string) $key, '#')) {
-        $add_listener($value);
-      }
-    }
-  };
-
-  if (isset($form['editor']['settings']['subform']['plugins'])) {
-    $add_listener($form['editor']['settings']['subform']['plugins']);
-  }
-
-  // Add an ID to the filter settings vertical tabs wrapper to facilitate AJAX
-  // updates.
-  $form['filter_settings']['#wrapper_attributes']['id'] = 'filter-settings-wrapper';
-  $form['#after_build'][] = [CKEditor5::class, 'assessActiveTextEditorAfterBuild'];
-  $form['#validate'][] = [CKEditor5::class, 'validateSwitchingToCKEditor5'];
-  array_unshift($form['actions']['submit']['#submit'], 'ckeditor5_filter_format_edit_form_submit');
-}
-
 /**
  * Form submission handler for filter format forms.
  */
@@ -403,184 +218,6 @@ function _ckeditor5_get_langcode_mapping($lang = FALSE) {
   return $langcodes;
 }
 
-/**
- * Implements hook_library_info_alter().
- */
-function ckeditor5_library_info_alter(&$libraries, $extension) {
-  if ($extension === 'filter') {
-    $libraries['drupal.filter.admin']['dependencies'][] = 'ckeditor5/internal.drupal.ckeditor5.filter.admin';
-  }
-
-  $moduleHandler = \Drupal::moduleHandler();
-
-  if ($extension === 'ckeditor5') {
-    // Add paths to stylesheets specified by a theme's ckeditor5-stylesheets
-    // config property.
-    $css = _ckeditor5_theme_css();
-    $libraries['internal.drupal.ckeditor5.stylesheets'] = [
-      'css' => [
-        'theme' => array_fill_keys(array_values($css), []),
-      ],
-    ];
-  }
-
-  if ($extension === 'core') {
-    // CSS rule to resolve the conflict with z-index between CKEditor 5 and jQuery UI.
-    $libraries['drupal.dialog']['css']['component']['modules/ckeditor5/css/ckeditor5.dialog.fix.css'] = [];
-    // Fix the CKEditor 5 focus management in dialogs. Modify the library
-    // declaration to ensure this file is always loaded after
-    // drupal.dialog.jquery-ui.js.
-    $libraries['drupal.dialog']['js']['modules/ckeditor5/js/ckeditor5.dialog.fix.js'] = [];
-  }
-
-  // Only add translation processing if the locale module is enabled.
-  if (!$moduleHandler->moduleExists('locale')) {
-    return;
-  }
-  // All possibles CKEditor 5 languages that can be used by Drupal.
-  $ckeditor_langcodes = array_values(_ckeditor5_get_langcode_mapping());
-
-  if ($extension === 'core') {
-    // Generate libraries for each of the CKEditor 5 translation files so that
-    // the correct translation file can be attached depending on the current
-    // language. This makes sure that caching caches the appropriate language.
-    // Only create libraries for languages that have a mapping to Drupal.
-    foreach ($ckeditor_langcodes as $langcode) {
-      $libraries['ckeditor5.translations.' . $langcode] = [
-        'remote' => $libraries['ckeditor5']['remote'],
-        'version' => $libraries['ckeditor5']['version'],
-        'license' => $libraries['ckeditor5']['license'],
-        'dependencies' => [
-          'core/ckeditor5',
-          'core/ckeditor5.translations',
-        ],
-      ];
-    }
-  }
-
-  // Copied from \Drupal\Core\Asset\LibraryDiscoveryParser::buildByExtension().
-  if ($extension === 'core') {
-    $path = 'core';
-  }
-  else {
-    if ($moduleHandler->moduleExists($extension)) {
-      $extension_type = 'module';
-    }
-    else {
-      $extension_type = 'theme';
-    }
-    $path = \Drupal::getContainer()->get('extension.path.resolver')->getPath($extension_type, $extension);
-  }
-
-  foreach ($libraries as &$library) {
-    // The way to know if a library has a translation is to depend on the
-    // special "core/ckeditor5.translations" library.
-    if (empty($library['js']) || empty($library['dependencies']) || !in_array('core/ckeditor5.translations', $library['dependencies'])) {
-      continue;
-    }
-
-    foreach ($library['js'] as $file => $options) {
-      // Only look for translations on libraries defined with a relative path.
-      if (!empty($options['type']) && $options['type'] === 'external') {
-        continue;
-      }
-      // Path relative to the current extension folder.
-      $dirname = dirname($file);
-      // Path of the folder in the filesystem relative to the Drupal root.
-      $dir = $path . '/' . $dirname;
-      // Exclude protocol-free URI.
-      if (str_starts_with($dirname, '//')) {
-        continue;
-      }
-      // CKEditor 5 plugins are most likely added through composer and
-      // installed in the module exposing it. Suppose the file path is
-      // relative to the module and not in the /libraries/ folder.
-      // Collect translations based on filename, and add all existing
-      // translations files to the plugin library. Unnecessary translations
-      // will be filtered in ckeditor5_js_alter() hook.
-      $files = scandir("$dir/translations");
-      foreach ($files as $file) {
-        if (str_ends_with($file, '.js')) {
-          $langcode = basename($file, '.js');
-          // Only add languages that Drupal can understands.
-          if (in_array($langcode, $ckeditor_langcodes)) {
-            $library['js']["$dirname/translations/$langcode.js"] = [
-              // Used in ckeditor5_js_alter() to filter unwanted translations.
-              'ckeditor5_langcode' => $langcode,
-              'minified' => TRUE,
-              'preprocess' => TRUE,
-            ];
-          }
-        }
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_js_alter().
- */
-function ckeditor5_js_alter(&$javascript, AttachedAssetsInterface $assets, LanguageInterface $language) {
-  // This file means CKEditor 5 translations are in use on the page.
-  // @see locale_js_alter()
-  $placeholder_file = 'core/assets/vendor/ckeditor5/translation.js';
-  // This file is used to get a weight that will make it possible to aggregate
-  // all translation files in a single aggregate.
-  $ckeditor_dll_file = 'core/assets/vendor/ckeditor5/ckeditor5-dll/ckeditor5-dll.js';
-  if (isset($javascript[$placeholder_file])) {
-    // Use the placeholder file weight to set all the translations files weights
-    // so they can be aggregated together as expected.
-    $default_weight = $javascript[$placeholder_file]['weight'];
-    if (isset($javascript[$ckeditor_dll_file])) {
-      $default_weight = $javascript[$ckeditor_dll_file]['weight'];
-    }
-    // The placeholder file is not a real file, remove it from the list.
-    unset($javascript[$placeholder_file]);
-
-    // When the locale module isn't installed there are no translations.
-    if (!\Drupal::moduleHandler()->moduleExists('locale')) {
-      return;
-    }
-
-    $ckeditor5_language = _ckeditor5_get_langcode_mapping($language->getId());
-
-    // Remove all CKEditor 5 translations files that are not in the current
-    // language.
-    foreach ($javascript as $index => &$item) {
-      // This is not a CKEditor 5 translation file, skip it.
-      if (empty($item['ckeditor5_langcode'])) {
-        continue;
-      }
-      // This file is the correct translation for this page.
-      if ($item['ckeditor5_langcode'] === $ckeditor5_language) {
-        // Set the weight for the translation file to be able to have the
-        // translation files aggregated.
-        $item['weight'] = $default_weight;
-      }
-      // When the file doesn't match the langcode remove it from the page.
-      else {
-        // Remove files that don't match the language requested.
-        unset($javascript[$index]);
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_config_schema_info_alter().
- */
-function ckeditor5_config_schema_info_alter(&$definitions) {
-  // In \Drupal\Tests\config\Functional\ConfigImportAllTest, this hook may be
-  // called without ckeditor5.pair.schema.yml being active.
-  if (!isset($definitions['ckeditor5_valid_pair__format_and_editor'])) {
-    return;
-  }
-  // @see filter.format.*.filters
-  $definitions['ckeditor5_valid_pair__format_and_editor']['mapping']['filters'] = $definitions['filter.format.*']['mapping']['filters'];
-  // @see @see editor.editor.*.image_upload
-  $definitions['ckeditor5_valid_pair__format_and_editor']['mapping']['image_upload'] = $definitions['editor.editor.*']['mapping']['image_upload'];
-}
-
 /**
  * Retrieves the default theme's CKEditor 5 stylesheets.
  *
diff --git a/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php b/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f89a1cbd222e17a5958f392a8ea2b72820eae6f1
--- /dev/null
+++ b/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php
@@ -0,0 +1,362 @@
+<?php
+
+namespace Drupal\ckeditor5\Hook;
+
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\Core\Render\Element;
+use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for ckeditor5.
+ */
+class Ckeditor5Hooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.ckeditor5':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The CKEditor 5 module provides a highly-accessible, highly-usable visual text editor and adds a toolbar to text fields. Users can use buttons to format content and to create semantically correct and valid HTML. The CKEditor module uses the framework provided by the <a href=":text_editor">Text Editor module</a>. It requires JavaScript to be enabled in the browser. For more information, see the <a href=":doc_url">online documentation for the CKEditor 5 module</a> and the <a href=":cke5_url">CKEditor 5 website</a>.', [
+          ':doc_url' => 'https://www.drupal.org/docs/contributed-modules/ckeditor-5',
+          ':cke5_url' => 'https://ckeditor.com/ckeditor-5/',
+          ':text_editor' => Url::fromRoute('help.page', [
+            'name' => 'editor',
+          ])->toString(),
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Enabling CKEditor 5 for individual text formats') . '</dt>';
+        $output .= '<dd>' . t('CKEditor 5 has to be installed and configured separately for individual text formats from the <a href=":formats">Text formats and editors page</a> because the filter settings for each text format can be different. For more information, see the <a href=":text_editor">Text Editor help page</a> and <a href=":filter">Filter help page</a>.', [
+          ':formats' => Url::fromRoute('filter.admin_overview')->toString(),
+          ':text_editor' => Url::fromRoute('help.page', [
+            'name' => 'editor',
+          ])->toString(),
+          ':filter' => Url::fromRoute('help.page', [
+            'name' => 'filter',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Configuring the toolbar') . '</dt>';
+        $output .= '<dd>' . t('When CKEditor 5 is chosen from the <em>Text editor</em> drop-down menu, its toolbar configuration is displayed. You can add and remove buttons from the <em>Active toolbar</em> by dragging and dropping them. Separators and rows can be added to organize the buttons.') . '</dd>';
+        $output .= '<dt>' . t('Filtering HTML content') . '</dt>';
+        $output .= '<dd>' . t("Unlike other text editors, plugin configuration determines the tags and attributes allowed in text formats using CKEditor 5. If using the <em>Limit allowed HTML tags and correct faulty HTML</em> filter, this filter's values will be automatically set based on enabled plugins and toolbar items.");
+        $output .= '<dt>' . t('Toggling between formatted text and HTML source') . '</dt>';
+        $output .= '<dd>' . t('If the <em>Source</em> button is available in the toolbar, users can click this button to disable the visual editor and edit the HTML source directly. After toggling back, the visual editor uses the HTML tags allowed via plugin configuration (and not explicity disallowed by filters) to format the text. Tags not enabled via plugin configuration will be stripped out of the HTML source when the user toggles back to the text editor.') . '</dd>';
+        $output .= '<dt>' . t('Developing CKEditor 5 plugins in Drupal') . '</dt>';
+        $output .= '<dd>' . t('See the <a href=":dev_docs_url">online documentation</a> for detailed information on developing CKEditor 5 plugins for use in Drupal.', [
+          ':dev_docs_url' => 'https://www.drupal.org/docs/contributed-modules/ckeditor-5/plugin-and-contrib-module-development',
+        ]) . '</dd>';
+        $output .= '</dd>';
+        $output .= '<dt>' . t('Accessibility features') . '</dt>';
+        $output .= '<dd>' . t('The built in WYSIWYG editor (CKEditor 5) comes with a number of accessibility features. CKEditor 5 comes with built in <a href=":shortcuts">keyboard shortcuts</a>, which can be beneficial for both power users and keyboard only users.', [
+          ':shortcuts' => 'https://ckeditor.com/docs/ckeditor5/latest/features/keyboard-support.html',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Generating accessible content') . '</dt>';
+        $output .= '<dd>';
+        $output .= '<ul>';
+        $output .= '<li>' . t('HTML tables can be created with table headers and caption/summary elements.') . '</li>';
+        $output .= '<li>' . t('Alt text is required by default on images added through CKEditor (note that this can be overridden).') . '</li>';
+        $output .= '<li>' . t('Semantic HTML5 figure/figcaption are available to add captions to images.') . '</li>';
+        $output .= '<li>' . t('To support multilingual page content, CKEditor 5 can be configured to include a language button in the toolbar.') . '</li>';
+        $output .= '</ul>';
+        $output .= '</dd>';
+        $output .= '</dl>';
+        $output .= '<h3 id="migration-settings">' . t('Migrating an Existing Text Format to CKEditor 5') . '</h2>';
+        $output .= '<p>' . t('When switching an existing text format to use CKEditor 5, an automatic process is initiated that helps text formats switching to CKEditor 5 from CKEditor 4 (or no text editor) to do so with minimal effort and zero data loss.') . '</p>';
+        $output .= '<p>' . t("This process is designed for there to be no data loss risk in switching to CKEditor 5. However some of your editor's functionality may not be 100% equivalent to what was available previously. In most cases, these changes are minimal. After the process completes, status and/or warning messages will summarize any changes that occurred, and more detailed information will be available in the site's logs.") . '</p>';
+        $output .= '<p>' . t('CKEditor 5 will attempt to enable plugins that provide equivalent toolbar items to those used prior to switching to CKEditor 5. All core CKEditor 4 plugins and many popular contrib plugins already have CKEditor 5 equivalents. In some cases, functionality that required contrib modules is now built into CKEditor 5. In instances where a plugin does not have an equivalent, no data loss will occur but elements previously provided via the plugin may need to be added manually as HTML via source editing.') . '</p>';
+        $output .= '<h4>' . t('Additional migration considerations for text formats with restricted HTML') . '</h4>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('The “Allowed HTML tags" field in the “Limit allowed HTML tags and correct Faulty HTML" filter is now read-only') . '</dt>';
+        $output .= '<dd>' . t('This field accurately represents the tags/attributes allowed by a text format, but the allowed tags are based on which plugins are enabled and how they are configured. For example, enabling the Underline plugin adds the &lt;u&gt; tag to “Allowed HTML tags".') . '</dd>';
+        $output .= '<dt id="required-tags">' . t('The &lt;p&gt; and &lt;br &gt; tags will be automatically added to your text format.') . '</dt>';
+        $output .= '<dd>' . t('CKEditor 5 requires the &lt;p&gt; and &lt;br &gt; tags to achieve basic functionality. They will be automatically added to “Allowed HTML tags" on formats that previously did not allow them.') . '</dd>';
+        $output .= '<dt id="source-editing">' . t('Tags/attributes that are not explicitly supported by any plugin are supported by Source Editing') . '</dt>';
+        $output .= '<dd>' . t('When a necessary tag/attribute is not directly supported by an available plugin, the "Source Editing" plugin is enabled. This plugin is typically used for by passing the CKEditor 5 UI and editing contents as HTML source. In the settings for Source Editing, tags/attributes that aren\'t available via other plugins are added to Source Editing\'s "Manually editable HTML tags" setting so they are supported by the text format.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return ['ckeditor5_settings_toolbar' => ['render element' => 'form']];
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_filter_format_form_alter')]
+  public function formFilterFormatFormAlter(array &$form, FormStateInterface $form_state, $form_id) : void {
+    $editor = $form_state->get('editor');
+    // CKEditor 5 plugin config determines the available HTML tags. If an HTML
+    // restricting filter is enabled and the editor is CKEditor 5, the 'Allowed
+    // HTML tags' field is made read only and automatically populated with the
+    // values needed by CKEditor 5 plugins.
+    // @see \Drupal\ckeditor5\Plugin\Editor\CKEditor5::buildConfigurationForm()
+    if ($editor && $editor->getEditor() === 'ckeditor5') {
+      if (isset($form['filters']['settings']['filter_html']['allowed_html'])) {
+        $filter_allowed_html =& $form['filters']['settings']['filter_html']['allowed_html'];
+        $filter_allowed_html['#value_callback'] = [CKEditor5::class, 'getGeneratedAllowedHtmlValue'];
+        // Set readonly and add the form-disabled wrapper class as using #disabled
+        // or the disabled attribute will prevent the new values from being
+        // validated.
+        $filter_allowed_html['#attributes']['readonly'] = TRUE;
+        $filter_allowed_html['#wrapper_attributes']['class'][] = 'form-disabled';
+        $filter_allowed_html['#description'] = t('With CKEditor 5 this is a
+          read-only field. The allowed HTML tags and attributes are determined
+          by the CKEditor 5 configuration. Manually removing tags would break
+          enabled functionality, and any manually added tags would be removed by
+          CKEditor 5 on render.');
+        // The media_filter_format_edit_form_validate validator is not needed
+        // with CKEditor 5 as it exists to enforce the inclusion of specific
+        // allowed tags that are added automatically by CKEditor 5. The
+        // validator is removed so it does not conflict with the automatic
+        // addition of those allowed tags.
+        $key = array_search('media_filter_format_edit_form_validate', $form['#validate']);
+        if ($key !== FALSE) {
+          unset($form['#validate'][$key]);
+        }
+      }
+    }
+    // Override the AJAX callbacks for changing editors, so multiple areas of the
+    // form can be updated on change.
+    $form['editor']['editor']['#ajax'] = [
+      'callback' => '_update_ckeditor5_html_filter',
+      'trigger_as' => [
+        'name' => 'editor_configure',
+      ],
+    ];
+    $form['editor']['configure']['#ajax'] = ['callback' => '_update_ckeditor5_html_filter'];
+    $form['editor']['settings']['subform']['toolbar']['items']['#ajax'] = [
+      'callback' => '_update_ckeditor5_html_filter',
+      'trigger_as' => [
+        'name' => 'editor_configure',
+      ],
+      'event' => 'change',
+      'ckeditor5_only' => 'true',
+    ];
+    foreach (Element::children($form['filters']['status']) as $filter_type) {
+      $form['filters']['status'][$filter_type]['#ajax'] = [
+        'callback' => '_update_ckeditor5_html_filter',
+        'trigger_as' => [
+          'name' => 'editor_configure',
+        ],
+        'event' => 'change',
+        'ckeditor5_only' => 'true',
+      ];
+    }
+    /**
+     * Recursively adds AJAX listeners to plugin settings elements.
+     *
+     * These are added so allowed tags and other fields that have values
+     * dependent on plugin settings can be updated via AJAX when these settings
+     * are changed in the editor form.
+     *
+     * @param array $plugins_config_form
+     *   The plugins config subform render array.
+     */
+    $add_listener = function (array &$plugins_config_form) use (&$add_listener) : void {
+      $field_types = ['checkbox', 'select', 'radios', 'textarea'];
+      if (isset($plugins_config_form['#type']) && in_array($plugins_config_form['#type'], $field_types) && !isset($plugins_config_form['#ajax'])) {
+        $plugins_config_form['#ajax'] = [
+          'callback' => '_update_ckeditor5_html_filter',
+          'trigger_as' => [
+            'name' => 'editor_configure',
+          ],
+          'event' => 'change',
+          'ckeditor5_only' => 'true',
+        ];
+      }
+      foreach ($plugins_config_form as $key => &$value) {
+        if (is_array($value) && !str_contains((string) $key, '#')) {
+          $add_listener($value);
+        }
+      }
+    };
+    if (isset($form['editor']['settings']['subform']['plugins'])) {
+      $add_listener($form['editor']['settings']['subform']['plugins']);
+    }
+    // Add an ID to the filter settings vertical tabs wrapper to facilitate AJAX
+    // updates.
+    $form['filter_settings']['#wrapper_attributes']['id'] = 'filter-settings-wrapper';
+    $form['#after_build'][] = [
+      CKEditor5::class,
+      'assessActiveTextEditorAfterBuild',
+    ];
+    $form['#validate'][] = [CKEditor5::class, 'validateSwitchingToCKEditor5'];
+    array_unshift($form['actions']['submit']['#submit'], 'ckeditor5_filter_format_edit_form_submit');
+  }
+
+  /**
+   * Implements hook_library_info_alter().
+   */
+  #[Hook('library_info_alter')]
+  public function libraryInfoAlter(&$libraries, $extension) {
+    if ($extension === 'filter') {
+      $libraries['drupal.filter.admin']['dependencies'][] = 'ckeditor5/internal.drupal.ckeditor5.filter.admin';
+    }
+    $moduleHandler = \Drupal::moduleHandler();
+    if ($extension === 'ckeditor5') {
+      // Add paths to stylesheets specified by a theme's ckeditor5-stylesheets
+      // config property.
+      $css = _ckeditor5_theme_css();
+      $libraries['internal.drupal.ckeditor5.stylesheets'] = ['css' => ['theme' => array_fill_keys(array_values($css), [])]];
+    }
+    if ($extension === 'core') {
+      // CSS rule to resolve the conflict with z-index between CKEditor 5 and jQuery UI.
+      $libraries['drupal.dialog']['css']['component']['modules/ckeditor5/css/ckeditor5.dialog.fix.css'] = [];
+      // Fix the CKEditor 5 focus management in dialogs. Modify the library
+      // declaration to ensure this file is always loaded after
+      // drupal.dialog.jquery-ui.js.
+      $libraries['drupal.dialog']['js']['modules/ckeditor5/js/ckeditor5.dialog.fix.js'] = [];
+    }
+    // Only add translation processing if the locale module is enabled.
+    if (!$moduleHandler->moduleExists('locale')) {
+      return;
+    }
+    // All possibles CKEditor 5 languages that can be used by Drupal.
+    $ckeditor_langcodes = array_values(_ckeditor5_get_langcode_mapping());
+    if ($extension === 'core') {
+      // Generate libraries for each of the CKEditor 5 translation files so that
+      // the correct translation file can be attached depending on the current
+      // language. This makes sure that caching caches the appropriate language.
+      // Only create libraries for languages that have a mapping to Drupal.
+      foreach ($ckeditor_langcodes as $langcode) {
+        $libraries['ckeditor5.translations.' . $langcode] = [
+          'remote' => $libraries['ckeditor5']['remote'],
+          'version' => $libraries['ckeditor5']['version'],
+          'license' => $libraries['ckeditor5']['license'],
+          'dependencies' => [
+            'core/ckeditor5',
+            'core/ckeditor5.translations',
+          ],
+        ];
+      }
+    }
+    // Copied from \Drupal\Core\Asset\LibraryDiscoveryParser::buildByExtension().
+    if ($extension === 'core') {
+      $path = 'core';
+    }
+    else {
+      if ($moduleHandler->moduleExists($extension)) {
+        $extension_type = 'module';
+      }
+      else {
+        $extension_type = 'theme';
+      }
+      $path = \Drupal::getContainer()->get('extension.path.resolver')->getPath($extension_type, $extension);
+    }
+    foreach ($libraries as &$library) {
+      // The way to know if a library has a translation is to depend on the
+      // special "core/ckeditor5.translations" library.
+      if (empty($library['js']) || empty($library['dependencies']) || !in_array('core/ckeditor5.translations', $library['dependencies'])) {
+        continue;
+      }
+      foreach ($library['js'] as $file => $options) {
+        // Only look for translations on libraries defined with a relative path.
+        if (!empty($options['type']) && $options['type'] === 'external') {
+          continue;
+        }
+        // Path relative to the current extension folder.
+        $dirname = dirname($file);
+        // Path of the folder in the filesystem relative to the Drupal root.
+        $dir = $path . '/' . $dirname;
+        // Exclude protocol-free URI.
+        if (str_starts_with($dirname, '//')) {
+          continue;
+        }
+        // CKEditor 5 plugins are most likely added through composer and
+        // installed in the module exposing it. Suppose the file path is
+        // relative to the module and not in the /libraries/ folder.
+        // Collect translations based on filename, and add all existing
+        // translations files to the plugin library. Unnecessary translations
+        // will be filtered in ckeditor5_js_alter() hook.
+        $files = scandir("{$dir}/translations");
+        foreach ($files as $file) {
+          if (str_ends_with($file, '.js')) {
+            $langcode = basename($file, '.js');
+            // Only add languages that Drupal can understands.
+            if (in_array($langcode, $ckeditor_langcodes)) {
+              $library['js']["{$dirname}/translations/{$langcode}.js"] = ['ckeditor5_langcode' => $langcode, 'minified' => TRUE, 'preprocess' => TRUE];
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_js_alter().
+   */
+  #[Hook('js_alter')]
+  public function jsAlter(&$javascript, AttachedAssetsInterface $assets, LanguageInterface $language) {
+    // This file means CKEditor 5 translations are in use on the page.
+    // @see locale_js_alter()
+    $placeholder_file = 'core/assets/vendor/ckeditor5/translation.js';
+    // This file is used to get a weight that will make it possible to aggregate
+    // all translation files in a single aggregate.
+    $ckeditor_dll_file = 'core/assets/vendor/ckeditor5/ckeditor5-dll/ckeditor5-dll.js';
+    if (isset($javascript[$placeholder_file])) {
+      // Use the placeholder file weight to set all the translations files weights
+      // so they can be aggregated together as expected.
+      $default_weight = $javascript[$placeholder_file]['weight'];
+      if (isset($javascript[$ckeditor_dll_file])) {
+        $default_weight = $javascript[$ckeditor_dll_file]['weight'];
+      }
+      // The placeholder file is not a real file, remove it from the list.
+      unset($javascript[$placeholder_file]);
+      // When the locale module isn't installed there are no translations.
+      if (!\Drupal::moduleHandler()->moduleExists('locale')) {
+        return;
+      }
+      $ckeditor5_language = _ckeditor5_get_langcode_mapping($language->getId());
+      // Remove all CKEditor 5 translations files that are not in the current
+      // language.
+      foreach ($javascript as $index => &$item) {
+        // This is not a CKEditor 5 translation file, skip it.
+        if (empty($item['ckeditor5_langcode'])) {
+          continue;
+        }
+        // This file is the correct translation for this page.
+        if ($item['ckeditor5_langcode'] === $ckeditor5_language) {
+          // Set the weight for the translation file to be able to have the
+          // translation files aggregated.
+          $item['weight'] = $default_weight;
+        }
+        else {
+          // Remove files that don't match the language requested.
+          unset($javascript[$index]);
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_config_schema_info_alter().
+   */
+  #[Hook('config_schema_info_alter')]
+  public function configSchemaInfoAlter(&$definitions) {
+    // In \Drupal\Tests\config\Functional\ConfigImportAllTest, this hook may be
+    // called without ckeditor5.pair.schema.yml being active.
+    if (!isset($definitions['ckeditor5_valid_pair__format_and_editor'])) {
+      return;
+    }
+    // @see filter.format.*.filters
+    $definitions['ckeditor5_valid_pair__format_and_editor']['mapping']['filters'] = $definitions['filter.format.*']['mapping']['filters'];
+    // @see @see editor.editor.*.image_upload
+    $definitions['ckeditor5_valid_pair__format_and_editor']['mapping']['image_upload'] = $definitions['editor.editor.*']['mapping']['image_upload'];
+  }
+
+}
diff --git a/core/modules/ckeditor5/tests/modules/ckeditor5_read_only_mode/ckeditor5_read_only_mode.module b/core/modules/ckeditor5/tests/modules/ckeditor5_read_only_mode/ckeditor5_read_only_mode.module
deleted file mode 100644
index c1702fbbc0672923834ee66e0736bfbba97a493c..0000000000000000000000000000000000000000
--- a/core/modules/ckeditor5/tests/modules/ckeditor5_read_only_mode/ckeditor5_read_only_mode.module
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-/**
- * @file
- * Implements hooks for the CKEditor 5 read-only mode test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function ckeditor5_read_only_mode_form_node_page_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
-  $form['body']['#disabled'] = \Drupal::state()->get('ckeditor5_read_only_mode_body_enabled', FALSE);
-  $form['field_second_ckeditor5_field']['#disabled'] = \Drupal::state()->get('ckeditor5_read_only_mode_second_ckeditor5_field_enabled', FALSE);
-}
diff --git a/core/modules/ckeditor5/tests/modules/ckeditor5_read_only_mode/src/Hook/Ckeditor5ReadOnlyModeHooks.php b/core/modules/ckeditor5/tests/modules/ckeditor5_read_only_mode/src/Hook/Ckeditor5ReadOnlyModeHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..00507324e61c8de4c109d3e036698b261b323332
--- /dev/null
+++ b/core/modules/ckeditor5/tests/modules/ckeditor5_read_only_mode/src/Hook/Ckeditor5ReadOnlyModeHooks.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\ckeditor5_read_only_mode\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for ckeditor5_read_only_mode.
+ */
+class Ckeditor5ReadOnlyModeHooks {
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_node_page_form_alter')]
+  public function formNodePageFormAlter(array &$form, FormStateInterface $form_state, string $form_id) : void {
+    $form['body']['#disabled'] = \Drupal::state()->get('ckeditor5_read_only_mode_body_enabled', FALSE);
+    $form['field_second_ckeditor5_field']['#disabled'] = \Drupal::state()->get('ckeditor5_read_only_mode_second_ckeditor5_field_enabled', FALSE);
+  }
+
+}
diff --git a/core/modules/ckeditor5/tests/modules/ckeditor5_test_module_allowed_image/ckeditor5_test_module_allowed_image.module b/core/modules/ckeditor5/tests/modules/ckeditor5_test_module_allowed_image/ckeditor5_test_module_allowed_image.module
deleted file mode 100644
index 49269eecc2f732334a609954fa5e0f5fccf0a79c..0000000000000000000000000000000000000000
--- a/core/modules/ckeditor5/tests/modules/ckeditor5_test_module_allowed_image/ckeditor5_test_module_allowed_image.module
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-/**
- * @file
- * A module to add a custom image type for CKEditor 5.
- */
-
-declare(strict_types=1);
-
-use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
-
-/**
- * Implements hook_ckeditor5_plugin_info_alter().
- */
-function ckeditor5_test_module_allowed_image_ckeditor5_plugin_info_alter(array &$plugin_definitions): void {
-  // Add a custom file type to the image upload plugin. Note that 'svg+xml'
-  // below should be an IANA image media type Name, with the "image/" prefix
-  // omitted. In other words: a subtype of type image.
-  // @see https://www.iana.org/assignments/media-types/media-types.xhtml#image
-  // @see https://ckeditor.com/docs/ckeditor5/latest/api/module_image_imageconfig-ImageUploadConfig.html#member-types
-  $image_upload_plugin_definition = $plugin_definitions['ckeditor5_imageUpload']->toArray();
-  $image_upload_plugin_definition['ckeditor5']['config']['image']['upload']['types'][] = 'svg+xml';
-  $plugin_definitions['ckeditor5_imageUpload'] = new CKEditor5PluginDefinition($image_upload_plugin_definition);
-}
diff --git a/core/modules/ckeditor5/tests/modules/ckeditor5_test_module_allowed_image/src/Hook/Ckeditor5TestModuleAllowedImageHooks.php b/core/modules/ckeditor5/tests/modules/ckeditor5_test_module_allowed_image/src/Hook/Ckeditor5TestModuleAllowedImageHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..21ea4cfe2ecc6b6195c3b6e134bfacf37692cf3a
--- /dev/null
+++ b/core/modules/ckeditor5/tests/modules/ckeditor5_test_module_allowed_image/src/Hook/Ckeditor5TestModuleAllowedImageHooks.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\ckeditor5_test_module_allowed_image\Hook;
+
+use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for ckeditor5_test_module_allowed_image.
+ */
+class Ckeditor5TestModuleAllowedImageHooks {
+
+  /**
+   * Implements hook_ckeditor5_plugin_info_alter().
+   */
+  #[Hook('ckeditor5_plugin_info_alter')]
+  public function ckeditor5PluginInfoAlter(array &$plugin_definitions) : void {
+    // Add a custom file type to the image upload plugin. Note that 'svg+xml'
+    // below should be an IANA image media type Name, with the "image/" prefix
+    // omitted. In other words: a subtype of type image.
+    // @see https://www.iana.org/assignments/media-types/media-types.xhtml#image
+    // @see https://ckeditor.com/docs/ckeditor5/latest/api/module_image_imageconfig-ImageUploadConfig.html#member-types
+    $image_upload_plugin_definition = $plugin_definitions['ckeditor5_imageUpload']->toArray();
+    $image_upload_plugin_definition['ckeditor5']['config']['image']['upload']['types'][] = 'svg+xml';
+    $plugin_definitions['ckeditor5_imageUpload'] = new CKEditor5PluginDefinition($image_upload_plugin_definition);
+  }
+
+}
diff --git a/core/modules/ckeditor5/tests/modules/ckeditor_test/ckeditor_test.module b/core/modules/ckeditor5/tests/modules/ckeditor_test/ckeditor_test.module
deleted file mode 100644
index 452f4039670f8fd46f4cd4d418199a5f6dae65cc..0000000000000000000000000000000000000000
--- a/core/modules/ckeditor5/tests/modules/ckeditor_test/ckeditor_test.module
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-/**
- * @file
- * Implements hooks for the CKEditor test module.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_editor_info_alter().
- */
-function ckeditor_test_editor_info_alter(array &$editors) {
-  // Drupal 9 used to have an editor called ckeditor. Copy the Unicorn editor to
-  // it to be able to test upgrading to CKEditor 5.
-  $editors['ckeditor'] = $editors['unicorn'];
-}
diff --git a/core/modules/ckeditor5/tests/modules/ckeditor_test/src/Hook/CkeditorTestHooks.php b/core/modules/ckeditor5/tests/modules/ckeditor_test/src/Hook/CkeditorTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..1b82c66b05b596aa035f36c66be4f9141c76742b
--- /dev/null
+++ b/core/modules/ckeditor5/tests/modules/ckeditor_test/src/Hook/CkeditorTestHooks.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\ckeditor_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for ckeditor_test.
+ */
+class CkeditorTestHooks {
+
+  /**
+   * Implements hook_editor_info_alter().
+   */
+  #[Hook('editor_info_alter')]
+  public function editorInfoAlter(array &$editors) {
+    // Drupal 9 used to have an editor called ckeditor. Copy the Unicorn editor to
+    // it to be able to test upgrading to CKEditor 5.
+    $editors['ckeditor'] = $editors['unicorn'];
+  }
+
+}
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 0c9c96e0c402b86c03136be40c6deb76c59b9d0c..fc3b898dfb334edbcf4ba1388035fd6bb27cee31 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -2,31 +2,14 @@
 
 /**
  * @file
- * Enables users to comment on published content.
- *
- * When installed, the Comment module creates a field that facilitates a
- * discussion board for each Drupal entity to which a comment field is attached.
- * Users can post comments to discuss a story, user etc.
  */
 
 use Drupal\comment\CommentInterface;
-use Drupal\comment\Entity\CommentType;
-use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
-use Drupal\Core\Entity\Entity\EntityViewMode;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Link;
 use Drupal\Core\Url;
-use Drupal\field\FieldConfigInterface;
-use Drupal\field\FieldStorageConfigInterface;
-use Drupal\node\NodeInterface;
-use Drupal\user\RoleInterface;
-use Drupal\user\UserInterface;
 
 /**
  * The time cutoff for comments marked as read for entity types other node.
@@ -39,35 +22,6 @@
  */
 define('COMMENT_NEW_LIMIT', ((int) $_SERVER['REQUEST_TIME']) - 30 * 24 * 60 * 60);
 
-/**
- * Implements hook_help().
- */
-function comment_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.comment':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Comment module allows users to comment on site content, set commenting defaults and permissions, and moderate comments. For more information, see the <a href=":comment">online documentation for the Comment module</a>.', [':comment' => 'https://www.drupal.org/documentation/modules/comment']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Enabling commenting') . '</dt>';
-      $output .= '<dd>' . t('Comment functionality can be enabled for any entity sub-type (for example, a <a href=":content-type">content type</a>) by adding a <em>Comments</em> field on its <em>Manage fields page</em>. Adding or removing commenting for an entity through the user interface requires the <a href=":field_ui">Field UI</a> module to be installed, even though the commenting functionality works without it. For more information on fields and entities, see the <a href=":field">Field module help page</a>.', [':content-type' => (\Drupal::moduleHandler()->moduleExists('node')) ? Url::fromRoute('entity.node_type.collection')->toString() : '#', ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Configuring commenting settings') . '</dt>';
-      $output .= '<dd>' . t('Commenting settings can be configured by editing the <em>Comments</em> field on the <em>Manage fields page</em> of an entity type if the <em>Field UI module</em> is installed. Configuration includes the label of the comments field, the number of comments to be displayed, and whether they are shown in threaded list. Commenting can be configured as: <em>Open</em> to allow new comments, <em>Closed</em> to view existing comments, but prevent new comments, or <em>Hidden</em> to hide existing comments and prevent new comments. Changing this configuration for an entity type will not change existing entity items.') . '</dd>';
-      $output .= '<dt>' . t('Overriding default settings') . '</dt>';
-      $output .= '<dd>' . t('Users with the appropriate permissions can override the default commenting settings of an entity type when they create an item of that type.') . '</dd>';
-      $output .= '<dt>' . t('Adding comment types') . '</dt>';
-      $output .= '<dd>' . t('Additional <em>comment types</em> can be created per entity sub-type and added on the <a href=":field">Comment types page</a>. If there are multiple comment types available you can select the appropriate one after adding a <em>Comments field</em>.', [':field' => Url::fromRoute('entity.comment_type.collection')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Approving and managing comments') . '</dt>';
-      $output .= '<dd>' . t('Comments from users who have the <em>Skip comment approval</em> permission are published immediately. All other comments are placed in the <a href=":comment-approval">Unapproved comments</a> queue, until a user who has permission to <em>Administer comments and comment settings</em> publishes or deletes them. Published comments can be bulk managed on the <a href=":admin-comment">Published comments</a> administration page. When a comment has no replies, it remains editable by its author, as long as the author has <em>Edit own comments</em> permission.', [':comment-approval' => Url::fromRoute('comment.admin_approval')->toString(), ':admin-comment' => Url::fromRoute('comment.admin')->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'entity.comment_type.collection':
-      $output = '<p>' . t('This page provides a list of all comment types on the site and allows you to manage the fields, form and display settings for each.') . '</p>';
-      return $output;
-  }
-}
-
 /**
  * Entity URI callback.
  */
@@ -81,258 +35,6 @@ function comment_uri(CommentInterface $comment) {
   );
 }
 
-/**
- * Implements hook_entity_extra_field_info().
- */
-function comment_entity_extra_field_info() {
-  $return = [];
-  foreach (CommentType::loadMultiple() as $comment_type) {
-    $return['comment'][$comment_type->id()] = [
-      'form' => [
-        'author' => [
-          'label' => t('Author'),
-          'description' => t('Author textfield'),
-          'weight' => -2,
-        ],
-      ],
-    ];
-    $return['comment'][$comment_type->id()]['display']['links'] = [
-      'label' => t('Links'),
-      'description' => t('Comment operation links'),
-      'weight' => 100,
-      'visible' => TRUE,
-    ];
-  }
-
-  return $return;
-}
-
-/**
- * Implements hook_theme().
- */
-function comment_theme(): array {
-  return [
-    'comment' => [
-      'render element' => 'elements',
-    ],
-    'field__comment' => [
-      'base hook' => 'field',
-    ],
-  ];
-}
-
-/**
- * Implements hook_ENTITY_TYPE_create() for 'field_config'.
- */
-function comment_field_config_create(FieldConfigInterface $field) {
-  if ($field->getType() == 'comment' && !$field->isSyncing()) {
-    // Assign default values for the field.
-    $default_value = $field->getDefaultValueLiteral();
-    $default_value += [[]];
-    $default_value[0] += [
-      'status' => CommentItemInterface::OPEN,
-      'cid' => 0,
-      'last_comment_timestamp' => 0,
-      'last_comment_name' => '',
-      'last_comment_uid' => 0,
-      'comment_count' => 0,
-    ];
-    $field->setDefaultValue($default_value);
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for 'field_config'.
- */
-function comment_field_config_update(FieldConfigInterface $field) {
-  if ($field->getType() == 'comment') {
-    // Comment field settings also affects the rendering of *comment* entities,
-    // not only the *commented* entities.
-    \Drupal::entityTypeManager()->getViewBuilder('comment')->resetCache();
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for 'field_storage_config'.
- */
-function comment_field_storage_config_insert(FieldStorageConfigInterface $field_storage) {
-  if ($field_storage->getType() == 'comment') {
-    // Check that the target entity type uses an integer ID.
-    $entity_type_id = $field_storage->getTargetEntityTypeId();
-    if (!_comment_entity_uses_integer_id($entity_type_id)) {
-      throw new \UnexpectedValueException('You cannot attach a comment field to an entity with a non-integer ID field');
-    }
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for 'field_config'.
- */
-function comment_field_config_delete(FieldConfigInterface $field) {
-  if ($field->getType() == 'comment') {
-    // Delete all comments that used by the entity bundle.
-    $entity_query = \Drupal::entityQuery('comment')->accessCheck(FALSE);
-    $entity_query->condition('entity_type', $field->getEntityTypeId());
-    $entity_query->condition('field_name', $field->getName());
-    $cids = $entity_query->execute();
-    $comment_storage = \Drupal::entityTypeManager()->getStorage('comment');
-    $comments = $comment_storage->loadMultiple($cids);
-    $comment_storage->delete($comments);
-  }
-}
-
-/**
- * Implements hook_node_links_alter().
- */
-function comment_node_links_alter(array &$links, NodeInterface $node, array &$context) {
-  // Comment links are only added to node entity type for backwards
-  // compatibility. Should you require comment links for other entity types you
-  // can do so by implementing a new field formatter.
-  // @todo Make this configurable from the formatter. See
-  //   https://www.drupal.org/node/1901110.
-
-  $comment_links = \Drupal::service('comment.link_builder')->buildCommentedEntityLinks($node, $context);
-  $links += $comment_links;
-}
-
-/**
- * Implements hook_entity_view().
- */
-function comment_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
-  if ($entity instanceof FieldableEntityInterface && $view_mode == 'rss' && $display->getComponent('links')) {
-    /** @var \Drupal\comment\CommentManagerInterface $comment_manager */
-    $comment_manager = \Drupal::service('comment.manager');
-    $fields = $comment_manager->getFields($entity->getEntityTypeId());
-    foreach ($fields as $field_name => $detail) {
-      if ($entity->hasField($field_name) && $entity->get($field_name)->status != CommentItemInterface::HIDDEN) {
-        // Add a comments RSS element which is a URL to the comments of this
-        // entity.
-        $options = [
-          'fragment' => 'comments',
-          'absolute' => TRUE,
-        ];
-        $entity->rss_elements[] = [
-          'key' => 'comments',
-          'value' => $entity->toUrl('canonical', $options)->toString(),
-        ];
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_view_alter() for node entities.
- */
-function comment_node_view_alter(array &$build, EntityInterface $node, EntityViewDisplayInterface $display) {
-  if (\Drupal::moduleHandler()->moduleExists('history')) {
-    $build['#attributes']['data-history-node-id'] = $node->id();
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for field_ui_field_storage_add_form.
- */
-function comment_form_field_ui_field_storage_add_form_alter(&$form, FormStateInterface $form_state): void {
-  $route_match = \Drupal::routeMatch();
-  if ($form_state->get('entity_type_id') == 'comment' && $route_match->getParameter('commented_entity_type')) {
-    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name'));
-  }
-}
-
-/**
- * Implements hook_field_info_entity_type_ui_definitions_alter().
- */
-function comment_field_info_entity_type_ui_definitions_alter(array &$ui_definitions, string $entity_type_id) {
-  if (!_comment_entity_uses_integer_id($entity_type_id)) {
-    unset($ui_definitions['comment']);
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function comment_form_field_ui_form_display_overview_form_alter(&$form, FormStateInterface $form_state): void {
-  $route_match = \Drupal::routeMatch();
-  if ($form['#entity_type'] == 'comment' && $route_match->getParameter('commented_entity_type')) {
-    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name'));
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function comment_form_field_ui_display_overview_form_alter(&$form, FormStateInterface $form_state): void {
-  $route_match = \Drupal::routeMatch();
-  if ($form['#entity_type'] == 'comment' && $route_match->getParameter('commented_entity_type')) {
-    $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name'));
-  }
-}
-
-/**
- * Implements hook_entity_storage_load().
- *
- * @see \Drupal\comment\Plugin\Field\FieldType\CommentItem::propertyDefinitions()
- */
-function comment_entity_storage_load($entities, $entity_type) {
-  // Comments can only be attached to content entities, so skip others.
-  if (!\Drupal::entityTypeManager()->getDefinition($entity_type)->entityClassImplements(FieldableEntityInterface::class)) {
-    return;
-  }
-  if (!\Drupal::service('comment.manager')->getFields($entity_type)) {
-    // Do not query database when entity has no comment fields.
-    return;
-  }
-  // Load comment information from the database and update the entity's
-  // comment statistics properties, which are defined on each CommentItem field.
-  $result = \Drupal::service('comment.statistics')->read($entities, $entity_type);
-  foreach ($result as $record) {
-    // Skip fields that entity does not have.
-    if (!$entities[$record->entity_id]->hasField($record->field_name)) {
-      continue;
-    }
-    $comment_statistics = $entities[$record->entity_id]->get($record->field_name);
-    $comment_statistics->cid = $record->cid;
-    $comment_statistics->last_comment_timestamp = $record->last_comment_timestamp;
-    $comment_statistics->last_comment_name = $record->last_comment_name;
-    $comment_statistics->last_comment_uid = $record->last_comment_uid;
-    $comment_statistics->comment_count = $record->comment_count;
-  }
-}
-
-/**
- * Implements hook_entity_insert().
- */
-function comment_entity_insert(EntityInterface $entity) {
-  // Allow bulk updates and inserts to temporarily disable the
-  // maintenance of the {comment_entity_statistics} table.
-  if (\Drupal::state()->get('comment.maintain_entity_statistics') &&
-    $fields = \Drupal::service('comment.manager')->getFields($entity->getEntityTypeId())) {
-    \Drupal::service('comment.statistics')->create($entity, $fields);
-  }
-}
-
-/**
- * Implements hook_entity_predelete().
- */
-function comment_entity_predelete(EntityInterface $entity) {
-  // Entities can have non-numeric IDs, but {comment} and
-  // {comment_entity_statistics} tables have integer columns for entity ID, and
-  // PostgreSQL throws exceptions if you attempt query conditions with
-  // mismatched types. So, we need to verify that the ID is numeric (even for an
-  // entity type that has an integer ID, $entity->id() might be a string
-  // containing a number), and then cast it to an integer when querying.
-  if ($entity instanceof FieldableEntityInterface && is_numeric($entity->id())) {
-    $entity_query = \Drupal::entityQuery('comment')->accessCheck(FALSE);
-    $entity_query->condition('entity_id', (int) $entity->id());
-    $entity_query->condition('entity_type', $entity->getEntityTypeId());
-    $cids = $entity_query->execute();
-    $comment_storage = \Drupal::entityTypeManager()->getStorage('comment');
-    $comments = $comment_storage->loadMultiple($cids);
-    $comment_storage->delete($comments);
-    \Drupal::service('comment.statistics')->delete($entity);
-  }
-}
-
 /**
  * Determines if an entity type is using an integer-based ID definition.
  *
@@ -354,143 +56,6 @@ function _comment_entity_uses_integer_id($entity_type_id) {
   return $entity_type_id_definition->getType() === 'integer';
 }
 
-/**
- * Implements hook_node_update_index().
- */
-function comment_node_update_index(EntityInterface $node) {
-  $index_comments = &drupal_static(__FUNCTION__);
-
-  if ($index_comments === NULL) {
-    // Do not index in the following three cases:
-    // 1. 'Authenticated user' can search content but can't access comments.
-    // 2. 'Anonymous user' can search content but can't access comments.
-    // 3. Any role can search content but can't access comments and access
-    // comments is not granted by the 'authenticated user' role. In this case
-    // all users might have both permissions from various roles but it is also
-    // possible to set up a user to have only search content and so a user
-    // edit could change the security situation so it is not safe to index the
-    // comments.
-    $index_comments = TRUE;
-    $roles = \Drupal::entityTypeManager()->getStorage('user_role')->loadMultiple();
-    $authenticated_can_access = $roles[RoleInterface::AUTHENTICATED_ID]->hasPermission('access comments');
-    foreach ($roles as $rid => $role) {
-      if ($role->hasPermission('search content') && !$role->hasPermission('access comments')) {
-        if ($rid == RoleInterface::AUTHENTICATED_ID || $rid == RoleInterface::ANONYMOUS_ID || !$authenticated_can_access) {
-          $index_comments = FALSE;
-          break;
-        }
-      }
-    }
-  }
-
-  $build = [];
-
-  if ($index_comments) {
-    foreach (\Drupal::service('comment.manager')->getFields('node') as $field_name => $info) {
-      // Skip fields that entity does not have.
-      if (!$node->hasField($field_name)) {
-        continue;
-      }
-      $field_definition = $node->getFieldDefinition($field_name);
-      $mode = $field_definition->getSetting('default_mode');
-      $comments_per_page = $field_definition->getSetting('per_page');
-      if ($node->get($field_name)->status) {
-        $comments = \Drupal::entityTypeManager()->getStorage('comment')
-          ->loadThread($node, $field_name, $mode, $comments_per_page);
-        if ($comments) {
-          $build[] = \Drupal::entityTypeManager()->getViewBuilder('comment')->viewMultiple($comments);
-        }
-      }
-    }
-  }
-  return \Drupal::service('renderer')->renderInIsolation($build);
-}
-
-/**
- * Implements hook_cron().
- */
-function comment_cron() {
-  // Store the maximum possible comments per thread (used for node search
-  // ranking by reply count).
-  \Drupal::state()->set('comment.node_comment_statistics_scale', 1.0 / max(1, \Drupal::service('comment.statistics')->getMaximumCount('node')));
-}
-
-/**
- * Implements hook_node_search_result().
- *
- * Formats a comment count string and returns it, for display with search
- * results.
- */
-function comment_node_search_result(EntityInterface $node) {
-  $comment_fields = \Drupal::service('comment.manager')->getFields('node');
-  $comments = 0;
-  $open = FALSE;
-  foreach ($comment_fields as $field_name => $info) {
-    // Skip fields that entity does not have.
-    if (!$node->hasField($field_name)) {
-      continue;
-    }
-    // Do not make a string if comments are hidden.
-    $status = $node->get($field_name)->status;
-    if (\Drupal::currentUser()->hasPermission('access comments') && $status != CommentItemInterface::HIDDEN) {
-      if ($status == CommentItemInterface::OPEN) {
-        // At least one comment field is open.
-        $open = TRUE;
-      }
-      $comments += $node->get($field_name)->comment_count;
-    }
-  }
-  // Do not make a string if there are no comment fields, or no comments exist
-  // or all comment fields are hidden.
-  if ($comments > 0 || $open) {
-    return ['comment' => \Drupal::translation()->formatPlural($comments, '1 comment', '@count comments')];
-  }
-}
-
-/**
- * Implements hook_user_cancel().
- */
-function comment_user_cancel($edit, UserInterface $account, $method) {
-  switch ($method) {
-    case 'user_cancel_block_unpublish':
-      $comments = \Drupal::entityTypeManager()->getStorage('comment')->loadByProperties(['uid' => $account->id()]);
-      foreach ($comments as $comment) {
-        $comment->setUnpublished();
-        $comment->save();
-      }
-      break;
-
-    case 'user_cancel_reassign':
-      /** @var \Drupal\comment\CommentInterface[] $comments */
-      $comments = \Drupal::entityTypeManager()->getStorage('comment')->loadByProperties(['uid' => $account->id()]);
-      foreach ($comments as $comment) {
-        $langcodes = array_keys($comment->getTranslationLanguages());
-        // For efficiency manually save the original comment before applying any
-        // changes.
-        $comment->original = clone $comment;
-        foreach ($langcodes as $langcode) {
-          $comment_translated = $comment->getTranslation($langcode);
-          $comment_translated->setOwnerId(0);
-          $comment_translated->setAuthorName(\Drupal::config('user.settings')->get('anonymous'));
-        }
-        $comment->save();
-      }
-      break;
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for user entities.
- */
-function comment_user_predelete($account) {
-  $entity_query = \Drupal::entityQuery('comment')->accessCheck(FALSE);
-  $entity_query->condition('uid', $account->id());
-  $cids = $entity_query->execute();
-  $comment_storage = \Drupal::entityTypeManager()->getStorage('comment');
-  $comments = $comment_storage->loadMultiple($cids);
-  $comment_storage->delete($comments);
-}
-
 /**
  * Generates a comment preview.
  *
@@ -728,59 +293,3 @@ function comment_preprocess_field(&$variables) {
     $variables['comment_form'] = $element[0]['comment_form'];
   }
 }
-
-/**
- * Implements hook_ranking().
- */
-function comment_ranking() {
-  return \Drupal::service('comment.statistics')->getRankingInfo();
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for entity_view_display entities.
- */
-function comment_entity_view_display_presave(EntityViewDisplayInterface $display) {
-  // Act only on comment view displays being disabled.
-  if ($display->isNew() || $display->getTargetEntityTypeId() !== 'comment' || $display->status()) {
-    return;
-  }
-  $storage = \Drupal::entityTypeManager()->getStorage('entity_view_display');
-  if (!$storage->loadUnchanged($display->getOriginalId())->status()) {
-    return;
-  }
-
-  // Disable the comment field formatter when the used view display is disabled.
-  foreach ($storage->loadMultiple() as $view_display) {
-    $changed = FALSE;
-    /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $view_display */
-    foreach ($view_display->getComponents() as $field => $component) {
-      if (isset($component['type']) && ($component['type'] === 'comment_default')) {
-        if ($component['settings']['view_mode'] === $display->getMode()) {
-          $view_display->removeComponent($field);
-          /** @var \Drupal\Core\Entity\EntityViewModeInterface $mode */
-          $mode = EntityViewMode::load($display->getTargetEntityTypeId() . '.' . $display->getMode());
-          $arguments = [
-            '@id' => $view_display->id(),
-            '@name' => $field,
-            '@display' => $mode->label(),
-            '@mode' => $display->getMode(),
-          ];
-          \Drupal::logger('system')->warning("View display '@id': Comment field formatter '@name' was disabled because it is using the comment view display '@display' (@mode) that was just disabled.", $arguments);
-          $changed = TRUE;
-        }
-      }
-    }
-    if ($changed) {
-      $view_display->save();
-    }
-  }
-}
-
-/**
- * Implements hook_field_type_category_info_alter().
- */
-function comment_field_type_category_info_alter(&$definitions) {
-  // The `comment` field type belongs in the `general` category, so the
-  // libraries need to be attached using an alter hook.
-  $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'comment/drupal.comment-icon';
-}
diff --git a/core/modules/comment/comment.tokens.inc b/core/modules/comment/comment.tokens.inc
deleted file mode 100644
index 946cf0d021751eb06963306fcd7859d5cd946f1f..0000000000000000000000000000000000000000
--- a/core/modules/comment/comment.tokens.inc
+++ /dev/null
@@ -1,286 +0,0 @@
-<?php
-
-/**
- * @file
- * Builds placeholder replacement tokens for comment-related data.
- */
-
-use Drupal\Component\Utility\UrlHelper;
-use Drupal\Core\Datetime\Entity\DateFormat;
-use Drupal\Core\Entity\ContentEntityInterface;
-use Drupal\Core\Entity\FieldableEntityInterface;
-use Drupal\Core\Render\BubbleableMetadata;
-
-/**
- * Implements hook_token_info().
- */
-function comment_token_info() {
-  $type = [
-    'name' => t('Comments'),
-    'description' => t('Tokens for comments posted on the site.'),
-    'needs-data' => 'comment',
-  ];
-
-  $tokens = [];
-  // Provides an integration for each entity type except comment.
-  foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
-    if ($entity_type_id == 'comment' || !$entity_type->entityClassImplements(ContentEntityInterface::class)) {
-      continue;
-    }
-
-    if (\Drupal::service('comment.manager')->getFields($entity_type_id)) {
-      // Get the correct token type.
-      $token_type = ($entity_type_id == 'taxonomy_term') ? 'term' : $entity_type_id;
-
-      // @todo Make this work per field. See https://www.drupal.org/node/2031903.
-      $tokens[$token_type]['comment-count'] = [
-        'name' => t("Comment count"),
-        'description' => t("The number of comments posted on an entity."),
-      ];
-      $tokens[$token_type]['comment-count-new'] = [
-        'name' => t("New comment count"),
-        'description' => t("The number of comments posted on an entity since the reader last viewed it."),
-      ];
-    }
-  }
-
-  // Core comment tokens
-  $comment['cid'] = [
-    'name' => t("Comment ID"),
-    'description' => t("The unique ID of the comment."),
-  ];
-  $comment['uuid'] = [
-    'name' => t('UUID'),
-    'description' => t("The UUID of the comment."),
-  ];
-  $comment['hostname'] = [
-    'name' => t("IP Address"),
-    'description' => t("The IP address of the computer the comment was posted from."),
-  ];
-  $comment['mail'] = [
-    'name' => t("Email address"),
-    'description' => t("The email address left by the comment author."),
-  ];
-  $comment['homepage'] = [
-    'name' => t("Home page"),
-    'description' => t("The home page URL left by the comment author."),
-  ];
-  $comment['title'] = [
-    'name' => t("Title"),
-    'description' => t("The title of the comment."),
-  ];
-  $comment['body'] = [
-    'name' => t("Content"),
-    'description' => t("The formatted content of the comment itself."),
-  ];
-  $comment['langcode'] = [
-    'name' => t('Language code'),
-    'description' => t('The language code of the language the comment is written in.'),
-  ];
-  $comment['url'] = [
-    'name' => t("URL"),
-    'description' => t("The URL of the comment."),
-  ];
-  $comment['edit-url'] = [
-    'name' => t("Edit URL"),
-    'description' => t("The URL of the comment's edit page."),
-  ];
-
-  // Chained tokens for comments
-  $comment['created'] = [
-    'name' => t("Date created"),
-    'description' => t("The date the comment was posted."),
-    'type' => 'date',
-  ];
-  $comment['changed'] = [
-    'name' => t("Date changed"),
-    'description' => t("The date the comment was most recently updated."),
-    'type' => 'date',
-  ];
-  $comment['parent'] = [
-    'name' => t("Parent"),
-    'description' => t("The comment's parent, if comment threading is active."),
-    'type' => 'comment',
-  ];
-  $comment['entity'] = [
-    'name' => t("Entity"),
-    'description' => t("The entity the comment was posted to."),
-    'type' => 'entity',
-  ];
-  $comment['author'] = [
-    'name' => t("Author"),
-    'description' => t("The author name of the comment."),
-    'type' => 'user',
-  ];
-
-  return [
-    'types' => ['comment' => $type],
-    'tokens' => [
-      'comment' => $comment,
-    ] + $tokens,
-  ];
-}
-
-/**
- * Implements hook_tokens().
- */
-function comment_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
-  $token_service = \Drupal::token();
-
-  $url_options = ['absolute' => TRUE];
-  if (isset($options['langcode'])) {
-    $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
-    $langcode = $options['langcode'];
-  }
-  else {
-    $langcode = NULL;
-  }
-  $replacements = [];
-
-  if ($type == 'comment' && !empty($data['comment'])) {
-    /** @var \Drupal\comment\CommentInterface $comment */
-    $comment = $data['comment'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Simple key values on the comment.
-        case 'cid':
-          $replacements[$original] = $comment->id();
-          break;
-
-        case 'uuid':
-          $replacements[$original] = $comment->uuid();
-          break;
-
-        // Poster identity information for comments.
-        case 'hostname':
-          $replacements[$original] = $comment->getHostname();
-          break;
-
-        case 'mail':
-          $mail = $comment->getAuthorEmail();
-          // Add the user cacheability metadata in case the author of the comment
-          // is not the anonymous user.
-          if ($comment->getOwnerId()) {
-            $bubbleable_metadata->addCacheableDependency($comment->getOwner());
-          }
-          $replacements[$original] = $mail;
-          break;
-
-        case 'homepage':
-          $replacements[$original] = UrlHelper::stripDangerousProtocols($comment->getHomepage());
-          break;
-
-        case 'title':
-          $replacements[$original] = $comment->getSubject();
-          break;
-
-        case 'body':
-          // "processed" returns a \Drupal\Component\Render\MarkupInterface via
-          // check_markup().
-          $replacements[$original] = $comment->comment_body->processed;
-          break;
-
-        case 'langcode':
-          $replacements[$original] = $comment->language()->getId();
-          break;
-
-        // Comment related URLs.
-        case 'url':
-          $url_options['fragment'] = 'comment-' . $comment->id();
-          $replacements[$original] = $comment->toUrl('canonical', $url_options)->toString();
-          break;
-
-        case 'edit-url':
-          $url_options['fragment'] = NULL;
-          $replacements[$original] = $comment->toUrl('edit-form', $url_options)->toString();
-          break;
-
-        case 'author':
-          $name = $comment->getAuthorName();
-          // Add the user cacheability metadata in case the author of the comment
-          // is not the anonymous user.
-          if ($comment->getOwnerId()) {
-            $bubbleable_metadata->addCacheableDependency($comment->getOwner());
-          }
-          $replacements[$original] = $name;
-          break;
-
-        case 'parent':
-          if ($comment->hasParentComment()) {
-            $parent = $comment->getParentComment();
-            $bubbleable_metadata->addCacheableDependency($parent);
-            $replacements[$original] = $parent->getSubject();
-          }
-          break;
-
-        case 'created':
-          $date_format = DateFormat::load('medium');
-          $bubbleable_metadata->addCacheableDependency($date_format);
-          $replacements[$original] = \Drupal::service('date.formatter')->format($comment->getCreatedTime(), 'medium', '', NULL, $langcode);
-          break;
-
-        case 'changed':
-          $date_format = DateFormat::load('medium');
-          $bubbleable_metadata->addCacheableDependency($date_format);
-          $replacements[$original] = \Drupal::service('date.formatter')->format($comment->getChangedTime(), 'medium', '', NULL, $langcode);
-          break;
-
-        case 'entity':
-          $entity = $comment->getCommentedEntity();
-          $bubbleable_metadata->addCacheableDependency($entity);
-          $title = $entity->label();
-          $replacements[$original] = $title;
-          break;
-      }
-    }
-
-    // Chained token relationships.
-    if ($entity_tokens = $token_service->findwithPrefix($tokens, 'entity')) {
-      $entity = $comment->getCommentedEntity();
-      $replacements += $token_service->generate($comment->getCommentedEntityTypeId(), $entity_tokens, [$comment->getCommentedEntityTypeId() => $entity], $options, $bubbleable_metadata);
-    }
-
-    if ($date_tokens = $token_service->findwithPrefix($tokens, 'created')) {
-      $replacements += $token_service->generate('date', $date_tokens, ['date' => $comment->getCreatedTime()], $options, $bubbleable_metadata);
-    }
-
-    if ($date_tokens = $token_service->findwithPrefix($tokens, 'changed')) {
-      $replacements += $token_service->generate('date', $date_tokens, ['date' => $comment->getChangedTime()], $options, $bubbleable_metadata);
-    }
-
-    if (($parent_tokens = $token_service->findwithPrefix($tokens, 'parent')) && $parent = $comment->getParentComment()) {
-      $replacements += $token_service->generate('comment', $parent_tokens, ['comment' => $parent], $options, $bubbleable_metadata);
-    }
-
-    if (($author_tokens = $token_service->findwithPrefix($tokens, 'author')) && $account = $comment->getOwner()) {
-      $replacements += $token_service->generate('user', $author_tokens, ['user' => $account], $options, $bubbleable_metadata);
-    }
-  }
-  // Replacement tokens for any content entities that have comment field.
-  elseif (!empty($data[$type]) && $data[$type] instanceof FieldableEntityInterface) {
-    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
-    $entity = $data[$type];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'comment-count':
-          $count = 0;
-          $fields = array_keys(\Drupal::service('comment.manager')->getFields($entity->getEntityTypeId()));
-          $definitions = array_keys($entity->getFieldDefinitions());
-          $valid_fields = array_intersect($fields, $definitions);
-          foreach ($valid_fields as $field_name) {
-            $count += $entity->get($field_name)->comment_count;
-          }
-          $replacements[$original] = $count;
-          break;
-
-        case 'comment-count-new':
-          $replacements[$original] = \Drupal::service('comment.manager')->getCountNewComments($entity);
-          break;
-      }
-    }
-  }
-
-  return $replacements;
-}
diff --git a/core/modules/comment/comment.views.inc b/core/modules/comment/comment.views.inc
deleted file mode 100644
index f9d83dbef96b43efd729b38423adcede481c9e57..0000000000000000000000000000000000000000
--- a/core/modules/comment/comment.views.inc
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views data for comment.module.
- */
-
-use Drupal\Core\Entity\ContentEntityInterface;
-
-/**
- * Implements hook_views_data_alter().
- */
-function comment_views_data_alter(&$data) {
-  // New comments are only supported for node table because it requires the
-  // history table.
-  $data['node']['new_comments'] = [
-    'title' => t('New comments'),
-    'help' => t('The number of new comments on the node.'),
-    'field' => [
-      'id' => 'node_new_comments',
-      'no group by' => TRUE,
-    ],
-  ];
-
-  // Provides an integration for each entity type except comment.
-  foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
-    if ($entity_type_id == 'comment' || !$entity_type->entityClassImplements(ContentEntityInterface::class) || !$entity_type->getBaseTable()) {
-      continue;
-    }
-    $fields = \Drupal::service('comment.manager')->getFields($entity_type_id);
-    $base_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable();
-    $args = ['@entity_type' => $entity_type_id];
-
-    if ($fields) {
-      $data[$base_table]['comments_link'] = [
-        'field' => [
-          'title' => t('Add comment link'),
-          'help' => t('Display the standard add comment link used on regular @entity_type, which will only display if the viewing user has access to add a comment.', $args),
-          'id' => 'comment_entity_link',
-        ],
-      ];
-
-      // Multilingual properties are stored in data table.
-      if (!($table = $entity_type->getDataTable())) {
-        $table = $entity_type->getBaseTable();
-      }
-      $data[$table]['uid_touch'] = [
-        'title' => t('User posted or commented'),
-        'help' => t('Display nodes only if a user posted the @entity_type or commented on the @entity_type.', $args),
-        'argument' => [
-          'field' => 'uid',
-          'name table' => 'users_field_data',
-          'name field' => 'name',
-          'id' => 'argument_comment_user_uid',
-          'no group by' => TRUE,
-          'entity_type' => $entity_type_id,
-          'entity_id' => $entity_type->getKey('id'),
-        ],
-        'filter' => [
-          'field' => 'uid',
-          'name table' => 'users_field_data',
-          'name field' => 'name',
-          'id' => 'comment_user_uid',
-          'entity_type' => $entity_type_id,
-          'entity_id' => $entity_type->getKey('id'),
-        ],
-      ];
-
-      foreach ($fields as $field_name => $field) {
-        $data[$base_table][$field_name . '_cid'] = [
-          'title' => t('Comments of the @entity_type using field: @field_name', $args + ['@field_name' => $field_name]),
-          'help' => t('Relate all comments on the @entity_type. This will create 1 duplicate record for every comment. Usually if you need this it is better to create a comment view.', $args),
-          'relationship' => [
-            'group' => t('Comment'),
-            'label' => t('Comments'),
-            'base' => 'comment_field_data',
-            'base field' => 'entity_id',
-            'relationship field' => $entity_type->getKey('id'),
-            'id' => 'standard',
-            'extra' => [
-              [
-                'field' => 'entity_type',
-                'value' => $entity_type_id,
-              ],
-              [
-                'field' => 'field_name',
-                'value' => $field_name,
-              ],
-            ],
-          ],
-        ];
-      }
-    }
-  }
-}
diff --git a/core/modules/comment/src/Hook/CommentHooks.php b/core/modules/comment/src/Hook/CommentHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..cf65ee0489d78141f11d53b45ea1e4071b247ccf
--- /dev/null
+++ b/core/modules/comment/src/Hook/CommentHooks.php
@@ -0,0 +1,530 @@
+<?php
+
+namespace Drupal\comment\Hook;
+
+use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
+use Drupal\Core\Entity\Entity\EntityViewMode;
+use Drupal\user\UserInterface;
+use Drupal\user\RoleInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\node\NodeInterface;
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
+use Drupal\field\FieldConfigInterface;
+use Drupal\comment\Entity\CommentType;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for comment.
+ */
+class CommentHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.comment':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Comment module allows users to comment on site content, set commenting defaults and permissions, and moderate comments. For more information, see the <a href=":comment">online documentation for the Comment module</a>.', [':comment' => 'https://www.drupal.org/documentation/modules/comment']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Enabling commenting') . '</dt>';
+        $output .= '<dd>' . t('Comment functionality can be enabled for any entity sub-type (for example, a <a href=":content-type">content type</a>) by adding a <em>Comments</em> field on its <em>Manage fields page</em>. Adding or removing commenting for an entity through the user interface requires the <a href=":field_ui">Field UI</a> module to be installed, even though the commenting functionality works without it. For more information on fields and entities, see the <a href=":field">Field module help page</a>.', [
+          ':content-type' => \Drupal::moduleHandler()->moduleExists('node') ? Url::fromRoute('entity.node_type.collection')->toString() : '#',
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Configuring commenting settings') . '</dt>';
+        $output .= '<dd>' . t('Commenting settings can be configured by editing the <em>Comments</em> field on the <em>Manage fields page</em> of an entity type if the <em>Field UI module</em> is installed. Configuration includes the label of the comments field, the number of comments to be displayed, and whether they are shown in threaded list. Commenting can be configured as: <em>Open</em> to allow new comments, <em>Closed</em> to view existing comments, but prevent new comments, or <em>Hidden</em> to hide existing comments and prevent new comments. Changing this configuration for an entity type will not change existing entity items.') . '</dd>';
+        $output .= '<dt>' . t('Overriding default settings') . '</dt>';
+        $output .= '<dd>' . t('Users with the appropriate permissions can override the default commenting settings of an entity type when they create an item of that type.') . '</dd>';
+        $output .= '<dt>' . t('Adding comment types') . '</dt>';
+        $output .= '<dd>' . t('Additional <em>comment types</em> can be created per entity sub-type and added on the <a href=":field">Comment types page</a>. If there are multiple comment types available you can select the appropriate one after adding a <em>Comments field</em>.', [
+          ':field' => Url::fromRoute('entity.comment_type.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Approving and managing comments') . '</dt>';
+        $output .= '<dd>' . t('Comments from users who have the <em>Skip comment approval</em> permission are published immediately. All other comments are placed in the <a href=":comment-approval">Unapproved comments</a> queue, until a user who has permission to <em>Administer comments and comment settings</em> publishes or deletes them. Published comments can be bulk managed on the <a href=":admin-comment">Published comments</a> administration page. When a comment has no replies, it remains editable by its author, as long as the author has <em>Edit own comments</em> permission.', [
+          ':comment-approval' => Url::fromRoute('comment.admin_approval')->toString(),
+          ':admin-comment' => Url::fromRoute('comment.admin')->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'entity.comment_type.collection':
+        $output = '<p>' . t('This page provides a list of all comment types on the site and allows you to manage the fields, form and display settings for each.') . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_entity_extra_field_info().
+   */
+  #[Hook('entity_extra_field_info')]
+  public function entityExtraFieldInfo() {
+    $return = [];
+    foreach (CommentType::loadMultiple() as $comment_type) {
+      $return['comment'][$comment_type->id()] = [
+        'form' => [
+          'author' => [
+            'label' => t('Author'),
+            'description' => t('Author textfield'),
+            'weight' => -2,
+          ],
+        ],
+      ];
+      $return['comment'][$comment_type->id()]['display']['links'] = [
+        'label' => t('Links'),
+        'description' => t('Comment operation links'),
+        'weight' => 100,
+        'visible' => TRUE,
+      ];
+    }
+    return $return;
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'comment' => [
+        'render element' => 'elements',
+      ],
+      'field__comment' => [
+        'base hook' => 'field',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_create() for 'field_config'.
+   */
+  #[Hook('field_config_create')]
+  public function fieldConfigCreate(FieldConfigInterface $field) {
+    if ($field->getType() == 'comment' && !$field->isSyncing()) {
+      // Assign default values for the field.
+      $default_value = $field->getDefaultValueLiteral();
+      $default_value += [[]];
+      $default_value[0] += [
+        'status' => CommentItemInterface::OPEN,
+        'cid' => 0,
+        'last_comment_timestamp' => 0,
+        'last_comment_name' => '',
+        'last_comment_uid' => 0,
+        'comment_count' => 0,
+      ];
+      $field->setDefaultValue($default_value);
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for 'field_config'.
+   */
+  #[Hook('field_config_update')]
+  public function fieldConfigUpdate(FieldConfigInterface $field) {
+    if ($field->getType() == 'comment') {
+      // Comment field settings also affects the rendering of *comment* entities,
+      // not only the *commented* entities.
+      \Drupal::entityTypeManager()->getViewBuilder('comment')->resetCache();
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for 'field_storage_config'.
+   */
+  #[Hook('field_storage_config_insert')]
+  public function fieldStorageConfigInsert(FieldStorageConfigInterface $field_storage) {
+    if ($field_storage->getType() == 'comment') {
+      // Check that the target entity type uses an integer ID.
+      $entity_type_id = $field_storage->getTargetEntityTypeId();
+      if (!_comment_entity_uses_integer_id($entity_type_id)) {
+        throw new \UnexpectedValueException('You cannot attach a comment field to an entity with a non-integer ID field');
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for 'field_config'.
+   */
+  #[Hook('field_config_delete')]
+  public function fieldConfigDelete(FieldConfigInterface $field) {
+    if ($field->getType() == 'comment') {
+      // Delete all comments that used by the entity bundle.
+      $entity_query = \Drupal::entityQuery('comment')->accessCheck(FALSE);
+      $entity_query->condition('entity_type', $field->getEntityTypeId());
+      $entity_query->condition('field_name', $field->getName());
+      $cids = $entity_query->execute();
+      $comment_storage = \Drupal::entityTypeManager()->getStorage('comment');
+      $comments = $comment_storage->loadMultiple($cids);
+      $comment_storage->delete($comments);
+    }
+  }
+
+  /**
+   * Implements hook_node_links_alter().
+   */
+  #[Hook('node_links_alter')]
+  public function nodeLinksAlter(array &$links, NodeInterface $node, array &$context) {
+    // Comment links are only added to node entity type for backwards
+    // compatibility. Should you require comment links for other entity types you
+    // can do so by implementing a new field formatter.
+    // @todo Make this configurable from the formatter. See
+    //   https://www.drupal.org/node/1901110.
+    $comment_links = \Drupal::service('comment.link_builder')->buildCommentedEntityLinks($node, $context);
+    $links += $comment_links;
+  }
+
+  /**
+   * Implements hook_entity_view().
+   */
+  #[Hook('entity_view')]
+  public function entityView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
+    if ($entity instanceof FieldableEntityInterface && $view_mode == 'rss' && $display->getComponent('links')) {
+      /** @var \Drupal\comment\CommentManagerInterface $comment_manager */
+      $comment_manager = \Drupal::service('comment.manager');
+      $fields = $comment_manager->getFields($entity->getEntityTypeId());
+      foreach ($fields as $field_name => $detail) {
+        if ($entity->hasField($field_name) && $entity->get($field_name)->status != CommentItemInterface::HIDDEN) {
+          // Add a comments RSS element which is a URL to the comments of this
+          // entity.
+          $options = ['fragment' => 'comments', 'absolute' => TRUE];
+          $entity->rss_elements[] = [
+            'key' => 'comments',
+            'value' => $entity->toUrl('canonical', $options)->toString(),
+          ];
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_view_alter() for node entities.
+   */
+  #[Hook('node_view_alter')]
+  public function nodeViewAlter(array &$build, EntityInterface $node, EntityViewDisplayInterface $display) {
+    if (\Drupal::moduleHandler()->moduleExists('history')) {
+      $build['#attributes']['data-history-node-id'] = $node->id();
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for field_ui_field_storage_add_form.
+   */
+  #[Hook('form_field_ui_field_storage_add_form_alter')]
+  public function formFieldUiFieldStorageAddFormAlter(&$form, FormStateInterface $form_state) : void {
+    $route_match = \Drupal::routeMatch();
+    if ($form_state->get('entity_type_id') == 'comment' && $route_match->getParameter('commented_entity_type')) {
+      $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name'));
+    }
+  }
+
+  /**
+   * Implements hook_field_info_entity_type_ui_definitions_alter().
+   */
+  #[Hook('field_info_entity_type_ui_definitions_alter')]
+  public function fieldInfoEntityTypeUiDefinitionsAlter(array &$ui_definitions, string $entity_type_id) {
+    if (!_comment_entity_uses_integer_id($entity_type_id)) {
+      unset($ui_definitions['comment']);
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_field_ui_form_display_overview_form_alter')]
+  public function formFieldUiFormDisplayOverviewFormAlter(&$form, FormStateInterface $form_state) : void {
+    $route_match = \Drupal::routeMatch();
+    if ($form['#entity_type'] == 'comment' && $route_match->getParameter('commented_entity_type')) {
+      $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name'));
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_field_ui_display_overview_form_alter')]
+  public function formFieldUiDisplayOverviewFormAlter(&$form, FormStateInterface $form_state) : void {
+    $route_match = \Drupal::routeMatch();
+    if ($form['#entity_type'] == 'comment' && $route_match->getParameter('commented_entity_type')) {
+      $form['#title'] = \Drupal::service('comment.manager')->getFieldUIPageTitle($route_match->getParameter('commented_entity_type'), $route_match->getParameter('field_name'));
+    }
+  }
+
+  /**
+   * Implements hook_entity_storage_load().
+   *
+   * @see \Drupal\comment\Plugin\Field\FieldType\CommentItem::propertyDefinitions()
+   */
+  #[Hook('entity_storage_load')]
+  public function entityStorageLoad($entities, $entity_type) {
+    // Comments can only be attached to content entities, so skip others.
+    if (!\Drupal::entityTypeManager()->getDefinition($entity_type)->entityClassImplements(FieldableEntityInterface::class)) {
+      return;
+    }
+    if (!\Drupal::service('comment.manager')->getFields($entity_type)) {
+      // Do not query database when entity has no comment fields.
+      return;
+    }
+    // Load comment information from the database and update the entity's
+    // comment statistics properties, which are defined on each CommentItem field.
+    $result = \Drupal::service('comment.statistics')->read($entities, $entity_type);
+    foreach ($result as $record) {
+      // Skip fields that entity does not have.
+      if (!$entities[$record->entity_id]->hasField($record->field_name)) {
+        continue;
+      }
+      $comment_statistics = $entities[$record->entity_id]->get($record->field_name);
+      $comment_statistics->cid = $record->cid;
+      $comment_statistics->last_comment_timestamp = $record->last_comment_timestamp;
+      $comment_statistics->last_comment_name = $record->last_comment_name;
+      $comment_statistics->last_comment_uid = $record->last_comment_uid;
+      $comment_statistics->comment_count = $record->comment_count;
+    }
+  }
+
+  /**
+   * Implements hook_entity_insert().
+   */
+  #[Hook('entity_insert')]
+  public function entityInsert(EntityInterface $entity) {
+    // Allow bulk updates and inserts to temporarily disable the
+    // maintenance of the {comment_entity_statistics} table.
+    if (\Drupal::state()->get('comment.maintain_entity_statistics') && ($fields = \Drupal::service('comment.manager')->getFields($entity->getEntityTypeId()))) {
+      \Drupal::service('comment.statistics')->create($entity, $fields);
+    }
+  }
+
+  /**
+   * Implements hook_entity_predelete().
+   */
+  #[Hook('entity_predelete')]
+  public function entityPredelete(EntityInterface $entity) {
+    // Entities can have non-numeric IDs, but {comment} and
+    // {comment_entity_statistics} tables have integer columns for entity ID, and
+    // PostgreSQL throws exceptions if you attempt query conditions with
+    // mismatched types. So, we need to verify that the ID is numeric (even for an
+    // entity type that has an integer ID, $entity->id() might be a string
+    // containing a number), and then cast it to an integer when querying.
+    if ($entity instanceof FieldableEntityInterface && is_numeric($entity->id())) {
+      $entity_query = \Drupal::entityQuery('comment')->accessCheck(FALSE);
+      $entity_query->condition('entity_id', (int) $entity->id());
+      $entity_query->condition('entity_type', $entity->getEntityTypeId());
+      $cids = $entity_query->execute();
+      $comment_storage = \Drupal::entityTypeManager()->getStorage('comment');
+      $comments = $comment_storage->loadMultiple($cids);
+      $comment_storage->delete($comments);
+      \Drupal::service('comment.statistics')->delete($entity);
+    }
+  }
+
+  /**
+   * Implements hook_node_update_index().
+   */
+  #[Hook('node_update_index')]
+  public function nodeUpdateIndex(EntityInterface $node) {
+    $index_comments =& drupal_static('comment_node_update_index');
+    if ($index_comments === NULL) {
+      // Do not index in the following three cases:
+      // 1. 'Authenticated user' can search content but can't access comments.
+      // 2. 'Anonymous user' can search content but can't access comments.
+      // 3. Any role can search content but can't access comments and access
+      // comments is not granted by the 'authenticated user' role. In this case
+      // all users might have both permissions from various roles but it is also
+      // possible to set up a user to have only search content and so a user
+      // edit could change the security situation so it is not safe to index the
+      // comments.
+      $index_comments = TRUE;
+      $roles = \Drupal::entityTypeManager()->getStorage('user_role')->loadMultiple();
+      $authenticated_can_access = $roles[RoleInterface::AUTHENTICATED_ID]->hasPermission('access comments');
+      foreach ($roles as $rid => $role) {
+        if ($role->hasPermission('search content') && !$role->hasPermission('access comments')) {
+          if ($rid == RoleInterface::AUTHENTICATED_ID || $rid == RoleInterface::ANONYMOUS_ID || !$authenticated_can_access) {
+            $index_comments = FALSE;
+            break;
+          }
+        }
+      }
+    }
+    $build = [];
+    if ($index_comments) {
+      foreach (\Drupal::service('comment.manager')->getFields('node') as $field_name => $info) {
+        // Skip fields that entity does not have.
+        if (!$node->hasField($field_name)) {
+          continue;
+        }
+        $field_definition = $node->getFieldDefinition($field_name);
+        $mode = $field_definition->getSetting('default_mode');
+        $comments_per_page = $field_definition->getSetting('per_page');
+        if ($node->get($field_name)->status) {
+          $comments = \Drupal::entityTypeManager()->getStorage('comment')->loadThread($node, $field_name, $mode, $comments_per_page);
+          if ($comments) {
+            $build[] = \Drupal::entityTypeManager()->getViewBuilder('comment')->viewMultiple($comments);
+          }
+        }
+      }
+    }
+    return \Drupal::service('renderer')->renderInIsolation($build);
+  }
+
+  /**
+   * Implements hook_cron().
+   */
+  #[Hook('cron')]
+  public function cron() {
+    // Store the maximum possible comments per thread (used for node search
+    // ranking by reply count).
+    \Drupal::state()->set('comment.node_comment_statistics_scale', 1.0 / max(1, \Drupal::service('comment.statistics')->getMaximumCount('node')));
+  }
+
+  /**
+   * Implements hook_node_search_result().
+   *
+   * Formats a comment count string and returns it, for display with search
+   * results.
+   */
+  #[Hook('node_search_result')]
+  public function nodeSearchResult(EntityInterface $node) {
+    $comment_fields = \Drupal::service('comment.manager')->getFields('node');
+    $comments = 0;
+    $open = FALSE;
+    foreach ($comment_fields as $field_name => $info) {
+      // Skip fields that entity does not have.
+      if (!$node->hasField($field_name)) {
+        continue;
+      }
+      // Do not make a string if comments are hidden.
+      $status = $node->get($field_name)->status;
+      if (\Drupal::currentUser()->hasPermission('access comments') && $status != CommentItemInterface::HIDDEN) {
+        if ($status == CommentItemInterface::OPEN) {
+          // At least one comment field is open.
+          $open = TRUE;
+        }
+        $comments += $node->get($field_name)->comment_count;
+      }
+    }
+    // Do not make a string if there are no comment fields, or no comments exist
+    // or all comment fields are hidden.
+    if ($comments > 0 || $open) {
+      return [
+        'comment' => \Drupal::translation()->formatPlural($comments, '1 comment', '@count comments'),
+      ];
+    }
+  }
+
+  /**
+   * Implements hook_user_cancel().
+   */
+  #[Hook('user_cancel')]
+  public function userCancel($edit, UserInterface $account, $method) {
+    switch ($method) {
+      case 'user_cancel_block_unpublish':
+        $comments = \Drupal::entityTypeManager()->getStorage('comment')->loadByProperties(['uid' => $account->id()]);
+        foreach ($comments as $comment) {
+          $comment->setUnpublished();
+          $comment->save();
+        }
+        break;
+
+      case 'user_cancel_reassign':
+        /** @var \Drupal\comment\CommentInterface[] $comments */
+        $comments = \Drupal::entityTypeManager()->getStorage('comment')->loadByProperties(['uid' => $account->id()]);
+        foreach ($comments as $comment) {
+          $langcodes = array_keys($comment->getTranslationLanguages());
+          // For efficiency manually save the original comment before applying any
+          // changes.
+          $comment->original = clone $comment;
+          foreach ($langcodes as $langcode) {
+            $comment_translated = $comment->getTranslation($langcode);
+            $comment_translated->setOwnerId(0);
+            $comment_translated->setAuthorName(\Drupal::config('user.settings')->get('anonymous'));
+          }
+          $comment->save();
+        }
+        break;
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for user entities.
+   */
+  #[Hook('user_predelete')]
+  public function userPredelete($account) {
+    $entity_query = \Drupal::entityQuery('comment')->accessCheck(FALSE);
+    $entity_query->condition('uid', $account->id());
+    $cids = $entity_query->execute();
+    $comment_storage = \Drupal::entityTypeManager()->getStorage('comment');
+    $comments = $comment_storage->loadMultiple($cids);
+    $comment_storage->delete($comments);
+  }
+
+  /**
+   * Implements hook_ranking().
+   */
+  #[Hook('ranking')]
+  public function ranking() {
+    return \Drupal::service('comment.statistics')->getRankingInfo();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for entity_view_display entities.
+   */
+  #[Hook('entity_view_display_presave')]
+  public function entityViewDisplayPresave(EntityViewDisplayInterface $display) {
+    // Act only on comment view displays being disabled.
+    if ($display->isNew() || $display->getTargetEntityTypeId() !== 'comment' || $display->status()) {
+      return;
+    }
+    $storage = \Drupal::entityTypeManager()->getStorage('entity_view_display');
+    if (!$storage->loadUnchanged($display->getOriginalId())->status()) {
+      return;
+    }
+    // Disable the comment field formatter when the used view display is disabled.
+    foreach ($storage->loadMultiple() as $view_display) {
+      $changed = FALSE;
+      /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $view_display */
+      foreach ($view_display->getComponents() as $field => $component) {
+        if (isset($component['type']) && $component['type'] === 'comment_default') {
+          if ($component['settings']['view_mode'] === $display->getMode()) {
+            $view_display->removeComponent($field);
+            /** @var \Drupal\Core\Entity\EntityViewModeInterface $mode */
+            $mode = EntityViewMode::load($display->getTargetEntityTypeId() . '.' . $display->getMode());
+            $arguments = [
+              '@id' => $view_display->id(),
+              '@name' => $field,
+              '@display' => $mode->label(),
+              '@mode' => $display->getMode(),
+            ];
+            \Drupal::logger('system')->warning("View display '@id': Comment field formatter '@name' was disabled because it is using the comment view display '@display' (@mode) that was just disabled.", $arguments);
+            $changed = TRUE;
+          }
+        }
+      }
+      if ($changed) {
+        $view_display->save();
+      }
+    }
+  }
+
+  /**
+   * Implements hook_field_type_category_info_alter().
+   */
+  #[Hook('field_type_category_info_alter')]
+  public function fieldTypeCategoryInfoAlter(&$definitions) {
+    // The `comment` field type belongs in the `general` category, so the
+    // libraries need to be attached using an alter hook.
+    $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'comment/drupal.comment-icon';
+  }
+
+}
diff --git a/core/modules/comment/src/Hook/CommentTokensHooks.php b/core/modules/comment/src/Hook/CommentTokensHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f151b047120ceadee618dff6f5cbd38b652e168b
--- /dev/null
+++ b/core/modules/comment/src/Hook/CommentTokensHooks.php
@@ -0,0 +1,259 @@
+<?php
+
+namespace Drupal\comment\Hook;
+
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Datetime\Entity\DateFormat;
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for comment.
+ */
+class CommentTokensHooks {
+
+  /**
+   * Implements hook_token_info().
+   */
+  #[Hook('token_info')]
+  public function tokenInfo() {
+    $type = [
+      'name' => t('Comments'),
+      'description' => t('Tokens for comments posted on the site.'),
+      'needs-data' => 'comment',
+    ];
+    $tokens = [];
+    // Provides an integration for each entity type except comment.
+    foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
+      if ($entity_type_id == 'comment' || !$entity_type->entityClassImplements(ContentEntityInterface::class)) {
+        continue;
+      }
+      if (\Drupal::service('comment.manager')->getFields($entity_type_id)) {
+        // Get the correct token type.
+        $token_type = $entity_type_id == 'taxonomy_term' ? 'term' : $entity_type_id;
+        // @todo Make this work per field. See https://www.drupal.org/node/2031903.
+        $tokens[$token_type]['comment-count'] = [
+          'name' => t("Comment count"),
+          'description' => t("The number of comments posted on an entity."),
+        ];
+        $tokens[$token_type]['comment-count-new'] = [
+          'name' => t("New comment count"),
+          'description' => t("The number of comments posted on an entity since the reader last viewed it."),
+        ];
+      }
+    }
+    // Core comment tokens
+    $comment['cid'] = ['name' => t("Comment ID"), 'description' => t("The unique ID of the comment.")];
+    $comment['uuid'] = ['name' => t('UUID'), 'description' => t("The UUID of the comment.")];
+    $comment['hostname'] = [
+      'name' => t("IP Address"),
+      'description' => t("The IP address of the computer the comment was posted from."),
+    ];
+    $comment['mail'] = [
+      'name' => t("Email address"),
+      'description' => t("The email address left by the comment author."),
+    ];
+    $comment['homepage'] = [
+      'name' => t("Home page"),
+      'description' => t("The home page URL left by the comment author."),
+    ];
+    $comment['title'] = ['name' => t("Title"), 'description' => t("The title of the comment.")];
+    $comment['body'] = [
+      'name' => t("Content"),
+      'description' => t("The formatted content of the comment itself."),
+    ];
+    $comment['langcode'] = [
+      'name' => t('Language code'),
+      'description' => t('The language code of the language the comment is written in.'),
+    ];
+    $comment['url'] = ['name' => t("URL"), 'description' => t("The URL of the comment.")];
+    $comment['edit-url'] = [
+      'name' => t("Edit URL"),
+      'description' => t("The URL of the comment's edit page."),
+    ];
+    // Chained tokens for comments
+    $comment['created'] = [
+      'name' => t("Date created"),
+      'description' => t("The date the comment was posted."),
+      'type' => 'date',
+    ];
+    $comment['changed'] = [
+      'name' => t("Date changed"),
+      'description' => t("The date the comment was most recently updated."),
+      'type' => 'date',
+    ];
+    $comment['parent'] = [
+      'name' => t("Parent"),
+      'description' => t("The comment's parent, if comment threading is active."),
+      'type' => 'comment',
+    ];
+    $comment['entity'] = [
+      'name' => t("Entity"),
+      'description' => t("The entity the comment was posted to."),
+      'type' => 'entity',
+    ];
+    $comment['author'] = [
+      'name' => t("Author"),
+      'description' => t("The author name of the comment."),
+      'type' => 'user',
+    ];
+    return ['types' => ['comment' => $type], 'tokens' => ['comment' => $comment] + $tokens];
+  }
+
+  /**
+   * Implements hook_tokens().
+   */
+  #[Hook('tokens')]
+  public function tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+    $token_service = \Drupal::token();
+    $url_options = ['absolute' => TRUE];
+    if (isset($options['langcode'])) {
+      $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
+      $langcode = $options['langcode'];
+    }
+    else {
+      $langcode = NULL;
+    }
+    $replacements = [];
+    if ($type == 'comment' && !empty($data['comment'])) {
+      /** @var \Drupal\comment\CommentInterface $comment */
+      $comment = $data['comment'];
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          // Simple key values on the comment.
+          case 'cid':
+            $replacements[$original] = $comment->id();
+            break;
+
+          case 'uuid':
+            $replacements[$original] = $comment->uuid();
+            break;
+
+          // Poster identity information for comments.
+          case 'hostname':
+            $replacements[$original] = $comment->getHostname();
+            break;
+
+          case 'mail':
+            $mail = $comment->getAuthorEmail();
+            // Add the user cacheability metadata in case the author of the comment
+            // is not the anonymous user.
+            if ($comment->getOwnerId()) {
+              $bubbleable_metadata->addCacheableDependency($comment->getOwner());
+            }
+            $replacements[$original] = $mail;
+            break;
+
+          case 'homepage':
+            $replacements[$original] = UrlHelper::stripDangerousProtocols($comment->getHomepage());
+            break;
+
+          case 'title':
+            $replacements[$original] = $comment->getSubject();
+            break;
+
+          case 'body':
+            // "processed" returns a \Drupal\Component\Render\MarkupInterface via
+            // check_markup().
+            $replacements[$original] = $comment->comment_body->processed;
+            break;
+
+          case 'langcode':
+            $replacements[$original] = $comment->language()->getId();
+            break;
+
+          // Comment related URLs.
+          case 'url':
+            $url_options['fragment'] = 'comment-' . $comment->id();
+            $replacements[$original] = $comment->toUrl('canonical', $url_options)->toString();
+            break;
+
+          case 'edit-url':
+            $url_options['fragment'] = NULL;
+            $replacements[$original] = $comment->toUrl('edit-form', $url_options)->toString();
+            break;
+
+          case 'author':
+            $name = $comment->getAuthorName();
+            // Add the user cacheability metadata in case the author of the comment
+            // is not the anonymous user.
+            if ($comment->getOwnerId()) {
+              $bubbleable_metadata->addCacheableDependency($comment->getOwner());
+            }
+            $replacements[$original] = $name;
+            break;
+
+          case 'parent':
+            if ($comment->hasParentComment()) {
+              $parent = $comment->getParentComment();
+              $bubbleable_metadata->addCacheableDependency($parent);
+              $replacements[$original] = $parent->getSubject();
+            }
+            break;
+
+          case 'created':
+            $date_format = DateFormat::load('medium');
+            $bubbleable_metadata->addCacheableDependency($date_format);
+            $replacements[$original] = \Drupal::service('date.formatter')->format($comment->getCreatedTime(), 'medium', '', NULL, $langcode);
+            break;
+
+          case 'changed':
+            $date_format = DateFormat::load('medium');
+            $bubbleable_metadata->addCacheableDependency($date_format);
+            $replacements[$original] = \Drupal::service('date.formatter')->format($comment->getChangedTime(), 'medium', '', NULL, $langcode);
+            break;
+
+          case 'entity':
+            $entity = $comment->getCommentedEntity();
+            $bubbleable_metadata->addCacheableDependency($entity);
+            $title = $entity->label();
+            $replacements[$original] = $title;
+            break;
+        }
+      }
+      // Chained token relationships.
+      if ($entity_tokens = $token_service->findwithPrefix($tokens, 'entity')) {
+        $entity = $comment->getCommentedEntity();
+        $replacements += $token_service->generate($comment->getCommentedEntityTypeId(), $entity_tokens, [$comment->getCommentedEntityTypeId() => $entity], $options, $bubbleable_metadata);
+      }
+      if ($date_tokens = $token_service->findwithPrefix($tokens, 'created')) {
+        $replacements += $token_service->generate('date', $date_tokens, ['date' => $comment->getCreatedTime()], $options, $bubbleable_metadata);
+      }
+      if ($date_tokens = $token_service->findwithPrefix($tokens, 'changed')) {
+        $replacements += $token_service->generate('date', $date_tokens, ['date' => $comment->getChangedTime()], $options, $bubbleable_metadata);
+      }
+      if (($parent_tokens = $token_service->findwithPrefix($tokens, 'parent')) && ($parent = $comment->getParentComment())) {
+        $replacements += $token_service->generate('comment', $parent_tokens, ['comment' => $parent], $options, $bubbleable_metadata);
+      }
+      if (($author_tokens = $token_service->findwithPrefix($tokens, 'author')) && ($account = $comment->getOwner())) {
+        $replacements += $token_service->generate('user', $author_tokens, ['user' => $account], $options, $bubbleable_metadata);
+      }
+    }
+    elseif (!empty($data[$type]) && $data[$type] instanceof FieldableEntityInterface) {
+      /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
+      $entity = $data[$type];
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          case 'comment-count':
+            $count = 0;
+            $fields = array_keys(\Drupal::service('comment.manager')->getFields($entity->getEntityTypeId()));
+            $definitions = array_keys($entity->getFieldDefinitions());
+            $valid_fields = array_intersect($fields, $definitions);
+            foreach ($valid_fields as $field_name) {
+              $count += $entity->get($field_name)->comment_count;
+            }
+            $replacements[$original] = $count;
+            break;
+
+          case 'comment-count-new':
+            $replacements[$original] = \Drupal::service('comment.manager')->getCountNewComments($entity);
+            break;
+        }
+      }
+    }
+    return $replacements;
+  }
+
+}
diff --git a/core/modules/comment/src/Hook/CommentViewsHooks.php b/core/modules/comment/src/Hook/CommentViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..03140709c4a3ec789d76fb1da6cb335ae0479deb
--- /dev/null
+++ b/core/modules/comment/src/Hook/CommentViewsHooks.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\comment\Hook;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for comment.
+ */
+class CommentViewsHooks {
+
+  /**
+   * Implements hook_views_data_alter().
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(&$data) {
+    // New comments are only supported for node table because it requires the
+    // history table.
+    $data['node']['new_comments'] = [
+      'title' => t('New comments'),
+      'help' => t('The number of new comments on the node.'),
+      'field' => [
+        'id' => 'node_new_comments',
+        'no group by' => TRUE,
+      ],
+    ];
+    // Provides an integration for each entity type except comment.
+    foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
+      if ($entity_type_id == 'comment' || !$entity_type->entityClassImplements(ContentEntityInterface::class) || !$entity_type->getBaseTable()) {
+        continue;
+      }
+      $fields = \Drupal::service('comment.manager')->getFields($entity_type_id);
+      $base_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable();
+      $args = ['@entity_type' => $entity_type_id];
+      if ($fields) {
+        $data[$base_table]['comments_link'] = [
+          'field' => [
+            'title' => t('Add comment link'),
+            'help' => t('Display the standard add comment link used on regular @entity_type, which will only display if the viewing user has access to add a comment.', $args),
+            'id' => 'comment_entity_link',
+          ],
+        ];
+        // Multilingual properties are stored in data table.
+        if (!($table = $entity_type->getDataTable())) {
+          $table = $entity_type->getBaseTable();
+        }
+        $data[$table]['uid_touch'] = [
+          'title' => t('User posted or commented'),
+          'help' => t('Display nodes only if a user posted the @entity_type or commented on the @entity_type.', $args),
+          'argument' => [
+            'field' => 'uid',
+            'name table' => 'users_field_data',
+            'name field' => 'name',
+            'id' => 'argument_comment_user_uid',
+            'no group by' => TRUE,
+            'entity_type' => $entity_type_id,
+            'entity_id' => $entity_type->getKey('id'),
+          ],
+          'filter' => [
+            'field' => 'uid',
+            'name table' => 'users_field_data',
+            'name field' => 'name',
+            'id' => 'comment_user_uid',
+            'entity_type' => $entity_type_id,
+            'entity_id' => $entity_type->getKey('id'),
+          ],
+        ];
+        foreach ($fields as $field_name => $field) {
+          $data[$base_table][$field_name . '_cid'] = [
+            'title' => t('Comments of the @entity_type using field: @field_name', $args + [
+              '@field_name' => $field_name,
+            ]),
+            'help' => t('Relate all comments on the @entity_type. This will create 1 duplicate record for every comment. Usually if you need this it is better to create a comment view.', $args),
+            'relationship' => [
+              'group' => t('Comment'),
+              'label' => t('Comments'),
+              'base' => 'comment_field_data',
+              'base field' => 'entity_id',
+              'relationship field' => $entity_type->getKey('id'),
+              'id' => 'standard',
+              'extra' => [
+                        [
+                          'field' => 'entity_type',
+                          'value' => $entity_type_id,
+                        ],
+                        [
+                          'field' => 'field_name',
+                          'value' => $field_name,
+                        ],
+              ],
+            ],
+          ];
+        }
+      }
+    }
+  }
+
+}
diff --git a/core/modules/comment/tests/modules/comment_display_configurable_test/comment_display_configurable_test.module b/core/modules/comment/tests/modules/comment_display_configurable_test/comment_display_configurable_test.module
deleted file mode 100644
index c4399d32b52d4541c275c6c38c152a72a61811f4..0000000000000000000000000000000000000000
--- a/core/modules/comment/tests/modules/comment_display_configurable_test/comment_display_configurable_test.module
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-/**
- * @file
- * A module for testing making comment base fields' displays configurable.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityTypeInterface;
-
-/**
- * Implements hook_entity_base_field_info_alter().
- */
-function comment_display_configurable_test_entity_base_field_info_alter(&$base_field_definitions, EntityTypeInterface $entity_type) {
-  if ($entity_type->id() == 'comment') {
-    foreach (['created', 'uid', 'pid', 'subject'] as $field) {
-      /** @var \Drupal\Core\Field\BaseFieldDefinition[] $base_field_definitions */
-      $base_field_definitions[$field]->setDisplayConfigurable('view', TRUE);
-    }
-  }
-}
-
-/**
- * Implements hook_entity_type_build().
- */
-function comment_display_configurable_test_entity_type_build(array &$entity_types) {
-  // Allow skipping of extra preprocessing for configurable display.
-  $entity_types['comment']->set('enable_base_field_custom_preprocess_skipping', TRUE);
-}
diff --git a/core/modules/comment/tests/modules/comment_display_configurable_test/src/Hook/CommentDisplayConfigurableTestHooks.php b/core/modules/comment/tests/modules/comment_display_configurable_test/src/Hook/CommentDisplayConfigurableTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..a25c75c37e114a83e1133decdfde50843f1771dd
--- /dev/null
+++ b/core/modules/comment/tests/modules/comment_display_configurable_test/src/Hook/CommentDisplayConfigurableTestHooks.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\comment_display_configurable_test\Hook;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for comment_display_configurable_test.
+ */
+class CommentDisplayConfigurableTestHooks {
+
+  /**
+   * Implements hook_entity_base_field_info_alter().
+   */
+  #[Hook('entity_base_field_info_alter')]
+  public function entityBaseFieldInfoAlter(&$base_field_definitions, EntityTypeInterface $entity_type) {
+    if ($entity_type->id() == 'comment') {
+      foreach (['created', 'uid', 'pid', 'subject'] as $field) {
+        /** @var \Drupal\Core\Field\BaseFieldDefinition[] $base_field_definitions */
+        $base_field_definitions[$field]->setDisplayConfigurable('view', TRUE);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public function entityTypeBuild(array &$entity_types) {
+    // Allow skipping of extra preprocessing for configurable display.
+    $entity_types['comment']->set('enable_base_field_custom_preprocess_skipping', TRUE);
+  }
+
+}
diff --git a/core/modules/comment/tests/modules/comment_test/comment_test.module b/core/modules/comment/tests/modules/comment_test/comment_test.module
deleted file mode 100644
index 06741ef12ce9ac0d774e30f298b43082f6f485b2..0000000000000000000000000000000000000000
--- a/core/modules/comment/tests/modules/comment_test/comment_test.module
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-
-/**
- * @file
- * Implements comment-related hooks to test API interactions.
- */
-
-declare(strict_types=1);
-
-use Drupal\comment\CommentInterface;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_entity_type_alter().
- */
-function comment_test_entity_type_alter(array &$entity_types): void {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  if (\Drupal::languageManager()->isMultilingual()) {
-    // Enable language handling for comment fields.
-    $translation = $entity_types['comment']->get('translation');
-    $translation['comment_test'] = TRUE;
-    $entity_types['comment']->set('translation', $translation);
-  }
-}
-
-/**
- * Implements hook_comment_links_alter().
- */
-function comment_test_comment_links_alter(array &$links, CommentInterface &$entity, array &$context) {
-  // Allow tests to enable or disable this alter hook.
-  if (!\Drupal::state()->get('comment_test_links_alter_enabled', FALSE)) {
-    return;
-  }
-
-  $links['comment_test'] = [
-    '#theme' => 'links__comment__comment_test',
-    '#attributes' => ['class' => ['links', 'inline']],
-    '#links' => [
-      'comment-report' => [
-        'title' => t('Report'),
-        'url' => Url::fromRoute('comment_test.report', ['comment' => $entity->id()], ['query' => ['token' => \Drupal::getContainer()->get('csrf_token')->get("comment/{$entity->id()}/report")]]),
-      ],
-    ],
-  ];
-}
diff --git a/core/modules/comment/tests/modules/comment_test/src/Hook/CommentTestHooks.php b/core/modules/comment/tests/modules/comment_test/src/Hook/CommentTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..616df00e848c66894e770793762fbeb01c8293ca
--- /dev/null
+++ b/core/modules/comment/tests/modules/comment_test/src/Hook/CommentTestHooks.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\comment_test\Hook;
+
+use Drupal\Core\Url;
+use Drupal\comment\CommentInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for comment_test.
+ */
+class CommentTestHooks {
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    if (\Drupal::languageManager()->isMultilingual()) {
+      // Enable language handling for comment fields.
+      $translation = $entity_types['comment']->get('translation');
+      $translation['comment_test'] = TRUE;
+      $entity_types['comment']->set('translation', $translation);
+    }
+  }
+
+  /**
+   * Implements hook_comment_links_alter().
+   */
+  #[Hook('comment_links_alter')]
+  public function commentLinksAlter(array &$links, CommentInterface &$entity, array &$context) {
+    // Allow tests to enable or disable this alter hook.
+    if (!\Drupal::state()->get('comment_test_links_alter_enabled', FALSE)) {
+      return;
+    }
+    $links['comment_test'] = [
+      '#theme' => 'links__comment__comment_test',
+      '#attributes' => [
+        'class' => [
+          'links',
+          'inline',
+        ],
+      ],
+      '#links' => [
+        'comment-report' => [
+          'title' => t('Report'),
+          'url' => Url::fromRoute('comment_test.report', [
+            'comment' => $entity->id(),
+          ], [
+            'query' => [
+              'token' => \Drupal::getContainer()->get('csrf_token')->get("comment/{$entity->id()}/report"),
+            ],
+          ]),
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/config/config.module b/core/modules/config/config.module
deleted file mode 100644
index 3a0fd7961f2e15b86110fc1909a89933a4912dbf..0000000000000000000000000000000000000000
--- a/core/modules/config/config.module
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-
-/**
- * @file
- * Allows site administrators to modify configuration.
- */
-
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\StreamWrapper\StreamWrapperManager;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_help().
- */
-function config_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.config':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Configuration Manager module provides a user interface for importing and exporting configuration changes between installations of your website in different environments. Configuration is stored in YAML format. For more information, see the <a href=":url">online documentation for the Configuration Manager module</a>.', [':url' => 'https://www.drupal.org/documentation/administer/config']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Exporting the full configuration') . '</dt>';
-      $output .= '<dd>' . t('You can create and download an archive consisting of all your site\'s configuration exported as <em>*.yml</em> files on the <a href=":url">Export</a> page.', [':url' => Url::fromRoute('config.export_full')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Importing a full configuration') . '</dt>';
-      $output .= '<dd>' . t('You can upload a full site configuration from an archive file on the <a href=":url">Import</a> page. When importing data from a different environment, the site and import files must have matching configuration values for UUID in the <em>system.site</em> configuration item. That means that your other environments should initially be set up as clones of the target site. Migrations are not supported.', [':url' => Url::fromRoute('config.import_full')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Synchronizing configuration') . '</dt>';
-      $output .= '<dd>' . t('You can review differences between the active configuration and an imported configuration archive on the <a href=":synchronize">Synchronize</a> page to ensure that the changes are as expected, before finalizing the import. The Synchronize page also shows configuration items that would be added or removed.', [':synchronize' => Url::fromRoute('config.sync')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Exporting a single configuration item') . '</dt>';
-      $output .= '<dd>' . t('You can export a single configuration item by selecting a <em>Configuration type</em> and <em>Configuration name</em> on the <a href=":single-export">Single export</a> page. The configuration and its corresponding <em>*.yml file name</em> are then displayed on the page for you to copy.', [':single-export' => Url::fromRoute('config.export_single')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Importing a single configuration item') . '</dt>';
-      $output .= '<dd>' . t('You can import a single configuration item by pasting it in YAML format into the form on the <a href=":single-import">Single import</a> page.', [':single-import' => Url::fromRoute('config.import_single')->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'config.sync':
-      $output = '';
-      $output .= '<p>' . t('Compare the configuration uploaded to your sync directory with the active configuration before completing the import.') . '</p>';
-      return $output;
-
-    case 'config.export_full':
-      $output = '';
-      $output .= '<p>' . t('Export and download the full configuration of this site as a gzipped tar file.') . '</p>';
-      return $output;
-
-    case 'config.import_full':
-      $output = '';
-      $output .= '<p>' . t('Upload a full site configuration archive to the sync directory. It can then be compared and imported on the Synchronize page.') . '</p>';
-      return $output;
-
-    case 'config.export_single':
-      $output = '';
-      $output .= '<p>' . t('Choose a configuration item to display its YAML structure.') . '</p>';
-      return $output;
-
-    case 'config.import_single':
-      $output = '';
-      $output .= '<p>' . t('Import a single configuration item by pasting its YAML structure into the text field.') . '</p>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_file_download().
- */
-function config_file_download($uri) {
-  $scheme = StreamWrapperManager::getScheme($uri);
-  $target = StreamWrapperManager::getTarget($uri);
-  if ($scheme == 'temporary' && $target == 'config.tar.gz') {
-    if (\Drupal::currentUser()->hasPermission('export configuration')) {
-      $request = \Drupal::request();
-      $date = DateTime::createFromFormat('U', $request->server->get('REQUEST_TIME'));
-      $date_string = $date->format('Y-m-d-H-i');
-      $hostname = str_replace('.', '-', $request->getHttpHost());
-      $filename = 'config-' . $hostname . '-' . $date_string . '.tar.gz';
-      $disposition = 'attachment; filename="' . $filename . '"';
-      return [
-        'Content-disposition' => $disposition,
-      ];
-    }
-    return -1;
-  }
-}
diff --git a/core/modules/config/src/Hook/ConfigHooks.php b/core/modules/config/src/Hook/ConfigHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..909ea5deff4ad72cacd9aee2dd16213dc9a82abc
--- /dev/null
+++ b/core/modules/config/src/Hook/ConfigHooks.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\config\Hook;
+
+use Drupal\Core\StreamWrapper\StreamWrapperManager;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for config.
+ */
+class ConfigHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.config':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Configuration Manager module provides a user interface for importing and exporting configuration changes between installations of your website in different environments. Configuration is stored in YAML format. For more information, see the <a href=":url">online documentation for the Configuration Manager module</a>.', [':url' => 'https://www.drupal.org/documentation/administer/config']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Exporting the full configuration') . '</dt>';
+        $output .= '<dd>' . t('You can create and download an archive consisting of all your site\'s configuration exported as <em>*.yml</em> files on the <a href=":url">Export</a> page.', [':url' => Url::fromRoute('config.export_full')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Importing a full configuration') . '</dt>';
+        $output .= '<dd>' . t('You can upload a full site configuration from an archive file on the <a href=":url">Import</a> page. When importing data from a different environment, the site and import files must have matching configuration values for UUID in the <em>system.site</em> configuration item. That means that your other environments should initially be set up as clones of the target site. Migrations are not supported.', [':url' => Url::fromRoute('config.import_full')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Synchronizing configuration') . '</dt>';
+        $output .= '<dd>' . t('You can review differences between the active configuration and an imported configuration archive on the <a href=":synchronize">Synchronize</a> page to ensure that the changes are as expected, before finalizing the import. The Synchronize page also shows configuration items that would be added or removed.', [':synchronize' => Url::fromRoute('config.sync')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Exporting a single configuration item') . '</dt>';
+        $output .= '<dd>' . t('You can export a single configuration item by selecting a <em>Configuration type</em> and <em>Configuration name</em> on the <a href=":single-export">Single export</a> page. The configuration and its corresponding <em>*.yml file name</em> are then displayed on the page for you to copy.', [
+          ':single-export' => Url::fromRoute('config.export_single')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Importing a single configuration item') . '</dt>';
+        $output .= '<dd>' . t('You can import a single configuration item by pasting it in YAML format into the form on the <a href=":single-import">Single import</a> page.', [
+          ':single-import' => Url::fromRoute('config.import_single')->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'config.sync':
+        $output = '';
+        $output .= '<p>' . t('Compare the configuration uploaded to your sync directory with the active configuration before completing the import.') . '</p>';
+        return $output;
+
+      case 'config.export_full':
+        $output = '';
+        $output .= '<p>' . t('Export and download the full configuration of this site as a gzipped tar file.') . '</p>';
+        return $output;
+
+      case 'config.import_full':
+        $output = '';
+        $output .= '<p>' . t('Upload a full site configuration archive to the sync directory. It can then be compared and imported on the Synchronize page.') . '</p>';
+        return $output;
+
+      case 'config.export_single':
+        $output = '';
+        $output .= '<p>' . t('Choose a configuration item to display its YAML structure.') . '</p>';
+        return $output;
+
+      case 'config.import_single':
+        $output = '';
+        $output .= '<p>' . t('Import a single configuration item by pasting its YAML structure into the text field.') . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_file_download().
+   */
+  #[Hook('file_download')]
+  public function fileDownload($uri) {
+    $scheme = StreamWrapperManager::getScheme($uri);
+    $target = StreamWrapperManager::getTarget($uri);
+    if ($scheme == 'temporary' && $target == 'config.tar.gz') {
+      if (\Drupal::currentUser()->hasPermission('export configuration')) {
+        $request = \Drupal::request();
+        $date = \DateTime::createFromFormat('U', $request->server->get('REQUEST_TIME'));
+        $date_string = $date->format('Y-m-d-H-i');
+        $hostname = str_replace('.', '-', $request->getHttpHost());
+        $filename = 'config-' . $hostname . '-' . $date_string . '.tar.gz';
+        $disposition = 'attachment; filename="' . $filename . '"';
+        return ['Content-disposition' => $disposition];
+      }
+      return -1;
+    }
+  }
+
+}
diff --git a/core/modules/config/tests/config_entity_static_cache_test/config_entity_static_cache_test.module b/core/modules/config/tests/config_entity_static_cache_test/config_entity_static_cache_test.module
deleted file mode 100644
index dd5d1a7ee2017d23c2d193fc00203e8ede1a0be2..0000000000000000000000000000000000000000
--- a/core/modules/config/tests/config_entity_static_cache_test/config_entity_static_cache_test.module
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides configuration entity static cache test helpers.
- */
-
-declare(strict_types=1);
-
-use Drupal\Component\Utility\Random;
-
-/**
- * Implements hook_ENTITY_TYPE_load() for 'static_cache_test_config_test'.
- */
-function config_entity_static_cache_test_config_test_load($entities) {
-  static $random;
-  if (!$random) {
-    $random = new Random();
-  }
-  foreach ($entities as $entity) {
-    // Add a random stamp for every load(), so that during tests, we can tell
-    // if an entity was retrieved from cache (unchanged stamp) or reloaded.
-    $entity->_loadStamp = $random->string(8, TRUE);
-  }
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function config_entity_static_cache_test_entity_type_alter(array &$entity_types): void {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  $entity_types['config_test']->set('static_cache', TRUE);
-}
diff --git a/core/modules/config/tests/config_entity_static_cache_test/src/Hook/ConfigEntityStaticCacheTestHooks.php b/core/modules/config/tests/config_entity_static_cache_test/src/Hook/ConfigEntityStaticCacheTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..92e617c3b2f1d62b158695681da9fe45186cd41a
--- /dev/null
+++ b/core/modules/config/tests/config_entity_static_cache_test/src/Hook/ConfigEntityStaticCacheTestHooks.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\config_entity_static_cache_test\Hook;
+
+use Drupal\Component\Utility\Random;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for config_entity_static_cache_test.
+ */
+class ConfigEntityStaticCacheTestHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_load() for 'static_cache_test_config_test'.
+   */
+  #[Hook('config_test_load')]
+  public function configTestLoad($entities) {
+    static $random;
+    if (!$random) {
+      $random = new Random();
+    }
+    foreach ($entities as $entity) {
+      // Add a random stamp for every load(), so that during tests, we can tell
+      // if an entity was retrieved from cache (unchanged stamp) or reloaded.
+      $entity->_loadStamp = $random->string(8, TRUE);
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    $entity_types['config_test']->set('static_cache', TRUE);
+  }
+
+}
diff --git a/core/modules/config/tests/config_import_test/config_import_test.module b/core/modules/config/tests/config_import_test/config_import_test.module
index 766e3c417c2ba132186823e3f8009b0e44726598..efb5f189ada272da5cbcacad38ce2e59a05ac569 100644
--- a/core/modules/config/tests/config_import_test/config_import_test.module
+++ b/core/modules/config/tests/config_import_test/config_import_test.module
@@ -9,13 +9,6 @@
 
 use Drupal\Core\Config\ConfigImporter;
 
-/**
- * Implements hook_config_import_steps_alter().
- */
-function config_import_test_config_import_steps_alter(&$sync_steps) {
-  $sync_steps[] = '_config_import_test_config_import_steps_alter';
-}
-
 /**
  * Implements configuration synchronization step added by an alter for testing.
  *
diff --git a/core/modules/config/tests/config_import_test/src/Hook/ConfigImportTestHooks.php b/core/modules/config/tests/config_import_test/src/Hook/ConfigImportTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..4a1c7d2c65737d030aa213a34934137eaf3645e7
--- /dev/null
+++ b/core/modules/config/tests/config_import_test/src/Hook/ConfigImportTestHooks.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\config_import_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for config_import_test.
+ */
+class ConfigImportTestHooks {
+
+  /**
+   * Implements hook_config_import_steps_alter().
+   */
+  #[Hook('config_import_steps_alter')]
+  public function configImportStepsAlter(&$sync_steps) {
+    $sync_steps[] = '_config_import_test_config_import_steps_alter';
+  }
+
+}
diff --git a/core/modules/config/tests/config_install_dependency_test/config_install_dependency_test.module b/core/modules/config/tests/config_install_dependency_test/config_install_dependency_test.module
deleted file mode 100644
index 47736c39005090f9858b0d964194067b9810c8f7..0000000000000000000000000000000000000000
--- a/core/modules/config/tests/config_install_dependency_test/config_install_dependency_test.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides hook implementations for testing purposes.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityInterface;
-
-/**
- * Implements hook_ENTITY_TYPE_create().
- */
-function config_install_dependency_test_config_test_create(EntityInterface $entity) {
-  // Add an enforced dependency on this module so that we can test if this is
-  // possible during module installation.
-  $entity->setEnforcedDependencies(['module' => ['config_install_dependency_test']]);
-}
diff --git a/core/modules/config/tests/config_install_dependency_test/src/Hook/ConfigInstallDependencyTestHooks.php b/core/modules/config/tests/config_install_dependency_test/src/Hook/ConfigInstallDependencyTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..72c99b89915913e8a507c6de086e083ca9c57eab
--- /dev/null
+++ b/core/modules/config/tests/config_install_dependency_test/src/Hook/ConfigInstallDependencyTestHooks.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\config_install_dependency_test\Hook;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for config_install_dependency_test.
+ */
+class ConfigInstallDependencyTestHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_create().
+   */
+  #[Hook('config_test_create')]
+  public function configTestCreate(EntityInterface $entity) {
+    // Add an enforced dependency on this module so that we can test if this is
+    // possible during module installation.
+    $entity->setEnforcedDependencies(['module' => ['config_install_dependency_test']]);
+  }
+
+}
diff --git a/core/modules/config/tests/config_override_message_test/config_override_message_test.module b/core/modules/config/tests/config_override_message_test/config_override_message_test.module
deleted file mode 100644
index 3ccbf84fad9bd04569d3d7205c85ae9e3cff2cea..0000000000000000000000000000000000000000
--- a/core/modules/config/tests/config_override_message_test/config_override_message_test.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Tests configuration override message functionality.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function config_override_message_test_form_system_site_information_settings_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
-  // Set a weight to a negative amount to ensure the config overrides message
-  // is above it.
-  $form['site_information']['#weight'] = -5;
-}
diff --git a/core/modules/config/tests/config_override_message_test/src/Hook/ConfigOverrideMessageTestHooks.php b/core/modules/config/tests/config_override_message_test/src/Hook/ConfigOverrideMessageTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..34d8c1a44a7b03f8feec1ee5a92db96cf554133e
--- /dev/null
+++ b/core/modules/config/tests/config_override_message_test/src/Hook/ConfigOverrideMessageTestHooks.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\config_override_message_test\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for config_override_message_test.
+ */
+class ConfigOverrideMessageTestHooks {
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_system_site_information_settings_alter')]
+  public function formSystemSiteInformationSettingsAlter(array &$form, FormStateInterface $form_state, string $form_id) : void {
+    // Set a weight to a negative amount to ensure the config overrides message
+    // is above it.
+    $form['site_information']['#weight'] = -5;
+  }
+
+}
diff --git a/core/modules/config/tests/config_schema_test/config_schema_test.module b/core/modules/config/tests/config_schema_test/config_schema_test.module
deleted file mode 100644
index 8ef2d716ce67f4b1e192620cccc4dd05470ad5fe..0000000000000000000000000000000000000000
--- a/core/modules/config/tests/config_schema_test/config_schema_test.module
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-/**
- * @file
- * Tests configuration schema functionality.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_config_schema_info_alter().
- */
-function config_schema_test_config_schema_info_alter(&$definitions) {
-  if (\Drupal::state()->get('config_schema_test_exception_add')) {
-    $definitions['config_schema_test.hook_added_definition'] = $definitions['config_schema_test.hook'];
-  }
-  if (\Drupal::state()->get('config_schema_test_exception_remove')) {
-    unset($definitions['config_schema_test.hook']);
-  }
-
-  // Since code can not be unloaded only alter the definition if it exists.
-  if (isset($definitions['config_schema_test.hook'])) {
-    $definitions['config_schema_test.hook']['additional_metadata'] = 'new schema info';
-  }
-
-  // @see \Drupal\KernelTests\Core\TypedData\ValidKeysConstraintValidatorTest
-  if (\Drupal::state()->get('config_schema_test_block_fully_validatable')) {
-    $definitions['block.block.*']['constraints']['FullyValidatable'] = NULL;
-  }
-  else {
-    unset($definitions['block.block.*']['constraints']);
-  }
-
-  // @see \Drupal\Tests\node\Kernel\NodeTypeValidationTest::testThirdPartySettingsMenuUi()
-  if (\Drupal::state()->get('config_schema_test_menu_ui_third_party_settings_fully_validatable')) {
-    $definitions['node.type.*.third_party.menu_ui']['constraints']['FullyValidatable'] = NULL;
-  }
-  else {
-    unset($definitions['node.type.*.third_party.menu_ui']['constraints']);
-  }
-}
diff --git a/core/modules/config/tests/config_schema_test/src/Hook/ConfigSchemaTestHooks.php b/core/modules/config/tests/config_schema_test/src/Hook/ConfigSchemaTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..7901b84fa2d3279c1e65a54e5040010087d6e44a
--- /dev/null
+++ b/core/modules/config/tests/config_schema_test/src/Hook/ConfigSchemaTestHooks.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\config_schema_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for config_schema_test.
+ */
+class ConfigSchemaTestHooks {
+
+  /**
+   * Implements hook_config_schema_info_alter().
+   */
+  #[Hook('config_schema_info_alter')]
+  public function configSchemaInfoAlter(&$definitions) {
+    if (\Drupal::state()->get('config_schema_test_exception_add')) {
+      $definitions['config_schema_test.hook_added_definition'] = $definitions['config_schema_test.hook'];
+    }
+    if (\Drupal::state()->get('config_schema_test_exception_remove')) {
+      unset($definitions['config_schema_test.hook']);
+    }
+    // Since code can not be unloaded only alter the definition if it exists.
+    if (isset($definitions['config_schema_test.hook'])) {
+      $definitions['config_schema_test.hook']['additional_metadata'] = 'new schema info';
+    }
+    // @see \Drupal\KernelTests\Core\TypedData\ValidKeysConstraintValidatorTest
+    if (\Drupal::state()->get('config_schema_test_block_fully_validatable')) {
+      $definitions['block.block.*']['constraints']['FullyValidatable'] = NULL;
+    }
+    else {
+      unset($definitions['block.block.*']['constraints']);
+    }
+    // @see \Drupal\Tests\node\Kernel\NodeTypeValidationTest::testThirdPartySettingsMenuUi()
+    if (\Drupal::state()->get('config_schema_test_menu_ui_third_party_settings_fully_validatable')) {
+      $definitions['node.type.*.third_party.menu_ui']['constraints']['FullyValidatable'] = NULL;
+    }
+    else {
+      unset($definitions['node.type.*.third_party.menu_ui']['constraints']);
+    }
+  }
+
+}
diff --git a/core/modules/config/tests/config_test/config_test.hooks.inc b/core/modules/config/tests/config_test/config_test.hooks.inc
deleted file mode 100644
index 7712cafdb0b166ede10c7f107256085e8b396286..0000000000000000000000000000000000000000
--- a/core/modules/config/tests/config_test/config_test.hooks.inc
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-
-/**
- * @file
- * Fake third-party hook implementations for ConfigTest entities.
- *
- * Testing the module/hook system is not the purpose of this test helper module.
- * Therefore, this file implements hooks on behalf of config_test module for
- * config_test entity hooks themselves.
- */
-
-declare(strict_types=1);
-
-use Drupal\config_test\Entity\ConfigTest;
-
-/**
- * Implements hook_config_test_load().
- */
-function config_test_config_test_load() {
-  $GLOBALS['hook_config_test']['load'] = __FUNCTION__;
-}
-
-/**
- * Implements hook_ENTITY_TYPE_create() for 'config_test'.
- */
-function config_test_config_test_create(ConfigTest $config_test) {
-  if (\Drupal::state()->get('config_test.prepopulate')) {
-    $config_test->set('foo', 'baz');
-  }
-  _config_test_update_is_syncing_store('create', $config_test);
-}
-
-/**
- * Implements hook_config_test_presave().
- */
-function config_test_config_test_presave(ConfigTest $config_test) {
-  $GLOBALS['hook_config_test']['presave'] = __FUNCTION__;
-  _config_test_update_is_syncing_store('presave', $config_test);
-}
-
-/**
- * Implements hook_config_test_insert().
- */
-function config_test_config_test_insert(ConfigTest $config_test) {
-  $GLOBALS['hook_config_test']['insert'] = __FUNCTION__;
-  _config_test_update_is_syncing_store('insert', $config_test);
-}
-
-/**
- * Implements hook_config_test_update().
- */
-function config_test_config_test_update(ConfigTest $config_test) {
-  $GLOBALS['hook_config_test']['update'] = __FUNCTION__;
-  _config_test_update_is_syncing_store('update', $config_test);
-}
-
-/**
- * Implements hook_config_test_predelete().
- */
-function config_test_config_test_predelete(ConfigTest $config_test) {
-  $GLOBALS['hook_config_test']['predelete'] = __FUNCTION__;
-  _config_test_update_is_syncing_store('predelete', $config_test);
-}
-
-/**
- * Implements hook_config_test_delete().
- */
-function config_test_config_test_delete(ConfigTest $config_test) {
-  $GLOBALS['hook_config_test']['delete'] = __FUNCTION__;
-  _config_test_update_is_syncing_store('delete', $config_test);
-}
-
-/**
- * Helper function for testing hooks during configuration sync.
- *
- * @param string $hook
- *   The fired hook.
- * @param \Drupal\config_test\Entity\ConfigTest $config_test
- *   The ConfigTest entity.
- */
-function _config_test_update_is_syncing_store($hook, ConfigTest $config_test) {
-  $current_value = \Drupal::state()->get('config_test.store_isSyncing', FALSE);
-  if ($current_value !== FALSE) {
-    $current_value['global_state::' . $hook] = \Drupal::isConfigSyncing();
-    $current_value['entity_state::' . $hook] = $config_test->isSyncing();
-    \Drupal::state()->set('config_test.store_isSyncing', $current_value);
-  }
-}
diff --git a/core/modules/config/tests/config_test/config_test.module b/core/modules/config/tests/config_test/config_test.module
deleted file mode 100644
index c1b1a8354e1f558ca5fc6e268354255c3fb40989..0000000000000000000000000000000000000000
--- a/core/modules/config/tests/config_test/config_test.module
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides Config module hook implementations for testing purposes.
- */
-
-declare(strict_types=1);
-
-require_once dirname(__FILE__) . '/config_test.hooks.inc';
-
-use Drupal\Core\Entity\Query\QueryInterface;
-
-/**
- * Implements hook_cache_flush().
- */
-function config_test_cache_flush() {
-  // Set a global value we can check in test code.
-  $GLOBALS['hook_cache_flush'] = __FUNCTION__;
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function config_test_entity_type_alter(array &$entity_types): void {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  // The 'translatable' entity key is not supposed to change over time. In this
-  // case we can safely do it because we set it once and we do not change it for
-  // all the duration of the test session.
-  $entity_types['config_test']->set('translatable', \Drupal::service('state')->get('config_test.translatable'));
-
-  // Create a clone of config_test that does not have a status.
-  $entity_types['config_test_no_status'] = clone $entity_types['config_test'];
-  $config_test_no_status = &$entity_types['config_test_no_status'];
-  $config_test_no_status->setLinkTemplate('edit-form', '/admin/structure/config_test/manage/{config_test_no_status}');
-  $config_test_no_status->setLinkTemplate('delete-form', '/admin/structure/config_test/manage/{config_test_no_status}/delete');
-
-  $keys = $config_test_no_status->getKeys();
-  unset($keys['status']);
-  $config_test_no_status->set('id', 'config_test_no_status');
-  $config_test_no_status->set('entity_keys', $keys);
-  $config_test_no_status->set('config_prefix', 'no_status');
-  $config_test_no_status->set('mergedConfigExport', ['id' => 'id', 'label' => 'label', 'uuid' => 'uuid', 'langcode' => 'langcode']);
-  if (\Drupal::service('state')->get('config_test.lookup_keys', FALSE)) {
-    $entity_types['config_test']->set('lookup_keys', ['uuid', 'style']);
-  }
-
-  if (\Drupal::service('state')->get('config_test.class_override', FALSE)) {
-    $entity_types['config_test']->setClass(\Drupal::service('state')->get('config_test.class_override'));
-  }
-}
-
-/**
- * Implements hook_entity_query_tag__ENTITY_TYPE__TAG_alter().
- *
- * Entity type is 'config_query_test' and tag is
- * 'config_entity_query_alter_hook_test'.
- *
- * @see Drupal\KernelTests\Core\Entity\ConfigEntityQueryTest::testAlterHook
- */
-function config_test_entity_query_tag__config_query_test__config_entity_query_alter_hook_test_alter(QueryInterface $query): void {
-  $query->condition('id', '7', '<>');
-}
diff --git a/core/modules/config/tests/config_test/src/Hook/ConfigTestHooks.php b/core/modules/config/tests/config_test/src/Hook/ConfigTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..7e570ea39ceafeeba4692f7d4000ec718d824bb3
--- /dev/null
+++ b/core/modules/config/tests/config_test/src/Hook/ConfigTestHooks.php
@@ -0,0 +1,66 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\config_test\Hook;
+
+use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for config_test.
+ */
+class ConfigTestHooks {
+
+  /**
+   * Implements hook_cache_flush().
+   */
+  #[Hook('cache_flush')]
+  public function cacheFlush() {
+    // Set a global value we can check in test code.
+    $GLOBALS['hook_cache_flush'] = 'config_test_cache_flush';
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    // The 'translatable' entity key is not supposed to change over time. In this
+    // case we can safely do it because we set it once and we do not change it for
+    // all the duration of the test session.
+    $entity_types['config_test']->set('translatable', \Drupal::service('state')->get('config_test.translatable'));
+    // Create a clone of config_test that does not have a status.
+    $entity_types['config_test_no_status'] = clone $entity_types['config_test'];
+    $config_test_no_status =& $entity_types['config_test_no_status'];
+    $config_test_no_status->setLinkTemplate('edit-form', '/admin/structure/config_test/manage/{config_test_no_status}');
+    $config_test_no_status->setLinkTemplate('delete-form', '/admin/structure/config_test/manage/{config_test_no_status}/delete');
+    $keys = $config_test_no_status->getKeys();
+    unset($keys['status']);
+    $config_test_no_status->set('id', 'config_test_no_status');
+    $config_test_no_status->set('entity_keys', $keys);
+    $config_test_no_status->set('config_prefix', 'no_status');
+    $config_test_no_status->set('mergedConfigExport', ['id' => 'id', 'label' => 'label', 'uuid' => 'uuid', 'langcode' => 'langcode']);
+    if (\Drupal::service('state')->get('config_test.lookup_keys', FALSE)) {
+      $entity_types['config_test']->set('lookup_keys', ['uuid', 'style']);
+    }
+    if (\Drupal::service('state')->get('config_test.class_override', FALSE)) {
+      $entity_types['config_test']->setClass(\Drupal::service('state')->get('config_test.class_override'));
+    }
+  }
+
+  /**
+   * Implements hook_entity_query_tag__ENTITY_TYPE__TAG_alter().
+   *
+   * Entity type is 'config_query_test' and tag is
+   * 'config_entity_query_alter_hook_test'.
+   *
+   * @see Drupal\KernelTests\Core\Entity\ConfigEntityQueryTest::testAlterHook
+   */
+  #[Hook('entity_query_tag__config_query_test__config_entity_query_alter_hook_test_alter')]
+  public function entityQueryTagConfigQueryTestConfigEntityQueryAlterHookTestAlter(QueryInterface $query) : void {
+    $query->condition('id', '7', '<>');
+  }
+
+}
diff --git a/core/modules/config/tests/config_test/src/Hook/ConfigTestHooksHooks.php b/core/modules/config/tests/config_test/src/Hook/ConfigTestHooksHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..7656407408659b9bc9b1093e081ec4c86466abc9
--- /dev/null
+++ b/core/modules/config/tests/config_test/src/Hook/ConfigTestHooksHooks.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\config_test\Hook;
+
+use Drupal\config_test\Entity\ConfigTest;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for config_test.
+ */
+class ConfigTestHooksHooks {
+
+  /**
+   * Implements hook_config_test_load().
+   */
+  #[Hook('config_test_load')]
+  public function configTestLoad() {
+    $GLOBALS['hook_config_test']['load'] = 'config_test_config_test_load';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_create() for 'config_test'.
+   */
+  #[Hook('config_test_create')]
+  public function configTestCreate(ConfigTest $config_test) {
+    if (\Drupal::state()->get('config_test.prepopulate')) {
+      $config_test->set('foo', 'baz');
+    }
+    $this->updateIsSyncingStore('create', $config_test);
+  }
+
+  /**
+   * Implements hook_config_test_presave().
+   */
+  #[Hook('config_test_presave')]
+  public function configTestPresave(ConfigTest $config_test) {
+    $GLOBALS['hook_config_test']['presave'] = 'config_test_config_test_presave';
+    $this->updateIsSyncingStore('presave', $config_test);
+  }
+
+  /**
+   * Implements hook_config_test_insert().
+   */
+  #[Hook('config_test_insert')]
+  public function configTestInsert(ConfigTest $config_test) {
+    $GLOBALS['hook_config_test']['insert'] = 'config_test_config_test_insert';
+    $this->updateIsSyncingStore('insert', $config_test);
+  }
+
+  /**
+   * Implements hook_config_test_update().
+   */
+  #[Hook('config_test_update')]
+  public function configTestUpdate(ConfigTest $config_test) {
+    $GLOBALS['hook_config_test']['update'] = 'config_test_config_test_update';
+    $this->updateIsSyncingStore('update', $config_test);
+  }
+
+  /**
+   * Implements hook_config_test_predelete().
+   */
+  #[Hook('config_test_predelete')]
+  public function configTestPredelete(ConfigTest $config_test) {
+    $GLOBALS['hook_config_test']['predelete'] = 'config_test_config_test_predelete';
+    $this->updateIsSyncingStore('predelete', $config_test);
+  }
+
+  /**
+   * Implements hook_config_test_delete().
+   */
+  #[Hook('config_test_delete')]
+  public function configTestDelete(ConfigTest $config_test) {
+    $GLOBALS['hook_config_test']['delete'] = 'config_test_config_test_delete';
+    $this->updateIsSyncingStore('delete', $config_test);
+  }
+
+  /**
+   * Helper function for testing hooks during configuration sync.
+   *
+   * @param string $hook
+   *   The fired hook.
+   * @param \Drupal\config_test\Entity\ConfigTest $config_test
+   *   The ConfigTest entity.
+   */
+  protected function updateIsSyncingStore($hook, ConfigTest $config_test) {
+    $current_value = \Drupal::state()->get('config_test.store_isSyncing', FALSE);
+    if ($current_value !== FALSE) {
+      $current_value['global_state::' . $hook] = \Drupal::isConfigSyncing();
+      $current_value['entity_state::' . $hook] = $config_test->isSyncing();
+      \Drupal::state()->set('config_test.store_isSyncing', $current_value);
+    }
+  }
+
+}
diff --git a/core/modules/config_translation/config_translation.module b/core/modules/config_translation/config_translation.module
deleted file mode 100644
index a4c738014e0d04a741a85c2f1eed94e939dc9628..0000000000000000000000000000000000000000
--- a/core/modules/config_translation/config_translation.module
+++ /dev/null
@@ -1,200 +0,0 @@
-<?php
-
-/**
- * @file
- * Configuration Translation module.
- */
-
-use Drupal\Core\Url;
-use Drupal\Core\Config\Entity\ConfigEntityInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\field\FieldConfigInterface;
-
-/**
- * Implements hook_help().
- */
-function config_translation_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.config_translation':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Configuration Translation module allows you to translate configuration text; for example, the site name, vocabularies, menus, or date formats. Together with the modules <a href=":language">Language</a>, <a href=":content-translation">Content Translation</a>, and <a href=":locale">Interface Translation</a>, it allows you to build multilingual websites. For more information, see the <a href=":doc_url">online documentation for the Configuration Translation module</a>.', [':doc_url' => 'https://www.drupal.org/documentation/modules/config_translation', ':config' => Url::fromRoute('help.page', ['name' => 'config'])->toString(), ':language' => Url::fromRoute('help.page', ['name' => 'language'])->toString(), ':locale' => Url::fromRoute('help.page', ['name' => 'locale'])->toString(), ':content-translation' => (\Drupal::moduleHandler()->moduleExists('content_translation')) ? Url::fromRoute('help.page', ['name' => 'content_translation'])->toString() : '#']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Enabling translation') . '</dt>';
-      $output .= '<dd>' . t('In order to translate configuration, the website must have at least two <a href=":url">languages</a>.', [':url' => Url::fromRoute('entity.configurable_language.collection')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Translating configuration text') . '</dt>';
-      $output .= '<dd>' . t('Users with the <em>Translate user edited configuration</em> permission can access the configuration translation overview, and manage translations for specific languages. The <a href=":translation-page">Configuration translation</a> page shows a list of all configuration text that can be translated, either as individual items or as lists. After you click on <em>Translate</em>, you are provided with a list of all languages. You can <em>add</em> or <em>edit</em> a translation for a specific language. Users with specific configuration permissions can also <em>edit</em> the text for the site\'s default language. For some configuration text items (for example for the site information), the specific translation pages can also be accessed directly from their configuration pages.', [':translation-page' => Url::fromRoute('config_translation.mapper_list')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Translating date formats') . '</dt>';
-      $output .= '<dd>' . t('You can choose to translate date formats on the <a href=":translation-page">Configuration translation</a> page. This allows you not only to translate the label text, but also to set a language-specific <em>PHP date format</em>.', [':translation-page' => Url::fromRoute('config_translation.mapper_list')->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'config_translation.mapper_list':
-      $output = '<p>' . t('This page lists all configuration items on your site that have translatable text, like your site name, role names, etc.') . '</p>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function config_translation_theme(): array {
-  return [
-    'config_translation_manage_form_element' => [
-      'render element' => 'element',
-      'template' => 'config_translation_manage_form_element',
-    ],
-  ];
-}
-
-/**
- * Implements hook_themes_installed().
- */
-function config_translation_themes_installed() {
-  // Themes can provide *.config_translation.yml declarations.
-  // @todo Make ThemeHandler trigger an event instead and make
-  //   ConfigMapperManager plugin manager subscribe to it.
-  // @see https://www.drupal.org/node/2206347
-  \Drupal::service('plugin.manager.config_translation.mapper')->clearCachedDefinitions();
-}
-
-/**
- * Implements hook_themes_uninstalled().
- */
-function config_translation_themes_uninstalled() {
-  // Themes can provide *.config_translation.yml declarations.
-  // @todo Make ThemeHandler trigger an event instead and make
-  //   ConfigMapperManager plugin manager subscribe to it.
-  // @see https://www.drupal.org/node/2206347
-  \Drupal::service('plugin.manager.config_translation.mapper')->clearCachedDefinitions();
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function config_translation_entity_type_alter(array &$entity_types): void {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  foreach ($entity_types as $entity_type_id => $entity_type) {
-    if ($entity_type->entityClassImplements(ConfigEntityInterface::class)) {
-      if ($entity_type_id == 'block') {
-        $class = 'Drupal\config_translation\Controller\ConfigTranslationBlockListBuilder';
-      }
-      elseif ($entity_type_id == 'field_config') {
-        $class = 'Drupal\config_translation\Controller\ConfigTranslationFieldListBuilder';
-        // Will be filled in dynamically, see \Drupal\field\Entity\FieldConfig::linkTemplates().
-        $entity_type->setLinkTemplate('config-translation-overview', $entity_type->getLinkTemplate('edit-form') . '/translate');
-      }
-      else {
-        $class = 'Drupal\config_translation\Controller\ConfigTranslationEntityListBuilder';
-      }
-      $entity_type->setHandlerClass('config_translation_list', $class);
-
-      if ($entity_type->hasLinkTemplate('edit-form')) {
-        $entity_type->setLinkTemplate('config-translation-overview', $entity_type->getLinkTemplate('edit-form') . '/translate');
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_config_translation_info().
- */
-function config_translation_config_translation_info(&$info) {
-  $entity_type_manager = \Drupal::entityTypeManager();
-
-  // If field UI is not enabled, the base routes of the type
-  // "entity.field_config.{$entity_type}_field_edit_form" are not defined.
-  if (\Drupal::moduleHandler()->moduleExists('field_ui')) {
-    // Add fields entity mappers to all fieldable entity types defined.
-    foreach ($entity_type_manager->getDefinitions() as $entity_type_id => $entity_type) {
-      // Make sure entity type has field UI enabled and has a base route.
-      if ($entity_type->get('field_ui_base_route')) {
-        $info[$entity_type_id . '_fields'] = [
-          'base_route_name' => "entity.field_config.{$entity_type_id}_field_edit_form",
-          'entity_type' => 'field_config',
-          'class' => '\Drupal\config_translation\ConfigFieldMapper',
-          'base_entity_type' => $entity_type_id,
-          'weight' => 10,
-        ];
-      }
-    }
-  }
-
-  // Discover configuration entities automatically.
-  foreach ($entity_type_manager->getDefinitions() as $entity_type_id => $entity_type) {
-    // Determine base path for entities automatically if provided via the
-    // configuration entity.
-    if (
-      !$entity_type->entityClassImplements(ConfigEntityInterface::class) ||
-      !$entity_type->hasLinkTemplate('edit-form')
-    ) {
-      // Do not record this entity mapper if the entity type does not
-      // provide a base route. We'll surely not be able to do anything with
-      // it anyway. Configuration entities with a dynamic base path, such as
-      // fields, need special treatment. See above.
-      continue;
-    }
-
-    // Use the entity type as the plugin ID.
-    $base_route_name = "entity.$entity_type_id.edit_form";
-    $info[$entity_type_id] = [
-      'class' => '\Drupal\config_translation\ConfigEntityMapper',
-      'base_route_name' => $base_route_name,
-      'title' => $entity_type->getSingularLabel(),
-      'names' => [],
-      'entity_type' => $entity_type_id,
-      'weight' => 10,
-    ];
-  }
-}
-
-/**
- * Implements hook_entity_operation().
- */
-function config_translation_entity_operation(EntityInterface $entity) {
-  $operations = [];
-  $entity_type = $entity->getEntityType();
-  if ($entity_type->entityClassImplements(ConfigEntityInterface::class) &&
-    $entity->hasLinkTemplate('config-translation-overview') &&
-    \Drupal::currentUser()->hasPermission('translate configuration')) {
-
-    $link_template = 'config-translation-overview';
-    if ($entity instanceof FieldConfigInterface) {
-      $link_template = "config-translation-overview.{$entity->getTargetEntityTypeId()}";
-    }
-
-    $operations['translate'] = [
-      'title' => t('Translate'),
-      'weight' => 50,
-      'url' => $entity->toUrl($link_template),
-    ];
-  }
-
-  return $operations;
-}
-
-/**
- * Implements hook_config_schema_info_alter().
- */
-function config_translation_config_schema_info_alter(&$definitions) {
-  $map = [
-    'label' => '\Drupal\config_translation\FormElement\Textfield',
-    'text' => '\Drupal\config_translation\FormElement\Textarea',
-    'date_format' => '\Drupal\config_translation\FormElement\DateFormat',
-    'text_format' => '\Drupal\config_translation\FormElement\TextFormat',
-    'mapping' => '\Drupal\config_translation\FormElement\ListElement',
-    'sequence' => '\Drupal\config_translation\FormElement\ListElement',
-    'plural_label' => '\Drupal\config_translation\FormElement\PluralVariants',
-  ];
-
-  // Enhance the text and date type definitions with classes to generate proper
-  // form elements in ConfigTranslationFormBase. Other translatable types will
-  // appear as a one line textfield.
-  foreach ($definitions as $type => &$definition) {
-    if (isset($map[$type]) && !isset($definition['form_element_class'])) {
-      $definition['form_element_class'] = $map[$type];
-    }
-  }
-}
diff --git a/core/modules/config_translation/src/Hook/ConfigTranslationHooks.php b/core/modules/config_translation/src/Hook/ConfigTranslationHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..a9251d8b13cd5ac575bebbad7cab295a54eb8d97
--- /dev/null
+++ b/core/modules/config_translation/src/Hook/ConfigTranslationHooks.php
@@ -0,0 +1,220 @@
+<?php
+
+namespace Drupal\config_translation\Hook;
+
+use Drupal\field\FieldConfigInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for config_translation.
+ */
+class ConfigTranslationHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.config_translation':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Configuration Translation module allows you to translate configuration text; for example, the site name, vocabularies, menus, or date formats. Together with the modules <a href=":language">Language</a>, <a href=":content-translation">Content Translation</a>, and <a href=":locale">Interface Translation</a>, it allows you to build multilingual websites. For more information, see the <a href=":doc_url">online documentation for the Configuration Translation module</a>.', [
+          ':doc_url' => 'https://www.drupal.org/documentation/modules/config_translation',
+          ':config' => Url::fromRoute('help.page', [
+            'name' => 'config',
+          ])->toString(),
+          ':language' => Url::fromRoute('help.page', [
+            'name' => 'language',
+          ])->toString(),
+          ':locale' => Url::fromRoute('help.page', [
+            'name' => 'locale',
+          ])->toString(),
+          ':content-translation' => \Drupal::moduleHandler()->moduleExists('content_translation') ? Url::fromRoute('help.page', [
+            'name' => 'content_translation',
+          ])->toString() : '#',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Enabling translation') . '</dt>';
+        $output .= '<dd>' . t('In order to translate configuration, the website must have at least two <a href=":url">languages</a>.', [
+          ':url' => Url::fromRoute('entity.configurable_language.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Translating configuration text') . '</dt>';
+        $output .= '<dd>' . t('Users with the <em>Translate user edited configuration</em> permission can access the configuration translation overview, and manage translations for specific languages. The <a href=":translation-page">Configuration translation</a> page shows a list of all configuration text that can be translated, either as individual items or as lists. After you click on <em>Translate</em>, you are provided with a list of all languages. You can <em>add</em> or <em>edit</em> a translation for a specific language. Users with specific configuration permissions can also <em>edit</em> the text for the site\'s default language. For some configuration text items (for example for the site information), the specific translation pages can also be accessed directly from their configuration pages.', [
+          ':translation-page' => Url::fromRoute('config_translation.mapper_list')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Translating date formats') . '</dt>';
+        $output .= '<dd>' . t('You can choose to translate date formats on the <a href=":translation-page">Configuration translation</a> page. This allows you not only to translate the label text, but also to set a language-specific <em>PHP date format</em>.', [
+          ':translation-page' => Url::fromRoute('config_translation.mapper_list')->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'config_translation.mapper_list':
+        $output = '<p>' . t('This page lists all configuration items on your site that have translatable text, like your site name, role names, etc.') . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'config_translation_manage_form_element' => [
+        'render element' => 'element',
+        'template' => 'config_translation_manage_form_element',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_themes_installed().
+   */
+  #[Hook('themes_installed')]
+  public function themesInstalled() {
+    // Themes can provide *.config_translation.yml declarations.
+    // @todo Make ThemeHandler trigger an event instead and make
+    //   ConfigMapperManager plugin manager subscribe to it.
+    // @see https://www.drupal.org/node/2206347
+    \Drupal::service('plugin.manager.config_translation.mapper')->clearCachedDefinitions();
+  }
+
+  /**
+   * Implements hook_themes_uninstalled().
+   */
+  #[Hook('themes_uninstalled')]
+  public function themesUninstalled() {
+    // Themes can provide *.config_translation.yml declarations.
+    // @todo Make ThemeHandler trigger an event instead and make
+    //   ConfigMapperManager plugin manager subscribe to it.
+    // @see https://www.drupal.org/node/2206347
+    \Drupal::service('plugin.manager.config_translation.mapper')->clearCachedDefinitions();
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    foreach ($entity_types as $entity_type_id => $entity_type) {
+      if ($entity_type->entityClassImplements(ConfigEntityInterface::class)) {
+        if ($entity_type_id == 'block') {
+          $class = 'Drupal\config_translation\Controller\ConfigTranslationBlockListBuilder';
+        }
+        elseif ($entity_type_id == 'field_config') {
+          $class = 'Drupal\config_translation\Controller\ConfigTranslationFieldListBuilder';
+          // Will be filled in dynamically, see \Drupal\field\Entity\FieldConfig::linkTemplates().
+          $entity_type->setLinkTemplate('config-translation-overview', $entity_type->getLinkTemplate('edit-form') . '/translate');
+        }
+        else {
+          $class = 'Drupal\config_translation\Controller\ConfigTranslationEntityListBuilder';
+        }
+        $entity_type->setHandlerClass('config_translation_list', $class);
+        if ($entity_type->hasLinkTemplate('edit-form')) {
+          $entity_type->setLinkTemplate('config-translation-overview', $entity_type->getLinkTemplate('edit-form') . '/translate');
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_config_translation_info().
+   */
+  #[Hook('config_translation_info')]
+  public function configTranslationInfo(&$info) {
+    $entity_type_manager = \Drupal::entityTypeManager();
+    // If field UI is not enabled, the base routes of the type
+    // "entity.field_config.{$entity_type}_field_edit_form" are not defined.
+    if (\Drupal::moduleHandler()->moduleExists('field_ui')) {
+      // Add fields entity mappers to all fieldable entity types defined.
+      foreach ($entity_type_manager->getDefinitions() as $entity_type_id => $entity_type) {
+        // Make sure entity type has field UI enabled and has a base route.
+        if ($entity_type->get('field_ui_base_route')) {
+          $info[$entity_type_id . '_fields'] = [
+            'base_route_name' => "entity.field_config.{$entity_type_id}_field_edit_form",
+            'entity_type' => 'field_config',
+            'class' => '\Drupal\config_translation\ConfigFieldMapper',
+            'base_entity_type' => $entity_type_id,
+            'weight' => 10,
+          ];
+        }
+      }
+    }
+    // Discover configuration entities automatically.
+    foreach ($entity_type_manager->getDefinitions() as $entity_type_id => $entity_type) {
+      // Determine base path for entities automatically if provided via the
+      // configuration entity.
+      if (!$entity_type->entityClassImplements(ConfigEntityInterface::class) || !$entity_type->hasLinkTemplate('edit-form')) {
+        // Do not record this entity mapper if the entity type does not
+        // provide a base route. We'll surely not be able to do anything with
+        // it anyway. Configuration entities with a dynamic base path, such as
+        // fields, need special treatment. See above.
+        continue;
+      }
+      // Use the entity type as the plugin ID.
+      $base_route_name = "entity.{$entity_type_id}.edit_form";
+      $info[$entity_type_id] = [
+        'class' => '\Drupal\config_translation\ConfigEntityMapper',
+        'base_route_name' => $base_route_name,
+        'title' => $entity_type->getSingularLabel(),
+        'names' => [],
+        'entity_type' => $entity_type_id,
+        'weight' => 10,
+      ];
+    }
+  }
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $entity) {
+    $operations = [];
+    $entity_type = $entity->getEntityType();
+    if ($entity_type->entityClassImplements(ConfigEntityInterface::class) && $entity->hasLinkTemplate('config-translation-overview') && \Drupal::currentUser()->hasPermission('translate configuration')) {
+      $link_template = 'config-translation-overview';
+      if ($entity instanceof FieldConfigInterface) {
+        $link_template = "config-translation-overview.{$entity->getTargetEntityTypeId()}";
+      }
+      $operations['translate'] = [
+        'title' => t('Translate'),
+        'weight' => 50,
+        'url' => $entity->toUrl($link_template),
+      ];
+    }
+    return $operations;
+  }
+
+  /**
+   * Implements hook_config_schema_info_alter().
+   */
+  #[Hook('config_schema_info_alter')]
+  public function configSchemaInfoAlter(&$definitions) {
+    $map = [
+      'label' => '\Drupal\config_translation\FormElement\Textfield',
+      'text' => '\Drupal\config_translation\FormElement\Textarea',
+      'date_format' => '\Drupal\config_translation\FormElement\DateFormat',
+      'text_format' => '\Drupal\config_translation\FormElement\TextFormat',
+      'mapping' => '\Drupal\config_translation\FormElement\ListElement',
+      'sequence' => '\Drupal\config_translation\FormElement\ListElement',
+      'plural_label' => '\Drupal\config_translation\FormElement\PluralVariants',
+    ];
+    // Enhance the text and date type definitions with classes to generate proper
+    // form elements in ConfigTranslationFormBase. Other translatable types will
+    // appear as a one line textfield.
+    foreach ($definitions as $type => &$definition) {
+      if (isset($map[$type]) && !isset($definition['form_element_class'])) {
+        $definition['form_element_class'] = $map[$type];
+      }
+    }
+  }
+
+}
diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module
deleted file mode 100644
index 6f34ff4dd30b3216baf958a3bd0669b31caca06b..0000000000000000000000000000000000000000
--- a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-
-/**
- * @file
- * Configuration Translation Test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Extension\Extension;
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * Implements hook_system_info_alter().
- */
-function config_translation_test_system_info_alter(array &$info, Extension $file, $type) {
-  // @see \Drupal\config_translation\Tests\ConfigTranslationUiThemeTest
-  if ($file->getType() == 'theme' && $file->getName() == 'config_translation_test_theme') {
-    $info['hidden'] = FALSE;
-  }
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function config_translation_test_entity_type_alter(array &$entity_types): void {
-  // Remove entity definition for these entity types from config_test module.
-  unset($entity_types['config_test_no_status']);
-  unset($entity_types['config_query_test']);
-}
-
-/**
- * Implements hook_config_translation_info_alter().
- */
-function config_translation_test_config_translation_info_alter(&$info) {
-  if (\Drupal::state()->get('config_translation_test_config_translation_info_alter')) {
-    // Limit account settings config files to only one of them.
-    $info['entity.user.admin_form']['names'] = ['user.settings'];
-
-    // Add one more config file to the site information page.
-    $info['system.site_information_settings']['names'][] = 'system.rss';
-  }
-}
-
-/**
- * Implements hook_form_BASE_FORM_ID_alter() for ConfigTranslationFormBase.
- *
- * Adds a list of configuration names to the top of the configuration
- * translation form.
- *
- * @see \Drupal\config_translation\Form\ConfigTranslationFormBase
- */
-function config_translation_test_form_config_translation_form_alter(&$form, FormStateInterface $form_state): void {
-  if (\Drupal::state()->get('config_translation_test_alter_form_alter')) {
-    $form['#base_altered'] = TRUE;
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for ConfigTranslationAddForm.
- *
- * Changes the title to include the source language.
- *
- * @see \Drupal\config_translation\Form\ConfigTranslationAddForm
- */
-function config_translation_test_form_config_translation_add_form_alter(&$form, FormStateInterface $form_state): void {
-  if (\Drupal::state()->get('config_translation_test_alter_form_alter')) {
-    $form['#altered'] = TRUE;
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for ConfigTranslationEditForm.
- *
- * Adds a column to the configuration translation edit form that shows the
- * current translation. Note that this column would not be displayed by default,
- * as the columns are hardcoded in
- * config_translation_manage_form_element.html.twig. The template would need to
- * be overridden for the column to be displayed.
- *
- * @see \Drupal\config_translation\Form\ConfigTranslationEditForm
- */
-function config_translation_test_form_config_translation_edit_form_alter(&$form, FormStateInterface $form_state): void {
-  if (\Drupal::state()->get('config_translation_test_alter_form_alter')) {
-    $form['#altered'] = TRUE;
-  }
-}
diff --git a/core/modules/config_translation/tests/modules/config_translation_test/src/Hook/ConfigTranslationTestHooks.php b/core/modules/config_translation/tests/modules/config_translation_test/src/Hook/ConfigTranslationTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..3b8d38f5dce402c7478c40e320bc1dae38033850
--- /dev/null
+++ b/core/modules/config_translation/tests/modules/config_translation_test/src/Hook/ConfigTranslationTestHooks.php
@@ -0,0 +1,97 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\config_translation_test\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for config_translation_test.
+ */
+class ConfigTranslationTestHooks {
+
+  /**
+   * Implements hook_system_info_alter().
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(array &$info, Extension $file, $type) {
+    // @see \Drupal\config_translation\Tests\ConfigTranslationUiThemeTest
+    if ($file->getType() == 'theme' && $file->getName() == 'config_translation_test_theme') {
+      $info['hidden'] = FALSE;
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    // Remove entity definition for these entity types from config_test module.
+    unset($entity_types['config_test_no_status']);
+    unset($entity_types['config_query_test']);
+  }
+
+  /**
+   * Implements hook_config_translation_info_alter().
+   */
+  #[Hook('config_translation_info_alter')]
+  public function configTranslationInfoAlter(&$info) {
+    if (\Drupal::state()->get('config_translation_test_config_translation_info_alter')) {
+      // Limit account settings config files to only one of them.
+      $info['entity.user.admin_form']['names'] = ['user.settings'];
+      // Add one more config file to the site information page.
+      $info['system.site_information_settings']['names'][] = 'system.rss';
+    }
+  }
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter() for ConfigTranslationFormBase.
+   *
+   * Adds a list of configuration names to the top of the configuration
+   * translation form.
+   *
+   * @see \Drupal\config_translation\Form\ConfigTranslationFormBase
+   */
+  #[Hook('form_config_translation_form_alter')]
+  public function formConfigTranslationFormAlter(&$form, FormStateInterface $form_state) : void {
+    if (\Drupal::state()->get('config_translation_test_alter_form_alter')) {
+      $form['#base_altered'] = TRUE;
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for ConfigTranslationAddForm.
+   *
+   * Changes the title to include the source language.
+   *
+   * @see \Drupal\config_translation\Form\ConfigTranslationAddForm
+   */
+  #[Hook('form_config_translation_add_form_alter')]
+  public function formConfigTranslationAddFormAlter(&$form, FormStateInterface $form_state) : void {
+    if (\Drupal::state()->get('config_translation_test_alter_form_alter')) {
+      $form['#altered'] = TRUE;
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for ConfigTranslationEditForm.
+   *
+   * Adds a column to the configuration translation edit form that shows the
+   * current translation. Note that this column would not be displayed by default,
+   * as the columns are hardcoded in
+   * config_translation_manage_form_element.html.twig. The template would need to
+   * be overridden for the column to be displayed.
+   *
+   * @see \Drupal\config_translation\Form\ConfigTranslationEditForm
+   */
+  #[Hook('form_config_translation_edit_form_alter')]
+  public function formConfigTranslationEditFormAlter(&$form, FormStateInterface $form_state) : void {
+    if (\Drupal::state()->get('config_translation_test_alter_form_alter')) {
+      $form['#altered'] = TRUE;
+    }
+  }
+
+}
diff --git a/core/modules/contact/contact.module b/core/modules/contact/contact.module
index 0ef33eb97e87c5927f4f13fce67df95ba058db0c..9fa4732bfe0a207a999496f9e14005b1a261231c 100644
--- a/core/modules/contact/contact.module
+++ b/core/modules/contact/contact.module
@@ -2,206 +2,9 @@
 
 /**
  * @file
- * Enables the use of personal and site-wide contact forms.
  */
 
-use Drupal\Core\Url;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\contact\Plugin\rest\resource\ContactMessageResource;
-use Drupal\user\Entity\User;
-
-/**
- * Implements hook_help().
- */
-function contact_help($route_name, RouteMatchInterface $route_match) {
-
-  switch ($route_name) {
-    case 'help.page.contact':
-      $menu_page = \Drupal::moduleHandler()->moduleExists('menu_ui') ? Url::fromRoute('entity.menu.collection')->toString() : '#';
-      $block_page = \Drupal::moduleHandler()->moduleExists('block') ? Url::fromRoute('block.admin_display')->toString() : '#';
-      $contact_page = Url::fromRoute('entity.contact_form.collection')->toString();
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Contact module allows visitors to contact registered users on your site, using the personal contact form, and also allows you to set up site-wide contact forms. For more information, see the <a href=":contact">online documentation for the Contact module</a>.', [':contact' => 'https://www.drupal.org/documentation/modules/contact']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Using the personal contact form') . '</dt>';
-      $output .= '<dd>' . t("Site visitors can email registered users on your site by using the personal contact form, without knowing or learning the email address of the recipient. When a site visitor is viewing a user profile, the viewer will see a <em>Contact</em> tab or link, which leads to the personal contact form. The personal contact link is not shown when you are viewing your own profile, and users must have both <em>View user information</em> (to see user profiles) and <em>Use users' personal contact forms</em> permission to see the link. The user whose profile is being viewed must also have their personal contact form enabled (this is a user account setting); viewers with <em>Administer users</em> permission can bypass this setting.") . '</dd>';
-      $output .= '<dt>' . t('Configuring contact forms') . '</dt>';
-      $output .= '<dd>' . t('On the <a href=":contact_admin">Contact forms page</a>, you can configure the fields and display of the personal contact form, and you can set up one or more site-wide contact forms. Each site-wide contact form has a machine name, a label, and one or more defined recipients; when a site visitor submits the form, the field values are sent to those recipients.', [':contact_admin' => $contact_page]) . '</dd>';
-      $output .= '<dt>' . t('Linking to contact forms') . '</dt>';
-      $output .= '<dd>' . t('One site-wide contact form can be designated as the default contact form. If you choose to designate a default form, the <em>Contact</em> menu link in the <em>Footer</em> menu will link to it. You can modify this link from the <a href=":menu-settings">Menus page</a> if you have the Menu UI module installed. You can also create links to other contact forms; the URL for each form you have set up has format <em>contact/machine_name_of_form</em>.', [':menu-settings' => $menu_page]) . '</p>';
-      $output .= '<dt>' . t('Adding content to contact forms') . '</dt>';
-      $output .= '<dd>' . t('From the <a href=":contact_admin">Contact forms page</a>, you can configure the fields to be shown on contact forms, including their labels and help text. If you would like other content (such as text or images) to appear on a contact form, use a block. You can create and edit blocks on the <a href=":blocks">Block layout page</a>, if the Block module is installed.', [':blocks' => $block_page, ':contact_admin' => $contact_page]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-  }
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function contact_entity_type_alter(array &$entity_types): void {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  $entity_types['user']->setLinkTemplate('contact-form', '/user/{user}/contact');
-}
-
-/**
- * Implements hook_entity_extra_field_info().
- */
-function contact_entity_extra_field_info() {
-  $fields = [];
-  foreach (array_keys(\Drupal::service('entity_type.bundle.info')->getBundleInfo('contact_message')) as $bundle) {
-    $fields['contact_message'][$bundle]['form']['name'] = [
-      'label' => t('Sender name'),
-      'description' => t('Text'),
-      'weight' => -50,
-    ];
-    $fields['contact_message'][$bundle]['form']['mail'] = [
-      'label' => t('Sender email'),
-      'description' => t('Email'),
-      'weight' => -40,
-    ];
-    if ($bundle == 'personal') {
-      $fields['contact_message'][$bundle]['form']['recipient'] = [
-        'label' => t('Recipient username'),
-        'description' => t('User'),
-        'weight' => -30,
-      ];
-    }
-    $fields['contact_message'][$bundle]['form']['preview'] = [
-      'label' => t('Preview sender message'),
-      'description' => t('Preview'),
-      'weight' => 40,
-    ];
-    $fields['contact_message'][$bundle]['form']['copy'] = [
-      'label' => t('Send copy to sender'),
-      'description' => t('Option'),
-      'weight' => 50,
-    ];
-  }
-
-  $fields['user']['user']['form']['contact'] = [
-    'label' => t('Contact settings'),
-    'description' => t('Contact module form element.'),
-    'weight' => 5,
-  ];
-  return $fields;
-}
-
-/**
- * Implements hook_menu_local_tasks_alter().
- *
- * Hides the 'Contact' tab on the user profile if the user does not have an
- * email address configured.
- */
-function contact_menu_local_tasks_alter(&$data, $route_name) {
-  if ($route_name == 'entity.user.canonical' && isset($data['tabs'][0])) {
-    foreach ($data['tabs'][0] as $href => $tab_data) {
-      if ($href == 'entity.user.contact_form') {
-        $link_params = $tab_data['#link']['url']->getRouteParameters();
-        $account = User::load($link_params['user']);
-        if (!$account->getEmail()) {
-          unset($data['tabs'][0]['entity.user.contact_form']);
-        }
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_mail().
- */
-function contact_mail($key, &$message, $params) {
-  $contact_message = $params['contact_message'];
-  /** @var \Drupal\user\UserInterface $sender */
-  $sender = $params['sender'];
-  $language = \Drupal::languageManager()->getLanguage($message['langcode']);
-
-  $variables = [
-    '@site-name' => \Drupal::config('system.site')->get('name'),
-    '@subject' => $contact_message->getSubject(),
-    '@form' => !empty($params['contact_form']) ? $params['contact_form']->label() : '',
-    '@form-url' => Url::fromRoute('<current>', [], ['absolute' => TRUE, 'language' => $language])->toString(),
-    '@sender-name' => $sender->getDisplayName(),
-  ];
-  if ($sender->isAuthenticated()) {
-    $variables['@sender-url'] = $sender->toUrl('canonical', ['absolute' => TRUE, 'language' => $language])->toString();
-  }
-  else {
-    $variables['@sender-url'] = $params['sender']->getEmail() ?? '';
-  }
-
-  $options = ['langcode' => $language->getId()];
-
-  switch ($key) {
-    case 'page_mail':
-    case 'page_copy':
-      $message['subject'] .= t('[@form] @subject', $variables, $options);
-      $message['body'][] = t("@sender-name (@sender-url) sent a message using the contact form at @form-url.", $variables, $options);
-      $build = \Drupal::entityTypeManager()
-        ->getViewBuilder('contact_message')
-        ->view($contact_message, 'mail');
-      $message['body'][] = \Drupal::service('renderer')->renderInIsolation($build);
-      break;
-
-    case 'page_autoreply':
-      $message['subject'] .= t('[@form] @subject', $variables, $options);
-      $message['body'][] = $params['contact_form']->getReply();
-      break;
-
-    case 'user_mail':
-    case 'user_copy':
-      $variables += [
-        '@recipient-name' => $params['recipient']->getDisplayName(),
-        '@recipient-edit-url' => $params['recipient']->toUrl('edit-form', ['absolute' => TRUE, 'language' => $language])->toString(),
-      ];
-      $message['subject'] .= t('[@site-name] @subject', $variables, $options);
-      $message['body'][] = t('Hello @recipient-name,', $variables, $options);
-      $message['body'][] = t("@sender-name (@sender-url) has sent you a message via your contact form at @site-name.", $variables, $options);
-      // Only include the opt-out line in the original email and not in the
-      // copy to the sender. Also exclude this if the email was sent from a
-      // user administrator because they can always send emails even if the
-      // contacted user has disabled their contact form.
-      if ($key === 'user_mail' && !$params['sender']->hasPermission('administer users')) {
-        $message['body'][] = t("If you don't want to receive such messages, you can change your settings at @recipient-edit-url.", $variables, $options);
-      }
-      $build = \Drupal::entityTypeManager()
-        ->getViewBuilder('contact_message')
-        ->view($contact_message, 'mail');
-      $message['body'][] = \Drupal::service('renderer')->renderInIsolation($build);
-      break;
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for \Drupal\user\ProfileForm.
- *
- * Add the enable personal contact form to an individual user's account page.
- *
- * @see \Drupal\user\ProfileForm::form()
- */
-function contact_form_user_form_alter(&$form, FormStateInterface $form_state): void {
-  $form['contact'] = [
-    '#type' => 'details',
-    '#title' => t('Contact settings'),
-    '#open' => TRUE,
-    '#weight' => 5,
-  ];
-  $account = $form_state->getFormObject()->getEntity();
-  if (!\Drupal::currentUser()->isAnonymous() && $account->id()) {
-    $account_data = \Drupal::service('user.data')->get('contact', $account->id(), 'enabled');
-  }
-  $form['contact']['contact'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Personal contact form'),
-    '#default_value' => $account_data ?? \Drupal::config('contact.settings')->get('user_default_enabled'),
-    '#description' => t('Allow other users to contact you via a personal contact form which keeps your email address hidden. Note that some privileged users such as site administrators are still able to contact you even if you choose to disable this feature.'),
-  ];
-  $form['actions']['submit']['#submit'][] = 'contact_user_profile_form_submit';
-}
 
 /**
  * Form submission handler for \Drupal\user\ProfileForm.
@@ -215,28 +18,6 @@ function contact_user_profile_form_submit($form, FormStateInterface $form_state)
   }
 }
 
-/**
- * Implements hook_form_FORM_ID_alter() for \Drupal\user\AccountSettingsForm.
- *
- * Adds the default personal contact setting on the user settings page.
- */
-function contact_form_user_admin_settings_alter(&$form, FormStateInterface $form_state): void {
-  $form['contact'] = [
-    '#type' => 'details',
-    '#title' => t('Contact settings'),
-    '#open' => TRUE,
-    '#weight' => 0,
-  ];
-  $form['contact']['contact_default_status'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Enable the personal contact form by default for new users'),
-    '#description' => t('Changing this setting will not affect existing users.'),
-    '#default_value' => \Drupal::configFactory()->getEditable('contact.settings')->get('user_default_enabled'),
-  ];
-  // Add submit handler to save contact configuration.
-  $form['#submit'][] = 'contact_form_user_admin_settings_submit';
-}
-
 /**
  * Form submission handler for \Drupal\user\AccountSettingsForm.
  *
@@ -247,10 +28,3 @@ function contact_form_user_admin_settings_submit($form, FormStateInterface $form
     ->set('user_default_enabled', $form_state->getValue('contact_default_status'))
     ->save();
 }
-
-/**
- * Implements hook_rest_resource_alter().
- */
-function contact_rest_resource_alter(&$definitions) {
-  $definitions['entity:contact_message']['class'] = ContactMessageResource::class;
-}
diff --git a/core/modules/contact/contact.views.inc b/core/modules/contact/contact.views.inc
deleted file mode 100644
index a18c9e796f32369c5e140847dbacfec4b5a940b2..0000000000000000000000000000000000000000
--- a/core/modules/contact/contact.views.inc
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views data for contact.module.
- */
-
-/**
- * Implements hook_views_data_alter().
- */
-function contact_views_data_alter(&$data) {
-  $data['users']['contact'] = [
-    'field' => [
-      'title' => t('Contact link'),
-      'help' => t('Provide a simple link to the user contact page.'),
-      'id' => 'contact_link',
-    ],
-  ];
-}
diff --git a/core/modules/contact/src/Hook/ContactHooks.php b/core/modules/contact/src/Hook/ContactHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..ac5d8058e9d2313c147a76dc5b439cc60e07e14a
--- /dev/null
+++ b/core/modules/contact/src/Hook/ContactHooks.php
@@ -0,0 +1,230 @@
+<?php
+
+namespace Drupal\contact\Hook;
+
+use Drupal\contact\Plugin\rest\resource\ContactMessageResource;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\user\Entity\User;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for contact.
+ */
+class ContactHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.contact':
+        $menu_page = \Drupal::moduleHandler()->moduleExists('menu_ui') ? Url::fromRoute('entity.menu.collection')->toString() : '#';
+        $block_page = \Drupal::moduleHandler()->moduleExists('block') ? Url::fromRoute('block.admin_display')->toString() : '#';
+        $contact_page = Url::fromRoute('entity.contact_form.collection')->toString();
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Contact module allows visitors to contact registered users on your site, using the personal contact form, and also allows you to set up site-wide contact forms. For more information, see the <a href=":contact">online documentation for the Contact module</a>.', [':contact' => 'https://www.drupal.org/documentation/modules/contact']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Using the personal contact form') . '</dt>';
+        $output .= '<dd>' . t("Site visitors can email registered users on your site by using the personal contact form, without knowing or learning the email address of the recipient. When a site visitor is viewing a user profile, the viewer will see a <em>Contact</em> tab or link, which leads to the personal contact form. The personal contact link is not shown when you are viewing your own profile, and users must have both <em>View user information</em> (to see user profiles) and <em>Use users' personal contact forms</em> permission to see the link. The user whose profile is being viewed must also have their personal contact form enabled (this is a user account setting); viewers with <em>Administer users</em> permission can bypass this setting.") . '</dd>';
+        $output .= '<dt>' . t('Configuring contact forms') . '</dt>';
+        $output .= '<dd>' . t('On the <a href=":contact_admin">Contact forms page</a>, you can configure the fields and display of the personal contact form, and you can set up one or more site-wide contact forms. Each site-wide contact form has a machine name, a label, and one or more defined recipients; when a site visitor submits the form, the field values are sent to those recipients.', [':contact_admin' => $contact_page]) . '</dd>';
+        $output .= '<dt>' . t('Linking to contact forms') . '</dt>';
+        $output .= '<dd>' . t('One site-wide contact form can be designated as the default contact form. If you choose to designate a default form, the <em>Contact</em> menu link in the <em>Footer</em> menu will link to it. You can modify this link from the <a href=":menu-settings">Menus page</a> if you have the Menu UI module installed. You can also create links to other contact forms; the URL for each form you have set up has format <em>contact/machine_name_of_form</em>.', [':menu-settings' => $menu_page]) . '</p>';
+        $output .= '<dt>' . t('Adding content to contact forms') . '</dt>';
+        $output .= '<dd>' . t('From the <a href=":contact_admin">Contact forms page</a>, you can configure the fields to be shown on contact forms, including their labels and help text. If you would like other content (such as text or images) to appear on a contact form, use a block. You can create and edit blocks on the <a href=":blocks">Block layout page</a>, if the Block module is installed.', [':blocks' => $block_page, ':contact_admin' => $contact_page]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    $entity_types['user']->setLinkTemplate('contact-form', '/user/{user}/contact');
+  }
+
+  /**
+   * Implements hook_entity_extra_field_info().
+   */
+  #[Hook('entity_extra_field_info')]
+  public function entityExtraFieldInfo() {
+    $fields = [];
+    foreach (array_keys(\Drupal::service('entity_type.bundle.info')->getBundleInfo('contact_message')) as $bundle) {
+      $fields['contact_message'][$bundle]['form']['name'] = ['label' => t('Sender name'), 'description' => t('Text'), 'weight' => -50];
+      $fields['contact_message'][$bundle]['form']['mail'] = ['label' => t('Sender email'), 'description' => t('Email'), 'weight' => -40];
+      if ($bundle == 'personal') {
+        $fields['contact_message'][$bundle]['form']['recipient'] = ['label' => t('Recipient username'), 'description' => t('User'), 'weight' => -30];
+      }
+      $fields['contact_message'][$bundle]['form']['preview'] = [
+        'label' => t('Preview sender message'),
+        'description' => t('Preview'),
+        'weight' => 40,
+      ];
+      $fields['contact_message'][$bundle]['form']['copy'] = [
+        'label' => t('Send copy to sender'),
+        'description' => t('Option'),
+        'weight' => 50,
+      ];
+    }
+    $fields['user']['user']['form']['contact'] = [
+      'label' => t('Contact settings'),
+      'description' => t('Contact module form element.'),
+      'weight' => 5,
+    ];
+    return $fields;
+  }
+
+  /**
+   * Implements hook_menu_local_tasks_alter().
+   *
+   * Hides the 'Contact' tab on the user profile if the user does not have an
+   * email address configured.
+   */
+  #[Hook('menu_local_tasks_alter')]
+  public function menuLocalTasksAlter(&$data, $route_name) {
+    if ($route_name == 'entity.user.canonical' && isset($data['tabs'][0])) {
+      foreach ($data['tabs'][0] as $href => $tab_data) {
+        if ($href == 'entity.user.contact_form') {
+          $link_params = $tab_data['#link']['url']->getRouteParameters();
+          $account = User::load($link_params['user']);
+          if (!$account->getEmail()) {
+            unset($data['tabs'][0]['entity.user.contact_form']);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_mail().
+   */
+  #[Hook('mail')]
+  public function mail($key, &$message, $params) {
+    $contact_message = $params['contact_message'];
+    /** @var \Drupal\user\UserInterface $sender */
+    $sender = $params['sender'];
+    $language = \Drupal::languageManager()->getLanguage($message['langcode']);
+    $variables = [
+      '@site-name' => \Drupal::config('system.site')->get('name'),
+      '@subject' => $contact_message->getSubject(),
+      '@form' => !empty($params['contact_form']) ? $params['contact_form']->label() : '',
+      '@form-url' => Url::fromRoute('<current>', [], [
+        'absolute' => TRUE,
+        'language' => $language,
+      ])->toString(),
+      '@sender-name' => $sender->getDisplayName(),
+    ];
+    if ($sender->isAuthenticated()) {
+      $variables['@sender-url'] = $sender->toUrl('canonical', ['absolute' => TRUE, 'language' => $language])->toString();
+    }
+    else {
+      $variables['@sender-url'] = $params['sender']->getEmail() ?? '';
+    }
+    $options = ['langcode' => $language->getId()];
+    switch ($key) {
+      case 'page_mail':
+      case 'page_copy':
+        $message['subject'] .= t('[@form] @subject', $variables, $options);
+        $message['body'][] = t("@sender-name (@sender-url) sent a message using the contact form at @form-url.", $variables, $options);
+        $build = \Drupal::entityTypeManager()->getViewBuilder('contact_message')->view($contact_message, 'mail');
+        $message['body'][] = \Drupal::service('renderer')->renderInIsolation($build);
+        break;
+
+      case 'page_autoreply':
+        $message['subject'] .= t('[@form] @subject', $variables, $options);
+        $message['body'][] = $params['contact_form']->getReply();
+        break;
+
+      case 'user_mail':
+      case 'user_copy':
+        $variables += [
+          '@recipient-name' => $params['recipient']->getDisplayName(),
+          '@recipient-edit-url' => $params['recipient']->toUrl('edit-form', [
+            'absolute' => TRUE,
+            'language' => $language,
+          ])->toString(),
+        ];
+        $message['subject'] .= t('[@site-name] @subject', $variables, $options);
+        $message['body'][] = t('Hello @recipient-name,', $variables, $options);
+        $message['body'][] = t("@sender-name (@sender-url) has sent you a message via your contact form at @site-name.", $variables, $options);
+        // Only include the opt-out line in the original email and not in the
+        // copy to the sender. Also exclude this if the email was sent from a
+        // user administrator because they can always send emails even if the
+        // contacted user has disabled their contact form.
+        if ($key === 'user_mail' && !$params['sender']->hasPermission('administer users')) {
+          $message['body'][] = t("If you don't want to receive such messages, you can change your settings at @recipient-edit-url.", $variables, $options);
+        }
+        $build = \Drupal::entityTypeManager()->getViewBuilder('contact_message')->view($contact_message, 'mail');
+        $message['body'][] = \Drupal::service('renderer')->renderInIsolation($build);
+        break;
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for \Drupal\user\ProfileForm.
+   *
+   * Add the enable personal contact form to an individual user's account page.
+   *
+   * @see \Drupal\user\ProfileForm::form()
+   */
+  #[Hook('form_user_form_alter')]
+  public function formUserFormAlter(&$form, FormStateInterface $form_state) : void {
+    $form['contact'] = [
+      '#type' => 'details',
+      '#title' => t('Contact settings'),
+      '#open' => TRUE,
+      '#weight' => 5,
+    ];
+    $account = $form_state->getFormObject()->getEntity();
+    if (!\Drupal::currentUser()->isAnonymous() && $account->id()) {
+      $account_data = \Drupal::service('user.data')->get('contact', $account->id(), 'enabled');
+    }
+    $form['contact']['contact'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Personal contact form'),
+      '#default_value' => $account_data ?? \Drupal::config('contact.settings')->get('user_default_enabled'),
+      '#description' => t('Allow other users to contact you via a personal contact form which keeps your email address hidden. Note that some privileged users such as site administrators are still able to contact you even if you choose to disable this feature.'),
+    ];
+    $form['actions']['submit']['#submit'][] = 'contact_user_profile_form_submit';
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for \Drupal\user\AccountSettingsForm.
+   *
+   * Adds the default personal contact setting on the user settings page.
+   */
+  #[Hook('form_user_admin_settings_alter')]
+  public function formUserAdminSettingsAlter(&$form, FormStateInterface $form_state) : void {
+    $form['contact'] = [
+      '#type' => 'details',
+      '#title' => t('Contact settings'),
+      '#open' => TRUE,
+      '#weight' => 0,
+    ];
+    $form['contact']['contact_default_status'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Enable the personal contact form by default for new users'),
+      '#description' => t('Changing this setting will not affect existing users.'),
+      '#default_value' => \Drupal::configFactory()->getEditable('contact.settings')->get('user_default_enabled'),
+    ];
+    // Add submit handler to save contact configuration.
+    $form['#submit'][] = 'contact_form_user_admin_settings_submit';
+  }
+
+  /**
+   * Implements hook_rest_resource_alter().
+   */
+  #[Hook('rest_resource_alter')]
+  public function restResourceAlter(&$definitions) {
+    $definitions['entity:contact_message']['class'] = ContactMessageResource::class;
+  }
+
+}
diff --git a/core/modules/contact/src/Hook/ContactViewsHooks.php b/core/modules/contact/src/Hook/ContactViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..fa1eab062d99ed7a3115ea0eb8e4655dfb6ccb7a
--- /dev/null
+++ b/core/modules/contact/src/Hook/ContactViewsHooks.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\contact\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for contact.
+ */
+class ContactViewsHooks {
+  /**
+   * @file
+   * Provide views data for contact.module.
+   */
+
+  /**
+   * Implements hook_views_data_alter().
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(&$data) {
+    $data['users']['contact'] = [
+      'field' => [
+        'title' => t('Contact link'),
+        'help' => t('Provide a simple link to the user contact page.'),
+        'id' => 'contact_link',
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module b/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module
index 30f5f47f988c9bce6f94c3878578a3a755581bde..792bd4758edc4480befbd2749085a30647c46757 100644
--- a/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module
+++ b/core/modules/contact/tests/modules/contact_storage_test/contact_storage_test.module
@@ -8,56 +8,8 @@
 declare(strict_types=1);
 
 use Drupal\contact\ContactFormInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Form\FormStateInterface;
 
-/**
- * Implements hook_entity_base_field_info().
- */
-function contact_storage_test_entity_base_field_info(EntityTypeInterface $entity_type) {
-  if ($entity_type->id() == 'contact_message') {
-    $fields = [];
-
-    $fields['id'] = BaseFieldDefinition::create('integer')
-      ->setLabel(t('Message ID'))
-      ->setDescription(t('The message ID.'))
-      ->setReadOnly(TRUE)
-      ->setSetting('unsigned', TRUE);
-
-    return $fields;
-  }
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function contact_storage_test_entity_type_alter(array &$entity_types): void {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  // Set the controller class for nodes to an alternate implementation of the
-  // Drupal\Core\Entity\EntityStorageInterface interface.
-  $entity_types['contact_message']->setStorageClass('\Drupal\Core\Entity\Sql\SqlContentEntityStorage');
-  $keys = $entity_types['contact_message']->getKeys();
-  $keys['id'] = 'id';
-  $entity_types['contact_message']->set('entity_keys', $keys);
-  $entity_types['contact_message']->set('base_table', 'contact_message');
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for contact_form_form().
- */
-function contact_storage_test_form_contact_form_form_alter(&$form, FormStateInterface $form_state): void {
-  /** @var \Drupal\contact\ContactFormInterface $contact_form */
-  $contact_form = $form_state->getFormObject()->getEntity();
-  $form['send_a_pony'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Send submitters a voucher for a free pony.'),
-    '#description' => t('Enable to send an additional email with a free pony voucher to anyone who submits the form.'),
-    '#default_value' => $contact_form->getThirdPartySetting('contact_storage_test', 'send_a_pony', FALSE),
-  ];
-  $form['#entity_builders'][] = 'contact_storage_test_contact_form_form_builder';
-}
-
 /**
  * Entity builder for the contact form edit form with third party options.
  *
diff --git a/core/modules/contact/tests/modules/contact_storage_test/src/Hook/ContactStorageTestHooks.php b/core/modules/contact/tests/modules/contact_storage_test/src/Hook/ContactStorageTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c5dd5ca65f2651c4a997d478bf7eec0303677009
--- /dev/null
+++ b/core/modules/contact/tests/modules/contact_storage_test/src/Hook/ContactStorageTestHooks.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\contact_storage_test\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for contact_storage_test.
+ */
+class ContactStorageTestHooks {
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    if ($entity_type->id() == 'contact_message') {
+      $fields = [];
+      $fields['id'] = BaseFieldDefinition::create('integer')->setLabel(t('Message ID'))->setDescription(t('The message ID.'))->setReadOnly(TRUE)->setSetting('unsigned', TRUE);
+      return $fields;
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    // Set the controller class for nodes to an alternate implementation of the
+    // Drupal\Core\Entity\EntityStorageInterface interface.
+    $entity_types['contact_message']->setStorageClass('\Drupal\Core\Entity\Sql\SqlContentEntityStorage');
+    $keys = $entity_types['contact_message']->getKeys();
+    $keys['id'] = 'id';
+    $entity_types['contact_message']->set('entity_keys', $keys);
+    $entity_types['contact_message']->set('base_table', 'contact_message');
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for contact_form_form().
+   */
+  #[Hook('form_contact_form_form_alter')]
+  public function formContactFormFormAlter(&$form, FormStateInterface $form_state) : void {
+    /** @var \Drupal\contact\ContactFormInterface $contact_form */
+    $contact_form = $form_state->getFormObject()->getEntity();
+    $form['send_a_pony'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Send submitters a voucher for a free pony.'),
+      '#description' => t('Enable to send an additional email with a free pony voucher to anyone who submits the form.'),
+      '#default_value' => $contact_form->getThirdPartySetting('contact_storage_test', 'send_a_pony', FALSE),
+    ];
+    $form['#entity_builders'][] = 'contact_storage_test_contact_form_form_builder';
+  }
+
+}
diff --git a/core/modules/contact/tests/src/Unit/ContactTest.php b/core/modules/contact/tests/src/Unit/ContactTest.php
index b1abf037238fa3879d9b2748d6f9e65fa7c010be..b11c9cef7e1881314361f799d08fa99119dbc491 100644
--- a/core/modules/contact/tests/src/Unit/ContactTest.php
+++ b/core/modules/contact/tests/src/Unit/ContactTest.php
@@ -5,6 +5,7 @@
 namespace Drupal\Tests\contact\Unit;
 
 use Drupal\Tests\UnitTestCase;
+use Drupal\contact\Hook\ContactHooks;
 
 /**
  * @group contact
@@ -17,7 +18,8 @@ class ContactTest extends UnitTestCase {
   public function testLocalTasksAlter(): void {
     require_once $this->root . '/core/modules/contact/contact.module';
     $data = [];
-    \contact_menu_local_tasks_alter($data, 'entity.user.canonical');
+    $contactMenuLocalTasksAlter = new ContactHooks();
+    $contactMenuLocalTasksAlter->menuLocalTasksAlter($data, 'entity.user.canonical');
     $this->assertTrue(TRUE, 'No warning thrown');
   }
 
diff --git a/core/modules/content_moderation/content_moderation.module b/core/modules/content_moderation/content_moderation.module
index cb5b1c6ad21ffe7e6a36f5962ae5ec2b841e136f..fc3ad6cf133bd1264c974c72c1d36616ca5731d9 100644
--- a/core/modules/content_moderation/content_moderation.module
+++ b/core/modules/content_moderation/content_moderation.module
@@ -2,168 +2,9 @@
 
 /**
  * @file
- * Contains content_moderation.module.
  */
 
-use Drupal\content_moderation\EntityOperations;
-use Drupal\content_moderation\EntityTypeInfo;
 use Drupal\content_moderation\ContentPreprocess;
-use Drupal\content_moderation\Plugin\Action\ModerationOptOutPublish;
-use Drupal\content_moderation\Plugin\Action\ModerationOptOutUnpublish;
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\EntityPublishedInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Url;
-use Drupal\views\Plugin\views\filter\Broken;
-use Drupal\views\ViewExecutable;
-use Drupal\views\Views;
-use Drupal\workflows\WorkflowInterface;
-use Drupal\Core\Action\Plugin\Action\PublishAction;
-use Drupal\Core\Action\Plugin\Action\UnpublishAction;
-use Drupal\workflows\Entity\Workflow;
-use Drupal\views\Entity\View;
-
-/**
- * Implements hook_help().
- */
-function content_moderation_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    // Main module help for the content_moderation module.
-    case 'help.page.content_moderation':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Content Moderation module allows you to expand on Drupal\'s "unpublished" and "published" states for content. It allows you to have a published version that is live, but have a separate working copy that is undergoing review before it is published. This is achieved by using <a href=":workflows">Workflows</a> to apply different states and transitions to entities as needed. For more information, see the <a href=":content_moderation">online documentation for the Content Moderation module</a>.', [':content_moderation' => 'https://www.drupal.org/documentation/modules/content_moderation', ':workflows' => Url::fromRoute('help.page', ['name' => 'workflows'])->toString()]) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Applying workflows') . '</dt>';
-      $output .= '<dd>' . t('Content Moderation allows you to apply <a href=":workflows">Workflows</a> to content, content blocks, and other <a href=":field_help" title="Field module help, with background on content entities">content entities</a>, to provide more fine-grained publishing options. For example, a Basic page might have states such as Draft and Published, with allowed transitions such as Draft to Published (making the current revision "live"), and Published to Draft (making a new draft revision of published content).', [':workflows' => Url::fromRoute('help.page', ['name' => 'workflows'])->toString(), ':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</dd>';
-      if (\Drupal::moduleHandler()->moduleExists('views')) {
-        $moderated_content_view = View::load('moderated_content');
-        if (isset($moderated_content_view) && $moderated_content_view->status() === TRUE) {
-          $output .= '<dt>' . t('Moderating content') . '</dt>';
-          $output .= '<dd>' . t('You can view a list of content awaiting moderation on the <a href=":moderated">moderated content page</a>. This will show any content in an unpublished state, such as Draft or Archived, to help surface content that requires more work from content editors.', [':moderated' => Url::fromRoute('view.moderated_content.moderated_content')->toString()]) . '</dd>';
-        }
-      }
-      $output .= '<dt>' . t('Configure Content Moderation permissions') . '</dt>';
-      $output .= '<dd>' . t('Each transition is exposed as a permission. If a user has the permission for a transition, they can use the transition to change the state of the content item, from Draft to Published.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_entity_base_field_info().
- */
-function content_moderation_entity_base_field_info(EntityTypeInterface $entity_type) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityTypeInfo::class)
-    ->entityBaseFieldInfo($entity_type);
-}
-
-/**
- * Implements hook_entity_bundle_field_info().
- */
-function content_moderation_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
-  if (isset($base_field_definitions['moderation_state'])) {
-    // Add the target bundle to the moderation state field. Since each bundle
-    // can be attached to a different moderation workflow, adding this
-    // information to the field definition allows the associated workflow to be
-    // derived where a field definition is present.
-    $base_field_definitions['moderation_state']->setTargetBundle($bundle);
-    return [
-      'moderation_state' => $base_field_definitions['moderation_state'],
-    ];
-  }
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function content_moderation_entity_type_alter(array &$entity_types): void {
-  \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityTypeInfo::class)
-    ->entityTypeAlter($entity_types);
-}
-
-/**
- * Implements hook_entity_presave().
- */
-function content_moderation_entity_presave(EntityInterface $entity) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityPresave($entity);
-}
-
-/**
- * Implements hook_entity_insert().
- */
-function content_moderation_entity_insert(EntityInterface $entity) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityInsert($entity);
-}
-
-/**
- * Implements hook_entity_update().
- */
-function content_moderation_entity_update(EntityInterface $entity) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityUpdate($entity);
-}
-
-/**
- * Implements hook_entity_delete().
- */
-function content_moderation_entity_delete(EntityInterface $entity) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityDelete($entity);
-}
-
-/**
- * Implements hook_entity_revision_delete().
- */
-function content_moderation_entity_revision_delete(EntityInterface $entity) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityRevisionDelete($entity);
-}
-
-/**
- * Implements hook_entity_translation_delete().
- */
-function content_moderation_entity_translation_delete(EntityInterface $translation) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityTranslationDelete($translation);
-}
-
-/**
- * Implements hook_entity_prepare_form().
- */
-function content_moderation_entity_prepare_form(EntityInterface $entity, $operation, FormStateInterface $form_state) {
-  \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityTypeInfo::class)
-    ->entityPrepareForm($entity, $operation, $form_state);
-}
-
-/**
- * Implements hook_form_alter().
- */
-function content_moderation_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityTypeInfo::class)
-    ->formAlter($form, $form_state, $form_id);
-}
 
 /**
  * Implements hook_preprocess_HOOK().
@@ -173,212 +14,3 @@ function content_moderation_preprocess_node(&$variables) {
     ->getInstanceFromDefinition(ContentPreprocess::class)
     ->preprocessNode($variables);
 }
-
-/**
- * Implements hook_entity_extra_field_info().
- */
-function content_moderation_entity_extra_field_info() {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityTypeInfo::class)
-    ->entityExtraFieldInfo();
-}
-
-/**
- * Implements hook_entity_view().
- */
-function content_moderation_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
-  \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityView($build, $entity, $display, $view_mode);
-}
-
-/**
- * Implements hook_entity_form_display_alter().
- */
-function content_moderation_entity_form_display_alter(EntityFormDisplayInterface $form_display, array $context) {
-  if ($context['form_mode'] === 'layout_builder') {
-    $form_display->setComponent('moderation_state', [
-      'type' => 'moderation_state_default',
-      'weight' => -900,
-      'settings' => [],
-    ]);
-  }
-}
-
-/**
- * Implements hook_entity_access().
- *
- * Entities should be viewable if unpublished and the user has the appropriate
- * permission. This permission is therefore effectively mandatory for any user
- * that wants to moderate things.
- */
-function content_moderation_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
-  $moderation_info = Drupal::service('content_moderation.moderation_information');
-
-  $access_result = NULL;
-  if ($operation === 'view') {
-    $access_result = (($entity instanceof EntityPublishedInterface) && !$entity->isPublished())
-      ? AccessResult::allowedIfHasPermission($account, 'view any unpublished content')
-      : AccessResult::neutral();
-
-    $access_result->addCacheableDependency($entity);
-  }
-  elseif ($operation === 'update' && $moderation_info->isModeratedEntity($entity) && $entity->moderation_state) {
-    /** @var \Drupal\content_moderation\StateTransitionValidation $transition_validation */
-    $transition_validation = \Drupal::service('content_moderation.state_transition_validation');
-
-    $valid_transition_targets = $transition_validation->getValidTransitions($entity, $account);
-    $access_result = $valid_transition_targets ? AccessResult::neutral() : AccessResult::forbidden('No valid transitions exist for given account.');
-
-    $access_result->addCacheableDependency($entity);
-    $workflow = $moderation_info->getWorkflowForEntity($entity);
-    $access_result->addCacheableDependency($workflow);
-    // The state transition validation service returns a list of transitions
-    // based on the user's permission to use them.
-    $access_result->cachePerPermissions();
-  }
-
-  // Do not allow users to delete the state that is configured as the default
-  // state for the workflow.
-  if ($entity instanceof WorkflowInterface) {
-    $configuration = $entity->getTypePlugin()->getConfiguration();
-    if (!empty($configuration['default_moderation_state']) && $operation === sprintf('delete-state:%s', $configuration['default_moderation_state'])) {
-      return AccessResult::forbidden()->addCacheableDependency($entity);
-    }
-  }
-
-  return $access_result;
-}
-
-/**
- * Implements hook_entity_field_access().
- */
-function content_moderation_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
-  if ($items && $operation === 'edit') {
-    /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
-    $moderation_info = Drupal::service('content_moderation.moderation_information');
-
-    $entity_type = \Drupal::entityTypeManager()->getDefinition($field_definition->getTargetEntityTypeId());
-
-    $entity = $items->getEntity();
-
-    // Deny edit access to the published field if the entity is being moderated.
-    if ($entity_type->hasKey('published') && $moderation_info->isModeratedEntity($entity) && $entity->moderation_state && $field_definition->getName() == $entity_type->getKey('published')) {
-      return AccessResult::forbidden('Cannot edit the published field of moderated entities.');
-    }
-  }
-
-  return AccessResult::neutral();
-}
-
-/**
- * Implements hook_theme().
- */
-function content_moderation_theme(): array {
-  return ['entity_moderation_form' => ['render element' => 'form']];
-}
-
-/**
- * Implements hook_action_info_alter().
- */
-function content_moderation_action_info_alter(&$definitions) {
-
-  // The publish/unpublish actions are not valid on moderated entities. So swap
-  // their implementations out for alternates that will become a no-op on a
-  // moderated entity. If another module has already swapped out those classes,
-  // though, we'll be polite and do nothing.
-  foreach ($definitions as &$definition) {
-    if ($definition['id'] === 'entity:publish_action' && $definition['class'] == PublishAction::class) {
-      $definition['class'] = ModerationOptOutPublish::class;
-    }
-    if ($definition['id'] === 'entity:unpublish_action' && $definition['class'] == UnpublishAction::class) {
-      $definition['class'] = ModerationOptOutUnpublish::class;
-    }
-  }
-}
-
-/**
- * Implements hook_entity_bundle_info_alter().
- */
-function content_moderation_entity_bundle_info_alter(&$bundles) {
-  $translatable = FALSE;
-  /** @var \Drupal\workflows\WorkflowInterface $workflow */
-  foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
-    /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
-    $plugin = $workflow->getTypePlugin();
-    foreach ($plugin->getEntityTypes() as $entity_type_id) {
-      foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) {
-        if (isset($bundles[$entity_type_id][$bundle_id])) {
-          $bundles[$entity_type_id][$bundle_id]['workflow'] = $workflow->id();
-          // If we have even one moderation-enabled translatable bundle, we need
-          // to make the moderation state bundle translatable as well, to enable
-          // the revision translation merge logic also for content moderation
-          // state revisions.
-          if (!empty($bundles[$entity_type_id][$bundle_id]['translatable'])) {
-            $translatable = TRUE;
-          }
-        }
-      }
-    }
-  }
-  $bundles['content_moderation_state']['content_moderation_state']['translatable'] = $translatable;
-}
-
-/**
- * Implements hook_entity_bundle_delete().
- */
-function content_moderation_entity_bundle_delete($entity_type_id, $bundle_id) {
-  // Remove non-configuration based bundles from content moderation based
-  // workflows when they are removed.
-  foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
-    if ($workflow->getTypePlugin()->appliesToEntityTypeAndBundle($entity_type_id, $bundle_id)) {
-      $workflow->getTypePlugin()->removeEntityTypeAndBundle($entity_type_id, $bundle_id);
-      $workflow->save();
-    }
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert().
- */
-function content_moderation_workflow_insert(WorkflowInterface $entity) {
-  // Clear bundle cache so workflow gets added or removed from the bundle
-  // information.
-  \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
-  // Clear field cache so extra field is added or removed.
-  \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
-  // Clear the views data cache so the extra field is available in views.
-  if (\Drupal::moduleHandler()->moduleExists('views')) {
-    Views::viewsData()->clear();
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update().
- */
-function content_moderation_workflow_update(WorkflowInterface $entity) {
-  // Clear bundle cache so workflow gets added or removed from the bundle
-  // information.
-  \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
-  // Clear field cache so extra field is added or removed.
-  \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
-  // Clear the views data cache so the extra field is available in views.
-  if (\Drupal::moduleHandler()->moduleExists('views')) {
-    Views::viewsData()->clear();
-  }
-}
-
-/**
- * Implements hook_views_post_execute().
- */
-function content_moderation_views_post_execute(ViewExecutable $view) {
-  // @todo Remove this once broken handlers in views configuration result in
-  //   a view no longer returning results. https://www.drupal.org/node/2907954.
-  foreach ($view->filter as $id => $filter) {
-    if (str_starts_with($id, 'moderation_state') && $filter instanceof Broken) {
-      $view->result = [];
-      break;
-    }
-  }
-}
diff --git a/core/modules/content_moderation/content_moderation.views.inc b/core/modules/content_moderation/content_moderation.views.inc
index 799af942410d1965c4f77a6a5a2c938878036daa..5c1b9e1ff24afefd4894ed0fa6d32ceaf117a4a6 100644
--- a/core/modules/content_moderation/content_moderation.views.inc
+++ b/core/modules/content_moderation/content_moderation.views.inc
@@ -2,20 +2,10 @@
 
 /**
  * @file
- * Provide views data for content_moderation.module.
- *
- * @ingroup views_module_handlers
  */
 
 use Drupal\content_moderation\ViewsData;
 
-/**
- * Implements hook_views_data().
- */
-function content_moderation_views_data() {
-  return _content_moderation_views_data_object()->getViewsData();
-}
-
 /**
  * Creates a ViewsData object to respond to views hooks.
  *
diff --git a/core/modules/content_moderation/content_moderation.views_execution.inc b/core/modules/content_moderation/content_moderation.views_execution.inc
deleted file mode 100644
index fcf5f95dc4b5b79b7bdbef0a8b8b80f8513e7597..0000000000000000000000000000000000000000
--- a/core/modules/content_moderation/content_moderation.views_execution.inc
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views runtime hooks for content_moderation.module.
- */
-
-use Drupal\views\ViewExecutable;
-
-/**
- * Implements hook_views_query_substitutions().
- */
-function content_moderation_views_query_substitutions(ViewExecutable $view) {
-  $account = \Drupal::currentUser();
-  return [
-    '***VIEW_ANY_UNPUBLISHED_NODES***' => intval($account->hasPermission('view any unpublished content')),
-  ];
-}
diff --git a/core/modules/content_moderation/src/Hook/ContentModerationHooks.php b/core/modules/content_moderation/src/Hook/ContentModerationHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..4b4b558311c885c13c76999a91bf41200a809d13
--- /dev/null
+++ b/core/modules/content_moderation/src/Hook/ContentModerationHooks.php
@@ -0,0 +1,374 @@
+<?php
+
+namespace Drupal\content_moderation\Hook;
+
+use Drupal\views\Plugin\views\filter\Broken;
+use Drupal\views\ViewExecutable;
+use Drupal\views\Views;
+use Drupal\workflows\Entity\Workflow;
+use Drupal\content_moderation\Plugin\Action\ModerationOptOutUnpublish;
+use Drupal\Core\Action\Plugin\Action\UnpublishAction;
+use Drupal\content_moderation\Plugin\Action\ModerationOptOutPublish;
+use Drupal\Core\Action\Plugin\Action\PublishAction;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\workflows\WorkflowInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityPublishedInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\content_moderation\EntityOperations;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\content_moderation\EntityTypeInfo;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\views\Entity\View;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for content_moderation.
+ */
+class ContentModerationHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      // Main module help for the content_moderation module.
+      case 'help.page.content_moderation':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Content Moderation module allows you to expand on Drupal\'s "unpublished" and "published" states for content. It allows you to have a published version that is live, but have a separate working copy that is undergoing review before it is published. This is achieved by using <a href=":workflows">Workflows</a> to apply different states and transitions to entities as needed. For more information, see the <a href=":content_moderation">online documentation for the Content Moderation module</a>.', [
+          ':content_moderation' => 'https://www.drupal.org/documentation/modules/content_moderation',
+          ':workflows' => Url::fromRoute('help.page', [
+            'name' => 'workflows',
+          ])->toString(),
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Applying workflows') . '</dt>';
+        $output .= '<dd>' . t('Content Moderation allows you to apply <a href=":workflows">Workflows</a> to content, content blocks, and other <a href=":field_help" title="Field module help, with background on content entities">content entities</a>, to provide more fine-grained publishing options. For example, a Basic page might have states such as Draft and Published, with allowed transitions such as Draft to Published (making the current revision "live"), and Published to Draft (making a new draft revision of published content).', [
+          ':workflows' => Url::fromRoute('help.page', [
+            'name' => 'workflows',
+          ])->toString(),
+          ':field_help' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+        ]) . '</dd>';
+        if (\Drupal::moduleHandler()->moduleExists('views')) {
+          $moderated_content_view = View::load('moderated_content');
+          if (isset($moderated_content_view) && $moderated_content_view->status() === TRUE) {
+            $output .= '<dt>' . t('Moderating content') . '</dt>';
+            $output .= '<dd>' . t('You can view a list of content awaiting moderation on the <a href=":moderated">moderated content page</a>. This will show any content in an unpublished state, such as Draft or Archived, to help surface content that requires more work from content editors.', [
+              ':moderated' => Url::fromRoute('view.moderated_content.moderated_content')->toString(),
+            ]) . '</dd>';
+          }
+        }
+        $output .= '<dt>' . t('Configure Content Moderation permissions') . '</dt>';
+        $output .= '<dd>' . t('Each transition is exposed as a permission. If a user has the permission for a transition, they can use the transition to change the state of the content item, from Draft to Published.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class)->entityBaseFieldInfo($entity_type);
+  }
+
+  /**
+   * Implements hook_entity_bundle_field_info().
+   */
+  #[Hook('entity_bundle_field_info')]
+  public function entityBundleFieldInfo(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
+    if (isset($base_field_definitions['moderation_state'])) {
+      // Add the target bundle to the moderation state field. Since each bundle
+      // can be attached to a different moderation workflow, adding this
+      // information to the field definition allows the associated workflow to be
+      // derived where a field definition is present.
+      $base_field_definitions['moderation_state']->setTargetBundle($bundle);
+      return ['moderation_state' => $base_field_definitions['moderation_state']];
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class)->entityTypeAlter($entity_types);
+  }
+
+  /**
+   * Implements hook_entity_presave().
+   */
+  #[Hook('entity_presave')]
+  public function entityPresave(EntityInterface $entity) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityPresave($entity);
+  }
+
+  /**
+   * Implements hook_entity_insert().
+   */
+  #[Hook('entity_insert')]
+  public function entityInsert(EntityInterface $entity) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityInsert($entity);
+  }
+
+  /**
+   * Implements hook_entity_update().
+   */
+  #[Hook('entity_update')]
+  public function entityUpdate(EntityInterface $entity) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityUpdate($entity);
+  }
+
+  /**
+   * Implements hook_entity_delete().
+   */
+  #[Hook('entity_delete')]
+  public function entityDelete(EntityInterface $entity) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityDelete($entity);
+  }
+
+  /**
+   * Implements hook_entity_revision_delete().
+   */
+  #[Hook('entity_revision_delete')]
+  public function entityRevisionDelete(EntityInterface $entity) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityRevisionDelete($entity);
+  }
+
+  /**
+   * Implements hook_entity_translation_delete().
+   */
+  #[Hook('entity_translation_delete')]
+  public function entityTranslationDelete(EntityInterface $translation) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityTranslationDelete($translation);
+  }
+
+  /**
+   * Implements hook_entity_prepare_form().
+   */
+  #[Hook('entity_prepare_form')]
+  public function entityPrepareForm(EntityInterface $entity, $operation, FormStateInterface $form_state) {
+    \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class)->entityPrepareForm($entity, $operation, $form_state);
+  }
+
+  /**
+   * Implements hook_form_alter().
+   */
+  #[Hook('form_alter')]
+  public function formAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class)->formAlter($form, $form_state, $form_id);
+  }
+
+  /**
+   * Implements hook_entity_extra_field_info().
+   */
+  #[Hook('entity_extra_field_info')]
+  public function entityExtraFieldInfo() {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class)->entityExtraFieldInfo();
+  }
+
+  /**
+   * Implements hook_entity_view().
+   */
+  #[Hook('entity_view')]
+  public function entityView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
+    \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityView($build, $entity, $display, $view_mode);
+  }
+
+  /**
+   * Implements hook_entity_form_display_alter().
+   */
+  #[Hook('entity_form_display_alter')]
+  public function entityFormDisplayAlter(EntityFormDisplayInterface $form_display, array $context) {
+    if ($context['form_mode'] === 'layout_builder') {
+      $form_display->setComponent('moderation_state', ['type' => 'moderation_state_default', 'weight' => -900, 'settings' => []]);
+    }
+  }
+
+  /**
+   * Implements hook_entity_access().
+   *
+   * Entities should be viewable if unpublished and the user has the appropriate
+   * permission. This permission is therefore effectively mandatory for any user
+   * that wants to moderate things.
+   */
+  #[Hook('entity_access')]
+  public function entityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
+    $moderation_info = \Drupal::service('content_moderation.moderation_information');
+    $access_result = NULL;
+    if ($operation === 'view') {
+      $access_result = $entity instanceof EntityPublishedInterface && !$entity->isPublished() ? AccessResult::allowedIfHasPermission($account, 'view any unpublished content') : AccessResult::neutral();
+      $access_result->addCacheableDependency($entity);
+    }
+    elseif ($operation === 'update' && $moderation_info->isModeratedEntity($entity) && $entity->moderation_state) {
+      /** @var \Drupal\content_moderation\StateTransitionValidation $transition_validation */
+      $transition_validation = \Drupal::service('content_moderation.state_transition_validation');
+      $valid_transition_targets = $transition_validation->getValidTransitions($entity, $account);
+      $access_result = $valid_transition_targets ? AccessResult::neutral() : AccessResult::forbidden('No valid transitions exist for given account.');
+      $access_result->addCacheableDependency($entity);
+      $workflow = $moderation_info->getWorkflowForEntity($entity);
+      $access_result->addCacheableDependency($workflow);
+      // The state transition validation service returns a list of transitions
+      // based on the user's permission to use them.
+      $access_result->cachePerPermissions();
+    }
+    // Do not allow users to delete the state that is configured as the default
+    // state for the workflow.
+    if ($entity instanceof WorkflowInterface) {
+      $configuration = $entity->getTypePlugin()->getConfiguration();
+      if (!empty($configuration['default_moderation_state']) && $operation === sprintf('delete-state:%s', $configuration['default_moderation_state'])) {
+        return AccessResult::forbidden()->addCacheableDependency($entity);
+      }
+    }
+    return $access_result;
+  }
+
+  /**
+   * Implements hook_entity_field_access().
+   */
+  #[Hook('entity_field_access')]
+  public function entityFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
+    if ($items && $operation === 'edit') {
+      /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
+      $moderation_info = \Drupal::service('content_moderation.moderation_information');
+      $entity_type = \Drupal::entityTypeManager()->getDefinition($field_definition->getTargetEntityTypeId());
+      $entity = $items->getEntity();
+      // Deny edit access to the published field if the entity is being moderated.
+      if ($entity_type->hasKey('published') && $moderation_info->isModeratedEntity($entity) && $entity->moderation_state && $field_definition->getName() == $entity_type->getKey('published')) {
+        return AccessResult::forbidden('Cannot edit the published field of moderated entities.');
+      }
+    }
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return ['entity_moderation_form' => ['render element' => 'form']];
+  }
+
+  /**
+   * Implements hook_action_info_alter().
+   */
+  #[Hook('action_info_alter')]
+  public function actionInfoAlter(&$definitions) {
+    // The publish/unpublish actions are not valid on moderated entities. So swap
+    // their implementations out for alternates that will become a no-op on a
+    // moderated entity. If another module has already swapped out those classes,
+    // though, we'll be polite and do nothing.
+    foreach ($definitions as &$definition) {
+      if ($definition['id'] === 'entity:publish_action' && $definition['class'] == PublishAction::class) {
+        $definition['class'] = ModerationOptOutPublish::class;
+      }
+      if ($definition['id'] === 'entity:unpublish_action' && $definition['class'] == UnpublishAction::class) {
+        $definition['class'] = ModerationOptOutUnpublish::class;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_bundle_info_alter().
+   */
+  #[Hook('entity_bundle_info_alter')]
+  public function entityBundleInfoAlter(&$bundles) {
+    $translatable = FALSE;
+    /** @var \Drupal\workflows\WorkflowInterface $workflow */
+    foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
+      /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
+      $plugin = $workflow->getTypePlugin();
+      foreach ($plugin->getEntityTypes() as $entity_type_id) {
+        foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) {
+          if (isset($bundles[$entity_type_id][$bundle_id])) {
+            $bundles[$entity_type_id][$bundle_id]['workflow'] = $workflow->id();
+            // If we have even one moderation-enabled translatable bundle, we need
+            // to make the moderation state bundle translatable as well, to enable
+            // the revision translation merge logic also for content moderation
+            // state revisions.
+            if (!empty($bundles[$entity_type_id][$bundle_id]['translatable'])) {
+              $translatable = TRUE;
+            }
+          }
+        }
+      }
+    }
+    $bundles['content_moderation_state']['content_moderation_state']['translatable'] = $translatable;
+  }
+
+  /**
+   * Implements hook_entity_bundle_delete().
+   */
+  #[Hook('entity_bundle_delete')]
+  public function entityBundleDelete($entity_type_id, $bundle_id) {
+    // Remove non-configuration based bundles from content moderation based
+    // workflows when they are removed.
+    foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
+      if ($workflow->getTypePlugin()->appliesToEntityTypeAndBundle($entity_type_id, $bundle_id)) {
+        $workflow->getTypePlugin()->removeEntityTypeAndBundle($entity_type_id, $bundle_id);
+        $workflow->save();
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert().
+   */
+  #[Hook('workflow_insert')]
+  public function workflowInsert(WorkflowInterface $entity) {
+    // Clear bundle cache so workflow gets added or removed from the bundle
+    // information.
+    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+    // Clear field cache so extra field is added or removed.
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+    // Clear the views data cache so the extra field is available in views.
+    if (\Drupal::moduleHandler()->moduleExists('views')) {
+      Views::viewsData()->clear();
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update().
+   */
+  #[Hook('workflow_update')]
+  public function workflowUpdate(WorkflowInterface $entity) {
+    // Clear bundle cache so workflow gets added or removed from the bundle
+    // information.
+    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+    // Clear field cache so extra field is added or removed.
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+    // Clear the views data cache so the extra field is available in views.
+    if (\Drupal::moduleHandler()->moduleExists('views')) {
+      Views::viewsData()->clear();
+    }
+  }
+
+  /**
+   * Implements hook_views_post_execute().
+   */
+  #[Hook('views_post_execute')]
+  public function viewsPostExecute(ViewExecutable $view) {
+    // @todo Remove this once broken handlers in views configuration result in
+    //   a view no longer returning results. https://www.drupal.org/node/2907954.
+    foreach ($view->filter as $id => $filter) {
+      if (str_starts_with($id, 'moderation_state') && $filter instanceof Broken) {
+        $view->result = [];
+        break;
+      }
+    }
+  }
+
+}
diff --git a/core/modules/content_moderation/src/Hook/ContentModerationViewsExecutionHooks.php b/core/modules/content_moderation/src/Hook/ContentModerationViewsExecutionHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..2fe6a0791fb0d36aa7a7f786dcbe738aab29f396
--- /dev/null
+++ b/core/modules/content_moderation/src/Hook/ContentModerationViewsExecutionHooks.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\content_moderation\Hook;
+
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for content_moderation.
+ */
+class ContentModerationViewsExecutionHooks {
+
+  /**
+   * Implements hook_views_query_substitutions().
+   */
+  #[Hook('views_query_substitutions')]
+  public function viewsQuerySubstitutions(ViewExecutable $view) {
+    $account = \Drupal::currentUser();
+    return [
+      '***VIEW_ANY_UNPUBLISHED_NODES***' => intval($account->hasPermission('view any unpublished content')),
+    ];
+  }
+
+}
diff --git a/core/modules/content_moderation/src/Hook/ContentModerationViewsHooks.php b/core/modules/content_moderation/src/Hook/ContentModerationViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..1a632964bd04c9012cd253969d2b483598c97d0a
--- /dev/null
+++ b/core/modules/content_moderation/src/Hook/ContentModerationViewsHooks.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Drupal\content_moderation\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for content_moderation.
+ */
+class ContentModerationViewsHooks {
+
+  /**
+   * Implements hook_views_data().
+   */
+  #[Hook('views_data')]
+  public function viewsData() {
+    return _content_moderation_views_data_object()->getViewsData();
+  }
+
+}
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.module b/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.module
deleted file mode 100644
index 609ca0c65dbc2f242dae154454a18f7612ad0531..0000000000000000000000000000000000000000
--- a/core/modules/content_moderation/tests/modules/content_moderation_test_resave/content_moderation_test_resave.module
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains hook implementations for the Content moderation test re-save module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityInterface;
-
-/**
- * Implements hook_entity_insert().
- */
-function content_moderation_test_resave_entity_insert(EntityInterface $entity) {
-  /** @var \Drupal\content_moderation\ModerationInformationInterface $content_moderation */
-  $content_moderation = \Drupal::service('content_moderation.moderation_information');
-  if ($content_moderation->isModeratedEntity($entity)) {
-    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
-    // Saving the passed entity object would populate its loaded revision ID,
-    // which we want to avoid. Thus, save a clone of the original object.
-    $cloned_entity = clone $entity;
-    // Set the entity's syncing status, as we do not want Content Moderation to
-    // create new revisions for the re-saving. Without this call Content
-    // Moderation would end up creating two separate content moderation state
-    // entities: one for the re-save revision and one for the initial revision.
-    $cloned_entity->setSyncing(TRUE)->save();
-
-    // Record the fact that a re-save happened.
-    \Drupal::state()->set('content_moderation_test_resave', TRUE);
-  }
-}
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_resave/src/Hook/ContentModerationTestResaveHooks.php b/core/modules/content_moderation/tests/modules/content_moderation_test_resave/src/Hook/ContentModerationTestResaveHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..8bcb56c4b57028fd7360c321225c55b288244aee
--- /dev/null
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test_resave/src/Hook/ContentModerationTestResaveHooks.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\content_moderation_test_resave\Hook;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for content_moderation_test_resave.
+ */
+class ContentModerationTestResaveHooks {
+
+  /**
+   * Implements hook_entity_insert().
+   */
+  #[Hook('entity_insert')]
+  public function entityInsert(EntityInterface $entity) {
+    /** @var \Drupal\content_moderation\ModerationInformationInterface $content_moderation */
+    $content_moderation = \Drupal::service('content_moderation.moderation_information');
+    if ($content_moderation->isModeratedEntity($entity)) {
+      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+      // Saving the passed entity object would populate its loaded revision ID,
+      // which we want to avoid. Thus, save a clone of the original object.
+      $cloned_entity = clone $entity;
+      // Set the entity's syncing status, as we do not want Content Moderation to
+      // create new revisions for the re-saving. Without this call Content
+      // Moderation would end up creating two separate content moderation state
+      // entities: one for the re-save revision and one for the initial revision.
+      $cloned_entity->setSyncing(TRUE)->save();
+      // Record the fact that a re-save happened.
+      \Drupal::state()->set('content_moderation_test_resave', TRUE);
+    }
+  }
+
+}
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.module b/core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.module
deleted file mode 100644
index b465af77169532d173716854f383c80ff21ceb2a..0000000000000000000000000000000000000000
--- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.module
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-/**
- * @file
- * Module file for the content moderation test views module.
- */
-
-declare(strict_types=1);
-
-use Drupal\views\Plugin\views\query\QueryPluginBase;
-use Drupal\views\ViewExecutable;
-
-/**
- * Implements hook_views_query_alter().
- *
- * @see \Drupal\Tests\content_moderation\Kernel\ViewsModerationStateSortTest::testSortRevisionBaseTable()
- */
-function content_moderation_test_views_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
-  // Add a secondary sort order to ensure consistent builds when testing click
-  // and table sorting.
-  if ($view->id() === 'test_content_moderation_state_sort_revision_table') {
-    $query->addOrderBy('node_field_revision', 'vid', 'ASC');
-  }
-}
-
-/**
- * Implements hook_views_data_alter().
- *
- * @see \Drupal\Tests\content_moderation\Kernel\ViewsModerationStateFilterTest
- */
-function content_moderation_test_views_views_data_alter(array &$data) {
-  if (isset($data['users_field_data'])) {
-    $data['users_field_data']['uid_revision_test'] = [
-      'help' => t('Relate the content revision to the user who created it.'),
-      'real field' => 'uid',
-      'relationship' => [
-        'title' => t('Content revision authored'),
-        'help' => t('Relate the content revision to the user who created it. This relationship will create one record for each content revision item created by the user.'),
-        'id' => 'standard',
-        'base' => 'node_field_revision',
-        'base field' => 'uid',
-        'field' => 'uid',
-        'label' => t('node revisions'),
-      ],
-    ];
-  }
-}
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/src/Hook/ContentModerationTestViewsHooks.php b/core/modules/content_moderation/tests/modules/content_moderation_test_views/src/Hook/ContentModerationTestViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f4d6e5601e0fedda6280cefd04c4a9107315c3e8
--- /dev/null
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/src/Hook/ContentModerationTestViewsHooks.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\content_moderation_test_views\Hook;
+
+use Drupal\views\Plugin\views\query\QueryPluginBase;
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for content_moderation_test_views.
+ */
+class ContentModerationTestViewsHooks {
+
+  /**
+   * Implements hook_views_query_alter().
+   *
+   * @see \Drupal\Tests\content_moderation\Kernel\ViewsModerationStateSortTest::testSortRevisionBaseTable()
+   */
+  #[Hook('views_query_alter')]
+  public function viewsQueryAlter(ViewExecutable $view, QueryPluginBase $query) {
+    // Add a secondary sort order to ensure consistent builds when testing click
+    // and table sorting.
+    if ($view->id() === 'test_content_moderation_state_sort_revision_table') {
+      $query->addOrderBy('node_field_revision', 'vid', 'ASC');
+    }
+  }
+
+  /**
+   * Implements hook_views_data_alter().
+   *
+   * @see \Drupal\Tests\content_moderation\Kernel\ViewsModerationStateFilterTest
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(array &$data) {
+    if (isset($data['users_field_data'])) {
+      $data['users_field_data']['uid_revision_test'] = [
+        'help' => t('Relate the content revision to the user who created it.'),
+        'real field' => 'uid',
+        'relationship' => [
+          'title' => t('Content revision authored'),
+          'help' => t('Relate the content revision to the user who created it. This relationship will create one record for each content revision item created by the user.'),
+          'id' => 'standard',
+          'base' => 'node_field_revision',
+          'base field' => 'uid',
+          'field' => 'uid',
+          'label' => t('node revisions'),
+        ],
+      ];
+    }
+  }
+
+}
diff --git a/core/modules/content_translation/content_translation.admin.inc b/core/modules/content_translation/content_translation.admin.inc
index f5dfde5bea53f405a6e2ae4da82581be31ed6d82..b12d94e05bf7d549e7ce2836d9bc14602c5658d9 100644
--- a/core/modules/content_translation/content_translation.admin.inc
+++ b/core/modules/content_translation/content_translation.admin.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * The content translation administration forms.
  */
 
 use Drupal\content_translation\BundleTranslationSettingsInterface;
diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module
index a88445f037c24b2eab47387d62a5042b1e334b8b..b78d838ef509f09bd762e5ab481df5f8ee499eb9 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -2,56 +2,13 @@
 
 /**
  * @file
- * Allows entities to be translated into different languages.
  */
 
-use Drupal\Core\Url;
-use Drupal\content_translation\BundleTranslationSettingsInterface;
-use Drupal\content_translation\ContentTranslationManager;
 use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Cache\CacheableMetadata;
-use Drupal\Core\Entity\ContentEntityFormInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\language\ContentLanguageSettingsInterface;
-
-/**
- * Implements hook_help().
- */
-function content_translation_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.content_translation':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Content Translation module allows you to translate content, comments, content blocks, taxonomy terms, users and other <a href=":field_help" title="Field module help, with background on content entities">content entities</a>. Together with the modules <a href=":language">Language</a>, <a href=":config-trans">Configuration Translation</a>, and <a href=":locale">Interface Translation</a>, it allows you to build multilingual websites. For more information, see the <a href=":translation-entity">online documentation for the Content Translation module</a>.', [':locale' => (\Drupal::moduleHandler()->moduleExists('locale')) ? Url::fromRoute('help.page', ['name' => 'locale'])->toString() : '#', ':config-trans' => (\Drupal::moduleHandler()->moduleExists('config_translation')) ? Url::fromRoute('help.page', ['name' => 'config_translation'])->toString() : '#', ':language' => Url::fromRoute('help.page', ['name' => 'language'])->toString(), ':translation-entity' => 'https://www.drupal.org/docs/8/core/modules/content-translation', ':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Enabling translation') . '</dt>';
-      $output .= '<dd>' . t('In order to translate content, the website must have at least two <a href=":url">languages</a>. When that is the case, you can enable translation for the desired content entities on the <a href=":translation-entity">Content language</a> page. When enabling translation you can choose the default language for content and decide whether to show the language selection field on the content editing forms.', [':url' => Url::fromRoute('entity.configurable_language.collection')->toString(), ':translation-entity' => Url::fromRoute('language.content_settings_page')->toString(), ':language-help' => Url::fromRoute('help.page', ['name' => 'language'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Enabling field translation') . '</dt>';
-      $output .= '<dd>' . t('You can define which fields of a content entity can be translated. For example, you might want to translate the title and body field while leaving the image field untranslated. If you exclude a field from being translated, it will still show up in the content editing form, but any changes made to that field will be applied to <em>all</em> translations of that content.') . '</dd>';
-      $output .= '<dt>' . t('Translating content') . '</dt>';
-      $output .= '<dd>' . t('If translation is enabled you can translate a content entity via the Translate tab (or Translate link). The Translations page of a content entity gives an overview of the translation status for the current content and lets you add, edit, and delete its translations. This process is similar for every translatable content entity on your site.') . '</dd>';
-      $output .= '<dt>' . t('Changing the source language for a translation') . '</dt>';
-      $output .= '<dd>' . t('When you add a new translation, the original text you are translating is displayed in the edit form as the <em>source</em>. If at least one translation of the original content already exists when you add a new translation, you can choose either the original content (default) or one of the other translations as the source, using the select list in the Source language section. After saving the translation, the chosen source language is then listed on the Translate tab of the content.') . '</dd>';
-      $output .= '<dt>' . t('Setting status of translations') . '</dt>';
-      $output .= '<dd>' . t('If you edit a translation in one language you may want to set the status of the other translations as <em>out-of-date</em>. You can set this status by selecting the <em>Flag other translations as outdated</em> checkbox in the Translation section of the content editing form. The status will be visible on the Translations page.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'language.content_settings_page':
-      $output = '';
-      if (!\Drupal::languageManager()->isMultilingual()) {
-        $output .= '<p>' . t('Before you can translate content, there must be at least two languages added on the <a href=":url">languages administration</a> page.', [':url' => Url::fromRoute('entity.configurable_language.collection')->toString()]) . '</p>';
-      }
-      return $output;
-  }
-}
 
 /**
  * Implements hook_module_implements_alter().
@@ -75,145 +32,6 @@ function content_translation_module_implements_alter(&$implementations, $hook) {
   }
 }
 
-/**
- * Implements hook_language_types_info_alter().
- */
-function content_translation_language_types_info_alter(array &$language_types) {
-  // Make content language negotiation configurable by removing the 'locked'
-  // flag.
-  $language_types[LanguageInterface::TYPE_CONTENT]['locked'] = FALSE;
-  unset($language_types[LanguageInterface::TYPE_CONTENT]['fixed']);
-}
-
-/**
- * Implements hook_entity_type_alter().
- *
- * The content translation UI relies on the entity info to provide its features.
- * See the documentation of hook_entity_type_build() in the Entity API
- * documentation for more details on all the entity info keys that may be
- * defined.
- *
- * To make Content Translation automatically support an entity type some keys
- * may need to be defined, but none of them is required unless the entity path
- * is different from the usual /ENTITY_TYPE/{ENTITY_TYPE} pattern (for instance
- * "/taxonomy/term/{taxonomy_term}"). Here are a list of those optional keys:
- * - canonical: This key (in the 'links' entity info property) must be defined
- *   if the entity path is different from /ENTITY_TYPE/{ENTITY_TYPE}
- * - translation: This key (in the 'handlers' entity annotation property)
- *   specifies the translation handler for the entity type. If an entity type is
- *   translatable and no translation handler is defined,
- *   \Drupal\content_translation\ContentTranslationHandler will be assumed.
- *   Every translation handler must implement
- *   \Drupal\content_translation\ContentTranslationHandlerInterface.
- * - content_translation_ui_skip: By default, entity types that do not have a
- *   canonical link template cannot be enabled for translation. Setting this key
- *   to TRUE overrides that. When that key is set, the Content Translation
- *   module will not provide any UI for translating the entity type, and the
- *   entity type should implement its own UI. For instance, this is useful for
- *   entity types that are embedded into others for editing (which would not
- *   need a canonical link, but could still support translation).
- * - content_translation_metadata: To implement its business logic the content
- *   translation UI relies on various metadata items describing the translation
- *   state. The default implementation is provided by
- *   \Drupal\content_translation\ContentTranslationMetadataWrapper, which is
- *   relying on one field for each metadata item (field definitions are provided
- *   by the translation handler). Entity types needing to customize this
- *   behavior can specify an alternative class through the
- *   'content_translation_metadata' key in the entity type definition. Every
- *   content translation metadata wrapper needs to implement
- *   \Drupal\content_translation\ContentTranslationMetadataWrapperInterface.
- *
- * If the entity paths match the default pattern above and there is no need for
- * an entity-specific translation handler, Content Translation will provide
- * built-in support for the entity. However enabling translation for each
- * translatable bundle will be required.
- *
- * @see \Drupal\Core\Entity\Annotation\EntityType
- */
-function content_translation_entity_type_alter(array &$entity_types): void {
-  // Provide defaults for translation info.
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  foreach ($entity_types as $entity_type) {
-    if ($entity_type->isTranslatable()) {
-      if (!$entity_type->hasHandlerClass('translation')) {
-        $entity_type->setHandlerClass('translation', 'Drupal\content_translation\ContentTranslationHandler');
-      }
-      if (!$entity_type->get('content_translation_metadata')) {
-        $entity_type->set('content_translation_metadata', 'Drupal\content_translation\ContentTranslationMetadataWrapper');
-      }
-      if (!$entity_type->getFormClass('content_translation_deletion')) {
-        $entity_type->setFormClass('content_translation_deletion', '\Drupal\content_translation\Form\ContentTranslationDeleteForm');
-      }
-
-      $translation = $entity_type->get('translation');
-      if (!$translation || !isset($translation['content_translation'])) {
-        $translation['content_translation'] = [];
-      }
-
-      if ($entity_type->hasLinkTemplate('canonical')) {
-        // Provide default route names for the translation paths.
-        if (!$entity_type->hasLinkTemplate('drupal:content-translation-overview')) {
-          $translations_path = $entity_type->getLinkTemplate('canonical') . '/translations';
-          $entity_type->setLinkTemplate('drupal:content-translation-overview', $translations_path);
-          $entity_type->setLinkTemplate('drupal:content-translation-add', $translations_path . '/add/{source}/{target}');
-          $entity_type->setLinkTemplate('drupal:content-translation-edit', $translations_path . '/edit/{language}');
-          $entity_type->setLinkTemplate('drupal:content-translation-delete', $translations_path . '/delete/{language}');
-        }
-        // @todo Remove this as soon as menu access checks rely on the
-        //   controller. See https://www.drupal.org/node/2155787.
-        $translation['content_translation'] += [
-          'access_callback' => 'content_translation_translate_access',
-        ];
-      }
-      $entity_type->set('translation', $translation);
-    }
-
-    $entity_type->addConstraint('ContentTranslationSynchronizedFields');
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert().
- *
- * Installs Content Translation's field storage definitions for the target
- * entity type, if required.
- *
- * Also clears the bundle information cache so that the bundle's translatability
- * will be set properly.
- *
- * @see content_translation_entity_bundle_info_alter()
- * @see \Drupal\content_translation\ContentTranslationManager::isEnabled()
- */
-function content_translation_language_content_settings_insert(ContentLanguageSettingsInterface $settings) {
-  if ($settings->getThirdPartySetting('content_translation', 'enabled', FALSE)) {
-    _content_translation_install_field_storage_definitions($settings->getTargetEntityTypeId());
-  }
-
-  \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update().
- *
- * Installs Content Translation's field storage definitions for the target
- * entity type, if required.
- *
- * Also clears the bundle information cache so that the bundle's translatability
- * will be changed properly.
- *
- * @see content_translation_entity_bundle_info_alter()
- * @see \Drupal\content_translation\ContentTranslationManager::isEnabled()
- */
-function content_translation_language_content_settings_update(ContentLanguageSettingsInterface $settings) {
-  $original_settings = $settings->original;
-  if ($settings->getThirdPartySetting('content_translation', 'enabled', FALSE)
-    && !$original_settings->getThirdPartySetting('content_translation', 'enabled', FALSE)
-  ) {
-    _content_translation_install_field_storage_definitions($settings->getTargetEntityTypeId());
-  }
-  \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
-}
-
 /**
  * Installs Content Translation's fields for a given entity type.
  *
@@ -241,127 +59,6 @@ function _content_translation_install_field_storage_definitions($entity_type_id)
   }
 }
 
-/**
- * Implements hook_entity_bundle_info_alter().
- */
-function content_translation_entity_bundle_info_alter(&$bundles) {
-  /** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
-  $content_translation_manager = \Drupal::service('content_translation.manager');
-  foreach ($bundles as $entity_type_id => &$info) {
-    foreach ($info as $bundle => &$bundle_info) {
-      $bundle_info['translatable'] = $content_translation_manager->isEnabled($entity_type_id, $bundle);
-      if ($bundle_info['translatable'] && $content_translation_manager instanceof BundleTranslationSettingsInterface) {
-        $settings = $content_translation_manager->getBundleTranslationSettings($entity_type_id, $bundle);
-        // If pending revision support is enabled for this bundle, we need to
-        // hide untranslatable field widgets, otherwise changes in pending
-        // revisions might be overridden by changes in later default revisions.
-        $bundle_info['untranslatable_fields.default_translation_affected'] =
-          !empty($settings['untranslatable_fields_hide']) || ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $bundle);
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_entity_base_field_info().
- */
-function content_translation_entity_base_field_info(EntityTypeInterface $entity_type) {
-  /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
-  $manager = \Drupal::service('content_translation.manager');
-  $entity_type_id = $entity_type->id();
-  if ($manager->isSupported($entity_type_id)) {
-    $definitions = $manager->getTranslationHandler($entity_type_id)->getFieldDefinitions();
-    $installed_storage_definitions = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions($entity_type_id);
-    // We return metadata storage fields whenever content translation is enabled
-    // or it was enabled before, so that we keep translation metadata around
-    // when translation is disabled.
-    // @todo Re-evaluate this approach and consider removing field storage
-    //   definitions and the related field data if the entity type has no bundle
-    //   enabled for translation.
-    // @see https://www.drupal.org/node/2907777
-    if ($manager->isEnabled($entity_type_id) || array_intersect_key($definitions, $installed_storage_definitions)) {
-      return $definitions;
-    }
-  }
-}
-
-/**
- * Implements hook_field_info_alter().
- *
- * Content translation extends the @FieldType annotation with following key:
- * - column_groups: contains information about the field type properties
- *   which columns should be synchronized across different translations and
- *   which are translatable. This is useful for instance to translate the
- *   "alt" and "title" textual elements of an image field, while keeping the
- *   same image on every translation. Each group has the following keys:
- *   - title: Title of the column group.
- *   - translatable: (optional) If the column group should be translatable by
- *     default, defaults to FALSE.
- *   - columns: (optional) A list of columns of this group. Defaults to the
- *     name of the group as the single column.
- *   - require_all_groups_for_translation: (optional) Set to TRUE to enforce
- *     that making this column group translatable requires all others to be
- *     translatable too.
- *
- * @see Drupal\image\Plugin\Field\FieldType\ImageItem
- */
-function content_translation_field_info_alter(&$info) {
-  foreach ($info as $key => $settings) {
-    // Supply the column_groups key if it's not there.
-    if (empty($settings['column_groups'])) {
-      $info[$key]['column_groups'] = [];
-    }
-  }
-}
-
-/**
- * Implements hook_entity_operation().
- */
-function content_translation_entity_operation(EntityInterface $entity) {
-  $operations = [];
-  if ($entity->hasLinkTemplate('drupal:content-translation-overview') && content_translation_translate_access($entity)->isAllowed()) {
-    $operations['translate'] = [
-      'title' => t('Translate'),
-      'url' => $entity->toUrl('drupal:content-translation-overview'),
-      'weight' => 50,
-    ];
-  }
-  return $operations;
-}
-
-/**
- * Implements hook_views_data_alter().
- */
-function content_translation_views_data_alter(array &$data) {
-  // Add the content translation entity link definition to Views data for entity
-  // types having translation enabled.
-  $entity_types = \Drupal::entityTypeManager()->getDefinitions();
-  /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
-  $manager = \Drupal::service('content_translation.manager');
-  foreach ($entity_types as $entity_type_id => $entity_type) {
-    $base_table = $entity_type->getBaseTable();
-    if (isset($data[$base_table]) && $entity_type->hasLinkTemplate('drupal:content-translation-overview') && $manager->isEnabled($entity_type_id)) {
-      $t_arguments = ['@entity_type_label' => $entity_type->getLabel()];
-      $data[$base_table]['translation_link'] = [
-        'field' => [
-          'title' => t('Link to translate @entity_type_label', $t_arguments),
-          'help' => t('Provide a translation link to the @entity_type_label.', $t_arguments),
-          'id' => 'content_translation_link',
-        ],
-      ];
-    }
-  }
-}
-
-/**
- * Implements hook_menu_links_discovered_alter().
- */
-function content_translation_menu_links_discovered_alter(array &$links) {
-  // Clarify where translation settings are located.
-  $links['language.content_settings_page']['title'] = new TranslatableMarkup('Content language and translation');
-  $links['language.content_settings_page']['description'] = new TranslatableMarkup('Configure language and translation support for content.');
-}
-
 /**
  * Access callback for the translation overview page.
  *
@@ -380,167 +77,6 @@ function content_translation_translate_access(EntityInterface $entity) {
   return AccessResult::allowedIf($condition)->cachePerPermissions()->addCacheableDependency($entity);
 }
 
-/**
- * Implements hook_form_alter().
- */
-function content_translation_form_alter(array &$form, FormStateInterface $form_state): void {
-  $form_object = $form_state->getFormObject();
-  if (!($form_object instanceof ContentEntityFormInterface)) {
-    return;
-  }
-  $entity = $form_object->getEntity();
-  $op = $form_object->getOperation();
-
-  // Let the content translation handler alter the content entity form. This can
-  // be the 'add' or 'edit' form. It also tries a 'default' form in case neither
-  // of the aforementioned forms are defined.
-  if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && count($entity->getTranslationLanguages()) > 1 && in_array($op, ['edit', 'add', 'default'], TRUE)) {
-    $controller = \Drupal::entityTypeManager()->getHandler($entity->getEntityTypeId(), 'translation');
-    $controller->entityFormAlter($form, $form_state, $entity);
-
-    // @todo Move the following lines to the code generating the property form
-    //   elements once we have an official #multilingual FAPI key.
-    $translations = $entity->getTranslationLanguages();
-    $form_langcode = $form_object->getFormLangcode($form_state);
-
-    // Handle fields shared between translations when there is at least one
-    // translation available or a new one is being created.
-    if (!$entity->isNew() && (!isset($translations[$form_langcode]) || count($translations) > 1)) {
-      foreach ($entity->getFieldDefinitions() as $field_name => $definition) {
-
-        // Allow the widget to define if it should be treated as multilingual
-        // by respecting an already set #multilingual key.
-        if (isset($form[$field_name]) && !isset($form[$field_name]['#multilingual'])) {
-          $form[$field_name]['#multilingual'] = $definition->isTranslatable();
-        }
-      }
-    }
-
-    // The footer region, if defined, may contain multilingual widgets so we
-    // need to always display it.
-    if (isset($form['footer'])) {
-      $form['footer']['#multilingual'] = TRUE;
-    }
-  }
-}
-
-/**
- * Implements hook_language_fallback_candidates_OPERATION_alter().
- *
- * Performs language fallback for inaccessible translations.
- */
-function content_translation_language_fallback_candidates_entity_view_alter(&$candidates, $context) {
-  /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
-  $entity = $context['data'];
-  $entity_type_id = $entity->getEntityTypeId();
-  /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
-  $manager = \Drupal::service('content_translation.manager');
-  if ($manager->isEnabled($entity_type_id, $entity->bundle())) {
-    /** @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */
-    $handler = \Drupal::entityTypeManager()->getHandler($entity->getEntityTypeId(), 'translation');
-    foreach ($entity->getTranslationLanguages() as $langcode => $language) {
-      $metadata = $manager->getTranslationMetadata($entity->getTranslation($langcode));
-      if (!$metadata->isPublished()) {
-        $access = $handler->getTranslationAccess($entity, 'update');
-        $entity->addCacheableDependency($access);
-        if (!$access->isAllowed()) {
-
-          // If the user has no translation update access, also check view
-          // access for that translation, to allow other modules to allow access
-          // to unpublished translations.
-          $access = $entity->getTranslation($langcode)->access('view', NULL, TRUE);
-          $entity->addCacheableDependency($access);
-          if (!$access->isAllowed()) {
-            unset($candidates[$langcode]);
-          }
-        }
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_entity_extra_field_info().
- */
-function content_translation_entity_extra_field_info() {
-  $extra = [];
-  $bundle_info_service = \Drupal::service('entity_type.bundle.info');
-  foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type => $info) {
-    foreach ($bundle_info_service->getBundleInfo($entity_type) as $bundle => $bundle_info) {
-      if (\Drupal::service('content_translation.manager')->isEnabled($entity_type, $bundle)) {
-        $extra[$entity_type][$bundle]['form']['translation'] = [
-          'label' => t('Translation'),
-          'description' => t('Translation settings'),
-          'weight' => 10,
-        ];
-      }
-    }
-  }
-
-  return $extra;
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for 'field_config_edit_form'.
- */
-function content_translation_form_field_config_edit_form_alter(array &$form, FormStateInterface $form_state): void {
-  $field = $form_state->getFormObject()->getEntity();
-  $bundle_is_translatable = \Drupal::service('content_translation.manager')->isEnabled($field->getTargetEntityTypeId(), $field->getTargetBundle());
-
-  $form['translatable'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Users may translate this field'),
-    '#default_value' => $field->isTranslatable(),
-    '#weight' => -1,
-    '#disabled' => !$bundle_is_translatable,
-    '#access' => $field->getFieldStorageDefinition()->isTranslatable(),
-  ];
-
-  // Provide helpful pointers for administrators.
-  if (\Drupal::currentUser()->hasPermission('administer content translation') &&  !$bundle_is_translatable) {
-    $toggle_url = Url::fromRoute('language.content_settings_page', [], [
-      'query' => \Drupal::destination()->getAsArray(),
-    ])->toString();
-    $form['translatable']['#description'] = t('To configure translation for this field, <a href=":language-settings-url">enable language support</a> for this type.', [
-      ':language-settings-url' => $toggle_url,
-    ]);
-  }
-
-  if ($field->isTranslatable()) {
-    \Drupal::moduleHandler()->loadInclude('content_translation', 'inc', 'content_translation.admin');
-    $element = content_translation_field_sync_widget($field);
-    if ($element) {
-      $form['third_party_settings']['content_translation']['translation_sync'] = $element;
-      $form['third_party_settings']['content_translation']['translation_sync']['#weight'] = -10;
-    }
-  }
-}
-
-/**
- * Implements hook_entity_presave().
- */
-function content_translation_entity_presave(EntityInterface $entity) {
-  if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && !$entity->isNew() && isset($entity->original)) {
-    /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
-    $manager = \Drupal::service('content_translation.manager');
-    if (!$manager->isEnabled($entity->getEntityTypeId(), $entity->bundle())) {
-      return;
-    }
-    $langcode = $entity->language()->getId();
-    $source_langcode = !$entity->original->hasTranslation($langcode) ? $manager->getTranslationMetadata($entity)->getSource() : NULL;
-    \Drupal::service('content_translation.synchronizer')->synchronizeFields($entity, $langcode, $source_langcode);
-  }
-}
-
-/**
- * Implements hook_element_info_alter().
- */
-function content_translation_element_info_alter(&$type) {
-  if (isset($type['language_configuration'])) {
-    $type['language_configuration']['#process'][] = 'content_translation_language_configuration_element_process';
-  }
-}
-
 /**
  * Returns a widget to enable content translation per entity bundle.
  *
@@ -646,14 +182,6 @@ function content_translation_language_configuration_element_submit(array $form,
   }
 }
 
-/**
- * Implements hook_form_FORM_ID_alter() for language_content_settings_form().
- */
-function content_translation_form_language_content_settings_form_alter(array &$form, FormStateInterface $form_state): void {
-  \Drupal::moduleHandler()->loadInclude('content_translation', 'inc', 'content_translation.admin');
-  _content_translation_form_language_content_settings_form_alter($form, $form_state);
-}
-
 /**
  * Implements hook_preprocess_HOOK() for language-content-settings-table.html.twig.
  */
@@ -661,64 +189,3 @@ function content_translation_preprocess_language_content_settings_table(&$variab
   \Drupal::moduleHandler()->loadInclude('content_translation', 'inc', 'content_translation.admin');
   _content_translation_preprocess_language_content_settings_table($variables);
 }
-
-/**
- * Implements hook_page_attachments().
- */
-function content_translation_page_attachments(&$page) {
-  $cache = CacheableMetadata::createFromRenderArray($page);
-  $route_match = \Drupal::routeMatch();
-
-  // If the current route has no parameters, return.
-  if (!($route = $route_match->getRouteObject()) || !($parameters = $route->getOption('parameters'))) {
-    return;
-  }
-  $is_front = \Drupal::service('path.matcher')->isFrontPage();
-
-  // Determine if the current route represents an entity.
-  foreach ($parameters as $name => $options) {
-    if (!isset($options['type']) || !str_starts_with($options['type'], 'entity:')) {
-      continue;
-    }
-
-    $entity = $route_match->getParameter($name);
-    if ($entity instanceof ContentEntityInterface && $entity->hasLinkTemplate('canonical')) {
-      // Current route represents a content entity. Build hreflang links.
-      foreach ($entity->getTranslationLanguages() as $language) {
-        // Skip any translation that cannot be viewed.
-        $translation = $entity->getTranslation($language->getId());
-        $access = $translation->access('view', NULL, TRUE);
-        $cache->addCacheableDependency($access);
-        if (!$access->isAllowed()) {
-          continue;
-        }
-        if ($is_front) {
-          // If the current page is front page, do not create hreflang links
-          // from the entity route, just add the languages to root path.
-          $url = Url::fromRoute('<front>', [], [
-            'absolute' => TRUE,
-            'language' => $language,
-          ])->toString();
-        }
-        // Create links for the entity path.
-        else {
-          $url = $entity->toUrl('canonical')
-            ->setOption('language', $language)
-            ->setAbsolute()
-            ->toString();
-        }
-        $page['#attached']['html_head_link'][] = [
-          [
-            'rel' => 'alternate',
-            'hreflang' => $language->getId(),
-            'href' => $url,
-          ],
-        ];
-      }
-    }
-    // Since entity was found, no need to iterate further.
-    break;
-  }
-  // Apply updated caching information.
-  $cache->applyTo($page);
-}
diff --git a/core/modules/content_translation/src/Hook/ContentTranslationHooks.php b/core/modules/content_translation/src/Hook/ContentTranslationHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..df52f08057e1327f8415b5879f04a706afc7a1e5
--- /dev/null
+++ b/core/modules/content_translation/src/Hook/ContentTranslationHooks.php
@@ -0,0 +1,550 @@
+<?php
+
+namespace Drupal\content_translation\Hook;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\ContentEntityFormInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\content_translation\ContentTranslationManager;
+use Drupal\content_translation\BundleTranslationSettingsInterface;
+use Drupal\language\ContentLanguageSettingsInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for content_translation.
+ */
+class ContentTranslationHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.content_translation':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Content Translation module allows you to translate content, comments, content blocks, taxonomy terms, users and other <a href=":field_help" title="Field module help, with background on content entities">content entities</a>. Together with the modules <a href=":language">Language</a>, <a href=":config-trans">Configuration Translation</a>, and <a href=":locale">Interface Translation</a>, it allows you to build multilingual websites. For more information, see the <a href=":translation-entity">online documentation for the Content Translation module</a>.', [
+          ':locale' => \Drupal::moduleHandler()->moduleExists('locale') ? Url::fromRoute('help.page', [
+            'name' => 'locale',
+          ])->toString() : '#',
+          ':config-trans' => \Drupal::moduleHandler()->moduleExists('config_translation') ? Url::fromRoute('help.page', [
+            'name' => 'config_translation',
+          ])->toString() : '#',
+          ':language' => Url::fromRoute('help.page', [
+            'name' => 'language',
+          ])->toString(),
+          ':translation-entity' => 'https://www.drupal.org/docs/8/core/modules/content-translation',
+          ':field_help' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Enabling translation') . '</dt>';
+        $output .= '<dd>' . t('In order to translate content, the website must have at least two <a href=":url">languages</a>. When that is the case, you can enable translation for the desired content entities on the <a href=":translation-entity">Content language</a> page. When enabling translation you can choose the default language for content and decide whether to show the language selection field on the content editing forms.', [
+          ':url' => Url::fromRoute('entity.configurable_language.collection')->toString(),
+          ':translation-entity' => Url::fromRoute('language.content_settings_page')->toString(),
+          ':language-help' => Url::fromRoute('help.page', [
+            'name' => 'language',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Enabling field translation') . '</dt>';
+        $output .= '<dd>' . t('You can define which fields of a content entity can be translated. For example, you might want to translate the title and body field while leaving the image field untranslated. If you exclude a field from being translated, it will still show up in the content editing form, but any changes made to that field will be applied to <em>all</em> translations of that content.') . '</dd>';
+        $output .= '<dt>' . t('Translating content') . '</dt>';
+        $output .= '<dd>' . t('If translation is enabled you can translate a content entity via the Translate tab (or Translate link). The Translations page of a content entity gives an overview of the translation status for the current content and lets you add, edit, and delete its translations. This process is similar for every translatable content entity on your site.') . '</dd>';
+        $output .= '<dt>' . t('Changing the source language for a translation') . '</dt>';
+        $output .= '<dd>' . t('When you add a new translation, the original text you are translating is displayed in the edit form as the <em>source</em>. If at least one translation of the original content already exists when you add a new translation, you can choose either the original content (default) or one of the other translations as the source, using the select list in the Source language section. After saving the translation, the chosen source language is then listed on the Translate tab of the content.') . '</dd>';
+        $output .= '<dt>' . t('Setting status of translations') . '</dt>';
+        $output .= '<dd>' . t('If you edit a translation in one language you may want to set the status of the other translations as <em>out-of-date</em>. You can set this status by selecting the <em>Flag other translations as outdated</em> checkbox in the Translation section of the content editing form. The status will be visible on the Translations page.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'language.content_settings_page':
+        $output = '';
+        if (!\Drupal::languageManager()->isMultilingual()) {
+          $output .= '<p>' . t('Before you can translate content, there must be at least two languages added on the <a href=":url">languages administration</a> page.', [
+            ':url' => Url::fromRoute('entity.configurable_language.collection')->toString(),
+          ]) . '</p>';
+        }
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_language_types_info_alter().
+   */
+  #[Hook('language_types_info_alter')]
+  public function languageTypesInfoAlter(array &$language_types) {
+    // Make content language negotiation configurable by removing the 'locked'
+    // flag.
+    $language_types[LanguageInterface::TYPE_CONTENT]['locked'] = FALSE;
+    unset($language_types[LanguageInterface::TYPE_CONTENT]['fixed']);
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   *
+   * The content translation UI relies on the entity info to provide its features.
+   * See the documentation of hook_entity_type_build() in the Entity API
+   * documentation for more details on all the entity info keys that may be
+   * defined.
+   *
+   * To make Content Translation automatically support an entity type some keys
+   * may need to be defined, but none of them is required unless the entity path
+   * is different from the usual /ENTITY_TYPE/{ENTITY_TYPE} pattern (for instance
+   * "/taxonomy/term/{taxonomy_term}"). Here are a list of those optional keys:
+   * - canonical: This key (in the 'links' entity info property) must be defined
+   *   if the entity path is different from /ENTITY_TYPE/{ENTITY_TYPE}
+   * - translation: This key (in the 'handlers' entity annotation property)
+   *   specifies the translation handler for the entity type. If an entity type is
+   *   translatable and no translation handler is defined,
+   *   \Drupal\content_translation\ContentTranslationHandler will be assumed.
+   *   Every translation handler must implement
+   *   \Drupal\content_translation\ContentTranslationHandlerInterface.
+   * - content_translation_ui_skip: By default, entity types that do not have a
+   *   canonical link template cannot be enabled for translation. Setting this key
+   *   to TRUE overrides that. When that key is set, the Content Translation
+   *   module will not provide any UI for translating the entity type, and the
+   *   entity type should implement its own UI. For instance, this is useful for
+   *   entity types that are embedded into others for editing (which would not
+   *   need a canonical link, but could still support translation).
+   * - content_translation_metadata: To implement its business logic the content
+   *   translation UI relies on various metadata items describing the translation
+   *   state. The default implementation is provided by
+   *   \Drupal\content_translation\ContentTranslationMetadataWrapper, which is
+   *   relying on one field for each metadata item (field definitions are provided
+   *   by the translation handler). Entity types needing to customize this
+   *   behavior can specify an alternative class through the
+   *   'content_translation_metadata' key in the entity type definition. Every
+   *   content translation metadata wrapper needs to implement
+   *   \Drupal\content_translation\ContentTranslationMetadataWrapperInterface.
+   *
+   * If the entity paths match the default pattern above and there is no need for
+   * an entity-specific translation handler, Content Translation will provide
+   * built-in support for the entity. However enabling translation for each
+   * translatable bundle will be required.
+   *
+   * @see \Drupal\Core\Entity\Annotation\EntityType
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    // Provide defaults for translation info.
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    foreach ($entity_types as $entity_type) {
+      if ($entity_type->isTranslatable()) {
+        if (!$entity_type->hasHandlerClass('translation')) {
+          $entity_type->setHandlerClass('translation', 'Drupal\content_translation\ContentTranslationHandler');
+        }
+        if (!$entity_type->get('content_translation_metadata')) {
+          $entity_type->set('content_translation_metadata', 'Drupal\content_translation\ContentTranslationMetadataWrapper');
+        }
+        if (!$entity_type->getFormClass('content_translation_deletion')) {
+          $entity_type->setFormClass('content_translation_deletion', '\Drupal\content_translation\Form\ContentTranslationDeleteForm');
+        }
+        $translation = $entity_type->get('translation');
+        if (!$translation || !isset($translation['content_translation'])) {
+          $translation['content_translation'] = [];
+        }
+        if ($entity_type->hasLinkTemplate('canonical')) {
+          // Provide default route names for the translation paths.
+          if (!$entity_type->hasLinkTemplate('drupal:content-translation-overview')) {
+            $translations_path = $entity_type->getLinkTemplate('canonical') . '/translations';
+            $entity_type->setLinkTemplate('drupal:content-translation-overview', $translations_path);
+            $entity_type->setLinkTemplate('drupal:content-translation-add', $translations_path . '/add/{source}/{target}');
+            $entity_type->setLinkTemplate('drupal:content-translation-edit', $translations_path . '/edit/{language}');
+            $entity_type->setLinkTemplate('drupal:content-translation-delete', $translations_path . '/delete/{language}');
+          }
+          // @todo Remove this as soon as menu access checks rely on the
+          //   controller. See https://www.drupal.org/node/2155787.
+          $translation['content_translation'] += ['access_callback' => 'content_translation_translate_access'];
+        }
+        $entity_type->set('translation', $translation);
+      }
+      $entity_type->addConstraint('ContentTranslationSynchronizedFields');
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert().
+   *
+   * Installs Content Translation's field storage definitions for the target
+   * entity type, if required.
+   *
+   * Also clears the bundle information cache so that the bundle's translatability
+   * will be set properly.
+   *
+   * @see content_translation_entity_bundle_info_alter()
+   * @see \Drupal\content_translation\ContentTranslationManager::isEnabled()
+   */
+  #[Hook('language_content_settings_insert')]
+  public function languageContentSettingsInsert(ContentLanguageSettingsInterface $settings) {
+    if ($settings->getThirdPartySetting('content_translation', 'enabled', FALSE)) {
+      _content_translation_install_field_storage_definitions($settings->getTargetEntityTypeId());
+    }
+    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update().
+   *
+   * Installs Content Translation's field storage definitions for the target
+   * entity type, if required.
+   *
+   * Also clears the bundle information cache so that the bundle's translatability
+   * will be changed properly.
+   *
+   * @see content_translation_entity_bundle_info_alter()
+   * @see \Drupal\content_translation\ContentTranslationManager::isEnabled()
+   */
+  #[Hook('language_content_settings_update')]
+  public function languageContentSettingsUpdate(ContentLanguageSettingsInterface $settings) {
+    $original_settings = $settings->original;
+    if ($settings->getThirdPartySetting('content_translation', 'enabled', FALSE) && !$original_settings->getThirdPartySetting('content_translation', 'enabled', FALSE)) {
+      _content_translation_install_field_storage_definitions($settings->getTargetEntityTypeId());
+    }
+    \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+  }
+
+  /**
+   * Implements hook_entity_bundle_info_alter().
+   */
+  #[Hook('entity_bundle_info_alter')]
+  public function entityBundleInfoAlter(&$bundles) {
+    /** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
+    $content_translation_manager = \Drupal::service('content_translation.manager');
+    foreach ($bundles as $entity_type_id => &$info) {
+      foreach ($info as $bundle => &$bundle_info) {
+        $bundle_info['translatable'] = $content_translation_manager->isEnabled($entity_type_id, $bundle);
+        if ($bundle_info['translatable'] && $content_translation_manager instanceof BundleTranslationSettingsInterface) {
+          $settings = $content_translation_manager->getBundleTranslationSettings($entity_type_id, $bundle);
+          // If pending revision support is enabled for this bundle, we need to
+          // hide untranslatable field widgets, otherwise changes in pending
+          // revisions might be overridden by changes in later default revisions.
+          $bundle_info['untranslatable_fields.default_translation_affected'] = !empty($settings['untranslatable_fields_hide']) || ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $bundle);
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
+    $manager = \Drupal::service('content_translation.manager');
+    $entity_type_id = $entity_type->id();
+    if ($manager->isSupported($entity_type_id)) {
+      $definitions = $manager->getTranslationHandler($entity_type_id)->getFieldDefinitions();
+      $installed_storage_definitions = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions($entity_type_id);
+      // We return metadata storage fields whenever content translation is enabled
+      // or it was enabled before, so that we keep translation metadata around
+      // when translation is disabled.
+      // @todo Re-evaluate this approach and consider removing field storage
+      //   definitions and the related field data if the entity type has no bundle
+      //   enabled for translation.
+      // @see https://www.drupal.org/node/2907777
+      if ($manager->isEnabled($entity_type_id) || array_intersect_key($definitions, $installed_storage_definitions)) {
+        return $definitions;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_field_info_alter().
+   *
+   * Content translation extends the @FieldType annotation with following key:
+   * - column_groups: contains information about the field type properties
+   *   which columns should be synchronized across different translations and
+   *   which are translatable. This is useful for instance to translate the
+   *   "alt" and "title" textual elements of an image field, while keeping the
+   *   same image on every translation. Each group has the following keys:
+   *   - title: Title of the column group.
+   *   - translatable: (optional) If the column group should be translatable by
+   *     default, defaults to FALSE.
+   *   - columns: (optional) A list of columns of this group. Defaults to the
+   *     name of the group as the single column.
+   *   - require_all_groups_for_translation: (optional) Set to TRUE to enforce
+   *     that making this column group translatable requires all others to be
+   *     translatable too.
+   *
+   * @see Drupal\image\Plugin\Field\FieldType\ImageItem
+   */
+  #[Hook('field_info_alter')]
+  public function fieldInfoAlter(&$info) {
+    foreach ($info as $key => $settings) {
+      // Supply the column_groups key if it's not there.
+      if (empty($settings['column_groups'])) {
+        $info[$key]['column_groups'] = [];
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $entity) {
+    $operations = [];
+    if ($entity->hasLinkTemplate('drupal:content-translation-overview') && content_translation_translate_access($entity)->isAllowed()) {
+      $operations['translate'] = [
+        'title' => t('Translate'),
+        'url' => $entity->toUrl('drupal:content-translation-overview'),
+        'weight' => 50,
+      ];
+    }
+    return $operations;
+  }
+
+  /**
+   * Implements hook_views_data_alter().
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(array &$data) {
+    // Add the content translation entity link definition to Views data for entity
+    // types having translation enabled.
+    $entity_types = \Drupal::entityTypeManager()->getDefinitions();
+    /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
+    $manager = \Drupal::service('content_translation.manager');
+    foreach ($entity_types as $entity_type_id => $entity_type) {
+      $base_table = $entity_type->getBaseTable();
+      if (isset($data[$base_table]) && $entity_type->hasLinkTemplate('drupal:content-translation-overview') && $manager->isEnabled($entity_type_id)) {
+        $t_arguments = ['@entity_type_label' => $entity_type->getLabel()];
+        $data[$base_table]['translation_link'] = [
+          'field' => [
+            'title' => t('Link to translate @entity_type_label', $t_arguments),
+            'help' => t('Provide a translation link to the @entity_type_label.', $t_arguments),
+            'id' => 'content_translation_link',
+          ],
+        ];
+      }
+    }
+  }
+
+  /**
+   * Implements hook_menu_links_discovered_alter().
+   */
+  #[Hook('menu_links_discovered_alter')]
+  public function menuLinksDiscoveredAlter(array &$links) {
+    // Clarify where translation settings are located.
+    $links['language.content_settings_page']['title'] = new TranslatableMarkup('Content language and translation');
+    $links['language.content_settings_page']['description'] = new TranslatableMarkup('Configure language and translation support for content.');
+  }
+
+  /**
+   * Implements hook_form_alter().
+   */
+  #[Hook('form_alter')]
+  public function formAlter(array &$form, FormStateInterface $form_state) : void {
+    $form_object = $form_state->getFormObject();
+    if (!$form_object instanceof ContentEntityFormInterface) {
+      return;
+    }
+    $entity = $form_object->getEntity();
+    $op = $form_object->getOperation();
+    // Let the content translation handler alter the content entity form. This can
+    // be the 'add' or 'edit' form. It also tries a 'default' form in case neither
+    // of the aforementioned forms are defined.
+    if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && count($entity->getTranslationLanguages()) > 1 && in_array($op, ['edit', 'add', 'default'], TRUE)) {
+      $controller = \Drupal::entityTypeManager()->getHandler($entity->getEntityTypeId(), 'translation');
+      $controller->entityFormAlter($form, $form_state, $entity);
+      // @todo Move the following lines to the code generating the property form
+      //   elements once we have an official #multilingual FAPI key.
+      $translations = $entity->getTranslationLanguages();
+      $form_langcode = $form_object->getFormLangcode($form_state);
+      // Handle fields shared between translations when there is at least one
+      // translation available or a new one is being created.
+      if (!$entity->isNew() && (!isset($translations[$form_langcode]) || count($translations) > 1)) {
+        foreach ($entity->getFieldDefinitions() as $field_name => $definition) {
+          // Allow the widget to define if it should be treated as multilingual
+          // by respecting an already set #multilingual key.
+          if (isset($form[$field_name]) && !isset($form[$field_name]['#multilingual'])) {
+            $form[$field_name]['#multilingual'] = $definition->isTranslatable();
+          }
+        }
+      }
+      // The footer region, if defined, may contain multilingual widgets so we
+      // need to always display it.
+      if (isset($form['footer'])) {
+        $form['footer']['#multilingual'] = TRUE;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_language_fallback_candidates_OPERATION_alter().
+   *
+   * Performs language fallback for inaccessible translations.
+   */
+  #[Hook('language_fallback_candidates_entity_view_alter')]
+  public function languageFallbackCandidatesEntityViewAlter(&$candidates, $context) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $context['data'];
+    $entity_type_id = $entity->getEntityTypeId();
+    /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
+    $manager = \Drupal::service('content_translation.manager');
+    if ($manager->isEnabled($entity_type_id, $entity->bundle())) {
+      /** @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */
+      $handler = \Drupal::entityTypeManager()->getHandler($entity->getEntityTypeId(), 'translation');
+      foreach ($entity->getTranslationLanguages() as $langcode => $language) {
+        $metadata = $manager->getTranslationMetadata($entity->getTranslation($langcode));
+        if (!$metadata->isPublished()) {
+          $access = $handler->getTranslationAccess($entity, 'update');
+          $entity->addCacheableDependency($access);
+          if (!$access->isAllowed()) {
+            // If the user has no translation update access, also check view
+            // access for that translation, to allow other modules to allow access
+            // to unpublished translations.
+            $access = $entity->getTranslation($langcode)->access('view', NULL, TRUE);
+            $entity->addCacheableDependency($access);
+            if (!$access->isAllowed()) {
+              unset($candidates[$langcode]);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_extra_field_info().
+   */
+  #[Hook('entity_extra_field_info')]
+  public function entityExtraFieldInfo() {
+    $extra = [];
+    $bundle_info_service = \Drupal::service('entity_type.bundle.info');
+    foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type => $info) {
+      foreach ($bundle_info_service->getBundleInfo($entity_type) as $bundle => $bundle_info) {
+        if (\Drupal::service('content_translation.manager')->isEnabled($entity_type, $bundle)) {
+          $extra[$entity_type][$bundle]['form']['translation'] = [
+            'label' => t('Translation'),
+            'description' => t('Translation settings'),
+            'weight' => 10,
+          ];
+        }
+      }
+    }
+    return $extra;
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for 'field_config_edit_form'.
+   */
+  #[Hook('form_field_config_edit_form_alter')]
+  public function formFieldConfigEditFormAlter(array &$form, FormStateInterface $form_state) : void {
+    $field = $form_state->getFormObject()->getEntity();
+    $bundle_is_translatable = \Drupal::service('content_translation.manager')->isEnabled($field->getTargetEntityTypeId(), $field->getTargetBundle());
+    $form['translatable'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Users may translate this field'),
+      '#default_value' => $field->isTranslatable(),
+      '#weight' => -1,
+      '#disabled' => !$bundle_is_translatable,
+      '#access' => $field->getFieldStorageDefinition()->isTranslatable(),
+    ];
+    // Provide helpful pointers for administrators.
+    if (\Drupal::currentUser()->hasPermission('administer content translation') && !$bundle_is_translatable) {
+      $toggle_url = Url::fromRoute('language.content_settings_page', [], ['query' => \Drupal::destination()->getAsArray()])->toString();
+      $form['translatable']['#description'] = t('To configure translation for this field, <a href=":language-settings-url">enable language support</a> for this type.', [':language-settings-url' => $toggle_url]);
+    }
+    if ($field->isTranslatable()) {
+      \Drupal::moduleHandler()->loadInclude('content_translation', 'inc', 'content_translation.admin');
+      $element = content_translation_field_sync_widget($field);
+      if ($element) {
+        $form['third_party_settings']['content_translation']['translation_sync'] = $element;
+        $form['third_party_settings']['content_translation']['translation_sync']['#weight'] = -10;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_presave().
+   */
+  #[Hook('entity_presave')]
+  public function entityPresave(EntityInterface $entity) {
+    if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && !$entity->isNew() && isset($entity->original)) {
+      /** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
+      $manager = \Drupal::service('content_translation.manager');
+      if (!$manager->isEnabled($entity->getEntityTypeId(), $entity->bundle())) {
+        return;
+      }
+      $langcode = $entity->language()->getId();
+      $source_langcode = !$entity->original->hasTranslation($langcode) ? $manager->getTranslationMetadata($entity)->getSource() : NULL;
+      \Drupal::service('content_translation.synchronizer')->synchronizeFields($entity, $langcode, $source_langcode);
+    }
+  }
+
+  /**
+   * Implements hook_element_info_alter().
+   */
+  #[Hook('element_info_alter')]
+  public function elementInfoAlter(&$type) {
+    if (isset($type['language_configuration'])) {
+      $type['language_configuration']['#process'][] = 'content_translation_language_configuration_element_process';
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for language_content_settings_form().
+   */
+  #[Hook('form_language_content_settings_form_alter')]
+  public function formLanguageContentSettingsFormAlter(array &$form, FormStateInterface $form_state) : void {
+    \Drupal::moduleHandler()->loadInclude('content_translation', 'inc', 'content_translation.admin');
+    _content_translation_form_language_content_settings_form_alter($form, $form_state);
+  }
+
+  /**
+   * Implements hook_page_attachments().
+   */
+  #[Hook('page_attachments')]
+  public function pageAttachments(&$page) {
+    $cache = CacheableMetadata::createFromRenderArray($page);
+    $route_match = \Drupal::routeMatch();
+    // If the current route has no parameters, return.
+    if (!($route = $route_match->getRouteObject()) || !($parameters = $route->getOption('parameters'))) {
+      return;
+    }
+    $is_front = \Drupal::service('path.matcher')->isFrontPage();
+    // Determine if the current route represents an entity.
+    foreach ($parameters as $name => $options) {
+      if (!isset($options['type']) || !str_starts_with($options['type'], 'entity:')) {
+        continue;
+      }
+      $entity = $route_match->getParameter($name);
+      if ($entity instanceof ContentEntityInterface && $entity->hasLinkTemplate('canonical')) {
+        // Current route represents a content entity. Build hreflang links.
+        foreach ($entity->getTranslationLanguages() as $language) {
+          // Skip any translation that cannot be viewed.
+          $translation = $entity->getTranslation($language->getId());
+          $access = $translation->access('view', NULL, TRUE);
+          $cache->addCacheableDependency($access);
+          if (!$access->isAllowed()) {
+            continue;
+          }
+          if ($is_front) {
+            // If the current page is front page, do not create hreflang links
+            // from the entity route, just add the languages to root path.
+            $url = Url::fromRoute('<front>', [], ['absolute' => TRUE, 'language' => $language])->toString();
+          }
+          else {
+            $url = $entity->toUrl('canonical')->setOption('language', $language)->setAbsolute()->toString();
+          }
+          $page['#attached']['html_head_link'][] = [['rel' => 'alternate', 'hreflang' => $language->getId(), 'href' => $url]];
+        }
+      }
+      // Since entity was found, no need to iterate further.
+      break;
+    }
+    // Apply updated caching information.
+    $cache->applyTo($page);
+  }
+
+}
diff --git a/core/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module b/core/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module
index c195951ab74bb3321e1a61adc1504724e1183bc0..2e37fcf54a7163690768227332f1f176e6153db7 100644
--- a/core/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module
+++ b/core/modules/content_translation/tests/modules/content_translation_test/content_translation_test.module
@@ -7,60 +7,7 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Implements hook_entity_bundle_info_alter().
- */
-function content_translation_test_entity_bundle_info_alter(&$bundles) {
-  // Store the initial status of the "translatable" property for the
-  // "entity_test_mul" bundle.
-  $translatable = !empty($bundles['entity_test_mul']['entity_test_mul']['translatable']);
-  \Drupal::state()->set('content_translation_test.translatable', $translatable);
-  // Make it translatable if Content Translation did not. This will make the
-  // entity object translatable even if it is disabled in Content Translation
-  // settings.
-  if (!$translatable) {
-    $bundles['entity_test_mul']['entity_test_mul']['translatable'] = TRUE;
-  }
-}
-
-/**
- * Implements hook_entity_access().
- */
-function content_translation_test_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  $access = \Drupal::state()->get('content_translation.entity_access.' . $entity->getEntityTypeId());
-  if (!empty($access[$operation])) {
-    return AccessResult::allowed();
-  }
-  else {
-    return AccessResult::neutral();
-  }
-}
-
-/**
- * Implements hook_form_BASE_FORM_ID_alter().
- *
- * Adds a textfield to node forms based on a request parameter.
- */
-function content_translation_test_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  $langcode = $form_state->getFormObject()->getFormLangcode($form_state);
-  if (in_array($langcode, ['en', 'fr']) && \Drupal::request()->get('test_field_only_en_fr')) {
-    $form['test_field_only_en_fr'] = [
-      '#type' => 'textfield',
-      '#title' => 'Field only available on the english and french form',
-    ];
-
-    foreach (array_keys($form['actions']) as $action) {
-      if ($action != 'preview' && isset($form['actions'][$action]['#type']) && $form['actions'][$action]['#type'] === 'submit') {
-        $form['actions'][$action]['#submit'][] = 'content_translation_test_form_node_form_submit';
-      }
-    }
-  }
-}
 
 /**
  * Form submission handler for custom field added based on a request parameter.
@@ -70,10 +17,3 @@ function content_translation_test_form_node_form_alter(&$form, FormStateInterfac
 function content_translation_test_form_node_form_submit($form, FormStateInterface $form_state) {
   \Drupal::state()->set('test_field_only_en_fr', $form_state->getValue('test_field_only_en_fr'));
 }
-
-/**
- * Implements hook_entity_translation_delete().
- */
-function content_translation_test_entity_translation_delete(EntityInterface $translation) {
-  \Drupal::state()->set('content_translation_test.translation_deleted', TRUE);
-}
diff --git a/core/modules/content_translation/tests/modules/content_translation_test/src/Hook/ContentTranslationTestHooks.php b/core/modules/content_translation/tests/modules/content_translation_test/src/Hook/ContentTranslationTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c2d1e50f3fcd26dabf05c759fc886acafe200b4c
--- /dev/null
+++ b/core/modules/content_translation/tests/modules/content_translation_test/src/Hook/ContentTranslationTestHooks.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\content_translation_test\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for content_translation_test.
+ */
+class ContentTranslationTestHooks {
+
+  /**
+   * Implements hook_entity_bundle_info_alter().
+   */
+  #[Hook('entity_bundle_info_alter')]
+  public function entityBundleInfoAlter(&$bundles) {
+    // Store the initial status of the "translatable" property for the
+    // "entity_test_mul" bundle.
+    $translatable = !empty($bundles['entity_test_mul']['entity_test_mul']['translatable']);
+    \Drupal::state()->set('content_translation_test.translatable', $translatable);
+    // Make it translatable if Content Translation did not. This will make the
+    // entity object translatable even if it is disabled in Content Translation
+    // settings.
+    if (!$translatable) {
+      $bundles['entity_test_mul']['entity_test_mul']['translatable'] = TRUE;
+    }
+  }
+
+  /**
+   * Implements hook_entity_access().
+   */
+  #[Hook('entity_access')]
+  public function entityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    $access = \Drupal::state()->get('content_translation.entity_access.' . $entity->getEntityTypeId());
+    if (!empty($access[$operation])) {
+      return AccessResult::allowed();
+    }
+    else {
+      return AccessResult::neutral();
+    }
+  }
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter().
+   *
+   * Adds a textfield to node forms based on a request parameter.
+   */
+  #[Hook('form_node_form_alter')]
+  public function formNodeFormAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    $langcode = $form_state->getFormObject()->getFormLangcode($form_state);
+    if (in_array($langcode, ['en', 'fr']) && \Drupal::request()->get('test_field_only_en_fr')) {
+      $form['test_field_only_en_fr'] = [
+        '#type' => 'textfield',
+        '#title' => 'Field only available on the english and french form',
+      ];
+      foreach (array_keys($form['actions']) as $action) {
+        if ($action != 'preview' && isset($form['actions'][$action]['#type']) && $form['actions'][$action]['#type'] === 'submit') {
+          $form['actions'][$action]['#submit'][] = 'content_translation_test_form_node_form_submit';
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_translation_delete().
+   */
+  #[Hook('entity_translation_delete')]
+  public function entityTranslationDelete(EntityInterface $translation) {
+    \Drupal::state()->set('content_translation_test.translation_deleted', TRUE);
+  }
+
+}
diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module
index d57ca6c17818ec5fab82d6ab0d6fe7c5a9ffb659..4b2a3ff1696bb531bc751ae071f4325da7dcfb19 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -2,103 +2,10 @@
 
 /**
  * @file
- * Adds contextual links to perform actions related to elements on a page.
  */
 
-use Drupal\Core\Url;
-use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_toolbar().
- */
-function contextual_toolbar() {
-  $items = [];
-  $items['contextual'] = [
-    '#cache' => [
-      'contexts' => [
-        'user.permissions',
-      ],
-    ],
-  ];
-
-  if (!\Drupal::currentUser()->hasPermission('access contextual links')) {
-    return $items;
-  }
-
-  $items['contextual'] += [
-    '#type' => 'toolbar_item',
-    'tab' => [
-      '#type' => 'html_tag',
-      '#tag' => 'button',
-      '#value' => t('Edit'),
-      '#attributes' => [
-        'class' => ['toolbar-icon', 'toolbar-icon-edit'],
-        'aria-pressed' => 'false',
-        'type' => 'button',
-      ],
-    ],
-    '#wrapper_attributes' => [
-      'class' => ['hidden', 'contextual-toolbar-tab'],
-    ],
-    '#attached' => [
-      'library' => [
-        'contextual/drupal.contextual-toolbar',
-      ],
-    ],
-  ];
-
-  return $items;
-}
-
-/**
- * Implements hook_page_attachments().
- *
- * Adds the drupal.contextual-links library to the page for any user who has the
- * 'access contextual links' permission.
- *
- * @see contextual_preprocess()
- */
-function contextual_page_attachments(array &$page) {
-  if (!\Drupal::currentUser()->hasPermission('access contextual links')) {
-    return;
-  }
-
-  $page['#attached']['library'][] = 'contextual/drupal.contextual-links';
-}
-
-/**
- * Implements hook_help().
- */
-function contextual_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.contextual':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Contextual links module gives users with the <em>Use contextual links</em> permission quick access to tasks associated with certain areas of pages on your site. For example, a menu displayed as a block has links to edit the menu and configure the block. For more information, see the <a href=":contextual">online documentation for the Contextual Links module</a>.', [':contextual' => 'https://www.drupal.org/docs/8/core/modules/contextual']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Displaying contextual links') . '</dt>';
-      $output .= '<dd>';
-      $output .= t('Contextual links for an area on a page are displayed using a contextual links button. There are two ways to make the contextual links button visible:');
-      $output .= '<ol>';
-      $sample_picture = [
-        '#theme' => 'image',
-        '#uri' => 'core/misc/icons/bebebe/pencil.svg',
-        '#alt' => t('contextual links button'),
-      ];
-      $sample_picture = \Drupal::service('renderer')->render($sample_picture);
-      $output .= '<li>' . t('Hovering over the area of interest will temporarily make the contextual links button visible (which looks like a pencil in most themes, and is normally displayed in the upper right corner of the area). The icon typically looks like this: @picture', ['@picture' => $sample_picture]) . '</li>';
-      $output .= '<li>' . t('If you have the <a href=":toolbar">Toolbar module</a> installed, clicking the contextual links button in the toolbar (which looks like a pencil) will make all contextual links buttons on the page visible. Clicking this button again will toggle them to invisible.', [':toolbar' => (\Drupal::moduleHandler()->moduleExists('toolbar')) ? Url::fromRoute('help.page', ['name' => 'toolbar'])->toString() : '#']) . '</li>';
-      $output .= '</ol>';
-      $output .= t('Once the contextual links button for the area of interest is visible, click the button to display the links.');
-      $output .= '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
 
 /**
  * Implements hook_preprocess().
@@ -139,18 +46,6 @@ function contextual_preprocess(&$variables, $hook, $info) {
   }
 }
 
-/**
- * Implements hook_contextual_links_view_alter().
- *
- * @see \Drupal\contextual\Plugin\views\field\ContextualLinks::render()
- */
-function contextual_contextual_links_view_alter(&$element, $items) {
-  if (isset($element['#contextual_links']['contextual'])) {
-    $encoded_links = $element['#contextual_links']['contextual']['metadata']['contextual-views-field-links'];
-    $element['#links'] = Json::decode(rawurldecode($encoded_links));
-  }
-}
-
 /**
  * Serializes #contextual_links property value array to a string.
  *
diff --git a/core/modules/contextual/contextual.views.inc b/core/modules/contextual/contextual.views.inc
deleted file mode 100644
index bac5522f159a03249dedaa62b9f32ecf0888d7b6..0000000000000000000000000000000000000000
--- a/core/modules/contextual/contextual.views.inc
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views data for contextual.module.
- */
-
-/**
- * Implements hook_views_data_alter().
- */
-function contextual_views_data_alter(&$data) {
-  $data['views']['contextual_links'] = [
-    'title' => t('Contextual Links'),
-    'help' => t('Display fields in a contextual links menu.'),
-    'field' => [
-      'id' => 'contextual_links',
-    ],
-  ];
-}
diff --git a/core/modules/contextual/src/Hook/ContextualHooks.php b/core/modules/contextual/src/Hook/ContextualHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c99f6d0ffee3128a39753f7340a05699f4f6e8f1
--- /dev/null
+++ b/core/modules/contextual/src/Hook/ContextualHooks.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Drupal\contextual\Hook;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for contextual.
+ */
+class ContextualHooks {
+
+  /**
+   * Implements hook_toolbar().
+   */
+  #[Hook('toolbar')]
+  public function toolbar() {
+    $items = [];
+    $items['contextual'] = ['#cache' => ['contexts' => ['user.permissions']]];
+    if (!\Drupal::currentUser()->hasPermission('access contextual links')) {
+      return $items;
+    }
+    $items['contextual'] += [
+      '#type' => 'toolbar_item',
+      'tab' => [
+        '#type' => 'html_tag',
+        '#tag' => 'button',
+        '#value' => t('Edit'),
+        '#attributes' => [
+          'class' => [
+            'toolbar-icon',
+            'toolbar-icon-edit',
+          ],
+          'aria-pressed' => 'false',
+          'type' => 'button',
+        ],
+      ],
+      '#wrapper_attributes' => [
+        'class' => [
+          'hidden',
+          'contextual-toolbar-tab',
+        ],
+      ],
+      '#attached' => [
+        'library' => [
+          'contextual/drupal.contextual-toolbar',
+        ],
+      ],
+    ];
+    return $items;
+  }
+
+  /**
+   * Implements hook_page_attachments().
+   *
+   * Adds the drupal.contextual-links library to the page for any user who has the
+   * 'access contextual links' permission.
+   *
+   * @see contextual_preprocess()
+   */
+  #[Hook('page_attachments')]
+  public function pageAttachments(array &$page) {
+    if (!\Drupal::currentUser()->hasPermission('access contextual links')) {
+      return;
+    }
+    $page['#attached']['library'][] = 'contextual/drupal.contextual-links';
+  }
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.contextual':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Contextual links module gives users with the <em>Use contextual links</em> permission quick access to tasks associated with certain areas of pages on your site. For example, a menu displayed as a block has links to edit the menu and configure the block. For more information, see the <a href=":contextual">online documentation for the Contextual Links module</a>.', [':contextual' => 'https://www.drupal.org/docs/8/core/modules/contextual']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Displaying contextual links') . '</dt>';
+        $output .= '<dd>';
+        $output .= t('Contextual links for an area on a page are displayed using a contextual links button. There are two ways to make the contextual links button visible:');
+        $output .= '<ol>';
+        $sample_picture = [
+          '#theme' => 'image',
+          '#uri' => 'core/misc/icons/bebebe/pencil.svg',
+          '#alt' => t('contextual links button'),
+        ];
+        $sample_picture = \Drupal::service('renderer')->render($sample_picture);
+        $output .= '<li>' . t('Hovering over the area of interest will temporarily make the contextual links button visible (which looks like a pencil in most themes, and is normally displayed in the upper right corner of the area). The icon typically looks like this: @picture', ['@picture' => $sample_picture]) . '</li>';
+        $output .= '<li>' . t('If you have the <a href=":toolbar">Toolbar module</a> installed, clicking the contextual links button in the toolbar (which looks like a pencil) will make all contextual links buttons on the page visible. Clicking this button again will toggle them to invisible.', [
+          ':toolbar' => \Drupal::moduleHandler()->moduleExists('toolbar') ? Url::fromRoute('help.page', [
+            'name' => 'toolbar',
+          ])->toString() : '#',
+        ]) . '</li>';
+        $output .= '</ol>';
+        $output .= t('Once the contextual links button for the area of interest is visible, click the button to display the links.');
+        $output .= '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_contextual_links_view_alter().
+   *
+   * @see \Drupal\contextual\Plugin\views\field\ContextualLinks::render()
+   */
+  #[Hook('contextual_links_view_alter')]
+  public function contextualLinksViewAlter(&$element, $items) {
+    if (isset($element['#contextual_links']['contextual'])) {
+      $encoded_links = $element['#contextual_links']['contextual']['metadata']['contextual-views-field-links'];
+      $element['#links'] = Json::decode(rawurldecode($encoded_links));
+    }
+  }
+
+}
diff --git a/core/modules/contextual/src/Hook/ContextualViewsHooks.php b/core/modules/contextual/src/Hook/ContextualViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..eb6d9bdf3982a0afd3ace5636d4bc99be768c9da
--- /dev/null
+++ b/core/modules/contextual/src/Hook/ContextualViewsHooks.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\contextual\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for contextual.
+ */
+class ContextualViewsHooks {
+  /**
+   * @file
+   * Provide views data for contextual.module.
+   */
+
+  /**
+   * Implements hook_views_data_alter().
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(&$data) {
+    $data['views']['contextual_links'] = [
+      'title' => t('Contextual Links'),
+      'help' => t('Display fields in a contextual links menu.'),
+      'field' => [
+        'id' => 'contextual_links',
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/contextual/tests/modules/contextual_test/contextual_test.module b/core/modules/contextual/tests/modules/contextual_test/contextual_test.module
deleted file mode 100644
index b9c78d0d9c07ef44d4a8ef6191f2c052cccfd1bb..0000000000000000000000000000000000000000
--- a/core/modules/contextual/tests/modules/contextual_test/contextual_test.module
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides test contextual link on blocks.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Block\BlockPluginInterface;
-
-/**
- * Implements hook_block_view_alter().
- */
-function contextual_test_block_view_alter(array &$build, BlockPluginInterface $block) {
-  $build['#contextual_links']['contextual_test'] = [
-    'route_parameters' => [],
-  ];
-}
-
-/**
- * Implements hook_contextual_links_view_alter().
- *
- * @todo Apparently this too late to attach the library?
- * It won't work without contextual_test_page_attachments_alter()
- * Is that a problem? Should the contextual module itself do the attaching?
- */
-function contextual_test_contextual_links_view_alter(&$element, $items) {
-  if (isset($element['#links']['contextual-test-ajax'])) {
-    $element['#attached']['library'][] = 'core/drupal.dialog.ajax';
-  }
-}
-
-/**
- * Implements hook_page_attachments_alter().
- */
-function contextual_test_page_attachments_alter(array &$attachments) {
-  $attachments['#attached']['library'][] = 'core/drupal.dialog.ajax';
-}
diff --git a/core/modules/contextual/tests/modules/contextual_test/src/Hook/ContextualTestHooks.php b/core/modules/contextual/tests/modules/contextual_test/src/Hook/ContextualTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f4e0ffcdcdb6cca743adf5d55390a76c56dc490e
--- /dev/null
+++ b/core/modules/contextual/tests/modules/contextual_test/src/Hook/ContextualTestHooks.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\contextual_test\Hook;
+
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for contextual_test.
+ */
+class ContextualTestHooks {
+
+  /**
+   * Implements hook_block_view_alter().
+   */
+  #[Hook('block_view_alter')]
+  public function blockViewAlter(array &$build, BlockPluginInterface $block) {
+    $build['#contextual_links']['contextual_test'] = ['route_parameters' => []];
+  }
+
+  /**
+   * Implements hook_contextual_links_view_alter().
+   *
+   * @todo Apparently this too late to attach the library?
+   * It won't work without contextual_test_page_attachments_alter()
+   * Is that a problem? Should the contextual module itself do the attaching?
+   */
+  #[Hook('contextual_links_view_alter')]
+  public function contextualLinksViewAlter(&$element, $items) {
+    if (isset($element['#links']['contextual-test-ajax'])) {
+      $element['#attached']['library'][] = 'core/drupal.dialog.ajax';
+    }
+  }
+
+  /**
+   * Implements hook_page_attachments_alter().
+   */
+  #[Hook('page_attachments_alter')]
+  public function pageAttachmentsAlter(array &$attachments) {
+    $attachments['#attached']['library'][] = 'core/drupal.dialog.ajax';
+  }
+
+}
diff --git a/core/modules/datetime/datetime.module b/core/modules/datetime/datetime.module
deleted file mode 100644
index d4b194d35854f41676db004eab853329f0ebbc02..0000000000000000000000000000000000000000
--- a/core/modules/datetime/datetime.module
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-/**
- * @file
- * Field hooks to implement a simple datetime field.
- */
-
-use Drupal\Core\Url;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function datetime_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.datetime':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Datetime module provides a Date field that stores dates and times. It also provides the Form API elements <em>datetime</em> and <em>datelist</em> for use in programming modules. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI module help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":datetime_do">online documentation for the Datetime module</a>.', [':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':datetime_do' => 'https://www.drupal.org/documentation/modules/datetime']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Managing and displaying date fields') . '</dt>';
-      $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the Date field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Displaying dates') . '</dt>';
-      $output .= '<dd>' . t('Dates can be displayed using the <em>Plain</em> or the <em>Default</em> formatter. The <em>Plain</em> formatter displays the date in the <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a> format. If you choose the <em>Default</em> formatter, you can choose a format from a predefined list that can be managed on the <a href=":date_format_list">Date and time formats</a> page.', [':date_format_list' => Url::fromRoute('entity.date_format.collection')->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
diff --git a/core/modules/datetime/datetime.views.inc b/core/modules/datetime/datetime.views.inc
index 1512dcce4928e02d861c4f4d4ac35f7f2512a67a..a1586168f18d47acc9938c884ac5b5ae3747268b 100644
--- a/core/modules/datetime/datetime.views.inc
+++ b/core/modules/datetime/datetime.views.inc
@@ -2,18 +2,10 @@
 
 /**
  * @file
- * Provides views data for the datetime module.
  */
 
 use Drupal\field\FieldStorageConfigInterface;
 
-/**
- * Implements hook_field_views_data().
- */
-function datetime_field_views_data(FieldStorageConfigInterface $field_storage) {
-  return datetime_type_field_views_data_helper($field_storage, [], $field_storage->getMainPropertyName());
-}
-
 /**
  * Provides Views integration for any datetime-based fields.
  *
diff --git a/core/modules/datetime/src/Hook/DatetimeHooks.php b/core/modules/datetime/src/Hook/DatetimeHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..2fe9e3eec71fba0d8a4edf9a3ad98edfa76bb62f
--- /dev/null
+++ b/core/modules/datetime/src/Hook/DatetimeHooks.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\datetime\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for datetime.
+ */
+class DatetimeHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.datetime':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Datetime module provides a Date field that stores dates and times. It also provides the Form API elements <em>datetime</em> and <em>datelist</em> for use in programming modules. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI module help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":datetime_do">online documentation for the Datetime module</a>.', [
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+          ':datetime_do' => 'https://www.drupal.org/documentation/modules/datetime',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Managing and displaying date fields') . '</dt>';
+        $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the Date field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Displaying dates') . '</dt>';
+        $output .= '<dd>' . t('Dates can be displayed using the <em>Plain</em> or the <em>Default</em> formatter. The <em>Plain</em> formatter displays the date in the <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a> format. If you choose the <em>Default</em> formatter, you can choose a format from a predefined list that can be managed on the <a href=":date_format_list">Date and time formats</a> page.', [
+          ':date_format_list' => Url::fromRoute('entity.date_format.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/datetime/src/Hook/DatetimeViewsHooks.php b/core/modules/datetime/src/Hook/DatetimeViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c41521ce99de8e1692bd376a1c8528d48c9038b0
--- /dev/null
+++ b/core/modules/datetime/src/Hook/DatetimeViewsHooks.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\datetime\Hook;
+
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for datetime.
+ */
+class DatetimeViewsHooks {
+
+  /**
+   * Implements hook_field_views_data().
+   */
+  #[Hook('field_views_data')]
+  public function fieldViewsData(FieldStorageConfigInterface $field_storage) {
+    return datetime_type_field_views_data_helper($field_storage, [], $field_storage->getMainPropertyName());
+  }
+
+}
diff --git a/core/modules/datetime_range/datetime_range.module b/core/modules/datetime_range/datetime_range.module
deleted file mode 100644
index c0be2a2089d8c0aeffabc89aa8bad47918792dd6..0000000000000000000000000000000000000000
--- a/core/modules/datetime_range/datetime_range.module
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-/**
- * @file
- * Field hooks to implement a datetime field that stores a start and end date.
- */
-
-use Drupal\Core\Url;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function datetime_range_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.datetime_range':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Datetime Range module provides a Date field that stores start dates and times, as well as end dates and times. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI module help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":datetime_do">online documentation for the Datetime Range module</a>.', [':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':datetime_do' => 'https://www.drupal.org/documentation/modules/datetime_range']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Managing and displaying date fields') . '</dt>';
-      $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the Date field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Displaying dates') . '</dt>';
-      $output .= '<dd>' . t('Dates can be displayed using the <em>Plain</em> or the <em>Default</em> formatter. The <em>Plain</em> formatter displays the date in the <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a> format. If you choose the <em>Default</em> formatter, you can choose a format from a predefined list that can be managed on the <a href=":date_format_list">Date and time formats</a> page.', [':date_format_list' => Url::fromRoute('entity.date_format.collection')->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
diff --git a/core/modules/datetime_range/datetime_range.views.inc b/core/modules/datetime_range/datetime_range.views.inc
deleted file mode 100644
index 6892ab244e16ea29ee4926e7cdb887902b4289cf..0000000000000000000000000000000000000000
--- a/core/modules/datetime_range/datetime_range.views.inc
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides views data for the datetime_range module.
- */
-
-use Drupal\field\FieldStorageConfigInterface;
-
-/**
- * Implements hook_field_views_data().
- */
-function datetime_range_field_views_data(FieldStorageConfigInterface $field_storage) {
-
-  // Include datetime.views.inc file in order for helper function
-  // datetime_type_field_views_data_helper() to be available.
-  \Drupal::moduleHandler()->loadInclude('datetime', 'inc', 'datetime.views');
-
-  // Get datetime field data for value and end_value.
-  $data = datetime_type_field_views_data_helper($field_storage, [], 'value');
-  $data = datetime_type_field_views_data_helper($field_storage, $data, 'end_value');
-
-  return $data;
-}
diff --git a/core/modules/datetime_range/src/Hook/DatetimeRangeHooks.php b/core/modules/datetime_range/src/Hook/DatetimeRangeHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..ab7e5fbc0266c47de062a5ac862b317b4510090c
--- /dev/null
+++ b/core/modules/datetime_range/src/Hook/DatetimeRangeHooks.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\datetime_range\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for datetime_range.
+ */
+class DatetimeRangeHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.datetime_range':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Datetime Range module provides a Date field that stores start dates and times, as well as end dates and times. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI module help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":datetime_do">online documentation for the Datetime Range module</a>.', [
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+          ':datetime_do' => 'https://www.drupal.org/documentation/modules/datetime_range',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Managing and displaying date fields') . '</dt>';
+        $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the Date field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Displaying dates') . '</dt>';
+        $output .= '<dd>' . t('Dates can be displayed using the <em>Plain</em> or the <em>Default</em> formatter. The <em>Plain</em> formatter displays the date in the <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a> format. If you choose the <em>Default</em> formatter, you can choose a format from a predefined list that can be managed on the <a href=":date_format_list">Date and time formats</a> page.', [
+          ':date_format_list' => Url::fromRoute('entity.date_format.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/datetime_range/src/Hook/DatetimeRangeViewsHooks.php b/core/modules/datetime_range/src/Hook/DatetimeRangeViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..714f415aa2c628bd60f630e8005e1ac75465fc67
--- /dev/null
+++ b/core/modules/datetime_range/src/Hook/DatetimeRangeViewsHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\datetime_range\Hook;
+
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for datetime_range.
+ */
+class DatetimeRangeViewsHooks {
+
+  /**
+   * Implements hook_field_views_data().
+   */
+  #[Hook('field_views_data')]
+  public function fieldViewsData(FieldStorageConfigInterface $field_storage) {
+    // Include datetime.views.inc file in order for helper function
+    // datetime_type_field_views_data_helper() to be available.
+    \Drupal::moduleHandler()->loadInclude('datetime', 'inc', 'datetime.views');
+    // Get datetime field data for value and end_value.
+    $data = datetime_type_field_views_data_helper($field_storage, [], 'value');
+    $data = datetime_type_field_views_data_helper($field_storage, $data, 'end_value');
+    return $data;
+  }
+
+}
diff --git a/core/modules/datetime_range/tests/modules/datetime_range_test/datetime_range_test.module b/core/modules/datetime_range/tests/modules/datetime_range_test/datetime_range_test.module
deleted file mode 100644
index 16f0f2e1ad034a9ab6abfc9f1a0f0e415b4f11cd..0000000000000000000000000000000000000000
--- a/core/modules/datetime_range/tests/modules/datetime_range_test/datetime_range_test.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains datetime_range_test.module.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_entity_type_alter().
- */
-function datetime_range_test_entity_type_alter(array &$entity_types): void {
-  // Inhibit views data for the 'taxonomy_term' entity type in order to cover
-  // the case when an entity type provides no views data.
-  // @see https://www.drupal.org/project/drupal/issues/2995578
-  // @see \Drupal\Tests\datetime_range\Kernel\Views\EntityTypeWithoutViewsDataTest
-  $entity_types['taxonomy_term']->setHandlerClass('views_data', NULL);
-}
diff --git a/core/modules/datetime_range/tests/modules/datetime_range_test/src/Hook/DatetimeRangeTestHooks.php b/core/modules/datetime_range/tests/modules/datetime_range_test/src/Hook/DatetimeRangeTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f40e50bb32204180787ddf6152eff2ea41d1e292
--- /dev/null
+++ b/core/modules/datetime_range/tests/modules/datetime_range_test/src/Hook/DatetimeRangeTestHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\datetime_range_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for datetime_range_test.
+ */
+class DatetimeRangeTestHooks {
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    // Inhibit views data for the 'taxonomy_term' entity type in order to cover
+    // the case when an entity type provides no views data.
+    // @see https://www.drupal.org/project/drupal/issues/2995578
+    // @see \Drupal\Tests\datetime_range\Kernel\Views\EntityTypeWithoutViewsDataTest
+    $entity_types['taxonomy_term']->setHandlerClass('views_data', NULL);
+  }
+
+}
diff --git a/core/modules/dblog/dblog.admin.inc b/core/modules/dblog/dblog.admin.inc
index 1af89a63ae30e272cc3230ae0349cc6839f57211..b5eae06ca8ddf9aeb2c58c96e19ce69d96fd3cc5 100644
--- a/core/modules/dblog/dblog.admin.inc
+++ b/core/modules/dblog/dblog.admin.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Administrative page callbacks for the Database Logging module.
  */
 
 use Drupal\Core\Logger\RfcLogLevel;
diff --git a/core/modules/dblog/dblog.module b/core/modules/dblog/dblog.module
index 98217b4b6003ea3ad54298c8a0f7e65449b24a37..f2f0eed1772a390282861b974b987915220ca2c7 100644
--- a/core/modules/dblog/dblog.module
+++ b/core/modules/dblog/dblog.module
@@ -2,88 +2,7 @@
 
 /**
  * @file
- * System monitoring and logging for administrators.
- *
- * The Database Logging module monitors your site and keeps a list of recorded
- * events containing usage and performance data, errors, warnings, and similar
- * operational information.
- */
-
-use Drupal\Core\Url;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\views\ViewExecutable;
-
-/**
- * Implements hook_help().
- */
-function dblog_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.dblog':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Database Logging module logs system events in the Drupal database. For more information, see the <a href=":dblog">online documentation for the Database Logging module</a>.', [':dblog' => 'https://www.drupal.org/documentation/modules/dblog']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Monitoring your site') . '</dt>';
-      $output .= '<dd>' . t('The Database Logging module allows you to view an event log on the <a href=":dblog">Recent log messages</a> page. The log is a chronological list of recorded events containing usage data, performance data, errors, warnings and operational information. Administrators should check the log on a regular basis to ensure their site is working properly.', [':dblog' => Url::fromRoute('dblog.overview')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Debugging site problems') . '</dt>';
-      $output .= '<dd>' . t('In case of errors or problems with the site, the <a href=":dblog">Recent log messages</a> page can be useful for debugging, since it shows the sequence of events. The log messages include usage information, warnings, and errors.', [':dblog' => Url::fromRoute('dblog.overview')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('This log is not persistent') . '</dt>';
-      $output .= '<dd>' . t('The Database Logging module logs may be cleared by administrators and automated cron tasks, so they should not be used for <a href=":audit_trail_wiki">forensic logging</a>. For forensic purposes, use the Syslog module.', [':audit_trail_wiki' => 'https://en.wikipedia.org/wiki/Audit_trail']) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'dblog.overview':
-      return '<p>' . t('The Database Logging module logs system events in the Drupal database. Monitor your site or debug site problems on this page.') . '</p>';
-  }
-}
-
-/**
- * Implements hook_menu_links_discovered_alter().
  */
-function dblog_menu_links_discovered_alter(&$links) {
-  if (\Drupal::moduleHandler()->moduleExists('search')) {
-    $links['dblog.search'] = [
-      'title' => new TranslatableMarkup('Top search phrases'),
-      'route_name' => 'dblog.search',
-      'description' => new TranslatableMarkup('View most popular search phrases.'),
-      'parent' => 'system.admin_reports',
-    ];
-  }
-
-  return $links;
-}
-
-/**
- * Implements hook_cron().
- *
- * Controls the size of the log table, paring it to 'dblog_row_limit' messages.
- */
-function dblog_cron() {
-  // Cleanup the watchdog table.
-  $row_limit = \Drupal::config('dblog.settings')->get('row_limit');
-
-  // For row limit n, get the wid of the nth row in descending wid order.
-  // Counting the most recent n rows avoids issues with wid number sequences,
-  // e.g. auto_increment value > 1 or rows deleted directly from the table.
-  if ($row_limit > 0) {
-    $connection = \Drupal::database();
-    $min_row = $connection->select('watchdog', 'w')
-      ->fields('w', ['wid'])
-      ->orderBy('wid', 'DESC')
-      ->range($row_limit - 1, 1)
-      ->execute()->fetchField();
-
-    // Delete all table entries older than the nth row, if nth row was found.
-    if ($min_row) {
-      $connection->delete('watchdog')
-        ->condition('wid', $min_row, '<')
-        ->execute();
-    }
-  }
-}
 
 /**
  * Gathers a list of uniquely defined database log message types.
@@ -95,26 +14,3 @@ function _dblog_get_message_types() {
   return \Drupal::database()->query('SELECT DISTINCT([type]) FROM {watchdog} ORDER BY [type]')
     ->fetchAllKeyed(0, 0);
 }
-
-/**
- * Implements hook_form_FORM_ID_alter() for system_logging_settings().
- */
-function dblog_form_system_logging_settings_alter(&$form, FormStateInterface $form_state): void {
-  $row_limits = [100, 1000, 10000, 100000, 1000000];
-  $form['dblog_row_limit'] = [
-    '#type' => 'select',
-    '#title' => t('Database log messages to keep'),
-    '#config_target' => 'dblog.settings:row_limit',
-    '#options' => [0 => t('All')] + array_combine($row_limits, $row_limits),
-    '#description' => t('The maximum number of messages to keep in the database log. Requires a <a href=":cron">cron maintenance task</a>.', [':cron' => Url::fromRoute('system.status')->toString()]),
-  ];
-}
-
-/**
- * Implements hook_views_pre_render().
- */
-function dblog_views_pre_render(ViewExecutable $view) {
-  if (isset($view) && ($view->storage->get('base_table') == 'watchdog')) {
-    $view->element['#attached']['library'][] = 'dblog/drupal.dblog';
-  }
-}
diff --git a/core/modules/dblog/dblog.views.inc b/core/modules/dblog/dblog.views.inc
deleted file mode 100644
index 6ad9e68ce41761b139b4f4fb86e36427c87046bb..0000000000000000000000000000000000000000
--- a/core/modules/dblog/dblog.views.inc
+++ /dev/null
@@ -1,215 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views data for dblog.module.
- */
-
-/**
- * Implements hook_views_data().
- */
-function dblog_views_data() {
-  $data = [];
-
-  $data['watchdog']['table']['group'] = t('Watchdog');
-  $data['watchdog']['table']['wizard_id'] = 'watchdog';
-
-  $data['watchdog']['table']['base'] = [
-    'field' => 'wid',
-    'title' => t('Log entries'),
-    'help' => t('Contains a list of log entries.'),
-  ];
-
-  $data['watchdog']['wid'] = [
-    'title' => t('WID'),
-    'help' => t('Unique watchdog event ID.'),
-    'field' => [
-      'id' => 'standard',
-    ],
-    'filter' => [
-      'id' => 'numeric',
-    ],
-    'argument' => [
-      'id' => 'numeric',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-  ];
-
-  $data['watchdog']['uid'] = [
-    'title' => t('UID'),
-    'help' => t('The user ID of the user on which the log entry was written.'),
-    'field' => [
-      'id' => 'standard',
-    ],
-    'filter' => [
-      'id' => 'numeric',
-    ],
-    'argument' => [
-      'id' => 'numeric',
-    ],
-    'relationship' => [
-      'title' => t('User'),
-      'help' => t('The user on which the log entry as written.'),
-      'base' => 'users_field_data',
-      'base field' => 'uid',
-      'id' => 'standard',
-    ],
-  ];
-
-  $data['watchdog']['type'] = [
-    'title' => t('Type'),
-    'help' => t('The type of the log entry, for example "user" or "page not found".'),
-    'field' => [
-      'id' => 'standard',
-    ],
-    'argument' => [
-      'id' => 'string',
-    ],
-    'filter' => [
-      'id' => 'dblog_types',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-  ];
-
-  $data['watchdog']['message'] = [
-    'title' => t('Message'),
-    'help' => t('The actual message of the log entry.'),
-    'field' => [
-      'id' => 'dblog_message',
-    ],
-    'argument' => [
-      'id' => 'string',
-    ],
-    'filter' => [
-      'id' => 'string',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-  ];
-
-  $data['watchdog']['variables'] = [
-    'title' => t('Variables'),
-    'help' => t('The variables of the log entry in a serialized format.'),
-    'field' => [
-      'id' => 'serialized',
-      'click sortable' => FALSE,
-    ],
-    'argument' => [
-      'id' => 'string',
-    ],
-    'filter' => [
-      'id' => 'string',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-  ];
-
-  $data['watchdog']['severity'] = [
-    'title' => t('Severity level'),
-    'help' => t('The severity level of the event; ranges from 0 (Emergency) to 7 (Debug).'),
-    'field' => [
-      'id' => 'machine_name',
-      'options callback' => 'Drupal\dblog\Controller\DbLogController::getLogLevelClassMap',
-    ],
-    'filter' => [
-      'id' => 'in_operator',
-      'options callback' => 'Drupal\Core\Logger\RfcLogLevel::getLevels',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-  ];
-
-  $data['watchdog']['link'] = [
-    'title' => t('Operations'),
-    'help' => t('Operation links for the event.'),
-    'field' => [
-      'id' => 'dblog_operations',
-    ],
-    'argument' => [
-      'id' => 'string',
-    ],
-    'filter' => [
-      'id' => 'string',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-  ];
-
-  $data['watchdog']['location'] = [
-    'title' => t('Location'),
-    'help' => t('URL of the origin of the event.'),
-    'field' => [
-      'id' => 'standard',
-    ],
-    'argument' => [
-      'id' => 'string',
-    ],
-    'filter' => [
-      'id' => 'string',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-  ];
-
-  $data['watchdog']['referer'] = [
-    'title' => t('Referer'),
-    'help' => t('URL of the previous page.'),
-    'field' => [
-      'id' => 'standard',
-    ],
-    'argument' => [
-      'id' => 'string',
-    ],
-    'filter' => [
-      'id' => 'string',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-  ];
-
-  $data['watchdog']['hostname'] = [
-    'title' => t('Hostname'),
-    'help' => t('Hostname of the user who triggered the event.'),
-    'field' => [
-      'id' => 'standard',
-    ],
-    'argument' => [
-      'id' => 'string',
-    ],
-    'filter' => [
-      'id' => 'string',
-    ],
-    'sort' => [
-      'id' => 'standard',
-    ],
-  ];
-
-  $data['watchdog']['timestamp'] = [
-    'title' => t('Timestamp'),
-    'help' => t('Date when the event occurred.'),
-    'field' => [
-      'id' => 'date',
-    ],
-    'argument' => [
-      'id' => 'date',
-    ],
-    'filter' => [
-      'id' => 'date',
-    ],
-    'sort' => [
-      'id' => 'date',
-    ],
-  ];
-
-  return $data;
-}
diff --git a/core/modules/dblog/src/Hook/DblogHooks.php b/core/modules/dblog/src/Hook/DblogHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..eb23fcd4dcebd10f035fa915fcdac058c76a383d
--- /dev/null
+++ b/core/modules/dblog/src/Hook/DblogHooks.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Drupal\dblog\Hook;
+
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for dblog.
+ */
+class DblogHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.dblog':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Database Logging module logs system events in the Drupal database. For more information, see the <a href=":dblog">online documentation for the Database Logging module</a>.', [':dblog' => 'https://www.drupal.org/documentation/modules/dblog']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Monitoring your site') . '</dt>';
+        $output .= '<dd>' . t('The Database Logging module allows you to view an event log on the <a href=":dblog">Recent log messages</a> page. The log is a chronological list of recorded events containing usage data, performance data, errors, warnings and operational information. Administrators should check the log on a regular basis to ensure their site is working properly.', [':dblog' => Url::fromRoute('dblog.overview')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Debugging site problems') . '</dt>';
+        $output .= '<dd>' . t('In case of errors or problems with the site, the <a href=":dblog">Recent log messages</a> page can be useful for debugging, since it shows the sequence of events. The log messages include usage information, warnings, and errors.', [':dblog' => Url::fromRoute('dblog.overview')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('This log is not persistent') . '</dt>';
+        $output .= '<dd>' . t('The Database Logging module logs may be cleared by administrators and automated cron tasks, so they should not be used for <a href=":audit_trail_wiki">forensic logging</a>. For forensic purposes, use the Syslog module.', [':audit_trail_wiki' => 'https://en.wikipedia.org/wiki/Audit_trail']) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'dblog.overview':
+        return '<p>' . t('The Database Logging module logs system events in the Drupal database. Monitor your site or debug site problems on this page.') . '</p>';
+    }
+  }
+
+  /**
+   * Implements hook_menu_links_discovered_alter().
+   */
+  #[Hook('menu_links_discovered_alter')]
+  public function menuLinksDiscoveredAlter(&$links) {
+    if (\Drupal::moduleHandler()->moduleExists('search')) {
+      $links['dblog.search'] = [
+        'title' => new TranslatableMarkup('Top search phrases'),
+        'route_name' => 'dblog.search',
+        'description' => new TranslatableMarkup('View most popular search phrases.'),
+        'parent' => 'system.admin_reports',
+      ];
+    }
+    return $links;
+  }
+
+  /**
+   * Implements hook_cron().
+   *
+   * Controls the size of the log table, paring it to 'dblog_row_limit' messages.
+   */
+  #[Hook('cron')]
+  public function cron() {
+    // Cleanup the watchdog table.
+    $row_limit = \Drupal::config('dblog.settings')->get('row_limit');
+    // For row limit n, get the wid of the nth row in descending wid order.
+    // Counting the most recent n rows avoids issues with wid number sequences,
+    // e.g. auto_increment value > 1 or rows deleted directly from the table.
+    if ($row_limit > 0) {
+      $connection = \Drupal::database();
+      $min_row = $connection->select('watchdog', 'w')->fields('w', ['wid'])->orderBy('wid', 'DESC')->range($row_limit - 1, 1)->execute()->fetchField();
+      // Delete all table entries older than the nth row, if nth row was found.
+      if ($min_row) {
+        $connection->delete('watchdog')->condition('wid', $min_row, '<')->execute();
+      }
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for system_logging_settings().
+   */
+  #[Hook('form_system_logging_settings_alter')]
+  public function formSystemLoggingSettingsAlter(&$form, FormStateInterface $form_state) : void {
+    $row_limits = [100, 1000, 10000, 100000, 1000000];
+    $form['dblog_row_limit'] = [
+      '#type' => 'select',
+      '#title' => t('Database log messages to keep'),
+      '#config_target' => 'dblog.settings:row_limit',
+      '#options' => [
+        0 => t('All'),
+      ] + array_combine($row_limits, $row_limits),
+      '#description' => t('The maximum number of messages to keep in the database log. Requires a <a href=":cron">cron maintenance task</a>.', [
+        ':cron' => Url::fromRoute('system.status')->toString(),
+      ]),
+    ];
+  }
+
+  /**
+   * Implements hook_views_pre_render().
+   */
+  #[Hook('views_pre_render')]
+  public function viewsPreRender(ViewExecutable $view) {
+    if (isset($view) && $view->storage->get('base_table') == 'watchdog') {
+      $view->element['#attached']['library'][] = 'dblog/drupal.dblog';
+    }
+  }
+
+}
diff --git a/core/modules/dblog/src/Hook/DblogViewsHooks.php b/core/modules/dblog/src/Hook/DblogViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..e6ceafc1f772667b01d0a4cc29257dc75c246bb3
--- /dev/null
+++ b/core/modules/dblog/src/Hook/DblogViewsHooks.php
@@ -0,0 +1,212 @@
+<?php
+
+namespace Drupal\dblog\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for dblog.
+ */
+class DblogViewsHooks {
+  /**
+   * @file
+   * Provide views data for dblog.module.
+   */
+
+  /**
+   * Implements hook_views_data().
+   */
+  #[Hook('views_data')]
+  public function viewsData() {
+    $data = [];
+    $data['watchdog']['table']['group'] = t('Watchdog');
+    $data['watchdog']['table']['wizard_id'] = 'watchdog';
+    $data['watchdog']['table']['base'] = [
+      'field' => 'wid',
+      'title' => t('Log entries'),
+      'help' => t('Contains a list of log entries.'),
+    ];
+    $data['watchdog']['wid'] = [
+      'title' => t('WID'),
+      'help' => t('Unique watchdog event ID.'),
+      'field' => [
+        'id' => 'standard',
+      ],
+      'filter' => [
+        'id' => 'numeric',
+      ],
+      'argument' => [
+        'id' => 'numeric',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+    ];
+    $data['watchdog']['uid'] = [
+      'title' => t('UID'),
+      'help' => t('The user ID of the user on which the log entry was written.'),
+      'field' => [
+        'id' => 'standard',
+      ],
+      'filter' => [
+        'id' => 'numeric',
+      ],
+      'argument' => [
+        'id' => 'numeric',
+      ],
+      'relationship' => [
+        'title' => t('User'),
+        'help' => t('The user on which the log entry as written.'),
+        'base' => 'users_field_data',
+        'base field' => 'uid',
+        'id' => 'standard',
+      ],
+    ];
+    $data['watchdog']['type'] = [
+      'title' => t('Type'),
+      'help' => t('The type of the log entry, for example "user" or "page not found".'),
+      'field' => [
+        'id' => 'standard',
+      ],
+      'argument' => [
+        'id' => 'string',
+      ],
+      'filter' => [
+        'id' => 'dblog_types',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+    ];
+    $data['watchdog']['message'] = [
+      'title' => t('Message'),
+      'help' => t('The actual message of the log entry.'),
+      'field' => [
+        'id' => 'dblog_message',
+      ],
+      'argument' => [
+        'id' => 'string',
+      ],
+      'filter' => [
+        'id' => 'string',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+    ];
+    $data['watchdog']['variables'] = [
+      'title' => t('Variables'),
+      'help' => t('The variables of the log entry in a serialized format.'),
+      'field' => [
+        'id' => 'serialized',
+        'click sortable' => FALSE,
+      ],
+      'argument' => [
+        'id' => 'string',
+      ],
+      'filter' => [
+        'id' => 'string',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+    ];
+    $data['watchdog']['severity'] = [
+      'title' => t('Severity level'),
+      'help' => t('The severity level of the event; ranges from 0 (Emergency) to 7 (Debug).'),
+      'field' => [
+        'id' => 'machine_name',
+        'options callback' => 'Drupal\dblog\Controller\DbLogController::getLogLevelClassMap',
+      ],
+      'filter' => [
+        'id' => 'in_operator',
+        'options callback' => 'Drupal\Core\Logger\RfcLogLevel::getLevels',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+    ];
+    $data['watchdog']['link'] = [
+      'title' => t('Operations'),
+      'help' => t('Operation links for the event.'),
+      'field' => [
+        'id' => 'dblog_operations',
+      ],
+      'argument' => [
+        'id' => 'string',
+      ],
+      'filter' => [
+        'id' => 'string',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+    ];
+    $data['watchdog']['location'] = [
+      'title' => t('Location'),
+      'help' => t('URL of the origin of the event.'),
+      'field' => [
+        'id' => 'standard',
+      ],
+      'argument' => [
+        'id' => 'string',
+      ],
+      'filter' => [
+        'id' => 'string',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+    ];
+    $data['watchdog']['referer'] = [
+      'title' => t('Referer'),
+      'help' => t('URL of the previous page.'),
+      'field' => [
+        'id' => 'standard',
+      ],
+      'argument' => [
+        'id' => 'string',
+      ],
+      'filter' => [
+        'id' => 'string',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+    ];
+    $data['watchdog']['hostname'] = [
+      'title' => t('Hostname'),
+      'help' => t('Hostname of the user who triggered the event.'),
+      'field' => [
+        'id' => 'standard',
+      ],
+      'argument' => [
+        'id' => 'string',
+      ],
+      'filter' => [
+        'id' => 'string',
+      ],
+      'sort' => [
+        'id' => 'standard',
+      ],
+    ];
+    $data['watchdog']['timestamp'] = [
+      'title' => t('Timestamp'),
+      'help' => t('Date when the event occurred.'),
+      'field' => [
+        'id' => 'date',
+      ],
+      'argument' => [
+        'id' => 'date',
+      ],
+      'filter' => [
+        'id' => 'date',
+      ],
+      'sort' => [
+        'id' => 'date',
+      ],
+    ];
+    return $data;
+  }
+
+}
diff --git a/core/modules/dynamic_page_cache/dynamic_page_cache.module b/core/modules/dynamic_page_cache/dynamic_page_cache.module
deleted file mode 100644
index 61d9b736989f5011650549d73dc6b069aa19d976..0000000000000000000000000000000000000000
--- a/core/modules/dynamic_page_cache/dynamic_page_cache.module
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-/**
- * @file
- * Caches responses for all users, handling dynamic content correctly.
- */
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function dynamic_page_cache_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.dynamic_page_cache':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Internal Dynamic Page Cache module caches pages for all users in the database, handling dynamic content correctly. For more information, see the <a href=":dynamic_page_cache-documentation">online documentation for the Internal Dynamic Page Cache module</a>.', [':dynamic_page_cache-documentation' => 'https://www.drupal.org/documentation/modules/dynamic_page_cache']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Speeding up your site') . '</dt>';
-      $output .= '<dd>' . t('Pages which are suitable for caching are cached the first time they are requested, then the cached version is served for all later requests. Dynamic content is handled automatically so that both cache correctness and hit ratio is maintained.') . '</dd>';
-      $output .= '<dd>' . t('The module requires no configuration. Every part of the page contains metadata that allows Internal Dynamic Page Cache to figure this out on its own.') . '</dd>';
-      $output .= '</dl>';
-
-      return $output;
-  }
-}
diff --git a/core/modules/dynamic_page_cache/src/Hook/DynamicPageCacheHooks.php b/core/modules/dynamic_page_cache/src/Hook/DynamicPageCacheHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..6ecdc26904a3e83cc46824cc675280291eb4490c
--- /dev/null
+++ b/core/modules/dynamic_page_cache/src/Hook/DynamicPageCacheHooks.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\dynamic_page_cache\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for dynamic_page_cache.
+ */
+class DynamicPageCacheHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.dynamic_page_cache':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Internal Dynamic Page Cache module caches pages for all users in the database, handling dynamic content correctly. For more information, see the <a href=":dynamic_page_cache-documentation">online documentation for the Internal Dynamic Page Cache module</a>.', [
+          ':dynamic_page_cache-documentation' => 'https://www.drupal.org/documentation/modules/dynamic_page_cache',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Speeding up your site') . '</dt>';
+        $output .= '<dd>' . t('Pages which are suitable for caching are cached the first time they are requested, then the cached version is served for all later requests. Dynamic content is handled automatically so that both cache correctness and hit ratio is maintained.') . '</dd>';
+        $output .= '<dd>' . t('The module requires no configuration. Every part of the page contains metadata that allows Internal Dynamic Page Cache to figure this out on its own.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/editor/editor.admin.inc b/core/modules/editor/editor.admin.inc
index e557da6a4a86a69688dabab58019bdf5ff2d0789..47ee599d235def29cc0b85d7826951574d392e14 100644
--- a/core/modules/editor/editor.admin.inc
+++ b/core/modules/editor/editor.admin.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Administration functions for editor.module.
  */
 
 use Drupal\Component\Utility\Environment;
diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module
index 8808f5aed0a5310960ee74fd9aa31d9ffae3981f..46599d004fdfcad129ce68336c3f1dc83f46bf96 100644
--- a/core/modules/editor/editor.module
+++ b/core/modules/editor/editor.module
@@ -2,169 +2,19 @@
 
 /**
  * @file
- * Adds bindings for client-side "text editors" to text formats.
  */
 
-use Drupal\Core\Url;
 use Drupal\Component\Utility\Html;
 use Drupal\Core\Form\SubformState;
 use Drupal\editor\Entity\Editor;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Render\Element;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\filter\FilterFormatInterface;
 use Drupal\filter\Plugin\FilterInterface;
 use Drupal\text\Plugin\Field\FieldType\TextItemBase;
 
-/**
- * Implements hook_help().
- */
-function editor_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.editor':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Text Editor module provides a framework that other modules (such as <a href=":ckeditor5">CKEditor5 module</a>) can use to provide toolbars and other functionality that allow users to format text more easily than typing HTML tags directly. For more information, see the <a href=":documentation">online documentation for the Text Editor module</a>.', [':documentation' => 'https://www.drupal.org/documentation/modules/editor', ':ckeditor5' => (\Drupal::moduleHandler()->moduleExists('ckeditor5')) ? Url::fromRoute('help.page', ['name' => 'ckeditor5'])->toString() : '#']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Installing text editors') . '</dt>';
-      $output .= '<dd>' . t('The Text Editor module provides a framework for managing editors. To use it, you also need to install a text editor. This can either be the core <a href=":ckeditor5">CKEditor5 module</a>, which can be installed on the <a href=":extend">Extend page</a>, or a contributed module for any other text editor. When installing a contributed text editor module, be sure to check the installation instructions, because you will most likely need to download an external library as well as the Drupal module.', [':ckeditor5' => (\Drupal::moduleHandler()->moduleExists('ckeditor5')) ? Url::fromRoute('help.page', ['name' => 'ckeditor5'])->toString() : '#', ':extend' => Url::fromRoute('system.modules_list')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Enabling a text editor for a text format') . '</dt>';
-      $output .= '<dd>' . t('On the <a href=":formats">Text formats and editors page</a> you can see which text editor is associated with each text format. You can change this by clicking on the <em>Configure</em> link, and then choosing a text editor or <em>none</em> from the <em>Text editor</em> drop-down list. The text editor will then be displayed with any text field for which this text format is chosen.', [':formats' => Url::fromRoute('filter.admin_overview')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Configuring a text editor') . '</dt>';
-      $output .= '<dd>' . t('Once a text editor is associated with a text format, you can configure it by clicking on the <em>Configure</em> link for this format. Depending on the specific text editor, you can configure it for example by adding buttons to its toolbar. Typically these buttons provide formatting or editing tools, and they often insert HTML tags into the field source. For details, see the help page of the specific text editor.') . '</dd>';
-      $output .= '<dt>' . t('Using different text editors and formats') . '</dt>';
-      $output .= '<dd>' . t('If you change the text format on a text field, the text editor will change as well because the text editor configuration is associated with the individual text format. This allows the use of the same text editor with different options for different text formats. It also allows users to choose between text formats with different text editors if they are installed.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_menu_links_discovered_alter().
- *
- * Rewrites the menu entries for filter module that relate to the configuration
- * of text editors.
- */
-function editor_menu_links_discovered_alter(array &$links) {
-  $links['filter.admin_overview']['title'] = new TranslatableMarkup('Text formats and editors');
-  $links['filter.admin_overview']['description'] = new TranslatableMarkup('Select and configure text editors, and how content is filtered when displayed.');
-}
-
-/**
- * Implements hook_element_info_alter().
- *
- * Extends the functionality of text_format elements (provided by Filter
- * module), so that selecting a text format notifies a client-side text editor
- * when it should be enabled or disabled.
- *
- * @see \Drupal\filter\Element\TextFormat
- */
-function editor_element_info_alter(&$types) {
-  $types['text_format']['#pre_render'][] = 'element.editor:preRenderTextFormat';
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function editor_form_filter_admin_overview_alter(&$form, FormStateInterface $form_state): void {
-  // @todo Cleanup column injection: https://www.drupal.org/node/1876718.
-  // Splice in the column for "Text editor" into the header.
-  $position = array_search('name', $form['formats']['#header']) + 1;
-  $start = array_splice($form['formats']['#header'], 0, $position, ['editor' => t('Text editor')]);
-  $form['formats']['#header'] = array_merge($start, $form['formats']['#header']);
-
-  // Then splice in the name of each text editor for each text format.
-  $editors = \Drupal::service('plugin.manager.editor')->getDefinitions();
-  foreach (Element::children($form['formats']) as $format_id) {
-    $editor = editor_load($format_id);
-    $editor_name = ($editor && isset($editors[$editor->getEditor()])) ? $editors[$editor->getEditor()]['label'] : '—';
-    $editor_column['editor'] = ['#markup' => $editor_name];
-    $position = array_search('name', array_keys($form['formats'][$format_id])) + 1;
-    $start = array_splice($form['formats'][$format_id], 0, $position, $editor_column);
-    $form['formats'][$format_id] = array_merge($start, $form['formats'][$format_id]);
-  }
-}
-
-/**
- * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\filter\FilterFormatEditForm.
- */
-function editor_form_filter_format_form_alter(&$form, FormStateInterface $form_state): void {
-  $editor = $form_state->get('editor');
-  if ($editor === NULL) {
-    $format = $form_state->getFormObject()->getEntity();
-    $format_id = $format->isNew() ? NULL : $format->id();
-    $editor = editor_load($format_id);
-    $form_state->set('editor', $editor);
-  }
-
-  // Associate a text editor with this text format.
-  $manager = \Drupal::service('plugin.manager.editor');
-  $editor_options = $manager->listOptions();
-  $form['editor'] = [
-    // Position the editor selection before the filter settings (weight of 0),
-    // but after the filter label and name (weight of -20).
-    '#weight' => -9,
-  ];
-  $form['editor']['editor'] = [
-    '#type' => 'select',
-    '#title' => t('Text editor'),
-    '#options' => $editor_options,
-    '#empty_option' => t('None'),
-    '#default_value' => $editor ? $editor->getEditor() : '',
-    '#ajax' => [
-      'trigger_as' => ['name' => 'editor_configure'],
-      'callback' => 'editor_form_filter_admin_form_ajax',
-      'wrapper' => 'editor-settings-wrapper',
-    ],
-    '#weight' => -10,
-  ];
-  $form['editor']['configure'] = [
-    '#type' => 'submit',
-    '#name' => 'editor_configure',
-    '#value' => t('Configure'),
-    '#limit_validation_errors' => [['editor']],
-    '#submit' => ['editor_form_filter_admin_format_editor_configure'],
-    '#ajax' => [
-      'callback' => 'editor_form_filter_admin_form_ajax',
-      'wrapper' => 'editor-settings-wrapper',
-    ],
-    '#weight' => -10,
-    '#attributes' => ['class' => ['js-hide']],
-  ];
-
-  // If there aren't any options (other than "None"), disable the select list.
-  if (empty($editor_options)) {
-    $form['editor']['editor']['#disabled'] = TRUE;
-    $form['editor']['editor']['#description'] = t('This option is disabled because no modules that provide a text editor are currently enabled.');
-  }
-
-  $form['editor']['settings'] = [
-    '#tree' => TRUE,
-    '#weight' => -8,
-    '#type' => 'container',
-    '#id' => 'editor-settings-wrapper',
-  ];
-
-  // Add editor-specific validation and submit handlers.
-  if ($editor) {
-    /** @var \Drupal\editor\Plugin\EditorPluginInterface $plugin */
-    $plugin = $manager->createInstance($editor->getEditor());
-    $form_state->set('editor_plugin', $plugin);
-    $form['editor']['settings']['subform'] = [];
-    $subform_state = SubformState::createForSubform($form['editor']['settings']['subform'], $form, $form_state);
-    $form['editor']['settings']['subform'] = $plugin->buildConfigurationForm($form['editor']['settings']['subform'], $subform_state);
-    $form['editor']['settings']['subform']['#parents'] = ['editor', 'settings'];
-  }
-
-  $form['#validate'][] = 'editor_form_filter_admin_format_validate';
-  $form['actions']['submit']['#submit'][] = 'editor_form_filter_admin_format_submit';
-}
-
 /**
  * Button submit handler for filter_format_form()'s 'editor_configure' button.
  */
@@ -356,92 +206,6 @@ function editor_filter_xss($html, ?FilterFormatInterface $format = NULL, ?Filter
   return call_user_func($editor_xss_filter_class . '::filterXss', $html, $format, $original_format);
 }
 
-/**
- * Implements hook_entity_insert().
- */
-function editor_entity_insert(EntityInterface $entity) {
-  // Only act on content entities.
-  if (!($entity instanceof FieldableEntityInterface)) {
-    return;
-  }
-  $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
-  foreach ($referenced_files_by_field as $uuids) {
-    _editor_record_file_usage($uuids, $entity);
-  }
-}
-
-/**
- * Implements hook_entity_update().
- */
-function editor_entity_update(EntityInterface $entity) {
-  // Only act on content entities.
-  if (!($entity instanceof FieldableEntityInterface)) {
-    return;
-  }
-
-  // On new revisions, all files are considered to be a new usage and no
-  // deletion of previous file usages are necessary.
-  if (!empty($entity->original) && $entity->getRevisionId() != $entity->original->getRevisionId()) {
-    $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
-    foreach ($referenced_files_by_field as $uuids) {
-      _editor_record_file_usage($uuids, $entity);
-    }
-  }
-  // On modified revisions, detect which file references have been added (and
-  // record their usage) and which ones have been removed (delete their usage).
-  // File references that existed both in the previous version of the revision
-  // and in the new one don't need their usage to be updated.
-  else {
-    $original_uuids_by_field = empty($entity->original) ? [] :
-      _editor_get_file_uuids_by_field($entity->original);
-
-    $uuids_by_field = _editor_get_file_uuids_by_field($entity);
-
-    // Detect file usages that should be incremented.
-    foreach ($uuids_by_field as $field => $uuids) {
-      $original_uuids = $original_uuids_by_field[$field] ?? [];
-
-      if ($added_files = array_diff($uuids_by_field[$field], $original_uuids)) {
-        _editor_record_file_usage($added_files, $entity);
-      }
-    }
-
-    // Detect file usages that should be decremented.
-    foreach ($original_uuids_by_field as $field => $uuids) {
-      $removed_files = array_diff($original_uuids_by_field[$field], $uuids_by_field[$field]);
-      _editor_delete_file_usage($removed_files, $entity, 1);
-    }
-  }
-}
-
-/**
- * Implements hook_entity_delete().
- */
-function editor_entity_delete(EntityInterface $entity) {
-  // Only act on content entities.
-  if (!($entity instanceof FieldableEntityInterface)) {
-    return;
-  }
-  $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
-  foreach ($referenced_files_by_field as $uuids) {
-    _editor_delete_file_usage($uuids, $entity, 0);
-  }
-}
-
-/**
- * Implements hook_entity_revision_delete().
- */
-function editor_entity_revision_delete(EntityInterface $entity) {
-  // Only act on content entities.
-  if (!($entity instanceof FieldableEntityInterface)) {
-    return;
-  }
-  $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
-  foreach ($referenced_files_by_field as $uuids) {
-    _editor_delete_file_usage($uuids, $entity, 1);
-  }
-}
-
 /**
  * Records file usage of files referenced by formatted text fields.
  *
@@ -486,67 +250,6 @@ function _editor_delete_file_usage(array $uuids, EntityInterface $entity, $count
   }
 }
 
-/**
- * Implements hook_file_download().
- *
- * @see file_file_download()
- * @see file_get_file_references()
- */
-function editor_file_download($uri) {
-  // Get the file record based on the URI. If not in the database just return.
-  /** @var \Drupal\file\FileRepositoryInterface $file_repository */
-  $file_repository = \Drupal::service('file.repository');
-  $file = $file_repository->loadByUri($uri);
-  if (!$file) {
-    return;
-  }
-
-  // Temporary files are handled by file_file_download(), so nothing to do here
-  // about them.
-  // @see file_file_download()
-
-  // Find out if any editor-backed field contains the file.
-  $usage_list = \Drupal::service('file.usage')->listUsage($file);
-
-  // Stop processing if there are no references in order to avoid returning
-  // headers for files controlled by other modules. Make an exception for
-  // temporary files where the host entity has not yet been saved (for example,
-  // an image preview on a node creation form) in which case, allow download by
-  // the file's owner.
-  if (empty($usage_list['editor']) && ($file->isPermanent() || $file->getOwnerId() != \Drupal::currentUser()->id())) {
-    return;
-  }
-
-  // Editor.module MUST NOT call $file->access() here (like file_file_download()
-  // does) as checking the 'download' access to a file entity would end up in
-  // FileAccessControlHandler->checkAccess() and ->getFileReferences(), which
-  // calls file_get_file_references(). This latter one would allow downloading
-  // files only handled by the file.module, which is exactly not the case right
-  // here. So instead we must check if the current user is allowed to view any
-  // of the entities that reference the image using the 'editor' module.
-  if ($file->isPermanent()) {
-    $referencing_entity_is_accessible = FALSE;
-    $references = empty($usage_list['editor']) ? [] : $usage_list['editor'];
-    foreach ($references as $entity_type => $entity_ids_usage_count) {
-      $referencing_entities = \Drupal::entityTypeManager()->getStorage($entity_type)->loadMultiple(array_keys($entity_ids_usage_count));
-      /** @var \Drupal\Core\Entity\EntityInterface $referencing_entity */
-      foreach ($referencing_entities as $referencing_entity) {
-        if ($referencing_entity->access('view', NULL, TRUE)->isAllowed()) {
-          $referencing_entity_is_accessible = TRUE;
-          break 2;
-        }
-      }
-    }
-    if (!$referencing_entity_is_accessible) {
-      return -1;
-    }
-  }
-
-  // Access is granted.
-  $headers = file_get_content_headers($file);
-  return $headers;
-}
-
 /**
  * Finds all files referenced (data-entity-uuid) by formatted text fields.
  *
@@ -621,32 +324,3 @@ function _editor_parse_file_uuids($text) {
   }
   return $uuids;
 }
-
-/**
- * Implements hook_ENTITY_TYPE_presave().
- *
- * Synchronizes the editor status to its paired text format status.
- *
- * @todo remove in https://www.drupal.org/project/drupal/issues/3231354.
- */
-function editor_filter_format_presave(FilterFormatInterface $format) {
-  // The text format being created cannot have a text editor yet.
-  if ($format->isNew()) {
-    return;
-  }
-
-  /** @var \Drupal\filter\FilterFormatInterface $original */
-  $original = \Drupal::entityTypeManager()
-    ->getStorage('filter_format')
-    ->loadUnchanged($format->getOriginalId());
-
-  // If the text format status is the same, return early.
-  if (($status = $format->status()) === $original->status()) {
-    return;
-  }
-
-  /** @var \Drupal\editor\EditorInterface $editor */
-  if ($editor = Editor::load($format->id())) {
-    $editor->setStatus($status)->save();
-  }
-}
diff --git a/core/modules/editor/src/Hook/EditorHooks.php b/core/modules/editor/src/Hook/EditorHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..fc53da2e160f61bd6256c372db0c3a384965e361
--- /dev/null
+++ b/core/modules/editor/src/Hook/EditorHooks.php
@@ -0,0 +1,346 @@
+<?php
+
+namespace Drupal\editor\Hook;
+
+use Drupal\editor\Entity\Editor;
+use Drupal\filter\FilterFormatInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\SubformState;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for editor.
+ */
+class EditorHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.editor':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Text Editor module provides a framework that other modules (such as <a href=":ckeditor5">CKEditor5 module</a>) can use to provide toolbars and other functionality that allow users to format text more easily than typing HTML tags directly. For more information, see the <a href=":documentation">online documentation for the Text Editor module</a>.', [
+          ':documentation' => 'https://www.drupal.org/documentation/modules/editor',
+          ':ckeditor5' => \Drupal::moduleHandler()->moduleExists('ckeditor5') ? Url::fromRoute('help.page', [
+            'name' => 'ckeditor5',
+          ])->toString() : '#',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Installing text editors') . '</dt>';
+        $output .= '<dd>' . t('The Text Editor module provides a framework for managing editors. To use it, you also need to install a text editor. This can either be the core <a href=":ckeditor5">CKEditor5 module</a>, which can be installed on the <a href=":extend">Extend page</a>, or a contributed module for any other text editor. When installing a contributed text editor module, be sure to check the installation instructions, because you will most likely need to download an external library as well as the Drupal module.', [
+          ':ckeditor5' => \Drupal::moduleHandler()->moduleExists('ckeditor5') ? Url::fromRoute('help.page', [
+            'name' => 'ckeditor5',
+          ])->toString() : '#',
+          ':extend' => Url::fromRoute('system.modules_list')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Enabling a text editor for a text format') . '</dt>';
+        $output .= '<dd>' . t('On the <a href=":formats">Text formats and editors page</a> you can see which text editor is associated with each text format. You can change this by clicking on the <em>Configure</em> link, and then choosing a text editor or <em>none</em> from the <em>Text editor</em> drop-down list. The text editor will then be displayed with any text field for which this text format is chosen.', [':formats' => Url::fromRoute('filter.admin_overview')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Configuring a text editor') . '</dt>';
+        $output .= '<dd>' . t('Once a text editor is associated with a text format, you can configure it by clicking on the <em>Configure</em> link for this format. Depending on the specific text editor, you can configure it for example by adding buttons to its toolbar. Typically these buttons provide formatting or editing tools, and they often insert HTML tags into the field source. For details, see the help page of the specific text editor.') . '</dd>';
+        $output .= '<dt>' . t('Using different text editors and formats') . '</dt>';
+        $output .= '<dd>' . t('If you change the text format on a text field, the text editor will change as well because the text editor configuration is associated with the individual text format. This allows the use of the same text editor with different options for different text formats. It also allows users to choose between text formats with different text editors if they are installed.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_menu_links_discovered_alter().
+   *
+   * Rewrites the menu entries for filter module that relate to the configuration
+   * of text editors.
+   */
+  #[Hook('menu_links_discovered_alter')]
+  public function menuLinksDiscoveredAlter(array &$links) {
+    $links['filter.admin_overview']['title'] = new TranslatableMarkup('Text formats and editors');
+    $links['filter.admin_overview']['description'] = new TranslatableMarkup('Select and configure text editors, and how content is filtered when displayed.');
+  }
+
+  /**
+   * Implements hook_element_info_alter().
+   *
+   * Extends the functionality of text_format elements (provided by Filter
+   * module), so that selecting a text format notifies a client-side text editor
+   * when it should be enabled or disabled.
+   *
+   * @see \Drupal\filter\Element\TextFormat
+   */
+  #[Hook('element_info_alter')]
+  public function elementInfoAlter(&$types) {
+    $types['text_format']['#pre_render'][] = 'element.editor:preRenderTextFormat';
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_filter_admin_overview_alter')]
+  public function formFilterAdminOverviewAlter(&$form, FormStateInterface $form_state) : void {
+    // @todo Cleanup column injection: https://www.drupal.org/node/1876718.
+    // Splice in the column for "Text editor" into the header.
+    $position = array_search('name', $form['formats']['#header']) + 1;
+    $start = array_splice($form['formats']['#header'], 0, $position, ['editor' => t('Text editor')]);
+    $form['formats']['#header'] = array_merge($start, $form['formats']['#header']);
+    // Then splice in the name of each text editor for each text format.
+    $editors = \Drupal::service('plugin.manager.editor')->getDefinitions();
+    foreach (Element::children($form['formats']) as $format_id) {
+      $editor = editor_load($format_id);
+      $editor_name = $editor && isset($editors[$editor->getEditor()]) ? $editors[$editor->getEditor()]['label'] : '—';
+      $editor_column['editor'] = ['#markup' => $editor_name];
+      $position = array_search('name', array_keys($form['formats'][$format_id])) + 1;
+      $start = array_splice($form['formats'][$format_id], 0, $position, $editor_column);
+      $form['formats'][$format_id] = array_merge($start, $form['formats'][$format_id]);
+    }
+  }
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\filter\FilterFormatEditForm.
+   */
+  #[Hook('form_filter_format_form_alter')]
+  public function formFilterFormatFormAlter(&$form, FormStateInterface $form_state) : void {
+    $editor = $form_state->get('editor');
+    if ($editor === NULL) {
+      $format = $form_state->getFormObject()->getEntity();
+      $format_id = $format->isNew() ? NULL : $format->id();
+      $editor = editor_load($format_id);
+      $form_state->set('editor', $editor);
+    }
+    // Associate a text editor with this text format.
+    $manager = \Drupal::service('plugin.manager.editor');
+    $editor_options = $manager->listOptions();
+    $form['editor'] = ['#weight' => -9];
+    $form['editor']['editor'] = [
+      '#type' => 'select',
+      '#title' => t('Text editor'),
+      '#options' => $editor_options,
+      '#empty_option' => t('None'),
+      '#default_value' => $editor ? $editor->getEditor() : '',
+      '#ajax' => [
+        'trigger_as' => [
+          'name' => 'editor_configure',
+        ],
+        'callback' => 'editor_form_filter_admin_form_ajax',
+        'wrapper' => 'editor-settings-wrapper',
+      ],
+      '#weight' => -10,
+    ];
+    $form['editor']['configure'] = [
+      '#type' => 'submit',
+      '#name' => 'editor_configure',
+      '#value' => t('Configure'),
+      '#limit_validation_errors' => [
+              [
+                'editor',
+              ],
+      ],
+      '#submit' => [
+        'editor_form_filter_admin_format_editor_configure',
+      ],
+      '#ajax' => [
+        'callback' => 'editor_form_filter_admin_form_ajax',
+        'wrapper' => 'editor-settings-wrapper',
+      ],
+      '#weight' => -10,
+      '#attributes' => [
+        'class' => [
+          'js-hide',
+        ],
+      ],
+    ];
+    // If there aren't any options (other than "None"), disable the select list.
+    if (empty($editor_options)) {
+      $form['editor']['editor']['#disabled'] = TRUE;
+      $form['editor']['editor']['#description'] = t('This option is disabled because no modules that provide a text editor are currently enabled.');
+    }
+    $form['editor']['settings'] = [
+      '#tree' => TRUE,
+      '#weight' => -8,
+      '#type' => 'container',
+      '#id' => 'editor-settings-wrapper',
+    ];
+    // Add editor-specific validation and submit handlers.
+    if ($editor) {
+      /** @var \Drupal\editor\Plugin\EditorPluginInterface $plugin */
+      $plugin = $manager->createInstance($editor->getEditor());
+      $form_state->set('editor_plugin', $plugin);
+      $form['editor']['settings']['subform'] = [];
+      $subform_state = SubformState::createForSubform($form['editor']['settings']['subform'], $form, $form_state);
+      $form['editor']['settings']['subform'] = $plugin->buildConfigurationForm($form['editor']['settings']['subform'], $subform_state);
+      $form['editor']['settings']['subform']['#parents'] = ['editor', 'settings'];
+    }
+    $form['#validate'][] = 'editor_form_filter_admin_format_validate';
+    $form['actions']['submit']['#submit'][] = 'editor_form_filter_admin_format_submit';
+  }
+
+  /**
+   * Implements hook_entity_insert().
+   */
+  #[Hook('entity_insert')]
+  public function entityInsert(EntityInterface $entity) {
+    // Only act on content entities.
+    if (!$entity instanceof FieldableEntityInterface) {
+      return;
+    }
+    $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
+    foreach ($referenced_files_by_field as $uuids) {
+      _editor_record_file_usage($uuids, $entity);
+    }
+  }
+
+  /**
+   * Implements hook_entity_update().
+   */
+  #[Hook('entity_update')]
+  public function entityUpdate(EntityInterface $entity) {
+    // Only act on content entities.
+    if (!$entity instanceof FieldableEntityInterface) {
+      return;
+    }
+    // On new revisions, all files are considered to be a new usage and no
+    // deletion of previous file usages are necessary.
+    if (!empty($entity->original) && $entity->getRevisionId() != $entity->original->getRevisionId()) {
+      $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
+      foreach ($referenced_files_by_field as $uuids) {
+        _editor_record_file_usage($uuids, $entity);
+      }
+    }
+    else {
+      $original_uuids_by_field = empty($entity->original) ? [] : _editor_get_file_uuids_by_field($entity->original);
+      $uuids_by_field = _editor_get_file_uuids_by_field($entity);
+      // Detect file usages that should be incremented.
+      foreach ($uuids_by_field as $field => $uuids) {
+        $original_uuids = $original_uuids_by_field[$field] ?? [];
+        if ($added_files = array_diff($uuids_by_field[$field], $original_uuids)) {
+          _editor_record_file_usage($added_files, $entity);
+        }
+      }
+      // Detect file usages that should be decremented.
+      foreach ($original_uuids_by_field as $field => $uuids) {
+        $removed_files = array_diff($original_uuids_by_field[$field], $uuids_by_field[$field]);
+        _editor_delete_file_usage($removed_files, $entity, 1);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_delete().
+   */
+  #[Hook('entity_delete')]
+  public function entityDelete(EntityInterface $entity) {
+    // Only act on content entities.
+    if (!$entity instanceof FieldableEntityInterface) {
+      return;
+    }
+    $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
+    foreach ($referenced_files_by_field as $uuids) {
+      _editor_delete_file_usage($uuids, $entity, 0);
+    }
+  }
+
+  /**
+   * Implements hook_entity_revision_delete().
+   */
+  #[Hook('entity_revision_delete')]
+  public function entityRevisionDelete(EntityInterface $entity) {
+    // Only act on content entities.
+    if (!$entity instanceof FieldableEntityInterface) {
+      return;
+    }
+    $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
+    foreach ($referenced_files_by_field as $uuids) {
+      _editor_delete_file_usage($uuids, $entity, 1);
+    }
+  }
+
+  /**
+   * Implements hook_file_download().
+   *
+   * @see file_file_download()
+   * @see file_get_file_references()
+   */
+  #[Hook('file_download')]
+  public function fileDownload($uri) {
+    // Get the file record based on the URI. If not in the database just return.
+    /** @var \Drupal\file\FileRepositoryInterface $file_repository */
+    $file_repository = \Drupal::service('file.repository');
+    $file = $file_repository->loadByUri($uri);
+    if (!$file) {
+      return;
+    }
+    // Temporary files are handled by file_file_download(), so nothing to do here
+    // about them.
+    // @see file_file_download()
+    // Find out if any editor-backed field contains the file.
+    $usage_list = \Drupal::service('file.usage')->listUsage($file);
+    // Stop processing if there are no references in order to avoid returning
+    // headers for files controlled by other modules. Make an exception for
+    // temporary files where the host entity has not yet been saved (for example,
+    // an image preview on a node creation form) in which case, allow download by
+    // the file's owner.
+    if (empty($usage_list['editor']) && ($file->isPermanent() || $file->getOwnerId() != \Drupal::currentUser()->id())) {
+      return;
+    }
+    // Editor.module MUST NOT call $file->access() here (like file_file_download()
+    // does) as checking the 'download' access to a file entity would end up in
+    // FileAccessControlHandler->checkAccess() and ->getFileReferences(), which
+    // calls file_get_file_references(). This latter one would allow downloading
+    // files only handled by the file.module, which is exactly not the case right
+    // here. So instead we must check if the current user is allowed to view any
+    // of the entities that reference the image using the 'editor' module.
+    if ($file->isPermanent()) {
+      $referencing_entity_is_accessible = FALSE;
+      $references = empty($usage_list['editor']) ? [] : $usage_list['editor'];
+      foreach ($references as $entity_type => $entity_ids_usage_count) {
+        $referencing_entities = \Drupal::entityTypeManager()->getStorage($entity_type)->loadMultiple(array_keys($entity_ids_usage_count));
+        /** @var \Drupal\Core\Entity\EntityInterface $referencing_entity */
+        foreach ($referencing_entities as $referencing_entity) {
+          if ($referencing_entity->access('view', NULL, TRUE)->isAllowed()) {
+            $referencing_entity_is_accessible = TRUE;
+            break 2;
+          }
+        }
+      }
+      if (!$referencing_entity_is_accessible) {
+        return -1;
+      }
+    }
+    // Access is granted.
+    $headers = file_get_content_headers($file);
+    return $headers;
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave().
+   *
+   * Synchronizes the editor status to its paired text format status.
+   *
+   * @todo remove in https://www.drupal.org/project/drupal/issues/3231354.
+   */
+  #[Hook('filter_format_presave')]
+  public function filterFormatPresave(FilterFormatInterface $format) {
+    // The text format being created cannot have a text editor yet.
+    if ($format->isNew()) {
+      return;
+    }
+    /** @var \Drupal\filter\FilterFormatInterface $original */
+    $original = \Drupal::entityTypeManager()->getStorage('filter_format')->loadUnchanged($format->getOriginalId());
+    // If the text format status is the same, return early.
+    if (($status = $format->status()) === $original->status()) {
+      return;
+    }
+    /** @var \Drupal\editor\EditorInterface $editor */
+    if ($editor = Editor::load($format->id())) {
+      $editor->setStatus($status)->save();
+    }
+  }
+
+}
diff --git a/core/modules/editor/tests/modules/editor_test/editor_test.module b/core/modules/editor/tests/modules/editor_test/editor_test.module
deleted file mode 100644
index fb7dd839c71e27eff0ec7f149706b1bc85371559..0000000000000000000000000000000000000000
--- a/core/modules/editor/tests/modules/editor_test/editor_test.module
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the Text Editor tests.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\node\NodeInterface;
-use Drupal\filter\FilterFormatInterface;
-use Drupal\file\FileInterface;
-
-/**
- * Implements hook_entity_update().
- *
- * @see \Drupal\Tests\editor\Kernel\EntityUpdateTest
- */
-function editor_test_entity_update(EntityInterface $entity) {
-  // Only act on nodes.
-  if (!$entity instanceof NodeInterface) {
-    return;
-  }
-
-  // Avoid infinite loop by only going through our post save logic once.
-  if (!empty($entity->editor_test_updating)) {
-    return;
-  }
-
-  // Set flag for whether or not the entity needs to be resaved.
-  $needs_update = FALSE;
-
-  // Perform our post save logic.
-  if ($entity->title->value == 'test updated') {
-    // Change the node title.
-    $entity->title->value = 'test updated 2';
-    $needs_update = TRUE;
-  }
-
-  if ($needs_update) {
-    // Set flag on entity that our logic was already executed.
-    $entity->editor_test_updating = TRUE;
-    // And resave entity.
-    $entity->save();
-  }
-}
-
-/**
- * Implements hook_editor_js_settings_alter().
- */
-function editor_test_editor_js_settings_alter(&$settings) {
-  // Allow tests to enable or disable this alter hook.
-  if (!\Drupal::state()->get('editor_test_js_settings_alter_enabled', FALSE)) {
-    return;
-  }
-
-  if (isset($settings['editor']['formats']['full_html'])) {
-    $settings['editor']['formats']['full_html']['editorSettings']['ponyModeEnabled'] = FALSE;
-  }
-}
-
-/**
- * Implements hook_editor_xss_filter_alter().
- */
-function editor_test_editor_xss_filter_alter(&$editor_xss_filter_class, FilterFormatInterface $format, ?FilterFormatInterface $original_format = NULL) {
-  // Allow tests to enable or disable this alter hook.
-  if (!\Drupal::state()->get('editor_test_editor_xss_filter_alter_enabled', FALSE)) {
-    return;
-  }
-
-  $filters = $format->filters()->getAll();
-  if (isset($filters['filter_html']) && $filters['filter_html']->status) {
-    $editor_xss_filter_class = '\Drupal\editor_test\EditorXssFilter\Insecure';
-  }
-}
-
-/**
- * Implements hook_editor_info_alter().
- */
-function editor_test_editor_info_alter(&$items) {
-  if (!\Drupal::state()->get('editor_test_give_me_a_trex_thanks', FALSE)) {
-    unset($items['trex']);
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for file entities.
- */
-function editor_test_file_presave(FileInterface $file) {
-  // Use state to keep track of how many times a file is saved.
-  $file_save_count = \Drupal::state()->get('editor_test.file_save_count', []);
-  $file_save_count[$file->getFilename()] = isset($file_save_count[$file->getFilename()]) ? $file_save_count[$file->getFilename()] + 1 : 1;
-  \Drupal::state()->set('editor_test.file_save_count', $file_save_count);
-}
diff --git a/core/modules/editor/tests/modules/editor_test/src/Hook/EditorTestHooks.php b/core/modules/editor/tests/modules/editor_test/src/Hook/EditorTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..03bfed565848216af43bc17056ee6c39ccfb2f47
--- /dev/null
+++ b/core/modules/editor/tests/modules/editor_test/src/Hook/EditorTestHooks.php
@@ -0,0 +1,99 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\editor_test\Hook;
+
+use Drupal\file\FileInterface;
+use Drupal\filter\FilterFormatInterface;
+use Drupal\node\NodeInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for editor_test.
+ */
+class EditorTestHooks {
+
+  /**
+   * Implements hook_entity_update().
+   *
+   * @see \Drupal\Tests\editor\Kernel\EntityUpdateTest
+   */
+  #[Hook('entity_update')]
+  public function entityUpdate(EntityInterface $entity) {
+    // Only act on nodes.
+    if (!$entity instanceof NodeInterface) {
+      return;
+    }
+    // Avoid infinite loop by only going through our post save logic once.
+    if (!empty($entity->editor_test_updating)) {
+      return;
+    }
+    // Set flag for whether or not the entity needs to be resaved.
+    $needs_update = FALSE;
+    // Perform our post save logic.
+    if ($entity->title->value == 'test updated') {
+      // Change the node title.
+      $entity->title->value = 'test updated 2';
+      $needs_update = TRUE;
+    }
+    if ($needs_update) {
+      // Set flag on entity that our logic was already executed.
+      $entity->editor_test_updating = TRUE;
+      // And resave entity.
+      $entity->save();
+    }
+  }
+
+  /**
+   * Implements hook_editor_js_settings_alter().
+   */
+  #[Hook('editor_js_settings_alter')]
+  public function editorJsSettingsAlter(&$settings) {
+    // Allow tests to enable or disable this alter hook.
+    if (!\Drupal::state()->get('editor_test_js_settings_alter_enabled', FALSE)) {
+      return;
+    }
+    if (isset($settings['editor']['formats']['full_html'])) {
+      $settings['editor']['formats']['full_html']['editorSettings']['ponyModeEnabled'] = FALSE;
+    }
+  }
+
+  /**
+   * Implements hook_editor_xss_filter_alter().
+   */
+  #[Hook('editor_xss_filter_alter')]
+  public function editorXssFilterAlter(&$editor_xss_filter_class, FilterFormatInterface $format, ?FilterFormatInterface $original_format = NULL) {
+    // Allow tests to enable or disable this alter hook.
+    if (!\Drupal::state()->get('editor_test_editor_xss_filter_alter_enabled', FALSE)) {
+      return;
+    }
+    $filters = $format->filters()->getAll();
+    if (isset($filters['filter_html']) && $filters['filter_html']->status) {
+      $editor_xss_filter_class = '\Drupal\editor_test\EditorXssFilter\Insecure';
+    }
+  }
+
+  /**
+   * Implements hook_editor_info_alter().
+   */
+  #[Hook('editor_info_alter')]
+  public function editorInfoAlter(&$items) {
+    if (!\Drupal::state()->get('editor_test_give_me_a_trex_thanks', FALSE)) {
+      unset($items['trex']);
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for file entities.
+   */
+  #[Hook('file_presave')]
+  public function filePresave(FileInterface $file) {
+    // Use state to keep track of how many times a file is saved.
+    $file_save_count = \Drupal::state()->get('editor_test.file_save_count', []);
+    $file_save_count[$file->getFilename()] = isset($file_save_count[$file->getFilename()]) ? $file_save_count[$file->getFilename()] + 1 : 1;
+    \Drupal::state()->set('editor_test.file_save_count', $file_save_count);
+  }
+
+}
diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php
index b33aa3c282b65211f8ab492c94e6bea094ad61be..afa0d5a49bf3481921edbfba5a669336cd3097db 100644
--- a/core/modules/field/field.api.php
+++ b/core/modules/field/field.api.php
@@ -1,5 +1,15 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\field\Entity\FieldConfig;
+
 /**
  * @file
  * Field API documentation.
@@ -124,14 +134,14 @@ function hook_field_ui_preconfigured_options_alter(array &$options, $field_type)
  *
  * @see entity_crud
  */
-function hook_field_storage_config_update_forbid(\Drupal\field\FieldStorageConfigInterface $field_storage, \Drupal\field\FieldStorageConfigInterface $prior_field_storage) {
+function hook_field_storage_config_update_forbid(FieldStorageConfigInterface $field_storage, FieldStorageConfigInterface $prior_field_storage) {
   if ($field_storage->getTypeProvider() == 'options' && $field_storage->hasData()) {
     // Forbid any update that removes allowed values with actual data.
     $allowed_values = $field_storage->getSetting('allowed_values');
     $prior_allowed_values = $prior_field_storage->getSetting('allowed_values');
     $lost_keys = array_keys(array_diff_key($prior_allowed_values, $allowed_values));
     if (_options_values_in_use($field_storage->getTargetEntityTypeId(), $field_storage->getName(), $lost_keys)) {
-      throw new \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException("A list field '{$field_storage->getName()}' with existing data cannot have its keys changed.");
+      throw new FieldStorageDefinitionUpdateForbiddenException("A list field '{$field_storage->getName()}' with existing data cannot have its keys changed.");
     }
   }
 }
@@ -210,7 +220,7 @@ function hook_field_widget_info_alter(array &$info) {
  * @see hook_field_widget_complete_form_alter()
  * @see https://www.drupal.org/node/3180429
  */
-function hook_field_widget_single_element_form_alter(array &$element, \Drupal\Core\Form\FormStateInterface $form_state, array $context) {
+function hook_field_widget_single_element_form_alter(array &$element, FormStateInterface $form_state, array $context) {
   // Add a css class to widget form elements for all fields of type my_type.
   $field_definition = $context['items']->getFieldDefinition();
   if ($field_definition->getType() == 'my_type') {
@@ -247,7 +257,7 @@ function hook_field_widget_single_element_form_alter(array &$element, \Drupal\Co
  * @see hook_field_widget_single_element_form_alter()
  * @see hook_field_widget_complete_WIDGET_TYPE_form_alter()
  */
-function hook_field_widget_single_element_WIDGET_TYPE_form_alter(array &$element, \Drupal\Core\Form\FormStateInterface $form_state, array $context) {
+function hook_field_widget_single_element_WIDGET_TYPE_form_alter(array &$element, FormStateInterface $form_state, array $context) {
   // Code here will only act on widgets of type WIDGET_TYPE.  For example,
   // hook_field_widget_single_element_my_module_autocomplete_form_alter() will
   // only act on widgets of type 'my_module_autocomplete'.
@@ -278,7 +288,7 @@ function hook_field_widget_single_element_WIDGET_TYPE_form_alter(array &$element
  * @see hook_field_widget_complete_WIDGET_TYPE_form_alter()
  * @see https://www.drupal.org/node/3180429
  */
-function hook_field_widget_complete_form_alter(&$field_widget_complete_form, \Drupal\Core\Form\FormStateInterface $form_state, $context) {
+function hook_field_widget_complete_form_alter(&$field_widget_complete_form, FormStateInterface $form_state, $context) {
   $field_widget_complete_form['#attributes']['class'][] = 'my-class';
 }
 
@@ -310,7 +320,7 @@ function hook_field_widget_complete_form_alter(&$field_widget_complete_form, \Dr
  * @see hook_field_widget_complete_form_alter()
  * @see https://www.drupal.org/node/3180429
  */
-function hook_field_widget_complete_WIDGET_TYPE_form_alter(&$field_widget_complete_form, \Drupal\Core\Form\FormStateInterface $form_state, $context) {
+function hook_field_widget_complete_WIDGET_TYPE_form_alter(&$field_widget_complete_form, FormStateInterface $form_state, $context) {
   $field_widget_complete_form['#attributes']['class'][] = 'my-class';
 }
 
@@ -407,7 +417,7 @@ function hook_field_info_max_weight($entity_type, $bundle, $context, $context_mo
  * @param $field_storage \Drupal\field\Entity\FieldStorageConfig
  *   The field storage being purged.
  */
-function hook_field_purge_field_storage(\Drupal\field\Entity\FieldStorageConfig $field_storage) {
+function hook_field_purge_field_storage(FieldStorageConfig $field_storage) {
   \Drupal::database()->delete('my_module_field_storage_info')
     ->condition('uuid', $field_storage->uuid())
     ->execute();
@@ -424,7 +434,7 @@ function hook_field_purge_field_storage(\Drupal\field\Entity\FieldStorageConfig
  * @param $field
  *   The field being purged.
  */
-function hook_field_purge_field(\Drupal\field\Entity\FieldConfig $field) {
+function hook_field_purge_field(FieldConfig $field) {
   \Drupal::database()->delete('my_module_field_info')
     ->condition('id', $field->id())
     ->execute();
diff --git a/core/modules/field/field.module b/core/modules/field/field.module
index b671dd6d991c6d15b81c7bf1afb439e716d2a4c0..ec84559d6b9515e41b0a807945bc5c27ecb4e4f8 100644
--- a/core/modules/field/field.module
+++ b/core/modules/field/field.module
@@ -2,22 +2,11 @@
 
 /**
  * @file
- * Attach custom data fields to Drupal entities.
  */
 
-use Drupal\Core\Config\ConfigImporter;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\DynamicallyFieldableEntityStorageInterface;
-use Drupal\field\ConfigImporterFieldPurger;
-use Drupal\field\Entity\FieldConfig;
-use Drupal\field\Entity\FieldStorageConfig;
-use Drupal\field\EntityDisplayRebuilder;
 use Drupal\field\FieldConfigInterface;
 use Drupal\field\FieldStorageConfigInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
 
 /*
  * Load all public Field API functions. Drupal currently has no
@@ -26,238 +15,6 @@
  */
 require_once __DIR__ . '/field.purge.inc';
 
-/**
- * @defgroup field Field API
- * @{
- * Attaches custom data fields to Drupal entities.
- *
- * The Field API allows custom data fields to be attached to Drupal entities and
- * takes care of storing, loading, editing, and rendering field data. Any entity
- * type (node, user, etc.) can use the Field API to make itself "fieldable" and
- * thus allow fields to be attached to it. Other modules can provide a user
- * interface for managing custom fields via a web browser as well as a wide and
- * flexible variety of data type, form element, and display format capabilities.
- *
- * The Field API defines two primary data structures, FieldStorage and Field,
- * and the concept of a Bundle. A FieldStorage defines a particular type of data
- * that can be attached to entities. A Field is attached to a single
- * Bundle. A Bundle is a set of fields that are treated as a group by the Field
- * Attach API and is related to a single fieldable entity type.
- *
- * For example, suppose a site administrator wants Article nodes to have a
- * subtitle and photo. Using the Field API or Field UI module, the administrator
- * creates a field named 'subtitle' of type 'text' and a field named 'photo' of
- * type 'image'. The administrator (again, via a UI) creates two Field
- * Instances, one attaching the field 'subtitle' to the 'node' bundle 'article'
- * and one attaching the field 'photo' to the 'node' bundle 'article'. When the
- * node storage loads an Article node, it loads the values of the
- * 'subtitle' and 'photo' fields because they are both attached to the 'node'
- * bundle 'article'.
- *
- * - @link field_types Field Types API @endlink: Defines field types, widget
- *   types, and display formatters. Field modules use this API to provide field
- *   types like Text and Node Reference along with the associated form elements
- *   and display formatters.
- *
- * - @link field_purge Field API bulk data deletion @endlink: Cleans up after
- *   bulk deletion operations such as deletion of field storage or field.
- */
-
-/**
- * Implements hook_help().
- */
-function field_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.field':
-      $field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Field module allows custom data fields to be defined for <em>entity</em> types (see below). The Field module takes care of storing, loading, editing, and rendering field data. Most users will not interact with the Field module directly, but will instead use the <a href=":field-ui-help">Field UI module</a> user interface. Module developers can use the Field API to make new entity types "fieldable" and thus allow fields to be attached to them. For more information, see the <a href=":field">online documentation for the Field module</a>.', [':field-ui-help' => $field_ui_url, ':field' => 'https://www.drupal.org/documentation/modules/field']) . '</p>';
-      $output .= '<h2>' . t('Terminology') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Entities and entity types') . '</dt>';
-      $output .= '<dd>' . t("The website's content and configuration is managed using <em>entities</em>, which are grouped into <em>entity types</em>. <em>Content entity types</em> are the entity types for site content (such as the main site content, comments, content blocks, taxonomy terms, and user accounts). <em>Configuration entity types</em> are used to store configuration information for your site, such as individual views in the Views module, and settings for your main site content types.") . '</dd>';
-      $output .= '<dt>' . t('Entity sub-types') . '</dt>';
-      $output .= '<dd>' . t('Some content entity types are further grouped into sub-types (for example, you could have article and page content types within the main site content entity type, and tag and category vocabularies within the taxonomy term entity type); other entity types, such as user accounts, do not have sub-types. Programmers use the term <em>bundle</em> for entity sub-types.') . '</dd>';
-      $output .= '<dt>' . t('Fields and field types') . '</dt>';
-      $output .= '<dd>' . t('Content entity types and sub-types store most of their text, file, and other information in <em>fields</em>. Fields are grouped by <em>field type</em>; field types define what type of data can be stored in that field, such as text, images, or taxonomy term references.') . '</dd>';
-      $output .= '<dt>' . t('Formatters and view modes') . '</dd>';
-      $output .= '<dd>' . t('Content entity types and sub-types can have one or more <em>view modes</em>, used for displaying the entity items. For instance, a content item could be viewed in full content mode on its own page, teaser mode in a list, or RSS mode in a feed. In each view mode, each field can be hidden or displayed, and if it is displayed, you can choose and configure the <em>formatter</em> that is used to display the field. For instance, a long text field can be displayed trimmed or full-length, and taxonomy term reference fields can be displayed in plain text or linked to the taxonomy term page.') . '</dd>';
-      $output .= '<dt>' . t('Widgets and form modes') . '</dd>';
-      $output .= '<dd>' . t('Content entity types and sub-types can have one or more <em>form modes</em>, used for editing. For instance, a content item could be edited in a compact format with only some fields editable, or a full format that allows all fields to be edited. In each form mode, each field can be hidden or displayed, and if it is displayed, you can choose and configure the <em>widget</em> that is used to edit the field. For instance, a taxonomy term reference field can be edited using a select list, radio buttons, or an autocomplete widget.') . '</dd>';
-      $output .= '</dl>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Enabling field types, widgets, and formatters') . '</dt>';
-      $output .= '<dd>' . t('The Field module provides the infrastructure for fields; the field types, formatters, and widgets are provided by Drupal core or additional modules. Some of the modules are required; the optional modules can be installed from the <a href=":modules">Extend administration page</a>. Additional fields, formatters, and widgets may be provided by contributed modules, which you can find in the <a href=":contrib">contributed module section of Drupal.org</a>.', [':modules' => Url::fromRoute('system.modules_list')->toString(), ':contrib' => 'https://www.drupal.org/project/modules']) . '</dd>';
-
-      $output .= '<h2>' . t('Field, widget, and formatter information') . '</h2>';
-
-      // Make a list of all widget, formatter, and field modules currently
-      // enabled, ordered by displayed module name (module names are not
-      // translated).
-      $items = [];
-      $modules = \Drupal::moduleHandler()->getModuleList();
-      $widgets = \Drupal::service('plugin.manager.field.widget')->getDefinitions();
-      $field_types = \Drupal::service('plugin.manager.field.field_type')->getUiDefinitions();
-      $formatters = \Drupal::service('plugin.manager.field.formatter')->getDefinitions();
-      $providers = [];
-      foreach (array_merge($field_types, $widgets, $formatters) as $plugin) {
-        $providers[] = $plugin['provider'];
-      }
-      $providers = array_unique($providers);
-      sort($providers);
-      $module_extension_list = \Drupal::service('extension.list.module');
-      foreach ($providers as $provider) {
-        // Skip plugins provided by core components as they do not implement
-        // hook_help().
-        if (isset($modules[$provider])) {
-          $display = $module_extension_list->getName($provider);
-          if (\Drupal::moduleHandler()->hasImplementations('help', $provider)) {
-            $items[] = Link::fromTextAndUrl($display, Url::fromRoute('help.page', ['name' => $provider]))->toRenderable();
-          }
-          else {
-            $items[] = $display;
-          }
-        }
-      }
-      if ($items) {
-        $output .= '<dt>' . t('Provided by modules') . '</dt>';
-        $output .= '<dd>' . t('Here is a list of the currently installed field, formatter, and widget modules:');
-        $item_list = [
-          '#theme' => 'item_list',
-          '#items' => $items,
-        ];
-        $output .= \Drupal::service('renderer')->renderInIsolation($item_list);
-        $output .= '</dd>';
-      }
-
-      $output .= '<dt>' . t('Provided by Drupal core') . '</dt>';
-      $output .= '<dd>' . t('As mentioned previously, some field types, widgets, and formatters are provided by Drupal core. Here are some notes on how to use some of these:');
-      $output .= '<ul>';
-      $output .= '<li><p>' . t('<strong>Entity Reference</strong> fields allow you to create fields that contain links to other entities (such as content items, taxonomy terms, etc.) within the site. This allows you, for example, to include a link to a user within a content item. For more information, see <a href=":er_do">the online documentation for the Entity Reference module</a>.', [':er_do' => 'https://drupal.org/documentation/modules/entityreference']) . '</p>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Managing and displaying entity reference fields') . '</dt>';
-      $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the entity reference field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => $field_ui_url]) . '</dd>';
-      $output .= '<dt>' . t('Selecting reference type') . '</dt>';
-      $output .= '<dd>' . t('In the field settings you can select which entity type you want to create a reference to.') . '</dd>';
-      $output .= '<dt>' . t('Filtering and sorting reference fields') . '</dt>';
-      $output .= '<dd>' . t('Depending on the chosen entity type, additional filtering and sorting options are available for the list of entities that can be referred to, in the field settings. For example, the list of users can be filtered by role and sorted by name or ID.') . '</dd>';
-      $output .= '<dt>' . t('Displaying a reference') . '</dt>';
-      $output .= '<dd>' . t('An entity reference can be displayed as a simple label with or without a link to the entity. Alternatively, the referenced entity can be displayed as a teaser (or any other available view mode) inside the referencing entity.') . '</dd>';
-      $output .= '<dt>' . t('Configuring form displays') . '</dt>';
-      $output .= '<dd>' . t('Reference fields have several widgets available on the <em>Manage form display</em> page:');
-      $output .= '<ul>';
-      $output .= '<li>' . t('The <em>Check boxes/radio buttons</em> widget displays the existing entities for the entity type as check boxes or radio buttons based on the <em>Allowed number of values</em> set for the field.') . '</li>';
-      $output .= '<li>' . t('The <em>Select list</em> widget displays the existing entities in a drop-down list or scrolling list box based on the <em>Allowed number of values</em> setting for the field.') . '</li>';
-      $output .= '<li>' . t('The <em>Autocomplete</em> widget displays text fields in which users can type entity labels based on the <em>Allowed number of values</em>. The widget can be configured to display all entities that contain the typed characters or restricted to those starting with those characters.') . '</li>';
-      $output .= '<li>' . t('The <em>Autocomplete (Tags style)</em> widget displays a multi-text field in which users can type in a comma-separated list of entity labels.') . '</li>';
-      $output .= '</ul></dd>';
-      $output .= '</dl></li>';
-      $output .= '<li>' . t('<strong>Number fields</strong>: When you add a number field you can choose from three types: <em>decimal</em>, <em>float</em>, and <em>integer</em>. The <em>decimal</em> number field type allows users to enter exact decimal values, with fixed numbers of decimal places. The <em>float</em> number field type allows users to enter approximate decimal values. The <em>integer</em> number field type allows users to enter whole numbers, such as years (for example, 2012) or values (for example, 1, 2, 5, 305). It does not allow decimals.') . '</li>';
-      $output .= '</ul></dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_cron().
- */
-function field_cron() {
-  // Do a pass of purging on deleted Field API data, if any exists.
-  $limit = \Drupal::config('field.settings')->get('purge_batch_size');
-  field_purge_batch($limit);
-}
-
-/**
- * Implements hook_entity_field_storage_info().
- */
-function field_entity_field_storage_info(EntityTypeInterface $entity_type) {
-  if (\Drupal::entityTypeManager()->getStorage($entity_type->id()) instanceof DynamicallyFieldableEntityStorageInterface) {
-    // Query by filtering on the ID as this is more efficient than filtering
-    // on the entity_type property directly.
-    $ids = \Drupal::entityQuery('field_storage_config')
-      ->condition('id', $entity_type->id() . '.', 'STARTS_WITH')
-      ->execute();
-    // Fetch all fields and key them by field name.
-    $field_storages = FieldStorageConfig::loadMultiple($ids);
-    $result = [];
-    foreach ($field_storages as $field_storage) {
-      $result[$field_storage->getName()] = $field_storage;
-    }
-
-    return $result;
-  }
-}
-
-/**
- * Implements hook_entity_bundle_field_info().
- */
-function field_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
-  if (\Drupal::entityTypeManager()->getStorage($entity_type->id()) instanceof DynamicallyFieldableEntityStorageInterface) {
-    // Query by filtering on the ID as this is more efficient than filtering
-    // on the entity_type property directly.
-    $ids = \Drupal::entityQuery('field_config')
-      ->condition('id', $entity_type->id() . '.' . $bundle . '.', 'STARTS_WITH')
-      ->execute();
-    // Fetch all fields and key them by field name.
-    $field_configs = FieldConfig::loadMultiple($ids);
-    $result = [];
-    foreach ($field_configs as $field_instance) {
-      $result[$field_instance->getName()] = $field_instance;
-    }
-
-    return $result;
-  }
-}
-
-/**
- * Implements hook_entity_bundle_delete().
- */
-function field_entity_bundle_delete($entity_type_id, $bundle) {
-  $storage = \Drupal::entityTypeManager()->getStorage('field_config');
-  // Get the fields on the bundle.
-  $fields = $storage->loadByProperties(['entity_type' => $entity_type_id, 'bundle' => $bundle]);
-  // This deletes the data for the field as well as the field themselves. This
-  // function actually just marks the data and fields as deleted, leaving the
-  // garbage collection for a separate process, because it is not always
-  // possible to delete this much data in a single page request (particularly
-  // since for some field types, the deletion is more than just a simple DELETE
-  // query).
-  foreach ($fields as $field) {
-    $field->delete();
-  }
-
-  // We are duplicating the work done by
-  // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::onDependencyRemoval()
-  // because we need to take into account bundles that are not provided by a
-  // config entity type so they are not part of the config dependencies.
-
-  // Gather a list of all entity reference fields.
-  $map = \Drupal::service('entity_field.manager')->getFieldMapByFieldType('entity_reference');
-  $ids = [];
-  foreach ($map as $type => $info) {
-    foreach ($info as $name => $data) {
-      foreach ($data['bundles'] as $bundle_name) {
-        $ids[] = "$type.$bundle_name.$name";
-      }
-    }
-  }
-
-  // Update the 'target_bundles' handler setting if needed.
-  foreach (FieldConfig::loadMultiple($ids) as $field_config) {
-    if ($field_config->getSetting('target_type') == $entity_type_id) {
-      $handler_settings = $field_config->getSetting('handler_settings');
-      if (isset($handler_settings['target_bundles'][$bundle])) {
-        unset($handler_settings['target_bundles'][$bundle]);
-        $field_config->setSetting('handler_settings', $handler_settings);
-        $field_config->save();
-      }
-    }
-  }
-}
-
 /**
  * @} End of "defgroup field".
  */
@@ -289,170 +46,6 @@ function _field_create_entity_from_ids($ids) {
     ->create($id_properties);
 }
 
-/**
- * Implements hook_config_import_steps_alter().
- */
-function field_config_import_steps_alter(&$sync_steps, ConfigImporter $config_importer) {
-  $field_storages = ConfigImporterFieldPurger::getFieldStoragesToPurge(
-    $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension'),
-    $config_importer->getStorageComparer()->getChangelist('delete')
-  );
-  if ($field_storages) {
-    // Add a step to the beginning of the configuration synchronization process
-    // to purge field data where the module that provides the field is being
-    // uninstalled.
-    array_unshift($sync_steps, ['\Drupal\field\ConfigImporterFieldPurger', 'process']);
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- *
- * Adds a warning if field data will be permanently removed by the configuration
- * synchronization.
- *
- * @see \Drupal\field\ConfigImporterFieldPurger
- */
-function field_form_config_admin_import_form_alter(&$form, FormStateInterface $form_state): void {
-  // Only display the message when core.extension is available in the source
-  // storage and the form is not submitted.
-  $user_input = $form_state->getUserInput();
-  $storage_comparer = $form_state->get('storage_comparer');
-  if ($storage_comparer?->getSourceStorage()->exists('core.extension') && empty($user_input)) {
-    $field_storages = ConfigImporterFieldPurger::getFieldStoragesToPurge(
-      $storage_comparer->getSourceStorage()->read('core.extension'),
-      $storage_comparer->getChangelist('delete')
-    );
-    if ($field_storages) {
-      foreach ($field_storages as $field) {
-        $field_labels[] = $field->label();
-      }
-      \Drupal::messenger()->addWarning(\Drupal::translation()->formatPlural(
-        count($field_storages),
-        'This synchronization will delete data from the field %fields.',
-        'This synchronization will delete data from the fields: %fields.',
-        ['%fields' => implode(', ', $field_labels)]
-      ));
-    }
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for 'field_config'.
- */
-function field_field_config_insert(FieldConfigInterface $field) {
-  if ($field->isSyncing()) {
-    // Don't change anything during a configuration sync.
-    return;
-  }
-
-  // Allow other view modes to update their configuration for the new field.
-  // Otherwise, configuration for view modes won't get updated until the mode
-  // is used for the first time, creating noise in config diffs.
-  \Drupal::classResolver(EntityDisplayRebuilder::class)
-    ->rebuildEntityTypeDisplays($field->getTargetEntityTypeId(), $field->getTargetBundle());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for 'field_storage_config'.
- *
- * Reset the field handler settings, when the storage target_type is changed on
- * an entity reference field.
- */
-function field_field_storage_config_update(FieldStorageConfigInterface $field_storage) {
-  if ($field_storage->isSyncing()) {
-    // Don't change anything during a configuration sync.
-    return;
-  }
-
-  // Act on all sub-types of the entity_reference field type.
-  /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
-  $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
-  $item_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem';
-  $class = $field_type_manager->getPluginClass($field_storage->getType());
-  if ($class !== $item_class && !is_subclass_of($class, $item_class)) {
-    return;
-  }
-
-  // If target_type changed, reset the handler in the fields using that storage.
-  if ($field_storage->getSetting('target_type') !== $field_storage->original->getSetting('target_type')) {
-    foreach ($field_storage->getBundles() as $bundle) {
-      $field = FieldConfig::loadByName($field_storage->getTargetEntityTypeId(), $bundle, $field_storage->getName());
-      // Reset the handler settings. This triggers field_field_config_presave(),
-      // which will take care of reassigning the handler to the correct
-      // derivative for the new target_type.
-      $field->setSetting('handler_settings', []);
-      $field->save();
-    }
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_create() for 'field_config'.
- *
- * Determine the selection handler plugin ID for an entity reference field.
- */
-function field_field_config_create(FieldConfigInterface $field) {
-  if ($field->isSyncing()) {
-    return;
-  }
-  // Act on all sub-types of the entity_reference field type.
-  /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
-  $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
-  $item_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem';
-  $class = $field_type_manager->getPluginClass($field->getType());
-  if ($class !== $item_class && !is_subclass_of($class, $item_class)) {
-    return;
-  }
-
-  // If we don't know the target type yet, there's nothing else we can do.
-  $target_type = $field->getFieldStorageDefinition()->getSetting('target_type');
-  if (empty($target_type)) {
-    return;
-  }
-
-  // Make sure the selection handler plugin is the correct derivative for the
-  // target entity type.
-  $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection');
-  [$current_handler] = explode(':', $field->getSetting('handler'), 2);
-  $field->setSetting('handler', $selection_manager->getPluginId($target_type, $current_handler));
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for 'field_config'.
- *
- * Determine the selection handler plugin ID for an entity reference field.
- */
-function field_field_config_presave(FieldConfigInterface $field) {
-  // Don't change anything during a configuration sync.
-  if ($field->isSyncing()) {
-    return;
-  }
-  field_field_config_create($field);
-
-  // Act on all sub-types of the entity_reference field type.
-  /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
-  $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
-  $item_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem';
-  $class = $field_type_manager->getPluginClass($field->getType());
-  if ($class !== $item_class && !is_subclass_of($class, $item_class)) {
-    return;
-  }
-
-  // In case we removed all the target bundles allowed by the field in
-  // EntityReferenceItem::onDependencyRemoval() or field_entity_bundle_delete()
-  // we have to log a critical message because the field will not function
-  // correctly anymore.
-  $handler_settings = $field->getSetting('handler_settings');
-  if (isset($handler_settings['target_bundles']) && $handler_settings['target_bundles'] === []) {
-    \Drupal::logger('entity_reference')->critical('The %field_name entity reference field (entity_type: %entity_type, bundle: %bundle) no longer has any valid bundle it can reference. The field is not working correctly anymore and has to be adjusted.', [
-      '%field_name' => $field->getName(),
-      '%entity_type' => $field->getTargetEntityTypeId(),
-      '%bundle' => $field->getTargetBundle(),
-    ]);
-  }
-}
-
 /**
  * Entity form builder for field config edit form.
  *
@@ -487,7 +80,7 @@ function field_form_field_config_edit_form_entity_builder($entity_type_id, $enti
     // @see field_field_storage_config_update().
     $entity->setSetting('handler_settings', []);
     // @see field_field_config_presave().
-    field_field_config_create($entity);
+    \Drupal::moduleHandler()->invoke('field', 'field_config_create', [$entity]);
 
     // Store updated settings in form state so that the form state can be copied
     // directly to the entity.
diff --git a/core/modules/field/field.purge.inc b/core/modules/field/field.purge.inc
index 8398f434c889258498d195e1f99cf5b613cd8903..449724ed8ee1da458a6f311caf33314465e29300 100644
--- a/core/modules/field/field.purge.inc
+++ b/core/modules/field/field.purge.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Provides support for field data purge after mass deletion.
  */
 
 use Drupal\Core\Field\FieldDefinitionInterface;
diff --git a/core/modules/field/src/Hook/FieldHooks.php b/core/modules/field/src/Hook/FieldHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f378d289f8439b8b73b057e68bb465e14693a650
--- /dev/null
+++ b/core/modules/field/src/Hook/FieldHooks.php
@@ -0,0 +1,403 @@
+<?php
+
+namespace Drupal\field\Hook;
+
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\field\EntityDisplayRebuilder;
+use Drupal\field\FieldConfigInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\field\ConfigImporterFieldPurger;
+use Drupal\Core\Config\ConfigImporter;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\Core\Entity\DynamicallyFieldableEntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for field.
+ */
+class FieldHooks {
+  /**
+   * @defgroup field Field API
+   * @{
+   * Attaches custom data fields to Drupal entities.
+   *
+   * The Field API allows custom data fields to be attached to Drupal entities and
+   * takes care of storing, loading, editing, and rendering field data. Any entity
+   * type (node, user, etc.) can use the Field API to make itself "fieldable" and
+   * thus allow fields to be attached to it. Other modules can provide a user
+   * interface for managing custom fields via a web browser as well as a wide and
+   * flexible variety of data type, form element, and display format capabilities.
+   *
+   * The Field API defines two primary data structures, FieldStorage and Field,
+   * and the concept of a Bundle. A FieldStorage defines a particular type of data
+   * that can be attached to entities. A Field is attached to a single
+   * Bundle. A Bundle is a set of fields that are treated as a group by the Field
+   * Attach API and is related to a single fieldable entity type.
+   *
+   * For example, suppose a site administrator wants Article nodes to have a
+   * subtitle and photo. Using the Field API or Field UI module, the administrator
+   * creates a field named 'subtitle' of type 'text' and a field named 'photo' of
+   * type 'image'. The administrator (again, via a UI) creates two Field
+   * Instances, one attaching the field 'subtitle' to the 'node' bundle 'article'
+   * and one attaching the field 'photo' to the 'node' bundle 'article'. When the
+   * node storage loads an Article node, it loads the values of the
+   * 'subtitle' and 'photo' fields because they are both attached to the 'node'
+   * bundle 'article'.
+   *
+   * - @link field_types Field Types API @endlink: Defines field types, widget
+   *   types, and display formatters. Field modules use this API to provide field
+   *   types like Text and Node Reference along with the associated form elements
+   *   and display formatters.
+   *
+   * - @link field_purge Field API bulk data deletion @endlink: Cleans up after
+   *   bulk deletion operations such as deletion of field storage or field.
+   */
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.field':
+        $field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Field module allows custom data fields to be defined for <em>entity</em> types (see below). The Field module takes care of storing, loading, editing, and rendering field data. Most users will not interact with the Field module directly, but will instead use the <a href=":field-ui-help">Field UI module</a> user interface. Module developers can use the Field API to make new entity types "fieldable" and thus allow fields to be attached to them. For more information, see the <a href=":field">online documentation for the Field module</a>.', [
+          ':field-ui-help' => $field_ui_url,
+          ':field' => 'https://www.drupal.org/documentation/modules/field',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Terminology') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Entities and entity types') . '</dt>';
+        $output .= '<dd>' . t("The website's content and configuration is managed using <em>entities</em>, which are grouped into <em>entity types</em>. <em>Content entity types</em> are the entity types for site content (such as the main site content, comments, content blocks, taxonomy terms, and user accounts). <em>Configuration entity types</em> are used to store configuration information for your site, such as individual views in the Views module, and settings for your main site content types.") . '</dd>';
+        $output .= '<dt>' . t('Entity sub-types') . '</dt>';
+        $output .= '<dd>' . t('Some content entity types are further grouped into sub-types (for example, you could have article and page content types within the main site content entity type, and tag and category vocabularies within the taxonomy term entity type); other entity types, such as user accounts, do not have sub-types. Programmers use the term <em>bundle</em> for entity sub-types.') . '</dd>';
+        $output .= '<dt>' . t('Fields and field types') . '</dt>';
+        $output .= '<dd>' . t('Content entity types and sub-types store most of their text, file, and other information in <em>fields</em>. Fields are grouped by <em>field type</em>; field types define what type of data can be stored in that field, such as text, images, or taxonomy term references.') . '</dd>';
+        $output .= '<dt>' . t('Formatters and view modes') . '</dd>';
+        $output .= '<dd>' . t('Content entity types and sub-types can have one or more <em>view modes</em>, used for displaying the entity items. For instance, a content item could be viewed in full content mode on its own page, teaser mode in a list, or RSS mode in a feed. In each view mode, each field can be hidden or displayed, and if it is displayed, you can choose and configure the <em>formatter</em> that is used to display the field. For instance, a long text field can be displayed trimmed or full-length, and taxonomy term reference fields can be displayed in plain text or linked to the taxonomy term page.') . '</dd>';
+        $output .= '<dt>' . t('Widgets and form modes') . '</dd>';
+        $output .= '<dd>' . t('Content entity types and sub-types can have one or more <em>form modes</em>, used for editing. For instance, a content item could be edited in a compact format with only some fields editable, or a full format that allows all fields to be edited. In each form mode, each field can be hidden or displayed, and if it is displayed, you can choose and configure the <em>widget</em> that is used to edit the field. For instance, a taxonomy term reference field can be edited using a select list, radio buttons, or an autocomplete widget.') . '</dd>';
+        $output .= '</dl>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Enabling field types, widgets, and formatters') . '</dt>';
+        $output .= '<dd>' . t('The Field module provides the infrastructure for fields; the field types, formatters, and widgets are provided by Drupal core or additional modules. Some of the modules are required; the optional modules can be installed from the <a href=":modules">Extend administration page</a>. Additional fields, formatters, and widgets may be provided by contributed modules, which you can find in the <a href=":contrib">contributed module section of Drupal.org</a>.', [
+          ':modules' => Url::fromRoute('system.modules_list')->toString(),
+          ':contrib' => 'https://www.drupal.org/project/modules',
+        ]) . '</dd>';
+        $output .= '<h2>' . t('Field, widget, and formatter information') . '</h2>';
+        // Make a list of all widget, formatter, and field modules currently
+        // enabled, ordered by displayed module name (module names are not
+        // translated).
+        $items = [];
+        $modules = \Drupal::moduleHandler()->getModuleList();
+        $widgets = \Drupal::service('plugin.manager.field.widget')->getDefinitions();
+        $field_types = \Drupal::service('plugin.manager.field.field_type')->getUiDefinitions();
+        $formatters = \Drupal::service('plugin.manager.field.formatter')->getDefinitions();
+        $providers = [];
+        foreach (array_merge($field_types, $widgets, $formatters) as $plugin) {
+          $providers[] = $plugin['provider'];
+        }
+        $providers = array_unique($providers);
+        sort($providers);
+        $module_extension_list = \Drupal::service('extension.list.module');
+        foreach ($providers as $provider) {
+          // Skip plugins provided by core components as they do not implement
+          // hook_help().
+          if (isset($modules[$provider])) {
+            $display = $module_extension_list->getName($provider);
+            if (\Drupal::moduleHandler()->hasImplementations('help', $provider)) {
+              $items[] = Link::fromTextAndUrl($display, Url::fromRoute('help.page', ['name' => $provider]))->toRenderable();
+            }
+            else {
+              $items[] = $display;
+            }
+          }
+        }
+        if ($items) {
+          $output .= '<dt>' . t('Provided by modules') . '</dt>';
+          $output .= '<dd>' . t('Here is a list of the currently installed field, formatter, and widget modules:');
+          $item_list = ['#theme' => 'item_list', '#items' => $items];
+          $output .= \Drupal::service('renderer')->renderInIsolation($item_list);
+          $output .= '</dd>';
+        }
+        $output .= '<dt>' . t('Provided by Drupal core') . '</dt>';
+        $output .= '<dd>' . t('As mentioned previously, some field types, widgets, and formatters are provided by Drupal core. Here are some notes on how to use some of these:');
+        $output .= '<ul>';
+        $output .= '<li><p>' . t('<strong>Entity Reference</strong> fields allow you to create fields that contain links to other entities (such as content items, taxonomy terms, etc.) within the site. This allows you, for example, to include a link to a user within a content item. For more information, see <a href=":er_do">the online documentation for the Entity Reference module</a>.', [':er_do' => 'https://drupal.org/documentation/modules/entityreference']) . '</p>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Managing and displaying entity reference fields') . '</dt>';
+        $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the entity reference field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => $field_ui_url]) . '</dd>';
+        $output .= '<dt>' . t('Selecting reference type') . '</dt>';
+        $output .= '<dd>' . t('In the field settings you can select which entity type you want to create a reference to.') . '</dd>';
+        $output .= '<dt>' . t('Filtering and sorting reference fields') . '</dt>';
+        $output .= '<dd>' . t('Depending on the chosen entity type, additional filtering and sorting options are available for the list of entities that can be referred to, in the field settings. For example, the list of users can be filtered by role and sorted by name or ID.') . '</dd>';
+        $output .= '<dt>' . t('Displaying a reference') . '</dt>';
+        $output .= '<dd>' . t('An entity reference can be displayed as a simple label with or without a link to the entity. Alternatively, the referenced entity can be displayed as a teaser (or any other available view mode) inside the referencing entity.') . '</dd>';
+        $output .= '<dt>' . t('Configuring form displays') . '</dt>';
+        $output .= '<dd>' . t('Reference fields have several widgets available on the <em>Manage form display</em> page:');
+        $output .= '<ul>';
+        $output .= '<li>' . t('The <em>Check boxes/radio buttons</em> widget displays the existing entities for the entity type as check boxes or radio buttons based on the <em>Allowed number of values</em> set for the field.') . '</li>';
+        $output .= '<li>' . t('The <em>Select list</em> widget displays the existing entities in a drop-down list or scrolling list box based on the <em>Allowed number of values</em> setting for the field.') . '</li>';
+        $output .= '<li>' . t('The <em>Autocomplete</em> widget displays text fields in which users can type entity labels based on the <em>Allowed number of values</em>. The widget can be configured to display all entities that contain the typed characters or restricted to those starting with those characters.') . '</li>';
+        $output .= '<li>' . t('The <em>Autocomplete (Tags style)</em> widget displays a multi-text field in which users can type in a comma-separated list of entity labels.') . '</li>';
+        $output .= '</ul></dd>';
+        $output .= '</dl></li>';
+        $output .= '<li>' . t('<strong>Number fields</strong>: When you add a number field you can choose from three types: <em>decimal</em>, <em>float</em>, and <em>integer</em>. The <em>decimal</em> number field type allows users to enter exact decimal values, with fixed numbers of decimal places. The <em>float</em> number field type allows users to enter approximate decimal values. The <em>integer</em> number field type allows users to enter whole numbers, such as years (for example, 2012) or values (for example, 1, 2, 5, 305). It does not allow decimals.') . '</li>';
+        $output .= '</ul></dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_cron().
+   */
+  #[Hook('cron')]
+  public function cron() {
+    // Do a pass of purging on deleted Field API data, if any exists.
+    $limit = \Drupal::config('field.settings')->get('purge_batch_size');
+    field_purge_batch($limit);
+  }
+
+  /**
+   * Implements hook_entity_field_storage_info().
+   */
+  #[Hook('entity_field_storage_info')]
+  public function entityFieldStorageInfo(EntityTypeInterface $entity_type) {
+    if (\Drupal::entityTypeManager()->getStorage($entity_type->id()) instanceof DynamicallyFieldableEntityStorageInterface) {
+      // Query by filtering on the ID as this is more efficient than filtering
+      // on the entity_type property directly.
+      $ids = \Drupal::entityQuery('field_storage_config')->condition('id', $entity_type->id() . '.', 'STARTS_WITH')->execute();
+      // Fetch all fields and key them by field name.
+      $field_storages = FieldStorageConfig::loadMultiple($ids);
+      $result = [];
+      foreach ($field_storages as $field_storage) {
+        $result[$field_storage->getName()] = $field_storage;
+      }
+      return $result;
+    }
+  }
+
+  /**
+   * Implements hook_entity_bundle_field_info().
+   */
+  #[Hook('entity_bundle_field_info')]
+  public function entityBundleFieldInfo(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
+    if (\Drupal::entityTypeManager()->getStorage($entity_type->id()) instanceof DynamicallyFieldableEntityStorageInterface) {
+      // Query by filtering on the ID as this is more efficient than filtering
+      // on the entity_type property directly.
+      $ids = \Drupal::entityQuery('field_config')->condition('id', $entity_type->id() . '.' . $bundle . '.', 'STARTS_WITH')->execute();
+      // Fetch all fields and key them by field name.
+      $field_configs = FieldConfig::loadMultiple($ids);
+      $result = [];
+      foreach ($field_configs as $field_instance) {
+        $result[$field_instance->getName()] = $field_instance;
+      }
+      return $result;
+    }
+  }
+
+  /**
+   * Implements hook_entity_bundle_delete().
+   */
+  #[Hook('entity_bundle_delete')]
+  public function entityBundleDelete($entity_type_id, $bundle) {
+    $storage = \Drupal::entityTypeManager()->getStorage('field_config');
+    // Get the fields on the bundle.
+    $fields = $storage->loadByProperties(['entity_type' => $entity_type_id, 'bundle' => $bundle]);
+    // This deletes the data for the field as well as the field themselves. This
+    // function actually just marks the data and fields as deleted, leaving the
+    // garbage collection for a separate process, because it is not always
+    // possible to delete this much data in a single page request (particularly
+    // since for some field types, the deletion is more than just a simple DELETE
+    // query).
+    foreach ($fields as $field) {
+      $field->delete();
+    }
+    // We are duplicating the work done by
+    // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::onDependencyRemoval()
+    // because we need to take into account bundles that are not provided by a
+    // config entity type so they are not part of the config dependencies.
+    // Gather a list of all entity reference fields.
+    $map = \Drupal::service('entity_field.manager')->getFieldMapByFieldType('entity_reference');
+    $ids = [];
+    foreach ($map as $type => $info) {
+      foreach ($info as $name => $data) {
+        foreach ($data['bundles'] as $bundle_name) {
+          $ids[] = "{$type}.{$bundle_name}.{$name}";
+        }
+      }
+    }
+    // Update the 'target_bundles' handler setting if needed.
+    foreach (FieldConfig::loadMultiple($ids) as $field_config) {
+      if ($field_config->getSetting('target_type') == $entity_type_id) {
+        $handler_settings = $field_config->getSetting('handler_settings');
+        if (isset($handler_settings['target_bundles'][$bundle])) {
+          unset($handler_settings['target_bundles'][$bundle]);
+          $field_config->setSetting('handler_settings', $handler_settings);
+          $field_config->save();
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_config_import_steps_alter().
+   */
+  #[Hook('config_import_steps_alter')]
+  public function configImportStepsAlter(&$sync_steps, ConfigImporter $config_importer) {
+    $field_storages = ConfigImporterFieldPurger::getFieldStoragesToPurge($config_importer->getStorageComparer()->getSourceStorage()->read('core.extension'), $config_importer->getStorageComparer()->getChangelist('delete'));
+    if ($field_storages) {
+      // Add a step to the beginning of the configuration synchronization process
+      // to purge field data where the module that provides the field is being
+      // uninstalled.
+      array_unshift($sync_steps, ['\Drupal\field\ConfigImporterFieldPurger', 'process']);
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   *
+   * Adds a warning if field data will be permanently removed by the configuration
+   * synchronization.
+   *
+   * @see \Drupal\field\ConfigImporterFieldPurger
+   */
+  #[Hook('form_config_admin_import_form_alter')]
+  public function formConfigAdminImportFormAlter(&$form, FormStateInterface $form_state) : void {
+    // Only display the message when core.extension is available in the source
+    // storage and the form is not submitted.
+    $user_input = $form_state->getUserInput();
+    $storage_comparer = $form_state->get('storage_comparer');
+    if ($storage_comparer?->getSourceStorage()->exists('core.extension') && empty($user_input)) {
+      $field_storages = ConfigImporterFieldPurger::getFieldStoragesToPurge($storage_comparer->getSourceStorage()->read('core.extension'), $storage_comparer->getChangelist('delete'));
+      if ($field_storages) {
+        foreach ($field_storages as $field) {
+          $field_labels[] = $field->label();
+        }
+        \Drupal::messenger()->addWarning(\Drupal::translation()->formatPlural(count($field_storages), 'This synchronization will delete data from the field %fields.', 'This synchronization will delete data from the fields: %fields.', ['%fields' => implode(', ', $field_labels)]));
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for 'field_config'.
+   */
+  #[Hook('field_config_insert')]
+  public function fieldConfigInsert(FieldConfigInterface $field) {
+    if ($field->isSyncing()) {
+      // Don't change anything during a configuration sync.
+      return;
+    }
+    // Allow other view modes to update their configuration for the new field.
+    // Otherwise, configuration for view modes won't get updated until the mode
+    // is used for the first time, creating noise in config diffs.
+    \Drupal::classResolver(EntityDisplayRebuilder::class)->rebuildEntityTypeDisplays($field->getTargetEntityTypeId(), $field->getTargetBundle());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for 'field_storage_config'.
+   *
+   * Reset the field handler settings, when the storage target_type is changed on
+   * an entity reference field.
+   */
+  #[Hook('field_storage_config_update')]
+  public function fieldStorageConfigUpdate(FieldStorageConfigInterface $field_storage) {
+    if ($field_storage->isSyncing()) {
+      // Don't change anything during a configuration sync.
+      return;
+    }
+    // Act on all sub-types of the entity_reference field type.
+    /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
+    $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
+    $item_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem';
+    $class = $field_type_manager->getPluginClass($field_storage->getType());
+    if ($class !== $item_class && !is_subclass_of($class, $item_class)) {
+      return;
+    }
+    // If target_type changed, reset the handler in the fields using that storage.
+    if ($field_storage->getSetting('target_type') !== $field_storage->original->getSetting('target_type')) {
+      foreach ($field_storage->getBundles() as $bundle) {
+        $field = FieldConfig::loadByName($field_storage->getTargetEntityTypeId(), $bundle, $field_storage->getName());
+        // Reset the handler settings. This triggers field_field_config_presave(),
+        // which will take care of reassigning the handler to the correct
+        // derivative for the new target_type.
+        $field->setSetting('handler_settings', []);
+        $field->save();
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_create() for 'field_config'.
+   *
+   * Determine the selection handler plugin ID for an entity reference field.
+   */
+  #[Hook('field_config_create')]
+  public function fieldConfigCreate(FieldConfigInterface $field) {
+    if ($field->isSyncing()) {
+      return;
+    }
+    // Act on all sub-types of the entity_reference field type.
+    /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
+    $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
+    $item_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem';
+    $class = $field_type_manager->getPluginClass($field->getType());
+    if ($class !== $item_class && !is_subclass_of($class, $item_class)) {
+      return;
+    }
+    // If we don't know the target type yet, there's nothing else we can do.
+    $target_type = $field->getFieldStorageDefinition()->getSetting('target_type');
+    if (empty($target_type)) {
+      return;
+    }
+    // Make sure the selection handler plugin is the correct derivative for the
+    // target entity type.
+    $selection_manager = \Drupal::service('plugin.manager.entity_reference_selection');
+    [$current_handler] = explode(':', $field->getSetting('handler'), 2);
+    $field->setSetting('handler', $selection_manager->getPluginId($target_type, $current_handler));
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for 'field_config'.
+   *
+   * Determine the selection handler plugin ID for an entity reference field.
+   */
+  #[Hook('field_config_presave')]
+  public function fieldConfigPresave(FieldConfigInterface $field) {
+    // Don't change anything during a configuration sync.
+    if ($field->isSyncing()) {
+      return;
+    }
+    $this->fieldConfigCreate($field);
+    // Act on all sub-types of the entity_reference field type.
+    /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
+    $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
+    $item_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem';
+    $class = $field_type_manager->getPluginClass($field->getType());
+    if ($class !== $item_class && !is_subclass_of($class, $item_class)) {
+      return;
+    }
+    // In case we removed all the target bundles allowed by the field in
+    // EntityReferenceItem::onDependencyRemoval() or field_entity_bundle_delete()
+    // we have to log a critical message because the field will not function
+    // correctly anymore.
+    $handler_settings = $field->getSetting('handler_settings');
+    if (isset($handler_settings['target_bundles']) && $handler_settings['target_bundles'] === []) {
+      \Drupal::logger('entity_reference')->critical('The %field_name entity reference field (entity_type: %entity_type, bundle: %bundle) no longer has any valid bundle it can reference. The field is not working correctly anymore and has to be adjusted.', [
+        '%field_name' => $field->getName(),
+        '%entity_type' => $field->getTargetEntityTypeId(),
+        '%bundle' => $field->getTargetBundle(),
+      ]);
+    }
+  }
+
+}
diff --git a/core/modules/field/tests/modules/field_test/field_test.entity.inc b/core/modules/field/tests/modules/field_test/field_test.entity.inc
index 2eb6285ee61b722e5d05a5dfd418d926a0348c85..24e79ea74dacc31644d04aeb1aac45a660f2ee72 100644
--- a/core/modules/field/tests/modules/field_test/field_test.entity.inc
+++ b/core/modules/field/tests/modules/field_test/field_test.entity.inc
@@ -7,16 +7,6 @@
 
 declare(strict_types=1);
 
-/**
- * Implements hook_entity_type_alter().
- */
-function field_test_entity_type_alter(array &$entity_types): void {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  foreach (field_test_entity_info_translatable() as $entity_type => $translatable) {
-    $entity_types[$entity_type]->set('translatable', $translatable);
-  }
-}
-
 /**
  * Helper function to enable entity translations.
  */
diff --git a/core/modules/field/tests/modules/field_test/field_test.field.inc b/core/modules/field/tests/modules/field_test/field_test.field.inc
index 9c564632974e33dda11f97243f38c1ae99f1d630..70099c92697215faffef28de40186e50d35685d9 100644
--- a/core/modules/field/tests/modules/field_test/field_test.field.inc
+++ b/core/modules/field/tests/modules/field_test/field_test.field.inc
@@ -8,36 +8,7 @@
 declare(strict_types=1);
 
 use Drupal\Core\Entity\FieldableEntityInterface;
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\field\FieldStorageConfigInterface;
-
-/**
- * Implements hook_field_widget_info_alter().
- */
-function field_test_field_widget_info_alter(&$info) {
-  $info['test_field_widget_multiple']['field_types'][] = 'test_field';
-  $info['test_field_widget_multiple']['field_types'][] = 'test_field_with_preconfigured_options';
-  // Add extra widget when needed for tests.
-  // @see \Drupal\field\Tests\FormTest::widgetAlterTest().
-  if ($alter_info = \Drupal::state()->get("field_test.widget_alter_test")) {
-    if ($alter_info['widget'] === 'test_field_widget_multiple_single_value') {
-      $info['test_field_widget_multiple_single_value']['field_types'][] = 'test_field';
-    }
-  }
-}
-
-/**
- * Implements hook_field_storage_config_update_forbid().
- */
-function field_test_field_storage_config_update_forbid(FieldStorageConfigInterface $field_storage, FieldStorageConfigInterface $prior_field_storage) {
-  if ($field_storage->getType() == 'test_field' && $field_storage->getSetting('unchangeable') != $prior_field_storage->getSetting('unchangeable')) {
-    throw new FieldStorageDefinitionUpdateForbiddenException("field_test 'unchangeable' setting cannot be changed'");
-  }
-}
 
 /**
  * Sample 'default value' callback.
@@ -45,20 +16,3 @@ function field_test_field_storage_config_update_forbid(FieldStorageConfigInterfa
 function field_test_default_value(FieldableEntityInterface $entity, FieldDefinitionInterface $definition) {
   return [['value' => 99]];
 }
-
-/**
- * Implements hook_entity_field_access().
- */
-function field_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
-  if ($field_definition->getName() == "field_no_{$operation}_access") {
-    return AccessResult::forbidden();
-  }
-
-  // Only grant view access to test_view_field fields when the user has
-  // 'view test_view_field content' permission.
-  if ($field_definition->getName() == 'test_view_field' && $operation == 'view') {
-    return AccessResult::forbiddenIf(!$account->hasPermission('view test_view_field content'))->cachePerPermissions();
-  }
-
-  return AccessResult::allowed();
-}
diff --git a/core/modules/field/tests/modules/field_test/field_test.module b/core/modules/field/tests/modules/field_test/field_test.module
index 9f46de3a3ae199d290bdb25ba2d6e3f7b1093378..449cc5033a3941eab0276e738379a2f30b9de871 100644
--- a/core/modules/field/tests/modules/field_test/field_test.module
+++ b/core/modules/field/tests/modules/field_test/field_test.module
@@ -14,11 +14,8 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\Query\QueryInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\field\FieldStorageConfigInterface;
 
 require_once __DIR__ . '/field_test.entity.inc';
@@ -87,53 +84,6 @@ function field_test_field_storage_config_create(FieldStorageConfigInterface $fie
   field_test_memorize(__FUNCTION__, $args);
 }
 
-/**
- * Implements hook_entity_display_build_alter().
- */
-function field_test_entity_display_build_alter(&$output, $context) {
-  $display_options = $context['display']->getComponent('test_field');
-  if (isset($display_options['settings']['alter'])) {
-    $output['test_field'][] = ['#markup' => 'field_test_entity_display_build_alter'];
-  }
-
-  if (isset($output['test_field'])) {
-    $output['test_field'][] = ['#markup' => 'entity language is ' . $context['entity']->language()->getId()];
-  }
-}
-
-/**
- * Implements hook_field_widget_single_element_form_alter().
- */
-function field_test_field_widget_single_element_form_alter(&$element, FormStateInterface $form_state, $context) {
-  // Set a message if this is for the form displayed to set default value for
-  // the field.
-  if ($context['default']) {
-    \Drupal::messenger()
-      ->addStatus('From hook_field_widget_single_element_form_alter(): Default form is true.');
-  }
-}
-
-/**
- * Implements hook_field_widget_complete_form_alter().
- */
-function field_test_field_widget_complete_form_alter(array &$field_widget_complete_form, FormStateInterface $form_state, array $context) {
-  _field_test_alter_widget("hook_field_widget_complete_form_alter", $field_widget_complete_form, $form_state, $context);
-}
-
-/**
- * Implements hook_field_widget_complete_WIDGET_TYPE_form_alter().
- */
-function field_test_field_widget_complete_test_field_widget_multiple_form_alter(array &$field_widget_complete_form, FormStateInterface $form_state, array $context) {
-  _field_test_alter_widget("hook_field_widget_complete_WIDGET_TYPE_form_alter", $field_widget_complete_form, $form_state, $context);
-}
-
-/**
- * Implements hook_field_widget_complete_WIDGET_TYPE_form_alter().
- */
-function field_test_field_widget_complete_test_field_widget_multiple_single_value_form_alter(array &$field_widget_complete_form, FormStateInterface $form_state, array $context) {
-  _field_test_alter_widget("hook_field_widget_complete_WIDGET_TYPE_form_alter", $field_widget_complete_form, $form_state, $context);
-}
-
 /**
  * Sets up alterations for widget alter tests.
  *
@@ -156,124 +106,8 @@ function _field_test_alter_widget($hook, array &$field_widget_complete_form, For
   }
 }
 
-/**
- * Implements hook_query_TAG_alter() for tag 'efq_table_prefixing_test'.
- *
- * @see \Drupal\system\Tests\Entity\EntityFieldQueryTest::testTablePrefixing()
- */
-function field_test_query_efq_table_prefixing_test_alter(&$query) {
-  // Add an additional join onto the entity base table. This will cause an
-  // exception if the EFQ does not properly prefix the base table.
-  $query->join('entity_test', 'et2', '[%alias].[id] = [entity_test].[id]');
-}
-
-/**
- * Implements hook_query_TAG_alter() for tag 'efq_metadata_test'.
- *
- * @see \Drupal\system\Tests\Entity\EntityQueryTest::testMetaData()
- */
-function field_test_query_efq_metadata_test_alter(&$query) {
-  field_test_memorize(__FUNCTION__, $query->getMetadata('foo'));
-}
-
-/**
- * Implements hook_entity_extra_field_info_alter().
- */
-function field_test_entity_extra_field_info_alter(&$info) {
-  // Remove all extra fields from the 'no_fields' content type;
-  unset($info['node']['no_fields']);
-}
-
-/**
- * Implements hook_entity_bundle_field_info_alter().
- */
-function field_test_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {
-  if (($field_name = \Drupal::state()->get('field_test_constraint', FALSE)) && $entity_type->id() == 'entity_test' && $bundle == 'entity_test' && !empty($fields[$field_name])) {
-    // Set a property constraint using
-    // \Drupal\Core\Field\FieldConfigInterface::setPropertyConstraints().
-    $fields[$field_name]->setPropertyConstraints('value', [
-      'TestField' => [
-        'value' => -2,
-        'message' => t('%name does not accept the value @value.', ['%name' => $field_name, '@value' => -2]),
-      ],
-    ]);
-
-    // Add a property constraint using
-    // \Drupal\Core\Field\FieldConfigInterface::addPropertyConstraints().
-    $fields[$field_name]->addPropertyConstraints('value', [
-      'Range' => [
-        'min' => 0,
-        'max' => 32,
-      ],
-    ]);
-  }
-}
-
-/**
- * Implements hook_field_ui_preconfigured_options_alter().
- */
-function field_test_field_ui_preconfigured_options_alter(array &$options, $field_type) {
-  if ($field_type === 'test_field_with_preconfigured_options') {
-    $options['custom_options']['entity_view_display']['settings'] = [
-      'test_formatter_setting_multiple' => 'altered dummy test string',
-    ];
-  }
-}
-
-/**
- * Implements hook_field_info_entity_type_ui_definitions_alter().
- */
-function field_test_field_info_entity_type_ui_definitions_alter(array &$ui_definitions, string $entity_type_id) {
-  if ($entity_type_id === 'node') {
-    $ui_definitions['boolean']['label'] = new TranslatableMarkup('Boolean (overridden by alter)');
-  }
-}
-
 function field_test_entity_reference_selection_alter(array &$definitions): void {
   if (\Drupal::state()->get('field_test_disable_broken_entity_reference_handler')) {
     unset($definitions['broken']);
   }
 }
-
-/**
- * Implements hook_entity_query_alter().
- *
- * @see Drupal\KernelTests\Core\Entity\EntityQueryTest::testAlterHook
- */
-function field_test_entity_query_alter(QueryInterface $query): void {
-  if ($query->hasTag('entity_query_alter_hook_test')) {
-    $query->condition('id', '5', '<>');
-  }
-}
-
-/**
- * Implements hook_entity_query_ENTITY_TYPE_alter() for 'entity_test_mulrev'.
- *
- * @see Drupal\KernelTests\Core\Entity\EntityQueryTest::testAlterHook
- */
-function field_test_entity_query_entity_test_mulrev_alter(QueryInterface $query): void {
-  if ($query->hasTag('entity_query_entity_test_mulrev_alter_hook_test')) {
-    $query->condition('id', '7', '<>');
-  }
-}
-
-/**
- * Implements hook_entity_query_tag__TAG_alter() for 'entity_query_alter_tag_test'.
- *
- * @see Drupal\KernelTests\Core\Entity\EntityQueryTest::testAlterHook
- */
-function field_test_entity_query_tag__entity_query_alter_tag_test_alter(QueryInterface $query): void {
-  $query->condition('id', '13', '<>');
-}
-
-/**
- * Implements hook_entity_query_tag__ENTITY_TYPE__TAG_alter().
- *
- * Entity type is 'entity_test_mulrev' and tag is
- * 'entity_query_entity_test_mulrev_alter_tag_test'.
- *
- * @see Drupal\KernelTests\Core\Entity\EntityQueryTest::testAlterHook
- */
-function field_test_entity_query_tag__entity_test_mulrev__entity_query_entity_test_mulrev_alter_tag_test_alter(QueryInterface $query): void {
-  $query->condition('id', '15', '<>');
-}
diff --git a/core/modules/field/tests/modules/field_test/src/Hook/FieldTestEntityHooks.php b/core/modules/field/tests/modules/field_test/src/Hook/FieldTestEntityHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..1f0bb302500f1e438dfd9eadbc0faaf0ed932e63
--- /dev/null
+++ b/core/modules/field/tests/modules/field_test/src/Hook/FieldTestEntityHooks.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\field_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for field_test.
+ */
+class FieldTestEntityHooks {
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    foreach (field_test_entity_info_translatable() as $entity_type => $translatable) {
+      $entity_types[$entity_type]->set('translatable', $translatable);
+    }
+  }
+
+}
diff --git a/core/modules/field/tests/modules/field_test/src/Hook/FieldTestFieldHooks.php b/core/modules/field/tests/modules/field_test/src/Hook/FieldTestFieldHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..2cfb6ddc19642ebfce4ab8c2d37b183dd56831e9
--- /dev/null
+++ b/core/modules/field/tests/modules/field_test/src/Hook/FieldTestFieldHooks.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\field_test\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for field_test.
+ */
+class FieldTestFieldHooks {
+
+  /**
+   * Implements hook_field_widget_info_alter().
+   */
+  #[Hook('field_widget_info_alter')]
+  public function fieldWidgetInfoAlter(&$info) {
+    $info['test_field_widget_multiple']['field_types'][] = 'test_field';
+    $info['test_field_widget_multiple']['field_types'][] = 'test_field_with_preconfigured_options';
+    // Add extra widget when needed for tests.
+    // @see \Drupal\field\Tests\FormTest::widgetAlterTest().
+    if ($alter_info = \Drupal::state()->get("field_test.widget_alter_test")) {
+      if ($alter_info['widget'] === 'test_field_widget_multiple_single_value') {
+        $info['test_field_widget_multiple_single_value']['field_types'][] = 'test_field';
+      }
+    }
+  }
+
+  /**
+   * Implements hook_field_storage_config_update_forbid().
+   */
+  #[Hook('field_storage_config_update_forbid')]
+  public function fieldStorageConfigUpdateForbid(FieldStorageConfigInterface $field_storage, FieldStorageConfigInterface $prior_field_storage) {
+    if ($field_storage->getType() == 'test_field' && $field_storage->getSetting('unchangeable') != $prior_field_storage->getSetting('unchangeable')) {
+      throw new FieldStorageDefinitionUpdateForbiddenException("field_test 'unchangeable' setting cannot be changed'");
+    }
+  }
+
+  /**
+   * Implements hook_entity_field_access().
+   */
+  #[Hook('entity_field_access')]
+  public function entityFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
+    if ($field_definition->getName() == "field_no_{$operation}_access") {
+      return AccessResult::forbidden();
+    }
+    // Only grant view access to test_view_field fields when the user has
+    // 'view test_view_field content' permission.
+    if ($field_definition->getName() == 'test_view_field' && $operation == 'view') {
+      return AccessResult::forbiddenIf(!$account->hasPermission('view test_view_field content'))->cachePerPermissions();
+    }
+    return AccessResult::allowed();
+  }
+
+}
diff --git a/core/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php b/core/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..bafde444918cb1b5dacfbb3c3cd27cccd128bcc1
--- /dev/null
+++ b/core/modules/field/tests/modules/field_test/src/Hook/FieldTestHooks.php
@@ -0,0 +1,189 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\field_test\Hook;
+
+use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for field_test.
+ */
+class FieldTestHooks {
+
+  /**
+   * Implements hook_entity_display_build_alter().
+   */
+  #[Hook('entity_display_build_alter')]
+  public function entityDisplayBuildAlter(&$output, $context) {
+    $display_options = $context['display']->getComponent('test_field');
+    if (isset($display_options['settings']['alter'])) {
+      $output['test_field'][] = ['#markup' => 'field_test_entity_display_build_alter'];
+    }
+    if (isset($output['test_field'])) {
+      $output['test_field'][] = ['#markup' => 'entity language is ' . $context['entity']->language()->getId()];
+    }
+  }
+
+  /**
+   * Implements hook_field_widget_single_element_form_alter().
+   */
+  #[Hook('field_widget_single_element_form_alter')]
+  public function fieldWidgetSingleElementFormAlter(&$element, FormStateInterface $form_state, $context) {
+    // Set a message if this is for the form displayed to set default value for
+    // the field.
+    if ($context['default']) {
+      \Drupal::messenger()->addStatus('From hook_field_widget_single_element_form_alter(): Default form is true.');
+    }
+  }
+
+  /**
+   * Implements hook_field_widget_complete_form_alter().
+   */
+  #[Hook('field_widget_complete_form_alter')]
+  public function fieldWidgetCompleteFormAlter(array &$field_widget_complete_form, FormStateInterface $form_state, array $context) {
+    _field_test_alter_widget("hook_field_widget_complete_form_alter", $field_widget_complete_form, $form_state, $context);
+  }
+
+  /**
+   * Implements hook_field_widget_complete_WIDGET_TYPE_form_alter().
+   */
+  #[Hook('field_widget_complete_test_field_widget_multiple_form_alter')]
+  public function fieldWidgetCompleteTestFieldWidgetMultipleFormAlter(array &$field_widget_complete_form, FormStateInterface $form_state, array $context) {
+    _field_test_alter_widget("hook_field_widget_complete_WIDGET_TYPE_form_alter", $field_widget_complete_form, $form_state, $context);
+  }
+
+  /**
+   * Implements hook_field_widget_complete_WIDGET_TYPE_form_alter().
+   */
+  #[Hook('field_widget_complete_test_field_widget_multiple_single_value_form_alter')]
+  public function fieldWidgetCompleteTestFieldWidgetMultipleSingleValueFormAlter(array &$field_widget_complete_form, FormStateInterface $form_state, array $context) {
+    _field_test_alter_widget("hook_field_widget_complete_WIDGET_TYPE_form_alter", $field_widget_complete_form, $form_state, $context);
+  }
+
+  /**
+   * Implements hook_query_TAG_alter() for tag 'efq_table_prefixing_test'.
+   *
+   * @see \Drupal\system\Tests\Entity\EntityFieldQueryTest::testTablePrefixing()
+   */
+  #[Hook('query_efq_table_prefixing_test_alter')]
+  public function queryEfqTablePrefixingTestAlter(&$query) {
+    // Add an additional join onto the entity base table. This will cause an
+    // exception if the EFQ does not properly prefix the base table.
+    $query->join('entity_test', 'et2', '[%alias].[id] = [entity_test].[id]');
+  }
+
+  /**
+   * Implements hook_query_TAG_alter() for tag 'efq_metadata_test'.
+   *
+   * @see \Drupal\system\Tests\Entity\EntityQueryTest::testMetaData()
+   */
+  #[Hook('query_efq_metadata_test_alter')]
+  public function queryEfqMetadataTestAlter(&$query) {
+    field_test_memorize('field_test_query_efq_metadata_test_alter', $query->getMetadata('foo'));
+  }
+
+  /**
+   * Implements hook_entity_extra_field_info_alter().
+   */
+  #[Hook('entity_extra_field_info_alter')]
+  public function entityExtraFieldInfoAlter(&$info) {
+    // Remove all extra fields from the 'no_fields' content type;
+    unset($info['node']['no_fields']);
+  }
+
+  /**
+   * Implements hook_entity_bundle_field_info_alter().
+   */
+  #[Hook('entity_bundle_field_info_alter')]
+  public function entityBundleFieldInfoAlter(&$fields, EntityTypeInterface $entity_type, $bundle) {
+    if (($field_name = \Drupal::state()->get('field_test_constraint', FALSE)) && $entity_type->id() == 'entity_test' && $bundle == 'entity_test' && !empty($fields[$field_name])) {
+      // Set a property constraint using
+      // \Drupal\Core\Field\FieldConfigInterface::setPropertyConstraints().
+      $fields[$field_name]->setPropertyConstraints('value', [
+        'TestField' => [
+          'value' => -2,
+          'message' => t('%name does not accept the value @value.', [
+            '%name' => $field_name,
+            '@value' => -2,
+          ]),
+        ],
+      ]);
+      // Add a property constraint using
+      // \Drupal\Core\Field\FieldConfigInterface::addPropertyConstraints().
+      $fields[$field_name]->addPropertyConstraints('value', ['Range' => ['min' => 0, 'max' => 32]]);
+    }
+  }
+
+  /**
+   * Implements hook_field_ui_preconfigured_options_alter().
+   */
+  #[Hook('field_ui_preconfigured_options_alter')]
+  public function fieldUiPreconfiguredOptionsAlter(array &$options, $field_type) {
+    if ($field_type === 'test_field_with_preconfigured_options') {
+      $options['custom_options']['entity_view_display']['settings'] = ['test_formatter_setting_multiple' => 'altered dummy test string'];
+    }
+  }
+
+  /**
+   * Implements hook_field_info_entity_type_ui_definitions_alter().
+   */
+  #[Hook('field_info_entity_type_ui_definitions_alter')]
+  public function fieldInfoEntityTypeUiDefinitionsAlter(array &$ui_definitions, string $entity_type_id) {
+    if ($entity_type_id === 'node') {
+      $ui_definitions['boolean']['label'] = new TranslatableMarkup('Boolean (overridden by alter)');
+    }
+  }
+
+  /**
+   * Implements hook_entity_query_alter().
+   *
+   * @see Drupal\KernelTests\Core\Entity\EntityQueryTest::testAlterHook
+   */
+  #[Hook('entity_query_alter')]
+  public function entityQueryAlter(QueryInterface $query) : void {
+    if ($query->hasTag('entity_query_alter_hook_test')) {
+      $query->condition('id', '5', '<>');
+    }
+  }
+
+  /**
+   * Implements hook_entity_query_ENTITY_TYPE_alter() for 'entity_test_mulrev'.
+   *
+   * @see Drupal\KernelTests\Core\Entity\EntityQueryTest::testAlterHook
+   */
+  #[Hook('entity_query_entity_test_mulrev_alter')]
+  public function entityQueryEntityTestMulrevAlter(QueryInterface $query) : void {
+    if ($query->hasTag('entity_query_entity_test_mulrev_alter_hook_test')) {
+      $query->condition('id', '7', '<>');
+    }
+  }
+
+  /**
+   * Implements hook_entity_query_tag__TAG_alter() for 'entity_query_alter_tag_test'.
+   *
+   * @see Drupal\KernelTests\Core\Entity\EntityQueryTest::testAlterHook
+   */
+  #[Hook('entity_query_tag__entity_query_alter_tag_test_alter')]
+  public function entityQueryTagEntityQueryAlterTagTestAlter(QueryInterface $query) : void {
+    $query->condition('id', '13', '<>');
+  }
+
+  /**
+   * Implements hook_entity_query_tag__ENTITY_TYPE__TAG_alter().
+   *
+   * Entity type is 'entity_test_mulrev' and tag is
+   * 'entity_query_entity_test_mulrev_alter_tag_test'.
+   *
+   * @see Drupal\KernelTests\Core\Entity\EntityQueryTest::testAlterHook
+   */
+  #[Hook('entity_query_tag__entity_test_mulrev__entity_query_entity_test_mulrev_alter_tag_test_alter')]
+  public function entityQueryTagEntityTestMulrevEntityQueryEntityTestMulrevAlterTagTestAlter(QueryInterface $query) : void {
+    $query->condition('id', '15', '<>');
+  }
+
+}
diff --git a/core/modules/field/tests/modules/field_test_boolean_access_denied/field_test_boolean_access_denied.module b/core/modules/field/tests/modules/field_test_boolean_access_denied/field_test_boolean_access_denied.module
deleted file mode 100644
index 1c840572e6117f3e33fdee32a6feb4ce64aa0181..0000000000000000000000000000000000000000
--- a/core/modules/field/tests/modules/field_test_boolean_access_denied/field_test_boolean_access_denied.module
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * Module for testing denying access to boolean fields.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Implements hook_entity_field_access().
- */
-function field_test_boolean_access_denied_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
-  return AccessResult::forbiddenIf($field_definition->getName() === \Drupal::state()->get('field.test_boolean_field_access_field'));
-}
diff --git a/core/modules/field/tests/modules/field_test_boolean_access_denied/src/Hook/FieldTestBooleanAccessDeniedHooks.php b/core/modules/field/tests/modules/field_test_boolean_access_denied/src/Hook/FieldTestBooleanAccessDeniedHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..438fa19bfaa32936adada32cd4f14716782b5fd6
--- /dev/null
+++ b/core/modules/field/tests/modules/field_test_boolean_access_denied/src/Hook/FieldTestBooleanAccessDeniedHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\field_test_boolean_access_denied\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for field_test_boolean_access_denied.
+ */
+class FieldTestBooleanAccessDeniedHooks {
+
+  /**
+   * Implements hook_entity_field_access().
+   */
+  #[Hook('entity_field_access')]
+  public function entityFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
+    return AccessResult::forbiddenIf($field_definition->getName() === \Drupal::state()->get('field.test_boolean_field_access_field'));
+  }
+
+}
diff --git a/core/modules/field/tests/modules/field_third_party_test/field_third_party_test.module b/core/modules/field/tests/modules/field_third_party_test/field_third_party_test.module
deleted file mode 100644
index 300859cd274701c01c79f867a8e5956bc4aba10d..0000000000000000000000000000000000000000
--- a/core/modules/field/tests/modules/field_third_party_test/field_third_party_test.module
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Field\FormatterInterface;
-use Drupal\Core\Field\WidgetInterface;
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * Implements hook_field_widget_third_party_settings_form().
- */
-function field_third_party_test_field_widget_third_party_settings_form(WidgetInterface $plugin, FieldDefinitionInterface $field_definition, $form_mode, $form, FormStateInterface $form_state) {
-  $element['field_test_widget_third_party_settings_form'] = [
-    '#type' => 'textfield',
-    '#title' => t('3rd party widget settings form'),
-    '#default_value' => $plugin->getThirdPartySetting('field_third_party_test', 'field_test_widget_third_party_settings_form'),
-  ];
-  return $element;
-}
-
-/**
- * Implements hook_field_widget_settings_summary_alter().
- */
-function field_third_party_test_field_widget_settings_summary_alter(&$summary, $context) {
-  $summary[] = 'field_test_field_widget_settings_summary_alter';
-  return $summary;
-}
-
-/**
- * Implements hook_field_formatter_third_party_settings_form().
- */
-function field_third_party_test_field_formatter_third_party_settings_form(FormatterInterface $plugin, FieldDefinitionInterface $field_definition, $view_mode, $form, FormStateInterface $form_state) {
-  $element['field_test_field_formatter_third_party_settings_form'] = [
-    '#type' => 'textfield',
-    '#title' => t('3rd party formatter settings form'),
-    '#default_value' => $plugin->getThirdPartySetting('field_third_party_test', 'field_test_field_formatter_third_party_settings_form'),
-  ];
-  return $element;
-}
-
-/**
- * Implements hook_field_formatter_settings_summary_alter().
- */
-function field_third_party_test_field_formatter_settings_summary_alter(&$summary, $context) {
-  $summary[] = 'field_test_field_formatter_settings_summary_alter';
-  return $summary;
-}
diff --git a/core/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php b/core/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..e2e749237ef2a0bccdbdf29333bd77621cfd81af
--- /dev/null
+++ b/core/modules/field/tests/modules/field_third_party_test/src/Hook/FieldThirdPartyTestHooks.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\field_third_party_test\Hook;
+
+use Drupal\Core\Field\FormatterInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\WidgetInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for field_third_party_test.
+ */
+class FieldThirdPartyTestHooks {
+
+  /**
+   * Implements hook_field_widget_third_party_settings_form().
+   */
+  #[Hook('field_widget_third_party_settings_form')]
+  public function fieldWidgetThirdPartySettingsForm(WidgetInterface $plugin, FieldDefinitionInterface $field_definition, $form_mode, $form, FormStateInterface $form_state) {
+    $element['field_test_widget_third_party_settings_form'] = [
+      '#type' => 'textfield',
+      '#title' => t('3rd party widget settings form'),
+      '#default_value' => $plugin->getThirdPartySetting('field_third_party_test', 'field_test_widget_third_party_settings_form'),
+    ];
+    return $element;
+  }
+
+  /**
+   * Implements hook_field_widget_settings_summary_alter().
+   */
+  #[Hook('field_widget_settings_summary_alter')]
+  public function fieldWidgetSettingsSummaryAlter(&$summary, $context) {
+    $summary[] = 'field_test_field_widget_settings_summary_alter';
+    return $summary;
+  }
+
+  /**
+   * Implements hook_field_formatter_third_party_settings_form().
+   */
+  #[Hook('field_formatter_third_party_settings_form')]
+  public function fieldFormatterThirdPartySettingsForm(FormatterInterface $plugin, FieldDefinitionInterface $field_definition, $view_mode, $form, FormStateInterface $form_state) {
+    $element['field_test_field_formatter_third_party_settings_form'] = [
+      '#type' => 'textfield',
+      '#title' => t('3rd party formatter settings form'),
+      '#default_value' => $plugin->getThirdPartySetting('field_third_party_test', 'field_test_field_formatter_third_party_settings_form'),
+    ];
+    return $element;
+  }
+
+  /**
+   * Implements hook_field_formatter_settings_summary_alter().
+   */
+  #[Hook('field_formatter_settings_summary_alter')]
+  public function fieldFormatterSettingsSummaryAlter(&$summary, $context) {
+    $summary[] = 'field_test_field_formatter_settings_summary_alter';
+    return $summary;
+  }
+
+}
diff --git a/core/modules/field_layout/field_layout.module b/core/modules/field_layout/field_layout.module
deleted file mode 100644
index 93dd27f4013a64c131f579fa4c426e4436297d64..0000000000000000000000000000000000000000
--- a/core/modules/field_layout/field_layout.module
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides hook implementations for Field Layout.
- */
-
-use Drupal\Core\Entity\ContentEntityFormInterface;
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
-use Drupal\field_layout\Entity\FieldLayoutEntityFormDisplay;
-use Drupal\field_layout\Entity\FieldLayoutEntityViewDisplay;
-use Drupal\field_layout\FieldLayoutBuilder;
-use Drupal\field_layout\Form\FieldLayoutEntityFormDisplayEditForm;
-use Drupal\field_layout\Form\FieldLayoutEntityViewDisplayEditForm;
-
-/**
- * Implements hook_help().
- */
-function field_layout_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.field_layout':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Field Layout module allows you to arrange fields into regions on forms and displays of entities such as nodes and users.') . '</p>';
-      $output .= '<p>' . t('For more information, see the <a href=":field-layout-documentation">online documentation for the Field Layout module</a>.', [':field-layout-documentation' => 'https://www.drupal.org/documentation/modules/field_layout']) . '</p>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function field_layout_entity_type_alter(array &$entity_types): void {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  $entity_types['entity_view_display']->setClass(FieldLayoutEntityViewDisplay::class);
-  $entity_types['entity_form_display']->setClass(FieldLayoutEntityFormDisplay::class);
-
-  // The form classes are only needed when Field UI is installed.
-  if (\Drupal::moduleHandler()->moduleExists('field_ui')) {
-    $entity_types['entity_view_display']->setFormClass('edit', FieldLayoutEntityViewDisplayEditForm::class);
-    $entity_types['entity_form_display']->setFormClass('edit', FieldLayoutEntityFormDisplayEditForm::class);
-  }
-}
-
-/**
- * Implements hook_entity_view_alter().
- */
-function field_layout_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
-  if ($display instanceof EntityDisplayWithLayoutInterface) {
-    \Drupal::classResolver(FieldLayoutBuilder::class)->buildView($build, $display);
-  }
-}
-
-/**
- * Implements hook_form_alter().
- */
-function field_layout_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  $form_object = $form_state->getFormObject();
-  if ($form_object instanceof ContentEntityFormInterface && $display = $form_object->getFormDisplay($form_state)) {
-    if ($display instanceof EntityDisplayWithLayoutInterface) {
-      \Drupal::classResolver(FieldLayoutBuilder::class)->buildForm($form, $display);
-    }
-  }
-}
diff --git a/core/modules/field_layout/src/Hook/FieldLayoutHooks.php b/core/modules/field_layout/src/Hook/FieldLayoutHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..79ed8b87fc870f5d3a4ccb9f5233ab8741842237
--- /dev/null
+++ b/core/modules/field_layout/src/Hook/FieldLayoutHooks.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\field_layout\Hook;
+
+use Drupal\Core\Entity\ContentEntityFormInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\field_layout\FieldLayoutBuilder;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\field_layout\Form\FieldLayoutEntityFormDisplayEditForm;
+use Drupal\field_layout\Form\FieldLayoutEntityViewDisplayEditForm;
+use Drupal\field_layout\Entity\FieldLayoutEntityFormDisplay;
+use Drupal\field_layout\Entity\FieldLayoutEntityViewDisplay;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for field_layout.
+ */
+class FieldLayoutHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.field_layout':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Field Layout module allows you to arrange fields into regions on forms and displays of entities such as nodes and users.') . '</p>';
+        $output .= '<p>' . t('For more information, see the <a href=":field-layout-documentation">online documentation for the Field Layout module</a>.', [
+          ':field-layout-documentation' => 'https://www.drupal.org/documentation/modules/field_layout',
+        ]) . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    $entity_types['entity_view_display']->setClass(FieldLayoutEntityViewDisplay::class);
+    $entity_types['entity_form_display']->setClass(FieldLayoutEntityFormDisplay::class);
+    // The form classes are only needed when Field UI is installed.
+    if (\Drupal::moduleHandler()->moduleExists('field_ui')) {
+      $entity_types['entity_view_display']->setFormClass('edit', FieldLayoutEntityViewDisplayEditForm::class);
+      $entity_types['entity_form_display']->setFormClass('edit', FieldLayoutEntityFormDisplayEditForm::class);
+    }
+  }
+
+  /**
+   * Implements hook_entity_view_alter().
+   */
+  #[Hook('entity_view_alter')]
+  public function entityViewAlter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
+    if ($display instanceof EntityDisplayWithLayoutInterface) {
+      \Drupal::classResolver(FieldLayoutBuilder::class)->buildView($build, $display);
+    }
+  }
+
+  /**
+   * Implements hook_form_alter().
+   */
+  #[Hook('form_alter')]
+  public function formAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    $form_object = $form_state->getFormObject();
+    if ($form_object instanceof ContentEntityFormInterface && ($display = $form_object->getFormDisplay($form_state))) {
+      if ($display instanceof EntityDisplayWithLayoutInterface) {
+        \Drupal::classResolver(FieldLayoutBuilder::class)->buildForm($form, $display);
+      }
+    }
+  }
+
+}
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.module b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.module
deleted file mode 100644
index ba45d420bfb0ba55bb8a077a63d95a97eb6d0723..0000000000000000000000000000000000000000
--- a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.module
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains hook implementations for field_layout_test.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_layout_alter().
- */
-function field_layout_test_layout_alter(&$definitions) {
-  /** @var \Drupal\Core\Layout\LayoutDefinition[] $definitions */
-  if (\Drupal::state()->get('field_layout_test.alter_regions') && isset($definitions['layout_onecol'])) {
-    $definitions['layout_onecol']->setRegions(['foo' => ['label' => 'Foo']]);
-  }
-}
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/src/Hook/FieldLayoutTestHooks.php b/core/modules/field_layout/tests/modules/field_layout_test/src/Hook/FieldLayoutTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..7ed4af9aadd4c4d9aacac8537220f4334d232711
--- /dev/null
+++ b/core/modules/field_layout/tests/modules/field_layout_test/src/Hook/FieldLayoutTestHooks.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\field_layout_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for field_layout_test.
+ */
+class FieldLayoutTestHooks {
+
+  /**
+   * Implements hook_layout_alter().
+   */
+  #[Hook('layout_alter')]
+  public function layoutAlter(&$definitions) {
+    /** @var \Drupal\Core\Layout\LayoutDefinition[] $definitions */
+    if (\Drupal::state()->get('field_layout_test.alter_regions') && isset($definitions['layout_onecol'])) {
+      $definitions['layout_onecol']->setRegions(['foo' => ['label' => 'Foo']]);
+    }
+  }
+
+}
diff --git a/core/modules/field_ui/field_ui.api.php b/core/modules/field_ui/field_ui.api.php
index 87f55ae6e1a5daba83f587268a2ad789cbb14e1a..23606315eaf04a3e02ad1bdd01fb7536cf9ce23e 100644
--- a/core/modules/field_ui/field_ui.api.php
+++ b/core/modules/field_ui/field_ui.api.php
@@ -1,5 +1,14 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\Core\Field\FormatterInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Field\WidgetInterface;
+
 /**
  * @file
  * Hooks provided by the Field UI module.
@@ -29,7 +38,7 @@
  *
  * @see \Drupal\field_ui\Form\EntityViewDisplayEditForm::thirdPartySettingsForm()
  */
-function hook_field_formatter_third_party_settings_form(\Drupal\Core\Field\FormatterInterface $plugin, \Drupal\Core\Field\FieldDefinitionInterface $field_definition, $view_mode, array $form, \Drupal\Core\Form\FormStateInterface $form_state) {
+function hook_field_formatter_third_party_settings_form(FormatterInterface $plugin, FieldDefinitionInterface $field_definition, $view_mode, array $form, FormStateInterface $form_state) {
   $element = [];
   // Add a 'my_setting' checkbox to the settings form for 'foo_formatter' field
   // formatters.
@@ -62,7 +71,7 @@ function hook_field_formatter_third_party_settings_form(\Drupal\Core\Field\Forma
  *
  * @see \Drupal\field_ui\Form\EntityFormDisplayEditForm::thirdPartySettingsForm()
  */
-function hook_field_widget_third_party_settings_form(\Drupal\Core\Field\WidgetInterface $plugin, \Drupal\Core\Field\FieldDefinitionInterface $field_definition, $form_mode, array $form, \Drupal\Core\Form\FormStateInterface $form_state) {
+function hook_field_widget_third_party_settings_form(WidgetInterface $plugin, FieldDefinitionInterface $field_definition, $form_mode, array $form, FormStateInterface $form_state) {
   $element = [];
   // Add a 'my_setting' checkbox to the settings form for 'foo_widget' field
   // widgets.
diff --git a/core/modules/field_ui/field_ui.module b/core/modules/field_ui/field_ui.module
index b2acbdc3e3dc7e85f6e75a254a91df7f3b22a68b..526468502516e7cbace7aa669e449b24b476e212 100644
--- a/core/modules/field_ui/field_ui.module
+++ b/core/modules/field_ui/field_ui.module
@@ -2,198 +2,10 @@
 
 /**
  * @file
- * Allows administrators to attach custom fields to fieldable types.
  */
 
-use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Entity\EntityViewModeInterface;
-use Drupal\Core\Entity\EntityFormModeInterface;
-use Drupal\Core\Url;
 use Drupal\field_ui\FieldUI;
-use Drupal\field_ui\Form\FieldConfigEditForm;
-use Drupal\field_ui\Form\FieldStorageConfigEditForm;
-use Drupal\field_ui\Plugin\Derivative\FieldUiLocalTask;
-
-/**
- * Implements hook_help().
- */
-function field_ui_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.field_ui':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Field UI module provides an administrative user interface (UI) for managing and displaying fields. Fields can be attached to most content entity sub-types. Different field types, widgets, and formatters are provided by the modules installed on your site, and managed by the Field module. For background information and terminology related to fields and entities, see the <a href=":field">Field module help page</a>. For more information about the Field UI, see the <a href=":field_ui_docs">online documentation for the Field UI module</a>.', [':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui_docs' => 'https://www.drupal.org/docs/8/core/modules/field-ui']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Creating a field') . '</dt>';
-      $output .= '<dd>' . t('On the <em>Manage fields</em> page for your entity type or sub-type, you can add, configure, and delete fields for that entity type or sub-type. Each field has a <em>machine name</em>, which is used internally to identify the field and must be unique across an entity type; once a field is created, you cannot change the machine name. Most fields have two types of settings. The field-level settings depend on the field type, and affect how the data in the field is stored. Once they are set, they can no longer be changed; examples include how many data values are allowed for the field and where files are stored. The sub-type-level settings are specific to each entity sub-type the field is used on, and they can be changed later; examples include the field label, help text, default value, and whether the field is required or not. You can return to these settings by choosing the <em>Edit</em> link for the field from the <em>Manage fields</em> page.');
-      $output .= '<dt>' . t('Re-using fields') . '</dt>';
-      $output .= '<dd>' . t('Once you have created a field, you can use it again in other sub-types of the same entity type. For instance, if you create a field for the article content type, you can also use it for the page content type, but you cannot use it for content blocks or taxonomy terms. If there are fields available for re-use, after clicking <em>Add field</em> from the <em>Manage fields</em> page, you will see a list of available fields for re-use. After selecting a field for re-use, you can configure the sub-type-level settings.') . '</dd>';
-      $output .= '<dt>' . t('Configuring field editing') . '</dt>';
-      $output .= '<dd>' . t('On the <em>Manage form display</em> page of your entity type or sub-type, you can configure how the field data is edited by default and in each form mode. If your entity type has multiple form modes (on most sites, most entities do not), you can toggle between the form modes at the top of the page, and you can toggle whether each form mode uses the default settings or custom settings in the <em>Custom display settings</em> section. For each field in each form mode, you can select the widget to use for editing; some widgets have additional configuration options, such as the size for a text field, and these can be edited using the Edit button (which looks like a wheel). You can also change the order of the fields on the form. You can exclude a field from a form by choosing <em>Hidden</em> from the widget drop-down list, or by dragging it into the <em>Disabled</em> section.') . '</dd>';
-      $output .= '<dt>' . t('Configuring field display') . '</dt>';
-      $output .= '<dd>' . t('On the <em>Manage display</em> page of your entity type or sub-type, you can configure how each field is displayed by default and in each view mode. If your entity type has multiple view modes, you can toggle between the view modes at the top of the page, and you can toggle whether each view mode uses the default settings or custom settings in the <em>Custom display settings</em> section. For each field in each view mode, you can choose whether and how to display the label of the field from the <em>Label</em> drop-down list. You can also select the formatter to use for display; some formatters have configuration options, which you can edit using the Edit button (which looks like a wheel). You can also change the display order of fields. You can exclude a field from a specific view mode by choosing <em>Hidden</em> from the formatter drop-down list, or by dragging it into the <em>Disabled</em> section.') . '</dd>';
-      $output .= '<dt>' . t('Configuring view and form modes') . '</dt>';
-      $output .= '<dd>' . t('You can add, edit, and delete view modes for entities on the <a href=":view_modes">View modes page</a>, and you can add, edit, and delete form modes for entities on the <a href=":form_modes">Form modes page</a>. Once you have defined a view mode or form mode for an entity type, it will be available on the Manage display or Manage form display page for each sub-type of that entity.', [':view_modes' => Url::fromRoute('entity.entity_view_mode.collection')->toString(), ':form_modes' => Url::fromRoute('entity.entity_form_mode.collection')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Listing fields') . '</dt>';
-      $output .= '<dd>' . t('There are two reports available that list the fields defined on your site. The <a href=":entity-list" title="Entities field list report">Entities</a> report lists all your fields, showing the field machine names, types, and the entity types or sub-types they are used on (each sub-type links to the Manage fields page). If the <a href=":views">Views</a> and <a href=":views-ui">Views UI</a> modules are installed, the <a href=":views-list" title="Used in views field list report">Used in views</a> report lists each field that is used in a view, with a link to edit that view.', [':entity-list' => Url::fromRoute('entity.field_storage_config.collection')->toString(), ':views-list' => (\Drupal::moduleHandler()->moduleExists('views_ui')) ? Url::fromRoute('views_ui.reports_fields')->toString() : '#', ':views' => (\Drupal::moduleHandler()->moduleExists('views')) ? Url::fromRoute('help.page', ['name' => 'views'])->toString() : '#', ':views-ui' => (\Drupal::moduleHandler()->moduleExists('views_ui')) ? Url::fromRoute('help.page', ['name' => 'views_ui'])->toString() : '#']) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'entity.field_storage_config.collection':
-      return '<p>' . t('This list shows all fields currently in use for easy reference.') . '</p>';
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function field_ui_theme(): array {
-  return [
-    'field_ui_table' => [
-      'variables' => [
-        'header' => NULL,
-        'rows' => NULL,
-        'footer' => NULL,
-        'attributes' => [],
-        'caption' => NULL,
-        'colgroups' => [],
-        'sticky' => FALSE,
-        'responsive' => TRUE,
-        'empty' => '',
-      ],
-    ],
-    // Provide a dedicated template for new storage options as their styling
-    // is quite different from a typical form element, so it works best to not
-    // include default form element classes.
-    'form_element__new_storage_type' => [
-      'base hook' => 'form_element',
-      'render element' => 'element',
-    ],
-  ];
-}
-
-/**
- * Implements hook_entity_type_build().
- */
-function field_ui_entity_type_build(array &$entity_types) {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  $entity_types['field_config']->setFormClass('edit', 'Drupal\field_ui\Form\FieldConfigEditForm');
-  $entity_types['field_config']->setFormClass('default', FieldConfigEditForm::class);
-  $entity_types['field_config']->setFormClass('delete', 'Drupal\field_ui\Form\FieldConfigDeleteForm');
-  $entity_types['field_config']->setListBuilderClass('Drupal\field_ui\FieldConfigListBuilder');
-
-  $entity_types['field_storage_config']->setFormClass('edit', 'Drupal\field_ui\Form\FieldStorageConfigEditForm');
-  $entity_types['field_storage_config']->setFormClass('default', FieldStorageConfigEditForm::class);
-  $entity_types['field_storage_config']->setListBuilderClass('Drupal\field_ui\FieldStorageConfigListBuilder');
-  $entity_types['field_storage_config']->setLinkTemplate('collection', '/admin/reports/fields');
-
-  $entity_types['entity_form_display']->setFormClass('edit', 'Drupal\field_ui\Form\EntityFormDisplayEditForm');
-  $entity_types['entity_view_display']->setFormClass('edit', 'Drupal\field_ui\Form\EntityViewDisplayEditForm');
-
-  $form_mode = $entity_types['entity_form_mode'];
-  $form_mode->setListBuilderClass('Drupal\field_ui\EntityFormModeListBuilder');
-  $form_mode->setFormClass('add', 'Drupal\field_ui\Form\EntityFormModeAddForm');
-  $form_mode->setFormClass('edit', 'Drupal\field_ui\Form\EntityDisplayModeEditForm');
-  $form_mode->setFormClass('delete', 'Drupal\field_ui\Form\EntityDisplayModeDeleteForm');
-  $form_mode->set('admin_permission', 'administer display modes');
-  $form_mode->setLinkTemplate('delete-form', '/admin/structure/display-modes/form/manage/{entity_form_mode}/delete');
-  $form_mode->setLinkTemplate('edit-form', '/admin/structure/display-modes/form/manage/{entity_form_mode}');
-  $form_mode->setLinkTemplate('add-form', '/admin/structure/display-modes/form/add/{entity_type_id}');
-  $form_mode->setLinkTemplate('collection', '/admin/structure/display-modes/form');
-
-  $view_mode = $entity_types['entity_view_mode'];
-  $view_mode->setListBuilderClass('Drupal\field_ui\EntityDisplayModeListBuilder');
-  $view_mode->setFormClass('add', 'Drupal\field_ui\Form\EntityDisplayModeAddForm');
-  $view_mode->setFormClass('edit', 'Drupal\field_ui\Form\EntityDisplayModeEditForm');
-  $view_mode->setFormClass('delete', 'Drupal\field_ui\Form\EntityDisplayModeDeleteForm');
-  $view_mode->set('admin_permission', 'administer display modes');
-  $view_mode->setLinkTemplate('delete-form', '/admin/structure/display-modes/view/manage/{entity_view_mode}/delete');
-  $view_mode->setLinkTemplate('edit-form', '/admin/structure/display-modes/view/manage/{entity_view_mode}');
-  $view_mode->setLinkTemplate('add-form', '/admin/structure/display-modes/view/add/{entity_type_id}');
-  $view_mode->setLinkTemplate('collection', '/admin/structure/display-modes/view');
-}
-
-/**
- * Implements hook_entity_bundle_create().
- */
-function field_ui_entity_bundle_create($entity_type, $bundle) {
-  // When a new bundle is created, the menu needs to be rebuilt to add our
-  // menu item tabs.
-  \Drupal::service('router.builder')->setRebuildNeeded();
-}
-
-/**
- * Implements hook_entity_operation().
- */
-function field_ui_entity_operation(EntityInterface $entity) {
-  $operations = [];
-  $info = $entity->getEntityType();
-  // Add manage fields and display links if this entity type is the bundle
-  // of another and that type has field UI enabled.
-  if (($bundle_of = $info->getBundleOf()) && \Drupal::entityTypeManager()->getDefinition($bundle_of)->get('field_ui_base_route')) {
-    $account = \Drupal::currentUser();
-    if ($account->hasPermission('administer ' . $bundle_of . ' fields')) {
-      $operations['manage-fields'] = [
-        'title' => t('Manage fields'),
-        'weight' => 15,
-        'url' => Url::fromRoute("entity.{$bundle_of}.field_ui_fields", [
-          $entity->getEntityTypeId() => $entity->id(),
-        ]),
-      ];
-    }
-    if ($account->hasPermission('administer ' . $bundle_of . ' form display')) {
-      $operations['manage-form-display'] = [
-        'title' => t('Manage form display'),
-        'weight' => 20,
-        'url' => Url::fromRoute("entity.entity_form_display.{$bundle_of}.default", [
-          $entity->getEntityTypeId() => $entity->id(),
-        ]),
-      ];
-    }
-    if ($account->hasPermission('administer ' . $bundle_of . ' display')) {
-      $operations['manage-display'] = [
-        'title' => t('Manage display'),
-        'weight' => 25,
-        'url' => Url::fromRoute("entity.entity_view_display.$bundle_of.default", [
-          $entity->getEntityTypeId() => $entity->id(),
-        ]),
-      ];
-    }
-  }
-
-  return $operations;
-}
-
-/**
- * Implements hook_entity_view_mode_presave().
- */
-function field_ui_entity_view_mode_presave(EntityViewModeInterface $view_mode) {
-  \Drupal::service('router.builder')->setRebuildNeeded();
-}
-
-/**
- * Implements hook_entity_form_mode_presave().
- */
-function field_ui_entity_form_mode_presave(EntityFormModeInterface $form_mode) {
-  \Drupal::service('router.builder')->setRebuildNeeded();
-}
-
-/**
- * Implements hook_entity_view_mode_delete().
- */
-function field_ui_entity_view_mode_delete(EntityViewModeInterface $view_mode) {
-  \Drupal::service('router.builder')->setRebuildNeeded();
-}
-
-/**
- * Implements hook_entity_form_mode_delete().
- */
-function field_ui_entity_form_mode_delete(EntityFormModeInterface $form_mode) {
-  \Drupal::service('router.builder')->setRebuildNeeded();
-}
 
 /**
  * Prepares variables for field UI overview table templates.
@@ -209,26 +21,6 @@ function template_preprocess_field_ui_table(&$variables) {
   template_preprocess_table($variables);
 }
 
-/**
- * Implements hook_local_tasks_alter().
- */
-function field_ui_local_tasks_alter(&$local_tasks) {
-  $container = \Drupal::getContainer();
-  $local_task = FieldUiLocalTask::create($container, 'field_ui.fields');
-  $local_task->alterLocalTasks($local_tasks);
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for 'field_ui_field_storage_add_form'.
- */
-function field_ui_form_field_ui_field_storage_add_form_alter(array &$form): void {
-  $optgroup = (string) t('Reference');
-  // Move the "Entity reference" option to the end of the list and rename it to
-  // "Other".
-  unset($form['add']['new_storage_type']['#options'][$optgroup]['entity_reference']);
-  $form['add']['new_storage_type']['#options'][$optgroup]['entity_reference'] = t('Other…');
-}
-
 /**
  * Implements hook_preprocess_HOOK().
  */
@@ -238,37 +30,6 @@ function field_ui_preprocess_form_element__new_storage_type(&$variables) {
   $variables['variant'] = $variables['element']['#variant'] ?? NULL;
 }
 
-/**
- * Implements hook_form_alter().
- *
- * Adds a button 'Save and manage fields' to forms.
- *
- * @see \Drupal\node\NodeTypeForm
- * @see \Drupal\comment\CommentTypeForm
- * @see \Drupal\media\MediaTypeForm
- * @see \Drupal\block_content\BlockContentTypeForm
- * @see field_ui_form_manage_field_form_submit()
- */
-function field_ui_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  $forms = [
-    'node_type_add_form',
-    'comment_type_add_form',
-    'media_type_add_form',
-    'block_content_type_add_form',
-  ];
-
-  if (!in_array($form_id, $forms)) {
-    return;
-  }
-  if ($form_state->getFormObject()->getEntity()->isNew()) {
-    $form['actions']['save_continue'] = $form['actions']['submit'];
-    unset($form['actions']['submit']['#button_type']);
-    $form['actions']['save_continue']['#value'] = t('Save and manage fields');
-    $form['actions']['save_continue']['#weight'] = $form['actions']['save_continue']['#weight'] - 5;
-    $form['actions']['save_continue']['#submit'][] = 'field_ui_form_manage_field_form_submit';
-  }
-}
-
 /**
  * Form submission handler for the 'Save and manage fields' button.
  *
diff --git a/core/modules/field_ui/src/Hook/FieldUiHooks.php b/core/modules/field_ui/src/Hook/FieldUiHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..e0d88fa48443cf6066d04acc3b6d8d1926d1200a
--- /dev/null
+++ b/core/modules/field_ui/src/Hook/FieldUiHooks.php
@@ -0,0 +1,274 @@
+<?php
+
+namespace Drupal\field_ui\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\field_ui\Plugin\Derivative\FieldUiLocalTask;
+use Drupal\Core\Entity\EntityFormModeInterface;
+use Drupal\Core\Entity\EntityViewModeInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\field_ui\Form\FieldStorageConfigEditForm;
+use Drupal\field_ui\Form\FieldConfigEditForm;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for field_ui.
+ */
+class FieldUiHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.field_ui':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Field UI module provides an administrative user interface (UI) for managing and displaying fields. Fields can be attached to most content entity sub-types. Different field types, widgets, and formatters are provided by the modules installed on your site, and managed by the Field module. For background information and terminology related to fields and entities, see the <a href=":field">Field module help page</a>. For more information about the Field UI, see the <a href=":field_ui_docs">online documentation for the Field UI module</a>.', [
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+          ':field_ui_docs' => 'https://www.drupal.org/docs/8/core/modules/field-ui',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Creating a field') . '</dt>';
+        $output .= '<dd>' . t('On the <em>Manage fields</em> page for your entity type or sub-type, you can add, configure, and delete fields for that entity type or sub-type. Each field has a <em>machine name</em>, which is used internally to identify the field and must be unique across an entity type; once a field is created, you cannot change the machine name. Most fields have two types of settings. The field-level settings depend on the field type, and affect how the data in the field is stored. Once they are set, they can no longer be changed; examples include how many data values are allowed for the field and where files are stored. The sub-type-level settings are specific to each entity sub-type the field is used on, and they can be changed later; examples include the field label, help text, default value, and whether the field is required or not. You can return to these settings by choosing the <em>Edit</em> link for the field from the <em>Manage fields</em> page.');
+        $output .= '<dt>' . t('Re-using fields') . '</dt>';
+        $output .= '<dd>' . t('Once you have created a field, you can use it again in other sub-types of the same entity type. For instance, if you create a field for the article content type, you can also use it for the page content type, but you cannot use it for content blocks or taxonomy terms. If there are fields available for re-use, after clicking <em>Add field</em> from the <em>Manage fields</em> page, you will see a list of available fields for re-use. After selecting a field for re-use, you can configure the sub-type-level settings.') . '</dd>';
+        $output .= '<dt>' . t('Configuring field editing') . '</dt>';
+        $output .= '<dd>' . t('On the <em>Manage form display</em> page of your entity type or sub-type, you can configure how the field data is edited by default and in each form mode. If your entity type has multiple form modes (on most sites, most entities do not), you can toggle between the form modes at the top of the page, and you can toggle whether each form mode uses the default settings or custom settings in the <em>Custom display settings</em> section. For each field in each form mode, you can select the widget to use for editing; some widgets have additional configuration options, such as the size for a text field, and these can be edited using the Edit button (which looks like a wheel). You can also change the order of the fields on the form. You can exclude a field from a form by choosing <em>Hidden</em> from the widget drop-down list, or by dragging it into the <em>Disabled</em> section.') . '</dd>';
+        $output .= '<dt>' . t('Configuring field display') . '</dt>';
+        $output .= '<dd>' . t('On the <em>Manage display</em> page of your entity type or sub-type, you can configure how each field is displayed by default and in each view mode. If your entity type has multiple view modes, you can toggle between the view modes at the top of the page, and you can toggle whether each view mode uses the default settings or custom settings in the <em>Custom display settings</em> section. For each field in each view mode, you can choose whether and how to display the label of the field from the <em>Label</em> drop-down list. You can also select the formatter to use for display; some formatters have configuration options, which you can edit using the Edit button (which looks like a wheel). You can also change the display order of fields. You can exclude a field from a specific view mode by choosing <em>Hidden</em> from the formatter drop-down list, or by dragging it into the <em>Disabled</em> section.') . '</dd>';
+        $output .= '<dt>' . t('Configuring view and form modes') . '</dt>';
+        $output .= '<dd>' . t('You can add, edit, and delete view modes for entities on the <a href=":view_modes">View modes page</a>, and you can add, edit, and delete form modes for entities on the <a href=":form_modes">Form modes page</a>. Once you have defined a view mode or form mode for an entity type, it will be available on the Manage display or Manage form display page for each sub-type of that entity.', [
+          ':view_modes' => Url::fromRoute('entity.entity_view_mode.collection')->toString(),
+          ':form_modes' => Url::fromRoute('entity.entity_form_mode.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Listing fields') . '</dt>';
+        $output .= '<dd>' . t('There are two reports available that list the fields defined on your site. The <a href=":entity-list" title="Entities field list report">Entities</a> report lists all your fields, showing the field machine names, types, and the entity types or sub-types they are used on (each sub-type links to the Manage fields page). If the <a href=":views">Views</a> and <a href=":views-ui">Views UI</a> modules are installed, the <a href=":views-list" title="Used in views field list report">Used in views</a> report lists each field that is used in a view, with a link to edit that view.', [
+          ':entity-list' => Url::fromRoute('entity.field_storage_config.collection')->toString(),
+          ':views-list' => \Drupal::moduleHandler()->moduleExists('views_ui') ? Url::fromRoute('views_ui.reports_fields')->toString() : '#',
+          ':views' => \Drupal::moduleHandler()->moduleExists('views') ? Url::fromRoute('help.page', [
+            'name' => 'views',
+          ])->toString() : '#',
+          ':views-ui' => \Drupal::moduleHandler()->moduleExists('views_ui') ? Url::fromRoute('help.page', [
+            'name' => 'views_ui',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'entity.field_storage_config.collection':
+        return '<p>' . t('This list shows all fields currently in use for easy reference.') . '</p>';
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'field_ui_table' => [
+        'variables' => [
+          'header' => NULL,
+          'rows' => NULL,
+          'footer' => NULL,
+          'attributes' => [],
+          'caption' => NULL,
+          'colgroups' => [],
+          'sticky' => FALSE,
+          'responsive' => TRUE,
+          'empty' => '',
+        ],
+      ],
+          // Provide a dedicated template for new storage options as their styling
+          // is quite different from a typical form element, so it works best to not
+          // include default form element classes.
+      'form_element__new_storage_type' => [
+        'base hook' => 'form_element',
+        'render element' => 'element',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public function entityTypeBuild(array &$entity_types) {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    $entity_types['field_config']->setFormClass('edit', 'Drupal\field_ui\Form\FieldConfigEditForm');
+    $entity_types['field_config']->setFormClass('default', FieldConfigEditForm::class);
+    $entity_types['field_config']->setFormClass('delete', 'Drupal\field_ui\Form\FieldConfigDeleteForm');
+    $entity_types['field_config']->setListBuilderClass('Drupal\field_ui\FieldConfigListBuilder');
+    $entity_types['field_storage_config']->setFormClass('edit', 'Drupal\field_ui\Form\FieldStorageConfigEditForm');
+    $entity_types['field_storage_config']->setFormClass('default', FieldStorageConfigEditForm::class);
+    $entity_types['field_storage_config']->setListBuilderClass('Drupal\field_ui\FieldStorageConfigListBuilder');
+    $entity_types['field_storage_config']->setLinkTemplate('collection', '/admin/reports/fields');
+    $entity_types['entity_form_display']->setFormClass('edit', 'Drupal\field_ui\Form\EntityFormDisplayEditForm');
+    $entity_types['entity_view_display']->setFormClass('edit', 'Drupal\field_ui\Form\EntityViewDisplayEditForm');
+    $form_mode = $entity_types['entity_form_mode'];
+    $form_mode->setListBuilderClass('Drupal\field_ui\EntityFormModeListBuilder');
+    $form_mode->setFormClass('add', 'Drupal\field_ui\Form\EntityFormModeAddForm');
+    $form_mode->setFormClass('edit', 'Drupal\field_ui\Form\EntityDisplayModeEditForm');
+    $form_mode->setFormClass('delete', 'Drupal\field_ui\Form\EntityDisplayModeDeleteForm');
+    $form_mode->set('admin_permission', 'administer display modes');
+    $form_mode->setLinkTemplate('delete-form', '/admin/structure/display-modes/form/manage/{entity_form_mode}/delete');
+    $form_mode->setLinkTemplate('edit-form', '/admin/structure/display-modes/form/manage/{entity_form_mode}');
+    $form_mode->setLinkTemplate('add-form', '/admin/structure/display-modes/form/add/{entity_type_id}');
+    $form_mode->setLinkTemplate('collection', '/admin/structure/display-modes/form');
+    $view_mode = $entity_types['entity_view_mode'];
+    $view_mode->setListBuilderClass('Drupal\field_ui\EntityDisplayModeListBuilder');
+    $view_mode->setFormClass('add', 'Drupal\field_ui\Form\EntityDisplayModeAddForm');
+    $view_mode->setFormClass('edit', 'Drupal\field_ui\Form\EntityDisplayModeEditForm');
+    $view_mode->setFormClass('delete', 'Drupal\field_ui\Form\EntityDisplayModeDeleteForm');
+    $view_mode->set('admin_permission', 'administer display modes');
+    $view_mode->setLinkTemplate('delete-form', '/admin/structure/display-modes/view/manage/{entity_view_mode}/delete');
+    $view_mode->setLinkTemplate('edit-form', '/admin/structure/display-modes/view/manage/{entity_view_mode}');
+    $view_mode->setLinkTemplate('add-form', '/admin/structure/display-modes/view/add/{entity_type_id}');
+    $view_mode->setLinkTemplate('collection', '/admin/structure/display-modes/view');
+  }
+
+  /**
+   * Implements hook_entity_bundle_create().
+   */
+  #[Hook('entity_bundle_create')]
+  public function entityBundleCreate($entity_type, $bundle) {
+    // When a new bundle is created, the menu needs to be rebuilt to add our
+    // menu item tabs.
+    \Drupal::service('router.builder')->setRebuildNeeded();
+  }
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $entity) {
+    $operations = [];
+    $info = $entity->getEntityType();
+    // Add manage fields and display links if this entity type is the bundle
+    // of another and that type has field UI enabled.
+    if (($bundle_of = $info->getBundleOf()) && \Drupal::entityTypeManager()->getDefinition($bundle_of)->get('field_ui_base_route')) {
+      $account = \Drupal::currentUser();
+      if ($account->hasPermission('administer ' . $bundle_of . ' fields')) {
+        $operations['manage-fields'] = [
+          'title' => t('Manage fields'),
+          'weight' => 15,
+          'url' => Url::fromRoute("entity.{$bundle_of}.field_ui_fields", [
+            $entity->getEntityTypeId() => $entity->id(),
+          ]),
+        ];
+      }
+      if ($account->hasPermission('administer ' . $bundle_of . ' form display')) {
+        $operations['manage-form-display'] = [
+          'title' => t('Manage form display'),
+          'weight' => 20,
+          'url' => Url::fromRoute("entity.entity_form_display.{$bundle_of}.default", [
+            $entity->getEntityTypeId() => $entity->id(),
+          ]),
+        ];
+      }
+      if ($account->hasPermission('administer ' . $bundle_of . ' display')) {
+        $operations['manage-display'] = [
+          'title' => t('Manage display'),
+          'weight' => 25,
+          'url' => Url::fromRoute("entity.entity_view_display.{$bundle_of}.default", [
+            $entity->getEntityTypeId() => $entity->id(),
+          ]),
+        ];
+      }
+    }
+    return $operations;
+  }
+
+  /**
+   * Implements hook_entity_view_mode_presave().
+   */
+  #[Hook('entity_view_mode_presave')]
+  public function entityViewModePresave(EntityViewModeInterface $view_mode) {
+    \Drupal::service('router.builder')->setRebuildNeeded();
+  }
+
+  /**
+   * Implements hook_entity_form_mode_presave().
+   */
+  #[Hook('entity_form_mode_presave')]
+  public function entityFormModePresave(EntityFormModeInterface $form_mode) {
+    \Drupal::service('router.builder')->setRebuildNeeded();
+  }
+
+  /**
+   * Implements hook_entity_view_mode_delete().
+   */
+  #[Hook('entity_view_mode_delete')]
+  public function entityViewModeDelete(EntityViewModeInterface $view_mode) {
+    \Drupal::service('router.builder')->setRebuildNeeded();
+  }
+
+  /**
+   * Implements hook_entity_form_mode_delete().
+   */
+  #[Hook('entity_form_mode_delete')]
+  public function entityFormModeDelete(EntityFormModeInterface $form_mode) {
+    \Drupal::service('router.builder')->setRebuildNeeded();
+  }
+
+  /**
+   * Implements hook_local_tasks_alter().
+   */
+  #[Hook('local_tasks_alter')]
+  public function localTasksAlter(&$local_tasks) {
+    $container = \Drupal::getContainer();
+    $local_task = FieldUiLocalTask::create($container, 'field_ui.fields');
+    $local_task->alterLocalTasks($local_tasks);
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for 'field_ui_field_storage_add_form'.
+   */
+  #[Hook('form_field_ui_field_storage_add_form_alter')]
+  public function formFieldUiFieldStorageAddFormAlter(array &$form) : void {
+    $optgroup = (string) t('Reference');
+    // Move the "Entity reference" option to the end of the list and rename it to
+    // "Other".
+    unset($form['add']['new_storage_type']['#options'][$optgroup]['entity_reference']);
+    $form['add']['new_storage_type']['#options'][$optgroup]['entity_reference'] = t('Other…');
+  }
+
+  /**
+   * Implements hook_form_alter().
+   *
+   * Adds a button 'Save and manage fields' to forms.
+   *
+   * @see \Drupal\node\NodeTypeForm
+   * @see \Drupal\comment\CommentTypeForm
+   * @see \Drupal\media\MediaTypeForm
+   * @see \Drupal\block_content\BlockContentTypeForm
+   * @see field_ui_form_manage_field_form_submit()
+   */
+  #[Hook('form_alter')]
+  public function formAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    $forms = [
+      'node_type_add_form',
+      'comment_type_add_form',
+      'media_type_add_form',
+      'block_content_type_add_form',
+    ];
+    if (!in_array($form_id, $forms)) {
+      return;
+    }
+    if ($form_state->getFormObject()->getEntity()->isNew()) {
+      $form['actions']['save_continue'] = $form['actions']['submit'];
+      unset($form['actions']['submit']['#button_type']);
+      $form['actions']['save_continue']['#value'] = t('Save and manage fields');
+      $form['actions']['save_continue']['#weight'] = $form['actions']['save_continue']['#weight'] - 5;
+      $form['actions']['save_continue']['#submit'][] = 'field_ui_form_manage_field_form_submit';
+    }
+  }
+
+}
diff --git a/core/modules/field_ui/tests/modules/field_ui_test/field_ui_test.module b/core/modules/field_ui/tests/modules/field_ui_test/field_ui_test.module
index 0e30af4ba30aa6ffc7a6b4e6c4694d1aef79ef91..5da81c7ab5f72432b9d388fd47981aa3a0e8c7fe 100644
--- a/core/modules/field_ui/tests/modules/field_ui_test/field_ui_test.module
+++ b/core/modules/field_ui/tests/modules/field_ui_test/field_ui_test.module
@@ -7,64 +7,6 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Render\Element;
-use Drupal\Core\Field\FieldConfigInterface;
-
-/**
- * Implements hook_ENTITY_TYPE_access().
- */
-function field_ui_test_field_config_access(FieldConfigInterface $field) {
-  return AccessResult::forbiddenIf($field->getName() == 'highlander');
-}
-
-/**
- * Implements hook_form_FORM_BASE_ID_alter().
- */
-function field_ui_test_form_entity_view_display_edit_form_alter(&$form, FormStateInterface $form_state): void {
-  $table = &$form['fields'];
-
-  foreach (Element::children($table) as $name) {
-    $table[$name]['parent_wrapper']['parent']['#options'] = ['indent' => 'Indent'];
-    $table[$name]['parent_wrapper']['parent']['#default_value'] = 'indent';
-  }
-
-  $table['indent'] = [
-    '#attributes' => ['class' => ['draggable', 'field-group'], 'id' => 'indent-id'],
-    '#row_type' => 'group',
-    '#region_callback' => 'field_ui_test_region_callback',
-    '#js_settings' => ['rowHandler' => 'group'],
-    'human_name' => [
-      '#markup' => 'Indent',
-      '#prefix' => '<span class="group-label">',
-      '#suffix' => '</span>',
-    ],
-    'weight' => [
-      '#type' => 'textfield',
-      '#default_value' => 0,
-      '#size' => 3,
-      '#attributes' => ['class' => ['field-weight']],
-    ],
-    'parent_wrapper' => [
-      'parent' => [
-        '#type' => 'select',
-        '#options' => ['indent' => 'Indent'],
-        '#empty_value' => '',
-        '#default_value' => '',
-        '#attributes' => ['class' => ['field-parent']],
-        '#parents' => ['fields', 'indent', 'parent'],
-      ],
-      'hidden_name' => [
-        '#type' => 'hidden',
-        '#default_value' => 'indent',
-        '#attributes' => ['class' => ['field-name']],
-      ],
-    ],
-  ];
-
-}
-
 function field_ui_test_region_callback($row) {
   return 'content';
 }
diff --git a/core/modules/field_ui/tests/modules/field_ui_test/src/Hook/FieldUiTestHooks.php b/core/modules/field_ui/tests/modules/field_ui_test/src/Hook/FieldUiTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..9aa01810a0872bee1c2ffcfcac5ef90c0d6fed87
--- /dev/null
+++ b/core/modules/field_ui/tests/modules/field_ui_test/src/Hook/FieldUiTestHooks.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\field_ui_test\Hook;
+
+use Drupal\Core\Render\Element;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Field\FieldConfigInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for field_ui_test.
+ */
+class FieldUiTestHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_access().
+   */
+  #[Hook('field_config_access')]
+  public function fieldConfigAccess(FieldConfigInterface $field) {
+    return AccessResult::forbiddenIf($field->getName() == 'highlander');
+  }
+
+  /**
+   * Implements hook_form_FORM_BASE_ID_alter().
+   */
+  #[Hook('form_entity_view_display_edit_form_alter')]
+  public function formEntityViewDisplayEditFormAlter(&$form, FormStateInterface $form_state) : void {
+    $table =& $form['fields'];
+    foreach (Element::children($table) as $name) {
+      $table[$name]['parent_wrapper']['parent']['#options'] = ['indent' => 'Indent'];
+      $table[$name]['parent_wrapper']['parent']['#default_value'] = 'indent';
+    }
+    $table['indent'] = [
+      '#attributes' => [
+        'class' => [
+          'draggable',
+          'field-group',
+        ],
+        'id' => 'indent-id',
+      ],
+      '#row_type' => 'group',
+      '#region_callback' => 'field_ui_test_region_callback',
+      '#js_settings' => [
+        'rowHandler' => 'group',
+      ],
+      'human_name' => [
+        '#markup' => 'Indent',
+        '#prefix' => '<span class="group-label">',
+        '#suffix' => '</span>',
+      ],
+      'weight' => [
+        '#type' => 'textfield',
+        '#default_value' => 0,
+        '#size' => 3,
+        '#attributes' => [
+          'class' => [
+            'field-weight',
+          ],
+        ],
+      ],
+      'parent_wrapper' => [
+        'parent' => [
+          '#type' => 'select',
+          '#options' => [
+            'indent' => 'Indent',
+          ],
+          '#empty_value' => '',
+          '#default_value' => '',
+          '#attributes' => [
+            'class' => [
+              'field-parent',
+            ],
+          ],
+          '#parents' => [
+            'fields',
+            'indent',
+            'parent',
+          ],
+        ],
+        'hidden_name' => [
+          '#type' => 'hidden',
+          '#default_value' => 'indent',
+          '#attributes' => [
+            'class' => [
+              'field-name',
+            ],
+          ],
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/field_ui/tests/modules/field_ui_test_deprecated/field_ui_test_deprecated.module b/core/modules/field_ui/tests/modules/field_ui_test_deprecated/field_ui_test_deprecated.module
deleted file mode 100644
index 1516949a516103c493c7ecfe60da268017e1181b..0000000000000000000000000000000000000000
--- a/core/modules/field_ui/tests/modules/field_ui_test_deprecated/field_ui_test_deprecated.module
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-/**
- * @file
- * Field UI test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\field\FieldStorageConfigInterface;
-use Drupal\field_ui\Form\FieldStorageConfigEditForm;
-
-/**
- * Implements hook_form_FORM_ID_alter() for field_storage_config_edit_form.
- */
-function field_ui_test_deprecated_form_field_storage_config_edit_form_alter(&$form, FormStateInterface $form_state): void {
-  if (!($form_state->getFormObject() instanceof FieldStorageConfigEditForm)) {
-    throw new \LogicException('field_storage_config_edit_form() expects to get access to the field storage config entity edit form.');
-  }
-  if (!($form_state->getFormObject()->getEntity() instanceof FieldStorageConfigInterface)) {
-    throw new \LogicException('field_storage_config_edit_form() expects to get access to the field storage config entity.');
-  }
-  if (!isset($form['cardinality_container']['cardinality'])) {
-    throw new \LogicException('field_storage_config_edit_form() expects to that the cardinality container with the cardinality form element exists.');
-  }
-
-  $form['cardinality_container']['hello'] = [
-    '#markup' => 'Greetings from the field_storage_config_edit_form() alter.',
-  ];
-}
diff --git a/core/modules/field_ui/tests/modules/field_ui_test_deprecated/src/Hook/FieldUiTestDeprecatedHooks.php b/core/modules/field_ui/tests/modules/field_ui_test_deprecated/src/Hook/FieldUiTestDeprecatedHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..b346894693a3ec5028ad7e70e8a9a21a8f115134
--- /dev/null
+++ b/core/modules/field_ui/tests/modules/field_ui_test_deprecated/src/Hook/FieldUiTestDeprecatedHooks.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\field_ui_test_deprecated\Hook;
+
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\field_ui\Form\FieldStorageConfigEditForm;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for field_ui_test_deprecated.
+ */
+class FieldUiTestDeprecatedHooks {
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for field_storage_config_edit_form.
+   */
+  #[Hook('form_field_storage_config_edit_form_alter')]
+  public function formFieldStorageConfigEditFormAlter(&$form, FormStateInterface $form_state) : void {
+    if (!$form_state->getFormObject() instanceof FieldStorageConfigEditForm) {
+      throw new \LogicException('field_storage_config_edit_form() expects to get access to the field storage config entity edit form.');
+    }
+    if (!$form_state->getFormObject()->getEntity() instanceof FieldStorageConfigInterface) {
+      throw new \LogicException('field_storage_config_edit_form() expects to get access to the field storage config entity.');
+    }
+    if (!isset($form['cardinality_container']['cardinality'])) {
+      throw new \LogicException('field_storage_config_edit_form() expects to that the cardinality container with the cardinality form element exists.');
+    }
+    $form['cardinality_container']['hello'] = ['#markup' => 'Greetings from the field_storage_config_edit_form() alter.'];
+  }
+
+}
diff --git a/core/modules/file/file.api.php b/core/modules/file/file.api.php
index 25d682a0e82e894c3f8a0643d1f2b766852c1783..fe7aa535ab620613d5b594c194caf59fbf36d8ad 100644
--- a/core/modules/file/file.api.php
+++ b/core/modules/file/file.api.php
@@ -1,5 +1,11 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\file\FileInterface;
+
 /**
  * @file
  * Hooks for file module.
@@ -69,7 +75,7 @@
  *
  * @see \Drupal\file\FileRepositoryInterface::copy()
  */
-function hook_file_copy(\Drupal\file\FileInterface $file, \Drupal\file\FileInterface $source) {
+function hook_file_copy(FileInterface $file, FileInterface $source) {
   // Make sure that the file name starts with the owner's user name.
   if (!str_starts_with($file->getFilename(), $file->getOwner()->name)) {
     $file->setFilename($file->getOwner()->name . '_' . $file->getFilename());
@@ -89,7 +95,7 @@ function hook_file_copy(\Drupal\file\FileInterface $file, \Drupal\file\FileInter
  *
  * @see \Drupal\file\FileRepositoryInterface::move()
  */
-function hook_file_move(\Drupal\file\FileInterface $file, \Drupal\file\FileInterface $source) {
+function hook_file_move(FileInterface $file, FileInterface $source) {
   // Make sure that the file name starts with the owner's user name.
   if (!str_starts_with($file->getFilename(), $file->getOwner()->name)) {
     $file->setFilename($file->getOwner()->name . '_' . $file->getFilename());
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 664b807c62083e14bee39f02d3d0e158c8aed777..9ade447d83eb4fe9bbb3815f16b294eb1e38553b 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -2,11 +2,9 @@
 
 /**
  * @file
- * Defines a "managed_file" Form API field and a "file" field for Field module.
  */
 
 use Drupal\Component\Utility\NestedArray;
-use Drupal\Core\Datetime\Entity\DateFormat;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldFilteredMarkup;
@@ -20,55 +18,13 @@
 use Drupal\Core\Link;
 use Drupal\Core\Lock\LockAcquiringException;
 use Drupal\Core\Messenger\MessengerInterface;
-use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Render\Element;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\StringTranslation\ByteSizeMarkup;
 use Drupal\Core\Template\Attribute;
-use Drupal\Core\Url;
-use Drupal\file\Entity\File;
 use Drupal\file\FileInterface;
 use Drupal\file\IconMimeTypes;
 use Drupal\file\Upload\FormUploadedFile;
 
-// cspell:ignore widthx
-
-/**
- * Implements hook_help().
- */
-function file_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.file':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The File module allows you to create fields that contain files. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":file_documentation">online documentation for the File module</a>.', [':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':file_documentation' => 'https://www.drupal.org/documentation/modules/file']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Managing and displaying file fields') . '</dt>';
-      $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the file field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Allowing file extensions') . '</dt>';
-      $output .= '<dd>' . t('In the field settings, you can define the allowed file extensions (for example <em>pdf docx psd</em>) for the files that will be uploaded with the file field.') . '</dd>';
-      $output .= '<dt>' . t('Storing files') . '</dt>';
-      $output .= '<dd>' . t('Uploaded files can either be stored as <em>public</em> or <em>private</em>, depending on the <a href=":file-system">File system settings</a>. For more information, see the <a href=":system-help">System module help page</a>.', [':file-system' => Url::fromRoute('system.file_system_settings')->toString(), ':system-help' => Url::fromRoute('help.page', ['name' => 'system'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Restricting the maximum file size') . '</dt>';
-      $output .= '<dd>' . t('The maximum file size that users can upload is limited by PHP settings of the server, but you can restrict by entering the desired value as the <em>Maximum upload size</em> setting. The maximum file size is automatically displayed to users in the help text of the file field.') . '</dd>';
-      $output .= '<dt>' . t('Displaying files and descriptions') . '<dt>';
-      $output .= '<dd>' . t('In the field settings, you can allow users to toggle whether individual files are displayed. In the display settings, you can then choose one of the following formats: <ul><li><em>Generic file</em> displays links to the files and adds icons that symbolize the file extensions. If <em>descriptions</em> are enabled and have been submitted, then the description is displayed instead of the file name.</li><li><em>URL to file</em> displays the full path to the file as plain text.</li><li><em>Table of files</em> lists links to the files and the file sizes in a table.</li><li><em>RSS enclosure</em> only displays the first file, and only in a RSS feed, formatted according to the RSS 2.0 syntax for enclosures.</li></ul> A file can still be linked to directly by its URI even if it is not displayed.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_field_widget_info_alter().
- */
-function file_field_widget_info_alter(array &$info) {
-  // Allows using the 'uri' widget for the 'file_uri' field type, which uses it
-  // as the default widget.
-  // @see \Drupal\file\Plugin\Field\FieldType\FileUriItem
-  $info['uri']['field_types'][] = 'file_uri';
-}
-
 /**
  * Examines a file entity and returns appropriate content headers for download.
  *
@@ -87,122 +43,6 @@ function file_get_content_headers(FileInterface $file) {
   ];
 }
 
-/**
- * Implements hook_theme().
- */
-function file_theme(): array {
-  return [
-    // From file.module.
-    'file_link' => [
-      'variables' => ['file' => NULL, 'description' => NULL, 'attributes' => []],
-    ],
-    'file_managed_file' => [
-      'render element' => 'element',
-    ],
-    'file_audio' => [
-      'variables' => ['files' => [], 'attributes' => NULL],
-    ],
-    'file_video' => [
-      'variables' => ['files' => [], 'attributes' => NULL],
-    ],
-    'file_widget_multiple' => [
-      'render element' => 'element',
-    ],
-    'file_upload_help' => [
-      'variables' => ['description' => NULL, 'upload_validators' => NULL, 'cardinality' => NULL],
-    ],
-  ];
-}
-
-/**
- * Implements hook_file_download().
- */
-function file_file_download($uri) {
-  // Get the file record based on the URI. If not in the database just return.
-  /** @var \Drupal\file\FileRepositoryInterface $file_repository */
-  $file_repository = \Drupal::service('file.repository');
-  $file = $file_repository->loadByUri($uri);
-  if (!$file) {
-    return;
-  }
-
-  // Find out if a temporary file is still used in the system.
-  if ($file->isTemporary()) {
-    $usage = \Drupal::service('file.usage')->listUsage($file);
-    if (empty($usage) && $file->getOwnerId() != \Drupal::currentUser()->id()) {
-      // Deny access to temporary files without usage that are not owned by the
-      // same user. This prevents the security issue that a private file that
-      // was protected by field permissions becomes available after its usage
-      // was removed and before it is actually deleted from the file system.
-      // Modules that depend on this behavior should make the file permanent
-      // instead.
-      return -1;
-    }
-  }
-
-  // Find out which (if any) fields of this type contain the file.
-  $references = file_get_file_references($file, NULL, EntityStorageInterface::FIELD_LOAD_CURRENT, NULL);
-
-  // Stop processing if there are no references in order to avoid returning
-  // headers for files controlled by other modules. Make an exception for
-  // temporary files where the host entity has not yet been saved (for example,
-  // an image preview on a node/add form) in which case, allow download by the
-  // file's owner.
-  if (empty($references) && ($file->isPermanent() || $file->getOwnerId() != \Drupal::currentUser()->id())) {
-    return;
-  }
-
-  if (!$file->access('download')) {
-    return -1;
-  }
-
-  // Access is granted.
-  $headers = file_get_content_headers($file);
-  return $headers;
-}
-
-/**
- * Implements hook_cron().
- */
-function file_cron() {
-  $age = \Drupal::config('system.file')->get('temporary_maximum_age');
-  $file_storage = \Drupal::entityTypeManager()->getStorage('file');
-
-  /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
-  $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
-
-  // Only delete temporary files if older than $age. Note that automatic cleanup
-  // is disabled if $age set to 0.
-  if ($age) {
-    $fids = Drupal::entityQuery('file')
-      ->accessCheck(FALSE)
-      ->condition('status', FileInterface::STATUS_PERMANENT, '<>')
-      ->condition('changed', \Drupal::time()->getRequestTime() - $age, '<')
-      ->range(0, 100)
-      ->execute();
-    $files = $file_storage->loadMultiple($fids);
-    foreach ($files as $file) {
-      $references = \Drupal::service('file.usage')->listUsage($file);
-      if (empty($references)) {
-        if (!file_exists($file->getFileUri())) {
-          if (!$stream_wrapper_manager->isValidUri($file->getFileUri())) {
-            \Drupal::logger('file system')->warning('Temporary file "%path" that was deleted during garbage collection did not exist on the filesystem. This could be caused by a missing stream wrapper.', ['%path' => $file->getFileUri()]);
-          }
-          else {
-            \Drupal::logger('file system')->warning('Temporary file "%path" that was deleted during garbage collection did not exist on the filesystem.', ['%path' => $file->getFileUri()]);
-          }
-        }
-        // Delete the file entity. If the file does not exist, this will
-        // generate a second notice in the watchdog.
-        $file->delete();
-      }
-      else {
-        \Drupal::logger('file system')->info('Did not delete temporary file "%path" during garbage collection because it is in use by the following modules: %modules.', ['%path' => $file->getFileUri(), '%modules' => implode(', ', array_keys($references))]);
-      }
-    }
-  }
-}
-
 /**
  * Saves form file uploads.
  *
@@ -438,172 +278,6 @@ function file_save_upload($form_field_name, $validators = [], $destination = FAL
   return isset($delta) ? $files[$delta] : $files;
 }
 
-/**
- * Implements hook_ENTITY_TYPE_predelete() for file entities.
- */
-function file_file_predelete(File $file) {
-  // @todo Remove references to a file that is in-use.
-}
-
-/**
- * Implements hook_tokens().
- */
-function file_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
-  $token_service = \Drupal::token();
-
-  $url_options = ['absolute' => TRUE];
-  if (isset($options['langcode'])) {
-    $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
-    $langcode = $options['langcode'];
-  }
-  else {
-    $langcode = NULL;
-  }
-
-  $replacements = [];
-
-  if ($type == 'file' && !empty($data['file'])) {
-    /** @var \Drupal\file\FileInterface $file */
-    $file = $data['file'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Basic keys and values.
-        case 'fid':
-          $replacements[$original] = $file->id();
-          break;
-
-        case 'uuid':
-          $replacements[$original] = $file->uuid();
-          break;
-
-        // Essential file data
-        case 'name':
-          $replacements[$original] = $file->getFilename();
-          break;
-
-        case 'path':
-          $replacements[$original] = $file->getFileUri();
-          break;
-
-        case 'mime':
-          $replacements[$original] = $file->getMimeType();
-          break;
-
-        case 'size':
-          $replacements[$original] = ByteSizeMarkup::create($file->getSize());
-          break;
-
-        case 'url':
-          // Ideally, this would use return a relative URL, but because tokens
-          // are also often used in emails, it's better to keep absolute file
-          // URLs. The 'url.site' cache context is associated to ensure the
-          // correct absolute URL is used in case of a multisite setup.
-          $replacements[$original] = $file->createFileUrl(FALSE);
-          $bubbleable_metadata->addCacheContexts(['url.site']);
-          break;
-
-        // These tokens are default variations on the chained tokens handled below.
-        case 'created':
-          $date_format = DateFormat::load('medium');
-          $bubbleable_metadata->addCacheableDependency($date_format);
-          $replacements[$original] = \Drupal::service('date.formatter')->format($file->getCreatedTime(), 'medium', '', NULL, $langcode);
-          break;
-
-        case 'changed':
-          $date_format = DateFormat::load('medium');
-          $bubbleable_metadata = $bubbleable_metadata->addCacheableDependency($date_format);
-          $replacements[$original] = \Drupal::service('date.formatter')->format($file->getChangedTime(), 'medium', '', NULL, $langcode);
-          break;
-
-        case 'owner':
-          $owner = $file->getOwner();
-          $bubbleable_metadata->addCacheableDependency($owner);
-          $name = $owner->label();
-          $replacements[$original] = $name;
-          break;
-      }
-    }
-
-    if ($date_tokens = $token_service->findWithPrefix($tokens, 'created')) {
-      $replacements += $token_service->generate('date', $date_tokens, ['date' => $file->getCreatedTime()], $options, $bubbleable_metadata);
-    }
-
-    if ($date_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
-      $replacements += $token_service->generate('date', $date_tokens, ['date' => $file->getChangedTime()], $options, $bubbleable_metadata);
-    }
-
-    if (($owner_tokens = $token_service->findWithPrefix($tokens, 'owner')) && $file->getOwner()) {
-      $replacements += $token_service->generate('user', $owner_tokens, ['user' => $file->getOwner()], $options, $bubbleable_metadata);
-    }
-  }
-
-  return $replacements;
-}
-
-/**
- * Implements hook_token_info().
- */
-function file_token_info() {
-  $types['file'] = [
-    'name' => t("Files"),
-    'description' => t("Tokens related to uploaded files."),
-    'needs-data' => 'file',
-  ];
-
-  // File related tokens.
-  $file['fid'] = [
-    'name' => t("File ID"),
-    'description' => t("The unique ID of the uploaded file."),
-  ];
-  $file['uuid'] = [
-    'name' => t('UUID'),
-    'description' => t('The UUID of the uploaded file.'),
-  ];
-  $file['name'] = [
-    'name' => t("File name"),
-    'description' => t("The name of the file on disk."),
-  ];
-  $file['path'] = [
-    'name' => t("Path"),
-    'description' => t("The location of the file relative to Drupal root."),
-  ];
-  $file['mime'] = [
-    'name' => t("MIME type"),
-    'description' => t("The MIME type of the file."),
-  ];
-  $file['size'] = [
-    'name' => t("File size"),
-    'description' => t("The size of the file."),
-  ];
-  $file['url'] = [
-    'name' => t("URL"),
-    'description' => t("The web-accessible URL for the file."),
-  ];
-  $file['created'] = [
-    'name' => t("Created"),
-    'description' => t("The date the file created."),
-    'type' => 'date',
-  ];
-  $file['changed'] = [
-    'name' => t("Changed"),
-    'description' => t("The date the file was most recently changed."),
-    'type' => 'date',
-  ];
-  $file['owner'] = [
-    'name' => t("Owner"),
-    'description' => t("The user who originally uploaded the file."),
-    'type' => 'user',
-  ];
-
-  return [
-    'types' => $types,
-    'tokens' => [
-      'file' => $file,
-    ],
-  ];
-}
-
 /**
  * Form submission handler for upload / remove buttons of managed_file elements.
  *
@@ -1082,74 +756,6 @@ function file_field_find_file_reference_column(FieldDefinitionInterface $field)
   return FALSE;
 }
 
-/**
- * Implements hook_form_FORM_ID_alter().
- *
- * Injects the file sanitization options into /admin/config/media/file-system.
- *
- * These settings are enforced during upload by the FileEventSubscriber that
- * listens to the FileUploadSanitizeNameEvent event.
- *
- * @see \Drupal\system\Form\FileSystemForm
- * @see \Drupal\Core\File\Event\FileUploadSanitizeNameEvent
- * @see \Drupal\file\EventSubscriber\FileEventSubscriber
- */
-function file_form_system_file_system_settings_alter(array &$form, FormStateInterface $form_state): void {
-  $config = \Drupal::config('file.settings');
-  $form['filename_sanitization'] = [
-    '#type' => 'details',
-    '#title' => t('Sanitize filenames'),
-    '#description' => t('These settings only apply to new files as they are uploaded. Changes here do not affect existing file names.'),
-    '#open' => TRUE,
-    '#tree' => TRUE,
-  ];
-
-  $form['filename_sanitization']['replacement_character'] = [
-    '#type' => 'select',
-    '#title' => t('Replacement character'),
-    '#default_value' => $config->get('filename_sanitization.replacement_character'),
-    '#options' => [
-      '-' => t('Dash (-)'),
-      '_' => t('Underscore (_)'),
-    ],
-    '#description' => t('Used when replacing whitespace, replacing non-alphanumeric characters or transliterating unknown characters.'),
-  ];
-
-  $form['filename_sanitization']['transliterate'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Transliterate'),
-    '#default_value' => $config->get('filename_sanitization.transliterate'),
-    '#description' => t('Transliteration replaces any characters that are not alphanumeric, underscores, periods or hyphens with the replacement character. It ensures filenames only contain ASCII characters. It is recommended to keep transliteration enabled.'),
-  ];
-
-  $form['filename_sanitization']['replace_whitespace'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Replace whitespace with the replacement character'),
-    '#default_value' => $config->get('filename_sanitization.replace_whitespace'),
-  ];
-
-  $form['filename_sanitization']['replace_non_alphanumeric'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Replace non-alphanumeric characters with the replacement character'),
-    '#default_value' => $config->get('filename_sanitization.replace_non_alphanumeric'),
-    '#description' => t('Alphanumeric characters, dots <span aria-hidden="true">(.)</span>, underscores <span aria-hidden="true">(_)</span> and dashes <span aria-hidden="true">(-)</span> are preserved.'),
-  ];
-
-  $form['filename_sanitization']['deduplicate_separators'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Replace sequences of dots, underscores and/or dashes with the replacement character'),
-    '#default_value' => $config->get('filename_sanitization.deduplicate_separators'),
-  ];
-
-  $form['filename_sanitization']['lowercase'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Convert to lowercase'),
-    '#default_value' => $config->get('filename_sanitization.lowercase'),
-  ];
-
-  $form['#submit'][] = 'file_system_settings_submit';
-}
-
 /**
  * Form submission handler for file system settings form.
  */
diff --git a/core/modules/file/file.views.inc b/core/modules/file/file.views.inc
deleted file mode 100644
index 666c7da49d83a9cd69b3c383e6cb4c1e1fd2b66c..0000000000000000000000000000000000000000
--- a/core/modules/file/file.views.inc
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views data for file.module.
- */
-
-use Drupal\field\FieldStorageConfigInterface;
-
-/**
- * Implements hook_field_views_data().
- *
- * Views integration for file fields. Adds a file relationship to the default
- * field data.
- *
- * @see views_field_default_views_data()
- */
-function file_field_views_data(FieldStorageConfigInterface $field_storage) {
-  $data = views_field_default_views_data($field_storage);
-  foreach ($data as $table_name => $table_data) {
-    // Add the relationship only on the fid field.
-    $data[$table_name][$field_storage->getName() . '_target_id']['relationship'] = [
-      'id' => 'standard',
-      'base' => 'file_managed',
-      'entity type' => 'file',
-      'base field' => 'fid',
-      'label' => t('file from @field_name', ['@field_name' => $field_storage->getName()]),
-    ];
-  }
-
-  return $data;
-}
-
-/**
- * Implements hook_field_views_data_views_data_alter().
- *
- * Views integration to provide reverse relationships on file fields.
- */
-function file_field_views_data_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
-  $entity_type_id = $field_storage->getTargetEntityTypeId();
-  $entity_type_manager = \Drupal::entityTypeManager();
-  $entity_type = $entity_type_manager->getDefinition($entity_type_id);
-  $field_name = $field_storage->getName();
-  $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
-  /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
-  $table_mapping = $entity_type_manager->getStorage($entity_type_id)->getTableMapping();
-
-  [$label] = views_entity_field_label($entity_type_id, $field_name);
-
-  $data['file_managed'][$pseudo_field_name]['relationship'] = [
-    'title' => t('@entity using @field', ['@entity' => $entity_type->getLabel(), '@field' => $label]),
-    'label' => t('@field_name', ['@field_name' => $field_name]),
-    'group' => $entity_type->getLabel(),
-    'help' => t('Relate each @entity with a @field set to the file.', ['@entity' => $entity_type->getLabel(), '@field' => $label]),
-    'id' => 'entity_reverse',
-    'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
-    'entity_type' => $entity_type_id,
-    'base field' => $entity_type->getKey('id'),
-    'field_name' => $field_name,
-    'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
-    'field field' => $field_name . '_target_id',
-    'join_extra' => [
-      0 => [
-        'field' => 'deleted',
-        'value' => 0,
-        'numeric' => TRUE,
-      ],
-    ],
-  ];
-}
diff --git a/core/modules/file/src/Hook/FileHooks.php b/core/modules/file/src/Hook/FileHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..ab503a227c1bf0a88a84ffe73d9ffd743b72a5db
--- /dev/null
+++ b/core/modules/file/src/Hook/FileHooks.php
@@ -0,0 +1,402 @@
+<?php
+
+namespace Drupal\file\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Datetime\Entity\DateFormat;
+use Drupal\Core\StringTranslation\ByteSizeMarkup;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\file\Entity\File;
+use Drupal\file\FileInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for file.
+ */
+class FileHooks {
+  // cspell:ignore widthx
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.file':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The File module allows you to create fields that contain files. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":file_documentation">online documentation for the File module</a>.', [
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+          ':file_documentation' => 'https://www.drupal.org/documentation/modules/file',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Managing and displaying file fields') . '</dt>';
+        $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the file field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Allowing file extensions') . '</dt>';
+        $output .= '<dd>' . t('In the field settings, you can define the allowed file extensions (for example <em>pdf docx psd</em>) for the files that will be uploaded with the file field.') . '</dd>';
+        $output .= '<dt>' . t('Storing files') . '</dt>';
+        $output .= '<dd>' . t('Uploaded files can either be stored as <em>public</em> or <em>private</em>, depending on the <a href=":file-system">File system settings</a>. For more information, see the <a href=":system-help">System module help page</a>.', [
+          ':file-system' => Url::fromRoute('system.file_system_settings')->toString(),
+          ':system-help' => Url::fromRoute('help.page', [
+            'name' => 'system',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Restricting the maximum file size') . '</dt>';
+        $output .= '<dd>' . t('The maximum file size that users can upload is limited by PHP settings of the server, but you can restrict by entering the desired value as the <em>Maximum upload size</em> setting. The maximum file size is automatically displayed to users in the help text of the file field.') . '</dd>';
+        $output .= '<dt>' . t('Displaying files and descriptions') . '<dt>';
+        $output .= '<dd>' . t('In the field settings, you can allow users to toggle whether individual files are displayed. In the display settings, you can then choose one of the following formats: <ul><li><em>Generic file</em> displays links to the files and adds icons that symbolize the file extensions. If <em>descriptions</em> are enabled and have been submitted, then the description is displayed instead of the file name.</li><li><em>URL to file</em> displays the full path to the file as plain text.</li><li><em>Table of files</em> lists links to the files and the file sizes in a table.</li><li><em>RSS enclosure</em> only displays the first file, and only in a RSS feed, formatted according to the RSS 2.0 syntax for enclosures.</li></ul> A file can still be linked to directly by its URI even if it is not displayed.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_field_widget_info_alter().
+   */
+  #[Hook('field_widget_info_alter')]
+  public function fieldWidgetInfoAlter(array &$info) {
+    // Allows using the 'uri' widget for the 'file_uri' field type, which uses it
+    // as the default widget.
+    // @see \Drupal\file\Plugin\Field\FieldType\FileUriItem
+    $info['uri']['field_types'][] = 'file_uri';
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+          // From file.module.
+      'file_link' => [
+        'variables' => [
+          'file' => NULL,
+          'description' => NULL,
+          'attributes' => [],
+        ],
+      ],
+      'file_managed_file' => [
+        'render element' => 'element',
+      ],
+      'file_audio' => [
+        'variables' => [
+          'files' => [],
+          'attributes' => NULL,
+        ],
+      ],
+      'file_video' => [
+        'variables' => [
+          'files' => [],
+          'attributes' => NULL,
+        ],
+      ],
+      'file_widget_multiple' => [
+        'render element' => 'element',
+      ],
+      'file_upload_help' => [
+        'variables' => [
+          'description' => NULL,
+          'upload_validators' => NULL,
+          'cardinality' => NULL,
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_file_download().
+   */
+  #[Hook('file_download')]
+  public function fileDownload($uri) {
+    // Get the file record based on the URI. If not in the database just return.
+    /** @var \Drupal\file\FileRepositoryInterface $file_repository */
+    $file_repository = \Drupal::service('file.repository');
+    $file = $file_repository->loadByUri($uri);
+    if (!$file) {
+      return;
+    }
+    // Find out if a temporary file is still used in the system.
+    if ($file->isTemporary()) {
+      $usage = \Drupal::service('file.usage')->listUsage($file);
+      if (empty($usage) && $file->getOwnerId() != \Drupal::currentUser()->id()) {
+        // Deny access to temporary files without usage that are not owned by the
+        // same user. This prevents the security issue that a private file that
+        // was protected by field permissions becomes available after its usage
+        // was removed and before it is actually deleted from the file system.
+        // Modules that depend on this behavior should make the file permanent
+        // instead.
+        return -1;
+      }
+    }
+    // Find out which (if any) fields of this type contain the file.
+    $references = file_get_file_references($file, NULL, EntityStorageInterface::FIELD_LOAD_CURRENT, NULL);
+    // Stop processing if there are no references in order to avoid returning
+    // headers for files controlled by other modules. Make an exception for
+    // temporary files where the host entity has not yet been saved (for example,
+    // an image preview on a node/add form) in which case, allow download by the
+    // file's owner.
+    if (empty($references) && ($file->isPermanent() || $file->getOwnerId() != \Drupal::currentUser()->id())) {
+      return;
+    }
+    if (!$file->access('download')) {
+      return -1;
+    }
+    // Access is granted.
+    $headers = file_get_content_headers($file);
+    return $headers;
+  }
+
+  /**
+   * Implements hook_cron().
+   */
+  #[Hook('cron')]
+  public function cron() {
+    $age = \Drupal::config('system.file')->get('temporary_maximum_age');
+    $file_storage = \Drupal::entityTypeManager()->getStorage('file');
+    /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
+    $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
+    // Only delete temporary files if older than $age. Note that automatic cleanup
+    // is disabled if $age set to 0.
+    if ($age) {
+      $fids = \Drupal::entityQuery('file')->accessCheck(FALSE)->condition('status', FileInterface::STATUS_PERMANENT, '<>')->condition('changed', \Drupal::time()->getRequestTime() - $age, '<')->range(0, 100)->execute();
+      $files = $file_storage->loadMultiple($fids);
+      foreach ($files as $file) {
+        $references = \Drupal::service('file.usage')->listUsage($file);
+        if (empty($references)) {
+          if (!file_exists($file->getFileUri())) {
+            if (!$stream_wrapper_manager->isValidUri($file->getFileUri())) {
+              \Drupal::logger('file system')->warning('Temporary file "%path" that was deleted during garbage collection did not exist on the filesystem. This could be caused by a missing stream wrapper.', ['%path' => $file->getFileUri()]);
+            }
+            else {
+              \Drupal::logger('file system')->warning('Temporary file "%path" that was deleted during garbage collection did not exist on the filesystem.', ['%path' => $file->getFileUri()]);
+            }
+          }
+          // Delete the file entity. If the file does not exist, this will
+          // generate a second notice in the watchdog.
+          $file->delete();
+        }
+        else {
+          \Drupal::logger('file system')->info('Did not delete temporary file "%path" during garbage collection because it is in use by the following modules: %modules.', [
+            '%path' => $file->getFileUri(),
+            '%modules' => implode(', ', array_keys($references)),
+          ]);
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for file entities.
+   */
+  #[Hook('file_predelete')]
+  public function filePredelete(File $file) {
+    // @todo Remove references to a file that is in-use.
+  }
+
+  /**
+   * Implements hook_tokens().
+   */
+  #[Hook('tokens')]
+  public function tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+    $token_service = \Drupal::token();
+    $url_options = ['absolute' => TRUE];
+    if (isset($options['langcode'])) {
+      $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
+      $langcode = $options['langcode'];
+    }
+    else {
+      $langcode = NULL;
+    }
+    $replacements = [];
+    if ($type == 'file' && !empty($data['file'])) {
+      /** @var \Drupal\file\FileInterface $file */
+      $file = $data['file'];
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          // Basic keys and values.
+          case 'fid':
+            $replacements[$original] = $file->id();
+            break;
+
+          case 'uuid':
+            $replacements[$original] = $file->uuid();
+            break;
+
+          // Essential file data
+          case 'name':
+            $replacements[$original] = $file->getFilename();
+            break;
+
+          case 'path':
+            $replacements[$original] = $file->getFileUri();
+            break;
+
+          case 'mime':
+            $replacements[$original] = $file->getMimeType();
+            break;
+
+          case 'size':
+            $replacements[$original] = ByteSizeMarkup::create($file->getSize());
+            break;
+
+          case 'url':
+            // Ideally, this would use return a relative URL, but because tokens
+            // are also often used in emails, it's better to keep absolute file
+            // URLs. The 'url.site' cache context is associated to ensure the
+            // correct absolute URL is used in case of a multisite setup.
+            $replacements[$original] = $file->createFileUrl(FALSE);
+            $bubbleable_metadata->addCacheContexts(['url.site']);
+            break;
+
+          // These tokens are default variations on the chained tokens handled below.
+          case 'created':
+            $date_format = DateFormat::load('medium');
+            $bubbleable_metadata->addCacheableDependency($date_format);
+            $replacements[$original] = \Drupal::service('date.formatter')->format($file->getCreatedTime(), 'medium', '', NULL, $langcode);
+            break;
+
+          case 'changed':
+            $date_format = DateFormat::load('medium');
+            $bubbleable_metadata = $bubbleable_metadata->addCacheableDependency($date_format);
+            $replacements[$original] = \Drupal::service('date.formatter')->format($file->getChangedTime(), 'medium', '', NULL, $langcode);
+            break;
+
+          case 'owner':
+            $owner = $file->getOwner();
+            $bubbleable_metadata->addCacheableDependency($owner);
+            $name = $owner->label();
+            $replacements[$original] = $name;
+            break;
+        }
+      }
+      if ($date_tokens = $token_service->findWithPrefix($tokens, 'created')) {
+        $replacements += $token_service->generate('date', $date_tokens, ['date' => $file->getCreatedTime()], $options, $bubbleable_metadata);
+      }
+      if ($date_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
+        $replacements += $token_service->generate('date', $date_tokens, ['date' => $file->getChangedTime()], $options, $bubbleable_metadata);
+      }
+      if (($owner_tokens = $token_service->findWithPrefix($tokens, 'owner')) && $file->getOwner()) {
+        $replacements += $token_service->generate('user', $owner_tokens, ['user' => $file->getOwner()], $options, $bubbleable_metadata);
+      }
+    }
+    return $replacements;
+  }
+
+  /**
+   * Implements hook_token_info().
+   */
+  #[Hook('token_info')]
+  public function tokenInfo() {
+    $types['file'] = [
+      'name' => t("Files"),
+      'description' => t("Tokens related to uploaded files."),
+      'needs-data' => 'file',
+    ];
+    // File related tokens.
+    $file['fid'] = [
+      'name' => t("File ID"),
+      'description' => t("The unique ID of the uploaded file."),
+    ];
+    $file['uuid'] = ['name' => t('UUID'), 'description' => t('The UUID of the uploaded file.')];
+    $file['name'] = ['name' => t("File name"), 'description' => t("The name of the file on disk.")];
+    $file['path'] = [
+      'name' => t("Path"),
+      'description' => t("The location of the file relative to Drupal root."),
+    ];
+    $file['mime'] = ['name' => t("MIME type"), 'description' => t("The MIME type of the file.")];
+    $file['size'] = ['name' => t("File size"), 'description' => t("The size of the file.")];
+    $file['url'] = ['name' => t("URL"), 'description' => t("The web-accessible URL for the file.")];
+    $file['created'] = [
+      'name' => t("Created"),
+      'description' => t("The date the file created."),
+      'type' => 'date',
+    ];
+    $file['changed'] = [
+      'name' => t("Changed"),
+      'description' => t("The date the file was most recently changed."),
+      'type' => 'date',
+    ];
+    $file['owner'] = [
+      'name' => t("Owner"),
+      'description' => t("The user who originally uploaded the file."),
+      'type' => 'user',
+    ];
+    return ['types' => $types, 'tokens' => ['file' => $file]];
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   *
+   * Injects the file sanitization options into /admin/config/media/file-system.
+   *
+   * These settings are enforced during upload by the FileEventSubscriber that
+   * listens to the FileUploadSanitizeNameEvent event.
+   *
+   * @see \Drupal\system\Form\FileSystemForm
+   * @see \Drupal\Core\File\Event\FileUploadSanitizeNameEvent
+   * @see \Drupal\file\EventSubscriber\FileEventSubscriber
+   */
+  #[Hook('form_system_file_system_settings_alter')]
+  public function formSystemFileSystemSettingsAlter(array &$form, FormStateInterface $form_state) : void {
+    $config = \Drupal::config('file.settings');
+    $form['filename_sanitization'] = [
+      '#type' => 'details',
+      '#title' => t('Sanitize filenames'),
+      '#description' => t('These settings only apply to new files as they are uploaded. Changes here do not affect existing file names.'),
+      '#open' => TRUE,
+      '#tree' => TRUE,
+    ];
+    $form['filename_sanitization']['replacement_character'] = [
+      '#type' => 'select',
+      '#title' => t('Replacement character'),
+      '#default_value' => $config->get('filename_sanitization.replacement_character'),
+      '#options' => [
+        '-' => t('Dash (-)'),
+        '_' => t('Underscore (_)'),
+      ],
+      '#description' => t('Used when replacing whitespace, replacing non-alphanumeric characters or transliterating unknown characters.'),
+    ];
+    $form['filename_sanitization']['transliterate'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Transliterate'),
+      '#default_value' => $config->get('filename_sanitization.transliterate'),
+      '#description' => t('Transliteration replaces any characters that are not alphanumeric, underscores, periods or hyphens with the replacement character. It ensures filenames only contain ASCII characters. It is recommended to keep transliteration enabled.'),
+    ];
+    $form['filename_sanitization']['replace_whitespace'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Replace whitespace with the replacement character'),
+      '#default_value' => $config->get('filename_sanitization.replace_whitespace'),
+    ];
+    $form['filename_sanitization']['replace_non_alphanumeric'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Replace non-alphanumeric characters with the replacement character'),
+      '#default_value' => $config->get('filename_sanitization.replace_non_alphanumeric'),
+      '#description' => t('Alphanumeric characters, dots <span aria-hidden="true">(.)</span>, underscores <span aria-hidden="true">(_)</span> and dashes <span aria-hidden="true">(-)</span> are preserved.'),
+    ];
+    $form['filename_sanitization']['deduplicate_separators'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Replace sequences of dots, underscores and/or dashes with the replacement character'),
+      '#default_value' => $config->get('filename_sanitization.deduplicate_separators'),
+    ];
+    $form['filename_sanitization']['lowercase'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Convert to lowercase'),
+      '#default_value' => $config->get('filename_sanitization.lowercase'),
+    ];
+    $form['#submit'][] = 'file_system_settings_submit';
+  }
+
+}
diff --git a/core/modules/file/src/Hook/FileViewsHooks.php b/core/modules/file/src/Hook/FileViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..b2582a1086d8e93d2ceb9c1f3b057b76786c13f8
--- /dev/null
+++ b/core/modules/file/src/Hook/FileViewsHooks.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\file\Hook;
+
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for file.
+ */
+class FileViewsHooks {
+
+  /**
+   * Implements hook_field_views_data().
+   *
+   * Views integration for file fields. Adds a file relationship to the default
+   * field data.
+   *
+   * @see views_field_default_views_data()
+   */
+  #[Hook('field_views_data')]
+  public function fieldViewsData(FieldStorageConfigInterface $field_storage) {
+    $data = views_field_default_views_data($field_storage);
+    foreach ($data as $table_name => $table_data) {
+      // Add the relationship only on the fid field.
+      $data[$table_name][$field_storage->getName() . '_target_id']['relationship'] = [
+        'id' => 'standard',
+        'base' => 'file_managed',
+        'entity type' => 'file',
+        'base field' => 'fid',
+        'label' => t('file from @field_name', [
+          '@field_name' => $field_storage->getName(),
+        ]),
+      ];
+    }
+    return $data;
+  }
+
+  /**
+   * Implements hook_field_views_data_views_data_alter().
+   *
+   * Views integration to provide reverse relationships on file fields.
+   */
+  #[Hook('field_views_data_views_data_alter')]
+  public function fieldViewsDataViewsDataAlter(array &$data, FieldStorageConfigInterface $field_storage) {
+    $entity_type_id = $field_storage->getTargetEntityTypeId();
+    $entity_type_manager = \Drupal::entityTypeManager();
+    $entity_type = $entity_type_manager->getDefinition($entity_type_id);
+    $field_name = $field_storage->getName();
+    $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
+    $table_mapping = $entity_type_manager->getStorage($entity_type_id)->getTableMapping();
+    [$label] = views_entity_field_label($entity_type_id, $field_name);
+    $data['file_managed'][$pseudo_field_name]['relationship'] = [
+      'title' => t('@entity using @field', [
+        '@entity' => $entity_type->getLabel(),
+        '@field' => $label,
+      ]),
+      'label' => t('@field_name', [
+        '@field_name' => $field_name,
+      ]),
+      'group' => $entity_type->getLabel(),
+      'help' => t('Relate each @entity with a @field set to the file.', [
+        '@entity' => $entity_type->getLabel(),
+        '@field' => $label,
+      ]),
+      'id' => 'entity_reverse',
+      'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
+      'entity_type' => $entity_type_id,
+      'base field' => $entity_type->getKey('id'),
+      'field_name' => $field_name,
+      'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
+      'field field' => $field_name . '_target_id',
+      'join_extra' => [
+        0 => [
+          'field' => 'deleted',
+          'value' => 0,
+          'numeric' => TRUE,
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/file/tests/file_test/file_test.module b/core/modules/file/tests/file_test/file_test.module
index a5fd82e64bbb1b95a2f0399d7b0aed0acbd41043..4aa4116762c35322ff1bcca5600b40b124a2d9f1 100644
--- a/core/modules/file/tests/file_test/file_test.module
+++ b/core/modules/file/tests/file_test/file_test.module
@@ -131,184 +131,6 @@ function file_test_set_return($op, $value) {
   \Drupal::state()->set('file_test.return', $return);
 }
 
-/**
- * Implements hook_ENTITY_TYPE_load() for file entities.
- */
-function file_test_file_load($files) {
-  foreach ($files as $file) {
-    _file_test_log_call('load', [$file->id()]);
-    // Assign a value on the object so that we can test that the $file is passed
-    // by reference.
-    $file->file_test['loaded'] = TRUE;
-  }
-}
-
-/**
- * Implements hook_file_download().
- */
-function file_test_file_download($uri) {
-  if (\Drupal::state()->get('file_test.allow_all', FALSE)) {
-    $files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $uri]);
-    $file = reset($files);
-    return file_get_content_headers($file);
-  }
-  _file_test_log_call('download', [$uri]);
-  return _file_test_get_return('download');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for file entities.
- */
-function file_test_file_insert(File $file) {
-  _file_test_log_call('insert', [$file->id()]);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for file entities.
- */
-function file_test_file_update(File $file) {
-  _file_test_log_call('update', [$file->id()]);
-}
-
-/**
- * Implements hook_file_copy().
- */
-function file_test_file_copy(File $file, $source) {
-  _file_test_log_call('copy', [$file->id(), $source->id()]);
-}
-
-/**
- * Implements hook_file_move().
- */
-function file_test_file_move(File $file, File $source) {
-  _file_test_log_call('move', [$file->id(), $source->id()]);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for file entities.
- */
-function file_test_file_predelete(File $file) {
-  _file_test_log_call('delete', [$file->id()]);
-}
-
-/**
- * Implements hook_file_url_alter().
- */
-function file_test_file_url_alter(&$uri) {
-  /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
-  $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
-
-  // Only run this hook when this variable is set. Otherwise, we'd have to add
-  // another hidden test module just for this hook.
-  $alter_mode = \Drupal::state()->get('file_test.hook_file_url_alter');
-  if (!$alter_mode) {
-    return;
-  }
-  // Test alteration of file URLs to use a CDN.
-  elseif ($alter_mode == 'cdn') {
-    $cdn_extensions = ['css', 'js', 'gif', 'jpg', 'jpeg', 'png'];
-
-    // Most CDNs don't support private file transfers without a lot of hassle,
-    // so don't support this in the common case.
-    $schemes = ['public'];
-
-    $scheme = $stream_wrapper_manager::getScheme($uri);
-
-    // Only serve shipped files and public created files from the CDN.
-    if (!$scheme || in_array($scheme, $schemes)) {
-      // Shipped files.
-      if (!$scheme) {
-        $path = $uri;
-      }
-      // Public created files.
-      else {
-        $wrapper = $stream_wrapper_manager->getViaScheme($scheme);
-        $path = $wrapper->getDirectoryPath() . '/' . $stream_wrapper_manager::getTarget($uri);
-      }
-
-      // Clean up Windows paths.
-      $path = str_replace('\\', '/', $path);
-
-      // Serve files with one of the CDN extensions from CDN 1, all others from
-      // CDN 2.
-      $pathinfo = pathinfo($path);
-      if (array_key_exists('extension', $pathinfo) && in_array($pathinfo['extension'], $cdn_extensions)) {
-        $uri = FILE_URL_TEST_CDN_1 . '/' . $path;
-      }
-      else {
-        $uri = FILE_URL_TEST_CDN_2 . '/' . $path;
-      }
-    }
-  }
-  // Test alteration of file URLs to use root-relative URLs.
-  elseif ($alter_mode == 'root-relative') {
-    // Only serve shipped files and public created files with root-relative
-    // URLs.
-    $scheme = $stream_wrapper_manager::getScheme($uri);
-    if (!$scheme || $scheme == 'public') {
-      // Shipped files.
-      if (!$scheme) {
-        $path = $uri;
-      }
-      // Public created files.
-      else {
-        $wrapper = $stream_wrapper_manager->getViaScheme($scheme);
-        $path = $wrapper->getDirectoryPath() . '/' . $stream_wrapper_manager::getTarget($uri);
-      }
-
-      // Clean up Windows paths.
-      $path = str_replace('\\', '/', $path);
-
-      // Generate a root-relative URL.
-      $uri = base_path() . '/' . $path;
-    }
-  }
-  // Test alteration of file URLs to use protocol-relative URLs.
-  elseif ($alter_mode == 'protocol-relative') {
-    // Only serve shipped files and public created files with protocol-relative
-    // URLs.
-    $scheme = $stream_wrapper_manager::getScheme($uri);
-    if (!$scheme || $scheme == 'public') {
-      // Shipped files.
-      if (!$scheme) {
-        $path = $uri;
-      }
-      // Public created files.
-      else {
-        $wrapper = $stream_wrapper_manager->getViaScheme($scheme);
-        $path = $wrapper->getDirectoryPath() . '/' . $stream_wrapper_manager::getTarget($uri);
-      }
-
-      // Clean up Windows paths.
-      $path = str_replace('\\', '/', $path);
-
-      // Generate a protocol-relative URL.
-      $uri = '/' . base_path() . '/' . $path;
-    }
-  }
-}
-
-/**
- * Implements hook_file_mimetype_mapping_alter().
- */
-function file_test_file_mimetype_mapping_alter(&$mapping) {
-  // Add new mappings.
-  $mapping['mimetypes']['file_test_mimetype_1'] = 'made_up/file_test_1';
-  $mapping['mimetypes']['file_test_mimetype_2'] = 'made_up/file_test_2';
-  $mapping['mimetypes']['file_test_mimetype_3'] = 'made_up/doc';
-  $mapping['mimetypes']['application-x-compress'] = 'application/x-compress';
-  $mapping['mimetypes']['application-x-tarz'] = 'application/x-tarz';
-  $mapping['mimetypes']['application-x-garply-waldo'] = 'application/x-garply-waldo';
-  $mapping['extensions']['file_test_1'] = 'file_test_mimetype_1';
-  $mapping['extensions']['file_test_2'] = 'file_test_mimetype_2';
-  $mapping['extensions']['file_test_3'] = 'file_test_mimetype_2';
-  $mapping['extensions']['z'] = 'application-x-compress';
-  $mapping['extensions']['tar.z'] = 'application-x-tarz';
-  $mapping['extensions']['garply.waldo'] = 'application-x-garply-waldo';
-  // Override existing mapping.
-  $mapping['extensions']['doc'] = 'file_test_mimetype_3';
-}
-
 /**
  * Helper validator that returns the $errors parameter.
  */
@@ -351,14 +173,3 @@ function file_test_file_scan_callback($filepath = NULL, $reset = FALSE) {
 function file_test_file_scan_callback_reset() {
   file_test_file_scan_callback(NULL, TRUE);
 }
-
-/**
- * Implements hook_entity_type_alter().
- */
-function file_test_entity_type_alter(&$entity_types): void {
-  if (\Drupal::state()->get('file_test_alternate_access_handler', FALSE)) {
-    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-    $entity_types['file']
-      ->setAccessClass('Drupal\file_test\FileTestAccessControlHandler');
-  }
-}
diff --git a/core/modules/file/tests/file_test/src/Hook/FileTestHooks.php b/core/modules/file/tests/file_test/src/Hook/FileTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f329e39052ce2e86318c34c40078b314dc5f73ef
--- /dev/null
+++ b/core/modules/file/tests/file_test/src/Hook/FileTestHooks.php
@@ -0,0 +1,200 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\file_test\Hook;
+
+use Drupal\file\Entity\File;
+use Drupal\Core\Hook\Attribute\Hook;
+
+// cspell:ignore tarz
+// cspell:ignore garply
+
+/**
+ * Hook implementations for file_test.
+ */
+class FileTestHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_load() for file entities.
+   */
+  #[Hook('file_load')]
+  public function fileLoad($files) {
+    foreach ($files as $file) {
+      _file_test_log_call('load', [$file->id()]);
+      // Assign a value on the object so that we can test that the $file is passed
+      // by reference.
+      $file->file_test['loaded'] = TRUE;
+    }
+  }
+
+  /**
+   * Implements hook_file_download().
+   */
+  #[Hook('file_download')]
+  public function fileDownload($uri) {
+    if (\Drupal::state()->get('file_test.allow_all', FALSE)) {
+      $files = \Drupal::entityTypeManager()->getStorage('file')->loadByProperties(['uri' => $uri]);
+      $file = reset($files);
+      return file_get_content_headers($file);
+    }
+    _file_test_log_call('download', [$uri]);
+    return _file_test_get_return('download');
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for file entities.
+   */
+  #[Hook('file_insert')]
+  public function fileInsert(File $file) {
+    _file_test_log_call('insert', [$file->id()]);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for file entities.
+   */
+  #[Hook('file_update')]
+  public function fileUpdate(File $file) {
+    _file_test_log_call('update', [$file->id()]);
+  }
+
+  /**
+   * Implements hook_file_copy().
+   */
+  #[Hook('file_copy')]
+  public function fileCopy(File $file, $source) {
+    _file_test_log_call('copy', [$file->id(), $source->id()]);
+  }
+
+  /**
+   * Implements hook_file_move().
+   */
+  #[Hook('file_move')]
+  public function fileMove(File $file, File $source) {
+    _file_test_log_call('move', [$file->id(), $source->id()]);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for file entities.
+   */
+  #[Hook('file_predelete')]
+  public function filePredelete(File $file) {
+    _file_test_log_call('delete', [$file->id()]);
+  }
+
+  /**
+   * Implements hook_file_url_alter().
+   */
+  #[Hook('file_url_alter')]
+  public function fileUrlAlter(&$uri) {
+    /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
+    $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
+    // Only run this hook when this variable is set. Otherwise, we'd have to add
+    // another hidden test module just for this hook.
+    $alter_mode = \Drupal::state()->get('file_test.hook_file_url_alter');
+    if (!$alter_mode) {
+      return;
+    }
+    elseif ($alter_mode == 'cdn') {
+      $cdn_extensions = ['css', 'js', 'gif', 'jpg', 'jpeg', 'png'];
+      // Most CDNs don't support private file transfers without a lot of hassle,
+      // so don't support this in the common case.
+      $schemes = ['public'];
+      $scheme = $stream_wrapper_manager::getScheme($uri);
+      // Only serve shipped files and public created files from the CDN.
+      if (!$scheme || in_array($scheme, $schemes)) {
+        // Shipped files.
+        if (!$scheme) {
+          $path = $uri;
+        }
+        else {
+          $wrapper = $stream_wrapper_manager->getViaScheme($scheme);
+          $path = $wrapper->getDirectoryPath() . '/' . $stream_wrapper_manager::getTarget($uri);
+        }
+        // Clean up Windows paths.
+        $path = str_replace('\\', '/', $path);
+        // Serve files with one of the CDN extensions from CDN 1, all others from
+        // CDN 2.
+        $pathinfo = pathinfo($path);
+        if (array_key_exists('extension', $pathinfo) && in_array($pathinfo['extension'], $cdn_extensions)) {
+          $uri = FILE_URL_TEST_CDN_1 . '/' . $path;
+        }
+        else {
+          $uri = FILE_URL_TEST_CDN_2 . '/' . $path;
+        }
+      }
+    }
+    elseif ($alter_mode == 'root-relative') {
+      // Only serve shipped files and public created files with root-relative
+      // URLs.
+      $scheme = $stream_wrapper_manager::getScheme($uri);
+      if (!$scheme || $scheme == 'public') {
+        // Shipped files.
+        if (!$scheme) {
+          $path = $uri;
+        }
+        else {
+          $wrapper = $stream_wrapper_manager->getViaScheme($scheme);
+          $path = $wrapper->getDirectoryPath() . '/' . $stream_wrapper_manager::getTarget($uri);
+        }
+        // Clean up Windows paths.
+        $path = str_replace('\\', '/', $path);
+        // Generate a root-relative URL.
+        $uri = base_path() . '/' . $path;
+      }
+    }
+    elseif ($alter_mode == 'protocol-relative') {
+      // Only serve shipped files and public created files with protocol-relative
+      // URLs.
+      $scheme = $stream_wrapper_manager::getScheme($uri);
+      if (!$scheme || $scheme == 'public') {
+        // Shipped files.
+        if (!$scheme) {
+          $path = $uri;
+        }
+        else {
+          $wrapper = $stream_wrapper_manager->getViaScheme($scheme);
+          $path = $wrapper->getDirectoryPath() . '/' . $stream_wrapper_manager::getTarget($uri);
+        }
+        // Clean up Windows paths.
+        $path = str_replace('\\', '/', $path);
+        // Generate a protocol-relative URL.
+        $uri = '/' . base_path() . '/' . $path;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_file_mimetype_mapping_alter().
+   */
+  #[Hook('file_mimetype_mapping_alter')]
+  public function fileMimetypeMappingAlter(&$mapping) {
+    // Add new mappings.
+    $mapping['mimetypes']['file_test_mimetype_1'] = 'made_up/file_test_1';
+    $mapping['mimetypes']['file_test_mimetype_2'] = 'made_up/file_test_2';
+    $mapping['mimetypes']['file_test_mimetype_3'] = 'made_up/doc';
+    $mapping['mimetypes']['application-x-compress'] = 'application/x-compress';
+    $mapping['mimetypes']['application-x-tarz'] = 'application/x-tarz';
+    $mapping['mimetypes']['application-x-garply-waldo'] = 'application/x-garply-waldo';
+    $mapping['extensions']['file_test_1'] = 'file_test_mimetype_1';
+    $mapping['extensions']['file_test_2'] = 'file_test_mimetype_2';
+    $mapping['extensions']['file_test_3'] = 'file_test_mimetype_2';
+    $mapping['extensions']['z'] = 'application-x-compress';
+    $mapping['extensions']['tar.z'] = 'application-x-tarz';
+    $mapping['extensions']['garply.waldo'] = 'application-x-garply-waldo';
+    // Override existing mapping.
+    $mapping['extensions']['doc'] = 'file_test_mimetype_3';
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(&$entity_types) : void {
+    if (\Drupal::state()->get('file_test_alternate_access_handler', FALSE)) {
+      /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+      $entity_types['file']->setAccessClass('Drupal\file_test\FileTestAccessControlHandler');
+    }
+  }
+
+}
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index e3fbaafdfdabd94d70b28b6ffd01463811101b6e..54f959acfc77d862c94f328963cf06e0f6deafd2 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -2,85 +2,17 @@
 
 /**
  * @file
- * Framework for handling the filtering of content.
  */
 
-use Drupal\Core\Url;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Cache\Cache;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Template\Attribute;
 use Drupal\filter\FilterFormatInterface;
 use Drupal\user\Entity\Role;
 use Drupal\user\RoleInterface;
 
-/**
- * Implements hook_help().
- */
-function filter_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.filter':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Filter module allows administrators to configure text formats. Text formats change how HTML tags and other text will be <em>processed and displayed</em> in the site. They are used to transform text, and also help to defend your website against potentially damaging input from malicious users. Visual text editors can be associated with text formats by using the <a href=":editor_help">Text Editor module</a>. For more information, see the <a href=":filter_do">online documentation for the Filter module</a>.', [':filter_do' => 'https://www.drupal.org/documentation/modules/filter/', ':editor_help' => (\Drupal::moduleHandler()->moduleExists('editor')) ? Url::fromRoute('help.page', ['name' => 'editor'])->toString() : '#']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Managing text formats') . '</dt>';
-      $output .= '<dd>' . t('You can create and edit text formats on the <a href=":formats">Text formats page</a> (if the Text Editor module is installed, this page is named Text formats and editors). One text format is included by default: Plain text (which removes all HTML tags). Additional text formats may be created during installation. You can create a text format by clicking "<a href=":add_format">Add text format</a>".', [':formats' => Url::fromRoute('filter.admin_overview')->toString(), ':add_format' => Url::fromRoute('filter.format_add')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Assigning roles to text formats') . '</dt>';
-      $output .= '<dd>' . t('You can define which users will be able to use each text format by selecting roles. To ensure security, anonymous and untrusted users should only have access to text formats that restrict them to either plain text or a safe set of HTML tags. This is because HTML tags can allow embedding malicious links or scripts in text. More trusted registered users may be granted permission to use less restrictive text formats in order to create rich text. <strong>Improper text format configuration is a security risk.</strong>') . '</dd>';
-      $output .= '<dt>' . t('Selecting filters') . '</dt>';
-      $output .= '<dd>' . t('Each text format uses filters that add, remove, or transform elements within user-entered text. For example, one filter removes unapproved HTML tags, while another transforms URLs into clickable links. Filters are applied in a specific order. They do not change the <em>stored</em> content: they define how it is processed and displayed.') . '</dd>';
-      $output .= '<dd>' . t('Each filter can have additional configuration options. For example, for the "Limit allowed HTML tags" filter you need to define the list of HTML tags that the filter leaves in the text.') . '</dd>';
-      $output .= '<dt>' . t('Using text fields with text formats') . '</dt>';
-      $output .= '<dd>' . t('Text fields that allow text formats are those with "formatted" in the description. These are <em>Text (formatted, long, with summary)</em>, <em>Text (formatted)</em>, and <em>Text (formatted, long)</em>. You cannot change the type of field once a field has been created.') . '</dd>';
-      $output .= '<dt>' . t('Choosing a text format') . '</dt>';
-      $output .= '<dd>' . t('When creating or editing data in a field that has text formats enabled, users can select the format under the field from the Text format select list.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'filter.admin_overview':
-      $output = '<p>' . t('Text formats define how text is filtered for output and how HTML tags and other text is displayed, replaced, or removed. <strong>Improper text format configuration is a security risk.</strong> Learn more on the <a href=":filter_help">Filter module help page</a>.', [':filter_help' => Url::fromRoute('help.page', ['name' => 'filter'])->toString()]) . '</p>';
-      $output .= '<p>' . t('Text formats are presented on content editing pages in the order defined on this page. The first format available to a user will be selected by default.') . '</p>';
-      return $output;
-
-    case 'entity.filter_format.edit_form':
-      $output = '<p>' . t('A text format contains filters that change the display of user input; for example, stripping out malicious HTML or making URLs clickable. Filters are executed from top to bottom and the order is important, since one filter may prevent another filter from doing its job. For example, when URLs are converted into links before disallowed HTML tags are removed, all links may be removed. When this happens, the order of filters may need to be rearranged.') . '</p>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function filter_theme(): array {
-  return [
-    'filter_tips' => [
-      'variables' => ['tips' => NULL, 'long' => FALSE],
-    ],
-    'text_format_wrapper' => [
-      'variables' => [
-        'children' => NULL,
-        'description' => NULL,
-        'attributes' => [],
-      ],
-    ],
-    'filter_guidelines' => [
-      'variables' => ['format' => NULL],
-    ],
-    'filter_caption' => [
-      'variables' => [
-        'node' => NULL,
-        'tag' => NULL,
-        'caption' => NULL,
-        'classes' => NULL,
-      ],
-    ],
-  ];
-}
-
 /**
  * Retrieves a list of enabled text formats, ordered by weight.
  *
@@ -837,27 +769,6 @@ function _filter_html_image_secure_process($text) {
   return $text;
 }
 
-/**
- * Implements hook_filter_secure_image_alter().
- *
- * Formats an image DOM element that has an invalid source.
- *
- * @see _filter_html_image_secure_process()
- */
-function filter_filter_secure_image_alter(&$image) {
-  // Turn an invalid image into an error indicator.
-  $image->setAttribute('src', base_path() . 'core/misc/icons/e32700/error.svg');
-  $image->setAttribute('alt', t('Image removed.'));
-  $image->setAttribute('title', t('This image has been removed. For security reasons, only images from the local domain are allowed.'));
-  $image->setAttribute('height', '16');
-  $image->setAttribute('width', '16');
-
-  // Add a CSS class to aid in styling.
-  $class = ($image->getAttribute('class') ? trim($image->getAttribute('class')) . ' ' : '');
-  $class .= 'filter-image-invalid';
-  $image->setAttribute('class', $class);
-}
-
 /**
  * @} End of "defgroup standard_filters".
  */
diff --git a/core/modules/filter/src/Hook/FilterHooks.php b/core/modules/filter/src/Hook/FilterHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..144350049bb5a253701242fe577e1a2a78ca9fb8
--- /dev/null
+++ b/core/modules/filter/src/Hook/FilterHooks.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\filter\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for filter.
+ */
+class FilterHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.filter':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Filter module allows administrators to configure text formats. Text formats change how HTML tags and other text will be <em>processed and displayed</em> in the site. They are used to transform text, and also help to defend your website against potentially damaging input from malicious users. Visual text editors can be associated with text formats by using the <a href=":editor_help">Text Editor module</a>. For more information, see the <a href=":filter_do">online documentation for the Filter module</a>.', [
+          ':filter_do' => 'https://www.drupal.org/documentation/modules/filter/',
+          ':editor_help' => \Drupal::moduleHandler()->moduleExists('editor') ? Url::fromRoute('help.page', [
+            'name' => 'editor',
+          ])->toString() : '#',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Managing text formats') . '</dt>';
+        $output .= '<dd>' . t('You can create and edit text formats on the <a href=":formats">Text formats page</a> (if the Text Editor module is installed, this page is named Text formats and editors). One text format is included by default: Plain text (which removes all HTML tags). Additional text formats may be created during installation. You can create a text format by clicking "<a href=":add_format">Add text format</a>".', [
+          ':formats' => Url::fromRoute('filter.admin_overview')->toString(),
+          ':add_format' => Url::fromRoute('filter.format_add')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Assigning roles to text formats') . '</dt>';
+        $output .= '<dd>' . t('You can define which users will be able to use each text format by selecting roles. To ensure security, anonymous and untrusted users should only have access to text formats that restrict them to either plain text or a safe set of HTML tags. This is because HTML tags can allow embedding malicious links or scripts in text. More trusted registered users may be granted permission to use less restrictive text formats in order to create rich text. <strong>Improper text format configuration is a security risk.</strong>') . '</dd>';
+        $output .= '<dt>' . t('Selecting filters') . '</dt>';
+        $output .= '<dd>' . t('Each text format uses filters that add, remove, or transform elements within user-entered text. For example, one filter removes unapproved HTML tags, while another transforms URLs into clickable links. Filters are applied in a specific order. They do not change the <em>stored</em> content: they define how it is processed and displayed.') . '</dd>';
+        $output .= '<dd>' . t('Each filter can have additional configuration options. For example, for the "Limit allowed HTML tags" filter you need to define the list of HTML tags that the filter leaves in the text.') . '</dd>';
+        $output .= '<dt>' . t('Using text fields with text formats') . '</dt>';
+        $output .= '<dd>' . t('Text fields that allow text formats are those with "formatted" in the description. These are <em>Text (formatted, long, with summary)</em>, <em>Text (formatted)</em>, and <em>Text (formatted, long)</em>. You cannot change the type of field once a field has been created.') . '</dd>';
+        $output .= '<dt>' . t('Choosing a text format') . '</dt>';
+        $output .= '<dd>' . t('When creating or editing data in a field that has text formats enabled, users can select the format under the field from the Text format select list.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'filter.admin_overview':
+        $output = '<p>' . t('Text formats define how text is filtered for output and how HTML tags and other text is displayed, replaced, or removed. <strong>Improper text format configuration is a security risk.</strong> Learn more on the <a href=":filter_help">Filter module help page</a>.', [
+          ':filter_help' => Url::fromRoute('help.page', [
+            'name' => 'filter',
+          ])->toString(),
+        ]) . '</p>';
+        $output .= '<p>' . t('Text formats are presented on content editing pages in the order defined on this page. The first format available to a user will be selected by default.') . '</p>';
+        return $output;
+
+      case 'entity.filter_format.edit_form':
+        $output = '<p>' . t('A text format contains filters that change the display of user input; for example, stripping out malicious HTML or making URLs clickable. Filters are executed from top to bottom and the order is important, since one filter may prevent another filter from doing its job. For example, when URLs are converted into links before disallowed HTML tags are removed, all links may be removed. When this happens, the order of filters may need to be rearranged.') . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'filter_tips' => [
+        'variables' => [
+          'tips' => NULL,
+          'long' => FALSE,
+        ],
+      ],
+      'text_format_wrapper' => [
+        'variables' => [
+          'children' => NULL,
+          'description' => NULL,
+          'attributes' => [],
+        ],
+      ],
+      'filter_guidelines' => [
+        'variables' => [
+          'format' => NULL,
+        ],
+      ],
+      'filter_caption' => [
+        'variables' => [
+          'node' => NULL,
+          'tag' => NULL,
+          'caption' => NULL,
+          'classes' => NULL,
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_filter_secure_image_alter().
+   *
+   * Formats an image DOM element that has an invalid source.
+   *
+   * @see _filter_html_image_secure_process()
+   */
+  #[Hook('filter_secure_image_alter')]
+  public function filterSecureImageAlter(&$image) {
+    // Turn an invalid image into an error indicator.
+    $image->setAttribute('src', base_path() . 'core/misc/icons/e32700/error.svg');
+    $image->setAttribute('alt', t('Image removed.'));
+    $image->setAttribute('title', t('This image has been removed. For security reasons, only images from the local domain are allowed.'));
+    $image->setAttribute('height', '16');
+    $image->setAttribute('width', '16');
+    // Add a CSS class to aid in styling.
+    $class = $image->getAttribute('class') ? trim($image->getAttribute('class')) . ' ' : '';
+    $class .= 'filter-image-invalid';
+    $image->setAttribute('class', $class);
+  }
+
+}
diff --git a/core/modules/filter/tests/filter_test/filter_test.module b/core/modules/filter/tests/filter_test/filter_test.module
deleted file mode 100644
index fe25ff89549a95a922ed58d7512e27b28ef68ed5..0000000000000000000000000000000000000000
--- a/core/modules/filter/tests/filter_test/filter_test.module
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module for Filter module hooks and functions not used in core.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_ENTITY_TYPE_insert().
- */
-function filter_test_filter_format_insert($format) {
-  \Drupal::messenger()->addStatus('hook_filter_format_insert invoked.');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update().
- */
-function filter_test_filter_format_update($format) {
-  \Drupal::messenger()->addStatus('hook_filter_format_update invoked.');
-}
-
-/**
- * Implements hook_filter_format_disable().
- */
-function filter_test_filter_format_disable($format) {
-  \Drupal::messenger()->addStatus('hook_filter_format_disable invoked.');
-}
diff --git a/core/modules/filter/tests/filter_test/src/Hook/FilterTestHooks.php b/core/modules/filter/tests/filter_test/src/Hook/FilterTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..2b3c90e2699f79e41bb27a58af97a588b7f2abb2
--- /dev/null
+++ b/core/modules/filter/tests/filter_test/src/Hook/FilterTestHooks.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\filter_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for filter_test.
+ */
+class FilterTestHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert().
+   */
+  #[Hook('filter_format_insert')]
+  public function filterFormatInsert($format) {
+    \Drupal::messenger()->addStatus('hook_filter_format_insert invoked.');
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update().
+   */
+  #[Hook('filter_format_update')]
+  public function filterFormatUpdate($format) {
+    \Drupal::messenger()->addStatus('hook_filter_format_update invoked.');
+  }
+
+  /**
+   * Implements hook_filter_format_disable().
+   */
+  #[Hook('filter_format_disable')]
+  public function filterFormatDisable($format) {
+    \Drupal::messenger()->addStatus('hook_filter_format_disable invoked.');
+  }
+
+}
diff --git a/core/modules/help/help.api.php b/core/modules/help/help.api.php
index 7d7c25a5dcfde3c534ac00fc7e6ec4cdb3353026..1dc6fb0d84e7646145916cd2e3d9bc72ee500ac2 100644
--- a/core/modules/help/help.api.php
+++ b/core/modules/help/help.api.php
@@ -5,6 +5,7 @@
  * Hooks for the Help system.
  */
 
+use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
 
 /**
@@ -68,7 +69,7 @@
  *   A render array, localized string, or object that can be rendered into
  *   a string, containing the help text.
  */
-function hook_help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match) {
+function hook_help($route_name, RouteMatchInterface $route_match) {
   switch ($route_name) {
     // Main module help for the block module.
     case 'help.page.block':
diff --git a/core/modules/help/help.module b/core/modules/help/help.module
index 7e0d807044786af322102d6c8b8a826aa9fb8e97..91edfa9aabc957df2e9309f9ed014b1ffc919030 100644
--- a/core/modules/help/help.module
+++ b/core/modules/help/help.module
@@ -2,88 +2,8 @@
 
 /**
  * @file
- * Manages displaying online help.
  */
 
-use Drupal\Core\Url;
-use Drupal\Core\Block\BlockPluginInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function help_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.main':
-      $output = '<h2>' . t('Getting Started') . '</h2>';
-      $output .= '<p>' . t('Follow these steps to set up and start using your website:') . '</p>';
-      $output .= '<ol>';
-      $output .= '<li>' . t('<strong>Configure your website</strong> Once logged in, visit the <a href=":admin">Administration page</a>, where you may <a href=":config">customize and configure</a> all aspects of your website.', [':admin' => Url::fromRoute('system.admin')->toString(), ':config' => Url::fromRoute('system.admin_config')->toString()]) . '</li>';
-      $output .= '<li>' . t('<strong>Enable additional functionality</strong> Next, visit the <a href=":modules">Extend page</a> and install modules that suit your specific needs. You can find additional modules at the <a href=":download_modules">Drupal.org modules page</a>.', [':modules' => Url::fromRoute('system.modules_list')->toString(), ':download_modules' => 'https://www.drupal.org/project/modules']) . '</li>';
-      $output .= '<li>' . t('<strong>Customize your website design</strong> To change the "look and feel" of your website, visit the <a href=":themes">Appearance page</a>. You may choose from one of the included themes or download additional themes from the <a href=":download_themes">Drupal.org themes page</a>.', [':themes' => Url::fromRoute('system.themes_page')->toString(), ':download_themes' => 'https://www.drupal.org/project/themes']) . '</li>';
-      // Display a link to the create content page if Node module is installed.
-      if (\Drupal::moduleHandler()->moduleExists('node')) {
-        $output .= '<li>' . t('<strong>Start posting content</strong> Finally, you may <a href=":content">add new content</a> to your website.', [':content' => Url::fromRoute('node.add_page')->toString()]) . '</li>';
-      }
-      $output .= '</ol>';
-      $output .= '<p>' . t('For more information, refer to the help listed on this page or to the <a href=":docs">online documentation</a> and <a href=":support">support</a> pages at <a href=":drupal">drupal.org</a>.', [':docs' => 'https://www.drupal.org/documentation', ':support' => 'https://www.drupal.org/support', ':drupal' => 'https://www.drupal.org']) . '</p>';
-      return ['#markup' => $output];
-
-    case 'help.page.help':
-      $help_home = Url::fromRoute('help.main')->toString();
-      $module_handler = \Drupal::moduleHandler();
-      $locale_help = ($module_handler->moduleExists('locale')) ? Url::fromRoute('help.page', ['name' => 'locale'])->toString() : '#';
-      $search_help = ($module_handler->moduleExists('search')) ? Url::fromRoute('help.page', ['name' => 'search'])->toString() : '#';
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Help module generates <a href=":help-page">Help topics and reference pages</a> to guide you through the use and configuration of modules, and provides a Help block with page-level help. The reference pages are a starting point for <a href=":handbook">Drupal.org online documentation</a> pages that contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the <a href=":help">online documentation for the Help module</a>.', [':help' => 'https://www.drupal.org/documentation/modules/help/', ':handbook' => 'https://www.drupal.org/documentation', ':help-page' => Url::fromRoute('help.main')->toString()]) . '</p>';
-      $output .= '<p>' . t('Help topics provided by modules and themes are also part of the Help module. If the core Search module is installed, these topics are searchable. For more information, see the <a href=":online">online documentation, Help Topic Standards</a>.', [':online' => 'https://www.drupal.org/docs/develop/managing-a-drupalorg-theme-module-or-distribution-project/documenting-your-project/help-topic-standards']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Providing a help reference') . '</dt>';
-      $output .= '<dd>' . t('The Help module displays explanations for using each module listed on the main <a href=":help">Help reference page</a>.', [':help' => Url::fromRoute('help.main')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Providing page-specific help') . '</dt>';
-      $output .= '<dd>' . t('Page-specific help text provided by modules is displayed in the Help block. This block can be placed and configured on the <a href=":blocks">Block layout page</a>.', [':blocks' => (\Drupal::moduleHandler()->moduleExists('block')) ? Url::fromRoute('block.admin_display')->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Viewing help topics') . '</dt>';
-      $output .= '<dd>' . t('The top-level help topics are listed on the main <a href=":help_page">Help page</a>. Links to other topics, including non-top-level help topics, can be found under the "Related" heading when viewing a topic page.', [':help_page' => $help_home]) . '</dd>';
-      $output .= '<dt>' . t('Providing help topics') . '</dt>';
-      $output .= '<dd>' . t("Modules and themes can provide help topics as Twig-file-based plugins in a project sub-directory called <em>help_topics</em>; plugin meta-data is provided in YAML front matter within each Twig file. Plugin-based help topics provided by modules and themes will automatically be updated when a module or theme is updated. Use the plugins in <em>core/modules/help/help_topics</em> as a guide when writing and formatting a help topic plugin for your theme or module.") . '</dd>';
-      $output .= '<dt>' . t('Translating help topics') . '</dt>';
-      $output .= '<dd>' . t('The title and body text of help topics provided by contributed modules and themes are translatable using the <a href=":locale_help">Interface Translation module</a>. Topics provided by custom modules and themes are also translatable if they have been viewed at least once in a non-English language, which triggers putting their translatable text into the translation database.', [':locale_help' => $locale_help]) . '</dd>';
-      $output .= '<dt>' . t('Configuring help search') . '</dt>';
-      $output .= '<dd>' . t('To search help, you will need to install the core Search module, configure a search page, and add a search block to the Help page or another administrative page. (A search page is provided automatically, and if you use the core Claro administrative theme, a help search block is shown on the main Help page.) Then users with search permissions, and permission to view help, will be able to search help. See the <a href=":search_help">Search module help page</a> for more information.', [':search_help' => $search_help]) . '</dd>';
-      $output .= '</dl>';
-      return ['#markup' => $output];
-
-    case 'help.help_topic':
-      $help_home = Url::fromRoute('help.main')->toString();
-      return '<p>' . t('See the <a href=":help_page">Help page</a> for more topics.', [
-        ':help_page' => $help_home,
-      ]) . '</p>';
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function help_theme($existing, $type, $theme, $path): array {
-  return [
-    'help_section' => [
-      'variables' => [
-        'title' => NULL,
-        'description' => NULL,
-        'links' => NULL,
-        'empty' => NULL,
-      ],
-    ],
-    'help_topic' => [
-      'variables' => [
-        'body' => [],
-        'related' => [],
-      ],
-    ],
-  ];
-}
-
 /**
  * Implements hook_preprocess_HOOK() for block templates.
  */
@@ -93,45 +13,6 @@ function help_preprocess_block(&$variables) {
   }
 }
 
-/**
- * Implements hook_block_view_BASE_BLOCK_ID_alter().
- */
-function help_block_view_help_block_alter(array &$build, BlockPluginInterface $block) {
-  // Assume that most users do not need or want to perform contextual actions on
-  // the help block, so don't needlessly draw attention to it.
-  unset($build['#contextual_links']);
-}
-
-/**
- * Implements hook_modules_uninstalled().
- */
-function help_modules_uninstalled(array $modules) {
-  _help_search_update($modules);
-}
-
-/**
- * Implements hook_themes_uninstalled().
- */
-function help_themes_uninstalled(array $themes) {
-  \Drupal::service('plugin.cache_clearer')->clearCachedDefinitions();
-  _help_search_update();
-}
-
-/**
- * Implements hook_modules_installed().
- */
-function help_modules_installed(array $modules, $is_syncing) {
-  _help_search_update();
-}
-
-/**
- * Implements hook_themes_installed().
- */
-function help_themes_installed(array $themes) {
-  \Drupal::service('plugin.cache_clearer')->clearCachedDefinitions();
-  _help_search_update();
-}
-
 /**
  * Ensure that search is updated when extensions are installed or uninstalled.
  *
diff --git a/core/modules/help/src/Hook/HelpHooks.php b/core/modules/help/src/Hook/HelpHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..4cb7c40963b9fbf28d112a32ff3361a52ac8041f
--- /dev/null
+++ b/core/modules/help/src/Hook/HelpHooks.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Drupal\help\Hook;
+
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for help.
+ */
+class HelpHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.main':
+        $output = '<h2>' . t('Getting Started') . '</h2>';
+        $output .= '<p>' . t('Follow these steps to set up and start using your website:') . '</p>';
+        $output .= '<ol>';
+        $output .= '<li>' . t('<strong>Configure your website</strong> Once logged in, visit the <a href=":admin">Administration page</a>, where you may <a href=":config">customize and configure</a> all aspects of your website.', [
+          ':admin' => Url::fromRoute('system.admin')->toString(),
+          ':config' => Url::fromRoute('system.admin_config')->toString(),
+        ]) . '</li>';
+        $output .= '<li>' . t('<strong>Enable additional functionality</strong> Next, visit the <a href=":modules">Extend page</a> and install modules that suit your specific needs. You can find additional modules at the <a href=":download_modules">Drupal.org modules page</a>.', [
+          ':modules' => Url::fromRoute('system.modules_list')->toString(),
+          ':download_modules' => 'https://www.drupal.org/project/modules',
+        ]) . '</li>';
+        $output .= '<li>' . t('<strong>Customize your website design</strong> To change the "look and feel" of your website, visit the <a href=":themes">Appearance page</a>. You may choose from one of the included themes or download additional themes from the <a href=":download_themes">Drupal.org themes page</a>.', [
+          ':themes' => Url::fromRoute('system.themes_page')->toString(),
+          ':download_themes' => 'https://www.drupal.org/project/themes',
+        ]) . '</li>';
+        // Display a link to the create content page if Node module is installed.
+        if (\Drupal::moduleHandler()->moduleExists('node')) {
+          $output .= '<li>' . t('<strong>Start posting content</strong> Finally, you may <a href=":content">add new content</a> to your website.', [':content' => Url::fromRoute('node.add_page')->toString()]) . '</li>';
+        }
+        $output .= '</ol>';
+        $output .= '<p>' . t('For more information, refer to the help listed on this page or to the <a href=":docs">online documentation</a> and <a href=":support">support</a> pages at <a href=":drupal">drupal.org</a>.', [
+          ':docs' => 'https://www.drupal.org/documentation',
+          ':support' => 'https://www.drupal.org/support',
+          ':drupal' => 'https://www.drupal.org',
+        ]) . '</p>';
+        return ['#markup' => $output];
+
+      case 'help.page.help':
+        $help_home = Url::fromRoute('help.main')->toString();
+        $module_handler = \Drupal::moduleHandler();
+        $locale_help = $module_handler->moduleExists('locale') ? Url::fromRoute('help.page', ['name' => 'locale'])->toString() : '#';
+        $search_help = $module_handler->moduleExists('search') ? Url::fromRoute('help.page', ['name' => 'search'])->toString() : '#';
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Help module generates <a href=":help-page">Help topics and reference pages</a> to guide you through the use and configuration of modules, and provides a Help block with page-level help. The reference pages are a starting point for <a href=":handbook">Drupal.org online documentation</a> pages that contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the <a href=":help">online documentation for the Help module</a>.', [
+          ':help' => 'https://www.drupal.org/documentation/modules/help/',
+          ':handbook' => 'https://www.drupal.org/documentation',
+          ':help-page' => Url::fromRoute('help.main')->toString(),
+        ]) . '</p>';
+        $output .= '<p>' . t('Help topics provided by modules and themes are also part of the Help module. If the core Search module is installed, these topics are searchable. For more information, see the <a href=":online">online documentation, Help Topic Standards</a>.', [
+          ':online' => 'https://www.drupal.org/docs/develop/managing-a-drupalorg-theme-module-or-distribution-project/documenting-your-project/help-topic-standards',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Providing a help reference') . '</dt>';
+        $output .= '<dd>' . t('The Help module displays explanations for using each module listed on the main <a href=":help">Help reference page</a>.', [':help' => Url::fromRoute('help.main')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Providing page-specific help') . '</dt>';
+        $output .= '<dd>' . t('Page-specific help text provided by modules is displayed in the Help block. This block can be placed and configured on the <a href=":blocks">Block layout page</a>.', [
+          ':blocks' => \Drupal::moduleHandler()->moduleExists('block') ? Url::fromRoute('block.admin_display')->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Viewing help topics') . '</dt>';
+        $output .= '<dd>' . t('The top-level help topics are listed on the main <a href=":help_page">Help page</a>. Links to other topics, including non-top-level help topics, can be found under the "Related" heading when viewing a topic page.', [':help_page' => $help_home]) . '</dd>';
+        $output .= '<dt>' . t('Providing help topics') . '</dt>';
+        $output .= '<dd>' . t("Modules and themes can provide help topics as Twig-file-based plugins in a project sub-directory called <em>help_topics</em>; plugin meta-data is provided in YAML front matter within each Twig file. Plugin-based help topics provided by modules and themes will automatically be updated when a module or theme is updated. Use the plugins in <em>core/modules/help/help_topics</em> as a guide when writing and formatting a help topic plugin for your theme or module.") . '</dd>';
+        $output .= '<dt>' . t('Translating help topics') . '</dt>';
+        $output .= '<dd>' . t('The title and body text of help topics provided by contributed modules and themes are translatable using the <a href=":locale_help">Interface Translation module</a>. Topics provided by custom modules and themes are also translatable if they have been viewed at least once in a non-English language, which triggers putting their translatable text into the translation database.', [':locale_help' => $locale_help]) . '</dd>';
+        $output .= '<dt>' . t('Configuring help search') . '</dt>';
+        $output .= '<dd>' . t('To search help, you will need to install the core Search module, configure a search page, and add a search block to the Help page or another administrative page. (A search page is provided automatically, and if you use the core Claro administrative theme, a help search block is shown on the main Help page.) Then users with search permissions, and permission to view help, will be able to search help. See the <a href=":search_help">Search module help page</a> for more information.', [':search_help' => $search_help]) . '</dd>';
+        $output .= '</dl>';
+        return ['#markup' => $output];
+
+      case 'help.help_topic':
+        $help_home = Url::fromRoute('help.main')->toString();
+        return '<p>' . t('See the <a href=":help_page">Help page</a> for more topics.', [':help_page' => $help_home]) . '</p>';
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme($existing, $type, $theme, $path) : array {
+    return [
+      'help_section' => [
+        'variables' => [
+          'title' => NULL,
+          'description' => NULL,
+          'links' => NULL,
+          'empty' => NULL,
+        ],
+      ],
+      'help_topic' => [
+        'variables' => [
+          'body' => [],
+          'related' => [],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_block_view_BASE_BLOCK_ID_alter().
+   */
+  #[Hook('block_view_help_block_alter')]
+  public function blockViewHelpBlockAlter(array &$build, BlockPluginInterface $block) {
+    // Assume that most users do not need or want to perform contextual actions on
+    // the help block, so don't needlessly draw attention to it.
+    unset($build['#contextual_links']);
+  }
+
+  /**
+   * Implements hook_modules_uninstalled().
+   */
+  #[Hook('modules_uninstalled')]
+  public function modulesUninstalled(array $modules) {
+    _help_search_update($modules);
+  }
+
+  /**
+   * Implements hook_themes_uninstalled().
+   */
+  #[Hook('themes_uninstalled')]
+  public function themesUninstalled(array $themes) {
+    \Drupal::service('plugin.cache_clearer')->clearCachedDefinitions();
+    _help_search_update();
+  }
+
+  /**
+   * Implements hook_modules_installed().
+   */
+  #[Hook('modules_installed')]
+  public function modulesInstalled(array $modules, $is_syncing) {
+    _help_search_update();
+  }
+
+  /**
+   * Implements hook_themes_installed().
+   */
+  #[Hook('themes_installed')]
+  public function themesInstalled(array $themes) {
+    \Drupal::service('plugin.cache_clearer')->clearCachedDefinitions();
+    _help_search_update();
+  }
+
+}
diff --git a/core/modules/help/tests/modules/help_page_test/help_page_test.module b/core/modules/help/tests/modules/help_page_test/help_page_test.module
deleted file mode 100644
index 2119c11547289093cb7c2a2b3ad21eee50e3f998..0000000000000000000000000000000000000000
--- a/core/modules/help/tests/modules/help_page_test/help_page_test.module
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-/**
- * @file
- * Help Page Test module to test the help page.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function help_page_test_help($route_name, RouteMatchInterface $route_match) {
-
-  switch ($route_name) {
-    case 'help.page.help_page_test':
-      // Make the help text conform to core standards. See
-      // \Drupal\system\Tests\Functional\GenericModuleTestBase::assertHookHelp().
-      return t('Read the <a href=":url">online documentation for the Help Page Test module</a>.', [':url' => 'http://www.example.com']);
-
-    case 'help_page_test.has_help':
-      return t('I have help!');
-
-    case 'help_page_test.test_array':
-      return ['#markup' => 'Help text from help_page_test_help module.'];
-  }
-
-  // Ensure that hook_help() can return an empty string and not cause the block
-  // to display.
-  return '';
-}
diff --git a/core/modules/help/tests/modules/help_page_test/src/Hook/HelpPageTestHooks.php b/core/modules/help/tests/modules/help_page_test/src/Hook/HelpPageTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..9563a4458e51236f6d5f5ab2bd6b616148a412e1
--- /dev/null
+++ b/core/modules/help/tests/modules/help_page_test/src/Hook/HelpPageTestHooks.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\help_page_test\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for help_page_test.
+ */
+class HelpPageTestHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.help_page_test':
+        // Make the help text conform to core standards. See
+        // \Drupal\system\Tests\Functional\GenericModuleTestBase::assertHookHelp().
+        return t('Read the <a href=":url">online documentation for the Help Page Test module</a>.', [':url' => 'http://www.example.com']);
+
+      case 'help_page_test.has_help':
+        return t('I have help!');
+
+      case 'help_page_test.test_array':
+        return ['#markup' => 'Help text from help_page_test_help module.'];
+    }
+    // Ensure that hook_help() can return an empty string and not cause the block
+    // to display.
+    return '';
+  }
+
+}
diff --git a/core/modules/help/tests/modules/help_test/help_test.module b/core/modules/help/tests/modules/help_test/help_test.module
deleted file mode 100644
index a88dfe527633dc36bf25f53b3fd2ba7d2ef45aa9..0000000000000000000000000000000000000000
--- a/core/modules/help/tests/modules/help_test/help_test.module
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-/**
- * @file
- * Test Help module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function help_test_help($route_name, RouteMatchInterface $route_match) {
-  // Do not implement a module overview page to test an empty implementation.
-  // @see \Drupal\help\Tests\HelpTest
-}
diff --git a/core/modules/help/tests/modules/help_test/src/Hook/HelpTestHooks.php b/core/modules/help/tests/modules/help_test/src/Hook/HelpTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..e716664ff48b55bf15523005f2111050dbfca1b2
--- /dev/null
+++ b/core/modules/help/tests/modules/help_test/src/Hook/HelpTestHooks.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\help_test\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for help_test.
+ */
+class HelpTestHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    // Do not implement a module overview page to test an empty implementation.
+    // @see \Drupal\help\Tests\HelpTest
+  }
+
+}
diff --git a/core/modules/help/tests/modules/help_topics_test/help_topics_test.module b/core/modules/help/tests/modules/help_topics_test/help_topics_test.module
deleted file mode 100644
index 95f32931d4547d0cd25cf4bbb3477bd2d07fdde4..0000000000000000000000000000000000000000
--- a/core/modules/help/tests/modules/help_topics_test/help_topics_test.module
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module for help.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function help_topics_test_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.help_topics_test':
-      return 'Some kind of non-empty output for testing';
-  }
-}
-
-/**
- * Implements hook_help_topics_info_alter().
- */
-function help_topics_test_help_topics_info_alter(array &$info) {
-  // To prevent false positive search results limit list to testing topis only.
-  $filter = fn(string $key) => str_starts_with($key, 'help_topics_test') || in_array($key, [
-    'help_topics_test_direct_yml',
-    'help_topics_derivatives:test_derived_topic',
-  ], TRUE);
-  $info = array_filter($info, $filter, ARRAY_FILTER_USE_KEY);
-  $info['help_topics_test.test']['top_level'] = \Drupal::state()->get('help_topics_test.test:top_level', TRUE);
-}
diff --git a/core/modules/help/tests/modules/help_topics_test/src/Hook/HelpTopicsTestHooks.php b/core/modules/help/tests/modules/help_topics_test/src/Hook/HelpTopicsTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..05856c2ce938485b57b3e5abe51014fd63859127
--- /dev/null
+++ b/core/modules/help/tests/modules/help_topics_test/src/Hook/HelpTopicsTestHooks.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\help_topics_test\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for help_topics_test.
+ */
+class HelpTopicsTestHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.help_topics_test':
+        return 'Some kind of non-empty output for testing';
+    }
+  }
+
+  /**
+   * Implements hook_help_topics_info_alter().
+   */
+  #[Hook('help_topics_info_alter')]
+  public function helpTopicsInfoAlter(array &$info) {
+    // To prevent false positive search results limit list to testing topis only.
+    $filter = fn(string $key) => str_starts_with($key, 'help_topics_test') || in_array($key, ['help_topics_test_direct_yml', 'help_topics_derivatives:test_derived_topic'], TRUE);
+    $info = array_filter($info, $filter, ARRAY_FILTER_USE_KEY);
+    $info['help_topics_test.test']['top_level'] = \Drupal::state()->get('help_topics_test.test:top_level', TRUE);
+  }
+
+}
diff --git a/core/modules/help/tests/modules/more_help_page_test/more_help_page_test.module b/core/modules/help/tests/modules/more_help_page_test/more_help_page_test.module
deleted file mode 100644
index de4a8d3bc419ff8fd75b2ecf50ae1a5953d5deb4..0000000000000000000000000000000000000000
--- a/core/modules/help/tests/modules/more_help_page_test/more_help_page_test.module
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-/**
- * @file
- * More Help Page Test module to test the help blocks.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function more_help_page_test_help($route_name, RouteMatchInterface $route_match) {
-
-  switch ($route_name) {
-    // Return help for the same route as the help_page_test module.
-    case 'help_page_test.test_array':
-      return ['#markup' => 'Help text from more_help_page_test_help module.'];
-  }
-}
-
-/**
- * Implements hook_help_section_info_alter().
- */
-function more_help_page_test_help_section_info_alter(array &$info) {
-  $info['hook_help']['weight'] = 500;
-}
diff --git a/core/modules/help/tests/modules/more_help_page_test/src/Hook/MoreHelpPageTestHooks.php b/core/modules/help/tests/modules/more_help_page_test/src/Hook/MoreHelpPageTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..337344e786bbd22c8a890bae673caeeffc1b9df4
--- /dev/null
+++ b/core/modules/help/tests/modules/more_help_page_test/src/Hook/MoreHelpPageTestHooks.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\more_help_page_test\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for more_help_page_test.
+ */
+class MoreHelpPageTestHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      // Return help for the same route as the help_page_test module.
+      case 'help_page_test.test_array':
+        return ['#markup' => 'Help text from more_help_page_test_help module.'];
+    }
+  }
+
+  /**
+   * Implements hook_help_section_info_alter().
+   */
+  #[Hook('help_section_info_alter')]
+  public function helpSectionInfoAlter(array &$info) {
+    $info['hook_help']['weight'] = 500;
+  }
+
+}
diff --git a/core/modules/history/history.module b/core/modules/history/history.module
index ca271da5a765a57ad187641c745f95303f43f238..1d53c536054607004a55757ee3fdc92abec86704 100644
--- a/core/modules/history/history.module
+++ b/core/modules/history/history.module
@@ -2,17 +2,8 @@
 
 /**
  * @file
- * Records which users have read which content.
- *
- * @todo Generic helper for node_mark().
  */
 
-use Drupal\Core\Url;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\user\UserInterface;
-
 /**
  * Entities changed before this time are always shown as read.
  *
@@ -23,18 +14,6 @@
  */
 define('HISTORY_READ_LIMIT', ((int) $_SERVER['REQUEST_TIME']) - 30 * 24 * 60 * 60);
 
-/**
- * Implements hook_help().
- */
-function history_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.history':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The History module keeps track of which content a user has read. It marks content as <em>new</em> or <em>updated</em> depending on the last time the user viewed it. History records that are older than one month are removed during cron, which means that content older than one month is always considered <em>read</em>. The History module does not have a user interface but it provides a filter to <a href=":views-help">Views</a> to show new or updated content. For more information, see the <a href=":url">online documentation for the History module</a>.', [':views-help' => (\Drupal::moduleHandler()->moduleExists('views')) ? Url::fromRoute('help.page', ['name' => 'views'])->toString() : '#', ':url' => 'https://www.drupal.org/documentation/modules/history']) . '</p>';
-      return $output;
-  }
-}
-
 /**
  * Retrieves the timestamp for the current user's last view of a specified node.
  *
@@ -123,64 +102,3 @@ function history_write($nid, $account = NULL) {
     $history[$nid] = $request_time;
   }
 }
-
-/**
- * Implements hook_cron().
- */
-function history_cron() {
-  \Drupal::database()->delete('history')
-    ->condition('timestamp', HISTORY_READ_LIMIT, '<')
-    ->execute();
-}
-
-/**
- * Implements hook_ENTITY_TYPE_view_alter() for node entities.
- */
-function history_node_view_alter(array &$build, EntityInterface $node, EntityViewDisplayInterface $display) {
-  if ($node->isNew() || isset($node->in_preview)) {
-    return;
-  }
-  // Update the history table, stating that this user viewed this node.
-  if ($display->getOriginalMode() === 'full') {
-    $build['#cache']['contexts'][] = 'user.roles:authenticated';
-    if (\Drupal::currentUser()->isAuthenticated()) {
-      // When the window's "load" event is triggered, mark the node as read.
-      // This still allows for Drupal behaviors (which are triggered on the
-      // "DOMContentReady" event) to add "new" and "updated" indicators.
-      $build['#attached']['library'][] = 'history/mark-as-read';
-      $build['#attached']['drupalSettings']['history']['nodesToMarkAsRead'][$node->id()] = TRUE;
-    }
-  }
-
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for node entities.
- */
-function history_node_delete(EntityInterface $node) {
-  \Drupal::database()->delete('history')
-    ->condition('nid', $node->id())
-    ->execute();
-}
-
-/**
- * Implements hook_user_cancel().
- */
-function history_user_cancel($edit, UserInterface $account, $method) {
-  switch ($method) {
-    case 'user_cancel_reassign':
-      \Drupal::database()->delete('history')
-        ->condition('uid', $account->id())
-        ->execute();
-      break;
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for user entities.
- */
-function history_user_delete($account) {
-  \Drupal::database()->delete('history')
-    ->condition('uid', $account->id())
-    ->execute();
-}
diff --git a/core/modules/history/history.views.inc b/core/modules/history/history.views.inc
deleted file mode 100644
index 60c95cd868a34bf38d07029ea5c9924b2d80f910..0000000000000000000000000000000000000000
--- a/core/modules/history/history.views.inc
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views data for history.module.
- */
-
-/**
- * Implements hook_views_data().
- */
-function history_views_data() {
-  // History table
-
-  // We're actually defining a specific instance of the table, so let's
-  // alias it so that we can later add the real table for other purposes if we
-  // need it.
-  $data['history']['table']['group'] = t('Content');
-
-  // Explain how this table joins to others.
-  $data['history']['table']['join'] = [
-     // Directly links to node table.
-    'node_field_data' => [
-      'table' => 'history',
-      'left_field' => 'nid',
-      'field' => 'nid',
-      'extra' => [
-        ['field' => 'uid', 'value' => '***CURRENT_USER***', 'numeric' => TRUE],
-      ],
-    ],
-  ];
-
-  $data['history']['timestamp'] = [
-    'title' => t('Has new content'),
-    'field' => [
-      'id' => 'history_user_timestamp',
-      'help' => t('Show a marker if the content is new or updated.'),
-    ],
-    'filter' => [
-      'help' => t('Show only content that is new or updated.'),
-      'id' => 'history_user_timestamp',
-    ],
-  ];
-
-  return $data;
-}
diff --git a/core/modules/history/src/Hook/HistoryHooks.php b/core/modules/history/src/Hook/HistoryHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..0857332b9193a80963d4fe501d978c26e3786fad
--- /dev/null
+++ b/core/modules/history/src/Hook/HistoryHooks.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\history\Hook;
+
+use Drupal\user\UserInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for history.
+ */
+class HistoryHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.history':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The History module keeps track of which content a user has read. It marks content as <em>new</em> or <em>updated</em> depending on the last time the user viewed it. History records that are older than one month are removed during cron, which means that content older than one month is always considered <em>read</em>. The History module does not have a user interface but it provides a filter to <a href=":views-help">Views</a> to show new or updated content. For more information, see the <a href=":url">online documentation for the History module</a>.', [
+          ':views-help' => \Drupal::moduleHandler()->moduleExists('views') ? Url::fromRoute('help.page', [
+            'name' => 'views',
+          ])->toString() : '#',
+          ':url' => 'https://www.drupal.org/documentation/modules/history',
+        ]) . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_cron().
+   */
+  #[Hook('cron')]
+  public function cron() {
+    \Drupal::database()->delete('history')->condition('timestamp', HISTORY_READ_LIMIT, '<')->execute();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_view_alter() for node entities.
+   */
+  #[Hook('node_view_alter')]
+  public function nodeViewAlter(array &$build, EntityInterface $node, EntityViewDisplayInterface $display) {
+    if ($node->isNew() || isset($node->in_preview)) {
+      return;
+    }
+    // Update the history table, stating that this user viewed this node.
+    if ($display->getOriginalMode() === 'full') {
+      $build['#cache']['contexts'][] = 'user.roles:authenticated';
+      if (\Drupal::currentUser()->isAuthenticated()) {
+        // When the window's "load" event is triggered, mark the node as read.
+        // This still allows for Drupal behaviors (which are triggered on the
+        // "DOMContentReady" event) to add "new" and "updated" indicators.
+        $build['#attached']['library'][] = 'history/mark-as-read';
+        $build['#attached']['drupalSettings']['history']['nodesToMarkAsRead'][$node->id()] = TRUE;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for node entities.
+   */
+  #[Hook('node_delete')]
+  public function nodeDelete(EntityInterface $node) {
+    \Drupal::database()->delete('history')->condition('nid', $node->id())->execute();
+  }
+
+  /**
+   * Implements hook_user_cancel().
+   */
+  #[Hook('user_cancel')]
+  public function userCancel($edit, UserInterface $account, $method) {
+    switch ($method) {
+      case 'user_cancel_reassign':
+        \Drupal::database()->delete('history')->condition('uid', $account->id())->execute();
+        break;
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for user entities.
+   */
+  #[Hook('user_delete')]
+  public function userDelete($account) {
+    \Drupal::database()->delete('history')->condition('uid', $account->id())->execute();
+  }
+
+}
diff --git a/core/modules/history/src/Hook/HistoryViewsHooks.php b/core/modules/history/src/Hook/HistoryViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c36464200522dcf659259c5c7040816b98290947
--- /dev/null
+++ b/core/modules/history/src/Hook/HistoryViewsHooks.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\history\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for history.
+ */
+class HistoryViewsHooks {
+  /**
+   * @file
+   * Provide views data for history.module.
+   */
+
+  /**
+   * Implements hook_views_data().
+   */
+  #[Hook('views_data')]
+  public function viewsData() {
+    // History table
+    // We're actually defining a specific instance of the table, so let's
+    // alias it so that we can later add the real table for other purposes if we
+    // need it.
+    $data['history']['table']['group'] = t('Content');
+    // Explain how this table joins to others.
+    $data['history']['table']['join'] = [
+          // Directly links to node table.
+      'node_field_data' => [
+        'table' => 'history',
+        'left_field' => 'nid',
+        'field' => 'nid',
+        'extra' => [
+                  [
+                    'field' => 'uid',
+                    'value' => '***CURRENT_USER***',
+                    'numeric' => TRUE,
+                  ],
+        ],
+      ],
+    ];
+    $data['history']['timestamp'] = [
+      'title' => t('Has new content'),
+      'field' => [
+        'id' => 'history_user_timestamp',
+        'help' => t('Show a marker if the content is new or updated.'),
+      ],
+      'filter' => [
+        'help' => t('Show only content that is new or updated.'),
+        'id' => 'history_user_timestamp',
+      ],
+    ];
+    return $data;
+  }
+
+}
diff --git a/core/modules/image/image.admin.inc b/core/modules/image/image.admin.inc
index 73b7ffdf9e1008b38e5b57984df6cb99615dbc58..e515fd64cee572272d07084634313a76b1139c0a 100644
--- a/core/modules/image/image.admin.inc
+++ b/core/modules/image/image.admin.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Administration pages for image settings.
  */
 
 use Drupal\Core\Render\Element;
diff --git a/core/modules/image/image.field.inc b/core/modules/image/image.field.inc
index 30942fc032b540401bf32dbf0938170bf6fbe1b1..9c1883d5044a1aade1d0c8a5da1d918da8d63c13 100644
--- a/core/modules/image/image.field.inc
+++ b/core/modules/image/image.field.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Implement an image field, based on the file module's file field.
  */
 
 use Drupal\Core\Render\Element;
diff --git a/core/modules/image/image.module b/core/modules/image/image.module
index 864be56cf41ab2e239e85c640d976b591a8969ee..52d946250718ffbc16393cbed345f123fc1e0fbe 100644
--- a/core/modules/image/image.module
+++ b/core/modules/image/image.module
@@ -2,18 +2,8 @@
 
 /**
  * @file
- * Exposes global functionality for creating image styles.
  */
 
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\File\FileSystemInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\StreamWrapper\StreamWrapperManager;
-use Drupal\Core\Url;
-use Drupal\field\FieldConfigInterface;
-use Drupal\field\FieldStorageConfigInterface;
-use Drupal\file\FileInterface;
-use Drupal\image\Controller\ImageStyleDownloadController;
 use Drupal\image\Entity\ImageStyle;
 
 /**
@@ -21,197 +11,6 @@
  */
 define('IMAGE_DERIVATIVE_TOKEN', 'itok');
 
-/**
- * Implements hook_help().
- */
-function image_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.image':
-      $field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
-
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Image module allows you to create fields that contain image files and to configure <a href=":image_styles">Image styles</a> that can be used to manipulate the display of images. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for terminology and general information on entities, fields, and how to create and manage fields. For more information, see the <a href=":image_documentation">online documentation for the Image module</a>.', [':image_styles' => Url::fromRoute('entity.image_style.collection')->toString(), ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => $field_ui_url, ':image_documentation' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/image-module/working-with-images']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dt>' . t('Defining image styles') . '</dt>';
-      $output .= '<dd>' . t('The concept of image styles is that you can upload a single image but display it in several ways; each display variation, or <em>image style</em>, is the result of applying one or more <em>effects</em> to the original image. As an example, you might upload a high-resolution image with a 4:3 aspect ratio, and display it scaled down, square cropped, or black-and-white (or any combination of these effects). The Image module provides a way to do this efficiently: you configure an image style with the desired effects on the <a href=":image">Image styles page</a>, and the first time a particular image is requested in that style, the effects are applied. The resulting image is saved, and the next time that same style is requested, the saved image is retrieved without the need to recalculate the effects. Drupal core provides several effects that you can use to define styles; others may be provided by contributed modules.', [':image' => Url::fromRoute('entity.image_style.collection')->toString()]);
-      $output .= '<dt>' . t('Naming image styles') . '</dt>';
-      $output .= '<dd>' . t('When you define an image style, you will need to choose a displayed name and a machine name. The displayed name is shown in administrative pages, and the machine name is used to generate the URL for accessing an image processed in that style. There are two common approaches to naming image styles: either based on the effects being applied (for example, <em>Square 85x85</em>), or based on where you plan to use it (for example, <em>Profile picture</em>).') . '</dd>';
-      $output .= '<dt>' . t('Configuring image fields') . '</dt>';
-      $output .= '<dd>' . t('A few of the settings for image fields are defined once when you create the field and cannot be changed later; these include the choice of public or private file storage and the number of images that can be stored in the field. The rest of the settings can be edited later; these settings include the field label, help text, allowed file extensions, image dimensions restrictions, and the subdirectory in the public or private file storage where the images will be stored. The editable settings can also have different values for different entity sub-types; for instance, if your image field is used on both Page and Article content types, you can store the files in a different subdirectory for the two content types.') . '</dd>';
-      $output .= '<dd>' . t('For accessibility and search engine optimization, all images that convey meaning on websites should have alternate text. Drupal also allows entry of title text for images, but it can lead to confusion for screen reader users and its use is not recommended. Image fields can be configured so that alternate and title text fields are enabled or disabled; if enabled, the fields can be set to be required. The recommended setting is to enable and require alternate text and disable title text.') . '</dd>';
-      $output .= '<dd>' . t('When you create an image field, you will need to choose whether the uploaded images will be stored in the public or private file directory defined in your settings.php file and shown on the <a href=":file-system">File system page</a>. This choice cannot be changed later. You can also configure your field to store files in a subdirectory of the public or private directory; this setting can be changed later and can be different for each entity sub-type using the field. For more information on file storage, see the <a href=":system-help">System module help page</a>.', [':file-system' => Url::fromRoute('system.file_system_settings')->toString(), ':system-help' => Url::fromRoute('help.page', ['name' => 'system'])->toString()]) . '</dd>';
-      $output .= '<dd>' . t('The maximum file size that can be uploaded is limited by PHP settings of the server, but you can restrict it further by configuring a <em>Maximum upload size</em> in the field settings (this setting can be changed later). The maximum file size, either from PHP server settings or field configuration, is automatically displayed to users in the help text of the image field.') . '</dd>';
-      $output .= '<dd>' . t('You can also configure a minimum and/or maximum dimensions for uploaded images. Images that are too small will be rejected. Images that are to large will be resized. During the resizing the <a href="http://wikipedia.org/wiki/Exchangeable_image_file_format">EXIF data</a> in the image will be lost.') . '</dd>';
-      $output .= '<dd>' . t('You can also configure a default image that will be used if no image is uploaded in an image field. This default can be defined for all instances of the field in the field storage settings when you create a field, and the setting can be overridden for each entity sub-type that uses the field.') . '</dd>';
-      $output .= '<dt>' . t('Configuring displays and form displays') . '</dt>';
-      $output .= '<dd>' . t('On the <em>Manage display</em> page, you can choose the image formatter, which determines the image style used to display the image in each display mode and whether or not to display the image as a link. On the <em>Manage form display</em> page, you can configure the image upload widget, including setting the preview image style shown on the entity edit form.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'entity.image_style.collection':
-      return '<p>' . t('Image styles commonly provide thumbnail sizes by scaling and cropping images, but can also add various effects before an image is displayed. When an image is displayed with a style, a new file is created and the original image is left unchanged.') . '</p>';
-
-    case 'image.effect_add_form':
-      $effect = \Drupal::service('plugin.manager.image.effect')->getDefinition($route_match->getParameter('image_effect'));
-      return isset($effect['description']) ? ('<p>' . $effect['description'] . '</p>') : NULL;
-
-    case 'image.effect_edit_form':
-      $effect = $route_match->getParameter('image_style')->getEffect($route_match->getParameter('image_effect'));
-      $effect_definition = $effect->getPluginDefinition();
-      return isset($effect_definition['description']) ? ('<p>' . $effect_definition['description'] . '</p>') : NULL;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function image_theme(): array {
-  return [
-    // Theme functions in image.module.
-    'image_style' => [
-      // HTML 4 and XHTML 1.0 always require an alt attribute. The HTML 5 draft
-      // allows the alt attribute to be omitted in some cases. Therefore,
-      // default the alt attribute to an empty string, but allow code using
-      // '#theme' => 'image_style' to pass explicit NULL for it to be omitted.
-      // Usually, neither omission nor an empty string satisfies accessibility
-      // requirements, so it is strongly encouraged for code using '#theme' =>
-      // 'image_style' to pass a meaningful value for the alt variable.
-      // - https://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8
-      // - https://www.w3.org/TR/xhtml1/dtds.html
-      // - http://dev.w3.org/html5/spec/Overview.html#alt
-      // The title attribute is optional in all cases, so it is omitted by
-      // default.
-      'variables' => [
-        'style_name' => NULL,
-        'uri' => NULL,
-        'width' => NULL,
-        'height' => NULL,
-        'alt' => '',
-        'title' => NULL,
-        'attributes' => [],
-      ],
-    ],
-
-    // Theme functions in image.admin.inc.
-    'image_style_preview' => [
-      'variables' => ['style' => NULL],
-      'file' => 'image.admin.inc',
-    ],
-    'image_anchor' => [
-      'render element' => 'element',
-      'file' => 'image.admin.inc',
-    ],
-    'image_resize_summary' => [
-      'variables' => ['data' => NULL, 'effect' => []],
-    ],
-    'image_scale_summary' => [
-      'variables' => ['data' => NULL, 'effect' => []],
-    ],
-    'image_crop_summary' => [
-      'variables' => ['data' => NULL, 'effect' => []],
-    ],
-    'image_scale_and_crop_summary' => [
-      'variables' => ['data' => NULL, 'effect' => []],
-    ],
-    'image_rotate_summary' => [
-      'variables' => ['data' => NULL, 'effect' => []],
-    ],
-
-    // Theme functions in image.field.inc.
-    'image_widget' => [
-      'render element' => 'element',
-      'file' => 'image.field.inc',
-    ],
-    'image_formatter' => [
-      'variables' => ['item' => NULL, 'item_attributes' => NULL, 'url' => NULL, 'image_style' => NULL],
-      'file' => 'image.field.inc',
-    ],
-  ];
-}
-
-/**
- * Implements hook_file_download().
- *
- * Control the access to files underneath the styles directory.
- */
-function image_file_download($uri) {
-
-  $path = StreamWrapperManager::getTarget($uri);
-
-  // Private file access for image style derivatives.
-  if (str_starts_with($path, 'styles/')) {
-    $args = explode('/', $path);
-
-    // Discard "styles", style name, and scheme from the path
-    $args = array_slice($args, 3);
-
-    // Then the remaining parts are the path to the image.
-    $original_uri = StreamWrapperManager::getScheme($uri) . '://' . implode('/', $args);
-
-    // Check that the file exists and is an image.
-    $image = \Drupal::service('image.factory')->get($uri);
-    if ($image->isValid()) {
-      // If the image style converted the extension, it has been added to the
-      // original file, resulting in filenames like image.png.jpeg. So to find
-      // the actual source image, we remove the extension and check if that
-      // image exists.
-      if (!file_exists($original_uri)) {
-        $converted_original_uri = ImageStyleDownloadController::getUriWithoutConvertedExtension($original_uri);
-        if ($converted_original_uri !== $original_uri && file_exists($converted_original_uri)) {
-          // The converted file does exist, use it as the source.
-          $original_uri = $converted_original_uri;
-        }
-      }
-
-      // Check the permissions of the original to grant access to this image.
-      $headers = \Drupal::moduleHandler()->invokeAll('file_download', [$original_uri]);
-      // Confirm there's at least one module granting access and none denying access.
-      if (!empty($headers) && !in_array(-1, $headers)) {
-        return [
-          // Send headers describing the image's size, and MIME-type.
-          'Content-Type' => $image->getMimeType(),
-          'Content-Length' => $image->getFileSize(),
-          // By not explicitly setting them here, this uses normal Drupal
-          // Expires, Cache-Control and ETag headers to prevent proxy or
-          // browser caching of private images.
-        ];
-      }
-    }
-    return -1;
-  }
-
-  // If it is the sample image we need to grant access.
-  $samplePath = \Drupal::config('image.settings')->get('preview_image');
-  if ($path === $samplePath) {
-    $image = \Drupal::service('image.factory')->get($samplePath);
-    return [
-      // Send headers describing the image's size, and MIME-type.
-      'Content-Type' => $image->getMimeType(),
-      'Content-Length' => $image->getFileSize(),
-      // By not explicitly setting them here, this uses normal Drupal
-      // Expires, Cache-Control and ETag headers to prevent proxy or
-      // browser caching of private images.
-    ];
-  }
-}
-
-/**
- * Implements hook_file_move().
- */
-function image_file_move(FileInterface $file, FileInterface $source) {
-  // Delete any image derivatives at the original image path.
-  image_path_flush($source->getFileUri());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for file entities.
- */
-function image_file_predelete(FileInterface $file) {
-  // Delete any image derivatives of this image.
-  image_path_flush($file->getFileUri());
-}
-
 /**
  * Clears cached versions of a specific file in all styles.
  *
@@ -360,156 +159,3 @@ function image_filter_keyword($anchor, $current_size, $new_size) {
       return $anchor;
   }
 }
-
-/**
- * Implements hook_entity_presave().
- *
- * Transforms default image of image field from array into single value at save.
- */
-function image_entity_presave(EntityInterface $entity) {
-  // Get the default image settings, return if not saving an image field storage
-  // or image field entity.
-  $default_image = [];
-  if (($entity instanceof FieldStorageConfigInterface || $entity instanceof FieldConfigInterface) && $entity->getType() == 'image') {
-    $default_image = $entity->getSetting('default_image');
-  }
-  else {
-    return;
-  }
-
-  if ($entity->isSyncing()) {
-    return;
-  }
-
-  $uuid = $default_image['uuid'];
-  if ($uuid) {
-    $original_uuid = isset($entity->original) ? $entity->original->getSetting('default_image')['uuid'] : NULL;
-    if ($uuid != $original_uuid) {
-      $file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid);
-      if ($file) {
-        $image = \Drupal::service('image.factory')->get($file->getFileUri());
-        $default_image['width'] = $image->getWidth();
-        $default_image['height'] = $image->getHeight();
-      }
-      else {
-        $default_image['uuid'] = NULL;
-      }
-    }
-  }
-  // Both FieldStorageConfigInterface and FieldConfigInterface have a
-  // setSetting() method.
-  $entity->setSetting('default_image', $default_image);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for 'field_storage_config'.
- */
-function image_field_storage_config_update(FieldStorageConfigInterface $field_storage) {
-  if ($field_storage->getType() != 'image') {
-    // Only act on image fields.
-    return;
-  }
-
-  $prior_field_storage = $field_storage->original;
-
-  // The value of a managed_file element can be an array if #extended == TRUE.
-  $uuid_new = $field_storage->getSetting('default_image')['uuid'];
-  $uuid_old = $prior_field_storage->getSetting('default_image')['uuid'];
-
-  $file_new = $uuid_new ? \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid_new) : FALSE;
-
-  if ($uuid_new != $uuid_old) {
-
-    // Is there a new file?
-    if ($file_new) {
-      $file_new->setPermanent();
-      $file_new->save();
-      \Drupal::service('file.usage')->add($file_new, 'image', 'default_image', $field_storage->uuid());
-    }
-
-    // Is there an old file?
-    if ($uuid_old && ($file_old = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid_old))) {
-      \Drupal::service('file.usage')->delete($file_old, 'image', 'default_image', $field_storage->uuid());
-    }
-  }
-
-  // If the upload destination changed, then move the file.
-  if ($file_new && (StreamWrapperManager::getScheme($file_new->getFileUri()) != $field_storage->getSetting('uri_scheme'))) {
-    $directory = $field_storage->getSetting('uri_scheme') . '://default_images/';
-    \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
-    \Drupal::service('file.repository')->move($file_new, $directory . $file_new->getFilename());
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for 'field_config'.
- */
-function image_field_config_update(FieldConfigInterface $field) {
-  $field_storage = $field->getFieldStorageDefinition();
-  if ($field_storage->getType() != 'image') {
-    // Only act on image fields.
-    return;
-  }
-
-  $prior_instance = $field->original;
-
-  $uuid_new = $field->getSetting('default_image')['uuid'];
-  $uuid_old = $prior_instance->getSetting('default_image')['uuid'];
-
-  // If the old and new files do not match, update the default accordingly.
-  $file_new = $uuid_new ? \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid_new) : FALSE;
-  if ($uuid_new != $uuid_old) {
-    // Save the new file, if present.
-    if ($file_new) {
-      $file_new->setPermanent();
-      $file_new->save();
-      \Drupal::service('file.usage')->add($file_new, 'image', 'default_image', $field->uuid());
-    }
-    // Delete the old file, if present.
-    if ($uuid_old && ($file_old = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid_old))) {
-      \Drupal::service('file.usage')->delete($file_old, 'image', 'default_image', $field->uuid());
-    }
-  }
-
-  // If the upload destination changed, then move the file.
-  if ($file_new && (StreamWrapperManager::getScheme($file_new->getFileUri()) != $field_storage->getSetting('uri_scheme'))) {
-    $directory = $field_storage->getSetting('uri_scheme') . '://default_images/';
-    \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
-    \Drupal::service('file.repository')->move($file_new, $directory . $file_new->getFilename());
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for 'field_storage_config'.
- */
-function image_field_storage_config_delete(FieldStorageConfigInterface $field) {
-  if ($field->getType() != 'image') {
-    // Only act on image fields.
-    return;
-  }
-
-  // The value of a managed_file element can be an array if #extended == TRUE.
-  $uuid = $field->getSetting('default_image')['uuid'];
-  if ($uuid && ($file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid))) {
-    \Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->uuid());
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for 'field_config'.
- */
-function image_field_config_delete(FieldConfigInterface $field) {
-  $field_storage = $field->getFieldStorageDefinition();
-  if ($field_storage->getType() != 'image') {
-    // Only act on image fields.
-    return;
-  }
-
-  // The value of a managed_file element can be an array if #extended == TRUE.
-  $uuid = $field->getSetting('default_image')['uuid'];
-
-  // Remove the default image when the instance is deleted.
-  if ($uuid && ($file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid))) {
-    \Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->uuid());
-  }
-}
diff --git a/core/modules/image/image.views.inc b/core/modules/image/image.views.inc
deleted file mode 100644
index 553678ccd83537691150dcc0c9a5fce22c6b803c..0000000000000000000000000000000000000000
--- a/core/modules/image/image.views.inc
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views data for image.module.
- */
-
-use Drupal\field\FieldStorageConfigInterface;
-
-/**
- * Implements hook_field_views_data().
- *
- * Views integration for image fields. Adds an image relationship to the default
- * field data.
- *
- * @see views_field_default_views_data()
- */
-function image_field_views_data(FieldStorageConfigInterface $field_storage) {
-  $data = views_field_default_views_data($field_storage);
-  foreach ($data as $table_name => $table_data) {
-    // Add the relationship only on the target_id field.
-    $data[$table_name][$field_storage->getName() . '_target_id']['relationship'] = [
-      'id' => 'standard',
-      'base' => 'file_managed',
-      'entity type' => 'file',
-      'base field' => 'fid',
-      'label' => t('image from @field_name', ['@field_name' => $field_storage->getName()]),
-    ];
-  }
-
-  return $data;
-}
-
-/**
- * Implements hook_field_views_data_views_data_alter().
- *
- * Views integration to provide reverse relationships on image fields.
- */
-function image_field_views_data_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
-  $entity_type_id = $field_storage->getTargetEntityTypeId();
-  $field_name = $field_storage->getName();
-  $entity_type_manager = \Drupal::entityTypeManager();
-  $entity_type = $entity_type_manager->getDefinition($entity_type_id);
-  $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
-  /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
-  $table_mapping = $entity_type_manager->getStorage($entity_type_id)->getTableMapping();
-
-  [$label] = views_entity_field_label($entity_type_id, $field_name);
-
-  $data['file_managed'][$pseudo_field_name]['relationship'] = [
-    'title' => t('@entity using @field', ['@entity' => $entity_type->getLabel(), '@field' => $label]),
-    'label' => t('@field_name', ['@field_name' => $field_name]),
-    'help' => t('Relate each @entity with a @field set to the image.', ['@entity' => $entity_type->getLabel(), '@field' => $label]),
-    'group' => $entity_type->getLabel(),
-    'id' => 'entity_reverse',
-    'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
-    'entity_type' => $entity_type_id,
-    'base field' => $entity_type->getKey('id'),
-    'field_name' => $field_name,
-    'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
-    'field field' => $field_name . '_target_id',
-    'join_extra' => [
-      0 => [
-        'field' => 'deleted',
-        'value' => 0,
-        'numeric' => TRUE,
-      ],
-    ],
-  ];
-}
diff --git a/core/modules/image/src/Hook/ImageHooks.php b/core/modules/image/src/Hook/ImageHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..45fab8c3fa56456a2ac0616d820a595d02e2001f
--- /dev/null
+++ b/core/modules/image/src/Hook/ImageHooks.php
@@ -0,0 +1,379 @@
+<?php
+
+namespace Drupal\image\Hook;
+
+use Drupal\Core\File\FileSystemInterface;
+use Drupal\field\FieldConfigInterface;
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\file\FileInterface;
+use Drupal\image\Controller\ImageStyleDownloadController;
+use Drupal\Core\StreamWrapper\StreamWrapperManager;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for image.
+ */
+class ImageHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.image':
+        $field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Image module allows you to create fields that contain image files and to configure <a href=":image_styles">Image styles</a> that can be used to manipulate the display of images. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for terminology and general information on entities, fields, and how to create and manage fields. For more information, see the <a href=":image_documentation">online documentation for the Image module</a>.', [
+          ':image_styles' => Url::fromRoute('entity.image_style.collection')->toString(),
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+          ':field_ui' => $field_ui_url,
+          ':image_documentation' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/image-module/working-with-images',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dt>' . t('Defining image styles') . '</dt>';
+        $output .= '<dd>' . t('The concept of image styles is that you can upload a single image but display it in several ways; each display variation, or <em>image style</em>, is the result of applying one or more <em>effects</em> to the original image. As an example, you might upload a high-resolution image with a 4:3 aspect ratio, and display it scaled down, square cropped, or black-and-white (or any combination of these effects). The Image module provides a way to do this efficiently: you configure an image style with the desired effects on the <a href=":image">Image styles page</a>, and the first time a particular image is requested in that style, the effects are applied. The resulting image is saved, and the next time that same style is requested, the saved image is retrieved without the need to recalculate the effects. Drupal core provides several effects that you can use to define styles; others may be provided by contributed modules.', [
+          ':image' => Url::fromRoute('entity.image_style.collection')->toString(),
+        ]);
+        $output .= '<dt>' . t('Naming image styles') . '</dt>';
+        $output .= '<dd>' . t('When you define an image style, you will need to choose a displayed name and a machine name. The displayed name is shown in administrative pages, and the machine name is used to generate the URL for accessing an image processed in that style. There are two common approaches to naming image styles: either based on the effects being applied (for example, <em>Square 85x85</em>), or based on where you plan to use it (for example, <em>Profile picture</em>).') . '</dd>';
+        $output .= '<dt>' . t('Configuring image fields') . '</dt>';
+        $output .= '<dd>' . t('A few of the settings for image fields are defined once when you create the field and cannot be changed later; these include the choice of public or private file storage and the number of images that can be stored in the field. The rest of the settings can be edited later; these settings include the field label, help text, allowed file extensions, image dimensions restrictions, and the subdirectory in the public or private file storage where the images will be stored. The editable settings can also have different values for different entity sub-types; for instance, if your image field is used on both Page and Article content types, you can store the files in a different subdirectory for the two content types.') . '</dd>';
+        $output .= '<dd>' . t('For accessibility and search engine optimization, all images that convey meaning on websites should have alternate text. Drupal also allows entry of title text for images, but it can lead to confusion for screen reader users and its use is not recommended. Image fields can be configured so that alternate and title text fields are enabled or disabled; if enabled, the fields can be set to be required. The recommended setting is to enable and require alternate text and disable title text.') . '</dd>';
+        $output .= '<dd>' . t('When you create an image field, you will need to choose whether the uploaded images will be stored in the public or private file directory defined in your settings.php file and shown on the <a href=":file-system">File system page</a>. This choice cannot be changed later. You can also configure your field to store files in a subdirectory of the public or private directory; this setting can be changed later and can be different for each entity sub-type using the field. For more information on file storage, see the <a href=":system-help">System module help page</a>.', [
+          ':file-system' => Url::fromRoute('system.file_system_settings')->toString(),
+          ':system-help' => Url::fromRoute('help.page', [
+            'name' => 'system',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dd>' . t('The maximum file size that can be uploaded is limited by PHP settings of the server, but you can restrict it further by configuring a <em>Maximum upload size</em> in the field settings (this setting can be changed later). The maximum file size, either from PHP server settings or field configuration, is automatically displayed to users in the help text of the image field.') . '</dd>';
+        $output .= '<dd>' . t('You can also configure a minimum and/or maximum dimensions for uploaded images. Images that are too small will be rejected. Images that are to large will be resized. During the resizing the <a href="http://wikipedia.org/wiki/Exchangeable_image_file_format">EXIF data</a> in the image will be lost.') . '</dd>';
+        $output .= '<dd>' . t('You can also configure a default image that will be used if no image is uploaded in an image field. This default can be defined for all instances of the field in the field storage settings when you create a field, and the setting can be overridden for each entity sub-type that uses the field.') . '</dd>';
+        $output .= '<dt>' . t('Configuring displays and form displays') . '</dt>';
+        $output .= '<dd>' . t('On the <em>Manage display</em> page, you can choose the image formatter, which determines the image style used to display the image in each display mode and whether or not to display the image as a link. On the <em>Manage form display</em> page, you can configure the image upload widget, including setting the preview image style shown on the entity edit form.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'entity.image_style.collection':
+        return '<p>' . t('Image styles commonly provide thumbnail sizes by scaling and cropping images, but can also add various effects before an image is displayed. When an image is displayed with a style, a new file is created and the original image is left unchanged.') . '</p>';
+
+      case 'image.effect_add_form':
+        $effect = \Drupal::service('plugin.manager.image.effect')->getDefinition($route_match->getParameter('image_effect'));
+        return isset($effect['description']) ? '<p>' . $effect['description'] . '</p>' : NULL;
+
+      case 'image.effect_edit_form':
+        $effect = $route_match->getParameter('image_style')->getEffect($route_match->getParameter('image_effect'));
+        $effect_definition = $effect->getPluginDefinition();
+        return isset($effect_definition['description']) ? '<p>' . $effect_definition['description'] . '</p>' : NULL;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+          // Theme functions in image.module.
+      'image_style' => [
+              // HTML 4 and XHTML 1.0 always require an alt attribute. The HTML 5 draft
+              // allows the alt attribute to be omitted in some cases. Therefore,
+              // default the alt attribute to an empty string, but allow code using
+              // '#theme' => 'image_style' to pass explicit NULL for it to be omitted.
+              // Usually, neither omission nor an empty string satisfies accessibility
+              // requirements, so it is strongly encouraged for code using '#theme' =>
+              // 'image_style' to pass a meaningful value for the alt variable.
+              // - https://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8
+              // - https://www.w3.org/TR/xhtml1/dtds.html
+              // - http://dev.w3.org/html5/spec/Overview.html#alt
+              // The title attribute is optional in all cases, so it is omitted by
+              // default.
+        'variables' => [
+          'style_name' => NULL,
+          'uri' => NULL,
+          'width' => NULL,
+          'height' => NULL,
+          'alt' => '',
+          'title' => NULL,
+          'attributes' => [],
+        ],
+      ],
+          // Theme functions in image.admin.inc.
+      'image_style_preview' => [
+        'variables' => [
+          'style' => NULL,
+        ],
+        'file' => 'image.admin.inc',
+      ],
+      'image_anchor' => [
+        'render element' => 'element',
+        'file' => 'image.admin.inc',
+      ],
+      'image_resize_summary' => [
+        'variables' => [
+          'data' => NULL,
+          'effect' => [],
+        ],
+      ],
+      'image_scale_summary' => [
+        'variables' => [
+          'data' => NULL,
+          'effect' => [],
+        ],
+      ],
+      'image_crop_summary' => [
+        'variables' => [
+          'data' => NULL,
+          'effect' => [],
+        ],
+      ],
+      'image_scale_and_crop_summary' => [
+        'variables' => [
+          'data' => NULL,
+          'effect' => [],
+        ],
+      ],
+      'image_rotate_summary' => [
+        'variables' => [
+          'data' => NULL,
+          'effect' => [],
+        ],
+      ],
+          // Theme functions in image.field.inc.
+      'image_widget' => [
+        'render element' => 'element',
+        'file' => 'image.field.inc',
+      ],
+      'image_formatter' => [
+        'variables' => [
+          'item' => NULL,
+          'item_attributes' => NULL,
+          'url' => NULL,
+          'image_style' => NULL,
+        ],
+        'file' => 'image.field.inc',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_file_download().
+   *
+   * Control the access to files underneath the styles directory.
+   */
+  #[Hook('file_download')]
+  public function fileDownload($uri) {
+    $path = StreamWrapperManager::getTarget($uri);
+    // Private file access for image style derivatives.
+    if (str_starts_with($path, 'styles/')) {
+      $args = explode('/', $path);
+      // Discard "styles", style name, and scheme from the path
+      $args = array_slice($args, 3);
+      // Then the remaining parts are the path to the image.
+      $original_uri = StreamWrapperManager::getScheme($uri) . '://' . implode('/', $args);
+      // Check that the file exists and is an image.
+      $image = \Drupal::service('image.factory')->get($uri);
+      if ($image->isValid()) {
+        // If the image style converted the extension, it has been added to the
+        // original file, resulting in filenames like image.png.jpeg. So to find
+        // the actual source image, we remove the extension and check if that
+        // image exists.
+        if (!file_exists($original_uri)) {
+          $converted_original_uri = ImageStyleDownloadController::getUriWithoutConvertedExtension($original_uri);
+          if ($converted_original_uri !== $original_uri && file_exists($converted_original_uri)) {
+            // The converted file does exist, use it as the source.
+            $original_uri = $converted_original_uri;
+          }
+        }
+        // Check the permissions of the original to grant access to this image.
+        $headers = \Drupal::moduleHandler()->invokeAll('file_download', [$original_uri]);
+        // Confirm there's at least one module granting access and none denying access.
+        if (!empty($headers) && !in_array(-1, $headers)) {
+          return [
+                // Send headers describing the image's size, and MIME-type.
+            'Content-Type' => $image->getMimeType(),
+            'Content-Length' => $image->getFileSize(),
+          ];
+        }
+      }
+      return -1;
+    }
+    // If it is the sample image we need to grant access.
+    $samplePath = \Drupal::config('image.settings')->get('preview_image');
+    if ($path === $samplePath) {
+      $image = \Drupal::service('image.factory')->get($samplePath);
+      return [
+            // Send headers describing the image's size, and MIME-type.
+        'Content-Type' => $image->getMimeType(),
+        'Content-Length' => $image->getFileSize(),
+      ];
+    }
+  }
+
+  /**
+   * Implements hook_file_move().
+   */
+  #[Hook('file_move')]
+  public function fileMove(FileInterface $file, FileInterface $source) {
+    // Delete any image derivatives at the original image path.
+    image_path_flush($source->getFileUri());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for file entities.
+   */
+  #[Hook('file_predelete')]
+  public function filePredelete(FileInterface $file) {
+    // Delete any image derivatives of this image.
+    image_path_flush($file->getFileUri());
+  }
+
+  /**
+   * Implements hook_entity_presave().
+   *
+   * Transforms default image of image field from array into single value at save.
+   */
+  #[Hook('entity_presave')]
+  public function entityPresave(EntityInterface $entity) {
+    // Get the default image settings, return if not saving an image field storage
+    // or image field entity.
+    $default_image = [];
+    if (($entity instanceof FieldStorageConfigInterface || $entity instanceof FieldConfigInterface) && $entity->getType() == 'image') {
+      $default_image = $entity->getSetting('default_image');
+    }
+    else {
+      return;
+    }
+    if ($entity->isSyncing()) {
+      return;
+    }
+    $uuid = $default_image['uuid'];
+    if ($uuid) {
+      $original_uuid = isset($entity->original) ? $entity->original->getSetting('default_image')['uuid'] : NULL;
+      if ($uuid != $original_uuid) {
+        $file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid);
+        if ($file) {
+          $image = \Drupal::service('image.factory')->get($file->getFileUri());
+          $default_image['width'] = $image->getWidth();
+          $default_image['height'] = $image->getHeight();
+        }
+        else {
+          $default_image['uuid'] = NULL;
+        }
+      }
+    }
+    // Both FieldStorageConfigInterface and FieldConfigInterface have a
+    // setSetting() method.
+    $entity->setSetting('default_image', $default_image);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for 'field_storage_config'.
+   */
+  #[Hook('field_storage_config_update')]
+  public function fieldStorageConfigUpdate(FieldStorageConfigInterface $field_storage) {
+    if ($field_storage->getType() != 'image') {
+      // Only act on image fields.
+      return;
+    }
+    $prior_field_storage = $field_storage->original;
+    // The value of a managed_file element can be an array if #extended == TRUE.
+    $uuid_new = $field_storage->getSetting('default_image')['uuid'];
+    $uuid_old = $prior_field_storage->getSetting('default_image')['uuid'];
+    $file_new = $uuid_new ? \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid_new) : FALSE;
+    if ($uuid_new != $uuid_old) {
+      // Is there a new file?
+      if ($file_new) {
+        $file_new->setPermanent();
+        $file_new->save();
+        \Drupal::service('file.usage')->add($file_new, 'image', 'default_image', $field_storage->uuid());
+      }
+      // Is there an old file?
+      if ($uuid_old && ($file_old = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid_old))) {
+        \Drupal::service('file.usage')->delete($file_old, 'image', 'default_image', $field_storage->uuid());
+      }
+    }
+    // If the upload destination changed, then move the file.
+    if ($file_new && StreamWrapperManager::getScheme($file_new->getFileUri()) != $field_storage->getSetting('uri_scheme')) {
+      $directory = $field_storage->getSetting('uri_scheme') . '://default_images/';
+      \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
+      \Drupal::service('file.repository')->move($file_new, $directory . $file_new->getFilename());
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for 'field_config'.
+   */
+  #[Hook('field_config_update')]
+  public function fieldConfigUpdate(FieldConfigInterface $field) {
+    $field_storage = $field->getFieldStorageDefinition();
+    if ($field_storage->getType() != 'image') {
+      // Only act on image fields.
+      return;
+    }
+    $prior_instance = $field->original;
+    $uuid_new = $field->getSetting('default_image')['uuid'];
+    $uuid_old = $prior_instance->getSetting('default_image')['uuid'];
+    // If the old and new files do not match, update the default accordingly.
+    $file_new = $uuid_new ? \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid_new) : FALSE;
+    if ($uuid_new != $uuid_old) {
+      // Save the new file, if present.
+      if ($file_new) {
+        $file_new->setPermanent();
+        $file_new->save();
+        \Drupal::service('file.usage')->add($file_new, 'image', 'default_image', $field->uuid());
+      }
+      // Delete the old file, if present.
+      if ($uuid_old && ($file_old = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid_old))) {
+        \Drupal::service('file.usage')->delete($file_old, 'image', 'default_image', $field->uuid());
+      }
+    }
+    // If the upload destination changed, then move the file.
+    if ($file_new && StreamWrapperManager::getScheme($file_new->getFileUri()) != $field_storage->getSetting('uri_scheme')) {
+      $directory = $field_storage->getSetting('uri_scheme') . '://default_images/';
+      \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
+      \Drupal::service('file.repository')->move($file_new, $directory . $file_new->getFilename());
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for 'field_storage_config'.
+   */
+  #[Hook('field_storage_config_delete')]
+  public function fieldStorageConfigDelete(FieldStorageConfigInterface $field) {
+    if ($field->getType() != 'image') {
+      // Only act on image fields.
+      return;
+    }
+    // The value of a managed_file element can be an array if #extended == TRUE.
+    $uuid = $field->getSetting('default_image')['uuid'];
+    if ($uuid && ($file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid))) {
+      \Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->uuid());
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for 'field_config'.
+   */
+  #[Hook('field_config_delete')]
+  public function fieldConfigDelete(FieldConfigInterface $field) {
+    $field_storage = $field->getFieldStorageDefinition();
+    if ($field_storage->getType() != 'image') {
+      // Only act on image fields.
+      return;
+    }
+    // The value of a managed_file element can be an array if #extended == TRUE.
+    $uuid = $field->getSetting('default_image')['uuid'];
+    // Remove the default image when the instance is deleted.
+    if ($uuid && ($file = \Drupal::service('entity.repository')->loadEntityByUuid('file', $uuid))) {
+      \Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->uuid());
+    }
+  }
+
+}
diff --git a/core/modules/image/src/Hook/ImageViewsHooks.php b/core/modules/image/src/Hook/ImageViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..5334d130eb7d1130ec12c30b121aed6c8981d67a
--- /dev/null
+++ b/core/modules/image/src/Hook/ImageViewsHooks.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\image\Hook;
+
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for image.
+ */
+class ImageViewsHooks {
+
+  /**
+   * Implements hook_field_views_data().
+   *
+   * Views integration for image fields. Adds an image relationship to the default
+   * field data.
+   *
+   * @see views_field_default_views_data()
+   */
+  #[Hook('field_views_data')]
+  public function fieldViewsData(FieldStorageConfigInterface $field_storage) {
+    $data = views_field_default_views_data($field_storage);
+    foreach ($data as $table_name => $table_data) {
+      // Add the relationship only on the target_id field.
+      $data[$table_name][$field_storage->getName() . '_target_id']['relationship'] = [
+        'id' => 'standard',
+        'base' => 'file_managed',
+        'entity type' => 'file',
+        'base field' => 'fid',
+        'label' => t('image from @field_name', [
+          '@field_name' => $field_storage->getName(),
+        ]),
+      ];
+    }
+    return $data;
+  }
+
+  /**
+   * Implements hook_field_views_data_views_data_alter().
+   *
+   * Views integration to provide reverse relationships on image fields.
+   */
+  #[Hook('field_views_data_views_data_alter')]
+  public function fieldViewsDataViewsDataAlter(array &$data, FieldStorageConfigInterface $field_storage) {
+    $entity_type_id = $field_storage->getTargetEntityTypeId();
+    $field_name = $field_storage->getName();
+    $entity_type_manager = \Drupal::entityTypeManager();
+    $entity_type = $entity_type_manager->getDefinition($entity_type_id);
+    $pseudo_field_name = 'reverse_' . $field_name . '_' . $entity_type_id;
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
+    $table_mapping = $entity_type_manager->getStorage($entity_type_id)->getTableMapping();
+    [$label] = views_entity_field_label($entity_type_id, $field_name);
+    $data['file_managed'][$pseudo_field_name]['relationship'] = [
+      'title' => t('@entity using @field', [
+        '@entity' => $entity_type->getLabel(),
+        '@field' => $label,
+      ]),
+      'label' => t('@field_name', [
+        '@field_name' => $field_name,
+      ]),
+      'help' => t('Relate each @entity with a @field set to the image.', [
+        '@entity' => $entity_type->getLabel(),
+        '@field' => $label,
+      ]),
+      'group' => $entity_type->getLabel(),
+      'id' => 'entity_reverse',
+      'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
+      'entity_type' => $entity_type_id,
+      'base field' => $entity_type->getKey('id'),
+      'field_name' => $field_name,
+      'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
+      'field field' => $field_name . '_target_id',
+      'join_extra' => [
+        0 => [
+          'field' => 'deleted',
+          'value' => 0,
+          'numeric' => TRUE,
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/image/tests/modules/image_access_test_hidden/image_access_test_hidden.module b/core/modules/image/tests/modules/image_access_test_hidden/image_access_test_hidden.module
deleted file mode 100644
index ad22524c57400a2bc926fa21b0c719ab56cddbfb..0000000000000000000000000000000000000000
--- a/core/modules/image/tests/modules/image_access_test_hidden/image_access_test_hidden.module
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-/**
- * @file
- * Image field access for hidden fields.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Access\AccessResult;
-
-/**
- * Implements hook_entity_field_access().
- */
-function image_access_test_hidden_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
-  if ($field_definition->getName() == 'field_image' && $operation == 'edit') {
-    return AccessResult::forbidden();
-  }
-  return AccessResult::neutral();
-}
diff --git a/core/modules/image/tests/modules/image_access_test_hidden/src/Hook/ImageAccessTestHiddenHooks.php b/core/modules/image/tests/modules/image_access_test_hidden/src/Hook/ImageAccessTestHiddenHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..13ea926a6c1efcb766e3763f5d8f91c3e4000cdf
--- /dev/null
+++ b/core/modules/image/tests/modules/image_access_test_hidden/src/Hook/ImageAccessTestHiddenHooks.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\image_access_test_hidden\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for image_access_test_hidden.
+ */
+class ImageAccessTestHiddenHooks {
+
+  /**
+   * Implements hook_entity_field_access().
+   */
+  #[Hook('entity_field_access')]
+  public function entityFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
+    if ($field_definition->getName() == 'field_image' && $operation == 'edit') {
+      return AccessResult::forbidden();
+    }
+    return AccessResult::neutral();
+  }
+
+}
diff --git a/core/modules/image/tests/modules/image_module_test/image_module_test.module b/core/modules/image/tests/modules/image_module_test/image_module_test.module
index ab5196319e3cb7516932629c9cf6be827ff622ca..a557f5acfe506e0aff6ea20c7b7cb4cb368b8033 100644
--- a/core/modules/image/tests/modules/image_module_test/image_module_test.module
+++ b/core/modules/image/tests/modules/image_module_test/image_module_test.module
@@ -7,44 +7,9 @@
 
 declare(strict_types=1);
 
-use Drupal\image\ImageStyleInterface;
-
 function image_module_test_file_download($uri) {
   $default_uri = \Drupal::state()->get('image.test_file_download', FALSE);
   if ($default_uri == $uri) {
     return ['X-Image-Owned-By' => 'image_module_test'];
   }
 }
-
-/**
- * Implements hook_image_effect_info_alter().
- */
-function image_module_test_image_effect_info_alter(&$effects) {
-  $state = \Drupal::state();
-  // The 'image_module_test.counter' state variable value is set and accessed
-  // from the ImageEffectsTest::testImageEffectsCaching() test and used to
-  // signal if the image effect plugin definitions were computed or were
-  // retrieved from the cache.
-  // @see \Drupal\Tests\image\Kernel\ImageEffectsTest::testImageEffectsCaching()
-  $counter = $state->get('image_module_test.counter');
-  // Increase the test counter, signaling that image effects were processed,
-  // rather than being served from the cache.
-  $state->set('image_module_test.counter', ++$counter);
-}
-
-/**
- * Implements hook_image_style_presave().
- *
- * Used to save test third party settings in the image style entity.
- */
-function image_module_test_image_style_presave(ImageStyleInterface $style) {
-  $style->setThirdPartySetting('image_module_test', 'foo', 'bar');
-}
-
-/**
- * Implements hook_image_style_flush().
- */
-function image_module_test_image_style_flush($style, $path = NULL) {
-  $state = \Drupal::state();
-  $state->set('image_module_test_image_style_flush.called', $path);
-}
diff --git a/core/modules/image/tests/modules/image_module_test/src/Hook/ImageModuleTestHooks.php b/core/modules/image/tests/modules/image_module_test/src/Hook/ImageModuleTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..1ae806ca92aac88b25dcda6d3002123269f3474c
--- /dev/null
+++ b/core/modules/image/tests/modules/image_module_test/src/Hook/ImageModuleTestHooks.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\image_module_test\Hook;
+
+use Drupal\image\ImageStyleInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for image_module_test.
+ */
+class ImageModuleTestHooks {
+
+  /**
+   * Implements hook_image_effect_info_alter().
+   */
+  #[Hook('image_effect_info_alter')]
+  public function imageEffectInfoAlter(&$effects) {
+    $state = \Drupal::state();
+    // The 'image_module_test.counter' state variable value is set and accessed
+    // from the ImageEffectsTest::testImageEffectsCaching() test and used to
+    // signal if the image effect plugin definitions were computed or were
+    // retrieved from the cache.
+    // @see \Drupal\Tests\image\Kernel\ImageEffectsTest::testImageEffectsCaching()
+    $counter = $state->get('image_module_test.counter');
+    // Increase the test counter, signaling that image effects were processed,
+    // rather than being served from the cache.
+    $state->set('image_module_test.counter', ++$counter);
+  }
+
+  /**
+   * Implements hook_image_style_presave().
+   *
+   * Used to save test third party settings in the image style entity.
+   */
+  #[Hook('image_style_presave')]
+  public function imageStylePresave(ImageStyleInterface $style) {
+    $style->setThirdPartySetting('image_module_test', 'foo', 'bar');
+  }
+
+  /**
+   * Implements hook_image_style_flush().
+   */
+  #[Hook('image_style_flush')]
+  public function imageStyleFlush($style, $path = NULL) {
+    $state = \Drupal::state();
+    $state->set('image_module_test_image_style_flush.called', $path);
+  }
+
+}
diff --git a/core/modules/inline_form_errors/inline_form_errors.module b/core/modules/inline_form_errors/inline_form_errors.module
index 66541146da35af1aa6a8a97a4d0f935b159b9f67..4d2f12be9edefc50e1379f7bbe48dc4719ee9c49 100644
--- a/core/modules/inline_form_errors/inline_form_errors.module
+++ b/core/modules/inline_form_errors/inline_form_errors.module
@@ -2,31 +2,8 @@
 
 /**
  * @file
- * Enables inline form errors.
  */
 
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\inline_form_errors\RenderElementHelper;
-
-/**
- * Implements hook_help().
- */
-function inline_form_errors_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.inline_form_errors':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Inline Form Errors module makes it easier for users to identify which errors need to be resolved by providing a summary of all errors and by placing the individual error messages next to the form elements themselves. For more information, see the <a href=":inline_form_error">online documentation for the Inline Form Errors module</a>.', [':inline_form_error' => 'https://www.drupal.org/docs/8/core/modules/inline-form-errors']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Displaying error messages') . '</dt>';
-      $output .= '<dd>' . t('When a form is not filled in correctly (for example, if a required field is left blank), a warning message is displayed at the top of the form. This message links to the affected form elements, and individual error messages are displayed next to each form element.') . '</dd>';
-      $output .= '<dt>' . t('Displaying error messages in browsers with HTML5 form validation') . '</dt>';
-      $output .= '<dd>' . t('In browsers that support HTML5 form validation, users will first see the error messages generated by their browser. In this case, the inline form error messages are only displayed after the HTML5 validation errors have been resolved.') . '</dd>';
-      return $output;
-  }
-}
-
 /**
  * Implements hook_preprocess_HOOK() for form element templates.
  */
@@ -55,13 +32,6 @@ function inline_form_errors_preprocess_datetime_wrapper(&$variables) {
   _inline_form_errors_set_errors($variables);
 }
 
-/**
- * Implements hook_element_info_alter().
- */
-function inline_form_errors_element_info_alter(array &$info) {
-  \Drupal::classResolver(RenderElementHelper::class)->alterElementInfo($info);
-}
-
 /**
  * Populates form errors in the template.
  */
diff --git a/core/modules/inline_form_errors/src/Hook/InlineFormErrorsHooks.php b/core/modules/inline_form_errors/src/Hook/InlineFormErrorsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..83b7739746ff4c889a535cb5f6394f14c3b98478
--- /dev/null
+++ b/core/modules/inline_form_errors/src/Hook/InlineFormErrorsHooks.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\inline_form_errors\Hook;
+
+use Drupal\inline_form_errors\RenderElementHelper;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for inline_form_errors.
+ */
+class InlineFormErrorsHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.inline_form_errors':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Inline Form Errors module makes it easier for users to identify which errors need to be resolved by providing a summary of all errors and by placing the individual error messages next to the form elements themselves. For more information, see the <a href=":inline_form_error">online documentation for the Inline Form Errors module</a>.', [
+          ':inline_form_error' => 'https://www.drupal.org/docs/8/core/modules/inline-form-errors',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Displaying error messages') . '</dt>';
+        $output .= '<dd>' . t('When a form is not filled in correctly (for example, if a required field is left blank), a warning message is displayed at the top of the form. This message links to the affected form elements, and individual error messages are displayed next to each form element.') . '</dd>';
+        $output .= '<dt>' . t('Displaying error messages in browsers with HTML5 form validation') . '</dt>';
+        $output .= '<dd>' . t('In browsers that support HTML5 form validation, users will first see the error messages generated by their browser. In this case, the inline form error messages are only displayed after the HTML5 validation errors have been resolved.') . '</dd>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_element_info_alter().
+   */
+  #[Hook('element_info_alter')]
+  public function elementInfoAlter(array &$info) {
+    \Drupal::classResolver(RenderElementHelper::class)->alterElementInfo($info);
+  }
+
+}
diff --git a/core/modules/jsonapi/jsonapi.api.php b/core/modules/jsonapi/jsonapi.api.php
index 4c024f814bcce07d21dec7d2a5176cb115cf0f41..54033a137c4c753619f22de0999a4d9483b856d8 100644
--- a/core/modules/jsonapi/jsonapi.api.php
+++ b/core/modules/jsonapi/jsonapi.api.php
@@ -5,6 +5,9 @@
  * Documentation related to JSON:API.
  */
 
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Access\AccessResult;
 
 /**
@@ -270,7 +273,7 @@
  *
  * @see hook_jsonapi_ENTITY_TYPE_filter_access()
  */
-function hook_jsonapi_entity_filter_access(\Drupal\Core\Entity\EntityTypeInterface $entity_type, \Drupal\Core\Session\AccountInterface $account) {
+function hook_jsonapi_entity_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
   // For every entity type that has an admin permission, allow access to filter
   // by all entities of that type to users with that permission.
   if ($admin_permission = $entity_type->getAdminPermission()) {
@@ -300,7 +303,7 @@ function hook_jsonapi_entity_filter_access(\Drupal\Core\Entity\EntityTypeInterfa
  *
  * @see hook_jsonapi_entity_filter_access()
  */
-function hook_jsonapi_ENTITY_TYPE_filter_access(\Drupal\Core\Entity\EntityTypeInterface $entity_type, \Drupal\Core\Session\AccountInterface $account) {
+function hook_jsonapi_ENTITY_TYPE_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
   return ([
     JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer llamas'),
     JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view all published llamas'),
@@ -338,7 +341,7 @@ function hook_jsonapi_ENTITY_TYPE_filter_access(\Drupal\Core\Entity\EntityTypeIn
  * @return \Drupal\Core\Access\AccessResultInterface
  *   The access result.
  */
-function hook_jsonapi_entity_field_filter_access(\Drupal\Core\Field\FieldDefinitionInterface $field_definition, \Drupal\Core\Session\AccountInterface $account) {
+function hook_jsonapi_entity_field_filter_access(FieldDefinitionInterface $field_definition, AccountInterface $account) {
   if ($field_definition->getTargetEntityTypeId() === 'node' && $field_definition->getName() === 'field_sensitive_data') {
     $has_sufficient_access = FALSE;
     foreach (['administer nodes', 'view all sensitive field data'] as $permission) {
diff --git a/core/modules/jsonapi/jsonapi.module b/core/modules/jsonapi/jsonapi.module
index 6f0cf21dc73b2ebdb4a6352a2dc427c69c2a8d2e..69414af650b9c7843d5240b4867fe698f214e83b 100644
--- a/core/modules/jsonapi/jsonapi.module
+++ b/core/modules/jsonapi/jsonapi.module
@@ -2,16 +2,8 @@
 
 /**
  * @file
- * Module implementation file.
  */
 
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\jsonapi\Routing\Routes as JsonApiRoutes;
-
 /**
  * Array key for denoting type-based filtering access.
  *
@@ -76,244 +68,3 @@
  * @see hook_jsonapi_ENTITY_TYPE_filter_access()
  */
 const JSONAPI_FILTER_AMONG_OWN = 'filter_among_own';
-
-/**
- * Implements hook_help().
- */
-function jsonapi_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.jsonapi':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The JSON:API module is a fully compliant implementation of the <a href=":spec">JSON:API Specification</a>. By following shared conventions, you can increase productivity, take advantage of generalized tooling, and focus on what matters: your application. Clients built around JSON:API are able to take advantage of features like efficient response caching, which can sometimes eliminate network requests entirely. For more information, see the <a href=":docs">online documentation for the JSON:API module</a>.', [
-        ':spec' => 'https://jsonapi.org',
-        ':docs' => 'https://www.drupal.org/docs/8/modules/json-api',
-      ]) . '</p>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('General') . '</dt>';
-      $output .= '<dd>' . t('JSON:API is a particular implementation of REST that provides conventions for resource relationships, collections, filters, pagination, and sorting. These conventions help developers build clients faster and encourages reuse of code.') . '</dd>';
-      $output .= '<dd>' . t('The <a href=":jsonapi-docs">JSON:API</a> and <a href=":rest-docs">RESTful Web Services</a> modules serve similar purposes. <a href=":comparison">Read the comparison of the RESTFul Web Services and JSON:API modules</a> to determine the best choice for your site.', [
-        ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/json-api',
-        ':rest-docs' => 'https://www.drupal.org/docs/8/core/modules/rest',
-        ':comparison' => 'https://www.drupal.org/docs/8/modules/jsonapi/jsonapi-vs-cores-rest-module',
-      ]) . '</dd>';
-      $output .= '<dd>' . t('Some multilingual features currently do not work well with JSON:API. See the <a href=":jsonapi-docs">JSON:API multilingual support documentation</a> for more information on the current status of multilingual support.', [
-        ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/jsonapi/translations',
-      ]) . '</dd>';
-      $output .= '<dd>' . t('Revision support is currently read-only and only for the "Content" and "Media" entity types in JSON:API. See the <a href=":jsonapi-docs">JSON:API revision support documentation</a> for more information on the current status of revision support.', [
-        ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/jsonapi/revisions',
-      ]) . '</dd>';
-      $output .= '</dl>';
-
-      return $output;
-  }
-  return NULL;
-}
-
-/**
- * Implements hook_modules_installed().
- */
-function jsonapi_modules_installed($modules) {
-  $potential_conflicts = [
-    'content_translation',
-    'config_translation',
-    'language',
-  ];
-  if (!empty(array_intersect($modules, $potential_conflicts))) {
-    \Drupal::messenger()->addWarning(t('Some multilingual features currently do not work well with JSON:API. See the <a href=":jsonapi-docs">JSON:API multilingual support documentation</a> for more information on the current status of multilingual support.', [
-      ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/jsonapi/translations',
-    ]));
-  }
-}
-
-/**
- * Implements hook_entity_bundle_create().
- */
-function jsonapi_entity_bundle_create() {
-  JsonApiRoutes::rebuild();
-}
-
-/**
- * Implements hook_entity_bundle_delete().
- */
-function jsonapi_entity_bundle_delete() {
-  JsonApiRoutes::rebuild();
-}
-
-/**
- * Implements hook_entity_create().
- */
-function jsonapi_entity_create(EntityInterface $entity) {
-  if (in_array($entity->getEntityTypeId(), ['field_storage_config', 'field_config'])) {
-    // @todo Only do this when relationship fields are updated, not just any field.
-    JsonApiRoutes::rebuild();
-  }
-}
-
-/**
- * Implements hook_entity_delete().
- */
-function jsonapi_entity_delete(EntityInterface $entity) {
-  if (in_array($entity->getEntityTypeId(), ['field_storage_config', 'field_config'])) {
-    // @todo Only do this when relationship fields are updated, not just any field.
-    JsonApiRoutes::rebuild();
-  }
-}
-
-/**
- * Implements hook_jsonapi_entity_filter_access().
- */
-function jsonapi_jsonapi_entity_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
-  // All core entity types and most or all contrib entity types allow users
-  // with the entity type's administrative permission to view all of the
-  // entities, so enable similarly permissive filtering to those users as well.
-  // A contrib module may override this decision by returning
-  // AccessResult::forbidden() from its implementation of this hook.
-  if ($admin_permission = $entity_type->getAdminPermission()) {
-    return ([
-      JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission),
-    ]);
-  }
-}
-
-/**
- * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'block_content'.
- */
-function jsonapi_jsonapi_block_content_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
-  // @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
-  // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
-  // (isReusable()), so this does not have to.
-  return ([
-    JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access block library'),
-    JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowed(),
-  ]);
-}
-
-/**
- * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'comment'.
- */
-function jsonapi_jsonapi_comment_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
-  // @see \Drupal\comment\CommentAccessControlHandler::checkAccess()
-  // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
-  // (access to the commented entity), so this does not have to.
-  return ([
-    JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer comments'),
-    JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access comments'),
-  ]);
-}
-
-/**
- * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'entity_test'.
- */
-function jsonapi_jsonapi_entity_test_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
-  // @see \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess()
-  return ([
-    JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view test entity'),
-  ]);
-}
-
-/**
- * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'file'.
- */
-function jsonapi_jsonapi_file_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
-  // @see \Drupal\file\FileAccessControlHandler::checkAccess()
-  // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
-  // (public OR owner), so this does not have to.
-  return ([
-    JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access content'),
-  ]);
-}
-
-/**
- * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'media'.
- */
-function jsonapi_jsonapi_media_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
-  // @see \Drupal\media\MediaAccessControlHandler::checkAccess()
-  return ([
-    JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view media'),
-  ]);
-}
-
-/**
- * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'node'.
- */
-function jsonapi_jsonapi_node_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
-  // @see \Drupal\node\NodeAccessControlHandler::access()
-  if ($account->hasPermission('bypass node access')) {
-    return ([
-      JSONAPI_FILTER_AMONG_ALL => AccessResult::allowed()->cachePerPermissions(),
-    ]);
-  }
-  if (!$account->hasPermission('access content')) {
-    $forbidden = AccessResult::forbidden("The 'access content' permission is required.")->cachePerPermissions();
-    return ([
-      JSONAPI_FILTER_AMONG_ALL => $forbidden,
-      JSONAPI_FILTER_AMONG_OWN => $forbidden,
-      JSONAPI_FILTER_AMONG_PUBLISHED => $forbidden,
-      // For legacy reasons, the Node entity type has a "status" key, so forbid
-      // this subset as well, even though it has no semantic meaning.
-      JSONAPI_FILTER_AMONG_ENABLED => $forbidden,
-    ]);
-  }
-
-  return ([
-    // @see \Drupal\node\NodeAccessControlHandler::checkAccess()
-    JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own unpublished content'),
-
-    // @see \Drupal\node\NodeGrantDatabaseStorage::access()
-    // Note that:
-    // - This is just for the default grant. Other node access conditions are
-    //   added via the 'node_access' query tag.
-    // - Permissions were checked earlier in this function, so we must vary the
-    //   cache by them.
-    JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowed()->cachePerPermissions(),
-  ]);
-}
-
-/**
- * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'shortcut'.
- */
-function jsonapi_jsonapi_shortcut_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
-  // @see \Drupal\shortcut\ShortcutAccessControlHandler::checkAccess()
-  // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
-  // (shortcut_set = $shortcut_set_storage->getDisplayedToUser($current_user)),
-  // so this does not have to.
-  return ([
-    JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer shortcuts')
-      ->orIf(AccessResult::allowedIfHasPermissions($account, ['access shortcuts', 'customize shortcut links'])),
-  ]);
-}
-
-/**
- * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'taxonomy_term'.
- */
-function jsonapi_jsonapi_taxonomy_term_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
-  // @see \Drupal\taxonomy\TermAccessControlHandler::checkAccess()
-  return ([
-    JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer taxonomy'),
-    JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access content'),
-  ]);
-}
-
-/**
- * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'user'.
- */
-function jsonapi_jsonapi_user_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
-  // @see \Drupal\user\UserAccessControlHandler::checkAccess()
-  // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
-  // (!isAnonymous()), so this does not have to.
-  return ([
-    JSONAPI_FILTER_AMONG_OWN => AccessResult::allowed(),
-    JSONAPI_FILTER_AMONG_ENABLED => AccessResult::allowedIfHasPermission($account, 'access user profiles'),
-  ]);
-}
-
-/**
- * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'workspace'.
- */
-function jsonapi_jsonapi_workspace_filter_access(EntityTypeInterface $entity_type, AccountInterface $account) {
-  // @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess()
-  return ([
-    JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view any workspace'),
-    JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own workspace'),
-  ]);
-}
diff --git a/core/modules/jsonapi/src/Hook/JsonapiHooks.php b/core/modules/jsonapi/src/Hook/JsonapiHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..0956eed10723e26189d289e7661c940e108e208a
--- /dev/null
+++ b/core/modules/jsonapi/src/Hook/JsonapiHooks.php
@@ -0,0 +1,265 @@
+<?php
+
+namespace Drupal\jsonapi\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\jsonapi\Routing\Routes;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for jsonapi.
+ */
+class JsonapiHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.jsonapi':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The JSON:API module is a fully compliant implementation of the <a href=":spec">JSON:API Specification</a>. By following shared conventions, you can increase productivity, take advantage of generalized tooling, and focus on what matters: your application. Clients built around JSON:API are able to take advantage of features like efficient response caching, which can sometimes eliminate network requests entirely. For more information, see the <a href=":docs">online documentation for the JSON:API module</a>.', [
+          ':spec' => 'https://jsonapi.org',
+          ':docs' => 'https://www.drupal.org/docs/8/modules/json-api',
+        ]) . '</p>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('General') . '</dt>';
+        $output .= '<dd>' . t('JSON:API is a particular implementation of REST that provides conventions for resource relationships, collections, filters, pagination, and sorting. These conventions help developers build clients faster and encourages reuse of code.') . '</dd>';
+        $output .= '<dd>' . t('The <a href=":jsonapi-docs">JSON:API</a> and <a href=":rest-docs">RESTful Web Services</a> modules serve similar purposes. <a href=":comparison">Read the comparison of the RESTFul Web Services and JSON:API modules</a> to determine the best choice for your site.', [
+          ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/json-api',
+          ':rest-docs' => 'https://www.drupal.org/docs/8/core/modules/rest',
+          ':comparison' => 'https://www.drupal.org/docs/8/modules/jsonapi/jsonapi-vs-cores-rest-module',
+        ]) . '</dd>';
+        $output .= '<dd>' . t('Some multilingual features currently do not work well with JSON:API. See the <a href=":jsonapi-docs">JSON:API multilingual support documentation</a> for more information on the current status of multilingual support.', [':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/jsonapi/translations']) . '</dd>';
+        $output .= '<dd>' . t('Revision support is currently read-only and only for the "Content" and "Media" entity types in JSON:API. See the <a href=":jsonapi-docs">JSON:API revision support documentation</a> for more information on the current status of revision support.', [':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/jsonapi/revisions']) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+    return NULL;
+  }
+
+  /**
+   * Implements hook_modules_installed().
+   */
+  #[Hook('modules_installed')]
+  public function modulesInstalled($modules) {
+    $potential_conflicts = ['content_translation', 'config_translation', 'language'];
+    if (!empty(array_intersect($modules, $potential_conflicts))) {
+      \Drupal::messenger()->addWarning(t('Some multilingual features currently do not work well with JSON:API. See the <a href=":jsonapi-docs">JSON:API multilingual support documentation</a> for more information on the current status of multilingual support.', [':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/jsonapi/translations']));
+    }
+  }
+
+  /**
+   * Implements hook_entity_bundle_create().
+   */
+  #[Hook('entity_bundle_create')]
+  public function entityBundleCreate() {
+    Routes::rebuild();
+  }
+
+  /**
+   * Implements hook_entity_bundle_delete().
+   */
+  #[Hook('entity_bundle_delete')]
+  public function entityBundleDelete() {
+    Routes::rebuild();
+  }
+
+  /**
+   * Implements hook_entity_create().
+   */
+  #[Hook('entity_create')]
+  public function entityCreate(EntityInterface $entity) {
+    if (in_array($entity->getEntityTypeId(), ['field_storage_config', 'field_config'])) {
+      // @todo Only do this when relationship fields are updated, not just any field.
+      Routes::rebuild();
+    }
+  }
+
+  /**
+   * Implements hook_entity_delete().
+   */
+  #[Hook('entity_delete')]
+  public function entityDelete(EntityInterface $entity) {
+    if (in_array($entity->getEntityTypeId(), ['field_storage_config', 'field_config'])) {
+      // @todo Only do this when relationship fields are updated, not just any field.
+      Routes::rebuild();
+    }
+  }
+
+  /**
+   * Implements hook_jsonapi_entity_filter_access().
+   */
+  #[Hook('jsonapi_entity_filter_access')]
+  public function jsonapiEntityFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account) {
+    // All core entity types and most or all contrib entity types allow users
+    // with the entity type's administrative permission to view all of the
+    // entities, so enable similarly permissive filtering to those users as well.
+    // A contrib module may override this decision by returning
+    // AccessResult::forbidden() from its implementation of this hook.
+    if ($admin_permission = $entity_type->getAdminPermission()) {
+      return [
+        JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, $admin_permission),
+      ];
+    }
+  }
+
+  /**
+   * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'block_content'.
+   */
+  #[Hook('jsonapi_block_content_filter_access')]
+  public function jsonapiBlockContentFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account) {
+    // @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
+    // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
+    // (isReusable()), so this does not have to.
+    return [
+      JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access block library'),
+      JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowed(),
+    ];
+  }
+
+  /**
+   * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'comment'.
+   */
+  #[Hook('jsonapi_comment_filter_access')]
+  public function jsonapiCommentFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account) {
+    // @see \Drupal\comment\CommentAccessControlHandler::checkAccess()
+    // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
+    // (access to the commented entity), so this does not have to.
+    return [
+      JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer comments'),
+      JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access comments'),
+    ];
+  }
+
+  /**
+   * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'entity_test'.
+   */
+  #[Hook('jsonapi_entity_test_filter_access')]
+  public function jsonapiEntityTestFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account) {
+    // @see \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess()
+    return [
+      JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view test entity'),
+    ];
+  }
+
+  /**
+   * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'file'.
+   */
+  #[Hook('jsonapi_file_filter_access')]
+  public function jsonapiFileFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account) {
+    // @see \Drupal\file\FileAccessControlHandler::checkAccess()
+    // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
+    // (public OR owner), so this does not have to.
+    return [
+      JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'access content'),
+    ];
+  }
+
+  /**
+   * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'media'.
+   */
+  #[Hook('jsonapi_media_filter_access')]
+  public function jsonapiMediaFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account) {
+    // @see \Drupal\media\MediaAccessControlHandler::checkAccess()
+    return [
+      JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'view media'),
+    ];
+  }
+
+  /**
+   * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'node'.
+   */
+  #[Hook('jsonapi_node_filter_access')]
+  public function jsonapiNodeFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account) {
+    // @see \Drupal\node\NodeAccessControlHandler::access()
+    if ($account->hasPermission('bypass node access')) {
+      return [
+        JSONAPI_FILTER_AMONG_ALL => AccessResult::allowed()->cachePerPermissions(),
+      ];
+    }
+    if (!$account->hasPermission('access content')) {
+      $forbidden = AccessResult::forbidden("The 'access content' permission is required.")->cachePerPermissions();
+      return [
+        JSONAPI_FILTER_AMONG_ALL => $forbidden,
+        JSONAPI_FILTER_AMONG_OWN => $forbidden,
+        JSONAPI_FILTER_AMONG_PUBLISHED => $forbidden,
+            // For legacy reasons, the Node entity type has a "status" key, so forbid
+            // this subset as well, even though it has no semantic meaning.
+        JSONAPI_FILTER_AMONG_ENABLED => $forbidden,
+      ];
+    }
+    return [
+          // @see \Drupal\node\NodeAccessControlHandler::checkAccess()
+      JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own unpublished content'),
+          // @see \Drupal\node\NodeGrantDatabaseStorage::access()
+          // Note that:
+          // - This is just for the default grant. Other node access conditions are
+          //   added via the 'node_access' query tag.
+          // - Permissions were checked earlier in this function, so we must vary the
+          //   cache by them.
+      JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowed()->cachePerPermissions(),
+    ];
+  }
+
+  /**
+   * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'shortcut'.
+   */
+  #[Hook('jsonapi_shortcut_filter_access')]
+  public function jsonapiShortcutFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account) {
+    // @see \Drupal\shortcut\ShortcutAccessControlHandler::checkAccess()
+    // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
+    // (shortcut_set = $shortcut_set_storage->getDisplayedToUser($current_user)),
+    // so this does not have to.
+    return [
+      JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer shortcuts')->orIf(AccessResult::allowedIfHasPermissions($account, [
+        'access shortcuts',
+        'customize shortcut links',
+      ])),
+    ];
+  }
+
+  /**
+   * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'taxonomy_term'.
+   */
+  #[Hook('jsonapi_taxonomy_term_filter_access')]
+  public function jsonapiTaxonomyTermFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account) {
+    // @see \Drupal\taxonomy\TermAccessControlHandler::checkAccess()
+    return [
+      JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'administer taxonomy'),
+      JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::allowedIfHasPermission($account, 'access content'),
+    ];
+  }
+
+  /**
+   * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'user'.
+   */
+  #[Hook('jsonapi_user_filter_access')]
+  public function jsonapiUserFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account) {
+    // @see \Drupal\user\UserAccessControlHandler::checkAccess()
+    // \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
+    // (!isAnonymous()), so this does not have to.
+    return [
+      JSONAPI_FILTER_AMONG_OWN => AccessResult::allowed(),
+      JSONAPI_FILTER_AMONG_ENABLED => AccessResult::allowedIfHasPermission($account, 'access user profiles'),
+    ];
+  }
+
+  /**
+   * Implements hook_jsonapi_ENTITY_TYPE_filter_access() for 'workspace'.
+   */
+  #[Hook('jsonapi_workspace_filter_access')]
+  public function jsonapiWorkspaceFilterAccess(EntityTypeInterface $entity_type, AccountInterface $account) {
+    // @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess()
+    return [
+      JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view any workspace'),
+      JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own workspace'),
+    ];
+  }
+
+}
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.module b/core/modules/jsonapi/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.module
deleted file mode 100644
index 275e3d9ab2750164c9baef21a4a8d0c8b136142b..0000000000000000000000000000000000000000
--- a/core/modules/jsonapi/tests/modules/jsonapi_test_field_access/jsonapi_test_field_access.module
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains hook implementations for testing the JSON:API module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Implements hook_entity_field_access().
- */
-function jsonapi_test_field_access_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account) {
-  // @see \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testRelationships().
-  if ($field_definition->getName() === 'field_jsonapi_test_entity_ref') {
-    // Forbid access in all cases.
-    $permission = "field_jsonapi_test_entity_ref $operation access";
-    $access_result = $account->hasPermission($permission)
-      ? AccessResult::allowed()
-      : AccessResult::forbidden("The '$permission' permission is required.");
-    return $access_result->addCacheContexts(['user.permissions']);
-  }
-
-  // No opinion.
-  return AccessResult::neutral();
-}
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_field_access/src/Hook/JsonapiTestFieldAccessHooks.php b/core/modules/jsonapi/tests/modules/jsonapi_test_field_access/src/Hook/JsonapiTestFieldAccessHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..0eac08911f9190cea33cc8ca958775f2b3f00ade
--- /dev/null
+++ b/core/modules/jsonapi/tests/modules/jsonapi_test_field_access/src/Hook/JsonapiTestFieldAccessHooks.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\jsonapi_test_field_access\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for jsonapi_test_field_access.
+ */
+class JsonapiTestFieldAccessHooks {
+
+  /**
+   * Implements hook_entity_field_access().
+   */
+  #[Hook('entity_field_access')]
+  public function entityFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account) {
+    // @see \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testRelationships().
+    if ($field_definition->getName() === 'field_jsonapi_test_entity_ref') {
+      // Forbid access in all cases.
+      $permission = "field_jsonapi_test_entity_ref {$operation} access";
+      $access_result = $account->hasPermission($permission) ? AccessResult::allowed() : AccessResult::forbidden("The '{$permission}' permission is required.");
+      return $access_result->addCacheContexts(['user.permissions']);
+    }
+    // No opinion.
+    return AccessResult::neutral();
+  }
+
+}
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_non_cacheable_methods/jsonapi_test_non_cacheable_methods.module b/core/modules/jsonapi/tests/modules/jsonapi_test_non_cacheable_methods/jsonapi_test_non_cacheable_methods.module
deleted file mode 100644
index 665052f6803db160f8a8113b63a8e59e6860e102..0000000000000000000000000000000000000000
--- a/core/modules/jsonapi/tests/modules/jsonapi_test_non_cacheable_methods/jsonapi_test_non_cacheable_methods.module
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains hook implementations for testing the JSON:API module.
- *
- * @see: https://www.drupal.org/project/drupal/issues/3072076.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_entity_presave().
- */
-function jsonapi_test_non_cacheable_methods_entity_presave(EntityInterface $entity) {
-  Url::fromRoute('<front>')->toString();
-}
-
-/**
- * Implements hook_entity_predelete().
- */
-function jsonapi_test_non_cacheable_methods_entity_predelete(EntityInterface $entity) {
-  Url::fromRoute('<front>')->toString();
-}
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_non_cacheable_methods/src/Hook/JsonapiTestNonCacheableMethodsHooks.php b/core/modules/jsonapi/tests/modules/jsonapi_test_non_cacheable_methods/src/Hook/JsonapiTestNonCacheableMethodsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f1ea916ce55d3f3d10933af3efd84b869823bdee
--- /dev/null
+++ b/core/modules/jsonapi/tests/modules/jsonapi_test_non_cacheable_methods/src/Hook/JsonapiTestNonCacheableMethodsHooks.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\jsonapi_test_non_cacheable_methods\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for jsonapi_test_non_cacheable_methods.
+ */
+class JsonapiTestNonCacheableMethodsHooks {
+
+  /**
+   * Implements hook_entity_presave().
+   */
+  #[Hook('entity_presave')]
+  public function entityPresave(EntityInterface $entity) {
+    Url::fromRoute('<front>')->toString();
+  }
+
+  /**
+   * Implements hook_entity_predelete().
+   */
+  #[Hook('entity_predelete')]
+  public function entityPredelete(EntityInterface $entity) {
+    Url::fromRoute('<front>')->toString();
+  }
+
+}
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_user/jsonapi_test_user.module b/core/modules/jsonapi/tests/modules/jsonapi_test_user/jsonapi_test_user.module
deleted file mode 100644
index 7e553506aa1de4b438220bbd09cd40e7b92a16bb..0000000000000000000000000000000000000000
--- a/core/modules/jsonapi/tests/modules/jsonapi_test_user/jsonapi_test_user.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Support module for JSON:API user hooks testing.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Implements hook_user_format_name_alter().
- */
-function jsonapi_test_user_user_format_name_alter(&$name, AccountInterface $account) {
-  if ($account->isAnonymous()) {
-    $name = 'User ' . $account->id();
-  }
-}
diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_user/src/Hook/JsonapiTestUserHooks.php b/core/modules/jsonapi/tests/modules/jsonapi_test_user/src/Hook/JsonapiTestUserHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..d25b219163df94850f8325b1caafa83a4a656f5a
--- /dev/null
+++ b/core/modules/jsonapi/tests/modules/jsonapi_test_user/src/Hook/JsonapiTestUserHooks.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\jsonapi_test_user\Hook;
+
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for jsonapi_test_user.
+ */
+class JsonapiTestUserHooks {
+
+  /**
+   * Implements hook_user_format_name_alter().
+   */
+  #[Hook('user_format_name_alter')]
+  public function userFormatNameAlter(&$name, AccountInterface $account) {
+    if ($account->isAnonymous()) {
+      $name = 'User ' . $account->id();
+    }
+  }
+
+}
diff --git a/core/modules/language/language.admin.inc b/core/modules/language/language.admin.inc
index 7a5c4fa1d1c53ccd60f24d4ea5271dd1d70ed968..82c07c4bf21ecef90b43abb540a45fac6b5f4d96 100644
--- a/core/modules/language/language.admin.inc
+++ b/core/modules/language/language.admin.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Administration functions for language.module.
  */
 
 use Drupal\Core\Render\Element;
diff --git a/core/modules/language/language.module b/core/modules/language/language.module
index 233e7c9498ba3bc0452ebccd395f097c3cc5e4b3..42a72f6e4e878e89b0352395362e2995516f4523 100644
--- a/core/modules/language/language.module
+++ b/core/modules/language/language.module
@@ -2,146 +2,12 @@
 
 /**
  * @file
- * Add language handling functionality to Drupal.
  */
 
-use Drupal\Core\Url;
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Config\FileStorage;
-use Drupal\Core\Config\InstallStorage;
-use Drupal\Core\Entity\ContentEntityFormInterface;
 use Drupal\Core\Entity\EntityFormInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Installer\InstallerKernel;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Session\AccountInterface;
 use Drupal\language\Entity\ContentLanguageSettings;
-use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUI;
-use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
-use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrlFallback;
-
-/**
- * Implements hook_help().
- */
-function language_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.language':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Language module allows you to configure the languages used on your site, and provides information for the <a href=":content">Content Translation</a>, <a href=":interface">Interface Translation</a>, and <a href=":configuration">Configuration Translation</a> modules, if they are installed. For more information, see the <a href=":doc_url">online documentation for the Language module</a>.', [':doc_url' => 'https://www.drupal.org/documentation/modules/language', ':content' => (\Drupal::moduleHandler()->moduleExists('content_translation')) ? Url::fromRoute('help.page', ['name' => 'content_translation'])->toString() : '#', ':interface' => (\Drupal::moduleHandler()->moduleExists('locale')) ? Url::fromRoute('help.page', ['name' => 'locale'])->toString() : '#', ':configuration' => (\Drupal::moduleHandler()->moduleExists('config_translation')) ? Url::fromRoute('help.page', ['name' => 'config_translation'])->toString() : '#']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Adding languages') . '</dt>';
-      $output .= '<dd>' . t('You can add languages on the <a href=":language_list">Languages</a> page by selecting <em>Add language</em> and choosing a language from the drop-down menu. This language is then displayed in the languages list, where it can be configured further. If the <a href=":interface">Interface translation module</a> is installed, and the <em>translation server</em> is set as a translation source, then the interface translation for this language is automatically downloaded as well.', [':language_list' => Url::fromRoute('entity.configurable_language.collection')->toString(), ':interface' => (\Drupal::moduleHandler()->moduleExists('locale')) ? Url::fromRoute('help.page', ['name' => 'locale'])->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Adding custom languages') . '</dt>';
-      $output .= '<dd>' . t('You can add a language that is not provided in the drop-down list by choosing <em>Custom language</em> at the end of the list. You then have to configure its language code, name, and direction in the form provided.') . '</dd>';
-      $output .= '<dt>' . t('Configuring content languages') . '</dt>';
-      $output .= '<dd>' . t('By default, content is created in the site\'s default language and no language selector is displayed on content creation pages. On the <a href=":content_language">Content language</a> page you can customize the language configuration for any supported content entity on your site (for example for content types or menu links). After choosing an entity, you are provided with a drop-down menu to set the default language and a check-box to display language selectors.', [':content_language' => Url::fromRoute('language.content_settings_page')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Adding a language switcher block') . '</dt>';
-      $output .= '<dd>' . t('If the Block module is installed, then you can add a language switcher block on the <a href=":blocks">Block layout</a> page to allow users to switch between languages.', [':blocks' => (\Drupal::moduleHandler()->moduleExists('block')) ? Url::fromRoute('block.admin_display')->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Making a block visible per language') . '</dt>';
-      $output .= '<dd>' . t('If the Block module is installed, then the Language module allows you to set the visibility of a block based on selected languages on the <a href=":blocks">Block layout</a> page.', [':blocks' => (\Drupal::moduleHandler()->moduleExists('block')) ? Url::fromRoute('block.admin_display')->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Choosing user languages') . '</dt>';
-      $output .= '<dd>' . t("Users can choose a <em>Site language</em> on their profile page. This language is used for email messages, and can be used by modules to determine a user's language. It can also be used for interface text, if the <em>User</em> method is enabled as a <em>Detection and selection</em> method (see below). Administrative users can choose a separate <em>Administration pages language</em> for the interface text on administration pages. This configuration is only available on the user's profile page if the <em>Account administration pages</em> method is enabled (see below).") . '</dd>';
-      $output .= '<dt>' . t('Language detection and selection') . '</dt>';
-      $output .= '<dd>' . t('The <a href=":detection">Detection and selection</a> page provides several methods for deciding which language is used for displaying interface text. When a method detects and selects an interface language, then the following methods in the list are not applied. You can order them by importance, with your preferred method at the top of the list, followed by one or several fall-back methods.', [':detection' => Url::fromRoute('language.negotiation')->toString()]);
-      $output .= '<ul><li>' . t('<em>URL</em> sets the interface language based on a path prefix or domain (for example specifying <em>de</em> for German would result in URLs like <em>example.com/de/contact</em>). The default language does not require a path prefix, but can have one assigned as well. If the language detection is done by domain name, a domain needs to be specified for each language.') . '</li>';
-      $output .= '<li>' . t('<em>Session</em> determines the interface language from a request or session parameter (for example <em>example.com?language=de</em> would set the interface language to German based on the use of <em>de</em> as the <em>language</em> parameter).') . '</li>';
-      $output .= '<li>' . t("<em>User</em> follows the language configuration set on the user's profile page.") . '</li>';
-      $output .= '<li>' . t('<em>Browser</em> sets the interface language based on the browser\'s language settings. Since browsers use different language codes to refer to the same languages, you can add and edit languages codes to map the browser language codes to the <a href=":language_list">language codes</a> used on your site.', [':language_list' => Url::fromRoute('entity.configurable_language.collection')->toString()]) . '</li>';
-      $output .= '<li>' . t('<em>Account administration pages</em> follows the configuration set as <em>Administration pages language</em> on the profile page of an administrative user. This method is similar to the <em>User</em> method, but only sets the interface text language on administration pages, independent of the interface text language on other pages.') . '</li>';
-      $output .= '<li>' . t("<em>Selected language</em> allows you to specify the site's default language or a specific language as the fall-back language. This method should be listed last.") . '</li></ul></dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'entity.configurable_language.collection':
-      $output = '<p>' . t('Reorder the configured languages to set their order in the language switcher block and, when editing content, in the list of selectable languages. This ordering does not impact <a href=":detection">detection and selection</a>.', [':detection' => Url::fromRoute('language.negotiation')->toString()]) . '</p>';
-      $output .= '<p>' . t('The site default language can also be set. It is not recommended to change the default language on a working site. <a href=":language-detection">Configure the Selected language</a> setting on the detection and selection page to change the fallback language for language selection.', [':language-detection' => Url::fromRoute('language.negotiation')->toString()]) . '</p>';
-      return $output;
-
-    case 'language.add':
-      return '<p>' . t('Add a language to be supported by your site. If your desired language is not available, pick <em>Custom language...</em> at the end and provide a language code and other details manually.') . '</p>';
-
-    case 'language.negotiation':
-      $output = '<p>' . t('Define how to decide which language is used to display page elements (primarily text provided by modules, such as field labels and help text). This decision is made by evaluating a series of detection methods for languages; the first detection method that gets a result will determine which language is used for that type of text. Be aware that some language detection methods are unreliable under certain conditions, such as browser detection when page-caching is enabled and a user is not currently logged in. Define the order of evaluation of language detection methods on this page. The default language can be changed in the <a href=":admin-change-language">list of languages</a>.', [':admin-change-language' => Url::fromRoute('entity.configurable_language.collection')->toString()]) . '</p>';
-      return $output;
-
-    case 'language.negotiation_session':
-      $output = '<p>' . t('Determine the language from a request/session parameter. Example: "http://example.com?language=de" sets language to German based on the use of "de" within the "language" parameter.') . '</p>';
-      return $output;
-
-    case 'language.negotiation_browser':
-      $output = '<p>' . t('Browsers use different language codes to refer to the same languages. Internally, a best effort is made to determine the correct language based on the code that the browser sends. You can add and edit additional mappings from browser language codes to <a href=":configure-languages">site languages</a>.', [':configure-languages' => Url::fromRoute('entity.configurable_language.collection')->toString()]) . '</p>';
-      return $output;
-
-    case 'language.negotiation_selected':
-      $output = '<p>' . t('Changing the selected language here (and leaving this option as the last among the detection and selection options) is the easiest way to change the fallback language for the website, if you need to change how your site works by default (e.g., when using an empty path prefix or using the default domain). <a href=":admin-change-language">Changing the site\'s default language</a> itself might have other undesired side effects.', [':admin-change-language' => Url::fromRoute('entity.configurable_language.collection')->toString()]) . '</p>';
-      return $output;
-
-    case 'entity.block.edit_form':
-      if (($block = $route_match->getParameter('block')) && $block->getPluginId() == 'language_block:language_interface') {
-        return '<p>' . t('With multiple languages configured, registered users can select their preferred language and authors can assign a specific language to content.') . '</p>';
-      }
-      break;
-
-    case 'block.admin_add':
-      if ($route_match->getParameter('plugin_id') == 'language_block:language_interface') {
-        return '<p>' . t('With multiple languages configured, registered users can select their preferred language and authors can assign a specific language to content.') . '</p>';
-      }
-      break;
-
-    case 'language.content_settings_page':
-      return '<p>' . t("Change language settings for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>user profiles</em>, or any other supported element on your site. By default, language settings hide the language selector and the language is the site's default language.") . '</p>';
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function language_theme(): array {
-  return [
-    'language_negotiation_configure_form' => [
-      'render element' => 'form',
-      'file' => 'language.admin.inc',
-    ],
-    'language_content_settings_table' => [
-      'render element' => 'element',
-      'file' => 'language.admin.inc',
-    ],
-  ];
-}
-
-/**
- * Implements hook_element_info_alter().
- *
- * @see \Drupal\Core\Render\Element\LanguageSelect
- * @see \Drupal\Core\Render\Element\Select
- */
-function language_element_info_alter(&$type) {
-  // Alter the language_select element so that it will be rendered like a select
-  // field.
-  if (isset($type['language_select'])) {
-    if (!isset($type['language_select']['#process'])) {
-      $type['language_select']['#process'] = [];
-    }
-    if (!isset($type['language_select']['#theme_wrappers'])) {
-      $type['language_select']['#theme_wrappers'] = [];
-    }
-    $type['language_select']['#process'] = array_merge($type['language_select']['#process'], [
-      'language_process_language_select',
-      ['Drupal\Core\Render\Element\Select', 'processSelect'],
-      ['Drupal\Core\Render\Element\RenderElementBase', 'processAjaxForm'],
-    ]);
-    $type['language_select']['#theme'] = 'select';
-    $type['language_select']['#theme_wrappers'] = array_merge($type['language_select']['#theme_wrappers'], ['form_element']);
-    $type['language_select']['#languages'] = LanguageInterface::STATE_CONFIGURABLE;
-    $type['language_select']['#multiple'] = FALSE;
-  }
-}
 
 /**
  * Processes a language select list form element.
@@ -164,24 +30,6 @@ function language_process_language_select($element) {
   return $element;
 }
 
-/**
- * Implements hook_entity_base_field_info_alter().
- */
-function language_entity_base_field_info_alter(&$fields) {
-  foreach ($fields as $definition) {
-    // Set configurable form display for language fields with display options.
-    if ($definition->getType() == 'language') {
-      foreach (['form', 'view'] as $type) {
-        if ($definition->getDisplayOptions($type)) {
-          // The related configurations will be purged manually on Language
-          // module uninstallation. @see language_modules_uninstalled().
-          $definition->setDisplayConfigurable($type, TRUE);
-        }
-      }
-    }
-  }
-}
-
 /**
  * Submit handler for the forms that have a language_configuration element.
  */
@@ -214,17 +62,6 @@ function language_configuration_element_submit(&$form, FormStateInterface $form_
   }
 }
 
-/**
- * Implements hook_entity_bundle_delete().
- */
-function language_entity_bundle_delete($entity_type_id, $bundle) {
-  // Remove the content language settings associated with the bundle.
-  $settings = ContentLanguageSettings::loadByEntityTypeBundle($entity_type_id, $bundle);
-  if (!$settings->isNew()) {
-    $settings->delete();
-  }
-}
-
 /**
  * Returns the default language code assigned to an entity type and a bundle.
  *
@@ -289,60 +126,6 @@ function language_negotiation_url_prefixes_update() {
   $config->set('url.prefixes', $prefixes)->save(TRUE);
 }
 
-/**
- * Implements hook_modules_installed().
- */
-function language_modules_installed($modules, $is_syncing) {
-  if ($is_syncing) {
-    return;
-  }
-
-  if (!in_array('language', $modules)) {
-    if (InstallerKernel::installationAttempted() && $profile = \Drupal::installProfile()) {
-      // If the install profile provides its own language.types configuration do
-      // not overwrite it.
-      $profile_directory = \Drupal::service('extension.list.profile')->getPath($profile);
-      $profile_storages = [
-        new FileStorage($profile_directory . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY),
-        new FileStorage($profile_directory . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY),
-      ];
-      foreach ($profile_storages as $storage) {
-        if ($storage->exists('language.types')) {
-          return;
-        }
-      }
-    }
-    // Since newly (un)installed modules may change the default settings for
-    // non-locked language types (e.g. content language), we need to resave the
-    // language type configuration.
-    /** @var \Drupal\language\LanguageNegotiatorInterface $negotiator */
-    $negotiator = \Drupal::service('language_negotiator');
-    $configurable = \Drupal::config('language.types')->get('configurable');
-    $negotiator->updateConfiguration($configurable);
-    $negotiator->purgeConfiguration();
-  }
-  else {
-    // In language_entity_base_field_info_alter() we are altering view/form
-    // display definitions to make language fields display configurable. Since
-    // this is not a hard dependency, and thus is not detected by the config
-    // system, we have to clean up the related values manually.
-    foreach (['entity_view_display', 'entity_form_display'] as $key) {
-      $displays = \Drupal::entityTypeManager()->getStorage($key)->loadMultiple();
-      /** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $display */
-      foreach ($displays as $display) {
-        $display->save();
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_modules_uninstalled().
- */
-function language_modules_uninstalled($modules, $is_syncing) {
-  language_modules_installed($modules, $is_syncing);
-}
-
 /**
  * Implements hook_preprocess_HOOK() for block templates.
  */
@@ -366,102 +149,3 @@ function language_get_browser_drupal_langcode_mappings() {
   }
   return $config->get('map');
 }
-
-/**
- * Implements hook_form_alter().
- */
-function language_form_alter(&$form, FormStateInterface $form_state): void {
-  // Content entity forms may have added a langcode field. But content language
-  // configuration should decide if it should be exposed or not in the forms.
-  $form_object = $form_state->getFormObject();
-  if ($form_object instanceof ContentEntityFormInterface && $form_object->getEntity()->getEntityType()->hasKey('langcode')) {
-    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
-    $entity = $form_object->getEntity();
-    $entity_type = $entity->getEntityType();
-    $langcode_key = $entity_type->getKey('langcode');
-    if (isset($form[$langcode_key]) && $form[$langcode_key]['#access'] !== FALSE) {
-      $language_configuration = ContentLanguageSettings::loadByEntityTypeBundle($entity->getEntityTypeId(), $entity->bundle());
-      $form[$langcode_key]['#access'] = $language_configuration->isLanguageAlterable();
-    }
-  }
-}
-
-/**
- * Implements hook_field_info_alter().
- */
-function language_field_info_alter(&$info) {
-  // Change the default behavior of language field.
-  $info['language']['class'] = '\Drupal\language\DefaultLanguageItem';
-}
-
-/**
- * Implements hook_entity_field_access().
- */
-function language_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
-  // Only allow edit access on a langcode field if the entity it is attached to
-  // is configured to have an alterable language. Also without items we can not
-  // decide whether or not to allow access.
-  if ($items && $operation == 'edit') {
-    // Check if we are dealing with a langcode field.
-    $langcode_key = $items->getEntity()->getEntityType()->getKey('langcode');
-    if ($field_definition->getName() == $langcode_key) {
-      // Grant access depending on whether the entity language can be altered.
-      $entity = $items->getEntity();
-      $config = ContentLanguageSettings::loadByEntityTypeBundle($entity->getEntityTypeId(), $entity->bundle());
-      return AccessResult::forbiddenIf(!$config->isLanguageAlterable());
-    }
-  }
-  return AccessResult::neutral();
-}
-
-/**
- * Implements hook_tour_tips_alter().
- */
-function language_tour_tips_alter(array &$tour_tips, EntityInterface $entity) {
-  $module_extension_list = \Drupal::service('extension.list.module');
-  foreach ($tour_tips as $tour_tip) {
-    if ($tour_tip->get('id') == 'language-overview') {
-      $additional_overview = '';
-      if (Drupal::service('module_handler')->moduleExists('locale')) {
-        $additional_overview = t("This page also provides an overview of how much of the site's interface has been translated for each configured language.");
-      }
-      else {
-        $additional_overview = t("If the Interface Translation module is installed, this page will provide an overview of how much of the site's interface has been translated for each configured language.");
-      }
-      $tour_tip->set('body', $tour_tip->get('body') . '<p>' . $additional_overview . '</p>');
-    }
-    elseif ($tour_tip->get('id') == 'language-continue') {
-      $additional_continue = '';
-      $additional_modules = [];
-      if (!Drupal::service('module_handler')->moduleExists('locale')) {
-        $additional_modules[] = $module_extension_list->getName('locale');
-      }
-      if (!Drupal::service('module_handler')->moduleExists('content_translation')) {
-        $additional_modules[] = $module_extension_list->getName('content_translation');
-      }
-      if (!empty($additional_modules)) {
-        $additional_continue = t('Depending on your site features, additional modules that you might want to install are:') . '<ul>';
-        foreach ($additional_modules as $additional_module) {
-          $additional_continue .= '<li>' . $additional_module . '</li>';
-        }
-        $additional_continue .= '</ul>';
-      }
-      if (!empty($additional_continue)) {
-        $tour_tip->set('body', $tour_tip->get('body') . '<p>' . $additional_continue . '</p>');
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_language_types_info_alter().
- *
- * We can't set the fixed properties in \Drupal\Core\Language\LanguageManager,
- * where the rest of the properties for the default language types are defined.
- * The LanguageNegation classes are only loaded when the language module is
- * enabled and we can't be sure of that in the LanguageManager.
- */
-function language_language_types_info_alter(array &$language_types) {
-  $language_types[LanguageInterface::TYPE_CONTENT]['fixed'] = [LanguageNegotiationUI::METHOD_ID];
-  $language_types[LanguageInterface::TYPE_URL]['fixed'] = [LanguageNegotiationUrl::METHOD_ID, LanguageNegotiationUrlFallback::METHOD_ID];
-}
diff --git a/core/modules/language/src/Hook/LanguageHooks.php b/core/modules/language/src/Hook/LanguageHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..2264461ec16f2041064909ef68b2e047eb297495
--- /dev/null
+++ b/core/modules/language/src/Hook/LanguageHooks.php
@@ -0,0 +1,377 @@
+<?php
+
+namespace Drupal\language\Hook;
+
+use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrlFallback;
+use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
+use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUI;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Entity\ContentEntityFormInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Config\InstallStorage;
+use Drupal\Core\Config\FileStorage;
+use Drupal\Core\Installer\InstallerKernel;
+use Drupal\language\Entity\ContentLanguageSettings;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for language.
+ */
+class LanguageHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.language':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Language module allows you to configure the languages used on your site, and provides information for the <a href=":content">Content Translation</a>, <a href=":interface">Interface Translation</a>, and <a href=":configuration">Configuration Translation</a> modules, if they are installed. For more information, see the <a href=":doc_url">online documentation for the Language module</a>.', [
+          ':doc_url' => 'https://www.drupal.org/documentation/modules/language',
+          ':content' => \Drupal::moduleHandler()->moduleExists('content_translation') ? Url::fromRoute('help.page', [
+            'name' => 'content_translation',
+          ])->toString() : '#',
+          ':interface' => \Drupal::moduleHandler()->moduleExists('locale') ? Url::fromRoute('help.page', [
+            'name' => 'locale',
+          ])->toString() : '#',
+          ':configuration' => \Drupal::moduleHandler()->moduleExists('config_translation') ? Url::fromRoute('help.page', [
+            'name' => 'config_translation',
+          ])->toString() : '#',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Adding languages') . '</dt>';
+        $output .= '<dd>' . t('You can add languages on the <a href=":language_list">Languages</a> page by selecting <em>Add language</em> and choosing a language from the drop-down menu. This language is then displayed in the languages list, where it can be configured further. If the <a href=":interface">Interface translation module</a> is installed, and the <em>translation server</em> is set as a translation source, then the interface translation for this language is automatically downloaded as well.', [
+          ':language_list' => Url::fromRoute('entity.configurable_language.collection')->toString(),
+          ':interface' => \Drupal::moduleHandler()->moduleExists('locale') ? Url::fromRoute('help.page', [
+            'name' => 'locale',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Adding custom languages') . '</dt>';
+        $output .= '<dd>' . t('You can add a language that is not provided in the drop-down list by choosing <em>Custom language</em> at the end of the list. You then have to configure its language code, name, and direction in the form provided.') . '</dd>';
+        $output .= '<dt>' . t('Configuring content languages') . '</dt>';
+        $output .= '<dd>' . t('By default, content is created in the site\'s default language and no language selector is displayed on content creation pages. On the <a href=":content_language">Content language</a> page you can customize the language configuration for any supported content entity on your site (for example for content types or menu links). After choosing an entity, you are provided with a drop-down menu to set the default language and a check-box to display language selectors.', [
+          ':content_language' => Url::fromRoute('language.content_settings_page')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Adding a language switcher block') . '</dt>';
+        $output .= '<dd>' . t('If the Block module is installed, then you can add a language switcher block on the <a href=":blocks">Block layout</a> page to allow users to switch between languages.', [
+          ':blocks' => \Drupal::moduleHandler()->moduleExists('block') ? Url::fromRoute('block.admin_display')->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Making a block visible per language') . '</dt>';
+        $output .= '<dd>' . t('If the Block module is installed, then the Language module allows you to set the visibility of a block based on selected languages on the <a href=":blocks">Block layout</a> page.', [
+          ':blocks' => \Drupal::moduleHandler()->moduleExists('block') ? Url::fromRoute('block.admin_display')->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Choosing user languages') . '</dt>';
+        $output .= '<dd>' . t("Users can choose a <em>Site language</em> on their profile page. This language is used for email messages, and can be used by modules to determine a user's language. It can also be used for interface text, if the <em>User</em> method is enabled as a <em>Detection and selection</em> method (see below). Administrative users can choose a separate <em>Administration pages language</em> for the interface text on administration pages. This configuration is only available on the user's profile page if the <em>Account administration pages</em> method is enabled (see below).") . '</dd>';
+        $output .= '<dt>' . t('Language detection and selection') . '</dt>';
+        $output .= '<dd>' . t('The <a href=":detection">Detection and selection</a> page provides several methods for deciding which language is used for displaying interface text. When a method detects and selects an interface language, then the following methods in the list are not applied. You can order them by importance, with your preferred method at the top of the list, followed by one or several fall-back methods.', [':detection' => Url::fromRoute('language.negotiation')->toString()]);
+        $output .= '<ul><li>' . t('<em>URL</em> sets the interface language based on a path prefix or domain (for example specifying <em>de</em> for German would result in URLs like <em>example.com/de/contact</em>). The default language does not require a path prefix, but can have one assigned as well. If the language detection is done by domain name, a domain needs to be specified for each language.') . '</li>';
+        $output .= '<li>' . t('<em>Session</em> determines the interface language from a request or session parameter (for example <em>example.com?language=de</em> would set the interface language to German based on the use of <em>de</em> as the <em>language</em> parameter).') . '</li>';
+        $output .= '<li>' . t("<em>User</em> follows the language configuration set on the user's profile page.") . '</li>';
+        $output .= '<li>' . t('<em>Browser</em> sets the interface language based on the browser\'s language settings. Since browsers use different language codes to refer to the same languages, you can add and edit languages codes to map the browser language codes to the <a href=":language_list">language codes</a> used on your site.', [
+          ':language_list' => Url::fromRoute('entity.configurable_language.collection')->toString(),
+        ]) . '</li>';
+        $output .= '<li>' . t('<em>Account administration pages</em> follows the configuration set as <em>Administration pages language</em> on the profile page of an administrative user. This method is similar to the <em>User</em> method, but only sets the interface text language on administration pages, independent of the interface text language on other pages.') . '</li>';
+        $output .= '<li>' . t("<em>Selected language</em> allows you to specify the site's default language or a specific language as the fall-back language. This method should be listed last.") . '</li></ul></dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'entity.configurable_language.collection':
+        $output = '<p>' . t('Reorder the configured languages to set their order in the language switcher block and, when editing content, in the list of selectable languages. This ordering does not impact <a href=":detection">detection and selection</a>.', [':detection' => Url::fromRoute('language.negotiation')->toString()]) . '</p>';
+        $output .= '<p>' . t('The site default language can also be set. It is not recommended to change the default language on a working site. <a href=":language-detection">Configure the Selected language</a> setting on the detection and selection page to change the fallback language for language selection.', [
+          ':language-detection' => Url::fromRoute('language.negotiation')->toString(),
+        ]) . '</p>';
+        return $output;
+
+      case 'language.add':
+        return '<p>' . t('Add a language to be supported by your site. If your desired language is not available, pick <em>Custom language...</em> at the end and provide a language code and other details manually.') . '</p>';
+
+      case 'language.negotiation':
+        $output = '<p>' . t('Define how to decide which language is used to display page elements (primarily text provided by modules, such as field labels and help text). This decision is made by evaluating a series of detection methods for languages; the first detection method that gets a result will determine which language is used for that type of text. Be aware that some language detection methods are unreliable under certain conditions, such as browser detection when page-caching is enabled and a user is not currently logged in. Define the order of evaluation of language detection methods on this page. The default language can be changed in the <a href=":admin-change-language">list of languages</a>.', [
+          ':admin-change-language' => Url::fromRoute('entity.configurable_language.collection')->toString(),
+        ]) . '</p>';
+        return $output;
+
+      case 'language.negotiation_session':
+        $output = '<p>' . t('Determine the language from a request/session parameter. Example: "http://example.com?language=de" sets language to German based on the use of "de" within the "language" parameter.') . '</p>';
+        return $output;
+
+      case 'language.negotiation_browser':
+        $output = '<p>' . t('Browsers use different language codes to refer to the same languages. Internally, a best effort is made to determine the correct language based on the code that the browser sends. You can add and edit additional mappings from browser language codes to <a href=":configure-languages">site languages</a>.', [
+          ':configure-languages' => Url::fromRoute('entity.configurable_language.collection')->toString(),
+        ]) . '</p>';
+        return $output;
+
+      case 'language.negotiation_selected':
+        $output = '<p>' . t('Changing the selected language here (and leaving this option as the last among the detection and selection options) is the easiest way to change the fallback language for the website, if you need to change how your site works by default (e.g., when using an empty path prefix or using the default domain). <a href=":admin-change-language">Changing the site\'s default language</a> itself might have other undesired side effects.', [
+          ':admin-change-language' => Url::fromRoute('entity.configurable_language.collection')->toString(),
+        ]) . '</p>';
+        return $output;
+
+      case 'entity.block.edit_form':
+        if (($block = $route_match->getParameter('block')) && $block->getPluginId() == 'language_block:language_interface') {
+          return '<p>' . t('With multiple languages configured, registered users can select their preferred language and authors can assign a specific language to content.') . '</p>';
+        }
+        break;
+
+      case 'block.admin_add':
+        if ($route_match->getParameter('plugin_id') == 'language_block:language_interface') {
+          return '<p>' . t('With multiple languages configured, registered users can select their preferred language and authors can assign a specific language to content.') . '</p>';
+        }
+        break;
+
+      case 'language.content_settings_page':
+        return '<p>' . t("Change language settings for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>user profiles</em>, or any other supported element on your site. By default, language settings hide the language selector and the language is the site's default language.") . '</p>';
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'language_negotiation_configure_form' => [
+        'render element' => 'form',
+        'file' => 'language.admin.inc',
+      ],
+      'language_content_settings_table' => [
+        'render element' => 'element',
+        'file' => 'language.admin.inc',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_element_info_alter().
+   *
+   * @see \Drupal\Core\Render\Element\LanguageSelect
+   * @see \Drupal\Core\Render\Element\Select
+   */
+  #[Hook('element_info_alter')]
+  public function elementInfoAlter(&$type) {
+    // Alter the language_select element so that it will be rendered like a select
+    // field.
+    if (isset($type['language_select'])) {
+      if (!isset($type['language_select']['#process'])) {
+        $type['language_select']['#process'] = [];
+      }
+      if (!isset($type['language_select']['#theme_wrappers'])) {
+        $type['language_select']['#theme_wrappers'] = [];
+      }
+      $type['language_select']['#process'] = array_merge($type['language_select']['#process'], [
+        'language_process_language_select',
+            [
+              'Drupal\Core\Render\Element\Select',
+              'processSelect',
+            ],
+            [
+              'Drupal\Core\Render\Element\RenderElementBase',
+              'processAjaxForm',
+            ],
+      ]);
+      $type['language_select']['#theme'] = 'select';
+      $type['language_select']['#theme_wrappers'] = array_merge($type['language_select']['#theme_wrappers'], ['form_element']);
+      $type['language_select']['#languages'] = LanguageInterface::STATE_CONFIGURABLE;
+      $type['language_select']['#multiple'] = FALSE;
+    }
+  }
+
+  /**
+   * Implements hook_entity_base_field_info_alter().
+   */
+  #[Hook('entity_base_field_info_alter')]
+  public function entityBaseFieldInfoAlter(&$fields) {
+    foreach ($fields as $definition) {
+      // Set configurable form display for language fields with display options.
+      if ($definition->getType() == 'language') {
+        foreach (['form', 'view'] as $type) {
+          if ($definition->getDisplayOptions($type)) {
+            // The related configurations will be purged manually on Language
+            // module uninstallation. @see language_modules_uninstalled().
+            $definition->setDisplayConfigurable($type, TRUE);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_bundle_delete().
+   */
+  #[Hook('entity_bundle_delete')]
+  public function entityBundleDelete($entity_type_id, $bundle) {
+    // Remove the content language settings associated with the bundle.
+    $settings = ContentLanguageSettings::loadByEntityTypeBundle($entity_type_id, $bundle);
+    if (!$settings->isNew()) {
+      $settings->delete();
+    }
+  }
+
+  /**
+   * Implements hook_modules_installed().
+   *
+   * Implements hook_modules_uninstalled().
+   */
+  #[Hook('modules_installed')]
+  #[Hook('modules_uninstalled')]
+  public function modulesInstalled($modules, $is_syncing) {
+    if ($is_syncing) {
+      return;
+    }
+    if (!in_array('language', $modules)) {
+      if (InstallerKernel::installationAttempted() && ($profile = \Drupal::installProfile())) {
+        // If the install profile provides its own language.types configuration do
+        // not overwrite it.
+        $profile_directory = \Drupal::service('extension.list.profile')->getPath($profile);
+        $profile_storages = [
+          new FileStorage($profile_directory . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY),
+          new FileStorage($profile_directory . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY),
+        ];
+        foreach ($profile_storages as $storage) {
+          if ($storage->exists('language.types')) {
+            return;
+          }
+        }
+      }
+      // Since newly (un)installed modules may change the default settings for
+      // non-locked language types (e.g. content language), we need to resave the
+      // language type configuration.
+      /** @var \Drupal\language\LanguageNegotiatorInterface $negotiator */
+      $negotiator = \Drupal::service('language_negotiator');
+      $configurable = \Drupal::config('language.types')->get('configurable');
+      $negotiator->updateConfiguration($configurable);
+      $negotiator->purgeConfiguration();
+    }
+    else {
+      // In language_entity_base_field_info_alter() we are altering view/form
+      // display definitions to make language fields display configurable. Since
+      // this is not a hard dependency, and thus is not detected by the config
+      // system, we have to clean up the related values manually.
+      foreach (['entity_view_display', 'entity_form_display'] as $key) {
+        $displays = \Drupal::entityTypeManager()->getStorage($key)->loadMultiple();
+        /** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $display */
+        foreach ($displays as $display) {
+          $display->save();
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_form_alter().
+   */
+  #[Hook('form_alter')]
+  public function formAlter(&$form, FormStateInterface $form_state) : void {
+    // Content entity forms may have added a langcode field. But content language
+    // configuration should decide if it should be exposed or not in the forms.
+    $form_object = $form_state->getFormObject();
+    if ($form_object instanceof ContentEntityFormInterface && $form_object->getEntity()->getEntityType()->hasKey('langcode')) {
+      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+      $entity = $form_object->getEntity();
+      $entity_type = $entity->getEntityType();
+      $langcode_key = $entity_type->getKey('langcode');
+      if (isset($form[$langcode_key]) && $form[$langcode_key]['#access'] !== FALSE) {
+        $language_configuration = ContentLanguageSettings::loadByEntityTypeBundle($entity->getEntityTypeId(), $entity->bundle());
+        $form[$langcode_key]['#access'] = $language_configuration->isLanguageAlterable();
+      }
+    }
+  }
+
+  /**
+   * Implements hook_field_info_alter().
+   */
+  #[Hook('field_info_alter')]
+  public function fieldInfoAlter(&$info) {
+    // Change the default behavior of language field.
+    $info['language']['class'] = '\Drupal\language\DefaultLanguageItem';
+  }
+
+  /**
+   * Implements hook_entity_field_access().
+   */
+  #[Hook('entity_field_access')]
+  public function entityFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
+    // Only allow edit access on a langcode field if the entity it is attached to
+    // is configured to have an alterable language. Also without items we can not
+    // decide whether or not to allow access.
+    if ($items && $operation == 'edit') {
+      // Check if we are dealing with a langcode field.
+      $langcode_key = $items->getEntity()->getEntityType()->getKey('langcode');
+      if ($field_definition->getName() == $langcode_key) {
+        // Grant access depending on whether the entity language can be altered.
+        $entity = $items->getEntity();
+        $config = ContentLanguageSettings::loadByEntityTypeBundle($entity->getEntityTypeId(), $entity->bundle());
+        return AccessResult::forbiddenIf(!$config->isLanguageAlterable());
+      }
+    }
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Implements hook_tour_tips_alter().
+   */
+  #[Hook('tour_tips_alter')]
+  public function tourTipsAlter(array &$tour_tips, EntityInterface $entity) {
+    $module_extension_list = \Drupal::service('extension.list.module');
+    foreach ($tour_tips as $tour_tip) {
+      if ($tour_tip->get('id') == 'language-overview') {
+        $additional_overview = '';
+        if (\Drupal::service('module_handler')->moduleExists('locale')) {
+          $additional_overview = t("This page also provides an overview of how much of the site's interface has been translated for each configured language.");
+        }
+        else {
+          $additional_overview = t("If the Interface Translation module is installed, this page will provide an overview of how much of the site's interface has been translated for each configured language.");
+        }
+        $tour_tip->set('body', $tour_tip->get('body') . '<p>' . $additional_overview . '</p>');
+      }
+      elseif ($tour_tip->get('id') == 'language-continue') {
+        $additional_continue = '';
+        $additional_modules = [];
+        if (!\Drupal::service('module_handler')->moduleExists('locale')) {
+          $additional_modules[] = $module_extension_list->getName('locale');
+        }
+        if (!\Drupal::service('module_handler')->moduleExists('content_translation')) {
+          $additional_modules[] = $module_extension_list->getName('content_translation');
+        }
+        if (!empty($additional_modules)) {
+          $additional_continue = t('Depending on your site features, additional modules that you might want to install are:') . '<ul>';
+          foreach ($additional_modules as $additional_module) {
+            $additional_continue .= '<li>' . $additional_module . '</li>';
+          }
+          $additional_continue .= '</ul>';
+        }
+        if (!empty($additional_continue)) {
+          $tour_tip->set('body', $tour_tip->get('body') . '<p>' . $additional_continue . '</p>');
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_language_types_info_alter().
+   *
+   * We can't set the fixed properties in \Drupal\Core\Language\LanguageManager,
+   * where the rest of the properties for the default language types are defined.
+   * The LanguageNegation classes are only loaded when the language module is
+   * enabled and we can't be sure of that in the LanguageManager.
+   */
+  #[Hook('language_types_info_alter')]
+  public function languageTypesInfoAlter(array &$language_types) {
+    $language_types[LanguageInterface::TYPE_CONTENT]['fixed'] = [LanguageNegotiationUI::METHOD_ID];
+    $language_types[LanguageInterface::TYPE_URL]['fixed'] = [
+      LanguageNegotiationUrl::METHOD_ID,
+      LanguageNegotiationUrlFallback::METHOD_ID,
+    ];
+  }
+
+}
diff --git a/core/modules/language/src/ProxyClass/LanguageConverter.php b/core/modules/language/src/ProxyClass/LanguageConverter.php
index 5511c6859e55394218bacb5e6fa677107490391f..9f2fbbb1a1f198b2e759b005af5eeea79d30740a 100644
--- a/core/modules/language/src/ProxyClass/LanguageConverter.php
+++ b/core/modules/language/src/ProxyClass/LanguageConverter.php
@@ -7,15 +7,20 @@
 
 namespace Drupal\language\ProxyClass {
 
+    use Drupal\Core\ParamConverter\ParamConverterInterface;
+    use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+    use Symfony\Component\DependencyInjection\ContainerInterface;
+    use Symfony\Component\Routing\Route;
+
     /**
      * Provides a proxy class for \Drupal\language\LanguageConverter.
      *
      * @see \Drupal\Component\ProxyBuilder
      */
-    class LanguageConverter implements \Drupal\Core\ParamConverter\ParamConverterInterface
+    class LanguageConverter implements ParamConverterInterface
     {
 
-        use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
+        use DependencySerializationTrait;
 
         /**
          * The id of the original proxied service.
@@ -46,7 +51,7 @@ class LanguageConverter implements \Drupal\Core\ParamConverter\ParamConverterInt
          * @param string $drupal_proxy_original_service_id
          *   The service ID of the original service.
          */
-        public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
+        public function __construct(ContainerInterface $container, $drupal_proxy_original_service_id)
         {
             $this->container = $container;
             $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
@@ -78,7 +83,7 @@ public function convert($value, $definition, $name, array $defaults)
         /**
          * {@inheritdoc}
          */
-        public function applies($definition, $name, \Symfony\Component\Routing\Route $route)
+        public function applies($definition, $name, Route $route)
         {
             return $this->lazyLoadItself()->applies($definition, $name, $route);
         }
diff --git a/core/modules/language/tests/language_entity_field_access_test/language_entity_field_access_test.module b/core/modules/language/tests/language_entity_field_access_test/language_entity_field_access_test.module
deleted file mode 100644
index 4b8ea6545b02390f9325a60313ee4407252576b7..0000000000000000000000000000000000000000
--- a/core/modules/language/tests/language_entity_field_access_test/language_entity_field_access_test.module
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * Hook implementations for language_entity_field_access_test.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Implements hook_entity_field_access().
- */
-function language_entity_field_access_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
-  return AccessResult::forbidden();
-}
diff --git a/core/modules/language/tests/language_entity_field_access_test/src/Hook/LanguageEntityFieldAccessTestHooks.php b/core/modules/language/tests/language_entity_field_access_test/src/Hook/LanguageEntityFieldAccessTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..95fcfeef468e8c573db3f9cf8d220355b46b4c49
--- /dev/null
+++ b/core/modules/language/tests/language_entity_field_access_test/src/Hook/LanguageEntityFieldAccessTestHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\language_entity_field_access_test\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for language_entity_field_access_test.
+ */
+class LanguageEntityFieldAccessTestHooks {
+
+  /**
+   * Implements hook_entity_field_access().
+   */
+  #[Hook('entity_field_access')]
+  public function entityFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
+    return AccessResult::forbidden();
+  }
+
+}
diff --git a/core/modules/language/tests/language_test/language_test.module b/core/modules/language/tests/language_test/language_test.module
index fefc9f6848b138b98a750818c79a9232856bb896..ffa153163de8fa84fa1deeec231ecbfd2982892e 100644
--- a/core/modules/language/tests/language_test/language_test.module
+++ b/core/modules/language/tests/language_test/language_test.module
@@ -7,65 +7,6 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Url;
-use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUI;
-
-/**
- * Implements hook_page_top().
- */
-function language_test_page_top() {
-  if (\Drupal::moduleHandler()->moduleExists('language')) {
-    language_test_store_language_negotiation();
-    \Drupal::messenger()->addStatus(t('Language negotiation method: @name', ['@name' => \Drupal::languageManager()->getNegotiatedLanguageMethod() ?? 'Not defined']));
-  }
-}
-
-/**
- * Implements hook_language_types_info().
- */
-function language_test_language_types_info() {
-  if (\Drupal::state()->get('language_test.language_types')) {
-    return [
-      'test_language_type' => [
-        'name' => t('Test'),
-        'description' => t('A test language type.'),
-      ],
-      'fixed_test_language_type' => [
-        'fixed' => ['test_language_negotiation_method'],
-        'locked' => TRUE,
-      ],
-    ];
-  }
-}
-
-/**
- * Implements hook_language_types_info_alter().
- */
-function language_test_language_types_info_alter(array &$language_types) {
-  if (\Drupal::state()->get('language_test.content_language_type')) {
-    $language_types[LanguageInterface::TYPE_CONTENT]['locked'] = FALSE;
-    unset($language_types[LanguageInterface::TYPE_CONTENT]['fixed']);
-    // By default languages are not configurable. Make
-    // LanguageInterface::TYPE_CONTENT configurable.
-    $config = \Drupal::configFactory()->getEditable('language.types');
-    $configurable = $config->get('configurable');
-    if (!in_array(LanguageInterface::TYPE_CONTENT, $configurable)) {
-      $configurable[] = LanguageInterface::TYPE_CONTENT;
-      $config->set('configurable', $configurable)->save();
-    }
-  }
-}
-
-/**
- * Implements hook_language_negotiation_info_alter().
- */
-function language_test_language_negotiation_info_alter(array &$negotiation_info) {
-  if (\Drupal::state()->get('language_test.language_negotiation_info_alter')) {
-    unset($negotiation_info[LanguageNegotiationUI::METHOD_ID]);
-  }
-}
-
 /**
  * Store the last negotiated languages.
  */
@@ -76,37 +17,3 @@ function language_test_store_language_negotiation() {
   }
   \Drupal::keyValue('language_test')->set('language_negotiation_last', $last);
 }
-
-/**
- * Implements hook_language_fallback_candidates_alter().
- */
-function language_test_language_fallback_candidates_alter(array &$candidates, array $context) {
-  if (Drupal::state()->get('language_test.fallback_alter.candidates')) {
-    unset($candidates[LanguageInterface::LANGCODE_NOT_SPECIFIED]);
-  }
-}
-
-/**
- * Implements hook_language_fallback_candidates_OPERATION_alter().
- */
-function language_test_language_fallback_candidates_test_alter(array &$candidates, array $context) {
-  if (Drupal::state()->get('language_test.fallback_operation_alter.candidates')) {
-    $langcode = LanguageInterface::LANGCODE_NOT_APPLICABLE;
-    $candidates[$langcode] = $langcode;
-  }
-}
-
-/**
- * Implements hook_module_preinstall().
- */
-function language_test_module_preinstall() {
-  \Drupal::state()->set('language_test.language_count_preinstall', count(\Drupal::languageManager()->getLanguages()));
-}
-
-/**
- * Implements hook_language_switch_links_alter().
- */
-function language_test_language_switch_links_alter(array &$links, $type, Url $url) {
-  // Record which languages had links passed in.
-  \Drupal::state()->set('language_test.language_switch_link_ids', array_keys($links));
-}
diff --git a/core/modules/language/tests/language_test/src/Hook/LanguageTestHooks.php b/core/modules/language/tests/language_test/src/Hook/LanguageTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..07a1d80932066e042560f381c202aa4e51e21e83
--- /dev/null
+++ b/core/modules/language/tests/language_test/src/Hook/LanguageTestHooks.php
@@ -0,0 +1,118 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\language_test\Hook;
+
+use Drupal\Core\Url;
+use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUI;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for language_test.
+ */
+class LanguageTestHooks {
+
+  /**
+   * Implements hook_page_top().
+   */
+  #[Hook('page_top')]
+  public function pageTop() {
+    if (\Drupal::moduleHandler()->moduleExists('language')) {
+      language_test_store_language_negotiation();
+      \Drupal::messenger()->addStatus(t('Language negotiation method: @name', [
+        '@name' => \Drupal::languageManager()->getNegotiatedLanguageMethod() ?? 'Not defined',
+      ]));
+    }
+  }
+
+  /**
+   * Implements hook_language_types_info().
+   */
+  #[Hook('language_types_info')]
+  public function languageTypesInfo() {
+    if (\Drupal::state()->get('language_test.language_types')) {
+      return [
+        'test_language_type' => [
+          'name' => t('Test'),
+          'description' => t('A test language type.'),
+        ],
+        'fixed_test_language_type' => [
+          'fixed' => [
+            'test_language_negotiation_method',
+          ],
+          'locked' => TRUE,
+        ],
+      ];
+    }
+  }
+
+  /**
+   * Implements hook_language_types_info_alter().
+   */
+  #[Hook('language_types_info_alter')]
+  public function languageTypesInfoAlter(array &$language_types) {
+    if (\Drupal::state()->get('language_test.content_language_type')) {
+      $language_types[LanguageInterface::TYPE_CONTENT]['locked'] = FALSE;
+      unset($language_types[LanguageInterface::TYPE_CONTENT]['fixed']);
+      // By default languages are not configurable. Make
+      // LanguageInterface::TYPE_CONTENT configurable.
+      $config = \Drupal::configFactory()->getEditable('language.types');
+      $configurable = $config->get('configurable');
+      if (!in_array(LanguageInterface::TYPE_CONTENT, $configurable)) {
+        $configurable[] = LanguageInterface::TYPE_CONTENT;
+        $config->set('configurable', $configurable)->save();
+      }
+    }
+  }
+
+  /**
+   * Implements hook_language_negotiation_info_alter().
+   */
+  #[Hook('language_negotiation_info_alter')]
+  public function languageNegotiationInfoAlter(array &$negotiation_info) {
+    if (\Drupal::state()->get('language_test.language_negotiation_info_alter')) {
+      unset($negotiation_info[LanguageNegotiationUI::METHOD_ID]);
+    }
+  }
+
+  /**
+   * Implements hook_language_fallback_candidates_alter().
+   */
+  #[Hook('language_fallback_candidates_alter')]
+  public function languageFallbackCandidatesAlter(array &$candidates, array $context) {
+    if (\Drupal::state()->get('language_test.fallback_alter.candidates')) {
+      unset($candidates[LanguageInterface::LANGCODE_NOT_SPECIFIED]);
+    }
+  }
+
+  /**
+   * Implements hook_language_fallback_candidates_OPERATION_alter().
+   */
+  #[Hook('language_fallback_candidates_test_alter')]
+  public function languageFallbackCandidatesTestAlter(array &$candidates, array $context) {
+    if (\Drupal::state()->get('language_test.fallback_operation_alter.candidates')) {
+      $langcode = LanguageInterface::LANGCODE_NOT_APPLICABLE;
+      $candidates[$langcode] = $langcode;
+    }
+  }
+
+  /**
+   * Implements hook_module_preinstall().
+   */
+  #[Hook('module_preinstall')]
+  public function modulePreinstall() {
+    \Drupal::state()->set('language_test.language_count_preinstall', count(\Drupal::languageManager()->getLanguages()));
+  }
+
+  /**
+   * Implements hook_language_switch_links_alter().
+   */
+  #[Hook('language_switch_links_alter')]
+  public function languageSwitchLinksAlter(array &$links, $type, Url $url) {
+    // Record which languages had links passed in.
+    \Drupal::state()->set('language_test.language_switch_link_ids', array_keys($links));
+  }
+
+}
diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module
index 59235c0ef7f172a0a6edca775b2d3092d76a4461..ddc5ab8fb34e2f976d711807a5e3374ebf191dbf 100644
--- a/core/modules/layout_builder/layout_builder.module
+++ b/core/modules/layout_builder/layout_builder.module
@@ -2,179 +2,10 @@
 
 /**
  * @file
- * Provides hook implementations for Layout Builder.
  */
 
-use Drupal\Core\Breadcrumb\Breadcrumb;
-use Drupal\Core\Cache\CacheableMetadata;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\FieldableEntityInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Link;
-use Drupal\Core\Render\Element;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
-use Drupal\field\FieldConfigInterface;
-use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
-use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplayStorage;
-use Drupal\layout_builder\Form\DefaultsEntityForm;
-use Drupal\layout_builder\Form\LayoutBuilderEntityViewDisplayForm;
-use Drupal\layout_builder\Form\OverridesEntityForm;
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
-use Drupal\layout_builder\Plugin\Block\ExtraFieldBlock;
-use Drupal\layout_builder\InlineBlockEntityOperations;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Access\AccessResult;
 use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
 
-/**
- * Implements hook_help().
- */
-function layout_builder_help($route_name, RouteMatchInterface $route_match) {
-  // Add help text to the Layout Builder UI.
-  if ($route_match->getRouteObject()->getOption('_layout_builder')) {
-    $output = '<p>' . t('This layout builder tool allows you to configure the layout of the main content area.') . '</p>';
-    if (\Drupal::currentUser()->hasPermission('administer blocks')) {
-      $output .= '<p>' . t('To manage other areas of the page, use the <a href="@block-ui">block administration page</a>.', ['@block-ui' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
-    }
-    else {
-      $output .= '<p>' . t('To manage other areas of the page, use the block administration page.') . '</p>';
-    }
-    $output .= '<p>' . t('Forms and links inside the content of the layout builder tool have been disabled.') . '</p>';
-    return $output;
-  }
-
-  switch ($route_name) {
-    case 'help.page.layout_builder':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('Layout Builder allows you to use layouts to customize how content, content blocks, and other <a href=":field_help" title="Field module help, with background on content entities">content entities</a> are displayed.', [':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</p>';
-      $output .= '<p>' . t('For more information, see the <a href=":layout-builder-documentation">online documentation for the Layout Builder module</a>.', [':layout-builder-documentation' => 'https://www.drupal.org/docs/8/core/modules/layout-builder']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Default layouts') . '</dt>';
-      $output .= '<dd>' . t('Layout Builder can be selectively enabled on the "Manage Display" page in the <a href=":field_ui">Field UI</a>. This allows you to control the output of each type of display individually. For example, a "Basic page" might have view modes such as Full and Teaser, with each view mode having different layouts selected.', [':field_ui' => Url::fromRoute('help.page', ['name' => 'field_ui'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Overridden layouts') . '</dt>';
-      $output .= '<dd>' . t('If enabled, each individual content item can have a custom layout. Once the layout for an individual content item is overridden, changes to the Default layout will no longer affect it. Overridden layouts may be reverted to return to matching and being synchronized to their Default layout.') . '</dd>';
-      $output .= '<dt>' . t('User permissions') . '</dt>';
-      $output .= '<dd>' . t('The Layout Builder module makes a number of permissions available, which can be set by role on the <a href=":permissions">permissions page</a>. For more information, see the <a href=":layout-builder-permissions">Configuring Layout Builder permissions</a> online documentation.', [
-        ':permissions' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'layout_builder'])->toString(),
-        ':layout-builder-permissions' => 'https://www.drupal.org/docs/8/core/modules/layout-builder/configuring-layout-builder-permissions',
-      ]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function layout_builder_entity_type_alter(array &$entity_types): void {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  $entity_types['entity_view_display']
-    ->setClass(LayoutBuilderEntityViewDisplay::class)
-    ->setStorageClass(LayoutBuilderEntityViewDisplayStorage::class)
-    ->setFormClass('layout_builder', DefaultsEntityForm::class)
-    ->setFormClass('edit', LayoutBuilderEntityViewDisplayForm::class);
-
-  // Ensure every fieldable entity type has a layout form.
-  foreach ($entity_types as $entity_type) {
-    if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
-      $entity_type->setFormClass('layout_builder', OverridesEntityForm::class);
-    }
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityFormDisplayEditForm.
- */
-function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state): void {
-  // Hides the Layout Builder field. It is rendered directly in
-  // \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
-  unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
-  $key = array_search(OverridesSectionStorage::FIELD_NAME, $form['#fields']);
-  if ($key !== FALSE) {
-    unset($form['#fields'][$key]);
-  }
-}
-
-/**
- * Implements hook_field_config_insert().
- */
-function layout_builder_field_config_insert(FieldConfigInterface $field_config) {
-  // Clear the sample entity for this entity type and bundle.
-  $sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator');
-  $sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle());
-  \Drupal::service('plugin.manager.block')->clearCachedDefinitions();
-}
-
-/**
- * Implements hook_field_config_delete().
- */
-function layout_builder_field_config_delete(FieldConfigInterface $field_config) {
-  // Clear the sample entity for this entity type and bundle.
-  $sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator');
-  $sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle());
-  \Drupal::service('plugin.manager.block')->clearCachedDefinitions();
-}
-
-/**
- * Implements hook_entity_view_alter().
- *
- * ExtraFieldBlock block plugins add placeholders for each extra field which is
- * configured to be displayed. Those placeholders are replaced by this hook.
- * Modules that implement hook_entity_extra_field_info() use their
- * implementations of hook_entity_view_alter() to add the rendered output of
- * the extra fields they provide, so we cannot get the rendered output of extra
- * fields before this point in the view process.
- * layout_builder_module_implements_alter() moves this implementation of
- * hook_entity_view_alter() to the end of the list.
- *
- * @see \Drupal\layout_builder\Plugin\Block\ExtraFieldBlock::build()
- * @see layout_builder_module_implements_alter()
- */
-function layout_builder_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
-  // Only replace extra fields when Layout Builder has been used to alter the
-  // build. See \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
-  if (isset($build['_layout_builder']) && !Element::isEmpty($build['_layout_builder'])) {
-    /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */
-    $field_manager = \Drupal::service('entity_field.manager');
-    $extra_fields = $field_manager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
-    if (!empty($extra_fields['display'])) {
-      foreach ($extra_fields['display'] as $field_name => $extra_field) {
-        // If the extra field is not set replace with an empty array to avoid
-        // the placeholder text from being rendered.
-        $replacement = $build[$field_name] ?? [];
-        ExtraFieldBlock::replaceFieldPlaceholder($build, $replacement, $field_name);
-        // After the rendered field in $build has been copied over to the
-        // ExtraFieldBlock block we must remove it from its original location or
-        // else it will be rendered twice.
-        unset($build[$field_name]);
-      }
-    }
-  }
-
-  $route_name = \Drupal::routeMatch()->getRouteName();
-
-  // If the entity is displayed within a Layout Builder block and the current
-  // route is in the Layout Builder UI, then remove all contextual link
-  // placeholders.
-  if ($route_name && $display instanceof LayoutBuilderEntityViewDisplay && str_starts_with($route_name, 'layout_builder.')) {
-    unset($build['#contextual_links']);
-  }
-}
-
-/**
- * Implements hook_entity_build_defaults_alter().
- */
-function layout_builder_entity_build_defaults_alter(array &$build, EntityInterface $entity, $view_mode) {
-  // Contextual links are removed for entities viewed in Layout Builder's UI.
-  // The route.name.is_layout_builder_ui cache context accounts for this
-  // difference.
-  // @see layout_builder_entity_view_alter()
-  // @see \Drupal\layout_builder\Cache\LayoutBuilderUiCacheContext
-  $build['#cache']['contexts'][] = 'route.name.is_layout_builder_ui';
-}
-
 /**
  * Implements hook_module_implements_alter().
  */
@@ -189,196 +20,6 @@ function layout_builder_module_implements_alter(&$implementations, $hook) {
   }
 }
 
-/**
- * Implements hook_entity_presave().
- */
-function layout_builder_entity_presave(EntityInterface $entity) {
-  if (\Drupal::moduleHandler()->moduleExists('block_content')) {
-    /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
-    $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
-    $entity_operations->handlePreSave($entity);
-  }
-}
-
-/**
- * Implements hook_entity_delete().
- */
-function layout_builder_entity_delete(EntityInterface $entity) {
-  if (\Drupal::moduleHandler()->moduleExists('block_content')) {
-    /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
-    $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
-    $entity_operations->handleEntityDelete($entity);
-  }
-}
-
-/**
- * Implements hook_cron().
- */
-function layout_builder_cron() {
-  if (\Drupal::moduleHandler()->moduleExists('block_content')) {
-    /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
-    $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
-    $entity_operations->removeUnused();
-  }
-}
-
-/**
- * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
- */
-function layout_builder_plugin_filter_block__layout_builder_alter(array &$definitions, array $extra) {
-  // Remove blocks that are not useful within Layout Builder.
-  unset($definitions['system_messages_block']);
-  unset($definitions['help_block']);
-  unset($definitions['local_tasks_block']);
-  unset($definitions['local_actions_block']);
-
-  // Remove blocks that are non-functional within Layout Builder.
-  unset($definitions['system_main_block']);
-  // @todo Restore the page title block in https://www.drupal.org/node/2938129.
-  unset($definitions['page_title_block']);
-}
-
-/**
- * Implements hook_plugin_filter_TYPE_alter().
- */
-function layout_builder_plugin_filter_block_alter(array &$definitions, array $extra, $consumer) {
-  // @todo Determine the 'inline_block' blocks should be allowed outside
-  //   of layout_builder https://www.drupal.org/node/2979142.
-  if ($consumer !== 'layout_builder' || !isset($extra['list']) || $extra['list'] !== 'inline_blocks') {
-    foreach ($definitions as $id => $definition) {
-      if ($definition['id'] === 'inline_block') {
-        unset($definitions[$id]);
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_access().
- */
-function layout_builder_block_content_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  /** @var \Drupal\block_content\BlockContentInterface $entity */
-  if ($operation === 'view' || $entity->isReusable() || empty(\Drupal::service('inline_block.usage')->getUsage($entity->id()))) {
-    // If the operation is 'view' or this is reusable block or if this is
-    // non-reusable that isn't used by this module then don't alter the access.
-    return AccessResult::neutral();
-  }
-
-  if ($account->hasPermission('create and edit custom blocks')) {
-    return AccessResult::allowed();
-  }
-  return AccessResult::forbidden();
-}
-
-/**
- * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
- */
-function layout_builder_plugin_filter_block__block_ui_alter(array &$definitions, array $extra) {
-  foreach ($definitions as $id => $definition) {
-    // Filter out any layout_builder-provided block that has required context
-    // definitions.
-    if ($definition['provider'] === 'layout_builder' && !empty($definition['context_definitions'])) {
-      /** @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context_definition */
-      foreach ($definition['context_definitions'] as $context_definition) {
-        if ($context_definition->isRequired()) {
-          unset($definitions[$id]);
-          break;
-        }
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
- */
-function layout_builder_plugin_filter_layout__layout_builder_alter(array &$definitions, array $extra) {
-  // Remove layouts provide by layout discovery that are not needed because of
-  // layouts provided by this module.
-  $duplicate_layouts = [
-    'layout_twocol',
-    'layout_twocol_bricks',
-    'layout_threecol_25_50_25',
-    'layout_threecol_33_34_33',
-  ];
-
-  foreach ($duplicate_layouts as $duplicate_layout) {
-    /** @var \Drupal\Core\Layout\LayoutDefinition[] $definitions */
-    if (isset($definitions[$duplicate_layout])) {
-      if ($definitions[$duplicate_layout]->getProvider() === 'layout_discovery') {
-        unset($definitions[$duplicate_layout]);
-      }
-    }
-  }
-
-  // Move the one column layout to the top.
-  if (isset($definitions['layout_onecol']) && $definitions['layout_onecol']->getProvider() === 'layout_discovery') {
-    $one_col = $definitions['layout_onecol'];
-    unset($definitions['layout_onecol']);
-    $definitions = [
-      'layout_onecol' => $one_col,
-    ] + $definitions;
-  }
-}
-
-/**
- * Implements hook_plugin_filter_TYPE_alter().
- */
-function layout_builder_plugin_filter_layout_alter(array &$definitions, array $extra, $consumer) {
-  // Hide the blank layout plugin from listings.
-  unset($definitions['layout_builder_blank']);
-}
-
-/**
- * Implements hook_system_breadcrumb_alter().
- */
-function layout_builder_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) {
-  // Remove the extra 'Manage display' breadcrumb for Layout Builder defaults.
-  if ($route_match->getRouteObject() && $route_match->getRouteObject()->hasOption('_layout_builder') && $route_match->getParameter('section_storage_type') === 'defaults') {
-    $links = array_filter($breadcrumb->getLinks(), function (Link $link) use ($route_match) {
-      $entity_type_id = $route_match->getParameter('entity_type_id');
-      if (!$link->getUrl()->isRouted()) {
-        return TRUE;
-      }
-      return $link->getUrl()->getRouteName() !== "entity.entity_view_display.$entity_type_id.default";
-    });
-    // Links cannot be removed from an existing breadcrumb object. Create a new
-    // object but carry over the cacheable metadata.
-    $cacheability = CacheableMetadata::createFromObject($breadcrumb);
-    $breadcrumb = new Breadcrumb();
-    $breadcrumb->setLinks($links);
-    $breadcrumb->addCacheableDependency($cacheability);
-  }
-}
-
-/**
- * Implements hook_entity_translation_create().
- */
-function layout_builder_entity_translation_create(EntityInterface $translation) {
-  /** @var \Drupal\Core\Entity\FieldableEntityInterface $translation */
-  if ($translation->hasField(OverridesSectionStorage::FIELD_NAME) && $translation->getFieldDefinition(OverridesSectionStorage::FIELD_NAME)->isTranslatable()) {
-    // When creating a new translation do not copy untranslated sections because
-    // per-language layouts are not supported.
-    $translation->set(OverridesSectionStorage::FIELD_NAME, []);
-  }
-}
-
-/**
- * Implements hook_theme_registry_alter().
- */
-function layout_builder_theme_registry_alter(&$theme_registry) {
-  // Move our preprocess to run after
-  // content_translation_preprocess_language_content_settings_table().
-  if (!empty($theme_registry['language_content_settings_table']['preprocess functions'])) {
-    $preprocess_functions = &$theme_registry['language_content_settings_table']['preprocess functions'];
-    $index = array_search('layout_builder_preprocess_language_content_settings_table', $preprocess_functions);
-    if ($index !== FALSE) {
-      unset($preprocess_functions[$index]);
-      $preprocess_functions[] = 'layout_builder_preprocess_language_content_settings_table';
-    }
-  }
-}
-
 /**
  * Implements hook_preprocess_HOOK() for language-content-settings-table.html.twig.
  */
@@ -400,15 +41,3 @@ function layout_builder_preprocess_language_content_settings_table(&$variables)
     }
   }
 }
-
-/**
- * Implements hook_theme_suggestions_HOOK_alter().
- */
-function layout_builder_theme_suggestions_field_alter(&$suggestions, array $variables) {
-  $element = $variables['element'];
-  if (isset($element['#third_party_settings']['layout_builder']['view_mode'])) {
-    // See system_theme_suggestions_field().
-    $suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name'] . '__' . $element['#bundle'] . '__' . $element['#third_party_settings']['layout_builder']['view_mode'];
-  }
-  return $suggestions;
-}
diff --git a/core/modules/layout_builder/modules/layout_builder_expose_all_field_blocks/layout_builder_expose_all_field_blocks.module b/core/modules/layout_builder/modules/layout_builder_expose_all_field_blocks/layout_builder_expose_all_field_blocks.module
deleted file mode 100644
index 71e00e371711ac853d76b4ea68b96bad92319447..0000000000000000000000000000000000000000
--- a/core/modules/layout_builder/modules/layout_builder_expose_all_field_blocks/layout_builder_expose_all_field_blocks.module
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-/**
- * @file
- * Module implementation file.
- */
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function layout_builder_expose_all_field_blocks_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.layout_builder_expose_all_field_blocks':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Layout Builder Expose All Field Blocks module is a Feature Flag module which exposes all fields on all bundles as field blocks for use in Layout Builder.') . '</p>';
-      $output .= '<p>' . t('Using this feature can significantly reduce the performance of medium to large sites due to the number of Field Block plugins that will be created. It is recommended to uninstall this module, if possible.') . '</p>';
-      $output .= '<p>' . t('While it is recommended to uninstall this module, doing so may remove blocks that are being used in your site.') . '</p>';
-      $output .= '<p>' . t("For example, if Layout Builder is enabled on a Node bundle (Content type), and that bundle's display is using field blocks from the User entity (e.g. the Author's name), but Layout Builder is not enabled for the User bundle, then that field block would no longer exist after uninstalling this module.") . '</p>';
-      $output .= '<p>' . t('For more information, see the <a href=":href">online documentation for the Layout Builder Expose All Field Blocks module</a>.', [':href' => 'https://www.drupal.org/node/3223395#s-layout-builder-expose-all-field-blocks']) . '</p>';
-      return $output;
-  }
-  return NULL;
-}
diff --git a/core/modules/layout_builder/modules/layout_builder_expose_all_field_blocks/src/Hook/LayoutBuilderExposeAllFieldBlocksHooks.php b/core/modules/layout_builder/modules/layout_builder_expose_all_field_blocks/src/Hook/LayoutBuilderExposeAllFieldBlocksHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c2d569ba893496c8e3634f26050f55c80509e041
--- /dev/null
+++ b/core/modules/layout_builder/modules/layout_builder_expose_all_field_blocks/src/Hook/LayoutBuilderExposeAllFieldBlocksHooks.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\layout_builder_expose_all_field_blocks\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for layout_builder_expose_all_field_blocks.
+ */
+class LayoutBuilderExposeAllFieldBlocksHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.layout_builder_expose_all_field_blocks':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Layout Builder Expose All Field Blocks module is a Feature Flag module which exposes all fields on all bundles as field blocks for use in Layout Builder.') . '</p>';
+        $output .= '<p>' . t('Using this feature can significantly reduce the performance of medium to large sites due to the number of Field Block plugins that will be created. It is recommended to uninstall this module, if possible.') . '</p>';
+        $output .= '<p>' . t('While it is recommended to uninstall this module, doing so may remove blocks that are being used in your site.') . '</p>';
+        $output .= '<p>' . t("For example, if Layout Builder is enabled on a Node bundle (Content type), and that bundle's display is using field blocks from the User entity (e.g. the Author's name), but Layout Builder is not enabled for the User bundle, then that field block would no longer exist after uninstalling this module.") . '</p>';
+        $output .= '<p>' . t('For more information, see the <a href=":href">online documentation for the Layout Builder Expose All Field Blocks module</a>.', [
+          ':href' => 'https://www.drupal.org/node/3223395#s-layout-builder-expose-all-field-blocks',
+        ]) . '</p>';
+        return $output;
+    }
+    return NULL;
+  }
+
+}
diff --git a/core/modules/layout_builder/src/Hook/LayoutBuilderHooks.php b/core/modules/layout_builder/src/Hook/LayoutBuilderHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..7617f050394e1a9fd43694e8305fd7d83a58a37b
--- /dev/null
+++ b/core/modules/layout_builder/src/Hook/LayoutBuilderHooks.php
@@ -0,0 +1,401 @@
+<?php
+
+namespace Drupal\layout_builder\Hook;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Link;
+use Drupal\Core\Breadcrumb\Breadcrumb;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\layout_builder\InlineBlockEntityOperations;
+use Drupal\layout_builder\Plugin\Block\ExtraFieldBlock;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\field\FieldConfigInterface;
+use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\layout_builder\Form\OverridesEntityForm;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\layout_builder\Form\LayoutBuilderEntityViewDisplayForm;
+use Drupal\layout_builder\Form\DefaultsEntityForm;
+use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplayStorage;
+use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for layout_builder.
+ */
+class LayoutBuilderHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    // Add help text to the Layout Builder UI.
+    if ($route_match->getRouteObject()->getOption('_layout_builder')) {
+      $output = '<p>' . t('This layout builder tool allows you to configure the layout of the main content area.') . '</p>';
+      if (\Drupal::currentUser()->hasPermission('administer blocks')) {
+        $output .= '<p>' . t('To manage other areas of the page, use the <a href="@block-ui">block administration page</a>.', ['@block-ui' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
+      }
+      else {
+        $output .= '<p>' . t('To manage other areas of the page, use the block administration page.') . '</p>';
+      }
+      $output .= '<p>' . t('Forms and links inside the content of the layout builder tool have been disabled.') . '</p>';
+      return $output;
+    }
+    switch ($route_name) {
+      case 'help.page.layout_builder':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('Layout Builder allows you to use layouts to customize how content, content blocks, and other <a href=":field_help" title="Field module help, with background on content entities">content entities</a> are displayed.', [
+          ':field_help' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+        ]) . '</p>';
+        $output .= '<p>' . t('For more information, see the <a href=":layout-builder-documentation">online documentation for the Layout Builder module</a>.', [
+          ':layout-builder-documentation' => 'https://www.drupal.org/docs/8/core/modules/layout-builder',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Default layouts') . '</dt>';
+        $output .= '<dd>' . t('Layout Builder can be selectively enabled on the "Manage Display" page in the <a href=":field_ui">Field UI</a>. This allows you to control the output of each type of display individually. For example, a "Basic page" might have view modes such as Full and Teaser, with each view mode having different layouts selected.', [
+          ':field_ui' => Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Overridden layouts') . '</dt>';
+        $output .= '<dd>' . t('If enabled, each individual content item can have a custom layout. Once the layout for an individual content item is overridden, changes to the Default layout will no longer affect it. Overridden layouts may be reverted to return to matching and being synchronized to their Default layout.') . '</dd>';
+        $output .= '<dt>' . t('User permissions') . '</dt>';
+        $output .= '<dd>' . t('The Layout Builder module makes a number of permissions available, which can be set by role on the <a href=":permissions">permissions page</a>. For more information, see the <a href=":layout-builder-permissions">Configuring Layout Builder permissions</a> online documentation.', [
+          ':permissions' => Url::fromRoute('user.admin_permissions.module', [
+            'modules' => 'layout_builder',
+          ])->toString(),
+          ':layout-builder-permissions' => 'https://www.drupal.org/docs/8/core/modules/layout-builder/configuring-layout-builder-permissions',
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    $entity_types['entity_view_display']->setClass(LayoutBuilderEntityViewDisplay::class)->setStorageClass(LayoutBuilderEntityViewDisplayStorage::class)->setFormClass('layout_builder', DefaultsEntityForm::class)->setFormClass('edit', LayoutBuilderEntityViewDisplayForm::class);
+    // Ensure every fieldable entity type has a layout form.
+    foreach ($entity_types as $entity_type) {
+      if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
+        $entity_type->setFormClass('layout_builder', OverridesEntityForm::class);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityFormDisplayEditForm.
+   */
+  #[Hook('form_entity_form_display_edit_form_alter')]
+  public function formEntityFormDisplayEditFormAlter(&$form, FormStateInterface $form_state) : void {
+    // Hides the Layout Builder field. It is rendered directly in
+    // \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
+    unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
+    $key = array_search(OverridesSectionStorage::FIELD_NAME, $form['#fields']);
+    if ($key !== FALSE) {
+      unset($form['#fields'][$key]);
+    }
+  }
+
+  /**
+   * Implements hook_field_config_insert().
+   */
+  #[Hook('field_config_insert')]
+  public function fieldConfigInsert(FieldConfigInterface $field_config) {
+    // Clear the sample entity for this entity type and bundle.
+    $sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator');
+    $sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle());
+    \Drupal::service('plugin.manager.block')->clearCachedDefinitions();
+  }
+
+  /**
+   * Implements hook_field_config_delete().
+   */
+  #[Hook('field_config_delete')]
+  public function fieldConfigDelete(FieldConfigInterface $field_config) {
+    // Clear the sample entity for this entity type and bundle.
+    $sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator');
+    $sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle());
+    \Drupal::service('plugin.manager.block')->clearCachedDefinitions();
+  }
+
+  /**
+   * Implements hook_entity_view_alter().
+   *
+   * ExtraFieldBlock block plugins add placeholders for each extra field which is
+   * configured to be displayed. Those placeholders are replaced by this hook.
+   * Modules that implement hook_entity_extra_field_info() use their
+   * implementations of hook_entity_view_alter() to add the rendered output of
+   * the extra fields they provide, so we cannot get the rendered output of extra
+   * fields before this point in the view process.
+   * layout_builder_module_implements_alter() moves this implementation of
+   * hook_entity_view_alter() to the end of the list.
+   *
+   * @see \Drupal\layout_builder\Plugin\Block\ExtraFieldBlock::build()
+   * @see layout_builder_module_implements_alter()
+   */
+  #[Hook('entity_view_alter')]
+  public function entityViewAlter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
+    // Only replace extra fields when Layout Builder has been used to alter the
+    // build. See \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
+    if (isset($build['_layout_builder']) && !Element::isEmpty($build['_layout_builder'])) {
+      /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */
+      $field_manager = \Drupal::service('entity_field.manager');
+      $extra_fields = $field_manager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
+      if (!empty($extra_fields['display'])) {
+        foreach ($extra_fields['display'] as $field_name => $extra_field) {
+          // If the extra field is not set replace with an empty array to avoid
+          // the placeholder text from being rendered.
+          $replacement = $build[$field_name] ?? [];
+          ExtraFieldBlock::replaceFieldPlaceholder($build, $replacement, $field_name);
+          // After the rendered field in $build has been copied over to the
+          // ExtraFieldBlock block we must remove it from its original location or
+          // else it will be rendered twice.
+          unset($build[$field_name]);
+        }
+      }
+    }
+    $route_name = \Drupal::routeMatch()->getRouteName();
+    // If the entity is displayed within a Layout Builder block and the current
+    // route is in the Layout Builder UI, then remove all contextual link
+    // placeholders.
+    if ($route_name && $display instanceof LayoutBuilderEntityViewDisplay && str_starts_with($route_name, 'layout_builder.')) {
+      unset($build['#contextual_links']);
+    }
+  }
+
+  /**
+   * Implements hook_entity_build_defaults_alter().
+   */
+  #[Hook('entity_build_defaults_alter')]
+  public function entityBuildDefaultsAlter(array &$build, EntityInterface $entity, $view_mode) {
+    // Contextual links are removed for entities viewed in Layout Builder's UI.
+    // The route.name.is_layout_builder_ui cache context accounts for this
+    // difference.
+    // @see layout_builder_entity_view_alter()
+    // @see \Drupal\layout_builder\Cache\LayoutBuilderUiCacheContext
+    $build['#cache']['contexts'][] = 'route.name.is_layout_builder_ui';
+  }
+
+  /**
+   * Implements hook_entity_presave().
+   */
+  #[Hook('entity_presave')]
+  public function entityPresave(EntityInterface $entity) {
+    if (\Drupal::moduleHandler()->moduleExists('block_content')) {
+      /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
+      $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
+      $entity_operations->handlePreSave($entity);
+    }
+  }
+
+  /**
+   * Implements hook_entity_delete().
+   */
+  #[Hook('entity_delete')]
+  public function entityDelete(EntityInterface $entity) {
+    if (\Drupal::moduleHandler()->moduleExists('block_content')) {
+      /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
+      $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
+      $entity_operations->handleEntityDelete($entity);
+    }
+  }
+
+  /**
+   * Implements hook_cron().
+   */
+  #[Hook('cron')]
+  public function cron() {
+    if (\Drupal::moduleHandler()->moduleExists('block_content')) {
+      /** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
+      $entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
+      $entity_operations->removeUnused();
+    }
+  }
+
+  /**
+   * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
+   */
+  #[Hook('plugin_filter_block__layout_builder_alter')]
+  public function pluginFilterBlockLayoutBuilderAlter(array &$definitions, array $extra) {
+    // Remove blocks that are not useful within Layout Builder.
+    unset($definitions['system_messages_block']);
+    unset($definitions['help_block']);
+    unset($definitions['local_tasks_block']);
+    unset($definitions['local_actions_block']);
+    // Remove blocks that are non-functional within Layout Builder.
+    unset($definitions['system_main_block']);
+    // @todo Restore the page title block in https://www.drupal.org/node/2938129.
+    unset($definitions['page_title_block']);
+  }
+
+  /**
+   * Implements hook_plugin_filter_TYPE_alter().
+   */
+  #[Hook('plugin_filter_block_alter')]
+  public function pluginFilterBlockAlter(array &$definitions, array $extra, $consumer) {
+    // @todo Determine the 'inline_block' blocks should be allowed outside
+    //   of layout_builder https://www.drupal.org/node/2979142.
+    if ($consumer !== 'layout_builder' || !isset($extra['list']) || $extra['list'] !== 'inline_blocks') {
+      foreach ($definitions as $id => $definition) {
+        if ($definition['id'] === 'inline_block') {
+          unset($definitions[$id]);
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_access().
+   */
+  #[Hook('block_content_access')]
+  public function blockContentAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\block_content\BlockContentInterface $entity */
+    if ($operation === 'view' || $entity->isReusable() || empty(\Drupal::service('inline_block.usage')->getUsage($entity->id()))) {
+      // If the operation is 'view' or this is reusable block or if this is
+      // non-reusable that isn't used by this module then don't alter the access.
+      return AccessResult::neutral();
+    }
+    if ($account->hasPermission('create and edit custom blocks')) {
+      return AccessResult::allowed();
+    }
+    return AccessResult::forbidden();
+  }
+
+  /**
+   * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
+   */
+  #[Hook('plugin_filter_block__block_ui_alter')]
+  public function pluginFilterBlockBlockUiAlter(array &$definitions, array $extra) {
+    foreach ($definitions as $id => $definition) {
+      // Filter out any layout_builder-provided block that has required context
+      // definitions.
+      if ($definition['provider'] === 'layout_builder' && !empty($definition['context_definitions'])) {
+        /** @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context_definition */
+        foreach ($definition['context_definitions'] as $context_definition) {
+          if ($context_definition->isRequired()) {
+            unset($definitions[$id]);
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
+   */
+  #[Hook('plugin_filter_layout__layout_builder_alter')]
+  public function pluginFilterLayoutLayoutBuilderAlter(array &$definitions, array $extra) {
+    // Remove layouts provide by layout discovery that are not needed because of
+    // layouts provided by this module.
+    $duplicate_layouts = [
+      'layout_twocol',
+      'layout_twocol_bricks',
+      'layout_threecol_25_50_25',
+      'layout_threecol_33_34_33',
+    ];
+    foreach ($duplicate_layouts as $duplicate_layout) {
+      /** @var \Drupal\Core\Layout\LayoutDefinition[] $definitions */
+      if (isset($definitions[$duplicate_layout])) {
+        if ($definitions[$duplicate_layout]->getProvider() === 'layout_discovery') {
+          unset($definitions[$duplicate_layout]);
+        }
+      }
+    }
+    // Move the one column layout to the top.
+    if (isset($definitions['layout_onecol']) && $definitions['layout_onecol']->getProvider() === 'layout_discovery') {
+      $one_col = $definitions['layout_onecol'];
+      unset($definitions['layout_onecol']);
+      $definitions = ['layout_onecol' => $one_col] + $definitions;
+    }
+  }
+
+  /**
+   * Implements hook_plugin_filter_TYPE_alter().
+   */
+  #[Hook('plugin_filter_layout_alter')]
+  public function pluginFilterLayoutAlter(array &$definitions, array $extra, $consumer) {
+    // Hide the blank layout plugin from listings.
+    unset($definitions['layout_builder_blank']);
+  }
+
+  /**
+   * Implements hook_system_breadcrumb_alter().
+   */
+  #[Hook('system_breadcrumb_alter')]
+  public function systemBreadcrumbAlter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) {
+    // Remove the extra 'Manage display' breadcrumb for Layout Builder defaults.
+    if ($route_match->getRouteObject() && $route_match->getRouteObject()->hasOption('_layout_builder') && $route_match->getParameter('section_storage_type') === 'defaults') {
+      $links = array_filter($breadcrumb->getLinks(), function (Link $link) use ($route_match) {
+          $entity_type_id = $route_match->getParameter('entity_type_id');
+        if (!$link->getUrl()->isRouted()) {
+                return TRUE;
+        }
+          return $link->getUrl()->getRouteName() !== "entity.entity_view_display.{$entity_type_id}.default";
+      });
+      // Links cannot be removed from an existing breadcrumb object. Create a new
+      // object but carry over the cacheable metadata.
+      $cacheability = CacheableMetadata::createFromObject($breadcrumb);
+      $breadcrumb = new Breadcrumb();
+      $breadcrumb->setLinks($links);
+      $breadcrumb->addCacheableDependency($cacheability);
+    }
+  }
+
+  /**
+   * Implements hook_entity_translation_create().
+   */
+  #[Hook('entity_translation_create')]
+  public function entityTranslationCreate(EntityInterface $translation) {
+    /** @var \Drupal\Core\Entity\FieldableEntityInterface $translation */
+    if ($translation->hasField(OverridesSectionStorage::FIELD_NAME) && $translation->getFieldDefinition(OverridesSectionStorage::FIELD_NAME)->isTranslatable()) {
+      // When creating a new translation do not copy untranslated sections because
+      // per-language layouts are not supported.
+      $translation->set(OverridesSectionStorage::FIELD_NAME, []);
+    }
+  }
+
+  /**
+   * Implements hook_theme_registry_alter().
+   */
+  #[Hook('theme_registry_alter')]
+  public function themeRegistryAlter(&$theme_registry) {
+    // Move our preprocess to run after
+    // content_translation_preprocess_language_content_settings_table().
+    if (!empty($theme_registry['language_content_settings_table']['preprocess functions'])) {
+      $preprocess_functions =& $theme_registry['language_content_settings_table']['preprocess functions'];
+      $index = array_search('layout_builder_preprocess_language_content_settings_table', $preprocess_functions);
+      if ($index !== FALSE) {
+        unset($preprocess_functions[$index]);
+        $preprocess_functions[] = 'layout_builder_preprocess_language_content_settings_table';
+      }
+    }
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK_alter().
+   */
+  #[Hook('theme_suggestions_field_alter')]
+  public function themeSuggestionsFieldAlter(&$suggestions, array $variables) {
+    $element = $variables['element'];
+    if (isset($element['#third_party_settings']['layout_builder']['view_mode'])) {
+      // See system_theme_suggestions_field().
+      $suggestions[] = 'field__' . $element['#entity_type'] . '__' . $element['#field_name'] . '__' . $element['#bundle'] . '__' . $element['#third_party_settings']['layout_builder']['view_mode'];
+    }
+    return $suggestions;
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_extra_field_test/layout_builder_extra_field_test.module b/core/modules/layout_builder/tests/modules/layout_builder_extra_field_test/layout_builder_extra_field_test.module
index 7af0cf235e28d483d4910aca2bd081fcc37f58ab..1b4365e0772de515a3c2b9395168998ff9ee2c63 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_extra_field_test/layout_builder_extra_field_test.module
+++ b/core/modules/layout_builder/tests/modules/layout_builder_extra_field_test/layout_builder_extra_field_test.module
@@ -10,18 +10,6 @@
 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Entity\EntityInterface;
 
-/**
- * Implements hook_entity_extra_field_info().
- */
-function layout_builder_extra_field_test_entity_extra_field_info() {
-  $extra['node']['bundle_with_section_field']['display']['layout_builder_extra_field_test'] = [
-    'label' => t('New Extra Field'),
-    'description' => t('New Extra Field description'),
-    'weight' => 0,
-  ];
-  return $extra;
-}
-
 /**
  * Implements hook_entity_node_view().
  */
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_extra_field_test/src/Hook/LayoutBuilderExtraFieldTestHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_extra_field_test/src/Hook/LayoutBuilderExtraFieldTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..2a3ca3c22e69ed623b53d923555e66f8916600e5
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/layout_builder_extra_field_test/src/Hook/LayoutBuilderExtraFieldTestHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\layout_builder_extra_field_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for layout_builder_extra_field_test.
+ */
+class LayoutBuilderExtraFieldTestHooks {
+
+  /**
+   * Implements hook_entity_extra_field_info().
+   */
+  #[Hook('entity_extra_field_info')]
+  public function entityExtraFieldInfo() {
+    $extra['node']['bundle_with_section_field']['display']['layout_builder_extra_field_test'] = [
+      'label' => t('New Extra Field'),
+      'description' => t('New Extra Field description'),
+      'weight' => 0,
+    ];
+    return $extra;
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module b/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module
index ea008e65480cc9bcd970dd369a1fe4b0af140ce2..a48a553a2871c9fb341ed4058acf9497bbb0fd13 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module
@@ -7,60 +7,8 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Breadcrumb\Breadcrumb;
-use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Link;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
- */
-function layout_builder_test_plugin_filter_block__layout_builder_alter(array &$definitions, array $extra) {
-  // Explicitly remove the "Help" blocks from the list.
-  unset($definitions['help_block']);
-
-  // Explicitly remove the "Sticky at top of lists field_block".
-  $disallowed_fields = [
-    'sticky',
-  ];
-
-  // Remove "Changed" field if this is the first section.
-  if ($extra['delta'] === 0) {
-    $disallowed_fields[] = 'changed';
-  }
-
-  foreach ($definitions as $plugin_id => $definition) {
-    // Field block IDs are in the form 'field_block:{entity}:{bundle}:{name}',
-    // for example 'field_block:node:article:revision_timestamp'.
-    preg_match('/field_block:.*:.*:(.*)/', $plugin_id, $parts);
-    if (isset($parts[1]) && in_array($parts[1], $disallowed_fields, TRUE)) {
-      // Unset any field blocks that match our predefined list.
-      unset($definitions[$plugin_id]);
-    }
-  }
-}
-
-/**
- * Implements hook_entity_extra_field_info().
- */
-function layout_builder_test_entity_extra_field_info() {
-  $extra['node']['bundle_with_section_field']['display']['layout_builder_test'] = [
-    'label' => t('Extra label'),
-    'description' => t('Extra description'),
-    'weight' => 0,
-  ];
-  $extra['node']['bundle_with_section_field']['display']['layout_builder_test_2'] = [
-    'label' => t('Extra Field 2'),
-    'description' => t('Extra Field 2 description'),
-    'weight' => 0,
-    'visible' => FALSE,
-  ];
-  return $extra;
-}
 
 /**
  * Implements hook_entity_node_view().
@@ -78,62 +26,6 @@ function layout_builder_test_node_view(array &$build, EntityInterface $entity, E
   }
 }
 
-/**
- * Implements hook_form_BASE_FORM_ID_alter() for layout_builder_configure_block.
- */
-function layout_builder_test_form_layout_builder_configure_block_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  /** @var \Drupal\layout_builder\Form\ConfigureBlockFormBase $form_object */
-  $form_object = $form_state->getFormObject();
-
-  $form['layout_builder_test']['storage'] = [
-    '#type' => 'item',
-    '#title' => 'Layout Builder Storage: ' . $form_object->getSectionStorage()->getStorageId(),
-  ];
-  $form['layout_builder_test']['section'] = [
-    '#type' => 'item',
-    '#title' => 'Layout Builder Section: ' . $form_object->getCurrentSection()->getLayoutId(),
-  ];
-  $form['layout_builder_test']['component'] = [
-    '#type' => 'item',
-    '#title' => 'Layout Builder Component: ' . $form_object->getCurrentComponent()->getPluginId(),
-  ];
-}
-
-/**
- * Implements hook_form_BASE_FORM_ID_alter() for layout_builder_configure_section.
- */
-function layout_builder_test_form_layout_builder_configure_section_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  /** @var \Drupal\layout_builder\Form\ConfigureSectionForm $form_object */
-  $form_object = $form_state->getFormObject();
-
-  $form['layout_builder_test']['storage'] = [
-    '#type' => 'item',
-    '#title' => 'Layout Builder Storage: ' . $form_object->getSectionStorage()->getStorageId(),
-  ];
-  $form['layout_builder_test']['section'] = [
-    '#type' => 'item',
-    '#title' => 'Layout Builder Section: ' . $form_object->getCurrentSection()->getLayoutId(),
-  ];
-  $form['layout_builder_test']['layout'] = [
-    '#type' => 'item',
-    '#title' => 'Layout Builder Layout: ' . $form_object->getCurrentLayout()->getPluginId(),
-  ];
-}
-
-/**
- * Implements hook_entity_form_display_alter().
- */
-function layout_builder_entity_form_display_alter(EntityFormDisplayInterface $form_display, array $context) {
-  if ($context['form_mode'] === 'layout_builder') {
-    $form_display->setComponent('status', [
-      'type' => 'boolean_checkbox',
-      'settings' => [
-        'display_label' => TRUE,
-      ],
-    ]);
-  }
-}
-
 /**
  * Implements hook_preprocess_HOOK() for one-column layout template.
  */
@@ -158,13 +50,6 @@ function layout_builder_test_preprocess_layout__twocol_section(&$vars) {
   }
 }
 
-/**
- * Implements hook_system_breadcrumb_alter().
- */
-function layout_builder_test_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) {
-  $breadcrumb->addLink(Link::fromTextAndUrl('External link', Url::fromUri('http://www.example.com')));
-}
-
 /**
  * Implements hook_module_implements_alter().
  */
@@ -178,14 +63,3 @@ function layout_builder_test_module_implements_alter(&$implementations, $hook) {
     ] + $implementations;
   }
 }
-
-/**
- * Implements hook_theme().
- */
-function layout_builder_test_theme(): array {
-  return [
-    'block__preview_aware_block' => [
-      'base hook' => 'block',
-    ],
-  ];
-}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..06b748b224ad46ce55d47554bd9331394cf1513a
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php
@@ -0,0 +1,131 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\layout_builder_test\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Link;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Breadcrumb\Breadcrumb;
+use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for layout_builder_test.
+ */
+class LayoutBuilderTestHooks {
+
+  /**
+   * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
+   */
+  #[Hook('plugin_filter_block__layout_builder_alter')]
+  public function pluginFilterBlockLayoutBuilderAlter(array &$definitions, array $extra) {
+    // Explicitly remove the "Help" blocks from the list.
+    unset($definitions['help_block']);
+    // Explicitly remove the "Sticky at top of lists field_block".
+    $disallowed_fields = ['sticky'];
+    // Remove "Changed" field if this is the first section.
+    if ($extra['delta'] === 0) {
+      $disallowed_fields[] = 'changed';
+    }
+    foreach ($definitions as $plugin_id => $definition) {
+      // Field block IDs are in the form 'field_block:{entity}:{bundle}:{name}',
+      // for example 'field_block:node:article:revision_timestamp'.
+      preg_match('/field_block:.*:.*:(.*)/', $plugin_id, $parts);
+      if (isset($parts[1]) && in_array($parts[1], $disallowed_fields, TRUE)) {
+        // Unset any field blocks that match our predefined list.
+        unset($definitions[$plugin_id]);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_extra_field_info().
+   */
+  #[Hook('entity_extra_field_info')]
+  public function entityExtraFieldInfo() {
+    $extra['node']['bundle_with_section_field']['display']['layout_builder_test'] = [
+      'label' => t('Extra label'),
+      'description' => t('Extra description'),
+      'weight' => 0,
+    ];
+    $extra['node']['bundle_with_section_field']['display']['layout_builder_test_2'] = [
+      'label' => t('Extra Field 2'),
+      'description' => t('Extra Field 2 description'),
+      'weight' => 0,
+      'visible' => FALSE,
+    ];
+    return $extra;
+  }
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter() for layout_builder_configure_block.
+   */
+  #[Hook('form_layout_builder_configure_block_alter')]
+  public function formLayoutBuilderConfigureBlockAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    /** @var \Drupal\layout_builder\Form\ConfigureBlockFormBase $form_object */
+    $form_object = $form_state->getFormObject();
+    $form['layout_builder_test']['storage'] = [
+      '#type' => 'item',
+      '#title' => 'Layout Builder Storage: ' . $form_object->getSectionStorage()->getStorageId(),
+    ];
+    $form['layout_builder_test']['section'] = [
+      '#type' => 'item',
+      '#title' => 'Layout Builder Section: ' . $form_object->getCurrentSection()->getLayoutId(),
+    ];
+    $form['layout_builder_test']['component'] = [
+      '#type' => 'item',
+      '#title' => 'Layout Builder Component: ' . $form_object->getCurrentComponent()->getPluginId(),
+    ];
+  }
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter() for layout_builder_configure_section.
+   */
+  #[Hook('form_layout_builder_configure_section_alter')]
+  public function formLayoutBuilderConfigureSectionAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    /** @var \Drupal\layout_builder\Form\ConfigureSectionForm $form_object */
+    $form_object = $form_state->getFormObject();
+    $form['layout_builder_test']['storage'] = [
+      '#type' => 'item',
+      '#title' => 'Layout Builder Storage: ' . $form_object->getSectionStorage()->getStorageId(),
+    ];
+    $form['layout_builder_test']['section'] = [
+      '#type' => 'item',
+      '#title' => 'Layout Builder Section: ' . $form_object->getCurrentSection()->getLayoutId(),
+    ];
+    $form['layout_builder_test']['layout'] = [
+      '#type' => 'item',
+      '#title' => 'Layout Builder Layout: ' . $form_object->getCurrentLayout()->getPluginId(),
+    ];
+  }
+
+  /**
+   * Implements hook_entity_form_display_alter().
+   */
+  #[Hook('entity_form_display_alter', module: 'layout_builder')]
+  public function layoutBuilderEntityFormDisplayAlter(EntityFormDisplayInterface $form_display, array $context) {
+    if ($context['form_mode'] === 'layout_builder') {
+      $form_display->setComponent('status', ['type' => 'boolean_checkbox', 'settings' => ['display_label' => TRUE]]);
+    }
+  }
+
+  /**
+   * Implements hook_system_breadcrumb_alter().
+   */
+  #[Hook('system_breadcrumb_alter')]
+  public function systemBreadcrumbAlter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) {
+    $breadcrumb->addLink(Link::fromTextAndUrl('External link', Url::fromUri('http://www.example.com')));
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return ['block__preview_aware_block' => ['base hook' => 'block']];
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/layout_builder_theme_suggestions_test.module b/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/layout_builder_theme_suggestions_test.module
index 00cdc82c18f98c3896343a501ff5198118ca659e..80708a9cc8d35e7a11cc438017620fea7bc688b1 100644
--- a/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/layout_builder_theme_suggestions_test.module
+++ b/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/layout_builder_theme_suggestions_test.module
@@ -7,19 +7,6 @@
 
 declare(strict_types=1);
 
-/**
- * Implements hook_theme().
- */
-function layout_builder_theme_suggestions_test_theme(): array {
-  // It is necessary to explicitly register the template via hook_theme()
-  // because it is added via a module, not a theme.
-  return [
-    'field__node__body__bundle_with_section_field__default' => [
-      'base hook' => 'field',
-    ],
-  ];
-}
-
 /**
  * Implements hook_preprocess_HOOK() for the list of layouts.
  */
diff --git a/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/src/Hook/LayoutBuilderThemeSuggestionsTestHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/src/Hook/LayoutBuilderThemeSuggestionsTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..6f90fe6287038b10dc855bf74c1d934d79682730
--- /dev/null
+++ b/core/modules/layout_builder/tests/modules/layout_builder_theme_suggestions_test/src/Hook/LayoutBuilderThemeSuggestionsTestHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\layout_builder_theme_suggestions_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for layout_builder_theme_suggestions_test.
+ */
+class LayoutBuilderThemeSuggestionsTestHooks {
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    // It is necessary to explicitly register the template via hook_theme()
+    // because it is added via a module, not a theme.
+    return [
+      'field__node__body__bundle_with_section_field__default' => [
+        'base hook' => 'field',
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Kernel/EntityViewAlterTest.php b/core/modules/layout_builder/tests/src/Kernel/EntityViewAlterTest.php
index 48f829a30836fba5917a80e0765d2a3e89066d7a..ea166c6f1938243a36b5b39b3aec75f6bbd44cb0 100644
--- a/core/modules/layout_builder/tests/src/Kernel/EntityViewAlterTest.php
+++ b/core/modules/layout_builder/tests/src/Kernel/EntityViewAlterTest.php
@@ -12,6 +12,7 @@
 use Symfony\Component\HttpFoundation\Session\Session;
 use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
 use Symfony\Component\Routing\Route;
+use Drupal\layout_builder\Hook\LayoutBuilderHooks;
 
 /**
  * @covers layout_builder_entity_view_alter
@@ -57,7 +58,8 @@ public function testContextualLinksRemoved(): void {
     $request->setSession(new Session(new MockArraySessionStorage()));
     \Drupal::requestStack()->push($request);
     // Assert the contextual links are removed.
-    layout_builder_entity_view_alter($build, $entity, $display);
+    $layoutBuilderEntityViewAlter = new LayoutBuilderHooks();
+    $layoutBuilderEntityViewAlter->entityViewAlter($build, $entity, $display);
     $this->assertArrayNotHasKey('#contextual_links', $build);
   }
 
diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderBreadcrumbAlterTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderBreadcrumbAlterTest.php
index 8fc56da03228797fd7ed5de21b9887fefa91a754..5ab541a40be3a2ec8e3b3f840442ca3fb6501fba 100644
--- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderBreadcrumbAlterTest.php
+++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderBreadcrumbAlterTest.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Breadcrumb\Breadcrumb;
 use Drupal\Core\Routing\NullRouteMatch;
 use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+use Drupal\layout_builder\Hook\LayoutBuilderHooks;
 
 /**
  * Tests layout_builder_system_breadcrumb_alter().
@@ -29,7 +30,8 @@ class LayoutBuilderBreadcrumbAlterTest extends EntityKernelTestBase {
   public function testBreadcrumbAlterNullRouteMatch(): void {
     $breadcrumb = new Breadcrumb();
     $route_match = new NullRouteMatch();
-    layout_builder_system_breadcrumb_alter($breadcrumb, $route_match, []);
+    $layoutBuilderSystemBreadcrumbAlter = new LayoutBuilderHooks();
+    $layoutBuilderSystemBreadcrumbAlter->systemBreadcrumbAlter($breadcrumb, $route_match, []);
   }
 
 }
diff --git a/core/modules/layout_discovery/layout_discovery.module b/core/modules/layout_discovery/layout_discovery.module
index 9411244371d56f14a7700eca6362c9b798ba98ac..5db696c7c4a8148b821ee40007f929a953ecc211 100644
--- a/core/modules/layout_discovery/layout_discovery.module
+++ b/core/modules/layout_discovery/layout_discovery.module
@@ -2,32 +2,11 @@
 
 /**
  * @file
- * Provides hook implementations for Layout Discovery.
  */
 
 use Drupal\Core\Render\Element;
 use Drupal\Core\Template\Attribute;
 
-/**
- * Implements hook_help().
- */
-function layout_discovery_help($route_name) {
-  switch ($route_name) {
-    case 'help.page.layout_discovery':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('Layout Discovery allows modules or themes to register layouts, and for other modules to list the available layouts and render them.') . '</p>';
-      $output .= '<p>' . t('For more information, see the <a href=":layout-discovery-documentation">online documentation for the Layout Discovery module</a>.', [':layout-discovery-documentation' => 'https://www.drupal.org/docs/8/api/layout-api']) . '</p>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function layout_discovery_theme(): array {
-  return \Drupal::service('plugin.manager.core.layout')->getThemeImplementations();
-}
-
 /**
  * Prepares variables for layout templates.
  *
diff --git a/core/modules/layout_discovery/src/Hook/LayoutDiscoveryHooks.php b/core/modules/layout_discovery/src/Hook/LayoutDiscoveryHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..a9efe3b10d4b528bd3ea0ec73c2e61a81d75cc8c
--- /dev/null
+++ b/core/modules/layout_discovery/src/Hook/LayoutDiscoveryHooks.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\layout_discovery\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for layout_discovery.
+ */
+class LayoutDiscoveryHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name) {
+    switch ($route_name) {
+      case 'help.page.layout_discovery':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('Layout Discovery allows modules or themes to register layouts, and for other modules to list the available layouts and render them.') . '</p>';
+        $output .= '<p>' . t('For more information, see the <a href=":layout-discovery-documentation">online documentation for the Layout Discovery module</a>.', [
+          ':layout-discovery-documentation' => 'https://www.drupal.org/docs/8/api/layout-api',
+        ]) . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return \Drupal::service('plugin.manager.core.layout')->getThemeImplementations();
+  }
+
+}
diff --git a/core/modules/link/link.module b/core/modules/link/link.module
index 27bbaead95856f2c27bbca8beb994ed5ae714bd2..bf08aecb40b33a6326e86bda6b4b0093d32809c4 100644
--- a/core/modules/link/link.module
+++ b/core/modules/link/link.module
@@ -2,52 +2,9 @@
 
 /**
  * @file
- * Defines simple link field types.
  */
 
-use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
 use Drupal\Core\Link;
-use Drupal\Core\Url;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function link_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.link':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Link module allows you to create fields that contain internal or external URLs and optional link text. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":link_documentation">online documentation for the Link module</a>.', [':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':link_documentation' => 'https://www.drupal.org/documentation/modules/link']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Managing and displaying link fields') . '</dt>';
-      $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the link field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Setting the allowed link type') . '</dt>';
-      $output .= '<dd>' . t('In the field settings you can define the allowed link type to be <em>internal links only</em>, <em>external links only</em>, or <em>both internal and external links</em>. <em>Internal links only</em> and <em>both internal and external links</em> options enable an autocomplete widget for internal links, so a user does not have to copy or remember a URL.') . '</dd>';
-      $output .= '<dt>' . t('Adding link text') . '</dt>';
-      $output .= '<dd>' . t('In the field settings you can define additional link text to be <em>optional</em> or <em>required</em> in any link field.') . '</dd>';
-      $output .= '<dt>' . t('Displaying link text') . '</dt>';
-      $output .= '<dd>' . t('If link text has been submitted for a URL, then by default this link text is displayed as a link to the URL. If you want to display both the link text <em>and</em> the URL, choose the appropriate link format from the drop-down menu in the <em>Manage display</em> page. If you only want to display the URL even if link text has been submitted, choose <em>Link</em> as the format, and then change its <em>Format settings</em> to display <em>URL only</em>.') . '</dd>';
-      $output .= '<dt>' . t('Adding attributes to links') . '</dt>';
-      $output .= '<dd>' . t('You can add attributes to links, by changing the <em>Format settings</em> in the <em>Manage display</em> page. Adding <em>rel="nofollow"</em> notifies search engines that links should not be followed.') . '</dd>';
-      $output .= '<dt>' . t('Validating URLs') . '</dt>';
-      $output .= '<dd>' . t('All links are validated after a link field is filled in. They can include anchors or query strings.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function link_theme(): array {
-  return [
-    'link_formatter_link_separate' => [
-      'variables' => ['title' => NULL, 'url_title' => NULL, 'url' => NULL],
-    ],
-  ];
-}
 
 /**
  * Prepares variables for separated link field templates.
@@ -66,12 +23,3 @@ function link_theme(): array {
 function template_preprocess_link_formatter_link_separate(&$variables) {
   $variables['link'] = Link::fromTextAndUrl($variables['url_title'], $variables['url'])->toString();
 }
-
-/**
- * Implements hook_field_type_category_info_alter().
- */
-function link_field_type_category_info_alter(&$definitions) {
-  // The `link` field type belongs in the `general` category, so the libraries
-  // need to be attached using an alter hook.
-  $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'link/drupal.link-icon';
-}
diff --git a/core/modules/link/src/Hook/LinkHooks.php b/core/modules/link/src/Hook/LinkHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..412b291467e20a44e7e49a5d182db7cd8b89b5b0
--- /dev/null
+++ b/core/modules/link/src/Hook/LinkHooks.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Drupal\link\Hook;
+
+use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for link.
+ */
+class LinkHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.link':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Link module allows you to create fields that contain internal or external URLs and optional link text. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":link_documentation">online documentation for the Link module</a>.', [
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+          ':link_documentation' => 'https://www.drupal.org/documentation/modules/link',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Managing and displaying link fields') . '</dt>';
+        $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the link field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Setting the allowed link type') . '</dt>';
+        $output .= '<dd>' . t('In the field settings you can define the allowed link type to be <em>internal links only</em>, <em>external links only</em>, or <em>both internal and external links</em>. <em>Internal links only</em> and <em>both internal and external links</em> options enable an autocomplete widget for internal links, so a user does not have to copy or remember a URL.') . '</dd>';
+        $output .= '<dt>' . t('Adding link text') . '</dt>';
+        $output .= '<dd>' . t('In the field settings you can define additional link text to be <em>optional</em> or <em>required</em> in any link field.') . '</dd>';
+        $output .= '<dt>' . t('Displaying link text') . '</dt>';
+        $output .= '<dd>' . t('If link text has been submitted for a URL, then by default this link text is displayed as a link to the URL. If you want to display both the link text <em>and</em> the URL, choose the appropriate link format from the drop-down menu in the <em>Manage display</em> page. If you only want to display the URL even if link text has been submitted, choose <em>Link</em> as the format, and then change its <em>Format settings</em> to display <em>URL only</em>.') . '</dd>';
+        $output .= '<dt>' . t('Adding attributes to links') . '</dt>';
+        $output .= '<dd>' . t('You can add attributes to links, by changing the <em>Format settings</em> in the <em>Manage display</em> page. Adding <em>rel="nofollow"</em> notifies search engines that links should not be followed.') . '</dd>';
+        $output .= '<dt>' . t('Validating URLs') . '</dt>';
+        $output .= '<dd>' . t('All links are validated after a link field is filled in. They can include anchors or query strings.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'link_formatter_link_separate' => [
+        'variables' => [
+          'title' => NULL,
+          'url_title' => NULL,
+          'url' => NULL,
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_field_type_category_info_alter().
+   */
+  #[Hook('field_type_category_info_alter')]
+  public function fieldTypeCategoryInfoAlter(&$definitions) {
+    // The `link` field type belongs in the `general` category, so the libraries
+    // need to be attached using an alter hook.
+    $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'link/drupal.link-icon';
+  }
+
+}
diff --git a/core/modules/link/tests/modules/link_test_base_field/link_test_base_field.module b/core/modules/link/tests/modules/link_test_base_field/link_test_base_field.module
deleted file mode 100644
index 734e8bb9baa275acb03621c8ec475b36219c8ddf..0000000000000000000000000000000000000000
--- a/core/modules/link/tests/modules/link_test_base_field/link_test_base_field.module
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains main module functions.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\Core\Field\FieldStorageDefinitionInterface;
-use Drupal\link\LinkItemInterface;
-
-/**
- * Implements hook_entity_base_field_info().
- */
-function link_test_base_field_entity_base_field_info(EntityTypeInterface $entity_type) {
-  $fields = [];
-  if ($entity_type->id() === 'entity_test') {
-    $fields['links'] = BaseFieldDefinition::create('link')
-      ->setLabel(t('Links'))
-      ->setRevisionable(TRUE)
-      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
-      ->setDescription(t('Add links to the entity.'))
-      ->setRequired(FALSE)
-      ->setSettings([
-        'link_type' => LinkItemInterface::LINK_GENERIC,
-        'title' => DRUPAL_REQUIRED,
-      ])
-      ->setDisplayOptions('form', [
-        'type' => 'link_default',
-        'weight' => 49,
-      ]);
-  }
-  return $fields;
-}
diff --git a/core/modules/link/tests/modules/link_test_base_field/src/Hook/LinkTestBaseFieldHooks.php b/core/modules/link/tests/modules/link_test_base_field/src/Hook/LinkTestBaseFieldHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..07a890d8af6a30c4d6cd6ae6e2baadbb1e8e829e
--- /dev/null
+++ b/core/modules/link/tests/modules/link_test_base_field/src/Hook/LinkTestBaseFieldHooks.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\link_test_base_field\Hook;
+
+use Drupal\link\LinkItemInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for link_test_base_field.
+ */
+class LinkTestBaseFieldHooks {
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    $fields = [];
+    if ($entity_type->id() === 'entity_test') {
+      $fields['links'] = BaseFieldDefinition::create('link')->setLabel(t('Links'))->setRevisionable(TRUE)->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)->setDescription(t('Add links to the entity.'))->setRequired(FALSE)->setSettings([
+        'link_type' => LinkItemInterface::LINK_GENERIC,
+        'title' => DRUPAL_REQUIRED,
+      ])->setDisplayOptions('form', ['type' => 'link_default', 'weight' => 49]);
+    }
+    return $fields;
+  }
+
+}
diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc
index d69a1a743fb19c26030b5e7036c489b9f6c77321..408ba383fa8c3bd168ee7802d8affe62b4eaaa20 100644
--- a/core/modules/locale/locale.batch.inc
+++ b/core/modules/locale/locale.batch.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Batch process to check the availability of remote or local po files.
  */
 
 use Drupal\Core\File\Exception\FileException;
diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc
index 44696b0874a68ca3daa99fc9dbe9d2c995267cf2..a5873e91ee00c92c8f2329704ff4800327d4035e 100644
--- a/core/modules/locale/locale.bulk.inc
+++ b/core/modules/locale/locale.bulk.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Mass import-export and batch import functionality for Gettext .po files.
  */
 
 use Drupal\Core\Batch\BatchBuilder;
diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc
index 02f1fd87d6c9a3d0f03563a23829e4a86519b388..8c46c245a9f8f3560788db1257dae36eae796c91 100644
--- a/core/modules/locale/locale.compare.inc
+++ b/core/modules/locale/locale.compare.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * The API for comparing project translation status with available translation.
  */
 
 use Drupal\Core\Batch\BatchBuilder;
diff --git a/core/modules/locale/locale.fetch.inc b/core/modules/locale/locale.fetch.inc
index a6a9b36689abba9d3f1d0ec34b96ef139c024b48..d47a9f1f59da58345d26472ddd4f8a0f7b4112d9 100644
--- a/core/modules/locale/locale.fetch.inc
+++ b/core/modules/locale/locale.fetch.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * The API for download and import of translations from remote and local sources.
  */
 
 use Drupal\Core\Batch\BatchBuilder;
diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module
index 4fe78109b982b83ef4cae37f28c27486fcc2b762..b34432847c54c632b4342df4342fa3fbebbcf364 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -2,12 +2,6 @@
 
 /**
  * @file
- * Enables the translation of the user interface to languages other than English.
- *
- * When enabled, multiple languages can be added. The site interface can be
- * displayed in different languages, and nodes can have languages assigned. The
- * setup of languages and translations is completely web based. Gettext portable
- * object files are supported.
  */
 
 use Drupal\Component\Gettext\PoItem;
@@ -18,14 +12,9 @@
 use Drupal\Core\File\Exception\FileException;
 use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\Installer\InstallerKernel;
-use Drupal\Core\Link;
 use Drupal\Core\Site\Settings;
-use Drupal\Core\Url;
-use Drupal\Core\Asset\AttachedAssetsInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\language\ConfigurableLanguageInterface;
 use Drupal\Component\Utility\Crypt;
 use Drupal\locale\LocaleEvent;
 use Drupal\locale\LocaleEvents;
@@ -145,109 +134,6 @@
  */
 const LOCALE_TRANSLATION_CURRENT = 'current';
 
-/**
- * Implements hook_help().
- */
-function locale_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.locale':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Interface Translation module allows you to translate interface text (<em>strings</em>) into different languages, and to switch between them for the display of interface text. It uses the functionality provided by the <a href=":language">Language module</a>. For more information, see the <a href=":doc-url">online documentation for the Interface Translation module</a>.', [':doc-url' => 'https://www.drupal.org/documentation/modules/locale/', ':language' => Url::fromRoute('help.page', ['name' => 'language'])->toString()]) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Importing translation files') . '</dt>';
-      $output .= '<dd>' . t('Translation files with translated interface text are imported automatically when languages are added on the <a href=":languages">Languages</a> page, or when modules or themes are installed. On the <a href=":locale-settings">Interface translation settings</a> page, the <em>Translation source</em> can be restricted to local files only, or to include the <a href=":server">Drupal translation server</a>. Although modules and themes may not be fully translated in all languages, new translations become available frequently. You can specify whether and how often to check for translation file updates and whether to overwrite existing translations on the <a href=":locale-settings">Interface translation settings</a> page. You can also manually import a translation file on the <a href=":import">Interface translation import</a> page.', [':import' => Url::fromRoute('locale.translate_import')->toString(), ':locale-settings' => Url::fromRoute('locale.settings')->toString(), ':languages' => Url::fromRoute('entity.configurable_language.collection')->toString(), ':server' => 'https://localize.drupal.org']) . '</dd>';
-      $output .= '<dt>' . t('Checking the translation status') . '</dt>';
-      $output .= '<dd>' . t('You can check how much of the interface on your site is translated into which language on the <a href=":languages">Languages</a> page. On the <a href=":translation-updates">Available translation updates</a> page, you can check whether interface translation updates are available on the <a href=":server">Drupal translation server</a>.', [':languages' => Url::fromRoute('entity.configurable_language.collection')->toString(), ':translation-updates' => Url::fromRoute('locale.translate_status')->toString(), ':server' => 'https://localize.drupal.org']) . '<dd>';
-      $output .= '<dt>' . t('Translating individual strings') . '</dt>';
-      $output .= '<dd>' . t('You can translate individual strings directly on the <a href=":translate">User interface translation</a> page, or download the currently-used translation file for a specific language on the <a href=":export">Interface translation export</a> page. Once you have edited the translation file, you can then import it again on the <a href=":import">Interface translation import</a> page.', [':translate' => Url::fromRoute('locale.translate_page')->toString(), ':export' => Url::fromRoute('locale.translate_export')->toString(), ':import' => Url::fromRoute('locale.translate_import')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Overriding default English strings') . '</dt>';
-      $output .= '<dd>' . t('If translation is enabled for English, you can <em>override</em> the default English interface text strings in your site with other English text strings on the <a href=":translate">User interface translation</a> page. Translation is off by default for English, but you can turn it on by visiting the <em>Edit language</em> page for <em>English</em> from the <a href=":languages">Languages</a> page.', [':translate' => Url::fromRoute('locale.translate_page')->toString(), ':languages' => Url::fromRoute('entity.configurable_language.collection')->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'entity.configurable_language.collection':
-      return '<p>' . t('Interface translations are automatically imported when a language is added, or when new modules or themes are installed. The report <a href=":update">Available translation updates</a> shows the status. Interface text can be customized in the <a href=":translate">user interface translation</a> page.', [':update' => Url::fromRoute('locale.translate_status')->toString(), ':translate' => Url::fromRoute('locale.translate_page')->toString()]) . '</p>';
-
-    case 'locale.translate_page':
-      $output = '<p>' . t('This page allows a translator to search for specific translated and untranslated strings, and is used when creating or editing translations. (Note: Because translation tasks involve many strings, it may be more convenient to <a title="User interface translation export" href=":export">export</a> strings for offline editing in a desktop Gettext translation editor.) Searches may be limited to strings in a specific language.', [':export' => Url::fromRoute('locale.translate_export')->toString()]) . '</p>';
-      return $output;
-
-    case 'locale.translate_import':
-      $output = '<p>' . t('Translation files are automatically downloaded and imported when <a title="Languages" href=":language">languages</a> are added, or when modules or themes are installed.', [':language' => Url::fromRoute('entity.configurable_language.collection')->toString()]) . '</p>';
-      $output .= '<p>' . t('This page allows translators to manually import translated strings contained in a Gettext Portable Object (.po) file. Manual import may be used for customized translations or for the translation of custom modules and themes. To customize translations you can download a translation file from the <a href=":url">Drupal translation server</a> or <a title="User interface translation export" href=":export">export</a> translations from the site, customize the translations using a Gettext translation editor, and import the result using this page.', [':url' => 'https://localize.drupal.org', ':export' => Url::fromRoute('locale.translate_export')->toString()]) . '</p>';
-      $output .= '<p>' . t('Note that importing large .po files may take several minutes.') . '</p>';
-      return $output;
-
-    case 'locale.translate_export':
-      return '<p>' . t('This page exports the translated strings used by your site. An export file may be in Gettext Portable Object (<em>.po</em>) form, which includes both the original string and the translation (used to share translations with others), or in Gettext Portable Object Template (<em>.pot</em>) form, which includes the original strings only (used to create new translations with a Gettext translation editor).') . '</p>';
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function locale_theme(): array {
-  return [
-    'locale_translation_last_check' => [
-      'variables' => ['last' => NULL],
-      'file' => 'locale.pages.inc',
-    ],
-    'locale_translation_update_info' => [
-      'variables' => ['updates' => [], 'not_found' => []],
-      'file' => 'locale.pages.inc',
-    ],
-  ];
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for 'configurable_language'.
- */
-function locale_configurable_language_insert(ConfigurableLanguageInterface $language) {
-  // @todo move these two cache clears out. See
-  //   https://www.drupal.org/node/1293252.
-  // Changing the language settings impacts the interface: clear render cache.
-  \Drupal::cache('render')->deleteAll();
-  // Force JavaScript translation file re-creation for the new language.
-  _locale_invalidate_js($language->id());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for 'configurable_language'.
- */
-function locale_configurable_language_update(ConfigurableLanguageInterface $language) {
-  // @todo move these two cache clears out. See
-  //   https://www.drupal.org/node/1293252.
-  // Changing the language settings impacts the interface: clear render cache.
-  \Drupal::cache('render')->deleteAll();
-  // Force JavaScript translation file re-creation for the modified language.
-  _locale_invalidate_js($language->id());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for 'configurable_language'.
- */
-function locale_configurable_language_delete(ConfigurableLanguageInterface $language) {
-  // Remove translations.
-  \Drupal::service('locale.storage')->deleteTranslations(['language' => $language->id()]);
-
-  // Remove interface translation files.
-  \Drupal::moduleHandler()->loadInclude('locale', 'inc', 'locale.bulk');
-  locale_translate_delete_translation_files([], [$language->id()]);
-
-  // Remove translated configuration objects.
-  \Drupal::service('locale.config_manager')->deleteLanguageTranslations($language->id());
-
-  // Changing the language settings impacts the interface:
-  _locale_invalidate_js($language->id());
-  \Drupal::cache('render')->deleteAll();
-
-  // Clear locale translation caches.
-  locale_translation_status_delete_languages([$language->id()]);
-  \Drupal::cache()->delete('locale:' . $language->id());
-}
-
 /**
  * Returns list of translatable languages.
  *
@@ -315,53 +201,6 @@ function locale_get_plural($count, $langcode = NULL) {
   return $plural_indexes[$langcode][$count];
 }
 
-/**
- * Implements hook_modules_installed().
- */
-function locale_modules_installed($modules) {
-  $components['module'] = $modules;
-  locale_system_update($components);
-}
-
-/**
- * Implements hook_module_preuninstall().
- */
-function locale_module_preuninstall($module) {
-  $components['module'] = [$module];
-  locale_system_remove($components);
-}
-
-/**
- * Implements hook_themes_installed().
- */
-function locale_themes_installed($themes) {
-  $components['theme'] = $themes;
-  locale_system_update($components);
-}
-
-/**
- * Implements hook_themes_uninstalled().
- */
-function locale_themes_uninstalled($themes) {
-  $components['theme'] = $themes;
-  locale_system_remove($components);
-}
-
-/**
- * Implements hook_cron().
- *
- * @see \Drupal\locale\Plugin\QueueWorker\LocaleTranslation
- */
-function locale_cron() {
-  // Update translations only when an update frequency was set by the admin
-  // and a translatable language was set.
-  // Update tasks are added to the queue here but processed by Drupal's cron.
-  if (\Drupal::config('locale.settings')->get('translation.update_interval_days') && locale_translatable_language_list()) {
-    \Drupal::moduleHandler()->loadInclude('locale', 'inc', 'locale.translation');
-    locale_cron_fill_queue();
-  }
-}
-
 /**
  * Updates default configuration when new modules or themes are installed.
  */
@@ -457,45 +296,6 @@ function locale_system_remove($components) {
   }
 }
 
-/**
- * Implements hook_cache_flush().
- */
-function locale_cache_flush() {
-  \Drupal::state()->delete('system.javascript_parsed');
-}
-
-/**
- * Implements hook_js_alter().
- */
-function locale_js_alter(&$javascript, AttachedAssetsInterface $assets, LanguageInterface $language) {
-  $files = [];
-  foreach ($javascript as $item) {
-    if (isset($item['type']) && $item['type'] == 'file') {
-      // Ignore the JS translation placeholder file.
-      if ($item['data'] === 'core/modules/locale/locale.translation.js') {
-        continue;
-      }
-      $files[] = $item['data'];
-    }
-  }
-
-  // Replace the placeholder file with the actual JS translation file.
-  $placeholder_file = 'core/modules/locale/locale.translation.js';
-  if (isset($javascript[$placeholder_file])) {
-    if ($translation_file = locale_js_translate($files, $language)) {
-      $js_translation_asset = &$javascript[$placeholder_file];
-      $js_translation_asset['data'] = $translation_file;
-      // @todo Remove this when https://www.drupal.org/node/1945262 lands.
-      // Decrease the weight so that the translation file is loaded first.
-      $js_translation_asset['weight'] = $javascript['core/misc/drupal.js']['weight'] - 0.001;
-    }
-    else {
-      // If no translation file exists, then remove the placeholder JS asset.
-      unset($javascript[$placeholder_file]);
-    }
-  }
-}
-
 /**
  * Returns a list of translation files given a list of JavaScript files.
  *
@@ -566,80 +366,6 @@ function locale_js_translate(array $files = [], $language_interface = NULL) {
   return $translation_file;
 }
 
-/**
- * Implements hook_library_info_alter().
- *
- * Provides language support.
- */
-function locale_library_info_alter(array &$libraries, $module) {
-  // When the locale module is enabled, we update the core/drupal library to
-  // have a dependency on the locale/translations library, which provides
-  // window.drupalTranslations, containing the translations for all strings in
-  // JavaScript assets in the current language.
-  // @see locale_js_alter()
-  if ($module === 'core' && isset($libraries['drupal'])) {
-    $libraries['drupal']['dependencies'][] = 'locale/translations';
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for language_admin_overview_form().
- */
-function locale_form_language_admin_overview_form_alter(&$form, FormStateInterface $form_state): void {
-  $languages = $form['languages']['#languages'];
-
-  $total_strings = \Drupal::service('locale.storage')->countStrings();
-  $stats = array_fill_keys(array_keys($languages), []);
-
-  // If we have source strings, count translations and calculate progress.
-  if (!empty($total_strings)) {
-    $translations = \Drupal::service('locale.storage')->countTranslations();
-    foreach ($translations as $langcode => $translated) {
-      $stats[$langcode]['translated'] = $translated;
-      if ($translated > 0) {
-        $stats[$langcode]['ratio'] = round($translated / $total_strings * 100, 2);
-      }
-    }
-  }
-
-  array_splice($form['languages']['#header'], -1, 0, ['translation-interface' => t('Interface translation')]);
-
-  foreach ($languages as $langcode => $language) {
-    $stats[$langcode] += [
-      'translated' => 0,
-      'ratio' => 0,
-    ];
-    if (!$language->isLocked() && locale_is_translatable($langcode)) {
-      $form['languages'][$langcode]['locale_statistics'] = Link::fromTextAndUrl(
-        t('@translated/@total (@ratio%)', [
-          '@translated' => $stats[$langcode]['translated'],
-          '@total' => $total_strings,
-          '@ratio' => $stats[$langcode]['ratio'],
-        ]),
-        Url::fromRoute('locale.translate_page', [], ['query' => ['langcode' => $langcode]])
-      )->toRenderable();
-    }
-    else {
-      $form['languages'][$langcode]['locale_statistics'] = [
-        '#markup' => t('not applicable'),
-      ];
-    }
-    // #type = link doesn't work with #weight on table.
-    // reset and set it back after locale_statistics to get it at the right end.
-    $operations = $form['languages'][$langcode]['operations'];
-    unset($form['languages'][$langcode]['operations']);
-    $form['languages'][$langcode]['operations'] = $operations;
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for language_admin_add_form().
- */
-function locale_form_language_admin_add_form_alter(&$form, FormStateInterface $form_state): void {
-  $form['predefined_submit']['#submit'][] = 'locale_form_language_admin_add_form_alter_submit';
-  $form['custom_language']['submit']['#submit'][] = 'locale_form_language_admin_add_form_alter_submit';
-}
-
 /**
  * Form submission handler for language_admin_add_form().
  *
@@ -673,20 +399,6 @@ function locale_form_language_admin_add_form_alter_submit($form, FormStateInterf
   }
 }
 
-/**
- * Implements hook_form_FORM_ID_alter() for language_admin_edit_form().
- */
-function locale_form_language_admin_edit_form_alter(&$form, FormStateInterface $form_state): void {
-  if ($form['langcode']['#type'] == 'value' && $form['langcode']['#value'] == 'en') {
-    $form['locale_translate_english'] = [
-      '#title' => t('Enable interface translation to English'),
-      '#type' => 'checkbox',
-      '#default_value' => \Drupal::configFactory()->getEditable('locale.settings')->get('translate_english'),
-    ];
-    $form['actions']['submit']['#submit'][] = 'locale_form_language_admin_edit_form_alter_submit';
-  }
-}
-
 /**
  * Form submission handler for language_admin_edit_form().
  */
@@ -707,28 +419,6 @@ function locale_is_translatable($langcode) {
   return $langcode != 'en' || \Drupal::config('locale.settings')->get('translate_english');
 }
 
-/**
- * Implements hook_form_FORM_ID_alter() for system_file_system_settings().
- *
- * Add interface translation directory setting to directories configuration.
- */
-function locale_form_system_file_system_settings_alter(&$form, FormStateInterface $form_state): void {
-  $form['translation_path'] = [
-    '#type' => 'textfield',
-    '#title' => t('Interface translations directory'),
-    '#default_value' => \Drupal::configFactory()->getEditable('locale.settings')->get('translation.path'),
-    '#maxlength' => 255,
-    '#description' => t('A local file system path where interface translation files will be stored.'),
-    '#required' => TRUE,
-    '#after_build' => ['system_check_directory'],
-    '#weight' => 10,
-  ];
-  if ($form['file_default_scheme']) {
-    $form['file_default_scheme']['#weight'] = 20;
-  }
-  $form['#submit'][] = 'locale_system_file_system_settings_submit';
-}
-
 /**
  * Submit handler for the file system settings form.
  *
diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc
index 578f066b1b2d7691559ecc153387537c1949cd75..c5ac2e5ec3d71f8b151ca82049b25165f27ed050 100644
--- a/core/modules/locale/locale.pages.inc
+++ b/core/modules/locale/locale.pages.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Interface translation summary, editing and deletion user interfaces.
  */
 
 use Drupal\Core\Link;
diff --git a/core/modules/locale/locale.translation.inc b/core/modules/locale/locale.translation.inc
index 44bf5c6661260f92d527b0d4311fb5deefde5e0e..b0c577c14850bf257fa8f5c04fc15320ed2167ee 100644
--- a/core/modules/locale/locale.translation.inc
+++ b/core/modules/locale/locale.translation.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Common API for interface translation.
  */
 
 use Drupal\Core\StreamWrapper\StreamWrapperManager;
diff --git a/core/modules/locale/src/Hook/LocaleHooks.php b/core/modules/locale/src/Hook/LocaleHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..0dbde7eeef5de0fbe27d302d0c323a4282a22d9d
--- /dev/null
+++ b/core/modules/locale/src/Hook/LocaleHooks.php
@@ -0,0 +1,354 @@
+<?php
+
+namespace Drupal\locale\Hook;
+
+use Drupal\Core\Link;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\language\ConfigurableLanguageInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for locale.
+ */
+class LocaleHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.locale':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Interface Translation module allows you to translate interface text (<em>strings</em>) into different languages, and to switch between them for the display of interface text. It uses the functionality provided by the <a href=":language">Language module</a>. For more information, see the <a href=":doc-url">online documentation for the Interface Translation module</a>.', [
+          ':doc-url' => 'https://www.drupal.org/documentation/modules/locale/',
+          ':language' => Url::fromRoute('help.page', [
+            'name' => 'language',
+          ])->toString(),
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Importing translation files') . '</dt>';
+        $output .= '<dd>' . t('Translation files with translated interface text are imported automatically when languages are added on the <a href=":languages">Languages</a> page, or when modules or themes are installed. On the <a href=":locale-settings">Interface translation settings</a> page, the <em>Translation source</em> can be restricted to local files only, or to include the <a href=":server">Drupal translation server</a>. Although modules and themes may not be fully translated in all languages, new translations become available frequently. You can specify whether and how often to check for translation file updates and whether to overwrite existing translations on the <a href=":locale-settings">Interface translation settings</a> page. You can also manually import a translation file on the <a href=":import">Interface translation import</a> page.', [
+          ':import' => Url::fromRoute('locale.translate_import')->toString(),
+          ':locale-settings' => Url::fromRoute('locale.settings')->toString(),
+          ':languages' => Url::fromRoute('entity.configurable_language.collection')->toString(),
+          ':server' => 'https://localize.drupal.org',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Checking the translation status') . '</dt>';
+        $output .= '<dd>' . t('You can check how much of the interface on your site is translated into which language on the <a href=":languages">Languages</a> page. On the <a href=":translation-updates">Available translation updates</a> page, you can check whether interface translation updates are available on the <a href=":server">Drupal translation server</a>.', [
+          ':languages' => Url::fromRoute('entity.configurable_language.collection')->toString(),
+          ':translation-updates' => Url::fromRoute('locale.translate_status')->toString(),
+          ':server' => 'https://localize.drupal.org',
+        ]) . '<dd>';
+        $output .= '<dt>' . t('Translating individual strings') . '</dt>';
+        $output .= '<dd>' . t('You can translate individual strings directly on the <a href=":translate">User interface translation</a> page, or download the currently-used translation file for a specific language on the <a href=":export">Interface translation export</a> page. Once you have edited the translation file, you can then import it again on the <a href=":import">Interface translation import</a> page.', [
+          ':translate' => Url::fromRoute('locale.translate_page')->toString(),
+          ':export' => Url::fromRoute('locale.translate_export')->toString(),
+          ':import' => Url::fromRoute('locale.translate_import')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Overriding default English strings') . '</dt>';
+        $output .= '<dd>' . t('If translation is enabled for English, you can <em>override</em> the default English interface text strings in your site with other English text strings on the <a href=":translate">User interface translation</a> page. Translation is off by default for English, but you can turn it on by visiting the <em>Edit language</em> page for <em>English</em> from the <a href=":languages">Languages</a> page.', [
+          ':translate' => Url::fromRoute('locale.translate_page')->toString(),
+          ':languages' => Url::fromRoute('entity.configurable_language.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'entity.configurable_language.collection':
+        return '<p>' . t('Interface translations are automatically imported when a language is added, or when new modules or themes are installed. The report <a href=":update">Available translation updates</a> shows the status. Interface text can be customized in the <a href=":translate">user interface translation</a> page.', [
+          ':update' => Url::fromRoute('locale.translate_status')->toString(),
+          ':translate' => Url::fromRoute('locale.translate_page')->toString(),
+        ]) . '</p>';
+
+      case 'locale.translate_page':
+        $output = '<p>' . t('This page allows a translator to search for specific translated and untranslated strings, and is used when creating or editing translations. (Note: Because translation tasks involve many strings, it may be more convenient to <a title="User interface translation export" href=":export">export</a> strings for offline editing in a desktop Gettext translation editor.) Searches may be limited to strings in a specific language.', [':export' => Url::fromRoute('locale.translate_export')->toString()]) . '</p>';
+        return $output;
+
+      case 'locale.translate_import':
+        $output = '<p>' . t('Translation files are automatically downloaded and imported when <a title="Languages" href=":language">languages</a> are added, or when modules or themes are installed.', [
+          ':language' => Url::fromRoute('entity.configurable_language.collection')->toString(),
+        ]) . '</p>';
+        $output .= '<p>' . t('This page allows translators to manually import translated strings contained in a Gettext Portable Object (.po) file. Manual import may be used for customized translations or for the translation of custom modules and themes. To customize translations you can download a translation file from the <a href=":url">Drupal translation server</a> or <a title="User interface translation export" href=":export">export</a> translations from the site, customize the translations using a Gettext translation editor, and import the result using this page.', [
+          ':url' => 'https://localize.drupal.org',
+          ':export' => Url::fromRoute('locale.translate_export')->toString(),
+        ]) . '</p>';
+        $output .= '<p>' . t('Note that importing large .po files may take several minutes.') . '</p>';
+        return $output;
+
+      case 'locale.translate_export':
+        return '<p>' . t('This page exports the translated strings used by your site. An export file may be in Gettext Portable Object (<em>.po</em>) form, which includes both the original string and the translation (used to share translations with others), or in Gettext Portable Object Template (<em>.pot</em>) form, which includes the original strings only (used to create new translations with a Gettext translation editor).') . '</p>';
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'locale_translation_last_check' => [
+        'variables' => [
+          'last' => NULL,
+        ],
+        'file' => 'locale.pages.inc',
+      ],
+      'locale_translation_update_info' => [
+        'variables' => [
+          'updates' => [],
+          'not_found' => [],
+        ],
+        'file' => 'locale.pages.inc',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for 'configurable_language'.
+   */
+  #[Hook('configurable_language_insert')]
+  public function configurableLanguageInsert(ConfigurableLanguageInterface $language) {
+    // @todo move these two cache clears out. See
+    //   https://www.drupal.org/node/1293252.
+    // Changing the language settings impacts the interface: clear render cache.
+    \Drupal::cache('render')->deleteAll();
+    // Force JavaScript translation file re-creation for the new language.
+    _locale_invalidate_js($language->id());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for 'configurable_language'.
+   */
+  #[Hook('configurable_language_update')]
+  public function configurableLanguageUpdate(ConfigurableLanguageInterface $language) {
+    // @todo move these two cache clears out. See
+    //   https://www.drupal.org/node/1293252.
+    // Changing the language settings impacts the interface: clear render cache.
+    \Drupal::cache('render')->deleteAll();
+    // Force JavaScript translation file re-creation for the modified language.
+    _locale_invalidate_js($language->id());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for 'configurable_language'.
+   */
+  #[Hook('configurable_language_delete')]
+  public function configurableLanguageDelete(ConfigurableLanguageInterface $language) {
+    // Remove translations.
+    \Drupal::service('locale.storage')->deleteTranslations(['language' => $language->id()]);
+    // Remove interface translation files.
+    \Drupal::moduleHandler()->loadInclude('locale', 'inc', 'locale.bulk');
+    locale_translate_delete_translation_files([], [$language->id()]);
+    // Remove translated configuration objects.
+    \Drupal::service('locale.config_manager')->deleteLanguageTranslations($language->id());
+    // Changing the language settings impacts the interface:
+    _locale_invalidate_js($language->id());
+    \Drupal::cache('render')->deleteAll();
+    // Clear locale translation caches.
+    locale_translation_status_delete_languages([$language->id()]);
+    \Drupal::cache()->delete('locale:' . $language->id());
+  }
+
+  /**
+   * Implements hook_modules_installed().
+   */
+  #[Hook('modules_installed')]
+  public function modulesInstalled($modules) {
+    $components['module'] = $modules;
+    locale_system_update($components);
+  }
+
+  /**
+   * Implements hook_module_preuninstall().
+   */
+  #[Hook('module_preuninstall')]
+  public function modulePreuninstall($module) {
+    $components['module'] = [$module];
+    locale_system_remove($components);
+  }
+
+  /**
+   * Implements hook_themes_installed().
+   */
+  #[Hook('themes_installed')]
+  public function themesInstalled($themes) {
+    $components['theme'] = $themes;
+    locale_system_update($components);
+  }
+
+  /**
+   * Implements hook_themes_uninstalled().
+   */
+  #[Hook('themes_uninstalled')]
+  public function themesUninstalled($themes) {
+    $components['theme'] = $themes;
+    locale_system_remove($components);
+  }
+
+  /**
+   * Implements hook_cron().
+   *
+   * @see \Drupal\locale\Plugin\QueueWorker\LocaleTranslation
+   */
+  #[Hook('cron')]
+  public function cron() {
+    // Update translations only when an update frequency was set by the admin
+    // and a translatable language was set.
+    // Update tasks are added to the queue here but processed by Drupal's cron.
+    if (\Drupal::config('locale.settings')->get('translation.update_interval_days') && locale_translatable_language_list()) {
+      \Drupal::moduleHandler()->loadInclude('locale', 'inc', 'locale.translation');
+      locale_cron_fill_queue();
+    }
+  }
+
+  /**
+   * Implements hook_cache_flush().
+   */
+  #[Hook('cache_flush')]
+  public function cacheFlush() {
+    \Drupal::state()->delete('system.javascript_parsed');
+  }
+
+  /**
+   * Implements hook_js_alter().
+   */
+  #[Hook('js_alter')]
+  public function jsAlter(&$javascript, AttachedAssetsInterface $assets, LanguageInterface $language) {
+    $files = [];
+    foreach ($javascript as $item) {
+      if (isset($item['type']) && $item['type'] == 'file') {
+        // Ignore the JS translation placeholder file.
+        if ($item['data'] === 'core/modules/locale/locale.translation.js') {
+          continue;
+        }
+        $files[] = $item['data'];
+      }
+    }
+    // Replace the placeholder file with the actual JS translation file.
+    $placeholder_file = 'core/modules/locale/locale.translation.js';
+    if (isset($javascript[$placeholder_file])) {
+      if ($translation_file = locale_js_translate($files, $language)) {
+        $js_translation_asset =& $javascript[$placeholder_file];
+        $js_translation_asset['data'] = $translation_file;
+        // @todo Remove this when https://www.drupal.org/node/1945262 lands.
+        // Decrease the weight so that the translation file is loaded first.
+        $js_translation_asset['weight'] = $javascript['core/misc/drupal.js']['weight'] - 0.001;
+      }
+      else {
+        // If no translation file exists, then remove the placeholder JS asset.
+        unset($javascript[$placeholder_file]);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_library_info_alter().
+   *
+   * Provides language support.
+   */
+  #[Hook('library_info_alter')]
+  public function libraryInfoAlter(array &$libraries, $module) {
+    // When the locale module is enabled, we update the core/drupal library to
+    // have a dependency on the locale/translations library, which provides
+    // window.drupalTranslations, containing the translations for all strings in
+    // JavaScript assets in the current language.
+    // @see locale_js_alter()
+    if ($module === 'core' && isset($libraries['drupal'])) {
+      $libraries['drupal']['dependencies'][] = 'locale/translations';
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for language_admin_overview_form().
+   */
+  #[Hook('form_language_admin_overview_form_alter')]
+  public function formLanguageAdminOverviewFormAlter(&$form, FormStateInterface $form_state) : void {
+    $languages = $form['languages']['#languages'];
+    $total_strings = \Drupal::service('locale.storage')->countStrings();
+    $stats = array_fill_keys(array_keys($languages), []);
+    // If we have source strings, count translations and calculate progress.
+    if (!empty($total_strings)) {
+      $translations = \Drupal::service('locale.storage')->countTranslations();
+      foreach ($translations as $langcode => $translated) {
+        $stats[$langcode]['translated'] = $translated;
+        if ($translated > 0) {
+          $stats[$langcode]['ratio'] = round($translated / $total_strings * 100, 2);
+        }
+      }
+    }
+    array_splice($form['languages']['#header'], -1, 0, ['translation-interface' => t('Interface translation')]);
+    foreach ($languages as $langcode => $language) {
+      $stats[$langcode] += ['translated' => 0, 'ratio' => 0];
+      if (!$language->isLocked() && locale_is_translatable($langcode)) {
+        $form['languages'][$langcode]['locale_statistics'] = Link::fromTextAndUrl(t('@translated/@total (@ratio%)', [
+          '@translated' => $stats[$langcode]['translated'],
+          '@total' => $total_strings,
+          '@ratio' => $stats[$langcode]['ratio'],
+        ]), Url::fromRoute('locale.translate_page', [], ['query' => ['langcode' => $langcode]]))->toRenderable();
+      }
+      else {
+        $form['languages'][$langcode]['locale_statistics'] = ['#markup' => t('not applicable')];
+      }
+      // #type = link doesn't work with #weight on table.
+      // reset and set it back after locale_statistics to get it at the right end.
+      $operations = $form['languages'][$langcode]['operations'];
+      unset($form['languages'][$langcode]['operations']);
+      $form['languages'][$langcode]['operations'] = $operations;
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for language_admin_add_form().
+   */
+  #[Hook('form_language_admin_add_form_alter')]
+  public function formLanguageAdminAddFormAlter(&$form, FormStateInterface $form_state) : void {
+    $form['predefined_submit']['#submit'][] = 'locale_form_language_admin_add_form_alter_submit';
+    $form['custom_language']['submit']['#submit'][] = 'locale_form_language_admin_add_form_alter_submit';
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for language_admin_edit_form().
+   */
+  #[Hook('form_language_admin_edit_form_alter')]
+  public function formLanguageAdminEditFormAlter(&$form, FormStateInterface $form_state) : void {
+    if ($form['langcode']['#type'] == 'value' && $form['langcode']['#value'] == 'en') {
+      $form['locale_translate_english'] = [
+        '#title' => t('Enable interface translation to English'),
+        '#type' => 'checkbox',
+        '#default_value' => \Drupal::configFactory()->getEditable('locale.settings')->get('translate_english'),
+      ];
+      $form['actions']['submit']['#submit'][] = 'locale_form_language_admin_edit_form_alter_submit';
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for system_file_system_settings().
+   *
+   * Add interface translation directory setting to directories configuration.
+   */
+  #[Hook('form_system_file_system_settings_alter')]
+  public function formSystemFileSystemSettingsAlter(&$form, FormStateInterface $form_state) : void {
+    $form['translation_path'] = [
+      '#type' => 'textfield',
+      '#title' => t('Interface translations directory'),
+      '#default_value' => \Drupal::configFactory()->getEditable('locale.settings')->get('translation.path'),
+      '#maxlength' => 255,
+      '#description' => t('A local file system path where interface translation files will be stored.'),
+      '#required' => TRUE,
+      '#after_build' => [
+        'system_check_directory',
+      ],
+      '#weight' => 10,
+    ];
+    if ($form['file_default_scheme']) {
+      $form['file_default_scheme']['#weight'] = 20;
+    }
+    $form['#submit'][] = 'locale_system_file_system_settings_submit';
+  }
+
+}
diff --git a/core/modules/locale/tests/modules/locale_test/locale_test.module b/core/modules/locale/tests/modules/locale_test/locale_test.module
deleted file mode 100644
index d35a2db70e1e43461f15814e2f2e8ea16619c649..0000000000000000000000000000000000000000
--- a/core/modules/locale/tests/modules/locale_test/locale_test.module
+++ /dev/null
@@ -1,225 +0,0 @@
-<?php
-
-/**
- * @file
- * Simulate a custom module with a local po file.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Url;
-use Drupal\Core\Extension\Extension;
-use Drupal\Core\StreamWrapper\PublicStream;
-
-/**
- * Implements hook_system_info_alter().
- *
- * Make the test scripts to be believe this is not a hidden test module, but
- * a regular custom module.
- */
-function locale_test_system_info_alter(&$info, Extension $file, $type) {
-  // Only modify the system info if required.
-  // By default the locale_test modules are hidden and have a project specified.
-  // To test the module detection process by locale_project_list() the
-  // test modules should mimic a custom module. I.e. be non-hidden.
-  if (\Drupal::state()->get('locale.test_system_info_alter')) {
-    if ($file->getName() == 'locale_test' || $file->getName() == 'locale_test_translate') {
-      // Don't hide the module.
-      $info['hidden'] = FALSE;
-    }
-  }
-
-  // Alter the name and the core version of the project. This should not affect
-  // the locale project information.
-  if (\Drupal::state()->get('locale.test_system_info_alter_name_core')) {
-    if ($file->getName() == 'locale_test') {
-      $info['core'] = '8.6.7';
-      $info['name'] = 'locale_test_alter';
-    }
-  }
-}
-
-/**
- * Implements hook_locale_translation_projects_alter().
- *
- * The translation status process by default checks the status of the installed
- * projects. This function replaces the data of the installed modules by a
- * predefined set of modules with fixed file names and release versions. Project
- * names, versions, timestamps etc must be fixed because they must match the
- * files created by the test script.
- *
- * The "locale.test_projects_alter" state variable must be set by the
- * test script in order for this hook to take effect.
- */
-function locale_test_locale_translation_projects_alter(&$projects) {
-  // Drupal core should not be translated. By overriding the server pattern we
-  // make sure that no translation for drupal core will be found and that the
-  // translation update system will not go out to l.d.o to check.
-  $projects['drupal']['server_pattern'] = 'translations://';
-
-  if (\Drupal::state()->get('locale.remove_core_project')) {
-    unset($projects['drupal']);
-  }
-
-  if (\Drupal::state()->get('locale.test_projects_alter')) {
-
-    // Instead of the default ftp.drupal.org we use the file system of the test
-    // instance to simulate a remote file location.
-    $url = Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString();
-    $remote_url = $url . PublicStream::basePath() . '/remote/';
-
-    // Completely replace the project data with a set of test projects.
-    $projects = [
-      'contrib_module_one' => [
-        'name' => 'contrib_module_one',
-        'info' => [
-          'name' => 'Contributed module one',
-          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
-          'package' => 'Other',
-          'version' => '8.x-1.1',
-          'project' => 'contrib_module_one',
-          'datestamp' => '1344471537',
-          '_info_file_ctime' => 1348767306,
-        ],
-        'datestamp' => '1344471537',
-        'project_type' => 'module',
-        'project_status' => TRUE,
-      ],
-      'contrib_module_two' => [
-        'name' => 'contrib_module_two',
-        'info' => [
-          'name' => 'Contributed module two',
-          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
-          'package' => 'Other',
-          'version' => '8.x-2.0-beta4',
-          'project' => 'contrib_module_two',
-          'datestamp' => '1344471537',
-          '_info_file_ctime' => 1348767306,
-        ],
-        'datestamp' => '1344471537',
-        'project_type' => 'module',
-        'project_status' => TRUE,
-      ],
-      'contrib_module_three' => [
-        'name' => 'contrib_module_three',
-        'info' => [
-          'name' => 'Contributed module three',
-          'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
-          'package' => 'Other',
-          'version' => '8.x-1.0',
-          'project' => 'contrib_module_three',
-          'datestamp' => '1344471537',
-          '_info_file_ctime' => 1348767306,
-        ],
-        'datestamp' => '1344471537',
-        'project_type' => 'module',
-        'project_status' => TRUE,
-      ],
-      'locale_test' => [
-        'name' => 'locale_test',
-        'info' => [
-          'name' => 'Locale test',
-          'interface translation project' => 'locale_test',
-          'interface translation server pattern' => 'core/modules/locale/tests/test.%language.po',
-          'package' => 'Other',
-          'version' => NULL,
-          'project' => 'locale_test',
-          '_info_file_ctime' => 1348767306,
-          'datestamp' => 0,
-        ],
-        'datestamp' => 0,
-        'project_type' => 'module',
-        'project_status' => TRUE,
-      ],
-      'custom_module_one' => [
-        'name' => 'custom_module_one',
-        'info' => [
-          'name' => 'Custom module one',
-          'interface translation project' => 'custom_module_one',
-          'interface translation server pattern' => 'translations://custom_module_one.%language.po',
-          'package' => 'Other',
-          'version' => NULL,
-          'project' => 'custom_module_one',
-          '_info_file_ctime' => 1348767306,
-          'datestamp' => 0,
-        ],
-        'datestamp' => 0,
-        'project_type' => 'module',
-        'project_status' => TRUE,
-      ],
-    ];
-  }
-}
-
-/**
- * Implements hook_language_fallback_candidates_OPERATION_alter().
- */
-function locale_test_language_fallback_candidates_locale_lookup_alter(array &$candidates, array $context) {
-  \Drupal::state()->set('locale.test_language_fallback_candidates_locale_lookup_alter_candidates', $candidates);
-  \Drupal::state()->set('locale.test_language_fallback_candidates_locale_lookup_alter_context', $context);
-}
-
-/**
- * Implements hook_theme().
- */
-function locale_test_theme(): array {
-  $return = [];
-
-  $return['locale_test_tokenized'] = [
-    'variable' => ['content' => ''],
-  ];
-
-  return $return;
-}
-
-/**
- * Implements hook_token_info().
- */
-function locale_test_token_info() {
-  $info = [];
-
-  $info['types']['locale_test'] = [
-    'name' => t('Locale test'),
-    'description' => t('Locale test'),
-  ];
-
-  $info['tokens']['locale_test']['security_test1'] = [
-    'type' => 'text',
-    'name' => t('Security test 1'),
-  ];
-  $info['tokens']['locale_test']['security_test2'] = [
-    'type' => 'text',
-    'name' => t('Security test 2'),
-  ];
-
-  return $info;
-}
-
-/**
- * Implements hook_tokens().
- */
-function locale_test_tokens($type, $tokens, array $data = [], array $options = []) {
-  $return = [];
-  if ($type == 'locale_test') {
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'security_test1':
-          $return[$original] = "javascript:alert('Hello!');";
-          break;
-
-        case 'security_test2':
-          $return[$original] = "<script>alert('Hello!');</script>";
-          break;
-      }
-    }
-  }
-
-  return $return;
-}
-
-/**
- * Implements hook_countries_alter().
- */
-function locale_test_countries_alter(&$countries) {
-  $countries['EB'] = 'Elbonia';
-}
diff --git a/core/modules/locale/tests/modules/locale_test/src/Hook/LocaleTestHooks.php b/core/modules/locale/tests/modules/locale_test/src/Hook/LocaleTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..7765470e711e3d496344708dfb6a3a6707061e20
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test/src/Hook/LocaleTestHooks.php
@@ -0,0 +1,215 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\locale_test\Hook;
+
+use Drupal\Core\StreamWrapper\PublicStream;
+use Drupal\Core\Url;
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for locale_test.
+ */
+class LocaleTestHooks {
+
+  /**
+   * Implements hook_system_info_alter().
+   *
+   * Make the test scripts to be believe this is not a hidden test module, but
+   * a regular custom module.
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(&$info, Extension $file, $type) {
+    // Only modify the system info if required.
+    // By default the locale_test modules are hidden and have a project specified.
+    // To test the module detection process by locale_project_list() the
+    // test modules should mimic a custom module. I.e. be non-hidden.
+    if (\Drupal::state()->get('locale.test_system_info_alter')) {
+      if ($file->getName() == 'locale_test' || $file->getName() == 'locale_test_translate') {
+        // Don't hide the module.
+        $info['hidden'] = FALSE;
+      }
+    }
+    // Alter the name and the core version of the project. This should not affect
+    // the locale project information.
+    if (\Drupal::state()->get('locale.test_system_info_alter_name_core')) {
+      if ($file->getName() == 'locale_test') {
+        $info['core'] = '8.6.7';
+        $info['name'] = 'locale_test_alter';
+      }
+    }
+  }
+
+  /**
+   * Implements hook_locale_translation_projects_alter().
+   *
+   * The translation status process by default checks the status of the installed
+   * projects. This function replaces the data of the installed modules by a
+   * predefined set of modules with fixed file names and release versions. Project
+   * names, versions, timestamps etc must be fixed because they must match the
+   * files created by the test script.
+   *
+   * The "locale.test_projects_alter" state variable must be set by the
+   * test script in order for this hook to take effect.
+   */
+  #[Hook('locale_translation_projects_alter')]
+  public function localeTranslationProjectsAlter(&$projects) {
+    // Drupal core should not be translated. By overriding the server pattern we
+    // make sure that no translation for drupal core will be found and that the
+    // translation update system will not go out to l.d.o to check.
+    $projects['drupal']['server_pattern'] = 'translations://';
+    if (\Drupal::state()->get('locale.remove_core_project')) {
+      unset($projects['drupal']);
+    }
+    if (\Drupal::state()->get('locale.test_projects_alter')) {
+      // Instead of the default ftp.drupal.org we use the file system of the test
+      // instance to simulate a remote file location.
+      $url = Url::fromRoute('<front>', [], ['absolute' => TRUE])->toString();
+      $remote_url = $url . PublicStream::basePath() . '/remote/';
+      // Completely replace the project data with a set of test projects.
+      $projects = [
+        'contrib_module_one' => [
+          'name' => 'contrib_module_one',
+          'info' => [
+            'name' => 'Contributed module one',
+            'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
+            'package' => 'Other',
+            'version' => '8.x-1.1',
+            'project' => 'contrib_module_one',
+            'datestamp' => '1344471537',
+            '_info_file_ctime' => 1348767306,
+          ],
+          'datestamp' => '1344471537',
+          'project_type' => 'module',
+          'project_status' => TRUE,
+        ],
+        'contrib_module_two' => [
+          'name' => 'contrib_module_two',
+          'info' => [
+            'name' => 'Contributed module two',
+            'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
+            'package' => 'Other',
+            'version' => '8.x-2.0-beta4',
+            'project' => 'contrib_module_two',
+            'datestamp' => '1344471537',
+            '_info_file_ctime' => 1348767306,
+          ],
+          'datestamp' => '1344471537',
+          'project_type' => 'module',
+          'project_status' => TRUE,
+        ],
+        'contrib_module_three' => [
+          'name' => 'contrib_module_three',
+          'info' => [
+            'name' => 'Contributed module three',
+            'interface translation server pattern' => $remote_url . '%core/%project/%project-%version.%language._po',
+            'package' => 'Other',
+            'version' => '8.x-1.0',
+            'project' => 'contrib_module_three',
+            'datestamp' => '1344471537',
+            '_info_file_ctime' => 1348767306,
+          ],
+          'datestamp' => '1344471537',
+          'project_type' => 'module',
+          'project_status' => TRUE,
+        ],
+        'locale_test' => [
+          'name' => 'locale_test',
+          'info' => [
+            'name' => 'Locale test',
+            'interface translation project' => 'locale_test',
+            'interface translation server pattern' => 'core/modules/locale/tests/test.%language.po',
+            'package' => 'Other',
+            'version' => NULL,
+            'project' => 'locale_test',
+            '_info_file_ctime' => 1348767306,
+            'datestamp' => 0,
+          ],
+          'datestamp' => 0,
+          'project_type' => 'module',
+          'project_status' => TRUE,
+        ],
+        'custom_module_one' => [
+          'name' => 'custom_module_one',
+          'info' => [
+            'name' => 'Custom module one',
+            'interface translation project' => 'custom_module_one',
+            'interface translation server pattern' => 'translations://custom_module_one.%language.po',
+            'package' => 'Other',
+            'version' => NULL,
+            'project' => 'custom_module_one',
+            '_info_file_ctime' => 1348767306,
+            'datestamp' => 0,
+          ],
+          'datestamp' => 0,
+          'project_type' => 'module',
+          'project_status' => TRUE,
+        ],
+      ];
+    }
+  }
+
+  /**
+   * Implements hook_language_fallback_candidates_OPERATION_alter().
+   */
+  #[Hook('language_fallback_candidates_locale_lookup_alter')]
+  public function languageFallbackCandidatesLocaleLookupAlter(array &$candidates, array $context) {
+    \Drupal::state()->set('locale.test_language_fallback_candidates_locale_lookup_alter_candidates', $candidates);
+    \Drupal::state()->set('locale.test_language_fallback_candidates_locale_lookup_alter_context', $context);
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    $return = [];
+    $return['locale_test_tokenized'] = ['variable' => ['content' => '']];
+    return $return;
+  }
+
+  /**
+   * Implements hook_token_info().
+   */
+  #[Hook('token_info')]
+  public function tokenInfo() {
+    $info = [];
+    $info['types']['locale_test'] = ['name' => t('Locale test'), 'description' => t('Locale test')];
+    $info['tokens']['locale_test']['security_test1'] = ['type' => 'text', 'name' => t('Security test 1')];
+    $info['tokens']['locale_test']['security_test2'] = ['type' => 'text', 'name' => t('Security test 2')];
+    return $info;
+  }
+
+  /**
+   * Implements hook_tokens().
+   */
+  #[Hook('tokens')]
+  public function tokens($type, $tokens, array $data = [], array $options = []) {
+    $return = [];
+    if ($type == 'locale_test') {
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          case 'security_test1':
+            $return[$original] = "javascript:alert('Hello!');";
+            break;
+
+          case 'security_test2':
+            $return[$original] = "<script>alert('Hello!');</script>";
+            break;
+        }
+      }
+    }
+    return $return;
+  }
+
+  /**
+   * Implements hook_countries_alter().
+   */
+  #[Hook('countries_alter')]
+  public function countriesAlter(&$countries) {
+    $countries['EB'] = 'Elbonia';
+  }
+
+}
diff --git a/core/modules/locale/tests/modules/locale_test_development_release/locale_test_development_release.module b/core/modules/locale/tests/modules/locale_test_development_release/locale_test_development_release.module
deleted file mode 100644
index f0f526611c000ae4d07278595b3e21f04bb53940..0000000000000000000000000000000000000000
--- a/core/modules/locale/tests/modules/locale_test_development_release/locale_test_development_release.module
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-/**
- * @file
- * Simulate a Drupal version.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Extension\Extension;
-
-/**
- * Implements hook_system_info_alter().
- *
- * Change the core version number to a development one for testing.
- * 8.0.0-alpha102-dev is the simulated version.
- */
-function locale_test_development_release_system_info_alter(&$info, Extension $file, $type) {
-  if (isset($info['package']) && $info['package'] == 'Core') {
-    $info['version'] = '8.0.0-alpha102-dev';
-  }
-}
-
-/**
- * Implements hook_locale_translation_projects_alter().
- *
- * Add a contrib module with a dev release to list of translatable modules.
- */
-function locale_test_development_release_locale_translation_projects_alter(&$projects) {
-  $projects['contrib'] = [
-    'name' => 'contrib',
-    'info' => [
-      'name' => 'Contributed module',
-      'package' => 'Other',
-      'version' => '12.x-10.4-unstable11+14-dev',
-      'project' => 'contrib_module',
-      'datestamp' => '0',
-      '_info_file_ctime' => 1442933959,
-    ],
-    'datestamp' => '0',
-    'project_type' => 'module',
-    'project_status' => TRUE,
-  ];
-}
diff --git a/core/modules/locale/tests/modules/locale_test_development_release/src/Hook/LocaleTestDevelopmentReleaseHooks.php b/core/modules/locale/tests/modules/locale_test_development_release/src/Hook/LocaleTestDevelopmentReleaseHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..3ebf6fed0e596e5194a06eb4fce98096f9611f9a
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_development_release/src/Hook/LocaleTestDevelopmentReleaseHooks.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\locale_test_development_release\Hook;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for locale_test_development_release.
+ */
+class LocaleTestDevelopmentReleaseHooks {
+
+  /**
+   * Implements hook_system_info_alter().
+   *
+   * Change the core version number to a development one for testing.
+   * 8.0.0-alpha102-dev is the simulated version.
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(&$info, Extension $file, $type) {
+    if (isset($info['package']) && $info['package'] == 'Core') {
+      $info['version'] = '8.0.0-alpha102-dev';
+    }
+  }
+
+  /**
+   * Implements hook_locale_translation_projects_alter().
+   *
+   * Add a contrib module with a dev release to list of translatable modules.
+   */
+  #[Hook('locale_translation_projects_alter')]
+  public function localeTranslationProjectsAlter(&$projects) {
+    $projects['contrib'] = [
+      'name' => 'contrib',
+      'info' => [
+        'name' => 'Contributed module',
+        'package' => 'Other',
+        'version' => '12.x-10.4-unstable11+14-dev',
+        'project' => 'contrib_module',
+        'datestamp' => '0',
+        '_info_file_ctime' => 1442933959,
+      ],
+      'datestamp' => '0',
+      'project_type' => 'module',
+      'project_status' => TRUE,
+    ];
+  }
+
+}
diff --git a/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.module b/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.module
deleted file mode 100644
index 7c9789018ca6d176694bfdc81dc5ae4eb8776253..0000000000000000000000000000000000000000
--- a/core/modules/locale/tests/modules/locale_test_translate/locale_test_translate.module
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-/**
- * @file
- * Simulates a custom module with a local po file.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Extension\Extension;
-
-/**
- * Implements hook_system_info_alter().
- *
- * By default this modules is hidden but once enabled it behaves like a normal
- * (not hidden) module. This hook implementation changes the .info.yml data by
- * setting the hidden status to FALSE.
- */
-function locale_test_translate_system_info_alter(&$info, Extension $file, $type) {
-  if ($file->getName() == 'locale_test_translate') {
-    // Don't hide the module.
-    $info['hidden'] = FALSE;
-  }
-}
-
-/**
- * Implements hook_modules_installed().
- *
- * @see \Drupal\Tests\locale\Functional\LocaleConfigTranslationImportTest::testConfigTranslationWithForeignLanguageDefault
- */
-function locale_test_translate_modules_installed($modules, $is_syncing) {
-  // Ensure that writing to configuration during install does not cause
-  // \Drupal\locale\LocaleConfigSubscriber to create incorrect translations due
-  // the configuration langcode and data being out-of-sync.
-  \Drupal::configFactory()->getEditable('locale_test_translate.settings')->set('key_set_during_install', TRUE)->save();
-}
diff --git a/core/modules/locale/tests/modules/locale_test_translate/src/Hook/LocaleTestTranslateHooks.php b/core/modules/locale/tests/modules/locale_test_translate/src/Hook/LocaleTestTranslateHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..9490e47b0624d91d336abc42422ce52711443a37
--- /dev/null
+++ b/core/modules/locale/tests/modules/locale_test_translate/src/Hook/LocaleTestTranslateHooks.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\locale_test_translate\Hook;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for locale_test_translate.
+ */
+class LocaleTestTranslateHooks {
+
+  /**
+   * Implements hook_system_info_alter().
+   *
+   * By default this modules is hidden but once enabled it behaves like a normal
+   * (not hidden) module. This hook implementation changes the .info.yml data by
+   * setting the hidden status to FALSE.
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(&$info, Extension $file, $type) {
+    if ($file->getName() == 'locale_test_translate') {
+      // Don't hide the module.
+      $info['hidden'] = FALSE;
+    }
+  }
+
+  /**
+   * Implements hook_modules_installed().
+   *
+   * @see \Drupal\Tests\locale\Functional\LocaleConfigTranslationImportTest::testConfigTranslationWithForeignLanguageDefault
+   */
+  #[Hook('modules_installed')]
+  public function modulesInstalled($modules, $is_syncing) {
+    // Ensure that writing to configuration during install does not cause
+    // \Drupal\locale\LocaleConfigSubscriber to create incorrect translations due
+    // the configuration langcode and data being out-of-sync.
+    \Drupal::configFactory()->getEditable('locale_test_translate.settings')->set('key_set_during_install', TRUE)->save();
+  }
+
+}
diff --git a/core/modules/locale/tests/src/Functional/LocaleUpdateCronTest.php b/core/modules/locale/tests/src/Functional/LocaleUpdateCronTest.php
index 3d3d6ceb6af0a75faada57a325627956f9083377..f0ab40d31cb074d59e20cb2efafcf936043c02b4 100644
--- a/core/modules/locale/tests/src/Functional/LocaleUpdateCronTest.php
+++ b/core/modules/locale/tests/src/Functional/LocaleUpdateCronTest.php
@@ -6,6 +6,7 @@
 
 use Drupal\Core\Database\Database;
 use Drupal\Tests\Traits\Core\CronRunTrait;
+use Drupal\locale\Hook\LocaleHooks;
 
 /**
  * Tests for using cron to update project interface translations.
@@ -79,7 +80,8 @@ public function testUpdateCron(): void {
     $this->submitForm($edit, 'Save configuration');
 
     // Execute locale cron tasks to add tasks to the queue.
-    locale_cron();
+    $localeCron = new LocaleHooks();
+    $localeCron->cron();
 
     // Check whether no tasks are added to the queue.
     $queue = \Drupal::queue('locale_translation', TRUE);
@@ -95,7 +97,7 @@ public function testUpdateCron(): void {
     $this->submitForm($edit, 'Save configuration');
 
     // Execute locale cron tasks to add tasks to the queue.
-    locale_cron();
+    $localeCron->cron();
 
     // Check whether tasks are added to the queue.
     // Expected tasks:
@@ -110,7 +112,7 @@ public function testUpdateCron(): void {
 
     // Test: Run cron for a second time and check if tasks are not added to
     // the queue twice.
-    locale_cron();
+    $localeCron->cron();
 
     // Check whether no more tasks are added to the queue.
     $queue = \Drupal::queue('locale_translation', TRUE);
diff --git a/core/modules/media/media.api.php b/core/modules/media/media.api.php
index 93244f58a8a11b0450bdeec7b8df18a1e9152c2f..1a17005961349e7094a480c52a031412882f7168 100644
--- a/core/modules/media/media.api.php
+++ b/core/modules/media/media.api.php
@@ -1,5 +1,11 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\media\OEmbed\Provider;
+
 /**
  * @file
  * Hooks related to Media and its plugins.
@@ -30,7 +36,7 @@ function hook_media_source_info_alter(array &$sources) {
  *
  * @see \Drupal\media\OEmbed\UrlResolverInterface::getResourceUrl()
  */
-function hook_oembed_resource_url_alter(array &$parsed_url, \Drupal\media\OEmbed\Provider $provider) {
+function hook_oembed_resource_url_alter(array &$parsed_url, Provider $provider) {
   // Always serve YouTube videos from youtube-nocookie.com.
   if ($provider->getName() === 'YouTube') {
     $parsed_url['path'] = str_replace('://youtube.com/', '://youtube-nocookie.com/', $parsed_url['path']);
diff --git a/core/modules/media/media.module b/core/modules/media/media.module
index 1bd372bdd71844527069005998b972c1b29c7d66..d10dd8601955af31fe14492f916c70bb32190b53 100644
--- a/core/modules/media/media.module
+++ b/core/modules/media/media.module
@@ -2,107 +2,15 @@
 
 /**
  * @file
- * Provides media items.
  */
 
 use Drupal\Component\Plugin\DerivativeInspectionInterface;
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\Element\RenderElementBase;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Url;
-use Drupal\field\FieldConfigInterface;
 use Drupal\media\Plugin\media\Source\OEmbedInterface;
-use Drupal\views\ViewExecutable;
-
-/**
- * Implements hook_help().
- */
-function media_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.media':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Media module manages the creation, editing, deletion, settings, and display of media. Items are typically images, documents, slideshows, YouTube videos, tweets, Instagram photos, etc. You can reference media items from any other content on your site. For more information, see the <a href=":media">online documentation for the Media module</a>.', [':media' => 'https://www.drupal.org/docs/8/core/modules/media']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Creating media items') . '</dt>';
-      $output .= '<dd>' . t('When a new media item is created, the Media module records basic information about it, including the author, date of creation, and the <a href=":media-type">media type</a>. It also manages the <em>publishing options</em>, which define whether or not the item is published. Default settings can be configured for each type of media on your site.', [':media-type' => Url::fromRoute('entity.media_type.collection')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Listing media items') . '</dt>';
-      $output .= '<dd>' . t('Media items are listed at the <a href=":media-collection">media administration page</a>.', [
-        ':media-collection' => Url::fromRoute('entity.media.collection')->toString(),
-      ]) . '</dd>';
-      $output .= '<dt>' . t('Creating custom media types') . '</dt>';
-      $output .= '<dd>' . t('The Media module gives users with the <em>Administer media types</em> permission the ability to <a href=":media-new">create new media types</a> in addition to the default ones already configured. Each media type has an associated media source (such as the image source) which support thumbnail generation and metadata extraction. Fields managed by the <a href=":field">Field module</a> may be added for storing that metadata, such as width and height, as well as any other associated values.', [
-        ':media-new' => Url::fromRoute('entity.media_type.add_form')->toString(),
-        ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(),
-      ]) . '</dd>';
-      $output .= '<dt>' . t('Creating revisions') . '</dt>';
-      $output .= '<dd>' . t('The Media module also enables you to create multiple versions of any media item, and revert to older versions using the <em>Revision information</em> settings.') . '</dd>';
-      $output .= '<dt>' . t('User permissions') . '</dt>';
-      $output .= '<dd>' . t('The Media module makes a number of permissions available, which can be set by role on the <a href=":permissions">permissions page</a>.', [
-        ':permissions' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'media'])->toString(),
-      ]) . '</dd>';
-      $output .= '<dt>' . t('Adding media to other content') . '</dt>';
-      $output .= '<dd>' . t('Users with permission to administer content types can add media support by adding a media reference field to the content type on the content type administration page. (The same is true of block types, taxonomy terms, user profiles, and other content that supports fields.) A media reference field can refer to any configured media type. It is possible to allow multiple media types in the same field.') . '</dd>';
-      $output .= '</dl>';
-      $output .= '<h2>' . t('Differences between Media, File, and Image reference fields') . '</h2>';
-      $output .= '<p>' . t('<em>Media</em> reference fields offer several advantages over basic <em>File</em> and <em>Image</em> references:') . '</p>';
-      $output .= '<ul>';
-      $output .= '<li>' . t('Media reference fields can reference multiple media types in the same field.') . '</li>';
-      $output .= '<li>' . t('Fields can also be added to media types themselves, which means that custom metadata like descriptions and taxonomy tags can be added for the referenced media. (Basic file and image fields do not support this.)') . '</li>';
-      $output .= '<li>' . t('Media types for audio and video files are provided by default, so there is no need for additional configuration to upload these media.') . '</li>';
-      $output .= '<li>' . t('Contributed or custom projects can provide additional media sources (such as third-party websites, Twitter, etc.).') . '</li>';
-      $output .= '<li>' . t('Existing media items can be reused on any other content items with a media reference field.') . '</li>';
-      $output .= '</ul>';
-      $output .= '<p>' . t('Use <em>Media</em> reference fields for most files, images, audio, videos, and remote media. Use <em>File</em> or <em>Image</em> reference fields when creating your own media types, or for legacy files and images created before installing the Media module.') . '</p>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function media_theme(): array {
-  return [
-    'media' => [
-      'render element' => 'elements',
-    ],
-    'media_reference_help' => [
-      'render element' => 'element',
-      'base hook' => 'field_multiple_value_form',
-    ],
-    'media_oembed_iframe' => [
-      'variables' => [
-        'resource' => NULL,
-        'media' => NULL,
-        'placeholder_token' => '',
-      ],
-    ],
-    'media_embed_error' => [
-      'variables' => [
-        'message' => NULL,
-        'attributes' => [],
-      ],
-    ],
-  ];
-}
-
-/**
- * Implements hook_entity_access().
- */
-function media_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  if ($operation === 'delete' && $entity instanceof FieldConfigInterface && $entity->getTargetEntityTypeId() === 'media') {
-    /** @var \Drupal\media\MediaTypeInterface $media_type */
-    $media_type = \Drupal::entityTypeManager()->getStorage('media_type')->load($entity->getTargetBundle());
-    return AccessResult::forbiddenIf($entity->id() === 'media.' . $media_type->id() . '.' . $media_type->getSource()->getConfiguration()['source_field']);
-  }
-  return AccessResult::neutral();
-}
 
 /**
  * Implements hook_theme_suggestions_HOOK().
@@ -168,135 +76,6 @@ function template_preprocess_media(array &$variables) {
   }
 }
 
-/**
- * Implements hook_field_ui_preconfigured_options_alter().
- */
-function media_field_ui_preconfigured_options_alter(array &$options, $field_type) {
-  // If the field is not an "entity_reference"-based field, bail out.
-  /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
-  $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
-  $class = $field_type_manager->getPluginClass($field_type);
-  if (!is_a($class, 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', TRUE)) {
-    return;
-  }
-
-  // Set the default formatter for media in entity reference fields to be the
-  // "Rendered entity" formatter.
-  if (!empty($options['media'])) {
-    $options['media']['description'] = t('Field to reference media. Allows uploading and selecting from uploaded media.');
-    $options['media']['weight'] = -25;
-    $options['media']['category'] = FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY;
-    $options['media']['entity_view_display']['type'] = 'entity_reference_entity_view';
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function media_form_field_ui_field_storage_add_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  // Provide some help text to aid users decide whether they need a Media,
-  // File, or Image reference field.
-  $description_text = t('Use <em>Media</em> reference fields for most files, images, audio, videos, and remote media. Use <em>File</em> or <em>Image</em> reference fields when creating your own media types, or for legacy files and images created before installing the Media module.');
-  if (\Drupal::moduleHandler()->moduleExists('help')) {
-    $description_text .= ' ' . t('For more information, see the <a href="@help_url">Media help page</a>.', [
-      '@help_url' => Url::fromRoute('help.page', ['name' => 'media'])->toString(),
-    ]);
-  }
-  $field_types = [
-    'file_upload',
-    'field_ui:entity_reference:media',
-  ];
-  if (in_array($form_state->getValue('new_storage_type'), $field_types)) {
-    $form['group_field_options_wrapper']['description_wrapper'] = [
-      '#type' => 'item',
-      '#markup' => $description_text,
-    ];
-  }
-}
-
-/**
- * Implements hook_field_widget_complete_form_alter().
- */
-function media_field_widget_complete_form_alter(array &$field_widget_complete_form, FormStateInterface $form_state, array $context) {
-  $elements = &$field_widget_complete_form['widget'];
-  // Do not alter the default settings form.
-  if ($context['default']) {
-    return;
-  }
-
-  // Only act on entity reference fields that reference media.
-  $field_type = $context['items']->getFieldDefinition()->getType();
-  $target_type = $context['items']->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type');
-  if ($field_type !== 'entity_reference' ||  $target_type !== 'media') {
-    return;
-  }
-
-  // Autocomplete widgets need different help text than options widgets.
-  $widget_plugin_id = $context['widget']->getPluginId();
-  if (in_array($widget_plugin_id, ['entity_reference_autocomplete', 'entity_reference_autocomplete_tags'])) {
-    $is_autocomplete = TRUE;
-  }
-  else {
-    // @todo We can't yet properly alter non-autocomplete fields. Resolve this
-    //   in https://www.drupal.org/node/2943020 and remove this condition.
-    return;
-  }
-  $elements['#media_help'] = [];
-
-  // Retrieve the media bundle list and add information for the user based on
-  // which bundles are available to be created or referenced.
-  $settings = $context['items']->getFieldDefinition()->getSetting('handler_settings');
-  $allowed_bundles = !empty($settings['target_bundles']) ? $settings['target_bundles'] : [];
-  $add_url = _media_get_add_url($allowed_bundles);
-  if ($add_url) {
-    $elements['#media_help']['#media_add_help'] = t('Create your media on the <a href=":add_page" target="_blank">media add page</a> (opens a new window), then add it by name to the field below.', [':add_page' => $add_url]);
-  }
-
-  $elements['#theme'] = 'media_reference_help';
-  // @todo template_preprocess_field_multiple_value_form() assumes this key
-  //   exists, but it does not exist in the case of a single widget that
-  //   accepts multiple values. This is for some reason necessary to use
-  //   our template for the entity_autocomplete_tags widget.
-  //   Research and resolve this in https://www.drupal.org/node/2943020.
-  if (empty($elements['#cardinality_multiple'])) {
-    $elements['#cardinality_multiple'] = NULL;
-  }
-
-  // Use the title set on the element if it exists, otherwise fall back to the
-  // field label.
-  $elements['#media_help']['#original_label'] = $elements['#title'] ?? $context['items']->getFieldDefinition()->getLabel();
-
-  // Customize the label for the field widget.
-  // @todo Research a better approach https://www.drupal.org/node/2943024.
-  $use_existing_label = t('Use existing media');
-  if (!empty($elements[0]['target_id']['#title'])) {
-    $elements[0]['target_id']['#title'] = $use_existing_label;
-  }
-  if (!empty($elements['#title'])) {
-    $elements['#title'] = $use_existing_label;
-  }
-  if (!empty($elements['target_id']['#title'])) {
-    $elements['target_id']['#title'] = $use_existing_label;
-  }
-
-  // This help text is only relevant for autocomplete widgets. When the user
-  // is presented with options, they don't need to type anything or know what
-  // types of media are allowed.
-  if ($is_autocomplete) {
-    $elements['#media_help']['#media_list_help'] = t('Type part of the media name.');
-
-    $overview_url = Url::fromRoute('entity.media.collection');
-    if ($overview_url->access()) {
-      $elements['#media_help']['#media_list_link'] = t('See the <a href=":list_url" target="_blank">media list</a> (opens a new window) to help locate media.', [':list_url' => $overview_url->toString()]);
-    }
-    $all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media');
-    $bundle_labels = array_map(function ($bundle) use ($all_bundles) {
-      return $all_bundles[$bundle]['label'];
-    }, $allowed_bundles);
-    $elements['#media_help']['#allowed_types_help'] = t('Allowed media types: %types', ['%types' => implode(", ", $bundle_labels)]);
-  }
-}
-
 /**
  * Implements hook_preprocess_HOOK() for media reference widgets.
  */
@@ -351,35 +130,6 @@ function _media_get_add_url($allowed_bundles) {
   return FALSE;
 }
 
-/**
- * Implements hook_entity_type_alter().
- */
-function media_entity_type_alter(array &$entity_types): void {
-  if (\Drupal::config('media.settings')->get('standalone_url')) {
-    /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
-    $entity_type = $entity_types['media'];
-    $entity_type->setLinkTemplate('canonical', '/media/{media}');
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function media_form_filter_format_edit_form_alter(array &$form, FormStateInterface $form_state, $form_id): void {
-  // Add an additional validate callback so we can ensure the order of filters
-  // is correct.
-  $form['#validate'][] = 'media_filter_format_edit_form_validate';
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function media_form_filter_format_add_form_alter(array &$form, FormStateInterface $form_state, $form_id): void {
-  // Add an additional validate callback so we can ensure the order of filters
-  // is correct.
-  $form['#validate'][] = 'media_filter_format_edit_form_validate';
-}
-
 /**
  * Validate callback to ensure filter order and allowed_html are compatible.
  */
@@ -498,34 +248,3 @@ function media_filter_format_edit_form_validate($form, FormStateInterface $form_
     $form_state->setErrorByName('filters', $error_message);
   }
 }
-
-/**
- * Implements hook_field_widget_single_element_form_alter().
- */
-function media_field_widget_single_element_form_alter(&$element, FormStateInterface $form_state, $context) {
-  // Add an attribute so that text editors plugins can pass the host entity's
-  // language, allowing it to present entities in the same language.
-  if (!empty($element['#type']) && $element['#type'] == 'text_format') {
-    $element['#attributes']['data-media-embed-host-entity-langcode'] = $context['items']->getLangcode();
-  }
-}
-
-/**
- * Implements hook_views_query_substitutions().
- */
-function media_views_query_substitutions(ViewExecutable $view) {
-  $account = \Drupal::currentUser();
-  return [
-    '***VIEW_OWN_UNPUBLISHED_MEDIA***' => (int) $account->hasPermission('view own unpublished media'),
-    '***ADMINISTER_MEDIA***' => (int) $account->hasPermission('administer media'),
-  ];
-}
-
-/**
- * Implements hook_field_type_category_info_alter().
- */
-function media_field_type_category_info_alter(&$definitions) {
-  // The `media` field type belongs in the `general` category, so the libraries
-  // need to be attached using an alter hook.
-  $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'media/drupal.media-icon';
-}
diff --git a/core/modules/media/src/Hook/MediaHooks.php b/core/modules/media/src/Hook/MediaHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..b39aa8aa294a49dbb9f0dddfe852cf88af57ec9f
--- /dev/null
+++ b/core/modules/media/src/Hook/MediaHooks.php
@@ -0,0 +1,299 @@
+<?php
+
+namespace Drupal\media\Hook;
+
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\field\FieldConfigInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for media.
+ */
+class MediaHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.media':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Media module manages the creation, editing, deletion, settings, and display of media. Items are typically images, documents, slideshows, YouTube videos, tweets, Instagram photos, etc. You can reference media items from any other content on your site. For more information, see the <a href=":media">online documentation for the Media module</a>.', [':media' => 'https://www.drupal.org/docs/8/core/modules/media']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Creating media items') . '</dt>';
+        $output .= '<dd>' . t('When a new media item is created, the Media module records basic information about it, including the author, date of creation, and the <a href=":media-type">media type</a>. It also manages the <em>publishing options</em>, which define whether or not the item is published. Default settings can be configured for each type of media on your site.', [
+          ':media-type' => Url::fromRoute('entity.media_type.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Listing media items') . '</dt>';
+        $output .= '<dd>' . t('Media items are listed at the <a href=":media-collection">media administration page</a>.', [
+          ':media-collection' => Url::fromRoute('entity.media.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Creating custom media types') . '</dt>';
+        $output .= '<dd>' . t('The Media module gives users with the <em>Administer media types</em> permission the ability to <a href=":media-new">create new media types</a> in addition to the default ones already configured. Each media type has an associated media source (such as the image source) which support thumbnail generation and metadata extraction. Fields managed by the <a href=":field">Field module</a> may be added for storing that metadata, such as width and height, as well as any other associated values.', [
+          ':media-new' => Url::fromRoute('entity.media_type.add_form')->toString(),
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Creating revisions') . '</dt>';
+        $output .= '<dd>' . t('The Media module also enables you to create multiple versions of any media item, and revert to older versions using the <em>Revision information</em> settings.') . '</dd>';
+        $output .= '<dt>' . t('User permissions') . '</dt>';
+        $output .= '<dd>' . t('The Media module makes a number of permissions available, which can be set by role on the <a href=":permissions">permissions page</a>.', [
+          ':permissions' => Url::fromRoute('user.admin_permissions.module', [
+            'modules' => 'media',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Adding media to other content') . '</dt>';
+        $output .= '<dd>' . t('Users with permission to administer content types can add media support by adding a media reference field to the content type on the content type administration page. (The same is true of block types, taxonomy terms, user profiles, and other content that supports fields.) A media reference field can refer to any configured media type. It is possible to allow multiple media types in the same field.') . '</dd>';
+        $output .= '</dl>';
+        $output .= '<h2>' . t('Differences between Media, File, and Image reference fields') . '</h2>';
+        $output .= '<p>' . t('<em>Media</em> reference fields offer several advantages over basic <em>File</em> and <em>Image</em> references:') . '</p>';
+        $output .= '<ul>';
+        $output .= '<li>' . t('Media reference fields can reference multiple media types in the same field.') . '</li>';
+        $output .= '<li>' . t('Fields can also be added to media types themselves, which means that custom metadata like descriptions and taxonomy tags can be added for the referenced media. (Basic file and image fields do not support this.)') . '</li>';
+        $output .= '<li>' . t('Media types for audio and video files are provided by default, so there is no need for additional configuration to upload these media.') . '</li>';
+        $output .= '<li>' . t('Contributed or custom projects can provide additional media sources (such as third-party websites, Twitter, etc.).') . '</li>';
+        $output .= '<li>' . t('Existing media items can be reused on any other content items with a media reference field.') . '</li>';
+        $output .= '</ul>';
+        $output .= '<p>' . t('Use <em>Media</em> reference fields for most files, images, audio, videos, and remote media. Use <em>File</em> or <em>Image</em> reference fields when creating your own media types, or for legacy files and images created before installing the Media module.') . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'media' => [
+        'render element' => 'elements',
+      ],
+      'media_reference_help' => [
+        'render element' => 'element',
+        'base hook' => 'field_multiple_value_form',
+      ],
+      'media_oembed_iframe' => [
+        'variables' => [
+          'resource' => NULL,
+          'media' => NULL,
+          'placeholder_token' => '',
+        ],
+      ],
+      'media_embed_error' => [
+        'variables' => [
+          'message' => NULL,
+          'attributes' => [],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_entity_access().
+   */
+  #[Hook('entity_access')]
+  public function entityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    if ($operation === 'delete' && $entity instanceof FieldConfigInterface && $entity->getTargetEntityTypeId() === 'media') {
+      /** @var \Drupal\media\MediaTypeInterface $media_type */
+      $media_type = \Drupal::entityTypeManager()->getStorage('media_type')->load($entity->getTargetBundle());
+      return AccessResult::forbiddenIf($entity->id() === 'media.' . $media_type->id() . '.' . $media_type->getSource()->getConfiguration()['source_field']);
+    }
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Implements hook_field_ui_preconfigured_options_alter().
+   */
+  #[Hook('field_ui_preconfigured_options_alter')]
+  public function fieldUiPreconfiguredOptionsAlter(array &$options, $field_type) {
+    // If the field is not an "entity_reference"-based field, bail out.
+    /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
+    $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
+    $class = $field_type_manager->getPluginClass($field_type);
+    if (!is_a($class, 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', TRUE)) {
+      return;
+    }
+    // Set the default formatter for media in entity reference fields to be the
+    // "Rendered entity" formatter.
+    if (!empty($options['media'])) {
+      $options['media']['description'] = t('Field to reference media. Allows uploading and selecting from uploaded media.');
+      $options['media']['weight'] = -25;
+      $options['media']['category'] = FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY;
+      $options['media']['entity_view_display']['type'] = 'entity_reference_entity_view';
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_field_ui_field_storage_add_form_alter')]
+  public function formFieldUiFieldStorageAddFormAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    // Provide some help text to aid users decide whether they need a Media,
+    // File, or Image reference field.
+    $description_text = t('Use <em>Media</em> reference fields for most files, images, audio, videos, and remote media. Use <em>File</em> or <em>Image</em> reference fields when creating your own media types, or for legacy files and images created before installing the Media module.');
+    if (\Drupal::moduleHandler()->moduleExists('help')) {
+      $description_text .= ' ' . t('For more information, see the <a href="@help_url">Media help page</a>.', [
+        '@help_url' => Url::fromRoute('help.page', [
+          'name' => 'media',
+        ])->toString(),
+      ]);
+    }
+    $field_types = ['file_upload', 'field_ui:entity_reference:media'];
+    if (in_array($form_state->getValue('new_storage_type'), $field_types)) {
+      $form['group_field_options_wrapper']['description_wrapper'] = ['#type' => 'item', '#markup' => $description_text];
+    }
+  }
+
+  /**
+   * Implements hook_field_widget_complete_form_alter().
+   */
+  #[Hook('field_widget_complete_form_alter')]
+  public function fieldWidgetCompleteFormAlter(array &$field_widget_complete_form, FormStateInterface $form_state, array $context) {
+    $elements =& $field_widget_complete_form['widget'];
+    // Do not alter the default settings form.
+    if ($context['default']) {
+      return;
+    }
+    // Only act on entity reference fields that reference media.
+    $field_type = $context['items']->getFieldDefinition()->getType();
+    $target_type = $context['items']->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type');
+    if ($field_type !== 'entity_reference' || $target_type !== 'media') {
+      return;
+    }
+    // Autocomplete widgets need different help text than options widgets.
+    $widget_plugin_id = $context['widget']->getPluginId();
+    if (in_array($widget_plugin_id, ['entity_reference_autocomplete', 'entity_reference_autocomplete_tags'])) {
+      $is_autocomplete = TRUE;
+    }
+    else {
+      // @todo We can't yet properly alter non-autocomplete fields. Resolve this
+      //   in https://www.drupal.org/node/2943020 and remove this condition.
+      return;
+    }
+    $elements['#media_help'] = [];
+    // Retrieve the media bundle list and add information for the user based on
+    // which bundles are available to be created or referenced.
+    $settings = $context['items']->getFieldDefinition()->getSetting('handler_settings');
+    $allowed_bundles = !empty($settings['target_bundles']) ? $settings['target_bundles'] : [];
+    $add_url = _media_get_add_url($allowed_bundles);
+    if ($add_url) {
+      $elements['#media_help']['#media_add_help'] = t('Create your media on the <a href=":add_page" target="_blank">media add page</a> (opens a new window), then add it by name to the field below.', [':add_page' => $add_url]);
+    }
+    $elements['#theme'] = 'media_reference_help';
+    // @todo template_preprocess_field_multiple_value_form() assumes this key
+    //   exists, but it does not exist in the case of a single widget that
+    //   accepts multiple values. This is for some reason necessary to use
+    //   our template for the entity_autocomplete_tags widget.
+    //   Research and resolve this in https://www.drupal.org/node/2943020.
+    if (empty($elements['#cardinality_multiple'])) {
+      $elements['#cardinality_multiple'] = NULL;
+    }
+    // Use the title set on the element if it exists, otherwise fall back to the
+    // field label.
+    $elements['#media_help']['#original_label'] = $elements['#title'] ?? $context['items']->getFieldDefinition()->getLabel();
+    // Customize the label for the field widget.
+    // @todo Research a better approach https://www.drupal.org/node/2943024.
+    $use_existing_label = t('Use existing media');
+    if (!empty($elements[0]['target_id']['#title'])) {
+      $elements[0]['target_id']['#title'] = $use_existing_label;
+    }
+    if (!empty($elements['#title'])) {
+      $elements['#title'] = $use_existing_label;
+    }
+    if (!empty($elements['target_id']['#title'])) {
+      $elements['target_id']['#title'] = $use_existing_label;
+    }
+    // This help text is only relevant for autocomplete widgets. When the user
+    // is presented with options, they don't need to type anything or know what
+    // types of media are allowed.
+    if ($is_autocomplete) {
+      $elements['#media_help']['#media_list_help'] = t('Type part of the media name.');
+      $overview_url = Url::fromRoute('entity.media.collection');
+      if ($overview_url->access()) {
+        $elements['#media_help']['#media_list_link'] = t('See the <a href=":list_url" target="_blank">media list</a> (opens a new window) to help locate media.', [':list_url' => $overview_url->toString()]);
+      }
+      $all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media');
+      $bundle_labels = array_map(function ($bundle) use ($all_bundles) {
+          return $all_bundles[$bundle]['label'];
+      }, $allowed_bundles);
+      $elements['#media_help']['#allowed_types_help'] = t('Allowed media types: %types', ['%types' => implode(", ", $bundle_labels)]);
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    if (\Drupal::config('media.settings')->get('standalone_url')) {
+      /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
+      $entity_type = $entity_types['media'];
+      $entity_type->setLinkTemplate('canonical', '/media/{media}');
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_filter_format_edit_form_alter')]
+  public function formFilterFormatEditFormAlter(array &$form, FormStateInterface $form_state, $form_id) : void {
+    // Add an additional validate callback so we can ensure the order of filters
+    // is correct.
+    $form['#validate'][] = 'media_filter_format_edit_form_validate';
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_filter_format_add_form_alter')]
+  public function formFilterFormatAddFormAlter(array &$form, FormStateInterface $form_state, $form_id) : void {
+    // Add an additional validate callback so we can ensure the order of filters
+    // is correct.
+    $form['#validate'][] = 'media_filter_format_edit_form_validate';
+  }
+
+  /**
+   * Implements hook_field_widget_single_element_form_alter().
+   */
+  #[Hook('field_widget_single_element_form_alter')]
+  public function fieldWidgetSingleElementFormAlter(&$element, FormStateInterface $form_state, $context) {
+    // Add an attribute so that text editors plugins can pass the host entity's
+    // language, allowing it to present entities in the same language.
+    if (!empty($element['#type']) && $element['#type'] == 'text_format') {
+      $element['#attributes']['data-media-embed-host-entity-langcode'] = $context['items']->getLangcode();
+    }
+  }
+
+  /**
+   * Implements hook_views_query_substitutions().
+   */
+  #[Hook('views_query_substitutions')]
+  public function viewsQuerySubstitutions(ViewExecutable $view) {
+    $account = \Drupal::currentUser();
+    return [
+      '***VIEW_OWN_UNPUBLISHED_MEDIA***' => (int) $account->hasPermission('view own unpublished media'),
+      '***ADMINISTER_MEDIA***' => (int) $account->hasPermission('administer media'),
+    ];
+  }
+
+  /**
+   * Implements hook_field_type_category_info_alter().
+   */
+  #[Hook('field_type_category_info_alter')]
+  public function fieldTypeCategoryInfoAlter(&$definitions) {
+    // The `media` field type belongs in the `general` category, so the libraries
+    // need to be attached using an alter hook.
+    $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'media/drupal.media-icon';
+  }
+
+}
diff --git a/core/modules/media/tests/modules/media_test_embed/media_test_embed.module b/core/modules/media/tests/modules/media_test_embed/media_test_embed.module
index 260adb2a178515a36d83dc4a8256404a19ab9519..ec3eec8279b610bb83a0ade1485e4d3810b48f74 100644
--- a/core/modules/media/tests/modules/media_test_embed/media_test_embed.module
+++ b/core/modules/media/tests/modules/media_test_embed/media_test_embed.module
@@ -7,29 +7,9 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Implements hook_entity_view_alter().
- */
-function media_test_embed_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
-  $build['#attributes']['data-media-embed-test-active-theme'] = \Drupal::theme()->getActiveTheme()->getName();
-  $build['#attributes']['data-media-embed-test-view-mode'] = $display->getMode();
-}
-
 /**
  * Implements hook_preprocess_HOOK().
  */
 function media_test_embed_preprocess_media_embed_error(&$variables) {
   $variables['attributes']['class'][] = 'this-error-message-is-themeable';
 }
-
-/**
- * Implements hook_entity_access().
- */
-function media_test_embed_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  return AccessResult::neutral()->addCacheTags(['_media_test_embed_filter_access:' . $entity->getEntityTypeId() . ':' . $entity->id()]);
-}
diff --git a/core/modules/media/tests/modules/media_test_embed/src/Hook/MediaTestEmbedHooks.php b/core/modules/media/tests/modules/media_test_embed/src/Hook/MediaTestEmbedHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..b9783bbdc62163d1f09a303d0dc7bbd753401185
--- /dev/null
+++ b/core/modules/media/tests/modules/media_test_embed/src/Hook/MediaTestEmbedHooks.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\media_test_embed\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for media_test_embed.
+ */
+class MediaTestEmbedHooks {
+
+  /**
+   * Implements hook_entity_view_alter().
+   */
+  #[Hook('entity_view_alter')]
+  public function entityViewAlter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
+    $build['#attributes']['data-media-embed-test-active-theme'] = \Drupal::theme()->getActiveTheme()->getName();
+    $build['#attributes']['data-media-embed-test-view-mode'] = $display->getMode();
+  }
+
+  /**
+   * Implements hook_entity_access().
+   */
+  #[Hook('entity_access')]
+  public function entityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    return AccessResult::neutral()->addCacheTags([
+      '_media_test_embed_filter_access:' . $entity->getEntityTypeId() . ':' . $entity->id(),
+    ]);
+  }
+
+}
diff --git a/core/modules/media/tests/modules/media_test_oembed/media_test_oembed.module b/core/modules/media/tests/modules/media_test_oembed/media_test_oembed.module
index 2084d3d518081c6e82211b0aa83ccf0a0d1833aa..52b4be100c5cbd1cd5bdc6c279fb9e02f66a5f37 100644
--- a/core/modules/media/tests/modules/media_test_oembed/media_test_oembed.module
+++ b/core/modules/media/tests/modules/media_test_oembed/media_test_oembed.module
@@ -7,8 +7,6 @@
 
 declare(strict_types=1);
 
-use Drupal\media\OEmbed\Provider;
-
 /**
  * Implements hook_preprocess_media_oembed_iframe().
  */
@@ -20,12 +18,3 @@ function media_test_oembed_preprocess_media_oembed_iframe(array &$variables) {
   $variables['#attached']['library'][] = 'media_test_oembed/frame';
   $variables['#cache']['tags'][] = 'yo_there';
 }
-
-/**
- * Implements hook_oembed_resource_url_alter().
- */
-function media_test_oembed_oembed_resource_url_alter(array &$parsed_url, Provider $provider) {
-  if ($provider->getName() === 'Vimeo') {
-    $parsed_url['query']['altered'] = 1;
-  }
-}
diff --git a/core/modules/media/tests/modules/media_test_oembed/src/Hook/MediaTestOembedHooks.php b/core/modules/media/tests/modules/media_test_oembed/src/Hook/MediaTestOembedHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea4004e3359decb36197a8a88490d6faeeff6b44
--- /dev/null
+++ b/core/modules/media/tests/modules/media_test_oembed/src/Hook/MediaTestOembedHooks.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\media_test_oembed\Hook;
+
+use Drupal\media\OEmbed\Provider;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for media_test_oembed.
+ */
+class MediaTestOembedHooks {
+
+  /**
+   * Implements hook_oembed_resource_url_alter().
+   */
+  #[Hook('oembed_resource_url_alter')]
+  public function oembedResourceUrlAlter(array &$parsed_url, Provider $provider) {
+    if ($provider->getName() === 'Vimeo') {
+      $parsed_url['query']['altered'] = 1;
+    }
+  }
+
+}
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiAddTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiAddTest.php
index 259f0831ed682f2623bec57d6d6d38c970d8c073..554fc35e37c55b8fb13033e83f18b8280559c537 100644
--- a/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiAddTest.php
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiAddTest.php
@@ -12,7 +12,7 @@
 class MediaEmbedFilterConfigurationUiAddTest extends MediaEmbedFilterTestBase {
 
   /**
-   * @covers ::media_form_filter_format_add_form_alter
+   * @covers \Drupal\media\Hook\MediaHooks::formFilterFormatAddFormAlter
    * @dataProvider providerTestValidations
    */
   public function testValidationWhenAdding($filter_html_status, $filter_align_status, $filter_caption_status, $filter_html_image_secure_status, $media_embed, $allowed_html, $expected_error_message): void {
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiEditTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiEditTest.php
index 0b798f855330567ac9cc0e3bcb1a9673a6174da1..bcc62c736216a3b9f190212de9d4b5cbede52d6c 100644
--- a/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiEditTest.php
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaEmbedFilterConfigurationUiEditTest.php
@@ -12,7 +12,7 @@
 class MediaEmbedFilterConfigurationUiEditTest extends MediaEmbedFilterTestBase {
 
   /**
-   * @covers ::media_form_filter_format_edit_form_alter
+   * @covers \Drupal\media\Hook\MediaHooks::formFilterFormatEditFormAlter
    * @dataProvider providerTestValidations
    */
   public function testValidationWhenEditing($filter_html_status, $filter_align_status, $filter_caption_status, $filter_html_image_secure_status, $media_embed, $allowed_html, $expected_error_message): void {
diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module
index 1d98112b4bab288da1563799223ea39601a60c4b..14f1969507b8d1ad9abd123a926f7fa20a96672b 100644
--- a/core/modules/media_library/media_library.module
+++ b/core/modules/media_library/media_library.module
@@ -2,117 +2,16 @@
 
 /**
  * @file
- * Contains hook implementations for the media_library module.
  */
 
-use Drupal\Component\Utility\UrlHelper;
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\Entity\EntityFormDisplay;
 use Drupal\Core\Entity\Entity\EntityViewDisplay;
-use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Template\Attribute;
-use Drupal\Core\Url;
 use Drupal\image\Entity\ImageStyle;
 use Drupal\image\Plugin\Field\FieldType\ImageItem;
-use Drupal\media\MediaTypeForm;
 use Drupal\media\MediaTypeInterface;
-use Drupal\media_library\Form\FileUploadForm;
-use Drupal\media_library\Form\OEmbedForm;
-use Drupal\media_library\MediaLibraryState;
-use Drupal\views\Plugin\views\cache\CachePluginBase;
-use Drupal\views\ViewExecutable;
-
-/**
- * Implements hook_help().
- */
-function media_library_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.media_library':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Media Library module provides a rich, visual interface for managing media, and allows media to be reused in entity reference fields or embedded into text content. It overrides the <a href=":media-collection">media administration page</a>, allowing users to toggle between the existing table-style interface and a new grid-style interface for browsing and performing administrative operations on media.', [
-        ':media-collection' => Url::fromRoute('entity.media.collection')->toString(),
-      ]) . '</p>';
-      $output .= '<p>' . t('To learn more about media management, begin by reviewing the <a href=":media-help">documentation for the Media module</a>. For more information about the media library and related functionality, see the <a href=":media-library-handbook">online documentation for the Media Library module</a>.', [
-        ':media-help' => Url::fromRoute('help.page', ['name' => 'media'])->toString(),
-        ':media-library-handbook' => 'https://www.drupal.org/docs/8/core/modules/media-library-module',
-      ]) . '</p>';
-      $output .= '<h2>' . t('Selection dialog') . '</h2>';
-      $output .= '<p>' . t('When selecting media for an entity reference field or a text editor, Media Library opens a modal dialog to help users easily find and select media. The modal dialog can toggle between a grid-style and table-style interface, and new media items can be uploaded directly into it.') . '</p>';
-      $output .= '<p>' . t('Within the dialog, media items are divided up by type. If more than one media type can be selected by the user, the available types will be displayed as a set of vertical tabs. To users who have appropriate permissions, each media type may also present a short form allowing you to upload or create new media items of that type.') . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Grid-style vs. table-style interface') . '</dt>';
-      $output .= '<dd>' . t('The Media Library module provides a new grid-style interface for the media administration page that displays media as thumbnails, with minimal textual information, allowing users to visually browse media in their site. The existing table-style interface is better suited to displaying additional information about media items, in addition to being more accessible to users with assistive technology.') . '</dd>';
-      $output .= '<dt>' . t('Reusing media in entity reference fields') . '</dt>';
-      $output .= '<dd>' . t('Any entity reference field that references media can use the media library. To enable, configure the form display for the field to use the "Media library" widget.') . '</dd>';
-      $output .= '<dt>' . t('Embedding media in text content') . '</dt>';
-      $output .= '<dd>' . t('To use the media library within CKEditor, you must add the "Insert from Media Library" button to the CKEditor toolbar, and enable the "Embed media" filter in the text format associated with the text editor.') . '</dd>';
-      $output .= '</dl>';
-      $output .= '<h2>' . t('Customize') . '</h2>';
-      $output .= '<ul>';
-      $output .= '<li>';
-      if (\Drupal::moduleHandler()->moduleExists('views_ui') && \Drupal::currentUser()->hasPermission('administer views')) {
-        $output .= t('Both the table-style and grid-style interfaces are regular views and can be customized via the <a href=":views-ui">Views UI</a>, including sorting and filtering. This is the case for both the administration page and the modal dialog.', [
-          ':views_ui' => Url::fromRoute('entity.view.collection')->toString(),
-        ]);
-      }
-      else {
-        $output .= t('Both the table-style and grid-style interfaces are regular views and can be customized via the Views UI, including sorting and filtering. This is the case for both the administration page and the modal dialog.');
-      }
-      $output .= '</li>';
-      $output .= '<li>' . t('In the grid-style interface, the fields that are displayed (including which image style is used for images) can be customized by configuring the "Media library" view mode for each of your <a href=":media-types">media types</a>. The thumbnail images in the grid-style interface can be customized by configuring the "Media Library thumbnail (220×220)" image style.', [
-        ':media-types' => Url::fromRoute('entity.media_type.collection')->toString(),
-      ]) . '</li>';
-      $output .= '<li>' . t('When adding new media items within the modal dialog, the fields that are displayed can be customized by configuring the "Media library" form mode for each of your <a href=":media-types">media types</a>.', [
-        ':media-types' => Url::fromRoute('entity.media_type.collection')->toString(),
-      ]) . '</li>';
-      $output .= '</ul>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_media_source_info_alter().
- */
-function media_library_media_source_info_alter(array &$sources) {
-  if (empty($sources['audio_file']['forms']['media_library_add'])) {
-    $sources['audio_file']['forms']['media_library_add'] = FileUploadForm::class;
-  }
-  if (empty($sources['file']['forms']['media_library_add'])) {
-    $sources['file']['forms']['media_library_add'] = FileUploadForm::class;
-  }
-  if (empty($sources['image']['forms']['media_library_add'])) {
-    $sources['image']['forms']['media_library_add'] = FileUploadForm::class;
-  }
-  if (empty($sources['video_file']['forms']['media_library_add'])) {
-    $sources['video_file']['forms']['media_library_add'] = FileUploadForm::class;
-  }
-  if (empty($sources['oembed:video']['forms']['media_library_add'])) {
-    $sources['oembed:video']['forms']['media_library_add'] = OEmbedForm::class;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function media_library_theme(): array {
-  return [
-    'media__media_library' => [
-      'base hook' => 'media',
-    ],
-    'media_library_wrapper' => [
-      'render element' => 'element',
-    ],
-    'media_library_item' => [
-      'render element' => 'element',
-    ],
-  ];
-}
 
 /**
  * Prepares variables for the media library modal dialog.
@@ -146,82 +45,6 @@ function template_preprocess_media_library_item(array &$variables) {
   }
 }
 
-/**
- * Implements hook_views_pre_render().
- */
-function media_library_views_pre_render(ViewExecutable $view) {
-  $add_classes = function (&$option, array $classes_to_add) {
-    $classes = $option ? preg_split('/\s+/', trim($option)) : [];
-    $classes = array_filter($classes);
-    $classes = array_merge($classes, $classes_to_add);
-    $option = implode(' ', array_unique($classes));
-  };
-
-  if ($view->id() === 'media_library') {
-    if ($view->current_display === 'page') {
-      $add_classes($view->style_plugin->options['row_class'], ['js-media-library-item', 'js-click-to-select']);
-
-      if (array_key_exists('media_bulk_form', $view->field)) {
-        $add_classes($view->field['media_bulk_form']->options['element_class'], ['js-click-to-select-checkbox']);
-      }
-    }
-    elseif (str_starts_with($view->current_display, 'widget')) {
-      if (array_key_exists('media_library_select_form', $view->field)) {
-        $add_classes($view->field['media_library_select_form']->options['element_wrapper_class'], ['js-click-to-select-checkbox']);
-      }
-      $add_classes($view->display_handler->options['css_class'], ['js-media-library-view']);
-    }
-
-    $add_classes($view->style_plugin->options['row_class'], ['js-media-library-item', 'js-click-to-select']);
-
-    if ($view->display_handler->options['defaults']['css_class']) {
-      $add_classes($view->displayHandlers->get('default')->options['css_class'], ['js-media-library-view']);
-    }
-    else {
-      $add_classes($view->display_handler->options['css_class'], ['js-media-library-view']);
-    }
-  }
-}
-
-/**
- * Implements hook_views_post_render().
- */
-function media_library_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) {
-  if ($view->id() === 'media_library') {
-    $output['#attached']['library'][] = 'media_library/view';
-    if (str_starts_with($view->current_display, 'widget')) {
-      try {
-        $query = MediaLibraryState::fromRequest($view->getRequest())->all();
-      }
-      catch (InvalidArgumentException $e) {
-        // MediaLibraryState::fromRequest() will throw an exception if the view
-        // is being previewed, since not all required query parameters will be
-        // present. In a preview, however, this can be omitted since we're
-        // merely previewing.
-        // @todo Use the views API for checking for the preview mode when it
-        //   lands. https://www.drupal.org/project/drupal/issues/3060855
-        if (empty($view->preview) && empty($view->live_preview)) {
-          throw $e;
-        }
-      }
-
-      // If the current query contains any parameters we use to contextually
-      // filter the view, ensure they persist across AJAX rebuilds.
-      // The ajax_path is shared for all AJAX views on the page, but our query
-      // parameters are prefixed and should not interfere with any other views.
-      // @todo Rework or remove this in https://www.drupal.org/node/2983451
-      if (!empty($query)) {
-        $ajax_path = &$output['#attached']['drupalSettings']['views']['ajax_path'];
-        $parsed_url = UrlHelper::parse($ajax_path);
-        $query = array_merge($query, $parsed_url['query']);
-        // Reset the pager so that the user starts on the first page.
-        unset($query['page']);
-        $ajax_path = $parsed_url['path'] . '?' . UrlHelper::buildQuery($query);
-      }
-    }
-  }
-}
-
 /**
  * Implements hook_preprocess_media().
  */
@@ -285,26 +108,6 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo
   }
 }
 
-/**
- * Implements hook_form_alter().
- */
-function media_library_form_alter(array &$form, FormStateInterface $form_state, $form_id): void {
-  // Add a process callback to ensure that the media library view's exposed
-  // filters submit button is not moved to the modal dialog's button area.
-  if ($form_id === 'views_exposed_form' && str_starts_with($form['#id'], 'views-exposed-form-media-library-widget')) {
-    $form['#after_build'][] = '_media_library_views_form_media_library_after_build';
-  }
-
-  // Configures media_library displays when a type is submitted.
-  if ($form_state->getFormObject() instanceof MediaTypeForm) {
-    $form['actions']['submit']['#submit'][] = '_media_library_media_type_form_submit';
-    // @see field_ui_form_alter()
-    if (isset($form['actions']['save_continue'])) {
-      $form['actions']['save_continue']['#submit'][] = '_media_library_media_type_form_submit';
-    }
-  }
-}
-
 /**
  * Form #after_build callback for media_library view's exposed filters form.
  */
@@ -339,49 +142,6 @@ function _media_library_media_type_form_submit(array &$form, FormStateInterface
   }
 }
 
-/**
- * Implements hook_field_ui_preconfigured_options_alter().
- */
-function media_library_field_ui_preconfigured_options_alter(array &$options, $field_type) {
-  // If the field is not an "entity_reference"-based field, bail out.
-  $class = \Drupal::service('plugin.manager.field.field_type')->getPluginClass($field_type);
-  if (!is_a($class, EntityReferenceItem::class, TRUE)) {
-    return;
-  }
-
-  // Set the default field widget for media to be the Media library.
-  if (!empty($options['media'])) {
-    $options['media']['entity_form_display']['type'] = 'media_library_widget';
-  }
-}
-
-/**
- * Implements hook_local_tasks_alter().
- *
- * Removes tasks for the Media library if the view display no longer exists.
- */
-function media_library_local_tasks_alter(&$local_tasks) {
-  /** @var \Symfony\Component\Routing\RouteCollection $route_collection */
-  $route_collection = \Drupal::service('router')->getRouteCollection();
-  foreach (['media_library.grid', 'media_library.table'] as $key) {
-    if (isset($local_tasks[$key]) && !$route_collection->get($local_tasks[$key]['route_name'])) {
-      unset($local_tasks[$key]);
-    }
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_access().
- */
-function media_library_image_style_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  // Prevent the fallback 'media_library' image style from being deleted.
-  // @todo Lock the image style instead of preventing delete access.
-  //   https://www.drupal.org/project/drupal/issues/2247293
-  if ($operation === 'delete' && $entity->id() === 'media_library') {
-    return AccessResult::forbidden();
-  }
-}
-
 /**
  * Ensures that the given media type has a media_library form display.
  *
diff --git a/core/modules/media_library/media_library.views.inc b/core/modules/media_library/media_library.views.inc
deleted file mode 100644
index 7d1ead4c8faa5d901df7d254ced553e7ea0d981b..0000000000000000000000000000000000000000
--- a/core/modules/media_library/media_library.views.inc
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains Views integration for the media_library module.
- */
-
-/**
- * Implements hook_views_data().
- */
-function media_library_views_data() {
-  $data = [];
-  $data['media']['media_library_select_form'] = [
-    'title' => t('Select media'),
-    'help' => t('Provides a field for selecting media entities in our media library view'),
-    'real field' => 'mid',
-    'field' => [
-      'id' => 'media_library_select_form',
-    ],
-  ];
-  return $data;
-}
diff --git a/core/modules/media_library/src/Hook/MediaLibraryHooks.php b/core/modules/media_library/src/Hook/MediaLibraryHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..d617f539196155fe2f4d5f16b99bb2e1e4af773c
--- /dev/null
+++ b/core/modules/media_library/src/Hook/MediaLibraryHooks.php
@@ -0,0 +1,254 @@
+<?php
+
+namespace Drupal\media_library\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
+use Drupal\media\MediaTypeForm;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\media_library\MediaLibraryState;
+use Drupal\views\Plugin\views\cache\CachePluginBase;
+use Drupal\views\ViewExecutable;
+use Drupal\media_library\Form\OEmbedForm;
+use Drupal\media_library\Form\FileUploadForm;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for media_library.
+ */
+class MediaLibraryHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.media_library':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Media Library module provides a rich, visual interface for managing media, and allows media to be reused in entity reference fields or embedded into text content. It overrides the <a href=":media-collection">media administration page</a>, allowing users to toggle between the existing table-style interface and a new grid-style interface for browsing and performing administrative operations on media.', [
+          ':media-collection' => Url::fromRoute('entity.media.collection')->toString(),
+        ]) . '</p>';
+        $output .= '<p>' . t('To learn more about media management, begin by reviewing the <a href=":media-help">documentation for the Media module</a>. For more information about the media library and related functionality, see the <a href=":media-library-handbook">online documentation for the Media Library module</a>.', [
+          ':media-help' => Url::fromRoute('help.page', [
+            'name' => 'media',
+          ])->toString(),
+          ':media-library-handbook' => 'https://www.drupal.org/docs/8/core/modules/media-library-module',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Selection dialog') . '</h2>';
+        $output .= '<p>' . t('When selecting media for an entity reference field or a text editor, Media Library opens a modal dialog to help users easily find and select media. The modal dialog can toggle between a grid-style and table-style interface, and new media items can be uploaded directly into it.') . '</p>';
+        $output .= '<p>' . t('Within the dialog, media items are divided up by type. If more than one media type can be selected by the user, the available types will be displayed as a set of vertical tabs. To users who have appropriate permissions, each media type may also present a short form allowing you to upload or create new media items of that type.') . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Grid-style vs. table-style interface') . '</dt>';
+        $output .= '<dd>' . t('The Media Library module provides a new grid-style interface for the media administration page that displays media as thumbnails, with minimal textual information, allowing users to visually browse media in their site. The existing table-style interface is better suited to displaying additional information about media items, in addition to being more accessible to users with assistive technology.') . '</dd>';
+        $output .= '<dt>' . t('Reusing media in entity reference fields') . '</dt>';
+        $output .= '<dd>' . t('Any entity reference field that references media can use the media library. To enable, configure the form display for the field to use the "Media library" widget.') . '</dd>';
+        $output .= '<dt>' . t('Embedding media in text content') . '</dt>';
+        $output .= '<dd>' . t('To use the media library within CKEditor, you must add the "Insert from Media Library" button to the CKEditor toolbar, and enable the "Embed media" filter in the text format associated with the text editor.') . '</dd>';
+        $output .= '</dl>';
+        $output .= '<h2>' . t('Customize') . '</h2>';
+        $output .= '<ul>';
+        $output .= '<li>';
+        if (\Drupal::moduleHandler()->moduleExists('views_ui') && \Drupal::currentUser()->hasPermission('administer views')) {
+          $output .= t('Both the table-style and grid-style interfaces are regular views and can be customized via the <a href=":views-ui">Views UI</a>, including sorting and filtering. This is the case for both the administration page and the modal dialog.', [':views_ui' => Url::fromRoute('entity.view.collection')->toString()]);
+        }
+        else {
+          $output .= t('Both the table-style and grid-style interfaces are regular views and can be customized via the Views UI, including sorting and filtering. This is the case for both the administration page and the modal dialog.');
+        }
+        $output .= '</li>';
+        $output .= '<li>' . t('In the grid-style interface, the fields that are displayed (including which image style is used for images) can be customized by configuring the "Media library" view mode for each of your <a href=":media-types">media types</a>. The thumbnail images in the grid-style interface can be customized by configuring the "Media Library thumbnail (220×220)" image style.', [
+          ':media-types' => Url::fromRoute('entity.media_type.collection')->toString(),
+        ]) . '</li>';
+        $output .= '<li>' . t('When adding new media items within the modal dialog, the fields that are displayed can be customized by configuring the "Media library" form mode for each of your <a href=":media-types">media types</a>.', [
+          ':media-types' => Url::fromRoute('entity.media_type.collection')->toString(),
+        ]) . '</li>';
+        $output .= '</ul>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_media_source_info_alter().
+   */
+  #[Hook('media_source_info_alter')]
+  public function mediaSourceInfoAlter(array &$sources) {
+    if (empty($sources['audio_file']['forms']['media_library_add'])) {
+      $sources['audio_file']['forms']['media_library_add'] = FileUploadForm::class;
+    }
+    if (empty($sources['file']['forms']['media_library_add'])) {
+      $sources['file']['forms']['media_library_add'] = FileUploadForm::class;
+    }
+    if (empty($sources['image']['forms']['media_library_add'])) {
+      $sources['image']['forms']['media_library_add'] = FileUploadForm::class;
+    }
+    if (empty($sources['video_file']['forms']['media_library_add'])) {
+      $sources['video_file']['forms']['media_library_add'] = FileUploadForm::class;
+    }
+    if (empty($sources['oembed:video']['forms']['media_library_add'])) {
+      $sources['oembed:video']['forms']['media_library_add'] = OEmbedForm::class;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'media__media_library' => [
+        'base hook' => 'media',
+      ],
+      'media_library_wrapper' => [
+        'render element' => 'element',
+      ],
+      'media_library_item' => [
+        'render element' => 'element',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_views_pre_render().
+   */
+  #[Hook('views_pre_render')]
+  public function viewsPreRender(ViewExecutable $view) {
+    $add_classes = function (&$option, array $classes_to_add) {
+      $classes = $option ? preg_split('/\s+/', trim($option)) : [];
+      $classes = array_filter($classes);
+      $classes = array_merge($classes, $classes_to_add);
+      $option = implode(' ', array_unique($classes));
+    };
+    if ($view->id() === 'media_library') {
+      if ($view->current_display === 'page') {
+        $add_classes($view->style_plugin->options['row_class'], ['js-media-library-item', 'js-click-to-select']);
+        if (array_key_exists('media_bulk_form', $view->field)) {
+          $add_classes($view->field['media_bulk_form']->options['element_class'], ['js-click-to-select-checkbox']);
+        }
+      }
+      elseif (str_starts_with($view->current_display, 'widget')) {
+        if (array_key_exists('media_library_select_form', $view->field)) {
+          $add_classes($view->field['media_library_select_form']->options['element_wrapper_class'], ['js-click-to-select-checkbox']);
+        }
+        $add_classes($view->display_handler->options['css_class'], ['js-media-library-view']);
+      }
+      $add_classes($view->style_plugin->options['row_class'], ['js-media-library-item', 'js-click-to-select']);
+      if ($view->display_handler->options['defaults']['css_class']) {
+        $add_classes($view->displayHandlers->get('default')->options['css_class'], ['js-media-library-view']);
+      }
+      else {
+        $add_classes($view->display_handler->options['css_class'], ['js-media-library-view']);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_views_post_render().
+   */
+  #[Hook('views_post_render')]
+  public function viewsPostRender(ViewExecutable $view, &$output, CachePluginBase $cache) {
+    if ($view->id() === 'media_library') {
+      $output['#attached']['library'][] = 'media_library/view';
+      if (str_starts_with($view->current_display, 'widget')) {
+        try {
+          $query = MediaLibraryState::fromRequest($view->getRequest())->all();
+        }
+        catch (\InvalidArgumentException $e) {
+          // MediaLibraryState::fromRequest() will throw an exception if the view
+          // is being previewed, since not all required query parameters will be
+          // present. In a preview, however, this can be omitted since we're
+          // merely previewing.
+          // @todo Use the views API for checking for the preview mode when it
+          //   lands. https://www.drupal.org/project/drupal/issues/3060855
+          if (empty($view->preview) && empty($view->live_preview)) {
+            throw $e;
+          }
+        }
+        // If the current query contains any parameters we use to contextually
+        // filter the view, ensure they persist across AJAX rebuilds.
+        // The ajax_path is shared for all AJAX views on the page, but our query
+        // parameters are prefixed and should not interfere with any other views.
+        // @todo Rework or remove this in https://www.drupal.org/node/2983451
+        if (!empty($query)) {
+          $ajax_path =& $output['#attached']['drupalSettings']['views']['ajax_path'];
+          $parsed_url = UrlHelper::parse($ajax_path);
+          $query = array_merge($query, $parsed_url['query']);
+          // Reset the pager so that the user starts on the first page.
+          unset($query['page']);
+          $ajax_path = $parsed_url['path'] . '?' . UrlHelper::buildQuery($query);
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_form_alter().
+   */
+  #[Hook('form_alter')]
+  public function formAlter(array &$form, FormStateInterface $form_state, $form_id) : void {
+    // Add a process callback to ensure that the media library view's exposed
+    // filters submit button is not moved to the modal dialog's button area.
+    if ($form_id === 'views_exposed_form' && str_starts_with($form['#id'], 'views-exposed-form-media-library-widget')) {
+      $form['#after_build'][] = '_media_library_views_form_media_library_after_build';
+    }
+    // Configures media_library displays when a type is submitted.
+    if ($form_state->getFormObject() instanceof MediaTypeForm) {
+      $form['actions']['submit']['#submit'][] = '_media_library_media_type_form_submit';
+      // @see field_ui_form_alter()
+      if (isset($form['actions']['save_continue'])) {
+        $form['actions']['save_continue']['#submit'][] = '_media_library_media_type_form_submit';
+      }
+    }
+  }
+
+  /**
+   * Implements hook_field_ui_preconfigured_options_alter().
+   */
+  #[Hook('field_ui_preconfigured_options_alter')]
+  public function fieldUiPreconfiguredOptionsAlter(array &$options, $field_type) {
+    // If the field is not an "entity_reference"-based field, bail out.
+    $class = \Drupal::service('plugin.manager.field.field_type')->getPluginClass($field_type);
+    if (!is_a($class, EntityReferenceItem::class, TRUE)) {
+      return;
+    }
+    // Set the default field widget for media to be the Media library.
+    if (!empty($options['media'])) {
+      $options['media']['entity_form_display']['type'] = 'media_library_widget';
+    }
+  }
+
+  /**
+   * Implements hook_local_tasks_alter().
+   *
+   * Removes tasks for the Media library if the view display no longer exists.
+   */
+  #[Hook('local_tasks_alter')]
+  public function localTasksAlter(&$local_tasks) {
+    /** @var \Symfony\Component\Routing\RouteCollection $route_collection */
+    $route_collection = \Drupal::service('router')->getRouteCollection();
+    foreach (['media_library.grid', 'media_library.table'] as $key) {
+      if (isset($local_tasks[$key]) && !$route_collection->get($local_tasks[$key]['route_name'])) {
+        unset($local_tasks[$key]);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_access().
+   */
+  #[Hook('image_style_access')]
+  public function imageStyleAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    // Prevent the fallback 'media_library' image style from being deleted.
+    // @todo Lock the image style instead of preventing delete access.
+    //   https://www.drupal.org/project/drupal/issues/2247293
+    if ($operation === 'delete' && $entity->id() === 'media_library') {
+      return AccessResult::forbidden();
+    }
+  }
+
+}
diff --git a/core/modules/media_library/src/Hook/MediaLibraryViewsHooks.php b/core/modules/media_library/src/Hook/MediaLibraryViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..60dd07f9c6a37599cccb2241f0e68137a95f4d5c
--- /dev/null
+++ b/core/modules/media_library/src/Hook/MediaLibraryViewsHooks.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\media_library\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for media_library.
+ */
+class MediaLibraryViewsHooks {
+  /**
+   * @file
+   * Contains Views integration for the media_library module.
+   */
+
+  /**
+   * Implements hook_views_data().
+   */
+  #[Hook('views_data')]
+  public function viewsData() {
+    $data = [];
+    $data['media']['media_library_select_form'] = [
+      'title' => t('Select media'),
+      'help' => t('Provides a field for selecting media entities in our media library view'),
+      'real field' => 'mid',
+      'field' => [
+        'id' => 'media_library_select_form',
+      ],
+    ];
+    return $data;
+  }
+
+}
diff --git a/core/modules/media_library/tests/modules/media_library_test/media_library_test.module b/core/modules/media_library/tests/modules/media_library_test/media_library_test.module
deleted file mode 100644
index 0c446a242558611b1d424a3743bed9635fcc28e5..0000000000000000000000000000000000000000
--- a/core/modules/media_library/tests/modules/media_library_test/media_library_test.module
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains hook implementations for the media_library_test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\media_library_test\Form\TestNodeFormOverride;
-
-/**
- * Implements hook_ENTITY_TYPE_create_access().
- */
-function media_library_test_media_create_access(AccountInterface $account, array $context, $entity_bundle) {
-  if (isset($context['media_library_state'])) {
-    /** @var \Drupal\media_library\MediaLibraryState $state */
-    $state = $context['media_library_state'];
-    return AccessResult::forbiddenIf($state->getSelectedTypeId() === 'deny_access');
-  }
-  return AccessResult::neutral();
-}
-
-/**
- * Implements hook_entity_field_access().
- */
-function media_library_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
-  $deny_fields = \Drupal::state()->get('media_library_test_entity_field_access_deny_fields', []);
-  // Always deny the field_media_no_access field.
-  $deny_fields[] = 'field_media_no_access';
-  return AccessResult::forbiddenIf(in_array($field_definition->getName(), $deny_fields, TRUE), 'Field access denied by test module');
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function media_library_test_entity_type_alter(array &$entity_types): void {
-  if (isset($entity_types['node'])) {
-    $entity_types['node']->setFormClass('default', TestNodeFormOverride::class);
-    $entity_types['node']->setFormClass('edit', TestNodeFormOverride::class);
-  }
-}
-
-/**
- * Implements hook_field_widget_info_alter().
- */
-function media_library_test_field_widget_info_alter(array &$info) {
-  $info['media_library_widget']['field_types'][] = 'entity_reference_subclass';
-}
diff --git a/core/modules/media_library/tests/modules/media_library_test/src/Hook/MediaLibraryTestHooks.php b/core/modules/media_library/tests/modules/media_library_test/src/Hook/MediaLibraryTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..af5ae96f8d526ee28277742961351f3b94aeba38
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/src/Hook/MediaLibraryTestHooks.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\media_library_test\Hook;
+
+use Drupal\media_library_test\Form\TestNodeFormOverride;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for media_library_test.
+ */
+class MediaLibraryTestHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_create_access().
+   */
+  #[Hook('media_create_access')]
+  public function mediaCreateAccess(AccountInterface $account, array $context, $entity_bundle) {
+    if (isset($context['media_library_state'])) {
+      /** @var \Drupal\media_library\MediaLibraryState $state */
+      $state = $context['media_library_state'];
+      return AccessResult::forbiddenIf($state->getSelectedTypeId() === 'deny_access');
+    }
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Implements hook_entity_field_access().
+   */
+  #[Hook('entity_field_access')]
+  public function entityFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
+    $deny_fields = \Drupal::state()->get('media_library_test_entity_field_access_deny_fields', []);
+    // Always deny the field_media_no_access field.
+    $deny_fields[] = 'field_media_no_access';
+    return AccessResult::forbiddenIf(in_array($field_definition->getName(), $deny_fields, TRUE), 'Field access denied by test module');
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    if (isset($entity_types['node'])) {
+      $entity_types['node']->setFormClass('default', TestNodeFormOverride::class);
+      $entity_types['node']->setFormClass('edit', TestNodeFormOverride::class);
+    }
+  }
+
+  /**
+   * Implements hook_field_widget_info_alter().
+   */
+  #[Hook('field_widget_info_alter')]
+  public function fieldWidgetInfoAlter(array &$info) {
+    $info['media_library_widget']['field_types'][] = 'entity_reference_subclass';
+  }
+
+}
diff --git a/core/modules/menu_link_content/menu_link_content.module b/core/modules/menu_link_content/menu_link_content.module
index 7df21836816d3a999367e54a23909c95fd4e8b2b..5894c00aaaa0f29efee599bbbfff52dc43ba2f16 100644
--- a/core/modules/menu_link_content/menu_link_content.module
+++ b/core/modules/menu_link_content/menu_link_content.module
@@ -2,61 +2,8 @@
 
 /**
  * @file
- * Allows administrators to create custom menu links.
  */
 
-use Drupal\Core\Url;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\path_alias\PathAliasInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\system\MenuInterface;
-
-/**
- * Implements hook_help().
- */
-function menu_link_content_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.menu_link_content':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Custom Menu Links module allows users to create menu links. These links can be translated if multiple languages are used for the site.');
-      if (\Drupal::moduleHandler()->moduleExists('menu_ui')) {
-        $output .= ' ' . t('It is required by the Menu UI module, which provides an interface for managing menus and menu links. For more information, see the <a href=":menu-help">Menu UI module help page</a> and the <a href=":drupal-org-help">online documentation for the Custom Menu Links module</a>.', [':menu-help' => Url::fromRoute('help.page', ['name' => 'menu_ui'])->toString(), ':drupal-org-help' => 'https://www.drupal.org/documentation/modules/menu_link']);
-      }
-      else {
-        $output .= ' ' . t('For more information, see the <a href=":drupal-org-help">online documentation for the Custom Menu Links module</a>. If you install the Menu UI module, it provides an interface for managing menus and menu links.', [':drupal-org-help' => 'https://www.drupal.org/documentation/modules/menu_link']);
-      }
-      $output .= '</p>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function menu_link_content_entity_type_alter(array &$entity_types): void {
-  // @todo Moderation is disabled for custom menu links until when we have an UI
-  //   for them.
-  //   @see https://www.drupal.org/project/drupal/issues/2350939
-  $entity_types['menu_link_content']->setHandlerClass('moderation', '');
-}
-
-/**
- * Implements hook_menu_delete().
- */
-function menu_link_content_menu_delete(MenuInterface $menu) {
-  $storage = \Drupal::entityTypeManager()->getStorage('menu_link_content');
-  $menu_links = $storage->loadByProperties(['menu_name' => $menu->id()]);
-  $storage->delete($menu_links);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for 'path_alias'.
- */
-function menu_link_content_path_alias_insert(PathAliasInterface $path_alias) {
-  _menu_link_content_update_path_alias($path_alias->getAlias());
-}
-
 /**
  * Helper function to update plugin definition using internal scheme.
  *
@@ -74,56 +21,3 @@ function _menu_link_content_update_path_alias($path) {
     $menu_link_manager->updateDefinition($menu_link->getPluginId(), $menu_link->getPluginDefinition(), FALSE);
   }
 }
-
-/**
- * Implements hook_ENTITY_TYPE_update() for 'path_alias'.
- */
-function menu_link_content_path_alias_update(PathAliasInterface $path_alias) {
-  if ($path_alias->getAlias() != $path_alias->original->getAlias()) {
-    _menu_link_content_update_path_alias($path_alias->getAlias());
-    _menu_link_content_update_path_alias($path_alias->original->getAlias());
-  }
-  elseif ($path_alias->getPath() != $path_alias->original->getPath()) {
-    _menu_link_content_update_path_alias($path_alias->getAlias());
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for 'path_alias'.
- */
-function menu_link_content_path_alias_delete(PathAliasInterface $path_alias) {
-  _menu_link_content_update_path_alias($path_alias->getAlias());
-}
-
-/**
- * Implements hook_entity_predelete().
- */
-function menu_link_content_entity_predelete(EntityInterface $entity) {
-  /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
-  $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
-  $entity_type_id = $entity->getEntityTypeId();
-  foreach ($entity->uriRelationships() as $rel) {
-    $url = $entity->toUrl($rel);
-    // Entities can provide uri relationships that are not routed, in this case
-    // getRouteParameters() will throw an exception.
-    if (!$url->isRouted()) {
-      continue;
-    }
-    $route_parameters = $url->getRouteParameters();
-    if (!isset($route_parameters[$entity_type_id])) {
-      // Do not delete links which do not relate to this exact entity. For
-      // example, "collection", "add-form", etc.
-      continue;
-    }
-    // Delete all MenuLinkContent links that point to this entity route.
-    $result = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $route_parameters);
-
-    if ($result) {
-      foreach ($result as $id => $instance) {
-        if ($instance->isDeletable() && str_starts_with($id, 'menu_link_content:')) {
-          $instance->deleteLink();
-        }
-      }
-    }
-  }
-}
diff --git a/core/modules/menu_link_content/src/Hook/MenuLinkContentHooks.php b/core/modules/menu_link_content/src/Hook/MenuLinkContentHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..5e4695b1b6dd8175b67ef0656c3bbf5f82d328a0
--- /dev/null
+++ b/core/modules/menu_link_content/src/Hook/MenuLinkContentHooks.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace Drupal\menu_link_content\Hook;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\path_alias\PathAliasInterface;
+use Drupal\system\MenuInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for menu_link_content.
+ */
+class MenuLinkContentHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.menu_link_content':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Custom Menu Links module allows users to create menu links. These links can be translated if multiple languages are used for the site.');
+        if (\Drupal::moduleHandler()->moduleExists('menu_ui')) {
+          $output .= ' ' . t('It is required by the Menu UI module, which provides an interface for managing menus and menu links. For more information, see the <a href=":menu-help">Menu UI module help page</a> and the <a href=":drupal-org-help">online documentation for the Custom Menu Links module</a>.', [
+            ':menu-help' => Url::fromRoute('help.page', [
+              'name' => 'menu_ui',
+            ])->toString(),
+            ':drupal-org-help' => 'https://www.drupal.org/documentation/modules/menu_link',
+          ]);
+        }
+        else {
+          $output .= ' ' . t('For more information, see the <a href=":drupal-org-help">online documentation for the Custom Menu Links module</a>. If you install the Menu UI module, it provides an interface for managing menus and menu links.', [':drupal-org-help' => 'https://www.drupal.org/documentation/modules/menu_link']);
+        }
+        $output .= '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    // @todo Moderation is disabled for custom menu links until when we have an UI
+    //   for them.
+    //   @see https://www.drupal.org/project/drupal/issues/2350939
+    $entity_types['menu_link_content']->setHandlerClass('moderation', '');
+  }
+
+  /**
+   * Implements hook_menu_delete().
+   */
+  #[Hook('menu_delete')]
+  public function menuDelete(MenuInterface $menu) {
+    $storage = \Drupal::entityTypeManager()->getStorage('menu_link_content');
+    $menu_links = $storage->loadByProperties(['menu_name' => $menu->id()]);
+    $storage->delete($menu_links);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for 'path_alias'.
+   */
+  #[Hook('path_alias_insert')]
+  public function pathAliasInsert(PathAliasInterface $path_alias) {
+    _menu_link_content_update_path_alias($path_alias->getAlias());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for 'path_alias'.
+   */
+  #[Hook('path_alias_update')]
+  public function pathAliasUpdate(PathAliasInterface $path_alias) {
+    if ($path_alias->getAlias() != $path_alias->original->getAlias()) {
+      _menu_link_content_update_path_alias($path_alias->getAlias());
+      _menu_link_content_update_path_alias($path_alias->original->getAlias());
+    }
+    elseif ($path_alias->getPath() != $path_alias->original->getPath()) {
+      _menu_link_content_update_path_alias($path_alias->getAlias());
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for 'path_alias'.
+   */
+  #[Hook('path_alias_delete')]
+  public function pathAliasDelete(PathAliasInterface $path_alias) {
+    _menu_link_content_update_path_alias($path_alias->getAlias());
+  }
+
+  /**
+   * Implements hook_entity_predelete().
+   */
+  #[Hook('entity_predelete')]
+  public function entityPredelete(EntityInterface $entity) {
+    /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
+    $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
+    $entity_type_id = $entity->getEntityTypeId();
+    foreach ($entity->uriRelationships() as $rel) {
+      $url = $entity->toUrl($rel);
+      // Entities can provide uri relationships that are not routed, in this case
+      // getRouteParameters() will throw an exception.
+      if (!$url->isRouted()) {
+        continue;
+      }
+      $route_parameters = $url->getRouteParameters();
+      if (!isset($route_parameters[$entity_type_id])) {
+        // Do not delete links which do not relate to this exact entity. For
+        // example, "collection", "add-form", etc.
+        continue;
+      }
+      // Delete all MenuLinkContent links that point to this entity route.
+      $result = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $route_parameters);
+      if ($result) {
+        foreach ($result as $id => $instance) {
+          if ($instance->isDeletable() && str_starts_with($id, 'menu_link_content:')) {
+            $instance->deleteLink();
+          }
+        }
+      }
+    }
+  }
+
+}
diff --git a/core/modules/menu_link_content/tests/menu_operations_link_test/menu_operations_link_test.module b/core/modules/menu_link_content/tests/menu_operations_link_test/menu_operations_link_test.module
deleted file mode 100644
index 3dbae150c605854cae588c0f55ec50f7b3b2189f..0000000000000000000000000000000000000000
--- a/core/modules/menu_link_content/tests/menu_operations_link_test/menu_operations_link_test.module
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-/**
- * @file
- * Primary module hooks for Menu Operations Link Test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\menu_link_content\Entity\MenuLinkContent;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_entity_operation_alter().
- */
-function menu_operations_link_test_entity_operation_alter(array &$operations, EntityInterface $entity) {
-  if (!$entity instanceof MenuLinkContent) {
-    return;
-  }
-  // Alter the title of the edit link appearing in operations menu.
-  $operations['edit']['title'] = t('Altered Edit Title');
-}
-
-/**
- * Implements hook_entity_operation().
- */
-function menu_operations_link_test_entity_operation(EntityInterface $entity) {
-  if (!$entity instanceof MenuLinkContent) {
-    return;
-  }
-  $operations['custom_operation'] = [
-    'title' => t('Custom Home'),
-    'weight' => 20,
-    'url' => Url::fromRoute('<front>'),
-  ];
-  return $operations;
-}
diff --git a/core/modules/menu_link_content/tests/menu_operations_link_test/src/Hook/MenuOperationsLinkTestHooks.php b/core/modules/menu_link_content/tests/menu_operations_link_test/src/Hook/MenuOperationsLinkTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..8d73790b61c30d9d5381a59d57b9931ab41d187c
--- /dev/null
+++ b/core/modules/menu_link_content/tests/menu_operations_link_test/src/Hook/MenuOperationsLinkTestHooks.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\menu_operations_link_test\Hook;
+
+use Drupal\Core\Url;
+use Drupal\menu_link_content\Entity\MenuLinkContent;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for menu_operations_link_test.
+ */
+class MenuOperationsLinkTestHooks {
+
+  /**
+   * Implements hook_entity_operation_alter().
+   */
+  #[Hook('entity_operation_alter')]
+  public function entityOperationAlter(array &$operations, EntityInterface $entity) {
+    if (!$entity instanceof MenuLinkContent) {
+      return;
+    }
+    // Alter the title of the edit link appearing in operations menu.
+    $operations['edit']['title'] = t('Altered Edit Title');
+  }
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $entity) {
+    if (!$entity instanceof MenuLinkContent) {
+      return;
+    }
+    $operations['custom_operation'] = [
+      'title' => t('Custom Home'),
+      'weight' => 20,
+      'url' => Url::fromRoute('<front>'),
+    ];
+    return $operations;
+  }
+
+}
diff --git a/core/modules/menu_ui/menu_ui.module b/core/modules/menu_ui/menu_ui.module
index 4ded1100112584e0215ffe875bd39edea294685d..eadff2cb0594e49fe474926380dcac7c4e40a041 100644
--- a/core/modules/menu_ui/menu_ui.module
+++ b/core/modules/menu_ui/menu_ui.module
@@ -2,86 +2,12 @@
 
 /**
  * @file
- * Allows administrators to customize the site's navigation menus.
- *
- * A menu (in this context) is a hierarchical collection of links, generally
- * used for navigation.
  */
 
-use Drupal\block\BlockInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Url;
-use Drupal\Core\Breadcrumb\Breadcrumb;
-use Drupal\Core\Cache\CacheableMetadata;
-use Drupal\Core\Block\BlockPluginInterface;
-use Drupal\Core\Link;
-use Drupal\Core\Menu\MenuLinkInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\menu_link_content\Entity\MenuLinkContent;
 use Drupal\node\NodeTypeInterface;
-use Drupal\system\Entity\Menu;
 use Drupal\node\NodeInterface;
-use Drupal\system\MenuInterface;
-
-/**
- * Implements hook_help().
- */
-function menu_ui_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.menu_ui':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Menu UI module provides an interface for managing menus. A menu is a hierarchical collection of links, which can be within or external to the site, generally used for navigation. For more information, see the <a href=":menu">online documentation for the Menu UI module</a>.', [':menu' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/menu-ui-module']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Managing menus') . '</dt>';
-      $output .= '<dd>' . t('Users with the <em>Administer menus and menu links</em> permission can add, edit, and delete custom menus on the <a href=":menu">Menus page</a>. Custom menus can be special site menus, menus of external links, or any combination of internal and external links. You may create an unlimited number of additional menus, each of which will automatically have an associated block (if you have the <a href=":block_help">Block module</a> installed). By selecting <em>Edit menu</em>, you can add, edit, or delete links for a given menu. The links listing page provides a drag-and-drop interface for controlling the order of links, and creating a hierarchy within the menu.', [':block_help' => (\Drupal::moduleHandler()->moduleExists('block')) ? Url::fromRoute('help.page', ['name' => 'block'])->toString() : '#', ':menu' => Url::fromRoute('entity.menu.collection')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Displaying menus') . '</dt>';
-      $output .= '<dd>' . t('If you have the Block module installed, then each menu that you create is rendered in a block that you enable and position on the <a href=":blocks">Block layout page</a>. In some <a href=":themes">themes</a>, the main menu and possibly the secondary menu will be output automatically; you may be able to disable this behavior on the <a href=":themes">theme\'s settings page</a>.', [':blocks' => (\Drupal::moduleHandler()->moduleExists('block')) ? Url::fromRoute('block.admin_display')->toString() : '#', ':themes' => Url::fromRoute('system.themes_page')->toString(), ':theme_settings' => Url::fromRoute('system.theme_settings')->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-  if ($route_name == 'entity.menu.add_form' && \Drupal::moduleHandler()->moduleExists('block') && \Drupal::currentUser()->hasPermission('administer blocks')) {
-    return '<p>' . t('You can enable the newly-created block for this menu on the <a href=":blocks">Block layout page</a>.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
-  }
-  elseif ($route_name == 'entity.menu.collection' && \Drupal::moduleHandler()->moduleExists('block') && \Drupal::currentUser()->hasPermission('administer blocks')) {
-    return '<p>' . t('Each menu has a corresponding block that is managed on the <a href=":blocks">Block layout page</a>.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
-  }
-}
-
-/**
- * Implements hook_entity_type_build().
- */
-function menu_ui_entity_type_build(array &$entity_types) {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  $entity_types['menu']
-    ->setFormClass('add', 'Drupal\menu_ui\MenuForm')
-    ->setFormClass('edit', 'Drupal\menu_ui\MenuForm')
-    ->setFormClass('delete', 'Drupal\menu_ui\Form\MenuDeleteForm')
-    ->setListBuilderClass('Drupal\menu_ui\MenuListBuilder')
-    ->setLinkTemplate('add-form', '/admin/structure/menu/add')
-    ->setLinkTemplate('delete-form', '/admin/structure/menu/manage/{menu}/delete')
-    ->setLinkTemplate('edit-form', '/admin/structure/menu/manage/{menu}')
-    ->setLinkTemplate('add-link-form', '/admin/structure/menu/manage/{menu}/add')
-    ->setLinkTemplate('collection', '/admin/structure/menu');
-
-  if (isset($entity_types['node'])) {
-    $entity_types['node']->addConstraint('MenuSettings', []);
-  }
-}
-
-/**
- * Implements hook_block_view_BASE_BLOCK_ID_alter() for 'system_menu_block'.
- */
-function menu_ui_block_view_system_menu_block_alter(array &$build, BlockPluginInterface $block) {
-  if ($block->getBaseId() == 'system_menu_block') {
-    $menu_name = $block->getDerivativeId();
-    $build['#contextual_links']['menu'] = [
-      'route_parameters' => ['menu' => $menu_name],
-    ];
-  }
-}
 
 /**
  * Helper function to create or update a menu link for a node.
@@ -206,112 +132,6 @@ function menu_ui_get_menu_link_defaults(NodeInterface $node) {
   return $defaults;
 }
 
-/**
- * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\node\NodeForm.
- *
- * Adds menu item fields to the node form.
- *
- * @see menu_ui_form_node_form_submit()
- */
-function menu_ui_form_node_form_alter(&$form, FormStateInterface $form_state): void {
-  // Generate a list of possible parents (not including this link or descendants).
-  // @todo This must be handled in a #process handler.
-  $node = $form_state->getFormObject()->getEntity();
-  $defaults = menu_ui_get_menu_link_defaults($node);
-  /** @var \Drupal\node\NodeTypeInterface $node_type */
-  $node_type = $node->type->entity;
-  /** @var \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector */
-  $menu_parent_selector = \Drupal::service('menu.parent_form_selector');
-  $type_menus_ids = $node_type->getThirdPartySetting('menu_ui', 'available_menus', ['main']);
-  if (empty($type_menus_ids)) {
-    return;
-  }
-  /** @var \Drupal\system\MenuInterface[] $type_menus */
-  $type_menus = Menu::loadMultiple($type_menus_ids);
-  $available_menus = [];
-  foreach ($type_menus as $menu) {
-    $available_menus[$menu->id()] = $menu->label();
-  }
-  if ($defaults['id']) {
-    $default = $defaults['menu_name'] . ':' . $defaults['parent'];
-  }
-  else {
-    $default = $node_type->getThirdPartySetting('menu_ui', 'parent', 'main:');
-  }
-  $parent_element = $menu_parent_selector->parentSelectElement($default, $defaults['id'], $available_menus);
-  // If no possible parent menu items were found, there is nothing to display.
-  if (empty($parent_element)) {
-    return;
-  }
-
-  $form['menu'] = [
-    '#type' => 'details',
-    '#title' => t('Menu settings'),
-    '#access' => \Drupal::currentUser()->hasPermission('administer menu'),
-    '#open' => (bool) $defaults['id'],
-    '#group' => 'advanced',
-    '#attached' => [
-      'library' => ['menu_ui/drupal.menu_ui'],
-    ],
-    '#tree' => TRUE,
-    '#weight' => -2,
-    '#attributes' => ['class' => ['menu-link-form']],
-  ];
-  $form['menu']['enabled'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Provide a menu link'),
-    '#default_value' => (int) (bool) $defaults['id'],
-  ];
-  $form['menu']['link'] = [
-    '#type' => 'container',
-    '#parents' => ['menu'],
-    '#states' => [
-      'invisible' => [
-        'input[name="menu[enabled]"]' => ['checked' => FALSE],
-      ],
-    ],
-  ];
-
-  // Populate the element with the link data.
-  foreach (['id', 'entity_id'] as $key) {
-    $form['menu']['link'][$key] = ['#type' => 'value', '#value' => $defaults[$key]];
-  }
-
-  $form['menu']['link']['title'] = [
-    '#type' => 'textfield',
-    '#title' => t('Menu link title'),
-    '#default_value' => $defaults['title'],
-    '#maxlength' => $defaults['title_max_length'],
-  ];
-
-  $form['menu']['link']['description'] = [
-    '#type' => 'textfield',
-    '#title' => t('Description'),
-    '#default_value' => $defaults['description'],
-    '#description' => t('Shown when hovering over the menu link.'),
-    '#maxlength' => $defaults['description_max_length'],
-  ];
-
-  $form['menu']['link']['menu_parent'] = $parent_element;
-  $form['menu']['link']['menu_parent']['#title'] = t('Parent link');
-  $form['menu']['link']['menu_parent']['#attributes']['class'][] = 'menu-parent-select';
-
-  $form['menu']['link']['weight'] = [
-    '#type' => 'number',
-    '#title' => t('Weight'),
-    '#default_value' => $defaults['weight'],
-    '#description' => t('Menu links with lower weights are displayed before links with higher weights.'),
-  ];
-
-  foreach (array_keys($form['actions']) as $action) {
-    if ($action != 'preview' && isset($form['actions'][$action]['#type']) && $form['actions'][$action]['#type'] === 'submit') {
-      $form['actions'][$action]['#submit'][] = 'menu_ui_form_node_form_submit';
-    }
-  }
-
-  $form['#entity_builders'][] = 'menu_ui_node_builder';
-}
-
 /**
  * Entity form builder to add the menu information to the node.
  */
@@ -351,59 +171,6 @@ function menu_ui_form_node_form_submit($form, FormStateInterface $form_state) {
   }
 }
 
-/**
- * Implements hook_form_FORM_ID_alter() for \Drupal\node\NodeTypeForm.
- *
- * Adds menu options to the node type form.
- *
- * @see NodeTypeForm::form()
- * @see menu_ui_form_node_type_form_builder()
- */
-function menu_ui_form_node_type_form_alter(&$form, FormStateInterface $form_state): void {
-  /** @var \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector */
-  $menu_parent_selector = \Drupal::service('menu.parent_form_selector');
-  $menu_options = array_map(function (MenuInterface $menu) {
-    return $menu->label();
-  }, Menu::loadMultiple());
-  asort($menu_options);
-  /** @var \Drupal\node\NodeTypeInterface $type */
-  $type = $form_state->getFormObject()->getEntity();
-  $form['menu'] = [
-    '#type' => 'details',
-    '#title' => t('Menu settings'),
-    '#attached' => [
-      'library' => ['menu_ui/drupal.menu_ui.admin'],
-    ],
-    '#group' => 'additional_settings',
-  ];
-  $form['menu']['menu_options'] = [
-    '#type' => 'checkboxes',
-    '#title' => t('Available menus'),
-    '#default_value' => $type->getThirdPartySetting('menu_ui', 'available_menus', ['main']),
-    '#options' => $menu_options,
-    '#description' => t('Content of this type can be placed in the selected menus.'),
-  ];
-  // @todo See if we can avoid pre-loading all options by changing the form or
-  //   using a #process callback. https://www.drupal.org/node/2310319
-  //   To avoid an 'illegal option' error after saving the form we have to load
-  //   all available menu parents. Otherwise, it is not possible to dynamically
-  //   add options to the list using ajax.
-  $options_cacheability = new CacheableMetadata();
-  $options = $menu_parent_selector->getParentSelectOptions('', NULL, $options_cacheability);
-  $form['menu']['menu_parent'] = [
-    '#type' => 'select',
-    '#title' => t('Default parent link'),
-    '#default_value' => $type->getThirdPartySetting('menu_ui', 'parent', 'main:'),
-    '#options' => $options,
-    '#description' => t('Choose the menu link to be the default parent for a new link in the content authoring form.'),
-    '#attributes' => ['class' => ['menu-title-select']],
-  ];
-  $options_cacheability->applyTo($form['menu']['menu_parent']);
-
-  $form['#validate'][] = 'menu_ui_form_node_type_form_validate';
-  $form['#entity_builders'][] = 'menu_ui_form_node_type_form_builder';
-}
-
 /**
  * Validate handler for forms with menu options.
  *
@@ -442,51 +209,3 @@ function menu_ui_preprocess_block(&$variables) {
     $variables['attributes']['role'] = 'navigation';
   }
 }
-
-/**
- * Implements hook_system_breadcrumb_alter().
- */
-function menu_ui_system_breadcrumb_alter(Breadcrumb $breadcrumb, RouteMatchInterface $route_match, array $context) {
-  // Custom breadcrumb behavior for editing menu links, we append a link to
-  // the menu in which the link is found.
-  if (($route_match->getRouteName() == 'menu_ui.link_edit') && $menu_link = $route_match->getParameter('menu_link_plugin')) {
-    if (($menu_link instanceof MenuLinkInterface)) {
-      // Add a link to the menu admin screen.
-      $menu = Menu::load($menu_link->getMenuName());
-      $breadcrumb->addLink(Link::createFromRoute($menu->label(), 'entity.menu.edit_form', ['menu' => $menu->id()]));
-    }
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function menu_ui_theme(): array {
-  return [
-    'menu_link_form' => [
-      'render element' => 'form',
-    ],
-  ];
-}
-
-/**
- * Implements hook_entity_operation().
- */
-function menu_ui_entity_operation(EntityInterface $entity): array {
-  $operations = [];
-  if ($entity instanceof BlockInterface) {
-    $plugin = $entity->getPlugin();
-    if ($plugin->getBaseId() === 'system_menu_block') {
-      $menu = Menu::load($plugin->getDerivativeId());
-      if ($menu && $menu->access('edit')) {
-        $operations['menu-edit'] = [
-          'title' => t('Edit menu'),
-          'url' => $menu->toUrl('edit-form'),
-          'weight' => 50,
-        ];
-      }
-    }
-  }
-
-  return $operations;
-}
diff --git a/core/modules/menu_ui/src/Hook/MenuUiHooks.php b/core/modules/menu_ui/src/Hook/MenuUiHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..ae91d3f2e2fb3bbb16dec8b2a1e03036c59c51b5
--- /dev/null
+++ b/core/modules/menu_ui/src/Hook/MenuUiHooks.php
@@ -0,0 +1,297 @@
+<?php
+
+namespace Drupal\menu_ui\Hook;
+
+use Drupal\block\BlockInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Menu\MenuLinkInterface;
+use Drupal\Core\Breadcrumb\Breadcrumb;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\system\MenuInterface;
+use Drupal\system\Entity\Menu;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for menu_ui.
+ */
+class MenuUiHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.menu_ui':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Menu UI module provides an interface for managing menus. A menu is a hierarchical collection of links, which can be within or external to the site, generally used for navigation. For more information, see the <a href=":menu">online documentation for the Menu UI module</a>.', [
+          ':menu' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/menu-ui-module',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Managing menus') . '</dt>';
+        $output .= '<dd>' . t('Users with the <em>Administer menus and menu links</em> permission can add, edit, and delete custom menus on the <a href=":menu">Menus page</a>. Custom menus can be special site menus, menus of external links, or any combination of internal and external links. You may create an unlimited number of additional menus, each of which will automatically have an associated block (if you have the <a href=":block_help">Block module</a> installed). By selecting <em>Edit menu</em>, you can add, edit, or delete links for a given menu. The links listing page provides a drag-and-drop interface for controlling the order of links, and creating a hierarchy within the menu.', [
+          ':block_help' => \Drupal::moduleHandler()->moduleExists('block') ? Url::fromRoute('help.page', [
+            'name' => 'block',
+          ])->toString() : '#',
+          ':menu' => Url::fromRoute('entity.menu.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Displaying menus') . '</dt>';
+        $output .= '<dd>' . t('If you have the Block module installed, then each menu that you create is rendered in a block that you enable and position on the <a href=":blocks">Block layout page</a>. In some <a href=":themes">themes</a>, the main menu and possibly the secondary menu will be output automatically; you may be able to disable this behavior on the <a href=":themes">theme\'s settings page</a>.', [
+          ':blocks' => \Drupal::moduleHandler()->moduleExists('block') ? Url::fromRoute('block.admin_display')->toString() : '#',
+          ':themes' => Url::fromRoute('system.themes_page')->toString(),
+          ':theme_settings' => Url::fromRoute('system.theme_settings')->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+    if ($route_name == 'entity.menu.add_form' && \Drupal::moduleHandler()->moduleExists('block') && \Drupal::currentUser()->hasPermission('administer blocks')) {
+      return '<p>' . t('You can enable the newly-created block for this menu on the <a href=":blocks">Block layout page</a>.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
+    }
+    elseif ($route_name == 'entity.menu.collection' && \Drupal::moduleHandler()->moduleExists('block') && \Drupal::currentUser()->hasPermission('administer blocks')) {
+      return '<p>' . t('Each menu has a corresponding block that is managed on the <a href=":blocks">Block layout page</a>.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public function entityTypeBuild(array &$entity_types) {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    $entity_types['menu']->setFormClass('add', 'Drupal\menu_ui\MenuForm')->setFormClass('edit', 'Drupal\menu_ui\MenuForm')->setFormClass('delete', 'Drupal\menu_ui\Form\MenuDeleteForm')->setListBuilderClass('Drupal\menu_ui\MenuListBuilder')->setLinkTemplate('add-form', '/admin/structure/menu/add')->setLinkTemplate('delete-form', '/admin/structure/menu/manage/{menu}/delete')->setLinkTemplate('edit-form', '/admin/structure/menu/manage/{menu}')->setLinkTemplate('add-link-form', '/admin/structure/menu/manage/{menu}/add')->setLinkTemplate('collection', '/admin/structure/menu');
+    if (isset($entity_types['node'])) {
+      $entity_types['node']->addConstraint('MenuSettings', []);
+    }
+  }
+
+  /**
+   * Implements hook_block_view_BASE_BLOCK_ID_alter() for 'system_menu_block'.
+   */
+  #[Hook('block_view_system_menu_block_alter')]
+  public function blockViewSystemMenuBlockAlter(array &$build, BlockPluginInterface $block) {
+    if ($block->getBaseId() == 'system_menu_block') {
+      $menu_name = $block->getDerivativeId();
+      $build['#contextual_links']['menu'] = ['route_parameters' => ['menu' => $menu_name]];
+    }
+  }
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\node\NodeForm.
+   *
+   * Adds menu item fields to the node form.
+   *
+   * @see menu_ui_form_node_form_submit()
+   */
+  #[Hook('form_node_form_alter')]
+  public function formNodeFormAlter(&$form, FormStateInterface $form_state) : void {
+    // Generate a list of possible parents (not including this link or descendants).
+    // @todo This must be handled in a #process handler.
+    $node = $form_state->getFormObject()->getEntity();
+    $defaults = menu_ui_get_menu_link_defaults($node);
+    /** @var \Drupal\node\NodeTypeInterface $node_type */
+    $node_type = $node->type->entity;
+    /** @var \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector */
+    $menu_parent_selector = \Drupal::service('menu.parent_form_selector');
+    $type_menus_ids = $node_type->getThirdPartySetting('menu_ui', 'available_menus', ['main']);
+    if (empty($type_menus_ids)) {
+      return;
+    }
+    /** @var \Drupal\system\MenuInterface[] $type_menus */
+    $type_menus = Menu::loadMultiple($type_menus_ids);
+    $available_menus = [];
+    foreach ($type_menus as $menu) {
+      $available_menus[$menu->id()] = $menu->label();
+    }
+    if ($defaults['id']) {
+      $default = $defaults['menu_name'] . ':' . $defaults['parent'];
+    }
+    else {
+      $default = $node_type->getThirdPartySetting('menu_ui', 'parent', 'main:');
+    }
+    $parent_element = $menu_parent_selector->parentSelectElement($default, $defaults['id'], $available_menus);
+    // If no possible parent menu items were found, there is nothing to display.
+    if (empty($parent_element)) {
+      return;
+    }
+    $form['menu'] = [
+      '#type' => 'details',
+      '#title' => t('Menu settings'),
+      '#access' => \Drupal::currentUser()->hasPermission('administer menu'),
+      '#open' => (bool) $defaults['id'],
+      '#group' => 'advanced',
+      '#attached' => [
+        'library' => [
+          'menu_ui/drupal.menu_ui',
+        ],
+      ],
+      '#tree' => TRUE,
+      '#weight' => -2,
+      '#attributes' => [
+        'class' => [
+          'menu-link-form',
+        ],
+      ],
+    ];
+    $form['menu']['enabled'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Provide a menu link'),
+      '#default_value' => (int) (bool) $defaults['id'],
+    ];
+    $form['menu']['link'] = [
+      '#type' => 'container',
+      '#parents' => [
+        'menu',
+      ],
+      '#states' => [
+        'invisible' => [
+          'input[name="menu[enabled]"]' => [
+            'checked' => FALSE,
+          ],
+        ],
+      ],
+    ];
+    // Populate the element with the link data.
+    foreach (['id', 'entity_id'] as $key) {
+      $form['menu']['link'][$key] = ['#type' => 'value', '#value' => $defaults[$key]];
+    }
+    $form['menu']['link']['title'] = [
+      '#type' => 'textfield',
+      '#title' => t('Menu link title'),
+      '#default_value' => $defaults['title'],
+      '#maxlength' => $defaults['title_max_length'],
+    ];
+    $form['menu']['link']['description'] = [
+      '#type' => 'textfield',
+      '#title' => t('Description'),
+      '#default_value' => $defaults['description'],
+      '#description' => t('Shown when hovering over the menu link.'),
+      '#maxlength' => $defaults['description_max_length'],
+    ];
+    $form['menu']['link']['menu_parent'] = $parent_element;
+    $form['menu']['link']['menu_parent']['#title'] = t('Parent link');
+    $form['menu']['link']['menu_parent']['#attributes']['class'][] = 'menu-parent-select';
+    $form['menu']['link']['weight'] = [
+      '#type' => 'number',
+      '#title' => t('Weight'),
+      '#default_value' => $defaults['weight'],
+      '#description' => t('Menu links with lower weights are displayed before links with higher weights.'),
+    ];
+    foreach (array_keys($form['actions']) as $action) {
+      if ($action != 'preview' && isset($form['actions'][$action]['#type']) && $form['actions'][$action]['#type'] === 'submit') {
+        $form['actions'][$action]['#submit'][] = 'menu_ui_form_node_form_submit';
+      }
+    }
+    $form['#entity_builders'][] = 'menu_ui_node_builder';
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for \Drupal\node\NodeTypeForm.
+   *
+   * Adds menu options to the node type form.
+   *
+   * @see NodeTypeForm::form()
+   * @see menu_ui_form_node_type_form_builder()
+   */
+  #[Hook('form_node_type_form_alter')]
+  public function formNodeTypeFormAlter(&$form, FormStateInterface $form_state) : void {
+    /** @var \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector */
+    $menu_parent_selector = \Drupal::service('menu.parent_form_selector');
+    $menu_options = array_map(function (MenuInterface $menu) {
+        return $menu->label();
+    }, Menu::loadMultiple());
+    asort($menu_options);
+    /** @var \Drupal\node\NodeTypeInterface $type */
+    $type = $form_state->getFormObject()->getEntity();
+    $form['menu'] = [
+      '#type' => 'details',
+      '#title' => t('Menu settings'),
+      '#attached' => [
+        'library' => [
+          'menu_ui/drupal.menu_ui.admin',
+        ],
+      ],
+      '#group' => 'additional_settings',
+    ];
+    $form['menu']['menu_options'] = [
+      '#type' => 'checkboxes',
+      '#title' => t('Available menus'),
+      '#default_value' => $type->getThirdPartySetting('menu_ui', 'available_menus', [
+        'main',
+      ]),
+      '#options' => $menu_options,
+      '#description' => t('Content of this type can be placed in the selected menus.'),
+    ];
+    // @todo See if we can avoid pre-loading all options by changing the form or
+    //   using a #process callback. https://www.drupal.org/node/2310319
+    //   To avoid an 'illegal option' error after saving the form we have to load
+    //   all available menu parents. Otherwise, it is not possible to dynamically
+    //   add options to the list using ajax.
+    $options_cacheability = new CacheableMetadata();
+    $options = $menu_parent_selector->getParentSelectOptions('', NULL, $options_cacheability);
+    $form['menu']['menu_parent'] = [
+      '#type' => 'select',
+      '#title' => t('Default parent link'),
+      '#default_value' => $type->getThirdPartySetting('menu_ui', 'parent', 'main:'),
+      '#options' => $options,
+      '#description' => t('Choose the menu link to be the default parent for a new link in the content authoring form.'),
+      '#attributes' => [
+        'class' => [
+          'menu-title-select',
+        ],
+      ],
+    ];
+    $options_cacheability->applyTo($form['menu']['menu_parent']);
+    $form['#validate'][] = 'menu_ui_form_node_type_form_validate';
+    $form['#entity_builders'][] = 'menu_ui_form_node_type_form_builder';
+  }
+
+  /**
+   * Implements hook_system_breadcrumb_alter().
+   */
+  #[Hook('system_breadcrumb_alter')]
+  public function systemBreadcrumbAlter(Breadcrumb $breadcrumb, RouteMatchInterface $route_match, array $context) {
+    // Custom breadcrumb behavior for editing menu links, we append a link to
+    // the menu in which the link is found.
+    if ($route_match->getRouteName() == 'menu_ui.link_edit' && ($menu_link = $route_match->getParameter('menu_link_plugin'))) {
+      if ($menu_link instanceof MenuLinkInterface) {
+        // Add a link to the menu admin screen.
+        $menu = Menu::load($menu_link->getMenuName());
+        $breadcrumb->addLink(Link::createFromRoute($menu->label(), 'entity.menu.edit_form', ['menu' => $menu->id()]));
+      }
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return ['menu_link_form' => ['render element' => 'form']];
+  }
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $entity) : array {
+    $operations = [];
+    if ($entity instanceof BlockInterface) {
+      $plugin = $entity->getPlugin();
+      if ($plugin->getBaseId() === 'system_menu_block') {
+        $menu = Menu::load($plugin->getDerivativeId());
+        if ($menu && $menu->access('edit')) {
+          $operations['menu-edit'] = ['title' => t('Edit menu'), 'url' => $menu->toUrl('edit-form'), 'weight' => 50];
+        }
+      }
+    }
+    return $operations;
+  }
+
+}
diff --git a/core/modules/menu_ui/tests/src/Kernel/MenuBlockTest.php b/core/modules/menu_ui/tests/src/Kernel/MenuBlockTest.php
index f1586612eeed23cf705cb37292eed551ecbf5b49..f87a2639b877248c6616d7daeb8204e1ae1dfc6f 100644
--- a/core/modules/menu_ui/tests/src/Kernel/MenuBlockTest.php
+++ b/core/modules/menu_ui/tests/src/Kernel/MenuBlockTest.php
@@ -9,6 +9,7 @@
 use Drupal\block\Entity\Block;
 use Drupal\system\MenuInterface;
 use Drupal\Tests\user\Traits\UserCreationTrait;
+use Drupal\menu_ui\Hook\MenuUiHooks;
 
 /**
  * Tests SystemMenuBlock.
@@ -70,17 +71,18 @@ public function testOperationLinks(): void {
     ]);
 
     // Test when user does have "administer menu" permission.
+    $menuUiEntityOperation = new MenuUiHooks();
     $this->assertEquals([
       'menu-edit' => [
         'title' => 'Edit menu',
         'url' => $this->menu->toUrl('edit-form'),
         'weight' => 50,
       ],
-    ], menu_ui_entity_operation($block));
+    ], $menuUiEntityOperation->entityOperation($block));
 
     $this->setUpCurrentUser();
     // Test when user doesn't have "administer menu" permission.
-    $this->assertEmpty(menu_ui_entity_operation($block));
+    $this->assertEmpty($menuUiEntityOperation->entityOperation($block));
   }
 
 }
diff --git a/core/modules/migrate/migrate.module b/core/modules/migrate/migrate.module
deleted file mode 100644
index 3aa82dce8c3e939deec016b6a7f09b5121f6ff44..0000000000000000000000000000000000000000
--- a/core/modules/migrate/migrate.module
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides the Migrate API.
- */
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function migrate_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.migrate':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>';
-      $output .= t('The Migrate module provides a framework for migrating data, usually from an external source into your site. It does not provide a user interface. For more information, see the <a href=":migrate">online documentation for the Migrate module</a>.', [':migrate' => 'https://www.drupal.org/documentation/modules/migrate']);
-      $output .= '</p>';
-      return $output;
-  }
-}
diff --git a/core/modules/migrate/src/Hook/MigrateHooks.php b/core/modules/migrate/src/Hook/MigrateHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..d1db2505a85abc617134298966a36f8ec320d4e3
--- /dev/null
+++ b/core/modules/migrate/src/Hook/MigrateHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\migrate\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for migrate.
+ */
+class MigrateHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.migrate':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>';
+        $output .= t('The Migrate module provides a framework for migrating data, usually from an external source into your site. It does not provide a user interface. For more information, see the <a href=":migrate">online documentation for the Migrate module</a>.', [':migrate' => 'https://www.drupal.org/documentation/modules/migrate']);
+        $output .= '</p>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/migrate/tests/modules/migrate_prepare_row_test/migrate_prepare_row_test.module b/core/modules/migrate/tests/modules/migrate_prepare_row_test/migrate_prepare_row_test.module
deleted file mode 100644
index 6c1bbe49752d7cda18aa157e111c0deabc9254c1..0000000000000000000000000000000000000000
--- a/core/modules/migrate/tests/modules/migrate_prepare_row_test/migrate_prepare_row_test.module
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-/**
- * @file
- * Tests the migration source plugin prepareRow() exception handling.
- */
-
-declare(strict_types=1);
-
-use Drupal\migrate\Plugin\MigrationInterface;
-use Drupal\migrate\MigrateSkipRowException;
-use Drupal\migrate\Plugin\MigrateSourceInterface;
-use Drupal\migrate\Row;
-
-/**
- * Implements hook_migrate_prepare_row().
- */
-function migrate_prepare_row_test_migrate_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
-  // Test both options for save_to_map.
-  $data = $row->getSourceProperty('data');
-  if ($data == 'skip_and_record') {
-    // Record mapping but don't record a message.
-    throw new MigrateSkipRowException('', TRUE);
-  }
-  elseif ($data == 'skip_and_do_not_record') {
-    // Don't record mapping but record a message.
-    throw new MigrateSkipRowException('skip_and_do_not_record message', FALSE);
-  }
-}
diff --git a/core/modules/migrate/tests/modules/migrate_prepare_row_test/src/Hook/MigratePrepareRowTestHooks.php b/core/modules/migrate/tests/modules/migrate_prepare_row_test/src/Hook/MigratePrepareRowTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..5e8838e89f507e0ca2685c4b75457f4df72671f7
--- /dev/null
+++ b/core/modules/migrate/tests/modules/migrate_prepare_row_test/src/Hook/MigratePrepareRowTestHooks.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\migrate_prepare_row_test\Hook;
+
+use Drupal\migrate\MigrateSkipRowException;
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Plugin\MigrateSourceInterface;
+use Drupal\migrate\Row;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for migrate_prepare_row_test.
+ */
+class MigratePrepareRowTestHooks {
+
+  /**
+   * Implements hook_migrate_prepare_row().
+   */
+  #[Hook('migrate_prepare_row')]
+  public function migratePrepareRow(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
+    // Test both options for save_to_map.
+    $data = $row->getSourceProperty('data');
+    if ($data == 'skip_and_record') {
+      // Record mapping but don't record a message.
+      throw new MigrateSkipRowException('', TRUE);
+    }
+    elseif ($data == 'skip_and_do_not_record') {
+      // Don't record mapping but record a message.
+      throw new MigrateSkipRowException('skip_and_do_not_record message', FALSE);
+    }
+  }
+
+}
diff --git a/core/modules/migrate/tests/modules/migrate_skip_all_rows_test/migrate_skip_all_rows_test.module b/core/modules/migrate/tests/modules/migrate_skip_all_rows_test/migrate_skip_all_rows_test.module
deleted file mode 100644
index ec33718e95ea424dc8e1f9ebb38c69d3a62cd5a6..0000000000000000000000000000000000000000
--- a/core/modules/migrate/tests/modules/migrate_skip_all_rows_test/migrate_skip_all_rows_test.module
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * Tests the migration source plugin prepareRow() exception.
- */
-
-declare(strict_types=1);
-
-use Drupal\migrate\Plugin\MigrationInterface;
-use Drupal\migrate\MigrateSkipRowException;
-use Drupal\migrate\Plugin\MigrateSourceInterface;
-use Drupal\migrate\Row;
-
-/**
- * Implements hook_migrate_prepare_row().
- */
-function migrate_skip_all_rows_test_migrate_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
-  if (\Drupal::state()->get('migrate_skip_all_rows_test_migrate_prepare_row')) {
-    throw new MigrateSkipRowException();
-  }
-}
diff --git a/core/modules/migrate/tests/modules/migrate_skip_all_rows_test/src/Hook/MigrateSkipAllRowsTestHooks.php b/core/modules/migrate/tests/modules/migrate_skip_all_rows_test/src/Hook/MigrateSkipAllRowsTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c20100048d3b1607d2c6a90558665b97493fe158
--- /dev/null
+++ b/core/modules/migrate/tests/modules/migrate_skip_all_rows_test/src/Hook/MigrateSkipAllRowsTestHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\migrate_skip_all_rows_test\Hook;
+
+use Drupal\migrate\MigrateSkipRowException;
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Plugin\MigrateSourceInterface;
+use Drupal\migrate\Row;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for migrate_skip_all_rows_test.
+ */
+class MigrateSkipAllRowsTestHooks {
+
+  /**
+   * Implements hook_migrate_prepare_row().
+   */
+  #[Hook('migrate_prepare_row')]
+  public function migratePrepareRow(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
+    if (\Drupal::state()->get('migrate_skip_all_rows_test_migrate_prepare_row')) {
+      throw new MigrateSkipRowException();
+    }
+  }
+
+}
diff --git a/core/modules/migrate_drupal/migrate_drupal.module b/core/modules/migrate_drupal/migrate_drupal.module
deleted file mode 100644
index d85af9d97b3d84b74cfada4c1989b53dc645b21c..0000000000000000000000000000000000000000
--- a/core/modules/migrate_drupal/migrate_drupal.module
+++ /dev/null
@@ -1,141 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides migration from other Drupal sites.
- */
-
-use Drupal\Core\Database\DatabaseExceptionWrapper;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
-use Drupal\migrate\Exception\RequirementsException;
-use Drupal\migrate\MigrateExecutable;
-use Drupal\migrate\Plugin\RequirementsInterface;
-use Drupal\migrate_drupal\NodeMigrateType;
-
-/**
- * Implements hook_help().
- */
-function migrate_drupal_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.migrate_drupal':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Migrate Drupal module provides a framework based on the <a href=":migrate">Migrate module</a> to facilitate migration from a Drupal (6, 7, or 8) site to your website. It does not provide a user interface. For more information, see the <a href=":migrate_drupal">online documentation for the Migrate Drupal module</a>.', [':migrate' => Url::fromRoute('help.page', ['name' => 'migrate'])->toString(), ':migrate_drupal' => 'https://www.drupal.org/documentation/modules/migrate_drupal']) . '</p>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_migration_plugins_alter().
- */
-function migrate_drupal_migration_plugins_alter(array &$definitions) {
-  $module_handler = \Drupal::service('module_handler');
-  $migration_plugin_manager = \Drupal::service('plugin.manager.migration');
-
-  // This is why the deriver can't do this: the 'd6_taxonomy_vocabulary'
-  // definition is not available to the deriver as it is running inside
-  // getDefinitions().
-  if (isset($definitions['d6_taxonomy_vocabulary'])) {
-    $vocabulary_migration_definition = [
-      'source' => [
-        'ignore_map' => TRUE,
-        'plugin' => 'd6_taxonomy_vocabulary',
-      ],
-      'destination' => [
-        'plugin' => 'null',
-      ],
-      'idMap' => [
-        'plugin' => 'null',
-      ],
-    ];
-    $vocabulary_migration = $migration_plugin_manager->createStubMigration($vocabulary_migration_definition);
-    $translation_active = $module_handler->moduleExists('content_translation');
-
-    try {
-      $source_plugin = $vocabulary_migration->getSourcePlugin();
-      if ($source_plugin instanceof RequirementsInterface) {
-        $source_plugin->checkRequirements();
-      }
-      $executable = new MigrateExecutable($vocabulary_migration);
-      $process = ['vid' => $definitions['d6_taxonomy_vocabulary']['process']['vid']];
-      foreach ($source_plugin as $row) {
-        $executable->processRow($row, $process);
-        $source_vid = $row->getSourceProperty('vid');
-        $plugin_ids = [
-          'd6_term_node:' . $source_vid,
-          'd6_term_node_revision:' . $source_vid,
-        ];
-        if ($translation_active) {
-          $plugin_ids[] = 'd6_term_node_translation:' . $source_vid;
-        }
-        foreach (array_intersect($plugin_ids, array_keys($definitions)) as $plugin_id) {
-          // Match the field name derivation in d6_vocabulary_field.yml.
-          $field_name = substr('field_' . $row->getDestinationProperty('vid'), 0, 32);
-
-          // The Forum module is expecting 'taxonomy_forums' as the field name
-          // for the forum nodes. The 'forum_vocabulary' source property is
-          // evaluated in Drupal\taxonomy\Plugin\migrate\source\d6\Vocabulary
-          // and is set to true if the vocabulary vid being migrated is the
-          // same as the one in the 'forum_nav_vocabulary' variable on the
-          // source site.
-          $destination_vid = $row->getSourceProperty('forum_vocabulary') ? 'taxonomy_forums' : $field_name;
-          $definitions[$plugin_id]['process'][$destination_vid] = 'tid';
-        }
-      }
-    }
-    catch (RequirementsException $e) {
-      // This code currently runs whenever the definitions are being loaded and
-      // if you have a Drupal 7 source site then the requirements will not be
-      // met for the d6_taxonomy_vocabulary migration.
-    }
-    catch (DatabaseExceptionWrapper $e) {
-      // When the definitions are loaded it is possible the tables will not
-      // exist.
-    }
-  }
-
-  if (!$module_handler->moduleExists('node')) {
-    return;
-  }
-
-  $connection = \Drupal::database();
-  // We need to get the version of the source database in order to check
-  // if the classic or complete node tables have been used in a migration.
-  if (isset($definitions['system_site'])) {
-    // Use the source plugin of the system_site migration to get the
-    // database connection.
-    $migration = $definitions['system_site'];
-    /** @var \Drupal\migrate\Plugin\migrate\source\SqlBase $source_plugin */
-    $source_plugin = $migration_plugin_manager->createStubMigration($migration)
-      ->getSourcePlugin();
-
-    try {
-      $source_connection = $source_plugin->getDatabase();
-      $version = NodeMigrateType::getLegacyDrupalVersion($source_connection);
-    }
-    catch (\Exception $e) {
-      \Drupal::messenger()
-        ->addError(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname?</li></ul>', ['%error' => $e->getMessage()]));
-    }
-  }
-  // If this is a complete node migration then for all migrations, except the
-  // classic node migrations, replace any dependency on a classic node migration
-  // with a dependency on the complete node migration.
-  if (NodeMigrateType::getNodeMigrateType($connection, $version ?? FALSE) === NodeMigrateType::NODE_MIGRATE_TYPE_COMPLETE) {
-    $classic_migration_match = '/d([67])_(node|node_translation|node_revision|node_entity_translation)($|:.*)/';
-    $replace_with_complete_migration = function (&$value, $key, $classic_migration_match) {
-      if (is_string($value)) {
-        $value = preg_replace($classic_migration_match, 'd$1_node_complete$3', $value);
-      }
-    };
-
-    foreach ($definitions as &$definition) {
-      $is_node_classic_migration = preg_match($classic_migration_match, $definition['id']);
-      if (!$is_node_classic_migration && isset($definition['migration_dependencies'])) {
-        array_walk_recursive($definition['migration_dependencies'], $replace_with_complete_migration, $classic_migration_match);
-      }
-    }
-  }
-
-}
diff --git a/core/modules/migrate_drupal/src/Hook/MigrateDrupalHooks.php b/core/modules/migrate_drupal/src/Hook/MigrateDrupalHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..0fa17f286588b9fc0ad1062dd0e6c028463a3d1c
--- /dev/null
+++ b/core/modules/migrate_drupal/src/Hook/MigrateDrupalHooks.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Drupal\migrate_drupal\Hook;
+
+use Drupal\migrate_drupal\NodeMigrateType;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\migrate\Exception\RequirementsException;
+use Drupal\migrate\MigrateExecutable;
+use Drupal\migrate\Plugin\RequirementsInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for migrate_drupal.
+ */
+class MigrateDrupalHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.migrate_drupal':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Migrate Drupal module provides a framework based on the <a href=":migrate">Migrate module</a> to facilitate migration from a Drupal (6, 7, or 8) site to your website. It does not provide a user interface. For more information, see the <a href=":migrate_drupal">online documentation for the Migrate Drupal module</a>.', [
+          ':migrate' => Url::fromRoute('help.page', [
+            'name' => 'migrate',
+          ])->toString(),
+          ':migrate_drupal' => 'https://www.drupal.org/documentation/modules/migrate_drupal',
+        ]) . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_migration_plugins_alter().
+   */
+  #[Hook('migration_plugins_alter')]
+  public function migrationPluginsAlter(array &$definitions) {
+    $module_handler = \Drupal::service('module_handler');
+    $migration_plugin_manager = \Drupal::service('plugin.manager.migration');
+    // This is why the deriver can't do this: the 'd6_taxonomy_vocabulary'
+    // definition is not available to the deriver as it is running inside
+    // getDefinitions().
+    if (isset($definitions['d6_taxonomy_vocabulary'])) {
+      $vocabulary_migration_definition = [
+        'source' => [
+          'ignore_map' => TRUE,
+          'plugin' => 'd6_taxonomy_vocabulary',
+        ],
+        'destination' => [
+          'plugin' => 'null',
+        ],
+        'idMap' => [
+          'plugin' => 'null',
+        ],
+      ];
+      $vocabulary_migration = $migration_plugin_manager->createStubMigration($vocabulary_migration_definition);
+      $translation_active = $module_handler->moduleExists('content_translation');
+      try {
+        $source_plugin = $vocabulary_migration->getSourcePlugin();
+        if ($source_plugin instanceof RequirementsInterface) {
+          $source_plugin->checkRequirements();
+        }
+        $executable = new MigrateExecutable($vocabulary_migration);
+        $process = ['vid' => $definitions['d6_taxonomy_vocabulary']['process']['vid']];
+        foreach ($source_plugin as $row) {
+          $executable->processRow($row, $process);
+          $source_vid = $row->getSourceProperty('vid');
+          $plugin_ids = ['d6_term_node:' . $source_vid, 'd6_term_node_revision:' . $source_vid];
+          if ($translation_active) {
+            $plugin_ids[] = 'd6_term_node_translation:' . $source_vid;
+          }
+          foreach (array_intersect($plugin_ids, array_keys($definitions)) as $plugin_id) {
+            // Match the field name derivation in d6_vocabulary_field.yml.
+            $field_name = substr('field_' . $row->getDestinationProperty('vid'), 0, 32);
+            // The Forum module is expecting 'taxonomy_forums' as the field name
+            // for the forum nodes. The 'forum_vocabulary' source property is
+            // evaluated in Drupal\taxonomy\Plugin\migrate\source\d6\Vocabulary
+            // and is set to true if the vocabulary vid being migrated is the
+            // same as the one in the 'forum_nav_vocabulary' variable on the
+            // source site.
+            $destination_vid = $row->getSourceProperty('forum_vocabulary') ? 'taxonomy_forums' : $field_name;
+            $definitions[$plugin_id]['process'][$destination_vid] = 'tid';
+          }
+        }
+      }
+      catch (RequirementsException $e) {
+        // This code currently runs whenever the definitions are being loaded and
+        // if you have a Drupal 7 source site then the requirements will not be
+        // met for the d6_taxonomy_vocabulary migration.
+      }
+      catch (DatabaseExceptionWrapper $e) {
+        // When the definitions are loaded it is possible the tables will not
+        // exist.
+      }
+    }
+    if (!$module_handler->moduleExists('node')) {
+      return;
+    }
+    $connection = \Drupal::database();
+    // We need to get the version of the source database in order to check
+    // if the classic or complete node tables have been used in a migration.
+    if (isset($definitions['system_site'])) {
+      // Use the source plugin of the system_site migration to get the
+      // database connection.
+      $migration = $definitions['system_site'];
+      /** @var \Drupal\migrate\Plugin\migrate\source\SqlBase $source_plugin */
+      $source_plugin = $migration_plugin_manager->createStubMigration($migration)->getSourcePlugin();
+      try {
+        $source_connection = $source_plugin->getDatabase();
+        $version = NodeMigrateType::getLegacyDrupalVersion($source_connection);
+      }
+      catch (\Exception $e) {
+        \Drupal::messenger()->addError(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname?</li></ul>', ['%error' => $e->getMessage()]));
+      }
+    }
+    // If this is a complete node migration then for all migrations, except the
+    // classic node migrations, replace any dependency on a classic node migration
+    // with a dependency on the complete node migration.
+    if (NodeMigrateType::getNodeMigrateType($connection, $version ?? FALSE) === NodeMigrateType::NODE_MIGRATE_TYPE_COMPLETE) {
+      $classic_migration_match = '/d([67])_(node|node_translation|node_revision|node_entity_translation)($|:.*)/';
+      $replace_with_complete_migration = function (&$value, $key, $classic_migration_match) {
+        if (is_string($value)) {
+          $value = preg_replace($classic_migration_match, 'd$1_node_complete$3', $value);
+        }
+      };
+      foreach ($definitions as &$definition) {
+        $is_node_classic_migration = preg_match($classic_migration_match, $definition['id']);
+        if (!$is_node_classic_migration && isset($definition['migration_dependencies'])) {
+          array_walk_recursive($definition['migration_dependencies'], $replace_with_complete_migration, $classic_migration_match);
+        }
+      }
+    }
+  }
+
+}
diff --git a/core/modules/migrate_drupal/tests/src/Kernel/NodeMigrationTypePluginAlterTest.php b/core/modules/migrate_drupal/tests/src/Kernel/NodeMigrationTypePluginAlterTest.php
index ebdbbb06db58e9e6cf8cd7a37e5426fbf01dafe0..291f59f6b42912b68a2ca07f8074f136e22fc9da 100644
--- a/core/modules/migrate_drupal/tests/src/Kernel/NodeMigrationTypePluginAlterTest.php
+++ b/core/modules/migrate_drupal/tests/src/Kernel/NodeMigrationTypePluginAlterTest.php
@@ -7,6 +7,7 @@
 use Drupal\migrate_drupal\NodeMigrateType;
 use Drupal\Tests\migrate\Kernel\MigrateTestBase;
 use Drupal\Tests\migrate_drupal\Traits\NodeMigrateTypeTestTrait;
+use Drupal\migrate_drupal\Hook\MigrateDrupalHooks;
 
 /**
  * Tests the assignment of the node migration type in migrations_plugin_alter.
@@ -46,7 +47,8 @@ protected function setUp(): void {
    */
   public function testMigrationPluginAlter($type, array $migration_definitions, array $expected): void {
     $this->makeNodeMigrateMapTable($type, '7');
-    migrate_drupal_migration_plugins_alter($migration_definitions);
+    $migrateDrupalMigrationPluginsAlter = new MigrateDrupalHooks();
+    $migrateDrupalMigrationPluginsAlter->migrationPluginsAlter($migration_definitions);
     $this->assertSame($expected, $migration_definitions);
   }
 
diff --git a/core/modules/migrate_drupal_ui/migrate_drupal_ui.module b/core/modules/migrate_drupal_ui/migrate_drupal_ui.module
deleted file mode 100644
index 98d10330400544169f2003b48b3b7524cc5cc619..0000000000000000000000000000000000000000
--- a/core/modules/migrate_drupal_ui/migrate_drupal_ui.module
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-/**
- * @file
- * Alert administrators before starting the import process.
- */
-
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_help().
- */
-function migrate_drupal_ui_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.migrate_drupal_ui':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Migrate Drupal UI module provides a simple user interface to perform an upgrade from an earlier version of Drupal. For more information, see the <a href=":migrate">online documentation for the Migrate Drupal UI module</a>.',
-          [':migrate' => 'https://www.drupal.org/upgrade/migrate']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Preparing the site') . '</dt>';
-      $output .= '<dd>' . t('You need to install all modules on this site that are installed on the previous site. For example, if you have used the Book module on the previous site then you must install the Book module on this site for that data to be available on this site.') . '</dd>';
-      $output .= '<dt>' . t('Performing the upgrade') . '</dt>';
-      $output .= '<dd>' . t('On the <a href=":upgrade">Upgrade</a> page, you are guided through performing the upgrade in several steps.',
-          [':upgrade' => Url::fromRoute('migrate_drupal_ui.upgrade')->toString()]) . '</dd>';
-      $output .= '<dd><ol><li>' . t('If an upgrade has been performed on this site, you will be informed.') . '</li>';
-      $output .= '<li>' . t('You need to enter the database credentials of the Drupal site that you want to upgrade. You can also include its files directory in the upgrade. For example local files, /var/www/docroot, or remote files http://www.example.com.') . '</li>';
-      $output .= '<li>' . t('If there is existing content on the site that may be overwritten by this upgrade, you will be informed.') . '</li>';
-      $output .= '<li>' . t('The next page provides an overview of the modules that will be upgraded and those that will not be upgraded, before you proceed to perform the upgrade.') . '</li>';
-      $output .= '<li>' . t('Finally, a message is displayed about the number of upgrade tasks that were successful or failed.') . '</li></ol></dd>';
-      $output .= '<dt>' . t('Reviewing the upgrade log') . '</dt>';
-      $output .= '<dd>' . t('You can review a <a href=":log">log of upgrade messages</a> by clicking the link in the message provided after the upgrade or by filtering the messages for the type <em>migrate_drupal_ui</em> on the <a href=":messages">Recent log messages</a> page.',
-          [':log' => Url::fromRoute('migrate_drupal_ui.log')->toString(), ':messages' => Url::fromRoute('dblog.overview')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Rolling back an upgrade') . '</dt>';
-      $output .= '<dd>' . t('Rolling back an upgrade is not yet supported through the user interface.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
diff --git a/core/modules/migrate_drupal_ui/src/Hook/MigrateDrupalUiHooks.php b/core/modules/migrate_drupal_ui/src/Hook/MigrateDrupalUiHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..48b045806d7e7618aff66735ed43f1b35128a7ae
--- /dev/null
+++ b/core/modules/migrate_drupal_ui/src/Hook/MigrateDrupalUiHooks.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\migrate_drupal_ui\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for migrate_drupal_ui.
+ */
+class MigrateDrupalUiHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.migrate_drupal_ui':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Migrate Drupal UI module provides a simple user interface to perform an upgrade from an earlier version of Drupal. For more information, see the <a href=":migrate">online documentation for the Migrate Drupal UI module</a>.', [':migrate' => 'https://www.drupal.org/upgrade/migrate']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Preparing the site') . '</dt>';
+        $output .= '<dd>' . t('You need to install all modules on this site that are installed on the previous site. For example, if you have used the Book module on the previous site then you must install the Book module on this site for that data to be available on this site.') . '</dd>';
+        $output .= '<dt>' . t('Performing the upgrade') . '</dt>';
+        $output .= '<dd>' . t('On the <a href=":upgrade">Upgrade</a> page, you are guided through performing the upgrade in several steps.', [
+          ':upgrade' => Url::fromRoute('migrate_drupal_ui.upgrade')->toString(),
+        ]) . '</dd>';
+        $output .= '<dd><ol><li>' . t('If an upgrade has been performed on this site, you will be informed.') . '</li>';
+        $output .= '<li>' . t('You need to enter the database credentials of the Drupal site that you want to upgrade. You can also include its files directory in the upgrade. For example local files, /var/www/docroot, or remote files http://www.example.com.') . '</li>';
+        $output .= '<li>' . t('If there is existing content on the site that may be overwritten by this upgrade, you will be informed.') . '</li>';
+        $output .= '<li>' . t('The next page provides an overview of the modules that will be upgraded and those that will not be upgraded, before you proceed to perform the upgrade.') . '</li>';
+        $output .= '<li>' . t('Finally, a message is displayed about the number of upgrade tasks that were successful or failed.') . '</li></ol></dd>';
+        $output .= '<dt>' . t('Reviewing the upgrade log') . '</dt>';
+        $output .= '<dd>' . t('You can review a <a href=":log">log of upgrade messages</a> by clicking the link in the message provided after the upgrade or by filtering the messages for the type <em>migrate_drupal_ui</em> on the <a href=":messages">Recent log messages</a> page.', [
+          ':log' => Url::fromRoute('migrate_drupal_ui.log')->toString(),
+          ':messages' => Url::fromRoute('dblog.overview')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Rolling back an upgrade') . '</dt>';
+        $output .= '<dd>' . t('Rolling back an upgrade is not yet supported through the user interface.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/mysql/mysql.module b/core/modules/mysql/mysql.module
deleted file mode 100644
index 746647d032edd3f5082d60009c9bb638b6f1c0d8..0000000000000000000000000000000000000000
--- a/core/modules/mysql/mysql.module
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * The MySQL module provides the connection between Drupal and a MySQL, MariaDB or equivalent database.
- */
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function mysql_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.mysql':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The MySQL module provides the connection between Drupal and a MySQL, MariaDB or equivalent database. For more information, see the <a href=":mysql">online documentation for the MySQL module</a>.', [':mysql' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/mysql-module']) . '</p>';
-      return $output;
-
-  }
-}
diff --git a/core/modules/mysql/src/Hook/MysqlHooks.php b/core/modules/mysql/src/Hook/MysqlHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..4b7e2db531cc0f76e65986de319477d3adb5ed6a
--- /dev/null
+++ b/core/modules/mysql/src/Hook/MysqlHooks.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\mysql\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for mysql.
+ */
+class MysqlHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.mysql':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The MySQL module provides the connection between Drupal and a MySQL, MariaDB or equivalent database. For more information, see the <a href=":mysql">online documentation for the MySQL module</a>.', [
+          ':mysql' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/mysql-module',
+        ]) . '</p>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/navigation/modules/navigation_top_bar/navigation_top_bar.module b/core/modules/navigation/modules/navigation_top_bar/navigation_top_bar.module
deleted file mode 100644
index aa9b39d8dd2c87e932e79286be30cc3da38be495..0000000000000000000000000000000000000000
--- a/core/modules/navigation/modules/navigation_top_bar/navigation_top_bar.module
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-/**
- * @file
- * Primary module hooks for navigation top bar module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function navigation_top_bar_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.navigation_top_bar':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Navigation Top Bar module is a Feature Flag module which, when enabled, renders the experimental navigation top bar.') . '</p>';
-      $output .= '<p>' . t('The top bar provides relevant administrative information and tasks for the current page. It is not feature complete nor fully functional.') . '</p>';
-      $output .= '<p>' . t('Leaving this module enabled can affect both admin and front-end pages layouts and blocks like Primary admin actions, whose content might be moved to te top bar.') . '</p>';
-      $output .= '<p>' . t('It is recommended to leave this module off while it is under active development and experimental phase.') . '</p>';
-      $output .= '<p>' . t('For more information, see the <a href=":docs">online documentation for the Navigation Top Bar module</a>.', [
-        ':docs' => 'https://www.drupal.org/project/navigation',
-      ]) . '</p>';
-      return $output;
-  }
-}
diff --git a/core/modules/navigation/modules/navigation_top_bar/src/Hook/NavigationTopBarHooks.php b/core/modules/navigation/modules/navigation_top_bar/src/Hook/NavigationTopBarHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..2d7f10f03dd62211ec80f6cb56482a7f0ce2cc35
--- /dev/null
+++ b/core/modules/navigation/modules/navigation_top_bar/src/Hook/NavigationTopBarHooks.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\navigation_top_bar\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for navigation_top_bar.
+ */
+class NavigationTopBarHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.navigation_top_bar':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Navigation Top Bar module is a Feature Flag module which, when enabled, renders the experimental navigation top bar.') . '</p>';
+        $output .= '<p>' . t('The top bar provides relevant administrative information and tasks for the current page. It is not feature complete nor fully functional.') . '</p>';
+        $output .= '<p>' . t('Leaving this module enabled can affect both admin and front-end pages layouts and blocks like Primary admin actions, whose content might be moved to te top bar.') . '</p>';
+        $output .= '<p>' . t('It is recommended to leave this module off while it is under active development and experimental phase.') . '</p>';
+        $output .= '<p>' . t('For more information, see the <a href=":docs">online documentation for the Navigation Top Bar module</a>.', [':docs' => 'https://www.drupal.org/project/navigation']) . '</p>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/navigation/navigation.module b/core/modules/navigation/navigation.module
index 01ff7c75aaef413243ff7cb3e041637288869a8c..74a136250832983e1ac97319154b9efb9179a7fd 100644
--- a/core/modules/navigation/navigation.module
+++ b/core/modules/navigation/navigation.module
@@ -2,73 +2,8 @@
 
 /**
  * @file
- * Primary module hooks for navigation module.
  */
 
-use Drupal\Component\Plugin\PluginBase;
-use Drupal\Core\Block\BlockPluginInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\navigation\NavigationContentLinks;
-use Drupal\navigation\NavigationRenderer;
-use Drupal\navigation\Plugin\SectionStorage\NavigationSectionStorage;
-use Drupal\navigation\RenderCallbacks;
-
-/**
- * Implements hook_help().
- */
-function navigation_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.navigation':
-      $output = '';
-      $output .= '<h3>' . t('About') . '</h3>';
-      $output .= '<p>' . t('The Navigation module provides a left-aligned, collapsible, vertical sidebar navigation.') . '</p>';
-      $output .= '<p>' . t('For more information, see the <a href=":docs">online documentation for the Navigation module</a>.', [
-        ':docs' => 'https://www.drupal.org/project/navigation',
-      ]) . '</p>';
-      return $output;
-  }
-  $configuration_route = 'layout_builder.navigation.';
-  if (!$route_match->getRouteObject()->getOption('_layout_builder') || !str_starts_with($route_name, $configuration_route)) {
-    return layout_builder_help($route_name, $route_match);
-  }
-  if (str_starts_with($route_name, $configuration_route)) {
-    $output = '<p>' . t('This layout builder tool allows you to configure the blocks in the navigation toolbar.') . '</p>';
-    $output .= '<p>' . t('Forms and links inside the content of the layout builder tool have been disabled.') . '</p>';
-    return $output;
-  }
-}
-
-/**
- * Implements hook_page_top().
- */
-function navigation_page_top(array &$page_top) {
-  if (!\Drupal::currentUser()->hasPermission('access navigation')) {
-    return;
-  }
-
-  $navigation_renderer = \Drupal::service('navigation.renderer');
-  assert($navigation_renderer instanceof NavigationRenderer);
-  $navigation_renderer->removeToolbar($page_top);
-  if (\Drupal::routeMatch()->getRouteName() !== 'layout_builder.navigation.view') {
-    // Don't render the admin toolbar if in layout edit mode.
-    $navigation_renderer->buildNavigation($page_top);
-    $navigation_renderer->buildTopBar($page_top);
-    return;
-  }
-  // But if in layout mode, add an empty element to leave space. We need to use
-  // an empty .admin-toolbar element because the css uses the adjacent sibling
-  // selector. The actual rendering of the navigation blocks/layout occurs in
-  // the layout form.
-  $page_top['navigation'] = [
-    '#type' => 'html_tag',
-    '#tag' => 'aside',
-    '#attributes' => [
-      'class' => 'admin-toolbar',
-    ],
-  ];
-  $navigation_renderer->buildTopBar($page_top);
-}
-
 /**
  * Implements hook_module_implements_alter().
  */
@@ -83,136 +18,3 @@ function navigation_module_implements_alter(&$implementations, $hook) {
     unset($implementations['layout_builder']);
   }
 }
-
-/**
- * Implements hook_theme().
- */
-function navigation_theme($existing, $type, $theme, $path): array {
-  $items['top_bar'] = [
-    'variables' => [
-      'local_tasks' => [],
-    ],
-  ];
-
-  $items['top_bar_local_tasks'] = [
-    'variables' => [
-      'local_tasks' => [],
-    ],
-  ];
-
-  $items['top_bar_local_task'] = [
-    'variables' => [
-      'link' => [],
-    ],
-  ];
-
-  $items['big_pipe_interface_preview__navigation_shortcut_lazy_builder_lazyLinks__Shortcuts'] = [
-    'variables' => [
-      'callback' => NULL,
-      'arguments' => NULL,
-      'preview' => NULL,
-    ],
-    'base hook' => 'big_pipe_interface_preview',
-  ];
-
-  $items['block__navigation'] = [
-    'render element' => 'elements',
-    'base hook' => 'block',
-  ];
-
-  $items['navigation_menu'] = [
-    'base hook' => 'menu',
-    'variables' => [
-      'menu_name' => NULL,
-      'title' => NULL,
-      'items' => [],
-      'attributes' => [],
-    ],
-  ];
-
-  $items['menu_region__footer'] = [
-    'variables' => [
-      'items' => [],
-      'title' => NULL,
-      'menu_name' => NULL,
-    ],
-  ];
-
-  return $items;
-}
-
-/**
- * Implements hook_menu_links_discovered_alter().
- */
-function navigation_menu_links_discovered_alter(&$links) {
-  $navigation_links = \Drupal::classResolver(NavigationContentLinks::class);
-  assert($navigation_links instanceof NavigationContentLinks);
-  $navigation_links->addMenuLinks($links);
-  $navigation_links->removeAdminContentLink($links);
-  $navigation_links->removeHelpLink($links);
-}
-
-/**
- * Implements hook_block_build_BASE_BLOCK_ID_alter().
- */
-function navigation_block_build_local_tasks_block_alter(array &$build, BlockPluginInterface $block) {
-  $navigation_renderer = \Drupal::service('navigation.renderer');
-  assert($navigation_renderer instanceof NavigationRenderer);
-  $navigation_renderer->removeLocalTasks($build, $block);
-}
-
-/**
- * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
- *
- * Curate the blocks available in the Layout Builder "Add Block" UI.
- */
-function navigation_plugin_filter_block__layout_builder_alter(array &$definitions, array $extra) {
-  if (($extra['section_storage'] ?? NULL) instanceof NavigationSectionStorage) {
-    // Remove all blocks other than the ones we support.
-    $navigation_safe = [
-      'navigation_user',
-      'navigation_shortcuts',
-      'navigation_menu',
-    ];
-    $definitions = array_filter($definitions, static function (array $definition, string $plugin_id) use ($navigation_safe): bool {
-      [$base_plugin_id] = explode(PluginBase::DERIVATIVE_SEPARATOR, $plugin_id);
-      return in_array($base_plugin_id, $navigation_safe, TRUE);
-    }, ARRAY_FILTER_USE_BOTH);
-  }
-}
-
-/**
- * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
- */
-function navigation_plugin_filter_layout__layout_builder_alter(array &$definitions, array $extra) {
-  if (($extra['section_storage'] ?? NULL) instanceof NavigationSectionStorage) {
-    // We don't allow adding a new section.
-    $definitions = [];
-  }
-}
-
-/**
- * Implements hook_block_alter().
- */
-function navigation_block_alter(&$definitions): void {
-  $hidden = [
-    'navigation_user',
-    'navigation_shortcuts',
-    'navigation_menu',
-    'navigation_link',
-  ];
-  foreach ($hidden as $block_id) {
-    if (isset($definitions[$block_id])) {
-      $definitions[$block_id]['_block_ui_hidden'] = TRUE;
-    }
-  }
-}
-
-/**
- * Implements hook_element_info_alter().
- */
-function navigation_element_info_alter(array &$info) {
-  if (array_key_exists('layout_builder', $info)) {
-    $info['layout_builder']['#pre_render'][] = [RenderCallbacks::class, 'alterLayoutBuilder'];
-  }
-}
diff --git a/core/modules/navigation/src/Hook/NavigationHooks.php b/core/modules/navigation/src/Hook/NavigationHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..3f93bb9832bfe521a546ce85db314d1f3c307aa7
--- /dev/null
+++ b/core/modules/navigation/src/Hook/NavigationHooks.php
@@ -0,0 +1,177 @@
+<?php
+
+namespace Drupal\navigation\Hook;
+
+use Drupal\navigation\RenderCallbacks;
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\navigation\Plugin\SectionStorage\NavigationSectionStorage;
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\navigation\NavigationContentLinks;
+use Drupal\navigation\NavigationRenderer;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for navigation.
+ */
+class NavigationHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.navigation':
+        $output = '';
+        $output .= '<h3>' . t('About') . '</h3>';
+        $output .= '<p>' . t('The Navigation module provides a left-aligned, collapsible, vertical sidebar navigation.') . '</p>';
+        $output .= '<p>' . t('For more information, see the <a href=":docs">online documentation for the Navigation module</a>.', [':docs' => 'https://www.drupal.org/project/navigation']) . '</p>';
+        return $output;
+    }
+    $configuration_route = 'layout_builder.navigation.';
+    if (!$route_match->getRouteObject()->getOption('_layout_builder') || !str_starts_with($route_name, $configuration_route)) {
+      return \Drupal::moduleHandler()->invoke('layout_builder', 'help', [$route_name, $route_match]);
+    }
+    if (str_starts_with($route_name, $configuration_route)) {
+      $output = '<p>' . t('This layout builder tool allows you to configure the blocks in the navigation toolbar.') . '</p>';
+      $output .= '<p>' . t('Forms and links inside the content of the layout builder tool have been disabled.') . '</p>';
+      return $output;
+    }
+  }
+
+  /**
+   * Implements hook_page_top().
+   */
+  #[Hook('page_top')]
+  public function pageTop(array &$page_top) {
+    if (!\Drupal::currentUser()->hasPermission('access navigation')) {
+      return;
+    }
+    $navigation_renderer = \Drupal::service('navigation.renderer');
+    assert($navigation_renderer instanceof NavigationRenderer);
+    $navigation_renderer->removeToolbar($page_top);
+    if (\Drupal::routeMatch()->getRouteName() !== 'layout_builder.navigation.view') {
+      // Don't render the admin toolbar if in layout edit mode.
+      $navigation_renderer->buildNavigation($page_top);
+      $navigation_renderer->buildTopBar($page_top);
+      return;
+    }
+    // But if in layout mode, add an empty element to leave space. We need to use
+    // an empty .admin-toolbar element because the css uses the adjacent sibling
+    // selector. The actual rendering of the navigation blocks/layout occurs in
+    // the layout form.
+    $page_top['navigation'] = [
+      '#type' => 'html_tag',
+      '#tag' => 'aside',
+      '#attributes' => [
+        'class' => 'admin-toolbar',
+      ],
+    ];
+    $navigation_renderer->buildTopBar($page_top);
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme($existing, $type, $theme, $path) : array {
+    $items['top_bar'] = ['variables' => ['local_tasks' => []]];
+    $items['top_bar_local_tasks'] = ['variables' => ['local_tasks' => []]];
+    $items['top_bar_local_task'] = ['variables' => ['link' => []]];
+    $items['big_pipe_interface_preview__navigation_shortcut_lazy_builder_lazyLinks__Shortcuts'] = [
+      'variables' => [
+        'callback' => NULL,
+        'arguments' => NULL,
+        'preview' => NULL,
+      ],
+      'base hook' => 'big_pipe_interface_preview',
+    ];
+    $items['block__navigation'] = ['render element' => 'elements', 'base hook' => 'block'];
+    $items['navigation_menu'] = [
+      'base hook' => 'menu',
+      'variables' => [
+        'menu_name' => NULL,
+        'title' => NULL,
+        'items' => [],
+        'attributes' => [],
+      ],
+    ];
+    $items['menu_region__footer'] = ['variables' => ['items' => [], 'title' => NULL, 'menu_name' => NULL]];
+    return $items;
+  }
+
+  /**
+   * Implements hook_menu_links_discovered_alter().
+   */
+  #[Hook('menu_links_discovered_alter')]
+  public function menuLinksDiscoveredAlter(&$links) {
+    $navigation_links = \Drupal::classResolver(NavigationContentLinks::class);
+    assert($navigation_links instanceof NavigationContentLinks);
+    $navigation_links->addMenuLinks($links);
+    $navigation_links->removeAdminContentLink($links);
+    $navigation_links->removeHelpLink($links);
+  }
+
+  /**
+   * Implements hook_block_build_BASE_BLOCK_ID_alter().
+   */
+  #[Hook('block_build_local_tasks_block_alter')]
+  public function blockBuildLocalTasksBlockAlter(array &$build, BlockPluginInterface $block) {
+    $navigation_renderer = \Drupal::service('navigation.renderer');
+    assert($navigation_renderer instanceof NavigationRenderer);
+    $navigation_renderer->removeLocalTasks($build, $block);
+  }
+
+  /**
+   * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
+   *
+   * Curate the blocks available in the Layout Builder "Add Block" UI.
+   */
+  #[Hook('plugin_filter_block__layout_builder_alter')]
+  public function pluginFilterBlockLayoutBuilderAlter(array &$definitions, array $extra) {
+    if (($extra['section_storage'] ?? NULL) instanceof NavigationSectionStorage) {
+      // Remove all blocks other than the ones we support.
+      $navigation_safe = ['navigation_user', 'navigation_shortcuts', 'navigation_menu'];
+      $definitions = array_filter($definitions, static function (array $definition, string $plugin_id) use ($navigation_safe) : bool {
+          [$base_plugin_id] = explode(PluginBase::DERIVATIVE_SEPARATOR, $plugin_id);
+          return in_array($base_plugin_id, $navigation_safe, TRUE);
+      }, ARRAY_FILTER_USE_BOTH);
+    }
+  }
+
+  /**
+   * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
+   */
+  #[Hook('plugin_filter_layout__layout_builder_alter')]
+  public function pluginFilterLayoutLayoutBuilderAlter(array &$definitions, array $extra) {
+    if (($extra['section_storage'] ?? NULL) instanceof NavigationSectionStorage) {
+      // We don't allow adding a new section.
+      $definitions = [];
+    }
+  }
+
+  /**
+   * Implements hook_block_alter().
+   */
+  #[Hook('block_alter')]
+  public function blockAlter(&$definitions) : void {
+    $hidden = ['navigation_user', 'navigation_shortcuts', 'navigation_menu', 'navigation_link'];
+    foreach ($hidden as $block_id) {
+      if (isset($definitions[$block_id])) {
+        $definitions[$block_id]['_block_ui_hidden'] = TRUE;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_element_info_alter().
+   */
+  #[Hook('element_info_alter')]
+  public function elementInfoAlter(array &$info) {
+    if (array_key_exists('layout_builder', $info)) {
+      $info['layout_builder']['#pre_render'][] = [RenderCallbacks::class, 'alterLayoutBuilder'];
+    }
+  }
+
+}
diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc
index ff5f8331907b9431146d28e10e1609e8f1f738e4..61bd54f1e74713d75e1c009547e59023840669b5 100644
--- a/core/modules/node/node.admin.inc
+++ b/core/modules/node/node.admin.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Content administration and module settings user interface.
  */
 
 use Drupal\Core\Batch\BatchBuilder;
diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php
index 793170dc220961476da1208b77a1d50ce0307da2..0c78f7d8c727930404a0cde95f79dc7e62cc61df 100644
--- a/core/modules/node/node.api.php
+++ b/core/modules/node/node.api.php
@@ -5,6 +5,7 @@
  * Hooks specific to the Node module.
  */
 
+use Drupal\Core\Session\AccountInterface;
 use Drupal\node\NodeInterface;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Xss;
@@ -73,7 +74,7 @@
  * @see node_access_rebuild()
  * @ingroup node_access
  */
-function hook_node_grants(\Drupal\Core\Session\AccountInterface $account, $operation) {
+function hook_node_grants(AccountInterface $account, $operation) {
   if ($account->hasPermission('access private content')) {
     $grants['example'] = [1];
   }
@@ -148,7 +149,7 @@ function hook_node_grants(\Drupal\Core\Session\AccountInterface $account, $opera
  * @see hook_node_access_records_alter()
  * @ingroup node_access
  */
-function hook_node_access_records(\Drupal\node\NodeInterface $node) {
+function hook_node_access_records(NodeInterface $node) {
   // We only care about the node if it has been marked private. If not, it is
   // treated just like any other node and we completely ignore it.
   if ($node->private->value) {
@@ -216,7 +217,7 @@ function hook_node_access_records(\Drupal\node\NodeInterface $node) {
  * @see hook_node_grants_alter()
  * @ingroup node_access
  */
-function hook_node_access_records_alter(&$grants, \Drupal\node\NodeInterface $node) {
+function hook_node_access_records_alter(&$grants, NodeInterface $node) {
   // Our module allows editors to mark specific articles with the 'is_preview'
   // field. If the node being saved has a TRUE value for that field, then only
   // our grants are retained, and other grants are removed. Doing so ensures
@@ -261,7 +262,7 @@ function hook_node_access_records_alter(&$grants, \Drupal\node\NodeInterface $no
  * @see hook_node_access_records_alter()
  * @ingroup node_access
  */
-function hook_node_grants_alter(&$grants, \Drupal\Core\Session\AccountInterface $account, $operation) {
+function hook_node_grants_alter(&$grants, AccountInterface $account, $operation) {
   // Our sample module never allows certain roles to edit or delete
   // content. Since some other node access modules might allow this
   // permission, we expressly remove it by returning an empty $grants
@@ -300,7 +301,7 @@ function hook_node_grants_alter(&$grants, \Drupal\Core\Session\AccountInterface
  *
  * @ingroup entity_crud
  */
-function hook_node_search_result(\Drupal\node\NodeInterface $node) {
+function hook_node_search_result(NodeInterface $node) {
   $rating = \Drupal::database()->query('SELECT SUM([points]) FROM {my_rating} WHERE [nid] = :nid', ['nid' => $node->id()])->fetchField();
   return ['rating' => \Drupal::translation()->formatPlural($rating, '1 point', '@count points')];
 }
@@ -319,7 +320,7 @@ function hook_node_search_result(\Drupal\node\NodeInterface $node) {
  *
  * @ingroup entity_crud
  */
-function hook_node_update_index(\Drupal\node\NodeInterface $node) {
+function hook_node_update_index(NodeInterface $node) {
   $text = '';
   $ratings = \Drupal::database()->query('SELECT [title], [description] FROM {my_ratings} WHERE [nid] = :nid', [':nid' => $node->id()]);
   foreach ($ratings as $rating) {
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 23e16d221e31f2fce86caaac8a8a2a670dcb137d..e1516c529154f84ac51a37ecdab0f09f749560bc 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -2,149 +2,24 @@
 
 /**
  * @file
- * The core module that allows content to be submitted to the site.
- *
- * Modules and scripts may programmatically submit nodes using the usual form
- * API pattern.
  */
 
 use Drupal\Component\Utility\Environment;
-use Drupal\Component\Utility\Xss;
-use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Batch\BatchBuilder;
 use Drupal\Core\Cache\Cache;
-use Drupal\Core\Database\Query\AlterableInterface;
-use Drupal\Core\Database\Query\SelectInterface;
 use Drupal\Core\Database\StatementInterface;
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Link;
 use Drupal\Core\Render\Element;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Url;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
-use Drupal\language\ConfigurableLanguageInterface;
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
 use Drupal\node\NodeInterface;
 use Drupal\node\NodeTypeInterface;
-use Drupal\node\Form\NodePreviewForm;
-
-/**
- * Implements hook_help().
- */
-function node_help($route_name, RouteMatchInterface $route_match) {
-  // Remind site administrators about the {node_access} table being flagged
-  // for rebuild. We don't need to issue the message on the confirm form, or
-  // while the rebuild is being processed.
-  if ($route_name != 'node.configure_rebuild_confirm' && $route_name != 'system.batch_page.html' && $route_name != 'help.page.node' && $route_name != 'help.main'
-    && \Drupal::currentUser()->hasPermission('administer nodes') && node_access_needs_rebuild()) {
-    if ($route_name == 'system.status') {
-      $message = t('The content access permissions need to be rebuilt.');
-    }
-    else {
-      $message = t('The content access permissions need to be rebuilt. <a href=":node_access_rebuild">Rebuild permissions</a>.', [':node_access_rebuild' => Url::fromRoute('node.configure_rebuild_confirm')->toString()]);
-    }
-    \Drupal::messenger()->addError($message);
-  }
-
-  switch ($route_name) {
-    case 'help.page.node':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Node module manages the creation, editing, deletion, settings, and display of the main site content. Content items managed by the Node module are typically displayed as pages on your site, and include a title, some meta-data (author, creation time, content type, etc.), and optional fields containing text or other data (fields are managed by the <a href=":field">Field module</a>). For more information, see the <a href=":node">online documentation for the Node module</a>.', [':node' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/node-module', ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Creating content') . '</dt>';
-      $output .= '<dd>' . t('When new content is created, the Node module records basic information about the content, including the author, date of creation, and the <a href=":content-type">Content type</a>. It also manages the <em>publishing options</em>, which define whether or not the content is published, promoted to the front page of the site, and/or sticky at the top of content lists. Default settings can be configured for each <a href=":content-type">type of content</a> on your site.', [':content-type' => Url::fromRoute('entity.node_type.collection')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Creating custom content types') . '</dt>';
-      $output .= '<dd>' . t('The Node module gives users with the <em>Administer content types</em> permission the ability to <a href=":content-new">create new content types</a> in addition to the default ones already configured. Creating custom content types gives you the flexibility to add <a href=":field">fields</a> and configure default settings that suit the differing needs of various site content.', [':content-new' => Url::fromRoute('node.type_add')->toString(), ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Administering content') . '</dt>';
-      $output .= '<dd>' . t('The <a href=":content">Content</a> page lists your content, allowing you add new content, filter, edit or delete existing content, or perform bulk operations on existing content.', [':content' => Url::fromRoute('system.admin_content')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Creating revisions') . '</dt>';
-      $output .= '<dd>' . t('The Node module also enables you to create multiple versions of any content, and revert to older versions using the <em>Revision information</em> settings.') . '</dd>';
-      $output .= '<dt>' . t('User permissions') . '</dt>';
-      $output .= '<dd>' . t('The Node module makes a number of permissions available for each content type, which can be set by role on the <a href=":permissions">permissions page</a>.', [':permissions' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'node'])->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'node.type_add':
-      return '<p>' . t('Individual content types can have different fields, behaviors, and permissions assigned to them.') . '</p>';
-
-    case 'entity.entity_form_display.node.default':
-    case 'entity.entity_form_display.node.form_mode':
-      $type = $route_match->getParameter('node_type');
-      return '<p>' . t('Content items can be edited using different form modes. Here, you can define which fields are shown and hidden when %type content is edited in each form mode, and define how the field form widgets are displayed in each form mode.', ['%type' => $type->label()]) . '</p>';
-
-    case 'entity.entity_view_display.node.default':
-    case 'entity.entity_view_display.node.view_mode':
-      $type = $route_match->getParameter('node_type');
-      return '<p>' . t('Content items can be displayed using different view modes: Teaser, Full content, Print, RSS, etc. <em>Teaser</em> is a short format that is typically used in lists of multiple content items. <em>Full content</em> is typically used when the content is displayed on its own page.') . '</p>' .
-        '<p>' . t('Here, you can define which fields are shown and hidden when %type content is displayed in each view mode, and define how the fields are displayed in each view mode.', ['%type' => $type->label()]) . '</p>';
-
-    case 'entity.node.version_history':
-      return '<p>' . t('Revisions allow you to track differences between multiple versions of your content, and revert to older versions.') . '</p>';
-
-    case 'entity.node.edit_form':
-      $node = $route_match->getParameter('node');
-      $type = NodeType::load($node->getType());
-      $help = $type->getHelp();
-      return (!empty($help) ? Xss::filterAdmin($help) : '');
-
-    case 'node.add':
-      $type = $route_match->getParameter('node_type');
-      $help = $type->getHelp();
-      return (!empty($help) ? Xss::filterAdmin($help) : '');
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function node_theme(): array {
-  return [
-    'node' => [
-      'render element' => 'elements',
-    ],
-    'node_add_list' => [
-      'variables' => ['content' => NULL],
-    ],
-    'node_edit_form' => [
-      'render element' => 'form',
-    ],
-    // @todo Delete the next three entries as part of
-    // https://www.drupal.org/node/3015623
-    'field__node__title' => [
-      'base hook' => 'field',
-    ],
-    'field__node__uid' => [
-      'base hook' => 'field',
-    ],
-    'field__node__created' => [
-      'base hook' => 'field',
-    ],
-  ];
-}
-
-/**
- * Implements hook_entity_view_display_alter().
- */
-function node_entity_view_display_alter(EntityViewDisplayInterface $display, $context) {
-  if ($context['entity_type'] == 'node') {
-    // Hide field labels in search index.
-    if ($context['view_mode'] == 'search_index') {
-      foreach ($display->getComponents() as $name => $options) {
-        if (isset($options['label'])) {
-          $options['label'] = 'hidden';
-          $display->setComponent($name, $options);
-        }
-      }
-    }
-  }
-}
 
 /**
  * Gathers a listing of links to nodes.
@@ -177,17 +52,6 @@ function node_title_list(StatementInterface $result, $title = NULL) {
   return $num_rows ? ['#theme' => 'item_list__node', '#items' => $items, '#title' => $title, '#cache' => ['tags' => Cache::mergeTags(['node_list'], Cache::buildTags('node', $nids))]] : FALSE;
 }
 
-/**
- * Implements hook_local_tasks_alter().
- */
-function node_local_tasks_alter(&$local_tasks): void {
-  // Removes 'Revisions' local task added by deriver. Local task
-  // 'entity.node.version_history' will be replaced by
-  // 'entity.version_history:node.version_history' after
-  // https://www.drupal.org/project/drupal/issues/3153559.
-  unset($local_tasks['entity.version_history:node.version_history']);
-}
-
 /**
  * Determines the type of marker to be displayed for a given node.
  *
@@ -318,24 +182,6 @@ function node_add_body_field(NodeTypeInterface $type, $label = 'Body') {
   return $field;
 }
 
-/**
- * Implements hook_entity_extra_field_info().
- */
-function node_entity_extra_field_info() {
-  $extra = [];
-  $description = t('Node module element');
-  foreach (NodeType::loadMultiple() as $bundle) {
-    $extra['node'][$bundle->id()]['display']['links'] = [
-      'label' => t('Links'),
-      'description' => $description,
-      'weight' => 100,
-      'visible' => TRUE,
-    ];
-  }
-
-  return $extra;
-}
-
 /**
  * Checks whether the current page is the full page view of the passed-in node.
  *
@@ -548,128 +394,6 @@ function template_preprocess_node(&$variables) {
 
 }
 
-/**
- * Implements hook_cron().
- */
-function node_cron() {
-  // Calculate the oldest and newest node created times, for use in search
-  // rankings. (Note that field aliases have to be variables passed by
-  // reference.)
-  if (\Drupal::moduleHandler()->moduleExists('search')) {
-    $min_alias = 'min_created';
-    $max_alias = 'max_created';
-    $result = \Drupal::entityQueryAggregate('node')
-      ->accessCheck(FALSE)
-      ->aggregate('created', 'MIN', NULL, $min_alias)
-      ->aggregate('created', 'MAX', NULL, $max_alias)
-      ->execute();
-    if (isset($result[0])) {
-      // Make an array with definite keys and store it in the state system.
-      $array = [
-        'min_created' => $result[0][$min_alias],
-        'max_created' => $result[0][$max_alias],
-      ];
-      \Drupal::state()->set('node.min_max_update_time', $array);
-    }
-  }
-}
-
-/**
- * Implements hook_ranking().
- */
-function node_ranking() {
-  // Create the ranking array and add the basic ranking options.
-  $ranking = [
-    'relevance' => [
-      'title' => t('Keyword relevance'),
-      // Average relevance values hover around 0.15
-      'score' => 'i.relevance',
-    ],
-    'sticky' => [
-      'title' => t('Content is sticky at top of lists'),
-      // The sticky flag is either 0 or 1, which is automatically normalized.
-      'score' => 'n.sticky',
-    ],
-    'promote' => [
-      'title' => t('Content is promoted to the front page'),
-      // The promote flag is either 0 or 1, which is automatically normalized.
-      'score' => 'n.promote',
-    ],
-  ];
-  // Add relevance based on updated date, but only if it the scale values have
-  // been calculated in node_cron().
-  if ($node_min_max = \Drupal::state()->get('node.min_max_update_time')) {
-    $ranking['recent'] = [
-      'title' => t('Recently created'),
-      // Exponential decay with half life of 14% of the age range of nodes.
-      'score' => 'EXP(-5 * (1 - (n.created - :node_oldest) / :node_range))',
-      'arguments' => [
-        ':node_oldest' => $node_min_max['min_created'],
-        ':node_range' => max($node_min_max['max_created'] - $node_min_max['min_created'], 1),
-      ],
-    ];
-  }
-  return $ranking;
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for user entities.
- */
-function node_user_predelete($account) {
-  // Delete nodes (current revisions).
-  // @todo Introduce node_mass_delete() or make node_mass_update() more flexible.
-  $nids = \Drupal::entityQuery('node')
-    ->condition('uid', $account->id())
-    ->accessCheck(FALSE)
-    ->execute();
-  // Delete old revisions.
-  $storage_controller = \Drupal::entityTypeManager()->getStorage('node');
-  $nodes = $storage_controller->loadMultiple($nids);
-  $storage_controller->delete($nodes);
-  $revisions = $storage_controller->userRevisionIds($account);
-  foreach ($revisions as $revision) {
-    $storage_controller->deleteRevision($revision);
-  }
-}
-
-/**
- * Implements hook_page_top().
- */
-function node_page_top(array &$page_top) {
-  // Add 'Back to content editing' link on preview page.
-  $route_match = \Drupal::routeMatch();
-  if ($route_match->getRouteName() == 'entity.node.preview') {
-    $page_top['node_preview'] = [
-      '#type' => 'container',
-      '#attributes' => [
-        'class' => ['node-preview-container', 'container-inline'],
-      ],
-      'view_mode' => \Drupal::formBuilder()->getForm(
-        NodePreviewForm::class,
-        $route_match->getParameter('node_preview')
-      ),
-    ];
-
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- *
- * Alters the theme form to use the admin theme on node editing.
- *
- * @see node_form_system_themes_admin_form_submit()
- */
-function node_form_system_themes_admin_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  $form['admin_theme']['use_admin_theme'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Use the administration theme when editing or creating content'),
-    '#description' => t('Control which roles can "View the administration theme" on the <a href=":permissions">Permissions page</a>.', [':permissions' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'system'])->toString()]),
-    '#default_value' => \Drupal::configFactory()->getEditable('node.settings')->get('use_admin_theme'),
-  ];
-  $form['#submit'][] = 'node_form_system_themes_admin_form_submit';
-}
-
 /**
  * Form submission handler for system_themes_admin_form().
  *
@@ -681,87 +405,6 @@ function node_form_system_themes_admin_form_submit($form, FormStateInterface $fo
     ->save();
 }
 
-/**
- * @defgroup node_access Node access rights
- * @{
- * The node access system determines who can do what to which nodes.
- *
- * In determining access rights for an existing node,
- * \Drupal\node\NodeAccessControlHandler first checks whether the user has the
- * "bypass node access" permission. Such users have unrestricted access to all
- * nodes. user 1 will always pass this check.
- *
- * Next, all implementations of hook_ENTITY_TYPE_access() for node will
- * be called. Each implementation may explicitly allow, explicitly forbid, or
- * ignore the access request. If at least one module says to forbid the request,
- * it will be rejected. If no modules deny the request and at least one says to
- * allow it, the request will be permitted.
- *
- * If all modules ignore the access request, then the node_access table is used
- * to determine access. All node access modules are queried using
- * hook_node_grants() to assemble a list of "grant IDs" for the user. This list
- * is compared against the table. If any row contains the node ID in question
- * (or 0, which stands for "all nodes"), one of the grant IDs returned, and a
- * value of TRUE for the operation in question, then access is granted. Note
- * that this table is a list of grants; any matching row is sufficient to grant
- * access to the node.
- *
- * In node listings (lists of nodes generated from a select query, such as the
- * default home page at path 'node', an RSS feed, a recent content block, etc.),
- * the process above is followed except that hook_ENTITY_TYPE_access() is not
- * called on each node for performance reasons and for proper functioning of
- * the pager system. When adding a node listing to your module, be sure to use
- * an entity query, which will add a tag of "node_access". This will allow
- * modules dealing with node access to ensure only nodes to which the user has
- * access are retrieved, through the use of hook_query_TAG_alter(). See the
- * @link entity_api Entity API topic @endlink for more information on entity
- * queries. Tagging a query with "node_access" does not check the
- * published/unpublished status of nodes, so the base query is responsible
- * for ensuring that unpublished nodes are not displayed to inappropriate users.
- *
- * Note: Even a single module returning an AccessResultInterface object from
- * hook_ENTITY_TYPE_access() whose isForbidden() method equals TRUE will block
- * access to the node. Therefore, implementers should take care to not deny
- * access unless they really intend to. Unless a module wishes to actively
- * forbid access it should return an AccessResultInterface object whose
- * isAllowed() nor isForbidden() methods return TRUE, to allow other modules or
- * the node_access table to control access.
- *
- * Note also that access to create nodes is handled by
- * hook_ENTITY_TYPE_create_access().
- *
- * @see \Drupal\node\NodeAccessControlHandler
- */
-
-/**
- * Implements hook_ENTITY_TYPE_access().
- */
-function node_node_access(NodeInterface $node, $operation, AccountInterface $account) {
-  $type = $node->bundle();
-
-  // Note create access is handled by hook_ENTITY_TYPE_create_access().
-  switch ($operation) {
-    case 'update':
-      $access = AccessResult::allowedIfHasPermission($account, 'edit any ' . $type . ' content');
-      if (!$access->isAllowed() && $account->hasPermission('edit own ' . $type . ' content')) {
-        $access = $access->orIf(AccessResult::allowedIf($account->id() == $node->getOwnerId())->cachePerUser()->addCacheableDependency($node));
-      }
-      break;
-
-    case 'delete':
-      $access = AccessResult::allowedIfHasPermission($account, 'delete any ' . $type . ' content');
-      if (!$access->isAllowed() && $account->hasPermission('delete own ' . $type . ' content')) {
-        $access = $access->orIf(AccessResult::allowedIf($account->id() == $node->getOwnerId()))->cachePerUser()->addCacheableDependency($node);
-      }
-      break;
-
-    default:
-      $access = AccessResult::neutral();
-  }
-
-  return $access;
-}
-
 /**
  * Fetches an array of permission IDs granted to the given user ID.
  *
@@ -836,87 +479,6 @@ function node_access_view_all_nodes($account = NULL) {
   return $access[$account->id()];
 }
 
-/**
- * Implements hook_query_TAG_alter().
- *
- * This is the hook_query_alter() for queries tagged with 'node_access'. It adds
- * node access checks for the user account given by the 'account' meta-data (or
- * current user if not provided), for an operation given by the 'op' meta-data
- * (or 'view' if not provided; other possible values are 'update' and 'delete').
- *
- * Queries tagged with 'node_access' that are not against the {node} table
- * must add the base table as metadata. For example:
- * @code
- *   $query
- *     ->addTag('node_access')
- *     ->addMetaData('base_table', 'taxonomy_index');
- * @endcode
- */
-function node_query_node_access_alter(AlterableInterface $query) {
-  // Read meta-data from query, if provided.
-  if (!$account = $query->getMetaData('account')) {
-    $account = \Drupal::currentUser();
-  }
-  if (!$op = $query->getMetaData('op')) {
-    $op = 'view';
-  }
-
-  // If $account can bypass node access, or there are no node access modules,
-  // or the operation is 'view' and the $account has a global view grant
-  // (such as a view grant for node ID 0), we don't need to alter the query.
-  if ($account->hasPermission('bypass node access')) {
-    return;
-  }
-  if (!\Drupal::moduleHandler()->hasImplementations('node_grants')) {
-    return;
-  }
-  if ($op == 'view' && node_access_view_all_nodes($account)) {
-    return;
-  }
-
-  $tables = $query->getTables();
-  $base_table = $query->getMetaData('base_table');
-  // If the base table is not given, default to one of the node base tables.
-  if (!$base_table) {
-    /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
-    $table_mapping = \Drupal::entityTypeManager()->getStorage('node')->getTableMapping();
-    $node_base_tables = $table_mapping->getTableNames();
-
-    foreach ($tables as $table_info) {
-      if (!($table_info instanceof SelectInterface)) {
-        $table = $table_info['table'];
-        // Ensure that 'node' and 'node_field_data' are always preferred over
-        // 'node_revision' and 'node_field_revision'.
-        if ($table == 'node' || $table == 'node_field_data') {
-          $base_table = $table;
-          break;
-        }
-        // If one of the node base tables are in the query, add it to the list
-        // of possible base tables to join against.
-        if (in_array($table, $node_base_tables)) {
-          $base_table = $table;
-        }
-      }
-    }
-
-    // Bail out if the base table is missing.
-    if (!$base_table) {
-      throw new Exception('Query tagged for node access but there is no node table, specify the base_table using meta data.');
-    }
-  }
-
-  // Update the query for the given storage method.
-  \Drupal::service('node.grant_storage')->alterQuery($query, $tables, $op, $account, $base_table);
-
-  // Bubble the 'user.node_grants:$op' cache context to the current render
-  // context.
-  $renderer = \Drupal::service('renderer');
-  if ($renderer->hasRenderContext()) {
-    $build = ['#cache' => ['contexts' => ['user.node_grants:' . $op]]];
-    $renderer->render($build);
-  }
-}
-
 /**
  * Toggles or reads the value of a flag for rebuilding the node access grants.
  *
@@ -1100,53 +662,6 @@ function _node_access_rebuild_batch_finished($success, $results, $operations) {
   }
 }
 
-/**
- * @} End of "defgroup node_access".
- */
-
-/**
- * Implements hook_modules_installed().
- */
-function node_modules_installed(array $modules) {
-  // Check if any of the newly enabled modules require the node_access table to
-  // be rebuilt.
-  if (!node_access_needs_rebuild() && \Drupal::moduleHandler()->hasImplementations('node_grants', $modules)) {
-    node_access_needs_rebuild(TRUE);
-  }
-}
-
-/**
- * Implements hook_modules_uninstalled().
- */
-function node_modules_uninstalled($modules) {
-  // Check whether any of the disabled modules implemented hook_node_grants(),
-  // in which case the node access table needs to be rebuilt.
-  foreach ($modules as $module) {
-    // At this point, the module is already disabled, but its code is still
-    // loaded in memory. Module functions must no longer be called. We only
-    // check whether a hook implementation function exists and do not invoke it.
-    // Node access also needs to be rebuilt if language module is disabled to
-    // remove any language-specific grants.
-    if (!node_access_needs_rebuild() && (\Drupal::moduleHandler()->hasImplementations('node_grants', $module) || $module == 'language')) {
-      node_access_needs_rebuild(TRUE);
-    }
-  }
-
-  // If there remains no more node_access module, rebuilding will be
-  // straightforward, we can do it right now.
-  if (node_access_needs_rebuild() && !\Drupal::moduleHandler()->hasImplementations('node_grants')) {
-    node_access_rebuild();
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for 'configurable_language'.
- */
-function node_configurable_language_delete(ConfigurableLanguageInterface $language) {
-  // On nodes with this language, unset the language.
-  \Drupal::entityTypeManager()->getStorage('node')->clearRevisionsLanguage($language);
-}
-
 /**
  * Marks a node to be re-indexed by the node_search plugin.
  *
@@ -1159,40 +674,3 @@ function node_reindex_node_search($nid) {
     \Drupal::service('search.index')->markForReindex('node_search', $nid);
   }
 }
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for comment entities.
- */
-function node_comment_insert($comment) {
-  // Reindex the node when comments are added.
-  if ($comment->getCommentedEntityTypeId() == 'node') {
-    node_reindex_node_search($comment->getCommentedEntityId());
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for comment entities.
- */
-function node_comment_update($comment) {
-  // Reindex the node when comments are changed.
-  if ($comment->getCommentedEntityTypeId() == 'node') {
-    node_reindex_node_search($comment->getCommentedEntityId());
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for comment entities.
- */
-function node_comment_delete($comment) {
-  // Reindex the node when comments are deleted.
-  if ($comment->getCommentedEntityTypeId() == 'node') {
-    node_reindex_node_search($comment->getCommentedEntityId());
-  }
-}
-
-/**
- * Implements hook_config_translation_info_alter().
- */
-function node_config_translation_info_alter(&$info) {
-  $info['node_type']['class'] = 'Drupal\node\ConfigTranslation\NodeTypeMapper';
-}
diff --git a/core/modules/node/node.tokens.inc b/core/modules/node/node.tokens.inc
deleted file mode 100644
index bf07de83451440e748606cc5a6b9898f1021e344..0000000000000000000000000000000000000000
--- a/core/modules/node/node.tokens.inc
+++ /dev/null
@@ -1,229 +0,0 @@
-<?php
-
-/**
- * @file
- * Builds placeholder replacement tokens for node-related data.
- */
-
-use Drupal\Core\Datetime\Entity\DateFormat;
-use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Render\BubbleableMetadata;
-use Drupal\user\Entity\User;
-
-/**
- * Implements hook_token_info().
- */
-function node_token_info() {
-  $type = [
-    'name' => t('Nodes'),
-    'description' => t('Tokens related to individual content items, or "nodes".'),
-    'needs-data' => 'node',
-  ];
-
-  // Core tokens for nodes.
-  $node['nid'] = [
-    'name' => t("Content ID"),
-    'description' => t('The unique ID of the content item, or "node".'),
-  ];
-  $node['uuid'] = [
-    'name' => t('UUID'),
-    'description' => t('The UUID of the content item, or "node".'),
-  ];
-  $node['vid'] = [
-    'name' => t("Revision ID"),
-    'description' => t("The unique ID of the node's latest revision."),
-  ];
-  $node['type'] = [
-    'name' => t("Content type"),
-  ];
-  $node['type-name'] = [
-    'name' => t("Content type name"),
-    'description' => t("The human-readable name of the node type."),
-  ];
-  $node['title'] = [
-    'name' => t("Title"),
-  ];
-  $node['body'] = [
-    'name' => t("Body"),
-    'description' => t("The main body text of the node."),
-  ];
-  $node['summary'] = [
-    'name' => t("Summary"),
-    'description' => t("The summary of the node's main body text."),
-  ];
-  $node['langcode'] = [
-    'name' => t('Language code'),
-    'description' => t('The language code of the language the node is written in.'),
-  ];
-  $node['published_status'] = [
-    'name' => t('Published'),
-    'description' => t('The publication status of the node ("Published" or "Unpublished").'),
-  ];
-  $node['url'] = [
-    'name' => t("URL"),
-    'description' => t("The URL of the node."),
-  ];
-  $node['edit-url'] = [
-    'name' => t("Edit URL"),
-    'description' => t("The URL of the node's edit page."),
-  ];
-
-  // Chained tokens for nodes.
-  $node['created'] = [
-    'name' => t("Date created"),
-    'type' => 'date',
-  ];
-  $node['changed'] = [
-    'name' => t("Date changed"),
-    'description' => t("The date the node was most recently updated."),
-    'type' => 'date',
-  ];
-  $node['author'] = [
-    'name' => t("Author"),
-    'type' => 'user',
-  ];
-
-  return [
-    'types' => ['node' => $type],
-    'tokens' => ['node' => $node],
-  ];
-}
-
-/**
- * Implements hook_tokens().
- */
-function node_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
-  $token_service = \Drupal::token();
-
-  $url_options = ['absolute' => TRUE];
-  if (isset($options['langcode'])) {
-    $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
-    $langcode = $options['langcode'];
-  }
-  else {
-    $langcode = LanguageInterface::LANGCODE_DEFAULT;
-  }
-  $replacements = [];
-
-  if ($type == 'node' && !empty($data['node'])) {
-    /** @var \Drupal\node\NodeInterface $node */
-    $node = $data['node'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Simple key values on the node.
-        case 'nid':
-          $replacements[$original] = $node->id();
-          break;
-
-        case 'uuid':
-          $replacements[$original] = $node->uuid();
-          break;
-
-        case 'vid':
-          $replacements[$original] = $node->getRevisionId();
-          break;
-
-        case 'type':
-          $replacements[$original] = $node->getType();
-          break;
-
-        case 'type-name':
-          $type_name = node_get_type_label($node);
-          $replacements[$original] = $type_name;
-          break;
-
-        case 'title':
-          $replacements[$original] = $node->getTitle();
-          break;
-
-        case 'body':
-        case 'summary':
-          $translation = \Drupal::service('entity.repository')->getTranslationFromContext($node, $langcode, ['operation' => 'node_tokens']);
-          if ($translation->hasField('body') && ($items = $translation->get('body')) && !$items->isEmpty()) {
-            $item = $items[0];
-            // If the summary was requested and is not empty, use it.
-            if ($name == 'summary' && !empty($item->summary)) {
-              $output = $item->summary_processed;
-            }
-            // Attempt to provide a suitable version of the 'body' field.
-            else {
-              $output = $item->processed;
-              // A summary was requested.
-              if ($name == 'summary') {
-                // Generate an optionally trimmed summary of the body field.
-
-                // Get the 'trim_length' size used for the 'teaser' mode, if
-                // present, or use the default trim_length size.
-                $display_options = \Drupal::service('entity_display.repository')
-                  ->getViewDisplay('node', $node->getType(), 'teaser')
-                  ->getComponent('body');
-                if (isset($display_options['settings']['trim_length'])) {
-                  $length = $display_options['settings']['trim_length'];
-                }
-                else {
-                  $settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings('text_summary_or_trimmed');
-                  $length = $settings['trim_length'];
-                }
-
-                $output = text_summary($output, $item->format, $length);
-              }
-            }
-            // "processed" returns a \Drupal\Component\Render\MarkupInterface
-            // via check_markup().
-            $replacements[$original] = $output;
-          }
-          break;
-
-        case 'langcode':
-          $replacements[$original] = $node->language()->getId();
-          break;
-
-        case 'published_status':
-          $replacements[$original] = $node->isPublished() ? t('Published') : t('Unpublished');
-          break;
-
-        case 'url':
-          $replacements[$original] = $node->toUrl('canonical', $url_options)->toString();
-          break;
-
-        case 'edit-url':
-          $replacements[$original] = $node->toUrl('edit-form', $url_options)->toString();
-          break;
-
-        // Default values for the chained tokens handled below.
-        case 'author':
-          $account = $node->getOwner() ? $node->getOwner() : User::load(0);
-          $bubbleable_metadata->addCacheableDependency($account);
-          $replacements[$original] = $account->label();
-          break;
-
-        case 'created':
-          $date_format = DateFormat::load('medium');
-          $bubbleable_metadata->addCacheableDependency($date_format);
-          $replacements[$original] = \Drupal::service('date.formatter')->format($node->getCreatedTime(), 'medium', '', NULL, $langcode);
-          break;
-
-        case 'changed':
-          $date_format = DateFormat::load('medium');
-          $bubbleable_metadata->addCacheableDependency($date_format);
-          $replacements[$original] = \Drupal::service('date.formatter')->format($node->getChangedTime(), 'medium', '', NULL, $langcode);
-          break;
-      }
-    }
-
-    if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) {
-      $replacements += $token_service->generate('user', $author_tokens, ['user' => $node->getOwner()], $options, $bubbleable_metadata);
-    }
-
-    if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) {
-      $replacements += $token_service->generate('date', $created_tokens, ['date' => $node->getCreatedTime()], $options, $bubbleable_metadata);
-    }
-
-    if ($changed_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
-      $replacements += $token_service->generate('date', $changed_tokens, ['date' => $node->getChangedTime()], $options, $bubbleable_metadata);
-    }
-  }
-
-  return $replacements;
-}
diff --git a/core/modules/node/node.views.inc b/core/modules/node/node.views.inc
deleted file mode 100644
index 28f545271208ea1df249bee3acce718cd923cb1c..0000000000000000000000000000000000000000
--- a/core/modules/node/node.views.inc
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views data for node.module.
- */
-
-use Drupal\user\RoleInterface;
-use Drupal\views\ViewExecutable;
-use Drupal\user\Entity\Role;
-use Drupal\views\Analyzer;
-
-/**
- * Implements hook_views_analyze().
- */
-function node_views_analyze(ViewExecutable $view) {
-  $ret = [];
-  // Check for something other than the default display:
-  if ($view->storage->get('base_table') == 'node') {
-    foreach ($view->displayHandlers as $display) {
-      if (!$display->isDefaulted('access') || !$display->isDefaulted('filters')) {
-        // Check for no access control
-        $access = $display->getOption('access');
-        if (empty($access['type']) || $access['type'] == 'none') {
-          $anonymous_role = Role::load(RoleInterface::ANONYMOUS_ID);
-          $anonymous_has_access = $anonymous_role && $anonymous_role->hasPermission('access content');
-          $authenticated_role = Role::load(RoleInterface::AUTHENTICATED_ID);
-          $authenticated_has_access = $authenticated_role && $authenticated_role->hasPermission('access content');
-          if (!$anonymous_has_access || !$authenticated_has_access) {
-            $ret[] = Analyzer::formatMessage(t('Some roles lack permission to access content, but display %display has no access control.', ['%display' => $display->display['display_title']]), 'warning');
-          }
-          $filters = $display->getOption('filters');
-          foreach ($filters as $filter) {
-            if ($filter['table'] == 'node' && ($filter['field'] == 'status' || $filter['field'] == 'status_extra')) {
-              continue 2;
-            }
-          }
-          $ret[] = Analyzer::formatMessage(t('Display %display has no access control but does not contain a filter for published nodes.', ['%display' => $display->display['display_title']]), 'warning');
-        }
-      }
-    }
-  }
-  foreach ($view->displayHandlers as $display) {
-    if ($display->getPluginId() == 'page') {
-      if ($display->getOption('path') == 'node/%') {
-        $ret[] = Analyzer::formatMessage(t('Display %display has set node/% as path. This will not produce what you want. If you want to have multiple versions of the node view, use Layout Builder.', ['%display' => $display->display['display_title']]), 'warning');
-      }
-    }
-  }
-
-  return $ret;
-}
diff --git a/core/modules/node/node.views_execution.inc b/core/modules/node/node.views_execution.inc
deleted file mode 100644
index f5401b17b4a123e94c6d3c82474dd08ce710005a..0000000000000000000000000000000000000000
--- a/core/modules/node/node.views_execution.inc
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views runtime hooks for node.module.
- */
-
-use Drupal\views\ViewExecutable;
-
-/**
- * Implements hook_views_query_substitutions().
- */
-function node_views_query_substitutions(ViewExecutable $view) {
-  $account = \Drupal::currentUser();
-  return [
-    '***ADMINISTER_NODES***' => intval($account->hasPermission('administer nodes')),
-    '***VIEW_OWN_UNPUBLISHED_NODES***' => intval($account->hasPermission('view own unpublished content')),
-    '***BYPASS_NODE_ACCESS***' => intval($account->hasPermission('bypass node access')),
-  ];
-}
diff --git a/core/modules/node/src/Hook/NodeHooks1.php b/core/modules/node/src/Hook/NodeHooks1.php
new file mode 100644
index 0000000000000000000000000000000000000000..5f1f885c67f7289c2b636f9cf57c0862bbcb97fe
--- /dev/null
+++ b/core/modules/node/src/Hook/NodeHooks1.php
@@ -0,0 +1,554 @@
+<?php
+
+namespace Drupal\node\Hook;
+
+use Drupal\language\ConfigurableLanguageInterface;
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\node\NodeInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\node\Form\NodePreviewForm;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Component\Utility\Xss;
+use Drupal\node\Entity\NodeType;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for node.
+ */
+class NodeHooks1 {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    // Remind site administrators about the {node_access} table being flagged
+    // for rebuild. We don't need to issue the message on the confirm form, or
+    // while the rebuild is being processed.
+    if ($route_name != 'node.configure_rebuild_confirm' && $route_name != 'system.batch_page.html' && $route_name != 'help.page.node' && $route_name != 'help.main' && \Drupal::currentUser()->hasPermission('administer nodes') && node_access_needs_rebuild()) {
+      if ($route_name == 'system.status') {
+        $message = t('The content access permissions need to be rebuilt.');
+      }
+      else {
+        $message = t('The content access permissions need to be rebuilt. <a href=":node_access_rebuild">Rebuild permissions</a>.', [
+          ':node_access_rebuild' => Url::fromRoute('node.configure_rebuild_confirm')->toString(),
+        ]);
+      }
+      \Drupal::messenger()->addError($message);
+    }
+    switch ($route_name) {
+      case 'help.page.node':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Node module manages the creation, editing, deletion, settings, and display of the main site content. Content items managed by the Node module are typically displayed as pages on your site, and include a title, some meta-data (author, creation time, content type, etc.), and optional fields containing text or other data (fields are managed by the <a href=":field">Field module</a>). For more information, see the <a href=":node">online documentation for the Node module</a>.', [
+          ':node' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/node-module',
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Creating content') . '</dt>';
+        $output .= '<dd>' . t('When new content is created, the Node module records basic information about the content, including the author, date of creation, and the <a href=":content-type">Content type</a>. It also manages the <em>publishing options</em>, which define whether or not the content is published, promoted to the front page of the site, and/or sticky at the top of content lists. Default settings can be configured for each <a href=":content-type">type of content</a> on your site.', [
+          ':content-type' => Url::fromRoute('entity.node_type.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Creating custom content types') . '</dt>';
+        $output .= '<dd>' . t('The Node module gives users with the <em>Administer content types</em> permission the ability to <a href=":content-new">create new content types</a> in addition to the default ones already configured. Creating custom content types gives you the flexibility to add <a href=":field">fields</a> and configure default settings that suit the differing needs of various site content.', [
+          ':content-new' => Url::fromRoute('node.type_add')->toString(),
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Administering content') . '</dt>';
+        $output .= '<dd>' . t('The <a href=":content">Content</a> page lists your content, allowing you add new content, filter, edit or delete existing content, or perform bulk operations on existing content.', [':content' => Url::fromRoute('system.admin_content')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Creating revisions') . '</dt>';
+        $output .= '<dd>' . t('The Node module also enables you to create multiple versions of any content, and revert to older versions using the <em>Revision information</em> settings.') . '</dd>';
+        $output .= '<dt>' . t('User permissions') . '</dt>';
+        $output .= '<dd>' . t('The Node module makes a number of permissions available for each content type, which can be set by role on the <a href=":permissions">permissions page</a>.', [
+          ':permissions' => Url::fromRoute('user.admin_permissions.module', [
+            'modules' => 'node',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'node.type_add':
+        return '<p>' . t('Individual content types can have different fields, behaviors, and permissions assigned to them.') . '</p>';
+
+      case 'entity.entity_form_display.node.default':
+      case 'entity.entity_form_display.node.form_mode':
+        $type = $route_match->getParameter('node_type');
+        return '<p>' . t('Content items can be edited using different form modes. Here, you can define which fields are shown and hidden when %type content is edited in each form mode, and define how the field form widgets are displayed in each form mode.', ['%type' => $type->label()]) . '</p>';
+
+      case 'entity.entity_view_display.node.default':
+      case 'entity.entity_view_display.node.view_mode':
+        $type = $route_match->getParameter('node_type');
+        return '<p>' . t('Content items can be displayed using different view modes: Teaser, Full content, Print, RSS, etc. <em>Teaser</em> is a short format that is typically used in lists of multiple content items. <em>Full content</em> is typically used when the content is displayed on its own page.') . '</p>' . '<p>' . t('Here, you can define which fields are shown and hidden when %type content is displayed in each view mode, and define how the fields are displayed in each view mode.', ['%type' => $type->label()]) . '</p>';
+
+      case 'entity.node.version_history':
+        return '<p>' . t('Revisions allow you to track differences between multiple versions of your content, and revert to older versions.') . '</p>';
+
+      case 'entity.node.edit_form':
+        $node = $route_match->getParameter('node');
+        $type = NodeType::load($node->getType());
+        $help = $type->getHelp();
+        return !empty($help) ? Xss::filterAdmin($help) : '';
+
+      case 'node.add':
+        $type = $route_match->getParameter('node_type');
+        $help = $type->getHelp();
+        return !empty($help) ? Xss::filterAdmin($help) : '';
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'node' => [
+        'render element' => 'elements',
+      ],
+      'node_add_list' => [
+        'variables' => [
+          'content' => NULL,
+        ],
+      ],
+      'node_edit_form' => [
+        'render element' => 'form',
+      ],
+          // @todo Delete the next three entries as part of
+          // https://www.drupal.org/node/3015623
+      'field__node__title' => [
+        'base hook' => 'field',
+      ],
+      'field__node__uid' => [
+        'base hook' => 'field',
+      ],
+      'field__node__created' => [
+        'base hook' => 'field',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_entity_view_display_alter().
+   */
+  #[Hook('entity_view_display_alter')]
+  public function entityViewDisplayAlter(EntityViewDisplayInterface $display, $context) {
+    if ($context['entity_type'] == 'node') {
+      // Hide field labels in search index.
+      if ($context['view_mode'] == 'search_index') {
+        foreach ($display->getComponents() as $name => $options) {
+          if (isset($options['label'])) {
+            $options['label'] = 'hidden';
+            $display->setComponent($name, $options);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_local_tasks_alter().
+   */
+  #[Hook('local_tasks_alter')]
+  public function localTasksAlter(&$local_tasks) : void {
+    // Removes 'Revisions' local task added by deriver. Local task
+    // 'entity.node.version_history' will be replaced by
+    // 'entity.version_history:node.version_history' after
+    // https://www.drupal.org/project/drupal/issues/3153559.
+    unset($local_tasks['entity.version_history:node.version_history']);
+  }
+
+  /**
+   * Implements hook_entity_extra_field_info().
+   */
+  #[Hook('entity_extra_field_info')]
+  public function entityExtraFieldInfo() {
+    $extra = [];
+    $description = t('Node module element');
+    foreach (NodeType::loadMultiple() as $bundle) {
+      $extra['node'][$bundle->id()]['display']['links'] = [
+        'label' => t('Links'),
+        'description' => $description,
+        'weight' => 100,
+        'visible' => TRUE,
+      ];
+    }
+    return $extra;
+  }
+
+  /**
+   * Implements hook_cron().
+   */
+  #[Hook('cron')]
+  public function cron() {
+    // Calculate the oldest and newest node created times, for use in search
+    // rankings. (Note that field aliases have to be variables passed by
+    // reference.)
+    if (\Drupal::moduleHandler()->moduleExists('search')) {
+      $min_alias = 'min_created';
+      $max_alias = 'max_created';
+      $result = \Drupal::entityQueryAggregate('node')->accessCheck(FALSE)->aggregate('created', 'MIN', NULL, $min_alias)->aggregate('created', 'MAX', NULL, $max_alias)->execute();
+      if (isset($result[0])) {
+        // Make an array with definite keys and store it in the state system.
+        $array = ['min_created' => $result[0][$min_alias], 'max_created' => $result[0][$max_alias]];
+        \Drupal::state()->set('node.min_max_update_time', $array);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ranking().
+   */
+  #[Hook('ranking')]
+  public function ranking() {
+    // Create the ranking array and add the basic ranking options.
+    $ranking = [
+      'relevance' => [
+        'title' => t('Keyword relevance'),
+              // Average relevance values hover around 0.15
+        'score' => 'i.relevance',
+      ],
+      'sticky' => [
+        'title' => t('Content is sticky at top of lists'),
+              // The sticky flag is either 0 or 1, which is automatically normalized.
+        'score' => 'n.sticky',
+      ],
+      'promote' => [
+        'title' => t('Content is promoted to the front page'),
+              // The promote flag is either 0 or 1, which is automatically normalized.
+        'score' => 'n.promote',
+      ],
+    ];
+    // Add relevance based on updated date, but only if it the scale values have
+    // been calculated in node_cron().
+    if ($node_min_max = \Drupal::state()->get('node.min_max_update_time')) {
+      $ranking['recent'] = [
+        'title' => t('Recently created'),
+            // Exponential decay with half life of 14% of the age range of nodes.
+        'score' => 'EXP(-5 * (1 - (n.created - :node_oldest) / :node_range))',
+        'arguments' => [
+          ':node_oldest' => $node_min_max['min_created'],
+          ':node_range' => max($node_min_max['max_created'] - $node_min_max['min_created'], 1),
+        ],
+      ];
+    }
+    return $ranking;
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for user entities.
+   */
+  #[Hook('user_predelete')]
+  public function userPredelete($account) {
+    // Delete nodes (current revisions).
+    // @todo Introduce node_mass_delete() or make node_mass_update() more flexible.
+    $nids = \Drupal::entityQuery('node')->condition('uid', $account->id())->accessCheck(FALSE)->execute();
+    // Delete old revisions.
+    $storage_controller = \Drupal::entityTypeManager()->getStorage('node');
+    $nodes = $storage_controller->loadMultiple($nids);
+    $storage_controller->delete($nodes);
+    $revisions = $storage_controller->userRevisionIds($account);
+    foreach ($revisions as $revision) {
+      $storage_controller->deleteRevision($revision);
+    }
+  }
+
+  /**
+   * Implements hook_page_top().
+   */
+  #[Hook('page_top')]
+  public function pageTop(array &$page_top) {
+    // Add 'Back to content editing' link on preview page.
+    $route_match = \Drupal::routeMatch();
+    if ($route_match->getRouteName() == 'entity.node.preview') {
+      $page_top['node_preview'] = [
+        '#type' => 'container',
+        '#attributes' => [
+          'class' => [
+            'node-preview-container',
+            'container-inline',
+          ],
+        ],
+        'view_mode' => \Drupal::formBuilder()->getForm(NodePreviewForm::class, $route_match->getParameter('node_preview')),
+      ];
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   *
+   * Alters the theme form to use the admin theme on node editing.
+   *
+   * @see node_form_system_themes_admin_form_submit()
+   */
+  #[Hook('form_system_themes_admin_form_alter')]
+  public function formSystemThemesAdminFormAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    $form['admin_theme']['use_admin_theme'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Use the administration theme when editing or creating content'),
+      '#description' => t('Control which roles can "View the administration theme" on the <a href=":permissions">Permissions page</a>.', [
+        ':permissions' => Url::fromRoute('user.admin_permissions.module', [
+          'modules' => 'system',
+        ])->toString(),
+      ]),
+      '#default_value' => \Drupal::configFactory()->getEditable('node.settings')->get('use_admin_theme'),
+    ];
+    $form['#submit'][] = 'node_form_system_themes_admin_form_submit';
+  }
+
+  /**
+   * @defgroup node_access Node access rights
+   * @{
+   * The node access system determines who can do what to which nodes.
+   *
+   * In determining access rights for an existing node,
+   * \Drupal\node\NodeAccessControlHandler first checks whether the user has the
+   * "bypass node access" permission. Such users have unrestricted access to all
+   * nodes. user 1 will always pass this check.
+   *
+   * Next, all implementations of hook_ENTITY_TYPE_access() for node will
+   * be called. Each implementation may explicitly allow, explicitly forbid, or
+   * ignore the access request. If at least one module says to forbid the request,
+   * it will be rejected. If no modules deny the request and at least one says to
+   * allow it, the request will be permitted.
+   *
+   * If all modules ignore the access request, then the node_access table is used
+   * to determine access. All node access modules are queried using
+   * hook_node_grants() to assemble a list of "grant IDs" for the user. This list
+   * is compared against the table. If any row contains the node ID in question
+   * (or 0, which stands for "all nodes"), one of the grant IDs returned, and a
+   * value of TRUE for the operation in question, then access is granted. Note
+   * that this table is a list of grants; any matching row is sufficient to grant
+   * access to the node.
+   *
+   * In node listings (lists of nodes generated from a select query, such as the
+   * default home page at path 'node', an RSS feed, a recent content block, etc.),
+   * the process above is followed except that hook_ENTITY_TYPE_access() is not
+   * called on each node for performance reasons and for proper functioning of
+   * the pager system. When adding a node listing to your module, be sure to use
+   * an entity query, which will add a tag of "node_access". This will allow
+   * modules dealing with node access to ensure only nodes to which the user has
+   * access are retrieved, through the use of hook_query_TAG_alter(). See the
+   * @link entity_api Entity API topic @endlink for more information on entity
+   * queries. Tagging a query with "node_access" does not check the
+   * published/unpublished status of nodes, so the base query is responsible
+   * for ensuring that unpublished nodes are not displayed to inappropriate users.
+   *
+   * Note: Even a single module returning an AccessResultInterface object from
+   * hook_ENTITY_TYPE_access() whose isForbidden() method equals TRUE will block
+   * access to the node. Therefore, implementers should take care to not deny
+   * access unless they really intend to. Unless a module wishes to actively
+   * forbid access it should return an AccessResultInterface object whose
+   * isAllowed() nor isForbidden() methods return TRUE, to allow other modules or
+   * the node_access table to control access.
+   *
+   * Note also that access to create nodes is handled by
+   * hook_ENTITY_TYPE_create_access().
+   *
+   * @see \Drupal\node\NodeAccessControlHandler
+   */
+
+  /**
+   * Implements hook_ENTITY_TYPE_access().
+   */
+  #[Hook('node_access')]
+  public function nodeAccess(NodeInterface $node, $operation, AccountInterface $account) {
+    $type = $node->bundle();
+    // Note create access is handled by hook_ENTITY_TYPE_create_access().
+    switch ($operation) {
+      case 'update':
+        $access = AccessResult::allowedIfHasPermission($account, 'edit any ' . $type . ' content');
+        if (!$access->isAllowed() && $account->hasPermission('edit own ' . $type . ' content')) {
+          $access = $access->orIf(AccessResult::allowedIf($account->id() == $node->getOwnerId())->cachePerUser()->addCacheableDependency($node));
+        }
+        break;
+
+      case 'delete':
+        $access = AccessResult::allowedIfHasPermission($account, 'delete any ' . $type . ' content');
+        if (!$access->isAllowed() && $account->hasPermission('delete own ' . $type . ' content')) {
+          $access = $access->orIf(AccessResult::allowedIf($account->id() == $node->getOwnerId()))->cachePerUser()->addCacheableDependency($node);
+        }
+        break;
+
+      default:
+        $access = AccessResult::neutral();
+    }
+    return $access;
+  }
+
+  /**
+   * Implements hook_query_TAG_alter().
+   *
+   * This is the hook_query_alter() for queries tagged with 'node_access'. It adds
+   * node access checks for the user account given by the 'account' meta-data (or
+   * current user if not provided), for an operation given by the 'op' meta-data
+   * (or 'view' if not provided; other possible values are 'update' and 'delete').
+   *
+   * Queries tagged with 'node_access' that are not against the {node} table
+   * must add the base table as metadata. For example:
+   * @code
+   *   $query
+   *     ->addTag('node_access')
+   *     ->addMetaData('base_table', 'taxonomy_index');
+   * @endcode
+   */
+  #[Hook('query_node_access_alter')]
+  public function queryNodeAccessAlter(AlterableInterface $query) {
+    // Read meta-data from query, if provided.
+    if (!($account = $query->getMetaData('account'))) {
+      $account = \Drupal::currentUser();
+    }
+    if (!($op = $query->getMetaData('op'))) {
+      $op = 'view';
+    }
+    // If $account can bypass node access, or there are no node access modules,
+    // or the operation is 'view' and the $account has a global view grant
+    // (such as a view grant for node ID 0), we don't need to alter the query.
+    if ($account->hasPermission('bypass node access')) {
+      return;
+    }
+    if (!\Drupal::moduleHandler()->hasImplementations('node_grants')) {
+      return;
+    }
+    if ($op == 'view' && node_access_view_all_nodes($account)) {
+      return;
+    }
+    $tables = $query->getTables();
+    $base_table = $query->getMetaData('base_table');
+    // If the base table is not given, default to one of the node base tables.
+    if (!$base_table) {
+      /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
+      $table_mapping = \Drupal::entityTypeManager()->getStorage('node')->getTableMapping();
+      $node_base_tables = $table_mapping->getTableNames();
+      foreach ($tables as $table_info) {
+        if (!$table_info instanceof SelectInterface) {
+          $table = $table_info['table'];
+          // Ensure that 'node' and 'node_field_data' are always preferred over
+          // 'node_revision' and 'node_field_revision'.
+          if ($table == 'node' || $table == 'node_field_data') {
+            $base_table = $table;
+            break;
+          }
+          // If one of the node base tables are in the query, add it to the list
+          // of possible base tables to join against.
+          if (in_array($table, $node_base_tables)) {
+            $base_table = $table;
+          }
+        }
+      }
+      // Bail out if the base table is missing.
+      if (!$base_table) {
+        throw new \Exception('Query tagged for node access but there is no node table, specify the base_table using meta data.');
+      }
+    }
+    // Update the query for the given storage method.
+    \Drupal::service('node.grant_storage')->alterQuery($query, $tables, $op, $account, $base_table);
+    // Bubble the 'user.node_grants:$op' cache context to the current render
+    // context.
+    $renderer = \Drupal::service('renderer');
+    if ($renderer->hasRenderContext()) {
+      $build = ['#cache' => ['contexts' => ['user.node_grants:' . $op]]];
+      $renderer->render($build);
+    }
+  }
+
+  /**
+   * @} End of "defgroup node_access".
+   */
+
+  /**
+   * Implements hook_modules_installed().
+   */
+  #[Hook('modules_installed')]
+  public function modulesInstalled(array $modules) {
+    // Check if any of the newly enabled modules require the node_access table to
+    // be rebuilt.
+    if (!node_access_needs_rebuild() && \Drupal::moduleHandler()->hasImplementations('node_grants', $modules)) {
+      node_access_needs_rebuild(TRUE);
+    }
+  }
+
+  /**
+   * Implements hook_modules_uninstalled().
+   */
+  #[Hook('modules_uninstalled')]
+  public function modulesUninstalled($modules) {
+    // Check whether any of the disabled modules implemented hook_node_grants(),
+    // in which case the node access table needs to be rebuilt.
+    foreach ($modules as $module) {
+      // At this point, the module is already disabled, but its code is still
+      // loaded in memory. Module functions must no longer be called. We only
+      // check whether a hook implementation function exists and do not invoke it.
+      // Node access also needs to be rebuilt if language module is disabled to
+      // remove any language-specific grants.
+      if (!node_access_needs_rebuild() && (\Drupal::moduleHandler()->hasImplementations('node_grants', $module) || $module == 'language')) {
+        node_access_needs_rebuild(TRUE);
+      }
+    }
+    // If there remains no more node_access module, rebuilding will be
+    // straightforward, we can do it right now.
+    if (node_access_needs_rebuild() && !\Drupal::moduleHandler()->hasImplementations('node_grants')) {
+      node_access_rebuild();
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for 'configurable_language'.
+   */
+  #[Hook('configurable_language_delete')]
+  public function configurableLanguageDelete(ConfigurableLanguageInterface $language) {
+    // On nodes with this language, unset the language.
+    \Drupal::entityTypeManager()->getStorage('node')->clearRevisionsLanguage($language);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for comment entities.
+   */
+  #[Hook('comment_insert')]
+  public function commentInsert($comment) {
+    // Reindex the node when comments are added.
+    if ($comment->getCommentedEntityTypeId() == 'node') {
+      node_reindex_node_search($comment->getCommentedEntityId());
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for comment entities.
+   */
+  #[Hook('comment_update')]
+  public function commentUpdate($comment) {
+    // Reindex the node when comments are changed.
+    if ($comment->getCommentedEntityTypeId() == 'node') {
+      node_reindex_node_search($comment->getCommentedEntityId());
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for comment entities.
+   */
+  #[Hook('comment_delete')]
+  public function commentDelete($comment) {
+    // Reindex the node when comments are deleted.
+    if ($comment->getCommentedEntityTypeId() == 'node') {
+      node_reindex_node_search($comment->getCommentedEntityId());
+    }
+  }
+
+  /**
+   * Implements hook_config_translation_info_alter().
+   */
+  #[Hook('config_translation_info_alter')]
+  public function configTranslationInfoAlter(&$info) {
+    $info['node_type']['class'] = 'Drupal\node\ConfigTranslation\NodeTypeMapper';
+  }
+
+}
diff --git a/core/modules/node/src/Hook/NodeTokensHooks.php b/core/modules/node/src/Hook/NodeTokensHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..fa3709fa157b2f99cd27d327fb4b916f9affa650
--- /dev/null
+++ b/core/modules/node/src/Hook/NodeTokensHooks.php
@@ -0,0 +1,199 @@
+<?php
+
+namespace Drupal\node\Hook;
+
+use Drupal\Core\Datetime\Entity\DateFormat;
+use Drupal\user\Entity\User;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for node.
+ */
+class NodeTokensHooks {
+
+  /**
+   * Implements hook_token_info().
+   */
+  #[Hook('token_info')]
+  public function tokenInfo() {
+    $type = [
+      'name' => t('Nodes'),
+      'description' => t('Tokens related to individual content items, or "nodes".'),
+      'needs-data' => 'node',
+    ];
+    // Core tokens for nodes.
+    $node['nid'] = [
+      'name' => t("Content ID"),
+      'description' => t('The unique ID of the content item, or "node".'),
+    ];
+    $node['uuid'] = [
+      'name' => t('UUID'),
+      'description' => t('The UUID of the content item, or "node".'),
+    ];
+    $node['vid'] = [
+      'name' => t("Revision ID"),
+      'description' => t("The unique ID of the node's latest revision."),
+    ];
+    $node['type'] = ['name' => t("Content type")];
+    $node['type-name'] = [
+      'name' => t("Content type name"),
+      'description' => t("The human-readable name of the node type."),
+    ];
+    $node['title'] = ['name' => t("Title")];
+    $node['body'] = ['name' => t("Body"), 'description' => t("The main body text of the node.")];
+    $node['summary'] = [
+      'name' => t("Summary"),
+      'description' => t("The summary of the node's main body text."),
+    ];
+    $node['langcode'] = [
+      'name' => t('Language code'),
+      'description' => t('The language code of the language the node is written in.'),
+    ];
+    $node['published_status'] = [
+      'name' => t('Published'),
+      'description' => t('The publication status of the node ("Published" or "Unpublished").'),
+    ];
+    $node['url'] = ['name' => t("URL"), 'description' => t("The URL of the node.")];
+    $node['edit-url'] = ['name' => t("Edit URL"), 'description' => t("The URL of the node's edit page.")];
+    // Chained tokens for nodes.
+    $node['created'] = ['name' => t("Date created"), 'type' => 'date'];
+    $node['changed'] = [
+      'name' => t("Date changed"),
+      'description' => t("The date the node was most recently updated."),
+      'type' => 'date',
+    ];
+    $node['author'] = ['name' => t("Author"), 'type' => 'user'];
+    return ['types' => ['node' => $type], 'tokens' => ['node' => $node]];
+  }
+
+  /**
+   * Implements hook_tokens().
+   */
+  #[Hook('tokens')]
+  public function tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+    $token_service = \Drupal::token();
+    $url_options = ['absolute' => TRUE];
+    if (isset($options['langcode'])) {
+      $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
+      $langcode = $options['langcode'];
+    }
+    else {
+      $langcode = LanguageInterface::LANGCODE_DEFAULT;
+    }
+    $replacements = [];
+    if ($type == 'node' && !empty($data['node'])) {
+      /** @var \Drupal\node\NodeInterface $node */
+      $node = $data['node'];
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          // Simple key values on the node.
+          case 'nid':
+            $replacements[$original] = $node->id();
+            break;
+
+          case 'uuid':
+            $replacements[$original] = $node->uuid();
+            break;
+
+          case 'vid':
+            $replacements[$original] = $node->getRevisionId();
+            break;
+
+          case 'type':
+            $replacements[$original] = $node->getType();
+            break;
+
+          case 'type-name':
+            $type_name = node_get_type_label($node);
+            $replacements[$original] = $type_name;
+            break;
+
+          case 'title':
+            $replacements[$original] = $node->getTitle();
+            break;
+
+          case 'body':
+          case 'summary':
+            $translation = \Drupal::service('entity.repository')->getTranslationFromContext($node, $langcode, ['operation' => 'node_tokens']);
+            if ($translation->hasField('body') && ($items = $translation->get('body')) && !$items->isEmpty()) {
+              $item = $items[0];
+              // If the summary was requested and is not empty, use it.
+              if ($name == 'summary' && !empty($item->summary)) {
+                $output = $item->summary_processed;
+              }
+              else {
+                $output = $item->processed;
+                // A summary was requested.
+                if ($name == 'summary') {
+                  // Generate an optionally trimmed summary of the body field.
+                  // Get the 'trim_length' size used for the 'teaser' mode, if
+                  // present, or use the default trim_length size.
+                  $display_options = \Drupal::service('entity_display.repository')->getViewDisplay('node', $node->getType(), 'teaser')->getComponent('body');
+                  if (isset($display_options['settings']['trim_length'])) {
+                    $length = $display_options['settings']['trim_length'];
+                  }
+                  else {
+                    $settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings('text_summary_or_trimmed');
+                    $length = $settings['trim_length'];
+                  }
+                  $output = text_summary($output, $item->format, $length);
+                }
+              }
+              // "processed" returns a \Drupal\Component\Render\MarkupInterface
+              // via check_markup().
+              $replacements[$original] = $output;
+            }
+            break;
+
+          case 'langcode':
+            $replacements[$original] = $node->language()->getId();
+            break;
+
+          case 'published_status':
+            $replacements[$original] = $node->isPublished() ? t('Published') : t('Unpublished');
+            break;
+
+          case 'url':
+            $replacements[$original] = $node->toUrl('canonical', $url_options)->toString();
+            break;
+
+          case 'edit-url':
+            $replacements[$original] = $node->toUrl('edit-form', $url_options)->toString();
+            break;
+
+          // Default values for the chained tokens handled below.
+          case 'author':
+            $account = $node->getOwner() ? $node->getOwner() : User::load(0);
+            $bubbleable_metadata->addCacheableDependency($account);
+            $replacements[$original] = $account->label();
+            break;
+
+          case 'created':
+            $date_format = DateFormat::load('medium');
+            $bubbleable_metadata->addCacheableDependency($date_format);
+            $replacements[$original] = \Drupal::service('date.formatter')->format($node->getCreatedTime(), 'medium', '', NULL, $langcode);
+            break;
+
+          case 'changed':
+            $date_format = DateFormat::load('medium');
+            $bubbleable_metadata->addCacheableDependency($date_format);
+            $replacements[$original] = \Drupal::service('date.formatter')->format($node->getChangedTime(), 'medium', '', NULL, $langcode);
+            break;
+        }
+      }
+      if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) {
+        $replacements += $token_service->generate('user', $author_tokens, ['user' => $node->getOwner()], $options, $bubbleable_metadata);
+      }
+      if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) {
+        $replacements += $token_service->generate('date', $created_tokens, ['date' => $node->getCreatedTime()], $options, $bubbleable_metadata);
+      }
+      if ($changed_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
+        $replacements += $token_service->generate('date', $changed_tokens, ['date' => $node->getChangedTime()], $options, $bubbleable_metadata);
+      }
+    }
+    return $replacements;
+  }
+
+}
diff --git a/core/modules/node/src/Hook/NodeViewsExecutionHooks.php b/core/modules/node/src/Hook/NodeViewsExecutionHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..fad4ca5db8f52835b77b5566301456d04b77361a
--- /dev/null
+++ b/core/modules/node/src/Hook/NodeViewsExecutionHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\node\Hook;
+
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for node.
+ */
+class NodeViewsExecutionHooks {
+
+  /**
+   * Implements hook_views_query_substitutions().
+   */
+  #[Hook('views_query_substitutions')]
+  public function viewsQuerySubstitutions(ViewExecutable $view) {
+    $account = \Drupal::currentUser();
+    return [
+      '***ADMINISTER_NODES***' => intval($account->hasPermission('administer nodes')),
+      '***VIEW_OWN_UNPUBLISHED_NODES***' => intval($account->hasPermission('view own unpublished content')),
+      '***BYPASS_NODE_ACCESS***' => intval($account->hasPermission('bypass node access')),
+    ];
+  }
+
+}
diff --git a/core/modules/node/src/Hook/NodeViewsHooks.php b/core/modules/node/src/Hook/NodeViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f801756c0cb8bddd5a635788e4f69247a6a6a920
--- /dev/null
+++ b/core/modules/node/src/Hook/NodeViewsHooks.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\node\Hook;
+
+use Drupal\views\Analyzer;
+use Drupal\user\RoleInterface;
+use Drupal\user\Entity\Role;
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for node.
+ */
+class NodeViewsHooks {
+
+  /**
+   * Implements hook_views_analyze().
+   */
+  #[Hook('views_analyze')]
+  public function viewsAnalyze(ViewExecutable $view) {
+    $ret = [];
+    // Check for something other than the default display:
+    if ($view->storage->get('base_table') == 'node') {
+      foreach ($view->displayHandlers as $display) {
+        if (!$display->isDefaulted('access') || !$display->isDefaulted('filters')) {
+          // Check for no access control
+          $access = $display->getOption('access');
+          if (empty($access['type']) || $access['type'] == 'none') {
+            $anonymous_role = Role::load(RoleInterface::ANONYMOUS_ID);
+            $anonymous_has_access = $anonymous_role && $anonymous_role->hasPermission('access content');
+            $authenticated_role = Role::load(RoleInterface::AUTHENTICATED_ID);
+            $authenticated_has_access = $authenticated_role && $authenticated_role->hasPermission('access content');
+            if (!$anonymous_has_access || !$authenticated_has_access) {
+              $ret[] = Analyzer::formatMessage(t('Some roles lack permission to access content, but display %display has no access control.', ['%display' => $display->display['display_title']]), 'warning');
+            }
+            $filters = $display->getOption('filters');
+            foreach ($filters as $filter) {
+              if ($filter['table'] == 'node' && ($filter['field'] == 'status' || $filter['field'] == 'status_extra')) {
+                continue 2;
+              }
+            }
+            $ret[] = Analyzer::formatMessage(t('Display %display has no access control but does not contain a filter for published nodes.', ['%display' => $display->display['display_title']]), 'warning');
+          }
+        }
+      }
+    }
+    foreach ($view->displayHandlers as $display) {
+      if ($display->getPluginId() == 'page') {
+        if ($display->getOption('path') == 'node/%') {
+          $ret[] = Analyzer::formatMessage(t('Display %display has set node/% as path. This will not produce what you want. If you want to have multiple versions of the node view, use Layout Builder.', ['%display' => $display->display['display_title']]), 'warning');
+        }
+      }
+    }
+    return $ret;
+  }
+
+}
diff --git a/core/modules/node/src/ProxyClass/ParamConverter/NodePreviewConverter.php b/core/modules/node/src/ProxyClass/ParamConverter/NodePreviewConverter.php
index 7dcd43e7cae6a5c1c718f8a362475e11e44c3c35..9e040cf07f928f9a4b45e65098789c8658a034a8 100644
--- a/core/modules/node/src/ProxyClass/ParamConverter/NodePreviewConverter.php
+++ b/core/modules/node/src/ProxyClass/ParamConverter/NodePreviewConverter.php
@@ -7,15 +7,20 @@
 
 namespace Drupal\node\ProxyClass\ParamConverter {
 
+    use Drupal\Core\ParamConverter\ParamConverterInterface;
+    use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+    use Symfony\Component\DependencyInjection\ContainerInterface;
+    use Symfony\Component\Routing\Route;
+
     /**
      * Provides a proxy class for \Drupal\node\ParamConverter\NodePreviewConverter.
      *
      * @see \Drupal\Component\ProxyBuilder
      */
-    class NodePreviewConverter implements \Drupal\Core\ParamConverter\ParamConverterInterface
+    class NodePreviewConverter implements ParamConverterInterface
     {
 
-        use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
+        use DependencySerializationTrait;
 
         /**
          * The id of the original proxied service.
@@ -46,7 +51,7 @@ class NodePreviewConverter implements \Drupal\Core\ParamConverter\ParamConverter
          * @param string $drupal_proxy_original_service_id
          *   The service ID of the original service.
          */
-        public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
+        public function __construct(ContainerInterface $container, $drupal_proxy_original_service_id)
         {
             $this->container = $container;
             $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
@@ -78,7 +83,7 @@ public function convert($value, $definition, $name, array $defaults)
         /**
          * {@inheritdoc}
          */
-        public function applies($definition, $name, \Symfony\Component\Routing\Route $route)
+        public function applies($definition, $name, Route $route)
         {
             return $this->lazyLoadItself()->applies($definition, $name, $route);
         }
diff --git a/core/modules/node/tests/modules/node_access_test/node_access_test.module b/core/modules/node/tests/modules/node_access_test/node_access_test.module
index 301da43072f24f833535761b2cc55abedd9bc42c..e6af0dbe82add2acb8d6f12add44b7edbcb79e66 100644
--- a/core/modules/node/tests/modules/node_access_test/node_access_test.module
+++ b/core/modules/node/tests/modules/node_access_test/node_access_test.module
@@ -21,96 +21,9 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Access\AccessResult;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\node\NodeTypeInterface;
-use Drupal\node\NodeInterface;
-
-/**
- * Implements hook_node_grants().
- *
- * Provides three grant realms:
- * - node_access_test_author: Grants users view, update, and delete privileges
- *   on nodes they have authored. Users receive a group ID matching their user
- *   ID on this realm.
- * - node_access_test: Grants users view privileges when they have the
- *   'node test view' permission. Users with this permission receive two group
- *   IDs for the realm, 8888 and 8889. Access for both realms is identical;
- *   the second group is added so that the interaction of multiple groups on
- *   a given grant realm can be tested in NodeAccessPagerTest.
- * - node_access_all: Provides grants for the user whose user ID matches the
- *   'node_access_test.no_access_uid' state variable. Access control on this
- *   realm is not provided in this module; instead,
- *   NodeQueryAlterTest::testNodeQueryAlterOverride() manually writes a node
- *   access record defining the access control for this realm.
- *
- * @see \Drupal\node\Tests\NodeQueryAlterTest::testNodeQueryAlterOverride()
- * @see \Drupal\node\Tests\NodeAccessPagerTest
- * @see node_access_test.permissions.yml
- * @see node_access_test_node_access_records()
- */
-function node_access_test_node_grants($account, $operation) {
-  $grants = [];
-  $grants['node_access_test_author'] = [$account->id()];
-  if ($operation == 'view' && $account->hasPermission('node test view')) {
-    $grants['node_access_test'] = [8888, 8889];
-  }
-
-  $no_access_uid = \Drupal::state()->get('node_access_test.no_access_uid', 0);
-  if ($operation == 'view' && $account->id() == $no_access_uid) {
-    $grants['node_access_all'] = [0];
-  }
-  return $grants;
-}
-
-/**
- * Implements hook_node_access_records().
- *
- * By default, records are written for all nodes. When the
- * 'node_access_test.private' state variable is set to TRUE, records
- * are only written for nodes with a "private" property set, which causes the
- * Node module to write the default global view grant for nodes that are not
- * marked private.
- *
- * @see \Drupal\node\Tests\NodeAccessBaseTableTest::setUp()
- * @see node_access_test_node_grants()
- * @see node_access_test.permissions.yml
- */
-function node_access_test_node_access_records(NodeInterface $node) {
-  $grants = [];
-  // For NodeAccessBaseTableTestCase, only set records for private nodes.
-  if (!\Drupal::state()->get('node_access_test.private') || (isset($node->private) && $node->private->value)) {
-    // Groups 8888 and 8889 for the node_access_test realm both receive a view
-    // grant for all controlled nodes. See node_access_test_node_grants().
-    $grants[] = [
-      'realm' => 'node_access_test',
-      'gid' => 8888,
-      'grant_view' => 1,
-      'grant_update' => 0,
-      'grant_delete' => 0,
-    ];
-    $grants[] = [
-      'realm' => 'node_access_test',
-      'gid' => 8889,
-      'grant_view' => 1,
-      'grant_update' => 0,
-      'grant_delete' => 0,
-    ];
-    // For the author realm, the group ID is equivalent to a user ID, which
-    // means there are many groups of just 1 user.
-    $grants[] = [
-      'realm' => 'node_access_test_author',
-      'gid' => $node->getOwnerId(),
-      'grant_view' => 1,
-      'grant_update' => 1,
-      'grant_delete' => 1,
-    ];
-  }
-
-  return $grants;
-}
 
 /**
  * Adds the private field to a node type.
@@ -141,23 +54,3 @@ function node_access_test_add_field(NodeTypeInterface $type) {
     ])
     ->save();
 }
-
-/**
- * Implements hook_ENTITY_TYPE_access().
- */
-function node_access_test_node_access(NodeInterface $node, $operation, AccountInterface $account) {
-  $secret_catalan = \Drupal::state()
-    ->get('node_access_test_secret_catalan') ?: 0;
-  if ($secret_catalan && $node->language()->getId() == 'ca') {
-    // Make all Catalan content secret.
-    return AccessResult::forbidden()->setCacheMaxAge(0);
-  }
-
-  // Grant access if a specific user is specified.
-  if (\Drupal::state()->get('node_access_test.allow_uid') === $account->id()) {
-    return AccessResult::allowed();
-  }
-
-  // No opinion.
-  return AccessResult::neutral()->setCacheMaxAge(0);
-}
diff --git a/core/modules/node/tests/modules/node_access_test/src/Hook/NodeAccessTestHooks.php b/core/modules/node/tests/modules/node_access_test/src/Hook/NodeAccessTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..40441ccbe889e84c855c1d09f171e7447003c0b0
--- /dev/null
+++ b/core/modules/node/tests/modules/node_access_test/src/Hook/NodeAccessTestHooks.php
@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\node_access_test\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\node\NodeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for node_access_test.
+ */
+class NodeAccessTestHooks {
+
+  /**
+   * Implements hook_node_grants().
+   *
+   * Provides three grant realms:
+   * - node_access_test_author: Grants users view, update, and delete privileges
+   *   on nodes they have authored. Users receive a group ID matching their user
+   *   ID on this realm.
+   * - node_access_test: Grants users view privileges when they have the
+   *   'node test view' permission. Users with this permission receive two group
+   *   IDs for the realm, 8888 and 8889. Access for both realms is identical;
+   *   the second group is added so that the interaction of multiple groups on
+   *   a given grant realm can be tested in NodeAccessPagerTest.
+   * - node_access_all: Provides grants for the user whose user ID matches the
+   *   'node_access_test.no_access_uid' state variable. Access control on this
+   *   realm is not provided in this module; instead,
+   *   NodeQueryAlterTest::testNodeQueryAlterOverride() manually writes a node
+   *   access record defining the access control for this realm.
+   *
+   * @see \Drupal\node\Tests\NodeQueryAlterTest::testNodeQueryAlterOverride()
+   * @see \Drupal\node\Tests\NodeAccessPagerTest
+   * @see node_access_test.permissions.yml
+   * @see node_access_test_node_access_records()
+   */
+  #[Hook('node_grants')]
+  public function nodeGrants($account, $operation) {
+    $grants = [];
+    $grants['node_access_test_author'] = [$account->id()];
+    if ($operation == 'view' && $account->hasPermission('node test view')) {
+      $grants['node_access_test'] = [8888, 8889];
+    }
+    $no_access_uid = \Drupal::state()->get('node_access_test.no_access_uid', 0);
+    if ($operation == 'view' && $account->id() == $no_access_uid) {
+      $grants['node_access_all'] = [0];
+    }
+    return $grants;
+  }
+
+  /**
+   * Implements hook_node_access_records().
+   *
+   * By default, records are written for all nodes. When the
+   * 'node_access_test.private' state variable is set to TRUE, records
+   * are only written for nodes with a "private" property set, which causes the
+   * Node module to write the default global view grant for nodes that are not
+   * marked private.
+   *
+   * @see \Drupal\node\Tests\NodeAccessBaseTableTest::setUp()
+   * @see node_access_test_node_grants()
+   * @see node_access_test.permissions.yml
+   */
+  #[Hook('node_access_records')]
+  public function nodeAccessRecords(NodeInterface $node) {
+    $grants = [];
+    // For NodeAccessBaseTableTestCase, only set records for private nodes.
+    if (!\Drupal::state()->get('node_access_test.private') || isset($node->private) && $node->private->value) {
+      // Groups 8888 and 8889 for the node_access_test realm both receive a view
+      // grant for all controlled nodes. See node_access_test_node_grants().
+      $grants[] = [
+        'realm' => 'node_access_test',
+        'gid' => 8888,
+        'grant_view' => 1,
+        'grant_update' => 0,
+        'grant_delete' => 0,
+      ];
+      $grants[] = [
+        'realm' => 'node_access_test',
+        'gid' => 8889,
+        'grant_view' => 1,
+        'grant_update' => 0,
+        'grant_delete' => 0,
+      ];
+      // For the author realm, the group ID is equivalent to a user ID, which
+      // means there are many groups of just 1 user.
+      $grants[] = [
+        'realm' => 'node_access_test_author',
+        'gid' => $node->getOwnerId(),
+        'grant_view' => 1,
+        'grant_update' => 1,
+        'grant_delete' => 1,
+      ];
+    }
+    return $grants;
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_access().
+   */
+  #[Hook('node_access')]
+  public function nodeAccess(NodeInterface $node, $operation, AccountInterface $account) {
+    $secret_catalan = \Drupal::state()->get('node_access_test_secret_catalan') ?: 0;
+    if ($secret_catalan && $node->language()->getId() == 'ca') {
+      // Make all Catalan content secret.
+      return AccessResult::forbidden()->setCacheMaxAge(0);
+    }
+    // Grant access if a specific user is specified.
+    if (\Drupal::state()->get('node_access_test.allow_uid') === $account->id()) {
+      return AccessResult::allowed();
+    }
+    // No opinion.
+    return AccessResult::neutral()->setCacheMaxAge(0);
+  }
+
+}
diff --git a/core/modules/node/tests/modules/node_access_test_empty/node_access_test_empty.module b/core/modules/node/tests/modules/node_access_test_empty/node_access_test_empty.module
deleted file mode 100644
index b775aaf242781da23f0a2811050b9dffee7e204b..0000000000000000000000000000000000000000
--- a/core/modules/node/tests/modules/node_access_test_empty/node_access_test_empty.module
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-/**
- * @file
- * Empty node access hook implementations.
- */
-
-declare(strict_types=1);
-
-use Drupal\node\NodeInterface;
-
-/**
- * Implements hook_node_grants().
- */
-function node_access_test_empty_node_grants($account, $operation) {
-  return [];
-}
-
-/**
- * Implements hook_node_access_records().
- */
-function node_access_test_empty_node_access_records(NodeInterface $node) {
-  return [];
-}
diff --git a/core/modules/node/tests/modules/node_access_test_empty/src/Hook/NodeAccessTestEmptyHooks.php b/core/modules/node/tests/modules/node_access_test_empty/src/Hook/NodeAccessTestEmptyHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..31e28793abe29b5bf557c2ba8ec8a46fd25a4326
--- /dev/null
+++ b/core/modules/node/tests/modules/node_access_test_empty/src/Hook/NodeAccessTestEmptyHooks.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\node_access_test_empty\Hook;
+
+use Drupal\node\NodeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for node_access_test_empty.
+ */
+class NodeAccessTestEmptyHooks {
+
+  /**
+   * Implements hook_node_grants().
+   */
+  #[Hook('node_grants')]
+  public function nodeGrants($account, $operation) {
+    return [];
+  }
+
+  /**
+   * Implements hook_node_access_records().
+   */
+  #[Hook('node_access_records')]
+  public function nodeAccessRecords(NodeInterface $node) {
+    return [];
+  }
+
+}
diff --git a/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.module b/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.module
deleted file mode 100644
index 50cb2b4c3dbc7e8f2195a2223bc856d07dfc1952..0000000000000000000000000000000000000000
--- a/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.module
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module with a language-aware node access implementation.
- *
- * The module adds a 'private' field to page nodes that allows each translation
- * of the node to be marked as private (viewable only by administrators).
- */
-
-declare(strict_types=1);
-
-use Drupal\node\NodeInterface;
-
-/**
- * Implements hook_node_grants().
- *
- * This module defines a single grant realm. All users belong to this group.
- */
-function node_access_test_language_node_grants($account, $operation) {
-  $grants['node_access_language_test'] = [7888];
-  return $grants;
-}
-
-/**
- * Implements hook_node_access_records().
- */
-function node_access_test_language_node_access_records(NodeInterface $node) {
-  $grants = [];
-
-  // Create grants for each translation of the node.
-  foreach ($node->getTranslationLanguages() as $langcode => $language) {
-    // If the translation is not marked as private, grant access.
-    $translation = $node->getTranslation($langcode);
-    $grants[] = [
-      'realm' => 'node_access_language_test',
-      'gid' => 7888,
-      'grant_view' => empty($translation->field_private->value) ? 1 : 0,
-      'grant_update' => 0,
-      'grant_delete' => 0,
-      'langcode' => $langcode,
-    ];
-  }
-  return $grants;
-}
diff --git a/core/modules/node/tests/modules/node_access_test_language/src/Hook/NodeAccessTestLanguageHooks.php b/core/modules/node/tests/modules/node_access_test_language/src/Hook/NodeAccessTestLanguageHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..8fc24398d301b57823bfcb5cbe4c83f2940f8e38
--- /dev/null
+++ b/core/modules/node/tests/modules/node_access_test_language/src/Hook/NodeAccessTestLanguageHooks.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\node_access_test_language\Hook;
+
+use Drupal\node\NodeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for node_access_test_language.
+ */
+class NodeAccessTestLanguageHooks {
+
+  /**
+   * Implements hook_node_grants().
+   *
+   * This module defines a single grant realm. All users belong to this group.
+   */
+  #[Hook('node_grants')]
+  public function nodeGrants($account, $operation) {
+    $grants['node_access_language_test'] = [7888];
+    return $grants;
+  }
+
+  /**
+   * Implements hook_node_access_records().
+   */
+  #[Hook('node_access_records')]
+  public function nodeAccessRecords(NodeInterface $node) {
+    $grants = [];
+    // Create grants for each translation of the node.
+    foreach ($node->getTranslationLanguages() as $langcode => $language) {
+      // If the translation is not marked as private, grant access.
+      $translation = $node->getTranslation($langcode);
+      $grants[] = [
+        'realm' => 'node_access_language_test',
+        'gid' => 7888,
+        'grant_view' => empty($translation->field_private->value) ? 1 : 0,
+        'grant_update' => 0,
+        'grant_delete' => 0,
+        'langcode' => $langcode,
+      ];
+    }
+    return $grants;
+  }
+
+}
diff --git a/core/modules/node/tests/modules/node_display_configurable_test/node_display_configurable_test.module b/core/modules/node/tests/modules/node_display_configurable_test/node_display_configurable_test.module
deleted file mode 100644
index 7abd116cad8a5b4f841f9086cdbe1c238ec4c4d4..0000000000000000000000000000000000000000
--- a/core/modules/node/tests/modules/node_display_configurable_test/node_display_configurable_test.module
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-/**
- * @file
- * A module for testing making node base fields' displays configurable.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityTypeInterface;
-
-/**
- * Implements hook_entity_base_field_info_alter().
- */
-function node_display_configurable_test_entity_base_field_info_alter(&$base_field_definitions, EntityTypeInterface $entity_type) {
-  if ($entity_type->id() == 'node') {
-    foreach (['created', 'uid', 'title'] as $field) {
-      /** @var \Drupal\Core\Field\BaseFieldDefinition[] $base_field_definitions */
-      $base_field_definitions[$field]->setDisplayConfigurable('view', TRUE);
-    }
-  }
-}
-
-/**
- * Implements hook_entity_type_build().
- */
-function node_display_configurable_test_entity_type_build(array &$entity_types) {
-  // Allow skipping of extra preprocessing for configurable display.
-  $entity_types['node']->set('enable_base_field_custom_preprocess_skipping', TRUE);
-  $entity_types['node']->set('enable_page_title_template', TRUE);
-}
diff --git a/core/modules/node/tests/modules/node_display_configurable_test/src/Hook/NodeDisplayConfigurableTestHooks.php b/core/modules/node/tests/modules/node_display_configurable_test/src/Hook/NodeDisplayConfigurableTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..12bff8f1c6155b485c44e28c1ab83988246598f9
--- /dev/null
+++ b/core/modules/node/tests/modules/node_display_configurable_test/src/Hook/NodeDisplayConfigurableTestHooks.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\node_display_configurable_test\Hook;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for node_display_configurable_test.
+ */
+class NodeDisplayConfigurableTestHooks {
+
+  /**
+   * Implements hook_entity_base_field_info_alter().
+   */
+  #[Hook('entity_base_field_info_alter')]
+  public function entityBaseFieldInfoAlter(&$base_field_definitions, EntityTypeInterface $entity_type) {
+    if ($entity_type->id() == 'node') {
+      foreach (['created', 'uid', 'title'] as $field) {
+        /** @var \Drupal\Core\Field\BaseFieldDefinition[] $base_field_definitions */
+        $base_field_definitions[$field]->setDisplayConfigurable('view', TRUE);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public function entityTypeBuild(array &$entity_types) {
+    // Allow skipping of extra preprocessing for configurable display.
+    $entity_types['node']->set('enable_base_field_custom_preprocess_skipping', TRUE);
+    $entity_types['node']->set('enable_page_title_template', TRUE);
+  }
+
+}
diff --git a/core/modules/node/tests/modules/node_test/node_test.module b/core/modules/node/tests/modules/node_test/node_test.module
deleted file mode 100644
index 834db4268ed7919b2a093057587bb0ccad556ab6..0000000000000000000000000000000000000000
--- a/core/modules/node/tests/modules/node_test/node_test.module
+++ /dev/null
@@ -1,194 +0,0 @@
-<?php
-
-/**
- * @file
- * A dummy module for testing node related hooks.
- *
- * This is a dummy module that implements node related hooks to test API
- * interaction with the Node module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\node\NodeInterface;
-
-/**
- * Implements hook_ENTITY_TYPE_view() for node entities.
- */
-function node_test_node_view(array &$build, NodeInterface $node, EntityViewDisplayInterface $display, $view_mode) {
-  if ($node->isNew()) {
-    return;
-  }
-  if ($view_mode == 'rss') {
-    // Add RSS elements and namespaces when building the RSS feed.
-    $node->rss_elements[] = [
-      'key' => 'testElement',
-      'value' => t('Value of testElement RSS element for node @nid.', ['@nid' => $node->id()]),
-    ];
-
-    // Add content that should be displayed only in the RSS feed.
-    $build['extra_feed_content'] = [
-      '#markup' => '<p>' . t('Extra data that should appear only in the RSS feed for node @nid.', ['@nid' => $node->id()]) . '</p>',
-      '#weight' => 10,
-    ];
-  }
-
-  if ($view_mode != 'rss') {
-    // Add content that should NOT be displayed in the RSS feed.
-    $build['extra_non_feed_content'] = [
-      '#markup' => '<p>' . t('Extra data that should appear everywhere except the RSS feed for node @nid.', ['@nid' => $node->id()]) . '</p>',
-    ];
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_build_defaults_alter() for node entities.
- */
-function node_test_node_build_defaults_alter(array &$build, NodeInterface &$node, $view_mode = 'full') {
-  if ($view_mode == 'rss') {
-    $node->rss_namespaces['xmlns:test'] = 'http://example.com/test-namespace';
-  }
-}
-
-/**
- * Implements hook_node_grants().
- */
-function node_test_node_grants(AccountInterface $account, $operation) {
-  // Give everyone full grants so we don't break other node tests.
-  // Our node access tests asserts three realms of access.
-  // See testGrantAlter().
-  return [
-    'test_article_realm' => [1],
-    'test_page_realm' => [1],
-    'test_alter_realm' => [2],
-  ];
-}
-
-/**
- * Implements hook_node_access_records().
- */
-function node_test_node_access_records(NodeInterface $node) {
-  // Return nothing when testing for empty responses.
-  if (!empty($node->disable_node_access)) {
-    return;
-  }
-  $grants = [];
-  if ($node->getType() == 'article') {
-    // Create grant in arbitrary article_realm for article nodes.
-    $grants[] = [
-      'realm' => 'test_article_realm',
-      'gid' => 1,
-      'grant_view' => 1,
-      'grant_update' => 0,
-      'grant_delete' => 0,
-    ];
-  }
-  elseif ($node->getType() == 'page') {
-    // Create grant in arbitrary page_realm for page nodes.
-    $grants[] = [
-      'realm' => 'test_page_realm',
-      'gid' => 1,
-      'grant_view' => 1,
-      'grant_update' => 0,
-      'grant_delete' => 0,
-    ];
-  }
-  return $grants;
-}
-
-/**
- * Implements hook_node_access_records_alter().
- */
-function node_test_node_access_records_alter(&$grants, NodeInterface $node) {
-  if (!empty($grants)) {
-    foreach ($grants as $key => $grant) {
-      // Alter grant from test_page_realm to test_alter_realm and modify the gid.
-      if ($grant['realm'] == 'test_page_realm' && $node->isPromoted()) {
-        $grants[$key]['realm'] = 'test_alter_realm';
-        $grants[$key]['gid'] = 2;
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_node_grants_alter().
- */
-function node_test_node_grants_alter(&$grants, AccountInterface $account, $operation) {
-  // Return an empty array of grants to prove that we can alter by reference.
-  $grants = [];
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for node entities.
- */
-function node_test_node_presave(NodeInterface $node) {
-  if ($node->getTitle() == 'testing_node_presave') {
-    // Sun, 19 Nov 1978 05:00:00 GMT
-    $node->setCreatedTime(280299600);
-    // Drupal 1.0 release.
-    $node->changed = 979534800;
-  }
-  // Determine changes.
-  if (!empty($node->original) && $node->original->getTitle() == 'test_changes') {
-    if ($node->original->getTitle() != $node->getTitle()) {
-      $node->title->value .= '_presave';
-    }
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for node entities.
- */
-function node_test_node_update(NodeInterface $node) {
-  // Determine changes on update.
-  if (!empty($node->original) && $node->original->getTitle() == 'test_changes') {
-    if ($node->original->getTitle() != $node->getTitle()) {
-      $node->title->value .= '_update';
-    }
-  }
-}
-
-/**
- * Implements hook_entity_view_mode_alter().
- */
-function node_test_entity_view_mode_alter(&$view_mode, EntityInterface $entity) {
-  // Only alter the view mode if we are on the test callback.
-  $change_view_mode = \Drupal::state()->get('node_test_change_view_mode', '');
-  if ($change_view_mode) {
-    $view_mode = $change_view_mode;
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for node entities.
- *
- * This tests saving a node on node insert.
- *
- * @see \Drupal\node\Tests\NodeSaveTest::testNodeSaveOnInsert()
- */
-function node_test_node_insert(NodeInterface $node) {
-  // Set the node title to the node ID and save.
-  if ($node->getTitle() == 'new') {
-    $node->setTitle('Node ' . $node->id());
-    $node->setNewRevision(FALSE);
-    $node->save();
-  }
-}
-
-/**
- * Implements hook_form_alter().
- */
-function node_test_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  if (!$form_state->get('node_test_form_alter')) {
-    \Drupal::messenger()->addStatus('Storage is not set');
-    $form_state->set('node_test_form_alter', TRUE);
-  }
-  else {
-    \Drupal::messenger()->addStatus('Storage is set');
-  }
-}
diff --git a/core/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php b/core/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..4d5a09f5952dbb8c6d3d34a629221e31b21d7127
--- /dev/null
+++ b/core/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php
@@ -0,0 +1,207 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\node_test\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\node\NodeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for node_test.
+ */
+class NodeTestHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_view() for node entities.
+   */
+  #[Hook('node_view')]
+  public function nodeView(array &$build, NodeInterface $node, EntityViewDisplayInterface $display, $view_mode) {
+    if ($node->isNew()) {
+      return;
+    }
+    if ($view_mode == 'rss') {
+      // Add RSS elements and namespaces when building the RSS feed.
+      $node->rss_elements[] = [
+        'key' => 'testElement',
+        'value' => t('Value of testElement RSS element for node @nid.', [
+          '@nid' => $node->id(),
+        ]),
+      ];
+      // Add content that should be displayed only in the RSS feed.
+      $build['extra_feed_content'] = [
+        '#markup' => '<p>' . t('Extra data that should appear only in the RSS feed for node @nid.', [
+          '@nid' => $node->id(),
+        ]) . '</p>',
+        '#weight' => 10,
+      ];
+    }
+    if ($view_mode != 'rss') {
+      // Add content that should NOT be displayed in the RSS feed.
+      $build['extra_non_feed_content'] = [
+        '#markup' => '<p>' . t('Extra data that should appear everywhere except the RSS feed for node @nid.', [
+          '@nid' => $node->id(),
+        ]) . '</p>',
+      ];
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_build_defaults_alter() for node entities.
+   */
+  #[Hook('node_build_defaults_alter')]
+  public function nodeBuildDefaultsAlter(array &$build, NodeInterface &$node, $view_mode = 'full') {
+    if ($view_mode == 'rss') {
+      $node->rss_namespaces['xmlns:test'] = 'http://example.com/test-namespace';
+    }
+  }
+
+  /**
+   * Implements hook_node_grants().
+   */
+  #[Hook('node_grants')]
+  public function nodeGrants(AccountInterface $account, $operation) {
+    // Give everyone full grants so we don't break other node tests.
+    // Our node access tests asserts three realms of access.
+    // See testGrantAlter().
+    return ['test_article_realm' => [1], 'test_page_realm' => [1], 'test_alter_realm' => [2]];
+  }
+
+  /**
+   * Implements hook_node_access_records().
+   */
+  #[Hook('node_access_records')]
+  public function nodeAccessRecords(NodeInterface $node) {
+    // Return nothing when testing for empty responses.
+    if (!empty($node->disable_node_access)) {
+      return;
+    }
+    $grants = [];
+    if ($node->getType() == 'article') {
+      // Create grant in arbitrary article_realm for article nodes.
+      $grants[] = [
+        'realm' => 'test_article_realm',
+        'gid' => 1,
+        'grant_view' => 1,
+        'grant_update' => 0,
+        'grant_delete' => 0,
+      ];
+    }
+    elseif ($node->getType() == 'page') {
+      // Create grant in arbitrary page_realm for page nodes.
+      $grants[] = [
+        'realm' => 'test_page_realm',
+        'gid' => 1,
+        'grant_view' => 1,
+        'grant_update' => 0,
+        'grant_delete' => 0,
+      ];
+    }
+    return $grants;
+  }
+
+  /**
+   * Implements hook_node_access_records_alter().
+   */
+  #[Hook('node_access_records_alter')]
+  public function nodeAccessRecordsAlter(&$grants, NodeInterface $node) {
+    if (!empty($grants)) {
+      foreach ($grants as $key => $grant) {
+        // Alter grant from test_page_realm to test_alter_realm and modify the gid.
+        if ($grant['realm'] == 'test_page_realm' && $node->isPromoted()) {
+          $grants[$key]['realm'] = 'test_alter_realm';
+          $grants[$key]['gid'] = 2;
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_node_grants_alter().
+   */
+  #[Hook('node_grants_alter')]
+  public function nodeGrantsAlter(&$grants, AccountInterface $account, $operation) {
+    // Return an empty array of grants to prove that we can alter by reference.
+    $grants = [];
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for node entities.
+   */
+  #[Hook('node_presave')]
+  public function nodePresave(NodeInterface $node) {
+    if ($node->getTitle() == 'testing_node_presave') {
+      // Sun, 19 Nov 1978 05:00:00 GMT
+      $node->setCreatedTime(280299600);
+      // Drupal 1.0 release.
+      $node->changed = 979534800;
+    }
+    // Determine changes.
+    if (!empty($node->original) && $node->original->getTitle() == 'test_changes') {
+      if ($node->original->getTitle() != $node->getTitle()) {
+        $node->title->value .= '_presave';
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for node entities.
+   */
+  #[Hook('node_update')]
+  public function nodeUpdate(NodeInterface $node) {
+    // Determine changes on update.
+    if (!empty($node->original) && $node->original->getTitle() == 'test_changes') {
+      if ($node->original->getTitle() != $node->getTitle()) {
+        $node->title->value .= '_update';
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_view_mode_alter().
+   */
+  #[Hook('entity_view_mode_alter')]
+  public function entityViewModeAlter(&$view_mode, EntityInterface $entity) {
+    // Only alter the view mode if we are on the test callback.
+    $change_view_mode = \Drupal::state()->get('node_test_change_view_mode', '');
+    if ($change_view_mode) {
+      $view_mode = $change_view_mode;
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for node entities.
+   *
+   * This tests saving a node on node insert.
+   *
+   * @see \Drupal\node\Tests\NodeSaveTest::testNodeSaveOnInsert()
+   */
+  #[Hook('node_insert')]
+  public function nodeInsert(NodeInterface $node) {
+    // Set the node title to the node ID and save.
+    if ($node->getTitle() == 'new') {
+      $node->setTitle('Node ' . $node->id());
+      $node->setNewRevision(FALSE);
+      $node->save();
+    }
+  }
+
+  /**
+   * Implements hook_form_alter().
+   */
+  #[Hook('form_alter')]
+  public function formAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    if (!$form_state->get('node_test_form_alter')) {
+      \Drupal::messenger()->addStatus('Storage is not set');
+      $form_state->set('node_test_form_alter', TRUE);
+    }
+    else {
+      \Drupal::messenger()->addStatus('Storage is set');
+    }
+  }
+
+}
diff --git a/core/modules/node/tests/modules/node_test_exception/node_test_exception.module b/core/modules/node/tests/modules/node_test_exception/node_test_exception.module
deleted file mode 100644
index 70858cbecc8bb894f70d8482e001ee2ef6bccfa9..0000000000000000000000000000000000000000
--- a/core/modules/node/tests/modules/node_test_exception/node_test_exception.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * A module implementing node related hooks to test API interaction.
- */
-
-declare(strict_types=1);
-
-use Drupal\node\NodeInterface;
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for node entities.
- */
-function node_test_exception_node_insert(NodeInterface $node) {
-  if ($node->getTitle() == 'testing_transaction_exception') {
-    throw new Exception('Test exception for rollback.');
-  }
-}
diff --git a/core/modules/node/tests/modules/node_test_exception/src/Hook/NodeTestExceptionHooks.php b/core/modules/node/tests/modules/node_test_exception/src/Hook/NodeTestExceptionHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..067a965bd2f4fd86016c625e6f27e4af65813c6b
--- /dev/null
+++ b/core/modules/node/tests/modules/node_test_exception/src/Hook/NodeTestExceptionHooks.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\node_test_exception\Hook;
+
+use Drupal\node\NodeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for node_test_exception.
+ */
+class NodeTestExceptionHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for node entities.
+   */
+  #[Hook('node_insert')]
+  public function nodeInsert(NodeInterface $node) {
+    if ($node->getTitle() == 'testing_transaction_exception') {
+      throw new \Exception('Test exception for rollback.');
+    }
+  }
+
+}
diff --git a/core/modules/node/tests/modules/node_test_views/node_test_views.views.inc b/core/modules/node/tests/modules/node_test_views/node_test_views.views.inc
deleted file mode 100644
index 0b9af74182528ca8124d7325b7b87b61b993b1b3..0000000000000000000000000000000000000000
--- a/core/modules/node/tests/modules/node_test_views/node_test_views.views.inc
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides views data and hooks for node_test_views module.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_views_data_alter().
- */
-function node_test_views_views_data_alter(array &$data) {
-  // Make node language use the basic field handler if requested.
-  if (\Drupal::state()->get('node_test_views.use_basic_handler')) {
-    $data['node_field_data']['langcode']['field']['id'] = 'language';
-  }
-}
diff --git a/core/modules/node/tests/modules/node_test_views/src/Hook/NodeTestViewsViewsHooks.php b/core/modules/node/tests/modules/node_test_views/src/Hook/NodeTestViewsViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f01907303cbf751df889780cb42b20b6adaf7f24
--- /dev/null
+++ b/core/modules/node/tests/modules/node_test_views/src/Hook/NodeTestViewsViewsHooks.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\node_test_views\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for node_test_views.
+ */
+class NodeTestViewsViewsHooks {
+
+  /**
+   * Implements hook_views_data_alter().
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(array &$data) {
+    // Make node language use the basic field handler if requested.
+    if (\Drupal::state()->get('node_test_views.use_basic_handler')) {
+      $data['node_field_data']['langcode']['field']['id'] = 'language';
+    }
+  }
+
+}
diff --git a/core/modules/node/tests/src/Kernel/NodeAccessRecordsTest.php b/core/modules/node/tests/src/Kernel/NodeAccessRecordsTest.php
index c2375cccdd5c0b90e6412bc8940451dab87d4ae7..153c28cb3dd2b856b0fe1dd780de7e8a864f54b2 100644
--- a/core/modules/node/tests/src/Kernel/NodeAccessRecordsTest.php
+++ b/core/modules/node/tests/src/Kernel/NodeAccessRecordsTest.php
@@ -88,7 +88,7 @@ public function testNodeAccessRecords(): void {
     // Create a user that is allowed to access content.
     $web_user = $this->drupalCreateUser(['access content']);
     foreach ($operations as $op) {
-      $grants = node_test_node_grants($web_user, $op);
+      $grants = \Drupal::moduleHandler()->invoke('node', 'node_grants', [$web_user, $op]);
       $altered_grants = $grants;
       \Drupal::moduleHandler()->alter('node_grants', $altered_grants, $web_user, $op);
       $this->assertNotEquals($grants, $altered_grants, "Altered the $op grant for a user.");
diff --git a/core/modules/options/options.module b/core/modules/options/options.module
index c1077d66382a342669a26d0d4e46e8707b9353b1..7c648a5c8e4c6c162570e161fef871539a688a5b 100644
--- a/core/modules/options/options.module
+++ b/core/modules/options/options.module
@@ -2,51 +2,10 @@
 
 /**
  * @file
- * Defines selection, check box and radio button widgets for text and numeric fields.
  */
 
-use Drupal\Core\Url;
 use Drupal\Core\Entity\FieldableEntityInterface;
-use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\field\FieldStorageConfigInterface;
-
-/**
- * Implements hook_help().
- */
-function options_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.options':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Options module allows you to create fields where data values are selected from a fixed list of options. Usually these items are entered through a select list, checkboxes, or radio buttons. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":options_do">online documentation for the Options module</a>.', [':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':options_do' => 'https://www.drupal.org/documentation/modules/options']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Managing and displaying list fields') . '</dt>';
-      $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the list fields can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Defining option keys and labels') . '</dt>';
-      $output .= '<dd>' . t('When you define the list options you can define a key and a label for each option in the list. The label will be shown to the users while the key gets stored in the database.') . '</dd>';
-      $output .= '<dt>' . t('Choosing list field type') . '</dt>';
-      $output .= '<dd>' . t('There are three types of list fields, which store different types of data: <em>float</em>, <em>integer</em> or, <em>text</em>. The <em>float</em> type allows storing approximate decimal values. The <em>integer</em> type allows storing whole numbers, such as years (for example, 2012) or values (for example, 1, 2, 5, 305). The <em>text</em> list field type allows storing text values. No matter which type of list field you choose, you can define whatever labels you wish for data entry.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for 'field_storage_config'.
- */
-function options_field_storage_config_update(FieldStorageConfigInterface $field_storage) {
-  drupal_static_reset('options_allowed_values');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for 'field_storage_config'.
- */
-function options_field_storage_config_delete(FieldStorageConfigInterface $field_storage) {
-  drupal_static_reset('options_allowed_values');
-}
 
 /**
  * Returns the array of allowed values for a list field.
@@ -103,21 +62,6 @@ function options_allowed_values(FieldStorageDefinitionInterface $definition, ?Fi
   return $allowed_values[$cache_id];
 }
 
-/**
- * Implements hook_field_storage_config_update_forbid().
- */
-function options_field_storage_config_update_forbid(FieldStorageConfigInterface $field_storage, FieldStorageConfigInterface $prior_field_storage) {
-  if ($field_storage->getTypeProvider() == 'options' && $field_storage->hasData()) {
-    // Forbid any update that removes allowed values with actual data.
-    $allowed_values = $field_storage->getSetting('allowed_values');
-    $prior_allowed_values = $prior_field_storage->getSetting('allowed_values');
-    $lost_keys = array_keys(array_diff_key($prior_allowed_values, $allowed_values));
-    if (_options_values_in_use($field_storage->getTargetEntityTypeId(), $field_storage->getName(), $lost_keys)) {
-      throw new FieldStorageDefinitionUpdateForbiddenException("A list field '{$field_storage->getName()}' with existing data cannot have its keys changed.");
-    }
-  }
-}
-
 /**
  * Checks if a list of values are being used in actual field values.
  */
diff --git a/core/modules/options/options.views.inc b/core/modules/options/options.views.inc
deleted file mode 100644
index 28ece79e88f9dfe6afcf76fc6a5c986a3320b734..0000000000000000000000000000000000000000
--- a/core/modules/options/options.views.inc
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide Views data for options.module.
- *
- * @ingroup views_module_handlers
- */
-
-use Drupal\field\FieldStorageConfigInterface;
-
-/**
- * Implements hook_field_views_data().
- *
- * Views integration for list fields. Have a different filter handler and
- * argument handlers for list fields. This should allow to select values of
- * the list.
- */
-function options_field_views_data(FieldStorageConfigInterface $field) {
-  $data = views_field_default_views_data($field);
-
-  foreach ($data as $table_name => $table_data) {
-    foreach ($table_data as $field_name => $field_data) {
-      if (isset($field_data['filter']) && $field_name != 'delta') {
-        $data[$table_name][$field_name]['filter']['id'] = 'list_field';
-      }
-      if (isset($field_data['argument']) && $field_name != 'delta') {
-        if ($field->getType() == 'list_string') {
-          $data[$table_name][$field_name]['argument']['id'] = 'string_list_field';
-        }
-        else {
-          $data[$table_name][$field_name]['argument']['id'] = 'number_list_field';
-        }
-      }
-    }
-  }
-
-  return $data;
-}
diff --git a/core/modules/options/src/Hook/OptionsHooks.php b/core/modules/options/src/Hook/OptionsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..9134b4532f2f659ce303829626bfbf63be18ed5b
--- /dev/null
+++ b/core/modules/options/src/Hook/OptionsHooks.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\options\Hook;
+
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for options.
+ */
+class OptionsHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.options':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Options module allows you to create fields where data values are selected from a fixed list of options. Usually these items are entered through a select list, checkboxes, or radio buttons. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":options_do">online documentation for the Options module</a>.', [
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+          ':options_do' => 'https://www.drupal.org/documentation/modules/options',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Managing and displaying list fields') . '</dt>';
+        $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the list fields can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Defining option keys and labels') . '</dt>';
+        $output .= '<dd>' . t('When you define the list options you can define a key and a label for each option in the list. The label will be shown to the users while the key gets stored in the database.') . '</dd>';
+        $output .= '<dt>' . t('Choosing list field type') . '</dt>';
+        $output .= '<dd>' . t('There are three types of list fields, which store different types of data: <em>float</em>, <em>integer</em> or, <em>text</em>. The <em>float</em> type allows storing approximate decimal values. The <em>integer</em> type allows storing whole numbers, such as years (for example, 2012) or values (for example, 1, 2, 5, 305). The <em>text</em> list field type allows storing text values. No matter which type of list field you choose, you can define whatever labels you wish for data entry.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for 'field_storage_config'.
+   */
+  #[Hook('field_storage_config_update')]
+  public function fieldStorageConfigUpdate(FieldStorageConfigInterface $field_storage) {
+    drupal_static_reset('options_allowed_values');
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for 'field_storage_config'.
+   */
+  #[Hook('field_storage_config_delete')]
+  public function fieldStorageConfigDelete(FieldStorageConfigInterface $field_storage) {
+    drupal_static_reset('options_allowed_values');
+  }
+
+  /**
+   * Implements hook_field_storage_config_update_forbid().
+   */
+  #[Hook('field_storage_config_update_forbid')]
+  public function fieldStorageConfigUpdateForbid(FieldStorageConfigInterface $field_storage, FieldStorageConfigInterface $prior_field_storage) {
+    if ($field_storage->getTypeProvider() == 'options' && $field_storage->hasData()) {
+      // Forbid any update that removes allowed values with actual data.
+      $allowed_values = $field_storage->getSetting('allowed_values');
+      $prior_allowed_values = $prior_field_storage->getSetting('allowed_values');
+      $lost_keys = array_keys(array_diff_key($prior_allowed_values, $allowed_values));
+      if (_options_values_in_use($field_storage->getTargetEntityTypeId(), $field_storage->getName(), $lost_keys)) {
+        throw new FieldStorageDefinitionUpdateForbiddenException("A list field '{$field_storage->getName()}' with existing data cannot have its keys changed.");
+      }
+    }
+  }
+
+}
diff --git a/core/modules/options/src/Hook/OptionsViewsHooks.php b/core/modules/options/src/Hook/OptionsViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..6165bccc17e6ba19f7fe1a4cad5ac1aa9535eb91
--- /dev/null
+++ b/core/modules/options/src/Hook/OptionsViewsHooks.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\options\Hook;
+
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for options.
+ */
+class OptionsViewsHooks {
+
+  /**
+   * Implements hook_field_views_data().
+   *
+   * Views integration for list fields. Have a different filter handler and
+   * argument handlers for list fields. This should allow to select values of
+   * the list.
+   */
+  #[Hook('field_views_data')]
+  public function fieldViewsData(FieldStorageConfigInterface $field) {
+    $data = views_field_default_views_data($field);
+    foreach ($data as $table_name => $table_data) {
+      foreach ($table_data as $field_name => $field_data) {
+        if (isset($field_data['filter']) && $field_name != 'delta') {
+          $data[$table_name][$field_name]['filter']['id'] = 'list_field';
+        }
+        if (isset($field_data['argument']) && $field_name != 'delta') {
+          if ($field->getType() == 'list_string') {
+            $data[$table_name][$field_name]['argument']['id'] = 'string_list_field';
+          }
+          else {
+            $data[$table_name][$field_name]['argument']['id'] = 'number_list_field';
+          }
+        }
+      }
+    }
+    return $data;
+  }
+
+}
diff --git a/core/modules/options/tests/options_test/options_test.module b/core/modules/options/tests/options_test/options_test.module
index d585d8783feb586d53a8ba8c3e63f632864e28e5..6138d62f005dea0978b85afc676c314dbd07f7fa 100644
--- a/core/modules/options/tests/options_test/options_test.module
+++ b/core/modules/options/tests/options_test/options_test.module
@@ -9,7 +9,6 @@
 
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
-use Drupal\Core\Form\FormStateInterface;
 
 /**
  * Implements callback_allowed_values_function().
@@ -57,27 +56,3 @@ function options_test_dynamic_values_callback(FieldStorageDefinitionInterface $d
   // We need the values of the entity as keys.
   return array_combine($values, $values);
 }
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function options_test_form_entity_test_entity_test_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  if (\Drupal::state()->get('options_test.form_alter_enable', FALSE)) {
-    $form['card_1']['widget']['#required_error'] = t('This is custom message for required field.');
-  }
-}
-
-/**
- * Implements hook_options_list_alter().
- */
-function options_test_options_list_alter(array &$options, array $context) {
-  if ($context['fieldDefinition']->getName() === 'card_4' && $context['widget']->getPluginId() === 'options_select') {
-    // Rename _none option.
-    $options['_none'] = '- Select something -';
-  }
-
-  if ($context['fieldDefinition']->getName() === 'card_4' && $context['entity']->bundle() === 'entity_test') {
-    // Remove 0 option.
-    unset($options[0]);
-  }
-}
diff --git a/core/modules/options/tests/options_test/src/Hook/OptionsTestHooks.php b/core/modules/options/tests/options_test/src/Hook/OptionsTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..9ffb20169f09cad248f29233c22aa789bfaf2915
--- /dev/null
+++ b/core/modules/options/tests/options_test/src/Hook/OptionsTestHooks.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\options_test\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for options_test.
+ */
+class OptionsTestHooks {
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_entity_test_entity_test_form_alter')]
+  public function formEntityTestEntityTestFormAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    if (\Drupal::state()->get('options_test.form_alter_enable', FALSE)) {
+      $form['card_1']['widget']['#required_error'] = t('This is custom message for required field.');
+    }
+  }
+
+  /**
+   * Implements hook_options_list_alter().
+   */
+  #[Hook('options_list_alter')]
+  public function optionsListAlter(array &$options, array $context) {
+    if ($context['fieldDefinition']->getName() === 'card_4' && $context['widget']->getPluginId() === 'options_select') {
+      // Rename _none option.
+      $options['_none'] = '- Select something -';
+    }
+    if ($context['fieldDefinition']->getName() === 'card_4' && $context['entity']->bundle() === 'entity_test') {
+      // Remove 0 option.
+      unset($options[0]);
+    }
+  }
+
+}
diff --git a/core/modules/package_manager/package_manager.module b/core/modules/package_manager/package_manager.module
deleted file mode 100644
index de607e6a9525cfd8e395252fc9bfd14173236df6..0000000000000000000000000000000000000000
--- a/core/modules/package_manager/package_manager.module
+++ /dev/null
@@ -1,107 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains hook implementations for Package Manager.
- */
-
-declare(strict_types=1);
-
-use Drupal\package_manager\ComposerInspector;
-
-/**
- * Implements hook_help().
- */
-function package_manager_help($route_name): ?string {
-  switch ($route_name) {
-    case 'help.page.package_manager':
-      $output = '<h3 id="package-manager-about">' . t('About') . '</h3>';
-      $output .= '<p>' . t('Package Manager is a framework for updating Drupal core and installing contributed modules and themes via Composer. It has no user interface, but it provides an API for creating a temporary copy of the current site, making changes to the copy, and then syncing those changes back into the live site.') . '</p>';
-      $output .= '<p>' . t('Package Manager dispatches events before and after various operations, and external code can integrate with it by subscribing to those events. For more information, see <code>package_manager.api.php</code>.') . '</p>';
-
-      $output .= '<h3 id="package-manager-requirements">' . t('Requirements') . '</h3>';
-      $output .= '<ul>';
-      $output .= '  <li>' . t("The Drupal application's codebase must be writable in order to use Automatic Updates. This includes Drupal core, modules, themes and the Composer dependencies in the <code>vendor</code> directory. This makes Automatic Updates incompatible with some hosting platforms.") . '</li>';
-      $output .= '  <li>' . t('Package Manager requires a Composer executable whose version satisfies <code>@version</code>, and PHP must have permission to run it.', ['@version' => ComposerInspector::SUPPORTED_VERSION]) . '</li>';
-      $output .= '  <li>' . t("Your Drupal site's <code>composer.json</code> file must be valid according to <code>composer validate</code>. See <a href=\":url\">Composer's documentation</a> for more information.", [':url' => 'https://getcomposer.org/doc/03-cli.md#validate']) . '</li>';
-      $output .= '  <li>' . t('Composer must be configured for secure downloads. This means that <a href=":disable-tls">the <code>disable-tls</code> option</a> must be <code>false</code>, and <a href=":secure-http">the <code>secure-http</code> option</a> must be <code>true</code> in the <code>config</code> section of your <code>composer.json</code> file. If these options are not set in your <code>composer.json</code>, Composer will behave securely by default. To set these values at the command line, run the following commands:', [
-        ':disable-tls' => 'https://getcomposer.org/doc/06-config.md#disable-tls',
-        ':secure-http' => 'https://getcomposer.org/doc/06-config.md#secure-http',
-      ]);
-      $output .= '<pre><code>';
-      $output .= "composer config --unset disable-tls\n";
-      $output .= "composer config --unset secure-http\n";
-      $output .= '</code></pre></li></ul>';
-
-      $output .= '<h3 id="package-manager-limitations">' . t('Limitations') . '</h3>';
-      $output .= '<p>' . t("Because Package Manager modifies the current site's code base, it is intentionally limited in certain ways to prevent unexpected changes to the live site:") . '</p>';
-      $output .= '<ul>';
-      $output .= '  <li>' . t('It does not support Drupal multi-site installations.') . '</li>';
-      $output .= '  <li>' . t('It only allows supported Composer plugins. If you have any, see <a href="#package-manager-faq-unsupported-composer-plugin">What if it says I have unsupported Composer plugins in my codebase?</a>.') . '</li>';
-      $output .= '  <li>' . t('It does not automatically perform version control operations, e.g., with Git. Site administrators are responsible for committing updates.') . '</li>';
-      $output .= '  <li>' . t('It can only maintain one copy of the site at any given time. If a copy of the site already exists, another one cannot be created until the existing copy is destroyed.') . '</li>';
-      $output .= '  <li>' . t('It associates the temporary copy of the site with the user or session that originally created it, and only that user or session can make changes to it.') . '</li>';
-      $output .= '  <li>' . t('It does not allow modules to be uninstalled while syncing changes into live site.') . '</li>';
-      $output .= '</ul>';
-      $output .= '<p>' . t('For more information, see the <a href=":url">online documentation for the Package Manager module</a>.', [':url' => 'https://www.drupal.org/docs/8/core/modules/package-manager']) . '</p>';
-
-      $output .= '<h3 id="package-manager-faq">' . t('FAQ') . '</h3>';
-      $output .= '<h4 id="package-manager-composer-related-faq">' . t('FAQs related to Composer') . '</h4>';
-      $output .= '<ul>';
-      $output .= '  <li>' . t('What if it says the <code>proc_open()</code> function is disabled on your PHP installation?');
-      $output .= '    <p>' . t('Ask your system administrator to remove <code>proc_open()</code> from the <a href=":url">disable_functions</a> setting in <code>php.ini</code>.', [':url' => 'https://www.php.net/manual/en/ini.core.php#ini.disable-functions']) . '</p>';
-      $output .= '  </li>';
-      $output .= '  <li>' . t('What if it says the <code>composer</code> executable cannot be found?');
-      $output .= '    <p>' . t("If the <code>composer</code> executable's path cannot be automatically determined, it can be explicitly set by adding the following line to <code>settings.php</code>:") . '</p>';
-      $output .= "    <pre><code>\$config['package_manager.settings']['executables']['composer'] = '/full/path/to/composer.phar';</code></pre>";
-      $output .= '  </li>';
-      $output .= '  <li>' . t('What if it says the detected version of Composer is not supported?');
-      $output .= '    <p>' . t('The version of the <code>composer</code> executable must satisfy <code>@version</code>. See the <a href=":url">the Composer documentation</a> for more information, or use this command to update Composer:', ['@version' => ComposerInspector::SUPPORTED_VERSION, ':url' => 'https://getcomposer.org/doc/03-cli.md#self-update-selfupdate']) . '</p>';
-      $output .= '    <pre><code>composer self-update</code></pre>';
-      $output .= '  </li>';
-      $output .= '  <li>' . t('What if it says the <code>composer validate</code> command failed?');
-      $output .= '    <p>' . t('Composer detected problems with your <code>composer.json</code> and/or <code>composer.lock</code> files, and the project is not in a completely valid state. See <a href=":url">the Composer documentation</a> for more information.', [':url' => 'https://getcomposer.org/doc/04-schema.md']) . '</p>';
-      $output .= '  </li>';
-      $output .= '</ul>';
-
-      $output .= '<h4 id="package-manager-faq-rsync">' . t('Using rsync') . '</h4>';
-      $output .= '<p>' . t('Package Manager must be able to run <code>rsync</code> to copy files between the live site and the stage directory. Package Manager will try to detect the path to <code>rsync</code>, but if it cannot be detected, you can set it explicitly by adding the following line to <code>settings.php</code>:') . '</p>';
-      $output .= "<pre><code>\$config['package_manager.settings']['executables']['rsync'] = '/full/path/to/rsync';</code></pre>";
-
-      $output .= '<h4 id="package-manager-tuf-info">' . t('Enabling PHP-TUF protection') . '</h4>';
-      $output .= '<p>' . t('Package Manager requires <a href=":php-tuf">PHP-TUF</a>, which implements <a href=":tuf">The Update Framework</a> as a way to help secure Composer package downloads via the <a href=":php-tuf-plugin">PHP-TUF Composer integration plugin</a>. This plugin must be installed and configured properly in order to use Package Manager.', [
-        ':php-tuf' => 'https://github.com/php-tuf/php-tuf',
-        ':tuf' => 'https://theupdateframework.io/',
-        ':php-tuf-plugin' => 'https://github.com/php-tuf/composer-integration',
-      ]) . '</p>';
-      $output .= '<p>' . t('To install and configure the plugin as needed, you can run the following commands:') . '</p>';
-      $output .= '<pre><code>';
-      $output .= "composer config allow-plugins.php-tuf/composer-integration true\n";
-      $output .= "composer require php-tuf/composer-integration";
-      $output .= '</code></pre>';
-      $output .= '<p>' . t('Package Manager currently requires the <code>https://packages.drupal.org/8</code> Composer repository to be protected by TUF. To set this up, run the following command:') . '</p>';
-      $output .= '<pre><code>';
-      $output .= "composer tuf:protect https://packages.drupal.org/8\n";
-      $output .= '</code></pre>';
-
-      $output .= '<h4 id="package-manager-faq-unsupported-composer-plugin">' . t('What if it says I have unsupported Composer plugins in my codebase?') . '</h4>';
-      $output .= '<p>' . t('A fresh Drupal installation only uses supported Composer plugins, but some modules or themes may depend on additional Composer plugins. <a href=":new-issue">Create a new issue</a> when you encounter this.', [
-        ':new-issue' => 'https://www.drupal.org/node/add/project-issue/auto_updates',
-      ]) . '</p>';
-      $output .= '<p>' . t('It is possible to <em>trust</em> additional Composer plugins, but this requires significant expertise: understanding the code of that Composer plugin, what the effects on the file system are and how it affects the Package Manager module. Some Composer plugins could result in a broken site!') . '</p>';
-
-      $output .= '<h4 id="package-manager-faq-composer-patches-installed-or-removed">' . t('What if it says <code>cweagans/composer-patches</code> cannot be installed/removed?') . '</h4>';
-      $output .= '<p>' . t('Installation or removal of <code>cweagans/composer-patches</code> via Package Manager is not supported. You can install or remove it manually by running Composer commands in your site root.') . '</p>';
-      $output .= '<p>' . t('To install it:') . '</p>';
-      $output .= '<pre><code>composer require cweagans/composer-patches</code></pre>';
-      $output .= '<p>' . t('To remove it:') . '</p>';
-      $output .= '<pre><code>composer remove cweagans/composer-patches</code></pre>';
-
-      $output .= '<h4 id="package-manager-faq-composer-patches-not-a-root-dependency">' . t('What if it says <code>cweagans/composer-patches</code> must be a root dependency?') . '</h4>';
-      $output .= '<p>' . t('If <code>cweagans/composer-patches</code> is installed, it must be defined as a dependency of the main project (i.e., it must be listed in the <code>require</code> or <code>require-dev</code> section of <code>composer.json</code>). You can run the following command in your site root to add it as a dependency of the main project:') . '</p>';
-      $output .= "<pre><code>composer require cweagans/composer-patches</code></pre>";
-
-      return $output;
-  }
-  return NULL;
-}
diff --git a/core/modules/package_manager/src/Hook/PackageManagerHooks.php b/core/modules/package_manager/src/Hook/PackageManagerHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..423ea9708c060879b9c814a55a54de45898fa1c4
--- /dev/null
+++ b/core/modules/package_manager/src/Hook/PackageManagerHooks.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Drupal\package_manager\Hook;
+
+use Drupal\package_manager\ComposerInspector;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for package_manager.
+ */
+class PackageManagerHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name) : ?string {
+    switch ($route_name) {
+      case 'help.page.package_manager':
+        $output = '<h3 id="package-manager-about">' . t('About') . '</h3>';
+        $output .= '<p>' . t('Package Manager is a framework for updating Drupal core and installing contributed modules and themes via Composer. It has no user interface, but it provides an API for creating a temporary copy of the current site, making changes to the copy, and then syncing those changes back into the live site.') . '</p>';
+        $output .= '<p>' . t('Package Manager dispatches events before and after various operations, and external code can integrate with it by subscribing to those events. For more information, see <code>package_manager.api.php</code>.') . '</p>';
+        $output .= '<h3 id="package-manager-requirements">' . t('Requirements') . '</h3>';
+        $output .= '<ul>';
+        $output .= '  <li>' . t("The Drupal application's codebase must be writable in order to use Automatic Updates. This includes Drupal core, modules, themes and the Composer dependencies in the <code>vendor</code> directory. This makes Automatic Updates incompatible with some hosting platforms.") . '</li>';
+        $output .= '  <li>' . t('Package Manager requires a Composer executable whose version satisfies <code>@version</code>, and PHP must have permission to run it.', ['@version' => ComposerInspector::SUPPORTED_VERSION]) . '</li>';
+        $output .= '  <li>' . t("Your Drupal site's <code>composer.json</code> file must be valid according to <code>composer validate</code>. See <a href=\":url\">Composer's documentation</a> for more information.", [':url' => 'https://getcomposer.org/doc/03-cli.md#validate']) . '</li>';
+        $output .= '  <li>' . t('Composer must be configured for secure downloads. This means that <a href=":disable-tls">the <code>disable-tls</code> option</a> must be <code>false</code>, and <a href=":secure-http">the <code>secure-http</code> option</a> must be <code>true</code> in the <code>config</code> section of your <code>composer.json</code> file. If these options are not set in your <code>composer.json</code>, Composer will behave securely by default. To set these values at the command line, run the following commands:', [
+          ':disable-tls' => 'https://getcomposer.org/doc/06-config.md#disable-tls',
+          ':secure-http' => 'https://getcomposer.org/doc/06-config.md#secure-http',
+        ]);
+        $output .= '<pre><code>';
+        $output .= "composer config --unset disable-tls\n";
+        $output .= "composer config --unset secure-http\n";
+        $output .= '</code></pre></li></ul>';
+        $output .= '<h3 id="package-manager-limitations">' . t('Limitations') . '</h3>';
+        $output .= '<p>' . t("Because Package Manager modifies the current site's code base, it is intentionally limited in certain ways to prevent unexpected changes to the live site:") . '</p>';
+        $output .= '<ul>';
+        $output .= '  <li>' . t('It does not support Drupal multi-site installations.') . '</li>';
+        $output .= '  <li>' . t('It only allows supported Composer plugins. If you have any, see <a href="#package-manager-faq-unsupported-composer-plugin">What if it says I have unsupported Composer plugins in my codebase?</a>.') . '</li>';
+        $output .= '  <li>' . t('It does not automatically perform version control operations, e.g., with Git. Site administrators are responsible for committing updates.') . '</li>';
+        $output .= '  <li>' . t('It can only maintain one copy of the site at any given time. If a copy of the site already exists, another one cannot be created until the existing copy is destroyed.') . '</li>';
+        $output .= '  <li>' . t('It associates the temporary copy of the site with the user or session that originally created it, and only that user or session can make changes to it.') . '</li>';
+        $output .= '  <li>' . t('It does not allow modules to be uninstalled while syncing changes into live site.') . '</li>';
+        $output .= '</ul>';
+        $output .= '<p>' . t('For more information, see the <a href=":url">online documentation for the Package Manager module</a>.', [':url' => 'https://www.drupal.org/docs/8/core/modules/package-manager']) . '</p>';
+        $output .= '<h3 id="package-manager-faq">' . t('FAQ') . '</h3>';
+        $output .= '<h4 id="package-manager-composer-related-faq">' . t('FAQs related to Composer') . '</h4>';
+        $output .= '<ul>';
+        $output .= '  <li>' . t('What if it says the <code>proc_open()</code> function is disabled on your PHP installation?');
+        $output .= '    <p>' . t('Ask your system administrator to remove <code>proc_open()</code> from the <a href=":url">disable_functions</a> setting in <code>php.ini</code>.', [':url' => 'https://www.php.net/manual/en/ini.core.php#ini.disable-functions']) . '</p>';
+        $output .= '  </li>';
+        $output .= '  <li>' . t('What if it says the <code>composer</code> executable cannot be found?');
+        $output .= '    <p>' . t("If the <code>composer</code> executable's path cannot be automatically determined, it can be explicitly set by adding the following line to <code>settings.php</code>:") . '</p>';
+        $output .= "    <pre><code>\$config['package_manager.settings']['executables']['composer'] = '/full/path/to/composer.phar';</code></pre>";
+        $output .= '  </li>';
+        $output .= '  <li>' . t('What if it says the detected version of Composer is not supported?');
+        $output .= '    <p>' . t('The version of the <code>composer</code> executable must satisfy <code>@version</code>. See the <a href=":url">the Composer documentation</a> for more information, or use this command to update Composer:', [
+          '@version' => ComposerInspector::SUPPORTED_VERSION,
+          ':url' => 'https://getcomposer.org/doc/03-cli.md#self-update-selfupdate',
+        ]) . '</p>';
+        $output .= '    <pre><code>composer self-update</code></pre>';
+        $output .= '  </li>';
+        $output .= '  <li>' . t('What if it says the <code>composer validate</code> command failed?');
+        $output .= '    <p>' . t('Composer detected problems with your <code>composer.json</code> and/or <code>composer.lock</code> files, and the project is not in a completely valid state. See <a href=":url">the Composer documentation</a> for more information.', [':url' => 'https://getcomposer.org/doc/04-schema.md']) . '</p>';
+        $output .= '  </li>';
+        $output .= '</ul>';
+        $output .= '<h4 id="package-manager-faq-rsync">' . t('Using rsync') . '</h4>';
+        $output .= '<p>' . t('Package Manager must be able to run <code>rsync</code> to copy files between the live site and the stage directory. Package Manager will try to detect the path to <code>rsync</code>, but if it cannot be detected, you can set it explicitly by adding the following line to <code>settings.php</code>:') . '</p>';
+        $output .= "<pre><code>\$config['package_manager.settings']['executables']['rsync'] = '/full/path/to/rsync';</code></pre>";
+        $output .= '<h4 id="package-manager-tuf-info">' . t('Enabling PHP-TUF protection') . '</h4>';
+        $output .= '<p>' . t('Package Manager requires <a href=":php-tuf">PHP-TUF</a>, which implements <a href=":tuf">The Update Framework</a> as a way to help secure Composer package downloads via the <a href=":php-tuf-plugin">PHP-TUF Composer integration plugin</a>. This plugin must be installed and configured properly in order to use Package Manager.', [
+          ':php-tuf' => 'https://github.com/php-tuf/php-tuf',
+          ':tuf' => 'https://theupdateframework.io/',
+          ':php-tuf-plugin' => 'https://github.com/php-tuf/composer-integration',
+        ]) . '</p>';
+        $output .= '<p>' . t('To install and configure the plugin as needed, you can run the following commands:') . '</p>';
+        $output .= '<pre><code>';
+        $output .= "composer config allow-plugins.php-tuf/composer-integration true\n";
+        $output .= "composer require php-tuf/composer-integration";
+        $output .= '</code></pre>';
+        $output .= '<p>' . t('Package Manager currently requires the <code>https://packages.drupal.org/8</code> Composer repository to be protected by TUF. To set this up, run the following command:') . '</p>';
+        $output .= '<pre><code>';
+        $output .= "composer tuf:protect https://packages.drupal.org/8\n";
+        $output .= '</code></pre>';
+        $output .= '<h4 id="package-manager-faq-unsupported-composer-plugin">' . t('What if it says I have unsupported Composer plugins in my codebase?') . '</h4>';
+        $output .= '<p>' . t('A fresh Drupal installation only uses supported Composer plugins, but some modules or themes may depend on additional Composer plugins. <a href=":new-issue">Create a new issue</a> when you encounter this.', [':new-issue' => 'https://www.drupal.org/node/add/project-issue/auto_updates']) . '</p>';
+        $output .= '<p>' . t('It is possible to <em>trust</em> additional Composer plugins, but this requires significant expertise: understanding the code of that Composer plugin, what the effects on the file system are and how it affects the Package Manager module. Some Composer plugins could result in a broken site!') . '</p>';
+        $output .= '<h4 id="package-manager-faq-composer-patches-installed-or-removed">' . t('What if it says <code>cweagans/composer-patches</code> cannot be installed/removed?') . '</h4>';
+        $output .= '<p>' . t('Installation or removal of <code>cweagans/composer-patches</code> via Package Manager is not supported. You can install or remove it manually by running Composer commands in your site root.') . '</p>';
+        $output .= '<p>' . t('To install it:') . '</p>';
+        $output .= '<pre><code>composer require cweagans/composer-patches</code></pre>';
+        $output .= '<p>' . t('To remove it:') . '</p>';
+        $output .= '<pre><code>composer remove cweagans/composer-patches</code></pre>';
+        $output .= '<h4 id="package-manager-faq-composer-patches-not-a-root-dependency">' . t('What if it says <code>cweagans/composer-patches</code> must be a root dependency?') . '</h4>';
+        $output .= '<p>' . t('If <code>cweagans/composer-patches</code> is installed, it must be defined as a dependency of the main project (i.e., it must be listed in the <code>require</code> or <code>require-dev</code> section of <code>composer.json</code>). You can run the following command in your site root to add it as a dependency of the main project:') . '</p>';
+        $output .= "<pre><code>composer require cweagans/composer-patches</code></pre>";
+        return $output;
+    }
+    return NULL;
+  }
+
+}
diff --git a/core/modules/page_cache/page_cache.module b/core/modules/page_cache/page_cache.module
deleted file mode 100644
index 7a4cb757ac9c2cf2e31e1e02a0ba6754de6745a1..0000000000000000000000000000000000000000
--- a/core/modules/page_cache/page_cache.module
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-/**
- * @file
- * Caches responses for anonymous users, request and response policies allowing.
- */
-
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_help().
- */
-function page_cache_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.page_cache':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Internal Page Cache module caches pages for anonymous users in the database. For more information, see the <a href=":pagecache-documentation">online documentation for the Internal Page Cache module</a>.', [':pagecache-documentation' => 'https://www.drupal.org/documentation/modules/internal_page_cache']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Speeding up your site') . '</dt>';
-      $output .= '<dd>' . t('Pages requested by anonymous users are stored the first time they are requested and then are reused. Depending on your site configuration and the amount of your web traffic tied to anonymous visitors, the caching system may significantly increase the speed of your site.') . '</dd>';
-      $output .= '<dd>' . t('Pages are usually identical for all anonymous users, while they can be personalized for each authenticated user. This is why entire pages can be cached for anonymous users, whereas they will have to be rebuilt for every authenticated user.') . '</dd>';
-      $output .= '<dd>' . t('To speed up your site for authenticated users, see the <a href=":dynamic_page_cache-help">Dynamic Page Cache module</a>.', [':dynamic_page_cache-help' => (\Drupal::moduleHandler()->moduleExists('dynamic_page_cache')) ? Url::fromRoute('help.page', ['name' => 'dynamic_page_cache'])->toString() : '#']) . '</p>';
-      $output .= '<dt>' . t('Configuring the internal page cache') . '</dt>';
-      $output .= '<dd>' . t('On the <a href=":cache-settings">Performance page</a>, you can configure how long browsers and proxies may cache pages based on the Cache-Control header; this setting is ignored by the Internal Page Cache module, which caches pages permanently until invalidation, unless they carry an Expires header. There is no other configuration.', [':cache-settings' => Url::fromRoute('system.performance_settings')->toString()]) . '</dd>';
-      $output .= '</dl>';
-
-      return $output;
-  }
-}
diff --git a/core/modules/page_cache/src/Hook/PageCacheHooks.php b/core/modules/page_cache/src/Hook/PageCacheHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..b2b59bc6e13a4dbf0d3bf9535714784dd14dc638
--- /dev/null
+++ b/core/modules/page_cache/src/Hook/PageCacheHooks.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\page_cache\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for page_cache.
+ */
+class PageCacheHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.page_cache':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Internal Page Cache module caches pages for anonymous users in the database. For more information, see the <a href=":pagecache-documentation">online documentation for the Internal Page Cache module</a>.', [
+          ':pagecache-documentation' => 'https://www.drupal.org/documentation/modules/internal_page_cache',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Speeding up your site') . '</dt>';
+        $output .= '<dd>' . t('Pages requested by anonymous users are stored the first time they are requested and then are reused. Depending on your site configuration and the amount of your web traffic tied to anonymous visitors, the caching system may significantly increase the speed of your site.') . '</dd>';
+        $output .= '<dd>' . t('Pages are usually identical for all anonymous users, while they can be personalized for each authenticated user. This is why entire pages can be cached for anonymous users, whereas they will have to be rebuilt for every authenticated user.') . '</dd>';
+        $output .= '<dd>' . t('To speed up your site for authenticated users, see the <a href=":dynamic_page_cache-help">Dynamic Page Cache module</a>.', [
+          ':dynamic_page_cache-help' => \Drupal::moduleHandler()->moduleExists('dynamic_page_cache') ? Url::fromRoute('help.page', [
+            'name' => 'dynamic_page_cache',
+          ])->toString() : '#',
+        ]) . '</p>';
+        $output .= '<dt>' . t('Configuring the internal page cache') . '</dt>';
+        $output .= '<dd>' . t('On the <a href=":cache-settings">Performance page</a>, you can configure how long browsers and proxies may cache pages based on the Cache-Control header; this setting is ignored by the Internal Page Cache module, which caches pages permanently until invalidation, unless they carry an Expires header. There is no other configuration.', [
+          ':cache-settings' => Url::fromRoute('system.performance_settings')->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/path/path.module b/core/modules/path/path.module
deleted file mode 100644
index 4186c9472da743ecbc2c0dc8a1718b5109d5b588..0000000000000000000000000000000000000000
--- a/core/modules/path/path.module
+++ /dev/null
@@ -1,158 +0,0 @@
-<?php
-
-/**
- * @file
- * Enables users to rename URLs.
- */
-
-use Drupal\Core\Url;
-use Drupal\Core\Entity\ContentEntityDeleteForm;
-use Drupal\Core\Entity\ContentEntityInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
-use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\path\PathAliasForm;
-use Drupal\path\PathAliasListBuilder;
-
-/**
- * Implements hook_help().
- */
-function path_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.path':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Path module allows you to specify an alias, or custom URL, for any existing internal system path. Aliases should not be confused with URL redirects, which allow you to forward a changed or inactive URL to a new URL. In addition to making URLs more readable, aliases also help search engines index content more effectively. Multiple aliases may be used for a single internal system path. To automate the aliasing of paths, you can install the contributed module <a href=":pathauto">Pathauto</a>. For more information, see the <a href=":path">online documentation for the Path module</a>.', [':path' => 'https://www.drupal.org/documentation/modules/path', ':pathauto' => 'https://www.drupal.org/project/pathauto']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Creating aliases') . '</dt>';
-      $output .= '<dd>' . t('If you create or edit a taxonomy term you can add an alias (for example <em>music/jazz</em>) in the field "URL alias". When creating or editing content you can add an alias (for example <em>about-us/team</em>) under the section "URL path settings" in the field "URL alias". Aliases for any other path can be added through the page <a href=":aliases">URL aliases</a>. To add aliases a user needs the permission <a href=":permissions">Create and edit URL aliases</a>.', [':aliases' => Url::fromRoute('entity.path_alias.collection')->toString(), ':permissions' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'path'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Managing aliases') . '</dt>';
-      $output .= '<dd>' . t('The Path module provides a way to search and view a <a href=":aliases">list of all aliases</a> that are in use on your website. Aliases can be added, edited and deleted through this list.', [':aliases' => Url::fromRoute('entity.path_alias.collection')->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'entity.path_alias.collection':
-      return '<p>' . t("An alias defines a different name for an existing URL path - for example, the alias 'about' for the URL path 'node/1'. A URL path can have multiple aliases.") . '</p>';
-
-    case 'entity.path_alias.add_form':
-      return '<p>' . t('Enter the path you wish to create the alias for, followed by the name of the new alias.') . '</p>';
-  }
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function path_entity_type_alter(array &$entity_types): void {
-  // @todo Remove the conditional once core fully supports "path_alias" as an
-  //   optional module. See https://drupal.org/node/3092090.
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  if (isset($entity_types['path_alias'])) {
-    $entity_types['path_alias']->setFormClass('default', PathAliasForm::class);
-    $entity_types['path_alias']->setFormClass('delete', ContentEntityDeleteForm::class);
-    $entity_types['path_alias']->setHandlerClass('route_provider', ['html' => AdminHtmlRouteProvider::class]);
-    $entity_types['path_alias']->setListBuilderClass(PathAliasListBuilder::class);
-    $entity_types['path_alias']->setLinkTemplate('collection', '/admin/config/search/path');
-    $entity_types['path_alias']->setLinkTemplate('add-form', '/admin/config/search/path/add');
-    $entity_types['path_alias']->setLinkTemplate('edit-form', '/admin/config/search/path/edit/{path_alias}');
-    $entity_types['path_alias']->setLinkTemplate('delete-form', '/admin/config/search/path/delete/{path_alias}');
-  }
-}
-
-/**
- * Implements hook_entity_base_field_info_alter().
- */
-function path_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
-  /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
-  if ($entity_type->id() === 'path_alias') {
-    $fields['langcode']->setDisplayOptions('form', [
-      'type' => 'language_select',
-      'weight' => 0,
-      'settings' => [
-        'include_locked' => FALSE,
-      ],
-    ]);
-
-    $fields['path']->setDisplayOptions('form', [
-      'type' => 'string_textfield',
-      'weight' => 5,
-      'settings' => [
-        'size' => 45,
-      ],
-    ]);
-
-    $fields['alias']->setDisplayOptions('form', [
-      'type' => 'string_textfield',
-      'weight' => 10,
-      'settings' => [
-        'size' => 45,
-      ],
-    ]);
-  }
-}
-
-/**
- * Implements hook_entity_base_field_info().
- */
-function path_entity_base_field_info(EntityTypeInterface $entity_type) {
-  if (in_array($entity_type->id(), ['taxonomy_term', 'node', 'media'], TRUE)) {
-    $fields['path'] = BaseFieldDefinition::create('path')
-      ->setLabel(t('URL alias'))
-      ->setTranslatable(TRUE)
-      ->setDisplayOptions('form', [
-        'type' => 'path',
-        'weight' => 30,
-      ])
-      ->setDisplayConfigurable('form', TRUE)
-      ->setComputed(TRUE);
-
-    return $fields;
-  }
-}
-
-/**
- * Implements hook_entity_translation_create().
- */
-function path_entity_translation_create(ContentEntityInterface $translation) {
-  foreach ($translation->getFieldDefinitions() as $field_name => $field_definition) {
-    if ($field_definition->getType() === 'path' && $translation->get($field_name)->pid) {
-      // If there are values and a path ID, update the langcode and unset the
-      // path ID to save this as a new alias.
-      $translation->get($field_name)->langcode = $translation->language()->getId();
-      $translation->get($field_name)->pid = NULL;
-    }
-  }
-}
-
-/**
- * Implements hook_field_widget_single_element_form_alter().
- */
-function path_field_widget_single_element_form_alter(&$element, FormStateInterface $form_state, $context) {
-  $field_definition = $context['items']->getFieldDefinition();
-  $field_name = $field_definition->getName();
-  $entity_type = $field_definition->getTargetEntityTypeId();
-  $widget_name = $context['widget']->getPluginId();
-
-  if ($entity_type === 'path_alias') {
-    if (($field_name === 'path' || $field_name === 'alias') && $widget_name === 'string_textfield') {
-      $element['value']['#field_prefix'] = \Drupal::service('router.request_context')->getCompleteBaseUrl();
-    }
-
-    if ($field_name === 'langcode') {
-      $element['value']['#description'] = t('A path alias set for a specific language will always be used when displaying this page in that language, and takes precedence over path aliases set as <em>- Not specified -</em>.');
-      $element['value']['#empty_value'] = LanguageInterface::LANGCODE_NOT_SPECIFIED;
-      $element['value']['#empty_option'] = t('- Not specified -');
-    }
-
-    if ($field_name === 'path') {
-      $element['value']['#description'] = t('Specify the existing path you wish to alias. For example: /node/28, /media/1, /taxonomy/term/1.');
-    }
-
-    if ($field_name === 'alias') {
-      $element['value']['#description'] = t('Specify an alternative path by which this data can be accessed. For example, type "/about" when writing an about page.');
-    }
-  }
-}
diff --git a/core/modules/path/src/Hook/PathHooks.php b/core/modules/path/src/Hook/PathHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..dd242afbf175dcdb9ed1fdd16169feef14e646a8
--- /dev/null
+++ b/core/modules/path/src/Hook/PathHooks.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace Drupal\path\Hook;
+
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\path\PathAliasListBuilder;
+use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
+use Drupal\Core\Entity\ContentEntityDeleteForm;
+use Drupal\path\PathAliasForm;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for path.
+ */
+class PathHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.path':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Path module allows you to specify an alias, or custom URL, for any existing internal system path. Aliases should not be confused with URL redirects, which allow you to forward a changed or inactive URL to a new URL. In addition to making URLs more readable, aliases also help search engines index content more effectively. Multiple aliases may be used for a single internal system path. To automate the aliasing of paths, you can install the contributed module <a href=":pathauto">Pathauto</a>. For more information, see the <a href=":path">online documentation for the Path module</a>.', [
+          ':path' => 'https://www.drupal.org/documentation/modules/path',
+          ':pathauto' => 'https://www.drupal.org/project/pathauto',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Creating aliases') . '</dt>';
+        $output .= '<dd>' . t('If you create or edit a taxonomy term you can add an alias (for example <em>music/jazz</em>) in the field "URL alias". When creating or editing content you can add an alias (for example <em>about-us/team</em>) under the section "URL path settings" in the field "URL alias". Aliases for any other path can be added through the page <a href=":aliases">URL aliases</a>. To add aliases a user needs the permission <a href=":permissions">Create and edit URL aliases</a>.', [
+          ':aliases' => Url::fromRoute('entity.path_alias.collection')->toString(),
+          ':permissions' => Url::fromRoute('user.admin_permissions.module', [
+            'modules' => 'path',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Managing aliases') . '</dt>';
+        $output .= '<dd>' . t('The Path module provides a way to search and view a <a href=":aliases">list of all aliases</a> that are in use on your website. Aliases can be added, edited and deleted through this list.', [
+          ':aliases' => Url::fromRoute('entity.path_alias.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'entity.path_alias.collection':
+        return '<p>' . t("An alias defines a different name for an existing URL path - for example, the alias 'about' for the URL path 'node/1'. A URL path can have multiple aliases.") . '</p>';
+
+      case 'entity.path_alias.add_form':
+        return '<p>' . t('Enter the path you wish to create the alias for, followed by the name of the new alias.') . '</p>';
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    // @todo Remove the conditional once core fully supports "path_alias" as an
+    //   optional module. See https://drupal.org/node/3092090.
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    if (isset($entity_types['path_alias'])) {
+      $entity_types['path_alias']->setFormClass('default', PathAliasForm::class);
+      $entity_types['path_alias']->setFormClass('delete', ContentEntityDeleteForm::class);
+      $entity_types['path_alias']->setHandlerClass('route_provider', ['html' => AdminHtmlRouteProvider::class]);
+      $entity_types['path_alias']->setListBuilderClass(PathAliasListBuilder::class);
+      $entity_types['path_alias']->setLinkTemplate('collection', '/admin/config/search/path');
+      $entity_types['path_alias']->setLinkTemplate('add-form', '/admin/config/search/path/add');
+      $entity_types['path_alias']->setLinkTemplate('edit-form', '/admin/config/search/path/edit/{path_alias}');
+      $entity_types['path_alias']->setLinkTemplate('delete-form', '/admin/config/search/path/delete/{path_alias}');
+    }
+  }
+
+  /**
+   * Implements hook_entity_base_field_info_alter().
+   */
+  #[Hook('entity_base_field_info_alter')]
+  public function entityBaseFieldInfoAlter(&$fields, EntityTypeInterface $entity_type) {
+    /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
+    if ($entity_type->id() === 'path_alias') {
+      $fields['langcode']->setDisplayOptions('form', [
+        'type' => 'language_select',
+        'weight' => 0,
+        'settings' => [
+          'include_locked' => FALSE,
+        ],
+      ]);
+      $fields['path']->setDisplayOptions('form', ['type' => 'string_textfield', 'weight' => 5, 'settings' => ['size' => 45]]);
+      $fields['alias']->setDisplayOptions('form', ['type' => 'string_textfield', 'weight' => 10, 'settings' => ['size' => 45]]);
+    }
+  }
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    if (in_array($entity_type->id(), ['taxonomy_term', 'node', 'media'], TRUE)) {
+      $fields['path'] = BaseFieldDefinition::create('path')->setLabel(t('URL alias'))->setTranslatable(TRUE)->setDisplayOptions('form', ['type' => 'path', 'weight' => 30])->setDisplayConfigurable('form', TRUE)->setComputed(TRUE);
+      return $fields;
+    }
+  }
+
+  /**
+   * Implements hook_entity_translation_create().
+   */
+  #[Hook('entity_translation_create')]
+  public function entityTranslationCreate(ContentEntityInterface $translation) {
+    foreach ($translation->getFieldDefinitions() as $field_name => $field_definition) {
+      if ($field_definition->getType() === 'path' && $translation->get($field_name)->pid) {
+        // If there are values and a path ID, update the langcode and unset the
+        // path ID to save this as a new alias.
+        $translation->get($field_name)->langcode = $translation->language()->getId();
+        $translation->get($field_name)->pid = NULL;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_field_widget_single_element_form_alter().
+   */
+  #[Hook('field_widget_single_element_form_alter')]
+  public function fieldWidgetSingleElementFormAlter(&$element, FormStateInterface $form_state, $context) {
+    $field_definition = $context['items']->getFieldDefinition();
+    $field_name = $field_definition->getName();
+    $entity_type = $field_definition->getTargetEntityTypeId();
+    $widget_name = $context['widget']->getPluginId();
+    if ($entity_type === 'path_alias') {
+      if (($field_name === 'path' || $field_name === 'alias') && $widget_name === 'string_textfield') {
+        $element['value']['#field_prefix'] = \Drupal::service('router.request_context')->getCompleteBaseUrl();
+      }
+      if ($field_name === 'langcode') {
+        $element['value']['#description'] = t('A path alias set for a specific language will always be used when displaying this page in that language, and takes precedence over path aliases set as <em>- Not specified -</em>.');
+        $element['value']['#empty_value'] = LanguageInterface::LANGCODE_NOT_SPECIFIED;
+        $element['value']['#empty_option'] = t('- Not specified -');
+      }
+      if ($field_name === 'path') {
+        $element['value']['#description'] = t('Specify the existing path you wish to alias. For example: /node/28, /media/1, /taxonomy/term/1.');
+      }
+      if ($field_name === 'alias') {
+        $element['value']['#description'] = t('Specify an alternative path by which this data can be accessed. For example, type "/about" when writing an about page.');
+      }
+    }
+  }
+
+}
diff --git a/core/modules/path/tests/modules/path_test_node_grants/path_test_node_grants.module b/core/modules/path/tests/modules/path_test_node_grants/path_test_node_grants.module
deleted file mode 100644
index 369e8665f1fe65de53341ebea33cf77ea95ab74e..0000000000000000000000000000000000000000
--- a/core/modules/path/tests/modules/path_test_node_grants/path_test_node_grants.module
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains hook implementations for the Path test with node grants module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Implements hook_node_grants().
- */
-function path_test_node_grants_node_grants(AccountInterface $account, $operation): array {
-  $grants = [];
-  return $grants;
-}
diff --git a/core/modules/path/tests/modules/path_test_node_grants/src/Hook/PathTestNodeGrantsHooks.php b/core/modules/path/tests/modules/path_test_node_grants/src/Hook/PathTestNodeGrantsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..7576de8d0c49e72032c4762da4d0981d4eb42915
--- /dev/null
+++ b/core/modules/path/tests/modules/path_test_node_grants/src/Hook/PathTestNodeGrantsHooks.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\path_test_node_grants\Hook;
+
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for path_test_node_grants.
+ */
+class PathTestNodeGrantsHooks {
+
+  /**
+   * Implements hook_node_grants().
+   */
+  #[Hook('node_grants')]
+  public function nodeGrants(AccountInterface $account, $operation) : array {
+    $grants = [];
+    return $grants;
+  }
+
+}
diff --git a/core/modules/pgsql/pgsql.module b/core/modules/pgsql/pgsql.module
deleted file mode 100644
index 3f46f1bd46e51a3282b0fed186ae3ac2394c8c3a..0000000000000000000000000000000000000000
--- a/core/modules/pgsql/pgsql.module
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * The PostgreSQL module provides the connection between Drupal and a PostgreSQL database.
- */
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function pgsql_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.pgsql':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The PostgreSQL module provides the connection between Drupal and a PostgreSQL database. For more information, see the <a href=":pgsql">online documentation for the PostgreSQL module</a>.', [':pgsql' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/postgresql-module']) . '</p>';
-      return $output;
-
-  }
-}
diff --git a/core/modules/pgsql/src/Hook/PgsqlHooks.php b/core/modules/pgsql/src/Hook/PgsqlHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..11fe1d0c2b6260bf089c5601881f0a8e9ea6ce62
--- /dev/null
+++ b/core/modules/pgsql/src/Hook/PgsqlHooks.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\pgsql\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for pgsql.
+ */
+class PgsqlHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.pgsql':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The PostgreSQL module provides the connection between Drupal and a PostgreSQL database. For more information, see the <a href=":pgsql">online documentation for the PostgreSQL module</a>.', [
+          ':pgsql' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/postgresql-module',
+        ]) . '</p>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/phpass/phpass.module b/core/modules/phpass/phpass.module
deleted file mode 100644
index 49fe0c43481a5755008dee774e958d528662dafe..0000000000000000000000000000000000000000
--- a/core/modules/phpass/phpass.module
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides the password checking algorithm used prior to version 10.1.0.
- */
-
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function phpass_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.phpass':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Password Compatibility module provides the password checking algorithm for user accounts created with Drupal prior to version 10.1.0. For more information, see the <a href=":phpass">online documentation for the Password Compatibility module</a>.', [':phpass' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/password-compatibility-module']) . '</p>';
-      $output .= '<p>' . t('Drupal 10.1.0 and later use a different algorithm to compute the hashed password. This provides better security against brute-force attacks. The hashed passwords are different from the ones computed with Drupal versions before 10.1.0.') . '</p>';
-      $output .= '<p>' . t('When the Password Compatibility module is installed, a user can log in with a username and password created before Drupal 10.1.0. The first time these credentials are used, a new hash is computed and saved. From then on, the user will be able to log in with the same username and password whether or not this module is installed.') . '</p>';
-      $output .= '<p>' . t('Passwords created before Drupal 10.1.0 <strong>will not work</strong> unless they are used at least once while this module is installed. Make sure that you can log in before uninstalling this module.') . '</p>';
-      return $output;
-
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for system_modules_uninstall_confirm_form.
- */
-function phpass_form_system_modules_uninstall_confirm_form_alter(array &$form, FormStateInterface $form_state): void {
-  $modules = \Drupal::keyValueExpirable('modules_uninstall')
-    ->get(\Drupal::currentUser()->id());
-  if (!in_array('phpass', $modules)) {
-    return;
-  }
-  \Drupal::messenger()->addWarning(t('Make sure that you can log in before uninstalling the Password Compatibility module. For more information, see the <a href=":phpass">online documentation for the Password Compatibility module</a>.', [
-    ':phpass' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/password-compatibility-module',
-  ]));
-}
diff --git a/core/modules/phpass/src/Hook/PhpassHooks.php b/core/modules/phpass/src/Hook/PhpassHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..1420e4e7b4a559e21c433d8e09b6179f18556c56
--- /dev/null
+++ b/core/modules/phpass/src/Hook/PhpassHooks.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\phpass\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for phpass.
+ */
+class PhpassHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.phpass':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Password Compatibility module provides the password checking algorithm for user accounts created with Drupal prior to version 10.1.0. For more information, see the <a href=":phpass">online documentation for the Password Compatibility module</a>.', [
+          ':phpass' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/password-compatibility-module',
+        ]) . '</p>';
+        $output .= '<p>' . t('Drupal 10.1.0 and later use a different algorithm to compute the hashed password. This provides better security against brute-force attacks. The hashed passwords are different from the ones computed with Drupal versions before 10.1.0.') . '</p>';
+        $output .= '<p>' . t('When the Password Compatibility module is installed, a user can log in with a username and password created before Drupal 10.1.0. The first time these credentials are used, a new hash is computed and saved. From then on, the user will be able to log in with the same username and password whether or not this module is installed.') . '</p>';
+        $output .= '<p>' . t('Passwords created before Drupal 10.1.0 <strong>will not work</strong> unless they are used at least once while this module is installed. Make sure that you can log in before uninstalling this module.') . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for system_modules_uninstall_confirm_form.
+   */
+  #[Hook('form_system_modules_uninstall_confirm_form_alter')]
+  public function formSystemModulesUninstallConfirmFormAlter(array &$form, FormStateInterface $form_state) : void {
+    $modules = \Drupal::keyValueExpirable('modules_uninstall')->get(\Drupal::currentUser()->id());
+    if (!in_array('phpass', $modules)) {
+      return;
+    }
+    \Drupal::messenger()->addWarning(t('Make sure that you can log in before uninstalling the Password Compatibility module. For more information, see the <a href=":phpass">online documentation for the Password Compatibility module</a>.', [
+      ':phpass' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/password-compatibility-module',
+    ]));
+  }
+
+}
diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module
index c1c0ce94ae305c7b896e87b3f8f6c9da13f34e21..fd064c3d441069aaec076fce1bd6af9b91287c4c 100644
--- a/core/modules/responsive_image/responsive_image.module
+++ b/core/modules/responsive_image/responsive_image.module
@@ -2,78 +2,15 @@
 
 /**
  * @file
- * Responsive image display formatter for image fields.
  */
 
-use Drupal\Core\Url;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Logger\RfcLogLevel;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\image\Entity\ImageStyle;
 use Drupal\responsive_image\Entity\ResponsiveImageStyle;
 use Drupal\responsive_image\ResponsiveImageStyleInterface;
 use Drupal\breakpoint\BreakpointInterface;
 
-/**
- * Implements hook_help().
- */
-function responsive_image_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.responsive_image':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Responsive Image module provides an image formatter that allows browsers to select which image file to display based on media queries or which image file types the browser supports, using the HTML 5 picture and source elements and/or the sizes, srcset and type attributes. For more information, see the <a href=":responsive_image">online documentation for the Responsive Image module</a>.', [':responsive_image' => 'https://www.drupal.org/documentation/modules/responsive_image']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Defining responsive image styles') . '</dt>';
-      $output .= '<dd>' . t('By creating responsive image styles you define which options the browser has in selecting which image file to display. In most cases this means providing different image sizes based on the viewport size. On the <a href=":responsive_image_style">Responsive image styles</a> page, click <em>Add responsive image style</em> to create a new style. First choose a label, a fallback image style and a breakpoint group and click Save.', [':responsive_image_style' => Url::fromRoute('entity.responsive_image_style.collection')->toString()]) . '</dd>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Fallback image style') . '</dt>';
-      $output .= '<dd>' . t('The fallback image style is typically the smallest size image you expect to appear in this space. The fallback image should only appear on a site if an error occurs.') . '</dd>';
-      $output .= '<dt>' . t('Breakpoint groups: viewport sizing vs art direction') . '</dt>';
-      $output .= '<dd>' . t('The breakpoint group typically only needs a single breakpoint with an empty media query in order to do <em>viewport sizing.</em> Multiple breakpoints are used for changing the crop or aspect ratio of images at different viewport sizes, which is often referred to as <em>art direction.</em> A new breakpoint group should be created for each aspect ratio to avoid content shift. Once you select a breakpoint group, you can choose which breakpoints to use for the responsive image style. By default, the option <em>do not use this breakpoint</em> is selected for each breakpoint. See the <a href=":breakpoint_help">help page of the Breakpoint module</a> for more information.', [':breakpoint_help' => Url::fromRoute('help.page', ['name' => 'breakpoint'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Breakpoint settings: sizes vs image styles') . '</dt>';
-      $output .= '<dd>' . t('While you have the option to provide only one image style per breakpoint, the sizes attribute allows you to provide more options to browsers as to which image file it can display. If using sizes field and art direction, all selected image styles should use the same aspect ratio to avoid content shifting. Breakpoints are defined in the configuration files of the theme.') . '</dd>';
-      $output .= '<dt>' . t('Sizes field') . '</dt>';
-      $output .= '<dd>' . t('The sizes attribute paired with the srcset attribute provides information on how much space these images take up within the viewport at different browser breakpoints, but the aspect ratios should remain the same across those breakpoints. Once the sizes option is selected, you can let the browser know the size of this image in relation to the site layout, using the <em>Sizes</em> field. For a hero image that always fills the entire screen, you could simply enter 100vw, which means 100% of the viewport width. For an image that fills 90% of the screen for small viewports, but only fills 40% of the screen when the viewport is larger than 40em (typically 640px), you could enter "(min-width: 40em) 40vw, 90vw" in the Sizes field. The last item in the comma-separated list is the smallest viewport size: other items in the comma-separated list should have a media condition paired with an image width. <em>Media conditions</em> are similar to a media query, often a min-width paired with a viewport width using em or px units: e.g. (min-width: 640px) or (min-width: 40em). This is paired with the <em>image width</em> at that viewport size using px, em or vw units. The vw unit is viewport width and is used instead of a percentage because the percentage always refers to the width of the entire viewport.') . '</dd>';
-      $output .= '<dt>' . t('Image styles for sizes') . '</dt>';
-      $output .= '<dd>' . t('Below the Sizes field you can choose multiple image styles so the browser can choose the best image file size to fill the space defined in the Sizes field. Typically you will want to use image styles that resize your image to have options that range from the smallest px width possible for the space the image will appear in to the largest px width possible, with a variety of widths in between. You may want to provide image styles with widths that are 1.5x to 2x the space available in the layout to account for high resolution screens. Image styles can be defined on the <a href=":image_styles">Image styles page</a> that is provided by the <a href=":image_help">Image module</a>.', [':image_styles' => Url::fromRoute('entity.image_style.collection')->toString(), ':image_help' => Url::fromRoute('help.page', ['name' => 'image'])->toString()]) . '</dd>';
-      $output .= '</dl></dd>';
-      $output .= '<dt>' . t('Using responsive image styles in Image fields') . '</dt>';
-      $output .= '<dd>' . t('After defining responsive image styles, you can use them in the display settings for your Image fields, so that the site displays responsive images using the HTML5 picture tag. Open the Manage display page for the entity type (content type, taxonomy vocabulary, etc.) that the Image field is attached to. Choose the format <em>Responsive image</em>, click the Edit icon, and select one of the responsive image styles that you have created. For general information on how to manage fields and their display see the <a href=":field_ui">Field UI module help page</a>. For background information about entities and fields see the <a href=":field_help">Field module help page</a>.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'entity.responsive_image_style.collection':
-      return '<p>' . t('A responsive image style associates an image style with each breakpoint defined by your theme.') . '</p>';
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function responsive_image_theme(): array {
-  return [
-    'responsive_image' => [
-      'variables' => [
-        'uri' => NULL,
-        'attributes' => [],
-        'responsive_image_style_id' => [],
-        'height' => NULL,
-        'width' => NULL,
-      ],
-    ],
-    'responsive_image_formatter' => [
-      'variables' => [
-        'item' => NULL,
-        'item_attributes' => NULL,
-        'url' => NULL,
-        'responsive_image_style_id' => NULL,
-      ],
-    ],
-  ];
-}
-
 /**
  * Prepares variables for responsive image formatter templates.
  *
@@ -535,14 +472,3 @@ function _responsive_image_image_style_url($style_name, $path) {
   }
   return $file_url_generator->generateString($path);
 }
-
-/**
- * Implements hook_library_info_alter().
- *
- * Load responsive_image.js whenever ajax is added.
- */
-function responsive_image_library_info_alter(array &$libraries, $module) {
-  if ($module === 'core' && isset($libraries['drupal.ajax'])) {
-    $libraries['drupal.ajax']['dependencies'][] = 'responsive_image/ajax';
-  }
-}
diff --git a/core/modules/responsive_image/src/Hook/ResponsiveImageHooks.php b/core/modules/responsive_image/src/Hook/ResponsiveImageHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..bbcd00167c7fee6910645751762dc8582cec5c89
--- /dev/null
+++ b/core/modules/responsive_image/src/Hook/ResponsiveImageHooks.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Drupal\responsive_image\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for responsive_image.
+ */
+class ResponsiveImageHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.responsive_image':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Responsive Image module provides an image formatter that allows browsers to select which image file to display based on media queries or which image file types the browser supports, using the HTML 5 picture and source elements and/or the sizes, srcset and type attributes. For more information, see the <a href=":responsive_image">online documentation for the Responsive Image module</a>.', [
+          ':responsive_image' => 'https://www.drupal.org/documentation/modules/responsive_image',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Defining responsive image styles') . '</dt>';
+        $output .= '<dd>' . t('By creating responsive image styles you define which options the browser has in selecting which image file to display. In most cases this means providing different image sizes based on the viewport size. On the <a href=":responsive_image_style">Responsive image styles</a> page, click <em>Add responsive image style</em> to create a new style. First choose a label, a fallback image style and a breakpoint group and click Save.', [
+          ':responsive_image_style' => Url::fromRoute('entity.responsive_image_style.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Fallback image style') . '</dt>';
+        $output .= '<dd>' . t('The fallback image style is typically the smallest size image you expect to appear in this space. The fallback image should only appear on a site if an error occurs.') . '</dd>';
+        $output .= '<dt>' . t('Breakpoint groups: viewport sizing vs art direction') . '</dt>';
+        $output .= '<dd>' . t('The breakpoint group typically only needs a single breakpoint with an empty media query in order to do <em>viewport sizing.</em> Multiple breakpoints are used for changing the crop or aspect ratio of images at different viewport sizes, which is often referred to as <em>art direction.</em> A new breakpoint group should be created for each aspect ratio to avoid content shift. Once you select a breakpoint group, you can choose which breakpoints to use for the responsive image style. By default, the option <em>do not use this breakpoint</em> is selected for each breakpoint. See the <a href=":breakpoint_help">help page of the Breakpoint module</a> for more information.', [
+          ':breakpoint_help' => Url::fromRoute('help.page', [
+            'name' => 'breakpoint',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Breakpoint settings: sizes vs image styles') . '</dt>';
+        $output .= '<dd>' . t('While you have the option to provide only one image style per breakpoint, the sizes attribute allows you to provide more options to browsers as to which image file it can display. If using sizes field and art direction, all selected image styles should use the same aspect ratio to avoid content shifting. Breakpoints are defined in the configuration files of the theme.') . '</dd>';
+        $output .= '<dt>' . t('Sizes field') . '</dt>';
+        $output .= '<dd>' . t('The sizes attribute paired with the srcset attribute provides information on how much space these images take up within the viewport at different browser breakpoints, but the aspect ratios should remain the same across those breakpoints. Once the sizes option is selected, you can let the browser know the size of this image in relation to the site layout, using the <em>Sizes</em> field. For a hero image that always fills the entire screen, you could simply enter 100vw, which means 100% of the viewport width. For an image that fills 90% of the screen for small viewports, but only fills 40% of the screen when the viewport is larger than 40em (typically 640px), you could enter "(min-width: 40em) 40vw, 90vw" in the Sizes field. The last item in the comma-separated list is the smallest viewport size: other items in the comma-separated list should have a media condition paired with an image width. <em>Media conditions</em> are similar to a media query, often a min-width paired with a viewport width using em or px units: e.g. (min-width: 640px) or (min-width: 40em). This is paired with the <em>image width</em> at that viewport size using px, em or vw units. The vw unit is viewport width and is used instead of a percentage because the percentage always refers to the width of the entire viewport.') . '</dd>';
+        $output .= '<dt>' . t('Image styles for sizes') . '</dt>';
+        $output .= '<dd>' . t('Below the Sizes field you can choose multiple image styles so the browser can choose the best image file size to fill the space defined in the Sizes field. Typically you will want to use image styles that resize your image to have options that range from the smallest px width possible for the space the image will appear in to the largest px width possible, with a variety of widths in between. You may want to provide image styles with widths that are 1.5x to 2x the space available in the layout to account for high resolution screens. Image styles can be defined on the <a href=":image_styles">Image styles page</a> that is provided by the <a href=":image_help">Image module</a>.', [
+          ':image_styles' => Url::fromRoute('entity.image_style.collection')->toString(),
+          ':image_help' => Url::fromRoute('help.page', [
+            'name' => 'image',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '</dl></dd>';
+        $output .= '<dt>' . t('Using responsive image styles in Image fields') . '</dt>';
+        $output .= '<dd>' . t('After defining responsive image styles, you can use them in the display settings for your Image fields, so that the site displays responsive images using the HTML5 picture tag. Open the Manage display page for the entity type (content type, taxonomy vocabulary, etc.) that the Image field is attached to. Choose the format <em>Responsive image</em>, click the Edit icon, and select one of the responsive image styles that you have created. For general information on how to manage fields and their display see the <a href=":field_ui">Field UI module help page</a>. For background information about entities and fields see the <a href=":field_help">Field module help page</a>.', [
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+          ':field_help' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'entity.responsive_image_style.collection':
+        return '<p>' . t('A responsive image style associates an image style with each breakpoint defined by your theme.') . '</p>';
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'responsive_image' => [
+        'variables' => [
+          'uri' => NULL,
+          'attributes' => [],
+          'responsive_image_style_id' => [],
+          'height' => NULL,
+          'width' => NULL,
+        ],
+      ],
+      'responsive_image_formatter' => [
+        'variables' => [
+          'item' => NULL,
+          'item_attributes' => NULL,
+          'url' => NULL,
+          'responsive_image_style_id' => NULL,
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_library_info_alter().
+   *
+   * Load responsive_image.js whenever ajax is added.
+   */
+  #[Hook('library_info_alter')]
+  public function libraryInfoAlter(array &$libraries, $module) {
+    if ($module === 'core' && isset($libraries['drupal.ajax'])) {
+      $libraries['drupal.ajax']['dependencies'][] = 'responsive_image/ajax';
+    }
+  }
+
+}
diff --git a/core/modules/rest/rest.module b/core/modules/rest/rest.module
deleted file mode 100644
index 814d80441338fe37fb32a529e306d017de329181..0000000000000000000000000000000000000000
--- a/core/modules/rest/rest.module
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-/**
- * @file
- * RESTful web services module.
- */
-
-use Drupal\Core\Url;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function rest_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.rest':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The RESTful Web Services module provides a framework for exposing REST resources on your site. It provides support for content entity types such as the main site content, comments, content blocks, taxonomy terms, and user accounts, etc. (see the <a href=":field">Field module help page</a> for more information about entities). REST support for content items of the Node module is installed by default, and support for other types of content entities can be enabled. Other modules may add support for other types of REST resources. For more information, see the <a href=":rest">online documentation for the RESTful Web Services module</a>.', [':rest' => 'https://www.drupal.org/documentation/modules/rest', ':field' => (\Drupal::moduleHandler()->moduleExists('field')) ? Url::fromRoute('help.page', ['name' => 'field'])->toString() : '#']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Installing supporting modules') . '</dt>';
-      $output .= '<dd>' . t('In order to use REST on a website, you need to install modules that provide serialization and authentication services. You can use the Core module <a href=":serialization">serialization</a> for serialization and <a href=":basic_auth">HTTP Basic Authentication</a> for authentication, or install a contributed or custom module.', [':serialization' => (\Drupal::moduleHandler()->moduleExists('serialization')) ? Url::fromRoute('help.page', ['name' => 'serialization'])->toString() : 'https://www.drupal.org/docs/8/core/modules/serialization/overview', ':basic_auth' => (\Drupal::moduleHandler()->moduleExists('basic_auth')) ? Url::fromRoute('help.page', ['name' => 'basic_auth'])->toString() : 'https://www.drupal.org/docs/8/core/modules/basic_auth/overview']) . '</dd>';
-      $output .= '<dt>' . t('Enabling REST support for an entity type') . '</dt>';
-      $output .= '<dd>' . t('REST support for content types (provided by the <a href=":node">Node</a> module) is enabled by default. To enable support for other content entity types, you can use a <a href=":config" target="blank">process based on configuration editing</a> or the contributed <a href=":restui">REST UI module</a>.', [':node' => (\Drupal::moduleHandler()->moduleExists('node')) ? Url::fromRoute('help.page', ['name' => 'node'])->toString() : '#', ':config' => 'https://www.drupal.org/documentation/modules/rest', ':restui' => 'https://www.drupal.org/project/restui']) . '</dd>';
-      $output .= '<dd>' . t('You will also need to grant anonymous users permission to perform each of the REST operations you want to be available, and set up authentication properly to authorize web requests.') . '</dd>';
-      $output .= '<dt>' . t('General') . '</dt>';
-      $output .= '<dd>' . t('The <a href=":rest-docs">RESTful Web Services</a> and <a href=":jsonapi-docs">JSON:API</a> modules serve similar purposes. <a href=":comparison">Read the comparison of the RESTFul Web Services and JSON:API modules</a> to determine the best choice for your site.', [
-        ':rest-docs' => 'https://www.drupal.org/docs/8/core/modules/rest',
-        ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/json-api',
-        ':comparison' => 'https://www.drupal.org/docs/8/modules/jsonapi/jsonapi-vs-cores-rest-module',
-      ]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
diff --git a/core/modules/rest/src/Hook/RestHooks.php b/core/modules/rest/src/Hook/RestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..e4dc99d9d604c00eeb73028541235a0790ec3e0b
--- /dev/null
+++ b/core/modules/rest/src/Hook/RestHooks.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\rest\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for rest.
+ */
+class RestHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.rest':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The RESTful Web Services module provides a framework for exposing REST resources on your site. It provides support for content entity types such as the main site content, comments, content blocks, taxonomy terms, and user accounts, etc. (see the <a href=":field">Field module help page</a> for more information about entities). REST support for content items of the Node module is installed by default, and support for other types of content entities can be enabled. Other modules may add support for other types of REST resources. For more information, see the <a href=":rest">online documentation for the RESTful Web Services module</a>.', [
+          ':rest' => 'https://www.drupal.org/documentation/modules/rest',
+          ':field' => \Drupal::moduleHandler()->moduleExists('field') ? Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString() : '#',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Installing supporting modules') . '</dt>';
+        $output .= '<dd>' . t('In order to use REST on a website, you need to install modules that provide serialization and authentication services. You can use the Core module <a href=":serialization">serialization</a> for serialization and <a href=":basic_auth">HTTP Basic Authentication</a> for authentication, or install a contributed or custom module.', [
+          ':serialization' => \Drupal::moduleHandler()->moduleExists('serialization') ? Url::fromRoute('help.page', [
+            'name' => 'serialization',
+          ])->toString() : 'https://www.drupal.org/docs/8/core/modules/serialization/overview',
+          ':basic_auth' => \Drupal::moduleHandler()->moduleExists('basic_auth') ? Url::fromRoute('help.page', [
+            'name' => 'basic_auth',
+          ])->toString() : 'https://www.drupal.org/docs/8/core/modules/basic_auth/overview',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Enabling REST support for an entity type') . '</dt>';
+        $output .= '<dd>' . t('REST support for content types (provided by the <a href=":node">Node</a> module) is enabled by default. To enable support for other content entity types, you can use a <a href=":config" target="blank">process based on configuration editing</a> or the contributed <a href=":restui">REST UI module</a>.', [
+          ':node' => \Drupal::moduleHandler()->moduleExists('node') ? Url::fromRoute('help.page', [
+            'name' => 'node',
+          ])->toString() : '#',
+          ':config' => 'https://www.drupal.org/documentation/modules/rest',
+          ':restui' => 'https://www.drupal.org/project/restui',
+        ]) . '</dd>';
+        $output .= '<dd>' . t('You will also need to grant anonymous users permission to perform each of the REST operations you want to be available, and set up authentication properly to authorize web requests.') . '</dd>';
+        $output .= '<dt>' . t('General') . '</dt>';
+        $output .= '<dd>' . t('The <a href=":rest-docs">RESTful Web Services</a> and <a href=":jsonapi-docs">JSON:API</a> modules serve similar purposes. <a href=":comparison">Read the comparison of the RESTFul Web Services and JSON:API modules</a> to determine the best choice for your site.', [
+          ':rest-docs' => 'https://www.drupal.org/docs/8/core/modules/rest',
+          ':jsonapi-docs' => 'https://www.drupal.org/docs/8/modules/json-api',
+          ':comparison' => 'https://www.drupal.org/docs/8/modules/jsonapi/jsonapi-vs-cores-rest-module',
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
deleted file mode 100644
index 102ce376af90d85f8983f5c4c0511543793e1cc9..0000000000000000000000000000000000000000
--- a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains hook implementations for testing REST module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Access\AccessResultReasonInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Implements hook_entity_type_alter().
- */
-function config_test_rest_entity_type_alter(array &$entity_types): void {
-  // Undo part of what config_test_entity_type_alter() did: remove this
-  // config_test_no_status entity type, because it uses the same entity class as
-  // the config_test entity type, which makes REST deserialization impossible.
-  unset($entity_types['config_test_no_status']);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_access().
- */
-function config_test_rest_config_test_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  // Add permission, so that EntityResourceTestBase's scenarios can test access
-  // being denied. By default, all access is always allowed for the config_test
-  // config entity.
-  $access_result = AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions();
-  if (!$access_result->isAllowed() && $access_result instanceof AccessResultReasonInterface) {
-    $access_result->setReason("The 'view config_test' permission is required.");
-  }
-  return $access_result;
-}
diff --git a/core/modules/rest/tests/modules/config_test_rest/src/Hook/ConfigTestRestHooks.php b/core/modules/rest/tests/modules/config_test_rest/src/Hook/ConfigTestRestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..808c95082f776b05b37f725f249d37de60119e6c
--- /dev/null
+++ b/core/modules/rest/tests/modules/config_test_rest/src/Hook/ConfigTestRestHooks.php
@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\config_test_rest\Hook;
+
+use Drupal\Core\Access\AccessResultReasonInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for config_test_rest.
+ */
+class ConfigTestRestHooks {
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    // Undo part of what config_test_entity_type_alter() did: remove this
+    // config_test_no_status entity type, because it uses the same entity class as
+    // the config_test entity type, which makes REST deserialization impossible.
+    unset($entity_types['config_test_no_status']);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_access().
+   */
+  #[Hook('config_test_access')]
+  public function configTestAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    // Add permission, so that EntityResourceTestBase's scenarios can test access
+    // being denied. By default, all access is always allowed for the config_test
+    // config entity.
+    $access_result = AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions();
+    if (!$access_result->isAllowed() && $access_result instanceof AccessResultReasonInterface) {
+      $access_result->setReason("The 'view config_test' permission is required.");
+    }
+    return $access_result;
+  }
+
+}
diff --git a/core/modules/rest/tests/modules/rest_test/rest_test.module b/core/modules/rest/tests/modules/rest_test/rest_test.module
deleted file mode 100644
index e9268fced5a237958cb2841c558ac73c2d1e1749..0000000000000000000000000000000000000000
--- a/core/modules/rest/tests/modules/rest_test/rest_test.module
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains hook implementations for testing REST module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Access\AccessResult;
-
-/**
- * Implements hook_entity_field_access().
- *
- * @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::setUp()
- */
-function rest_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
-  // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPost()
-  // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPatch()
-  if ($field_definition->getName() === 'field_rest_test') {
-    switch ($operation) {
-      case 'view':
-        // Never ever allow this field to be viewed: this lets
-        // EntityResourceTestBase::testGet() test in a "vanilla" way.
-        return AccessResult::forbidden();
-
-      case 'edit':
-        return AccessResult::forbidden();
-    }
-  }
-
-  // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()
-  // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPatch()
-  if ($field_definition->getName() === 'field_rest_test_multivalue') {
-    switch ($operation) {
-      case 'view':
-        // Never ever allow this field to be viewed: this lets
-        // EntityResourceTestBase::testGet() test in a "vanilla" way.
-        return AccessResult::forbidden();
-    }
-  }
-
-  // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()
-  // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPatch()
-  if ($field_definition->getName() === 'rest_test_validation') {
-    switch ($operation) {
-      case 'view':
-        // Never ever allow this field to be viewed: this lets
-        // EntityResourceTestBase::testGet() test in a "vanilla" way.
-        return AccessResult::forbidden();
-    }
-  }
-
-  // No opinion.
-  return AccessResult::neutral();
-}
-
-/**
- * Implements hook_entity_base_field_info().
- */
-function rest_test_entity_base_field_info(EntityTypeInterface $entity_type) {
-  $fields = [];
-  $fields['rest_test_validation'] = BaseFieldDefinition::create('string')
-    ->setLabel(t('REST test validation field'))
-    ->setDescription(t('A text field with some special validations attached used for testing purposes'))
-    ->addConstraint('rest_test_validation');
-
-  return $fields;
-}
diff --git a/core/modules/rest/tests/modules/rest_test/src/Hook/RestTestHooks.php b/core/modules/rest/tests/modules/rest_test/src/Hook/RestTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..7e2ba048cd5352b514bce90edd0e9ce72c1e75ef
--- /dev/null
+++ b/core/modules/rest/tests/modules/rest_test/src/Hook/RestTestHooks.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\rest_test\Hook;
+
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for rest_test.
+ */
+class RestTestHooks {
+
+  /**
+   * Implements hook_entity_field_access().
+   *
+   * @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::setUp()
+   */
+  #[Hook('entity_field_access')]
+  public function entityFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
+    // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPost()
+    // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPatch()
+    if ($field_definition->getName() === 'field_rest_test') {
+      switch ($operation) {
+        case 'view':
+          // Never ever allow this field to be viewed: this lets
+          // EntityResourceTestBase::testGet() test in a "vanilla" way.
+          return AccessResult::forbidden();
+
+        case 'edit':
+          return AccessResult::forbidden();
+      }
+    }
+    // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()
+    // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPatch()
+    if ($field_definition->getName() === 'field_rest_test_multivalue') {
+      switch ($operation) {
+        case 'view':
+          // Never ever allow this field to be viewed: this lets
+          // EntityResourceTestBase::testGet() test in a "vanilla" way.
+          return AccessResult::forbidden();
+      }
+    }
+    // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testGet()
+    // @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPatch()
+    if ($field_definition->getName() === 'rest_test_validation') {
+      switch ($operation) {
+        case 'view':
+          // Never ever allow this field to be viewed: this lets
+          // EntityResourceTestBase::testGet() test in a "vanilla" way.
+          return AccessResult::forbidden();
+      }
+    }
+    // No opinion.
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    $fields = [];
+    $fields['rest_test_validation'] = BaseFieldDefinition::create('string')->setLabel(t('REST test validation field'))->setDescription(t('A text field with some special validations attached used for testing purposes'))->addConstraint('rest_test_validation');
+    return $fields;
+  }
+
+}
diff --git a/core/modules/rest/tests/modules/rest_test_views/rest_test_views.module b/core/modules/rest/tests/modules/rest_test_views/rest_test_views.module
deleted file mode 100644
index 7aaa699b17908a44dd33161263b11ecf3aceb889..0000000000000000000000000000000000000000
--- a/core/modules/rest/tests/modules/rest_test_views/rest_test_views.module
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-/**
- * @file
- * Test hook implementations for the REST views test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\views\ViewExecutable;
-
-/**
- * Implements hook_views_post_execute().
- */
-function rest_test_views_views_post_execute(ViewExecutable $view) {
-  // Attach a custom header to the test_data_export view.
-  if ($view->id() === 'test_serializer_display_entity') {
-    if ($value = \Drupal::state()->get('rest_test_views_set_header', FALSE)) {
-      $view->getResponse()->headers->set('Custom-Header', $value);
-    }
-  }
-
-}
diff --git a/core/modules/rest/tests/modules/rest_test_views/src/Hook/RestTestViewsHooks.php b/core/modules/rest/tests/modules/rest_test_views/src/Hook/RestTestViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..d7bc69f05e36c8676aaf335db86fd6d78d271e30
--- /dev/null
+++ b/core/modules/rest/tests/modules/rest_test_views/src/Hook/RestTestViewsHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\rest_test_views\Hook;
+
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for rest_test_views.
+ */
+class RestTestViewsHooks {
+
+  /**
+   * Implements hook_views_post_execute().
+   */
+  #[Hook('views_post_execute')]
+  public function viewsPostExecute(ViewExecutable $view) {
+    // Attach a custom header to the test_data_export view.
+    if ($view->id() === 'test_serializer_display_entity') {
+      if ($value = \Drupal::state()->get('rest_test_views_set_header', FALSE)) {
+        $view->getResponse()->headers->set('Custom-Header', $value);
+      }
+    }
+  }
+
+}
diff --git a/core/modules/search/search.module b/core/modules/search/search.module
index 1f922ade061f1af7264ecb37ea95880069c222ec..49a42bb54832704fa724c5ceffd1195dae0731cb 100644
--- a/core/modules/search/search.module
+++ b/core/modules/search/search.module
@@ -2,56 +2,12 @@
 
 /**
  * @file
- * Enables site-wide keyword searching.
  */
 
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Unicode;
-use Drupal\block\BlockInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
 use Drupal\search\SearchTextProcessorInterface;
 
-/**
- * Implements hook_help().
- */
-function search_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.search':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Search module provides the ability to set up search pages based on plugins provided by other modules. In Drupal core, there are two page-type plugins: the Content page type provides keyword searching for content managed by the Node module, and the Users page type provides keyword searching for registered users. Contributed modules may provide other page-type plugins. For more information, see the <a href=":search-module">online documentation for the Search module</a>.', [':search-module' => 'https://www.drupal.org/documentation/modules/search']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Configuring search pages') . '</dt>';
-      $output .= '<dd>' . t('To configure search pages, visit the <a href=":search-settings">Search pages page</a>. In the Search pages section, you can add a new search page, edit the configuration of existing search pages, enable and disable search pages, and choose the default search page. Each enabled search page has a URL path starting with <em>search</em>, and each will appear as a tab or local task link on the <a href=":search-url">search page</a>; you can configure the text that is shown in the tab. In addition, some search page plugins have additional settings that you can configure for each search page.', [':search-settings' => Url::fromRoute('entity.search_page.collection')->toString(), ':search-url' => Url::fromRoute('search.view')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Managing the search index') . '</dt>';
-      $output .= '<dd>' . t('Some search page plugins, such as the core Content search page, index searchable text using the Drupal core search index, and will not work unless content is indexed. Indexing is done during <em>cron</em> runs, so it requires a <a href=":cron">cron maintenance task</a> to be set up. There are also several settings affecting indexing that can be configured on the <a href=":search-settings">Search pages page</a>: the number of items to index per cron run, the minimum word length to index, and how to handle Chinese, Japanese, and Korean characters.', [':cron' => Url::fromRoute('system.cron_settings')->toString(), ':search-settings' => Url::fromRoute('entity.search_page.collection')->toString()]) . '</dd>';
-      $output .= '<dd>' . t('Modules providing search page plugins generally ensure that content-related actions on your site (creating, editing, or deleting content and comments) automatically cause affected content items to be marked for indexing or reindexing at the next cron run. When content is marked for reindexing, the previous content remains in the index until cron runs, at which time it is replaced by the new content. However, there are some actions related to the structure of your site that do not cause affected content to be marked for reindexing. Examples of structure-related actions that affect content include deleting or editing taxonomy terms, installing or uninstalling modules that add text to content (such as Taxonomy, Comment, and field-providing modules), and modifying the fields or display parameters of your content types. If you take one of these actions and you want to ensure that the search index is updated to reflect your changed site structure, you can mark all content for reindexing by clicking the "Re-index site" button on the <a href=":search-settings">Search pages page</a>. If you have a lot of content on your site, it may take several cron runs for the content to be reindexed.', [':search-settings' => Url::fromRoute('entity.search_page.collection')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Displaying the Search block') . '</dt>';
-      $output .= '<dd>' . t('The Search module includes a block, which can be enabled and configured on the <a href=":blocks">Block layout page</a>, if you have the Block module installed; the default block title is Search, and it is the Search form block in the Forms category, if you wish to add another instance. The block is available to users with the <a href=":search_permission">Use search</a> permission, and it performs a search using the configured default search page.', [':blocks' => (\Drupal::moduleHandler()->moduleExists('block')) ? Url::fromRoute('block.admin_display')->toString() : '#', ':search_permission' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'search'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Searching your site') . '</dt>';
-      $output .= '<dd>' . t('Users with <a href=":search_permission">Use search</a> permission can use the Search block and <a href=":search">Search page</a>. Users with the <a href=":node_permission">View published content</a> permission can use configured search pages of type <em>Content</em> to search for content containing exact keywords; in addition, users with <a href=":search_permission">Use advanced search</a> permission can use more complex search filtering. Users with the <a href=":user_permission">View user information</a> permission can use configured search pages of type <em>Users</em> to search for active users containing the keyword anywhere in the username, and users with the <a href=":user_permission">Administer users</a> permission can search for active and blocked users, by email address or username keyword.', [':search' => Url::fromRoute('search.view')->toString(), ':search_permission' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'search'])->toString(), ':node_permission' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'node'])->toString(), ':user_permission' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'user'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Extending the Search module') . '</dt>';
-      $output .= '<dd>' . t('By default, the Search module only supports exact keyword matching in content searches. You can modify this behavior by installing a language-specific stemming module for your language (such as <a href=":porterstemmer_url">Porter Stemmer</a> for American English), which allows words such as walk, walking, and walked to be matched in the Search module. Another approach is to use a third-party search technology with stemming or partial word matching features built in, such as <a href=":solr_url">Apache Solr</a> or <a href=":sphinx_url">Sphinx</a>. There are also contributed modules that provide additional search pages. These and other <a href=":contrib-search">search-related contributed modules</a> can be downloaded by visiting Drupal.org.', [':contrib-search' => 'https://www.drupal.org/project/project_module?f[2]=im_vid_3%3A105', ':porterstemmer_url' => 'https://www.drupal.org/project/porterstemmer', ':solr_url' => 'https://www.drupal.org/project/apachesolr', ':sphinx_url' => 'https://www.drupal.org/project/sphinx']) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function search_theme(): array {
-  return [
-    'search_result' => [
-      'variables' => ['result' => NULL, 'plugin_id' => NULL],
-      'file' => 'search.pages.inc',
-    ],
-  ];
-}
-
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
@@ -68,20 +24,6 @@ function search_preprocess_block(&$variables) {
   }
 }
 
-/**
- * Implements hook_cron().
- *
- * Fires updateIndex() in the plugins for all indexable active search pages,
- * and cleans up dirty words.
- */
-function search_cron() {
-  /** @var \Drupal\search\SearchPageRepositoryInterface $search_page_repository */
-  $search_page_repository = \Drupal::service('search.search_page_repository');
-  foreach ($search_page_repository->getIndexableSearchPages() as $entity) {
-    $entity->getPlugin()->updateIndex();
-  }
-}
-
 /**
  * @defgroup search Search interface
  * @{
@@ -403,34 +345,3 @@ function _search_find_match_with_simplify($key, $text, $boundary, $langcode = NU
   // so they are Unicode-safe byte positions, not character positions.
   return trim(substr($text, $words[$start_index][1], $words[$end_index][1] - $words[$start_index][1]));
 }
-
-/**
- * Implements hook_form_FORM_ID_alter() for the search_block_form form.
- *
- * Since the exposed form is a GET form, we don't want it to send the form
- * tokens. However, you cannot make this happen in the form builder function
- * itself, because the tokens are added to the form after the builder function
- * is called. So, we have to do it in a form_alter.
- *
- * @see \Drupal\search\Form\SearchBlockForm
- */
-function search_form_search_block_form_alter(&$form, FormStateInterface $form_state): void {
-  $form['form_build_id']['#access'] = FALSE;
-  $form['form_token']['#access'] = FALSE;
-  $form['form_id']['#access'] = FALSE;
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for block entities.
- */
-function search_block_presave(BlockInterface $block) {
-  // @see \Drupal\search\Plugin\Block\SearchBlock
-  if ($block->getPluginId() === 'search_form_block') {
-    $settings = $block->get('settings');
-    if ($settings['page_id'] === '') {
-      @trigger_error('Saving a search block with an empty page ID is deprecated in drupal:11.1.0 and removed in drupal:12.0.0. To use the default search page, use NULL. See https://www.drupal.org/node/3463132', E_USER_DEPRECATED);
-      $settings['page_id'] = NULL;
-      $block->set('settings', $settings);
-    }
-  }
-}
diff --git a/core/modules/search/search.pages.inc b/core/modules/search/search.pages.inc
index a9e49ffc21a6b9900fe058bbdd4fddea08926ea2..afdb8fd635fa4c24530a37c3bf11e19daaaf4815 100644
--- a/core/modules/search/search.pages.inc
+++ b/core/modules/search/search.pages.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * User page callbacks for the Search module.
  */
 
 use Drupal\Component\Utility\UrlHelper;
diff --git a/core/modules/search/src/Hook/SearchHooks.php b/core/modules/search/src/Hook/SearchHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..dc507ce4504ce9b4e5fa4f9fec61a98a02a16ec0
--- /dev/null
+++ b/core/modules/search/src/Hook/SearchHooks.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Drupal\search\Hook;
+
+use Drupal\block\BlockInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for search.
+ */
+class SearchHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.search':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Search module provides the ability to set up search pages based on plugins provided by other modules. In Drupal core, there are two page-type plugins: the Content page type provides keyword searching for content managed by the Node module, and the Users page type provides keyword searching for registered users. Contributed modules may provide other page-type plugins. For more information, see the <a href=":search-module">online documentation for the Search module</a>.', [':search-module' => 'https://www.drupal.org/documentation/modules/search']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Configuring search pages') . '</dt>';
+        $output .= '<dd>' . t('To configure search pages, visit the <a href=":search-settings">Search pages page</a>. In the Search pages section, you can add a new search page, edit the configuration of existing search pages, enable and disable search pages, and choose the default search page. Each enabled search page has a URL path starting with <em>search</em>, and each will appear as a tab or local task link on the <a href=":search-url">search page</a>; you can configure the text that is shown in the tab. In addition, some search page plugins have additional settings that you can configure for each search page.', [
+          ':search-settings' => Url::fromRoute('entity.search_page.collection')->toString(),
+          ':search-url' => Url::fromRoute('search.view')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Managing the search index') . '</dt>';
+        $output .= '<dd>' . t('Some search page plugins, such as the core Content search page, index searchable text using the Drupal core search index, and will not work unless content is indexed. Indexing is done during <em>cron</em> runs, so it requires a <a href=":cron">cron maintenance task</a> to be set up. There are also several settings affecting indexing that can be configured on the <a href=":search-settings">Search pages page</a>: the number of items to index per cron run, the minimum word length to index, and how to handle Chinese, Japanese, and Korean characters.', [
+          ':cron' => Url::fromRoute('system.cron_settings')->toString(),
+          ':search-settings' => Url::fromRoute('entity.search_page.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dd>' . t('Modules providing search page plugins generally ensure that content-related actions on your site (creating, editing, or deleting content and comments) automatically cause affected content items to be marked for indexing or reindexing at the next cron run. When content is marked for reindexing, the previous content remains in the index until cron runs, at which time it is replaced by the new content. However, there are some actions related to the structure of your site that do not cause affected content to be marked for reindexing. Examples of structure-related actions that affect content include deleting or editing taxonomy terms, installing or uninstalling modules that add text to content (such as Taxonomy, Comment, and field-providing modules), and modifying the fields or display parameters of your content types. If you take one of these actions and you want to ensure that the search index is updated to reflect your changed site structure, you can mark all content for reindexing by clicking the "Re-index site" button on the <a href=":search-settings">Search pages page</a>. If you have a lot of content on your site, it may take several cron runs for the content to be reindexed.', [
+          ':search-settings' => Url::fromRoute('entity.search_page.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Displaying the Search block') . '</dt>';
+        $output .= '<dd>' . t('The Search module includes a block, which can be enabled and configured on the <a href=":blocks">Block layout page</a>, if you have the Block module installed; the default block title is Search, and it is the Search form block in the Forms category, if you wish to add another instance. The block is available to users with the <a href=":search_permission">Use search</a> permission, and it performs a search using the configured default search page.', [
+          ':blocks' => \Drupal::moduleHandler()->moduleExists('block') ? Url::fromRoute('block.admin_display')->toString() : '#',
+          ':search_permission' => Url::fromRoute('user.admin_permissions.module', [
+            'modules' => 'search',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Searching your site') . '</dt>';
+        $output .= '<dd>' . t('Users with <a href=":search_permission">Use search</a> permission can use the Search block and <a href=":search">Search page</a>. Users with the <a href=":node_permission">View published content</a> permission can use configured search pages of type <em>Content</em> to search for content containing exact keywords; in addition, users with <a href=":search_permission">Use advanced search</a> permission can use more complex search filtering. Users with the <a href=":user_permission">View user information</a> permission can use configured search pages of type <em>Users</em> to search for active users containing the keyword anywhere in the username, and users with the <a href=":user_permission">Administer users</a> permission can search for active and blocked users, by email address or username keyword.', [
+          ':search' => Url::fromRoute('search.view')->toString(),
+          ':search_permission' => Url::fromRoute('user.admin_permissions.module', [
+            'modules' => 'search',
+          ])->toString(),
+          ':node_permission' => Url::fromRoute('user.admin_permissions.module', [
+            'modules' => 'node',
+          ])->toString(),
+          ':user_permission' => Url::fromRoute('user.admin_permissions.module', [
+            'modules' => 'user',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Extending the Search module') . '</dt>';
+        $output .= '<dd>' . t('By default, the Search module only supports exact keyword matching in content searches. You can modify this behavior by installing a language-specific stemming module for your language (such as <a href=":porterstemmer_url">Porter Stemmer</a> for American English), which allows words such as walk, walking, and walked to be matched in the Search module. Another approach is to use a third-party search technology with stemming or partial word matching features built in, such as <a href=":solr_url">Apache Solr</a> or <a href=":sphinx_url">Sphinx</a>. There are also contributed modules that provide additional search pages. These and other <a href=":contrib-search">search-related contributed modules</a> can be downloaded by visiting Drupal.org.', [
+          ':contrib-search' => 'https://www.drupal.org/project/project_module?f[2]=im_vid_3%3A105',
+          ':porterstemmer_url' => 'https://www.drupal.org/project/porterstemmer',
+          ':solr_url' => 'https://www.drupal.org/project/apachesolr',
+          ':sphinx_url' => 'https://www.drupal.org/project/sphinx',
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'search_result' => [
+        'variables' => [
+          'result' => NULL,
+          'plugin_id' => NULL,
+        ],
+        'file' => 'search.pages.inc',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_cron().
+   *
+   * Fires updateIndex() in the plugins for all indexable active search pages,
+   * and cleans up dirty words.
+   */
+  #[Hook('cron')]
+  public function cron() {
+    /** @var \Drupal\search\SearchPageRepositoryInterface $search_page_repository */
+    $search_page_repository = \Drupal::service('search.search_page_repository');
+    foreach ($search_page_repository->getIndexableSearchPages() as $entity) {
+      $entity->getPlugin()->updateIndex();
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for the search_block_form form.
+   *
+   * Since the exposed form is a GET form, we don't want it to send the form
+   * tokens. However, you cannot make this happen in the form builder function
+   * itself, because the tokens are added to the form after the builder function
+   * is called. So, we have to do it in a form_alter.
+   *
+   * @see \Drupal\search\Form\SearchBlockForm
+   */
+  #[Hook('form_search_block_form_alter')]
+  public function formSearchBlockFormAlter(&$form, FormStateInterface $form_state) : void {
+    $form['form_build_id']['#access'] = FALSE;
+    $form['form_token']['#access'] = FALSE;
+    $form['form_id']['#access'] = FALSE;
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for block entities.
+   */
+  #[Hook('block_presave')]
+  public function blockPresave(BlockInterface $block) {
+    // @see \Drupal\search\Plugin\Block\SearchBlock
+    if ($block->getPluginId() === 'search_form_block') {
+      $settings = $block->get('settings');
+      if ($settings['page_id'] === '') {
+        @trigger_error('Saving a search block with an empty page ID is deprecated in drupal:11.1.0 and removed in drupal:12.0.0. To use the default search page, use NULL. See https://www.drupal.org/node/3463132', E_USER_DEPRECATED);
+        $settings['page_id'] = NULL;
+        $block->set('settings', $settings);
+      }
+    }
+  }
+
+}
diff --git a/core/modules/search/tests/modules/search_date_query_alter/search_date_query_alter.module b/core/modules/search/tests/modules/search_date_query_alter/search_date_query_alter.module
deleted file mode 100644
index a09af99a2536bbff51db0d61964ab484e4287097..0000000000000000000000000000000000000000
--- a/core/modules/search/tests/modules/search_date_query_alter/search_date_query_alter.module
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * Adds date conditions to node searches.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Database\Query\AlterableInterface;
-
-/**
- * Implements hook_query_TAG_alter().
- *
- * Tags search_$type with $type node_search.
- */
-function search_date_query_alter_query_search_node_search_alter(AlterableInterface $query) {
-  // Start date Sat, 19 Mar 2016 00:00:00 GMT.
-  $query->condition('n.created', 1458345600, '>=');
-  // End date Sun, 20 Mar 2016 00:00:00 GMT.
-  $query->condition('n.created', 1458432000, '<');
-}
diff --git a/core/modules/search/tests/modules/search_date_query_alter/src/Hook/SearchDateQueryAlterHooks.php b/core/modules/search/tests/modules/search_date_query_alter/src/Hook/SearchDateQueryAlterHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f599df199ed99cb7f9618a4d910b53e80af8abcd
--- /dev/null
+++ b/core/modules/search/tests/modules/search_date_query_alter/src/Hook/SearchDateQueryAlterHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\search_date_query_alter\Hook;
+
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for search_date_query_alter.
+ */
+class SearchDateQueryAlterHooks {
+
+  /**
+   * Implements hook_query_TAG_alter().
+   *
+   * Tags search_$type with $type node_search.
+   */
+  #[Hook('query_search_node_search_alter')]
+  public function querySearchNodeSearchAlter(AlterableInterface $query) {
+    // Start date Sat, 19 Mar 2016 00:00:00 GMT.
+    $query->condition('n.created', 1458345600, '>=');
+    // End date Sun, 20 Mar 2016 00:00:00 GMT.
+    $query->condition('n.created', 1458432000, '<');
+  }
+
+}
diff --git a/core/modules/search/tests/modules/search_langcode_test/search_langcode_test.module b/core/modules/search/tests/modules/search_langcode_test/search_langcode_test.module
deleted file mode 100644
index 780d0fdbba2f3c04734962458ace9ddb8262f16c..0000000000000000000000000000000000000000
--- a/core/modules/search/tests/modules/search_langcode_test/search_langcode_test.module
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-/**
- * @file
- * Tests the preprocessing of search text.
- *
- * Preprocessing is tested when the language code is passed to the preprocess
- * hook and also when with alternate verb forms for the stemming test.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_search_preprocess().
- */
-function search_langcode_test_search_preprocess($text, $langcode = NULL) {
-  if (isset($langcode) && $langcode == 'en') {
-    // Add the alternate verb forms for the word "testing".
-    if ($text == 'we are testing') {
-      $text .= ' test tested';
-    }
-    // Prints the langcode for testPreprocessLangcode() and adds some
-    // extra text.
-    else {
-      \Drupal::messenger()->addStatus('Langcode Preprocess Test: ' . $langcode);
-      $text .= 'Additional text';
-    }
-  }
-  // Prints the langcode for testPreprocessLangcode().
-  elseif (isset($langcode)) {
-    \Drupal::messenger()->addStatus('Langcode Preprocess Test: ' . $langcode);
-
-    // Preprocessing for the excerpt test.
-    if ($langcode == 'ex') {
-      $text = str_replace('finding', 'find', $text);
-      $text = str_replace('finds', 'find', $text);
-      $text = str_replace('dic', ' dependency injection container', $text);
-      $text = str_replace('hypertext markup language', 'html', $text);
-    }
-  }
-
-  return $text;
-}
diff --git a/core/modules/search/tests/modules/search_langcode_test/src/Hook/SearchLangcodeTestHooks.php b/core/modules/search/tests/modules/search_langcode_test/src/Hook/SearchLangcodeTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..6b09bde12a95ea795fc8d071c03b75eb9e28a8fc
--- /dev/null
+++ b/core/modules/search/tests/modules/search_langcode_test/src/Hook/SearchLangcodeTestHooks.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\search_langcode_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for search_langcode_test.
+ */
+class SearchLangcodeTestHooks {
+
+  /**
+   * Implements hook_search_preprocess().
+   */
+  #[Hook('search_preprocess')]
+  public function searchPreprocess($text, $langcode = NULL) {
+    if (isset($langcode) && $langcode == 'en') {
+      // Add the alternate verb forms for the word "testing".
+      if ($text == 'we are testing') {
+        $text .= ' test tested';
+      }
+      else {
+        \Drupal::messenger()->addStatus('Langcode Preprocess Test: ' . $langcode);
+        $text .= 'Additional text';
+      }
+    }
+    elseif (isset($langcode)) {
+      \Drupal::messenger()->addStatus('Langcode Preprocess Test: ' . $langcode);
+      // Preprocessing for the excerpt test.
+      if ($langcode == 'ex') {
+        $text = str_replace('finding', 'find', $text);
+        $text = str_replace('finds', 'find', $text);
+        $text = str_replace('dic', ' dependency injection container', $text);
+        $text = str_replace('hypertext markup language', 'html', $text);
+      }
+    }
+    return $text;
+  }
+
+}
diff --git a/core/modules/search/tests/modules/search_query_alter/search_query_alter.module b/core/modules/search/tests/modules/search_query_alter/search_query_alter.module
deleted file mode 100644
index 1d68476f8fb278d58e872b586966811cbd55fede..0000000000000000000000000000000000000000
--- a/core/modules/search/tests/modules/search_query_alter/search_query_alter.module
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module that alters search queries.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Database\Query\AlterableInterface;
-
-/**
- * Implements hook_query_TAG_alter().
- *
- * Tags search_$type with $type node_search.
- */
-function search_query_alter_query_search_node_search_alter(AlterableInterface $query) {
-  // For testing purposes, restrict the query to node type 'article' only.
-  $query->condition('n.type', 'article');
-}
diff --git a/core/modules/search/tests/modules/search_query_alter/src/Hook/SearchQueryAlterHooks.php b/core/modules/search/tests/modules/search_query_alter/src/Hook/SearchQueryAlterHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..90486b5a17aed1596d938d0656a9c5fd90164500
--- /dev/null
+++ b/core/modules/search/tests/modules/search_query_alter/src/Hook/SearchQueryAlterHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\search_query_alter\Hook;
+
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for search_query_alter.
+ */
+class SearchQueryAlterHooks {
+
+  /**
+   * Implements hook_query_TAG_alter().
+   *
+   * Tags search_$type with $type node_search.
+   */
+  #[Hook('query_search_node_search_alter')]
+  public function querySearchNodeSearchAlter(AlterableInterface $query) {
+    // For testing purposes, restrict the query to node type 'article' only.
+    $query->condition('n.type', 'article');
+  }
+
+}
diff --git a/core/modules/serialization/serialization.module b/core/modules/serialization/serialization.module
deleted file mode 100644
index 07c8bb2a7405f1b159bdaa7346656a3367a7cb76..0000000000000000000000000000000000000000
--- a/core/modules/serialization/serialization.module
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides a service for (de)serializing data to/from formats such as JSON and XML.
- */
-
-use Drupal\Core\Url;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function serialization_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.serialization':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Serialization module provides a service for serializing and deserializing data to and from formats such as JSON and XML.') . '</p>';
-      $output .= '<p>' . t('Serialization is the process of converting data structures like arrays and objects into a string. This allows the data to be represented in a way that is easy to exchange and store (for example, for transmission over the Internet or for storage in a local file system). These representations can then be deserialized to get back to the original data structures.') . '</p>';
-      $output .= '<p>' . t('The serializer splits this process into two parts. Normalization converts an object to a normalized array structure. Encoding takes that array and converts it to a string.') . '</p>';
-      $output .= '<p>' . t('This module does not have a user interface. It is used by other modules which need to serialize data, such as <a href=":rest">REST</a>.', [':rest' => (\Drupal::moduleHandler()->moduleExists('rest')) ? Url::fromRoute('help.page', ['name' => 'rest'])->toString() : '#']) . '</p>';
-      $output .= '<p>' . t('For more information, see the <a href=":doc_url">online documentation for the Serialization module</a>.', [':doc_url' => 'https://www.drupal.org/documentation/modules/serialization']) . '</p>';
-      return $output;
-  }
-}
diff --git a/core/modules/serialization/src/Hook/SerializationHooks.php b/core/modules/serialization/src/Hook/SerializationHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..e2d6b36244f90d35f9b175a8347e684fd4b66f65
--- /dev/null
+++ b/core/modules/serialization/src/Hook/SerializationHooks.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\serialization\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for serialization.
+ */
+class SerializationHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.serialization':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Serialization module provides a service for serializing and deserializing data to and from formats such as JSON and XML.') . '</p>';
+        $output .= '<p>' . t('Serialization is the process of converting data structures like arrays and objects into a string. This allows the data to be represented in a way that is easy to exchange and store (for example, for transmission over the Internet or for storage in a local file system). These representations can then be deserialized to get back to the original data structures.') . '</p>';
+        $output .= '<p>' . t('The serializer splits this process into two parts. Normalization converts an object to a normalized array structure. Encoding takes that array and converts it to a string.') . '</p>';
+        $output .= '<p>' . t('This module does not have a user interface. It is used by other modules which need to serialize data, such as <a href=":rest">REST</a>.', [
+          ':rest' => \Drupal::moduleHandler()->moduleExists('rest') ? Url::fromRoute('help.page', [
+            'name' => 'rest',
+          ])->toString() : '#',
+        ]) . '</p>';
+        $output .= '<p>' . t('For more information, see the <a href=":doc_url">online documentation for the Serialization module</a>.', [':doc_url' => 'https://www.drupal.org/documentation/modules/serialization']) . '</p>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/serialization/tests/modules/entity_serialization_test/entity_serialization_test.module b/core/modules/serialization/tests/modules/entity_serialization_test/entity_serialization_test.module
deleted file mode 100644
index 700c5bd0f56ce9d84aaafec62377040e2da11679..0000000000000000000000000000000000000000
--- a/core/modules/serialization/tests/modules/entity_serialization_test/entity_serialization_test.module
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-/**
- * @file
- * Test support module for entity serialization tests.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Access\AccessResult;
-
-/**
- * Implements hook_entity_field_access_alter().
- *
- * Overrides some default access control to support testing.
- *
- * @see Drupal\serialization\Tests\EntitySerializationTest::testUserNormalize()
- */
-function entity_serialization_test_entity_field_access_alter(array &$grants, array $context) {
-  // Override default access control from UserAccessControlHandler to allow
-  // access to 'pass' field for the test user.
-  if ($context['field_definition']->getName() == 'pass' && $context['account']->getAccountName() == 'serialization_test_user') {
-    $grants[':default'] = AccessResult::allowed()->inheritCacheability($grants[':default'])->addCacheableDependency($context['items']->getEntity());
-  }
-}
diff --git a/core/modules/serialization/tests/modules/entity_serialization_test/src/Hook/EntitySerializationTestHooks.php b/core/modules/serialization/tests/modules/entity_serialization_test/src/Hook/EntitySerializationTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..4015befa54cb36cf2124f48b5bc1230b8ad25261
--- /dev/null
+++ b/core/modules/serialization/tests/modules/entity_serialization_test/src/Hook/EntitySerializationTestHooks.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\entity_serialization_test\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for entity_serialization_test.
+ */
+class EntitySerializationTestHooks {
+
+  /**
+   * Implements hook_entity_field_access_alter().
+   *
+   * Overrides some default access control to support testing.
+   *
+   * @see Drupal\serialization\Tests\EntitySerializationTest::testUserNormalize()
+   */
+  #[Hook('entity_field_access_alter')]
+  public function entityFieldAccessAlter(array &$grants, array $context) {
+    // Override default access control from UserAccessControlHandler to allow
+    // access to 'pass' field for the test user.
+    if ($context['field_definition']->getName() == 'pass' && $context['account']->getAccountName() == 'serialization_test_user') {
+      $grants[':default'] = AccessResult::allowed()->inheritCacheability($grants[':default'])->addCacheableDependency($context['items']->getEntity());
+    }
+  }
+
+}
diff --git a/core/modules/settings_tray/settings_tray.module b/core/modules/settings_tray/settings_tray.module
index 35a4466575673c036d9ac579bf8f3a11a0e4d254..409b18e0a389a3ac85c8975550fa1ba5adcab264 100644
--- a/core/modules/settings_tray/settings_tray.module
+++ b/core/modules/settings_tray/settings_tray.module
@@ -2,58 +2,10 @@
 
 /**
  * @file
- * Allows configuring blocks and other configuration from the site front-end.
  */
 
-use Drupal\Core\Url;
-use Drupal\Core\Asset\AttachedAssetsInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Language\LanguageInterface;
 use Drupal\block\Entity\Block;
 use Drupal\block\BlockInterface;
-use Drupal\settings_tray\Block\BlockEntitySettingTrayForm;
-
-/**
- * Implements hook_help().
- */
-function settings_tray_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.settings_tray':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Settings Tray module allows users with the <a href=":administer_block_permission">Administer blocks</a> and <a href=":contextual_permission">Use contextual links</a> permissions to edit blocks without visiting a separate page. For more information, see the <a href=":handbook_url">online documentation for the Settings Tray module</a>.', [':handbook_url' => 'https://www.drupal.org/documentation/modules/settings_tray', ':administer_block_permission' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'block'])->toString(), ':contextual_permission' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'contextual'])->toString()]) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Editing blocks in place') . '</dt>';
-      $output .= '<dd>';
-      $output .= '<p>' . t('To edit blocks in place, either click the <strong>Edit</strong> button in the toolbar and then click on the block, or choose "Quick edit" from the block\'s contextual link. (See the <a href=":contextual">Contextual Links module help</a> for more information about how to use contextual links.)', [':contextual' => Url::fromRoute('help.page', ['name' => 'contextual'])->toString()]) . '</p>';
-      $output .= '<p>' . t('The Settings Tray for the block will open in a sidebar, with a compact form for configuring what the block shows.') . '</p>';
-      $output .= '<p>' . t('Save the form and the changes will be immediately visible on the page.') . '</p>';
-      $output .= '</dd>';
-      $output .= '</dl>';
-      return ['#markup' => $output];
-  }
-}
-
-/**
- * Implements hook_contextual_links_view_alter().
- *
- * Change Configure Blocks into off_canvas links.
- */
-function settings_tray_contextual_links_view_alter(&$element, $items) {
-  if (isset($element['#links']['settings-trayblock-configure'])) {
-    // Place settings_tray link first.
-    $settings_tray_link = $element['#links']['settings-trayblock-configure'];
-    unset($element['#links']['settings-trayblock-configure']);
-    $element['#links'] = ['settings-trayblock-configure' => $settings_tray_link] + $element['#links'];
-
-    // If this is content block change title to avoid duplicate "Quick Edit".
-    if (isset($element['#links']['block-contentblock-edit'])) {
-      $element['#links']['settings-trayblock-configure']['title'] = t('Quick edit settings');
-    }
-
-    $element['#attached']['library'][] = 'core/drupal.dialog.off_canvas';
-  }
-}
 
 /**
  * Checks if a block has overrides.
@@ -72,35 +24,6 @@ function _settings_tray_has_block_overrides(BlockInterface $block) {
   return \Drupal::config($block->getEntityType()->getConfigPrefix() . '.' . $block->id())->hasOverrides();
 }
 
-/**
- * Implements hook_block_view_alter().
- */
-function settings_tray_block_view_alter(array &$build) {
-  if (isset($build['#contextual_links']['block'])) {
-    // Ensure that contextual links vary by whether the block has config overrides
-    // or not.
-    // @see _contextual_links_to_id()
-    $build['#contextual_links']['block']['metadata']['has_overrides'] = _settings_tray_has_block_overrides($build['#block']) ? 1 : 0;
-  }
-
-  // Force a new 'data-contextual-id' attribute on blocks when this module is
-  // enabled so as not to reuse stale data cached client-side.
-  // @todo Remove when https://www.drupal.org/node/2773591 is fixed.
-  $build['#contextual_links']['settings_tray'] = [
-    'route_parameters' => [],
-  ];
-}
-
-/**
- * Implements hook_entity_type_build().
- */
-function settings_tray_entity_type_build(array &$entity_types) {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  $entity_types['block']
-    ->setFormClass('settings_tray', BlockEntitySettingTrayForm::class)
-    ->setLinkTemplate('settings_tray-form', '/admin/structure/block/manage/{block}/settings-tray');
-}
-
 /**
  * Implements hook_preprocess_HOOK() for block templates.
  */
@@ -127,63 +50,3 @@ function settings_tray_preprocess_block(&$variables) {
     }
   }
 }
-
-/**
- * Implements hook_toolbar_alter().
- *
- * Alters the 'contextual' toolbar tab if it exists (meaning the user is allowed
- * to use contextual links) and if they can administer blocks.
- *
- * @todo Remove the "administer blocks" requirement in
- *   https://www.drupal.org/node/2822965.
- *
- * @see contextual_toolbar()
- */
-function settings_tray_toolbar_alter(&$items) {
-  $items['contextual']['#cache']['contexts'][] = 'user.permissions';
-  if (isset($items['contextual']['tab']) && \Drupal::currentUser()->hasPermission('administer blocks')) {
-    $items['contextual']['#weight'] = -1000;
-    $items['contextual']['#attached']['library'][] = 'settings_tray/drupal.settings_tray';
-    $items['contextual']['tab']['#attributes']['data-drupal-settingstray'] = 'toggle';
-
-    // Set a class on items to mark whether they should be active in edit mode.
-    // @todo Create a dynamic method for modules to set their own items.
-    //   https://www.drupal.org/node/2784589.
-    $edit_mode_items = ['contextual'];
-    foreach ($items as $key => $item) {
-      if (!in_array($key, $edit_mode_items) && (!isset($items[$key]['#wrapper_attributes']['class']) || !in_array('hidden', $items[$key]['#wrapper_attributes']['class']))) {
-        $items[$key]['#wrapper_attributes']['class'][] = 'edit-mode-inactive';
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_block_alter().
- *
- * Ensures every block plugin definition has an 'settings_tray' form specified.
- *
- * @see \Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck
- */
-function settings_tray_block_alter(&$definitions) {
-  foreach ($definitions as &$definition) {
-    // If a block plugin does not define its own 'settings_tray' form, use the
-    // plugin class itself.
-    if (!isset($definition['forms']['settings_tray'])) {
-      $definition['forms']['settings_tray'] = $definition['class'];
-    }
-  }
-}
-
-/**
- * Implements hook_css_alter().
- */
-function settings_tray_css_alter(&$css, AttachedAssetsInterface $assets, LanguageInterface $language) {
-  // @todo Remove once conditional ordering is introduced in
-  //   https://www.drupal.org/node/1945262.
-  $path = \Drupal::service('extension.list.module')->getPath('settings_tray') . '/css/settings_tray.theme.css';
-  if (isset($css[$path])) {
-    // Use 200 to come after CSS_AGGREGATE_THEME.
-    $css[$path]['group'] = 200;
-  }
-}
diff --git a/core/modules/settings_tray/src/Hook/SettingsTrayHooks.php b/core/modules/settings_tray/src/Hook/SettingsTrayHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..e6e196335cce152682dd20354cb6e7c363ae5799
--- /dev/null
+++ b/core/modules/settings_tray/src/Hook/SettingsTrayHooks.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace Drupal\settings_tray\Hook;
+
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\settings_tray\Block\BlockEntitySettingTrayForm;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for settings_tray.
+ */
+class SettingsTrayHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.settings_tray':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Settings Tray module allows users with the <a href=":administer_block_permission">Administer blocks</a> and <a href=":contextual_permission">Use contextual links</a> permissions to edit blocks without visiting a separate page. For more information, see the <a href=":handbook_url">online documentation for the Settings Tray module</a>.', [
+          ':handbook_url' => 'https://www.drupal.org/documentation/modules/settings_tray',
+          ':administer_block_permission' => Url::fromRoute('user.admin_permissions.module', [
+            'modules' => 'block',
+          ])->toString(),
+          ':contextual_permission' => Url::fromRoute('user.admin_permissions.module', [
+            'modules' => 'contextual',
+          ])->toString(),
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Editing blocks in place') . '</dt>';
+        $output .= '<dd>';
+        $output .= '<p>' . t('To edit blocks in place, either click the <strong>Edit</strong> button in the toolbar and then click on the block, or choose "Quick edit" from the block\'s contextual link. (See the <a href=":contextual">Contextual Links module help</a> for more information about how to use contextual links.)', [
+          ':contextual' => Url::fromRoute('help.page', [
+            'name' => 'contextual',
+          ])->toString(),
+        ]) . '</p>';
+        $output .= '<p>' . t('The Settings Tray for the block will open in a sidebar, with a compact form for configuring what the block shows.') . '</p>';
+        $output .= '<p>' . t('Save the form and the changes will be immediately visible on the page.') . '</p>';
+        $output .= '</dd>';
+        $output .= '</dl>';
+        return ['#markup' => $output];
+    }
+  }
+
+  /**
+   * Implements hook_contextual_links_view_alter().
+   *
+   * Change Configure Blocks into off_canvas links.
+   */
+  #[Hook('contextual_links_view_alter')]
+  public function contextualLinksViewAlter(&$element, $items) {
+    if (isset($element['#links']['settings-trayblock-configure'])) {
+      // Place settings_tray link first.
+      $settings_tray_link = $element['#links']['settings-trayblock-configure'];
+      unset($element['#links']['settings-trayblock-configure']);
+      $element['#links'] = ['settings-trayblock-configure' => $settings_tray_link] + $element['#links'];
+      // If this is content block change title to avoid duplicate "Quick Edit".
+      if (isset($element['#links']['block-contentblock-edit'])) {
+        $element['#links']['settings-trayblock-configure']['title'] = t('Quick edit settings');
+      }
+      $element['#attached']['library'][] = 'core/drupal.dialog.off_canvas';
+    }
+  }
+
+  /**
+   * Implements hook_block_view_alter().
+   */
+  #[Hook('block_view_alter')]
+  public function blockViewAlter(array &$build) {
+    if (isset($build['#contextual_links']['block'])) {
+      // Ensure that contextual links vary by whether the block has config overrides
+      // or not.
+      // @see _contextual_links_to_id()
+      $build['#contextual_links']['block']['metadata']['has_overrides'] = _settings_tray_has_block_overrides($build['#block']) ? 1 : 0;
+    }
+    // Force a new 'data-contextual-id' attribute on blocks when this module is
+    // enabled so as not to reuse stale data cached client-side.
+    // @todo Remove when https://www.drupal.org/node/2773591 is fixed.
+    $build['#contextual_links']['settings_tray'] = ['route_parameters' => []];
+  }
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public function entityTypeBuild(array &$entity_types) {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    $entity_types['block']->setFormClass('settings_tray', BlockEntitySettingTrayForm::class)->setLinkTemplate('settings_tray-form', '/admin/structure/block/manage/{block}/settings-tray');
+  }
+
+  /**
+   * Implements hook_toolbar_alter().
+   *
+   * Alters the 'contextual' toolbar tab if it exists (meaning the user is allowed
+   * to use contextual links) and if they can administer blocks.
+   *
+   * @todo Remove the "administer blocks" requirement in
+   *   https://www.drupal.org/node/2822965.
+   *
+   * @see contextual_toolbar()
+   */
+  #[Hook('toolbar_alter')]
+  public function toolbarAlter(&$items) {
+    $items['contextual']['#cache']['contexts'][] = 'user.permissions';
+    if (isset($items['contextual']['tab']) && \Drupal::currentUser()->hasPermission('administer blocks')) {
+      $items['contextual']['#weight'] = -1000;
+      $items['contextual']['#attached']['library'][] = 'settings_tray/drupal.settings_tray';
+      $items['contextual']['tab']['#attributes']['data-drupal-settingstray'] = 'toggle';
+      // Set a class on items to mark whether they should be active in edit mode.
+      // @todo Create a dynamic method for modules to set their own items.
+      //   https://www.drupal.org/node/2784589.
+      $edit_mode_items = ['contextual'];
+      foreach ($items as $key => $item) {
+        if (!in_array($key, $edit_mode_items) && (!isset($items[$key]['#wrapper_attributes']['class']) || !in_array('hidden', $items[$key]['#wrapper_attributes']['class']))) {
+          $items[$key]['#wrapper_attributes']['class'][] = 'edit-mode-inactive';
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_block_alter().
+   *
+   * Ensures every block plugin definition has an 'settings_tray' form specified.
+   *
+   * @see \Drupal\settings_tray\Access\BlockPluginHasSettingsTrayFormAccessCheck
+   */
+  #[Hook('block_alter')]
+  public function blockAlter(&$definitions) {
+    foreach ($definitions as &$definition) {
+      // If a block plugin does not define its own 'settings_tray' form, use the
+      // plugin class itself.
+      if (!isset($definition['forms']['settings_tray'])) {
+        $definition['forms']['settings_tray'] = $definition['class'];
+      }
+    }
+  }
+
+  /**
+   * Implements hook_css_alter().
+   */
+  #[Hook('css_alter')]
+  public function cssAlter(&$css, AttachedAssetsInterface $assets, LanguageInterface $language) {
+    // @todo Remove once conditional ordering is introduced in
+    //   https://www.drupal.org/node/1945262.
+    $path = \Drupal::service('extension.list.module')->getPath('settings_tray') . '/css/settings_tray.theme.css';
+    if (isset($css[$path])) {
+      // Use 200 to come after CSS_AGGREGATE_THEME.
+      $css[$path]['group'] = 200;
+    }
+  }
+
+}
diff --git a/core/modules/settings_tray/tests/modules/settings_tray_test_css/settings_tray_test_css.module b/core/modules/settings_tray/tests/modules/settings_tray_test_css/settings_tray_test_css.module
deleted file mode 100644
index 3edfb898b56d3a9f90cf3e915839f778b05a824b..0000000000000000000000000000000000000000
--- a/core/modules/settings_tray/tests/modules/settings_tray_test_css/settings_tray_test_css.module
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-/**
- * @file
- * Module for attaching CSS during tests.
- *
- * CSS pointer-events properties cause testing errors.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_page_attachments().
- */
-function settings_tray_test_css_page_attachments(array &$attachments) {
-  // Unconditionally attach an asset to the page.
-  $attachments['#attached']['library'][] = 'settings_tray_test_css/drupal.css_fix';
-}
diff --git a/core/modules/settings_tray/tests/modules/settings_tray_test_css/src/Hook/SettingsTrayTestCssHooks.php b/core/modules/settings_tray/tests/modules/settings_tray_test_css/src/Hook/SettingsTrayTestCssHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..277d7e3b6ec00526ebc82d5f45956fc6317ebc81
--- /dev/null
+++ b/core/modules/settings_tray/tests/modules/settings_tray_test_css/src/Hook/SettingsTrayTestCssHooks.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\settings_tray_test_css\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for settings_tray_test_css.
+ */
+class SettingsTrayTestCssHooks {
+
+  /**
+   * Implements hook_page_attachments().
+   */
+  #[Hook('page_attachments')]
+  public function pageAttachments(array &$attachments) {
+    // Unconditionally attach an asset to the page.
+    $attachments['#attached']['library'][] = 'settings_tray_test_css/drupal.css_fix';
+  }
+
+}
diff --git a/core/modules/shortcut/shortcut.api.php b/core/modules/shortcut/shortcut.api.php
index abdf357cd8af245334b6243c374b7b6e95b5bf36..949afb2d8f75bc4c30192cda94b37adb5a098e77 100644
--- a/core/modules/shortcut/shortcut.api.php
+++ b/core/modules/shortcut/shortcut.api.php
@@ -1,5 +1,11 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\Core\Session\AccountInterface;
+
 /**
  * @file
  * Hooks provided by the Shortcut module.
@@ -31,7 +37,7 @@
  *   The name of the shortcut set that this module recommends for that user, if
  *   there is one.
  */
-function hook_shortcut_default_set(\Drupal\Core\Session\AccountInterface $account) {
+function hook_shortcut_default_set(AccountInterface $account) {
   // Use a special set of default shortcuts for administrators only.
   $roles = \Drupal::entityTypeManager()->getStorage('user_role')->loadByProperties(['is_admin' => TRUE]);
   $user_admin_roles = array_intersect(array_keys($roles), $account->getRoles());
diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module
index 44ee73483044992d0e3d7c4c819dc66fc13fec7f..2b5ef4b8cf63bea9b6c5ec02b7fb1fd3cdf86ad6 100644
--- a/core/modules/shortcut/shortcut.module
+++ b/core/modules/shortcut/shortcut.module
@@ -2,49 +2,15 @@
 
 /**
  * @file
- * Allows users to manage customizable lists of shortcut links.
  */
 
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableMetadata;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
 use Drupal\shortcut\ShortcutSetInterface;
 
-/**
- * Implements hook_help().
- */
-function shortcut_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.shortcut':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Shortcut module allows users to create sets of <em>shortcut</em> links to commonly-visited pages of the site. Shortcuts are contained within <em>sets</em>. Each user with <em>Select any shortcut set</em> permission can select a shortcut set created by anyone at the site. For more information, see the <a href=":shortcut">online documentation for the Shortcut module</a>.', [':shortcut' => 'https://www.drupal.org/docs/8/core/modules/shortcut']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl><dt>' . t('Administering shortcuts') . '</dt>';
-      $output .= '<dd>' . t('Users with the <em>Administer shortcuts</em> permission can manage shortcut sets and edit the shortcuts within sets from the <a href=":shortcuts">Shortcuts administration page</a>.', [':shortcuts' => Url::fromRoute('entity.shortcut_set.collection')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Choosing shortcut sets') . '</dt>';
-      $output .= '<dd>' . t('Users with permission to switch shortcut sets can choose a shortcut set to use from the Shortcuts tab of their user account page.') . '</dd>';
-      $output .= '<dt>' . t('Adding and removing shortcuts') . '</dt>';
-      $output .= '<dd>' . t('The Shortcut module creates an add/remove link for each page on your site; the link lets you add or remove the current page from the currently-enabled set of shortcuts (if your theme displays it and you have permission to edit your shortcut set). The core Claro administration theme displays this link next to the page title, as a gray or yellow star. If you click on the gray star, you will add that page to your preferred set of shortcuts. If the page is already part of your shortcut set, the link will be a yellow star, and will allow you to remove the current page from your shortcut set.') . '</dd>';
-      $output .= '<dt>' . t('Displaying shortcuts') . '</dt>';
-      $output .= '<dd>' . t('You can display your shortcuts by enabling the <em>Shortcuts</em> block on the <a href=":blocks">Blocks administration page</a>. Certain administrative modules also display your shortcuts; for example, the core <a href=":toolbar-help">Toolbar module</a> provides a corresponding menu link.', [':blocks' => (\Drupal::moduleHandler()->moduleExists('block')) ? Url::fromRoute('block.admin_display')->toString() : '#', ':toolbar-help' => (\Drupal::moduleHandler()->moduleExists('toolbar')) ? Url::fromRoute('help.page', ['name' => 'toolbar'])->toString() : '#']) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'entity.shortcut_set.collection':
-    case 'shortcut.set_add':
-    case 'entity.shortcut_set.edit_form':
-      $user = \Drupal::currentUser();
-      if ($user->hasPermission('access shortcuts') && $user->hasPermission('switch shortcut sets')) {
-        $output = '<p>' . t('Define which shortcut set you are using on the <a href=":shortcut-link">Shortcuts tab</a> of your account page.', [':shortcut-link' => Url::fromRoute('shortcut.set_switch', ['user' => $user->id()])->toString()]) . '</p>';
-        return $output;
-      }
-  }
-}
-
 /**
  * Access callback for editing a shortcut set.
  *
@@ -256,81 +222,3 @@ function shortcut_preprocess_page_title(&$variables) {
     ];
   }
 }
-
-/**
- * Implements hook_toolbar().
- */
-function shortcut_toolbar() {
-  $user = \Drupal::currentUser();
-
-  $items = [];
-  $items['shortcuts'] = [
-    '#cache' => [
-      'contexts' => [
-        'user.permissions',
-      ],
-    ],
-  ];
-
-  if ($user->hasPermission('access shortcuts')) {
-    $shortcut_set = \Drupal::entityTypeManager()
-      ->getStorage('shortcut_set')
-      ->getDisplayedToUser($user);
-
-    $items['shortcuts'] += [
-      '#type' => 'toolbar_item',
-      'tab' => [
-        '#type' => 'link',
-        '#title' => t('Shortcuts'),
-        '#url' => $shortcut_set->toUrl('collection'),
-        '#attributes' => [
-          'title' => t('Shortcuts'),
-          'class' => ['toolbar-icon', 'toolbar-icon-shortcut'],
-        ],
-      ],
-      'tray' => [
-        '#heading' => t('User-defined shortcuts'),
-        'children' => [
-          '#lazy_builder' => ['shortcut.lazy_builders:lazyLinks', []],
-          '#create_placeholder' => TRUE,
-          '#cache' => [
-            'keys' => ['shortcut_set_toolbar_links'],
-            'contexts' => ['user'],
-          ],
-          '#lazy_builder_preview' => [
-            '#markup' => '<a href="#" class="toolbar-tray-lazy-placeholder-link">&nbsp;</a>',
-          ],
-        ],
-      ],
-      '#weight' => -10,
-      '#attached' => [
-        'library' => [
-          'shortcut/drupal.shortcut',
-        ],
-      ],
-    ];
-  }
-
-  return $items;
-}
-
-/**
- * Implements hook_themes_installed().
- */
-function shortcut_themes_installed($theme_list) {
-  // Theme settings are not configuration entities and cannot depend on modules
-  // so to set a module-specific setting, we need to set it with logic.
-  if (in_array('claro', $theme_list, TRUE)) {
-    \Drupal::configFactory()->getEditable("claro.settings")
-      ->set('third_party_settings.shortcut.module_link', TRUE)
-      ->save(TRUE);
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete().
- */
-function shortcut_user_delete(EntityInterface $entity) {
-  // Clean up shortcut set mapping of removed user account.
-  \Drupal::entityTypeManager()->getStorage('shortcut_set')->unassignUser($entity);
-}
diff --git a/core/modules/shortcut/src/Hook/ShortcutHooks.php b/core/modules/shortcut/src/Hook/ShortcutHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..d74b4978b2d77df03a5f70477ae1ec79df2153f9
--- /dev/null
+++ b/core/modules/shortcut/src/Hook/ShortcutHooks.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Drupal\shortcut\Hook;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for shortcut.
+ */
+class ShortcutHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.shortcut':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Shortcut module allows users to create sets of <em>shortcut</em> links to commonly-visited pages of the site. Shortcuts are contained within <em>sets</em>. Each user with <em>Select any shortcut set</em> permission can select a shortcut set created by anyone at the site. For more information, see the <a href=":shortcut">online documentation for the Shortcut module</a>.', [':shortcut' => 'https://www.drupal.org/docs/8/core/modules/shortcut']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl><dt>' . t('Administering shortcuts') . '</dt>';
+        $output .= '<dd>' . t('Users with the <em>Administer shortcuts</em> permission can manage shortcut sets and edit the shortcuts within sets from the <a href=":shortcuts">Shortcuts administration page</a>.', [
+          ':shortcuts' => Url::fromRoute('entity.shortcut_set.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Choosing shortcut sets') . '</dt>';
+        $output .= '<dd>' . t('Users with permission to switch shortcut sets can choose a shortcut set to use from the Shortcuts tab of their user account page.') . '</dd>';
+        $output .= '<dt>' . t('Adding and removing shortcuts') . '</dt>';
+        $output .= '<dd>' . t('The Shortcut module creates an add/remove link for each page on your site; the link lets you add or remove the current page from the currently-enabled set of shortcuts (if your theme displays it and you have permission to edit your shortcut set). The core Claro administration theme displays this link next to the page title, as a gray or yellow star. If you click on the gray star, you will add that page to your preferred set of shortcuts. If the page is already part of your shortcut set, the link will be a yellow star, and will allow you to remove the current page from your shortcut set.') . '</dd>';
+        $output .= '<dt>' . t('Displaying shortcuts') . '</dt>';
+        $output .= '<dd>' . t('You can display your shortcuts by enabling the <em>Shortcuts</em> block on the <a href=":blocks">Blocks administration page</a>. Certain administrative modules also display your shortcuts; for example, the core <a href=":toolbar-help">Toolbar module</a> provides a corresponding menu link.', [
+          ':blocks' => \Drupal::moduleHandler()->moduleExists('block') ? Url::fromRoute('block.admin_display')->toString() : '#',
+          ':toolbar-help' => \Drupal::moduleHandler()->moduleExists('toolbar') ? Url::fromRoute('help.page', [
+            'name' => 'toolbar',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'entity.shortcut_set.collection':
+      case 'shortcut.set_add':
+      case 'entity.shortcut_set.edit_form':
+        $user = \Drupal::currentUser();
+        if ($user->hasPermission('access shortcuts') && $user->hasPermission('switch shortcut sets')) {
+          $output = '<p>' . t('Define which shortcut set you are using on the <a href=":shortcut-link">Shortcuts tab</a> of your account page.', [
+            ':shortcut-link' => Url::fromRoute('shortcut.set_switch', [
+              'user' => $user->id(),
+            ])->toString(),
+          ]) . '</p>';
+          return $output;
+        }
+    }
+  }
+
+  /**
+   * Implements hook_toolbar().
+   */
+  #[Hook('toolbar')]
+  public function toolbar() {
+    $user = \Drupal::currentUser();
+    $items = [];
+    $items['shortcuts'] = ['#cache' => ['contexts' => ['user.permissions']]];
+    if ($user->hasPermission('access shortcuts')) {
+      $shortcut_set = \Drupal::entityTypeManager()->getStorage('shortcut_set')->getDisplayedToUser($user);
+      $items['shortcuts'] += [
+        '#type' => 'toolbar_item',
+        'tab' => [
+          '#type' => 'link',
+          '#title' => t('Shortcuts'),
+          '#url' => $shortcut_set->toUrl('collection'),
+          '#attributes' => [
+            'title' => t('Shortcuts'),
+            'class' => [
+              'toolbar-icon',
+              'toolbar-icon-shortcut',
+            ],
+          ],
+        ],
+        'tray' => [
+          '#heading' => t('User-defined shortcuts'),
+          'children' => [
+            '#lazy_builder' => [
+              'shortcut.lazy_builders:lazyLinks',
+                        [],
+            ],
+            '#create_placeholder' => TRUE,
+            '#cache' => [
+              'keys' => [
+                'shortcut_set_toolbar_links',
+              ],
+              'contexts' => [
+                'user',
+              ],
+            ],
+            '#lazy_builder_preview' => [
+              '#markup' => '<a href="#" class="toolbar-tray-lazy-placeholder-link">&nbsp;</a>',
+            ],
+          ],
+        ],
+        '#weight' => -10,
+        '#attached' => [
+          'library' => [
+            'shortcut/drupal.shortcut',
+          ],
+        ],
+      ];
+    }
+    return $items;
+  }
+
+  /**
+   * Implements hook_themes_installed().
+   */
+  #[Hook('themes_installed')]
+  public function themesInstalled($theme_list) {
+    // Theme settings are not configuration entities and cannot depend on modules
+    // so to set a module-specific setting, we need to set it with logic.
+    if (in_array('claro', $theme_list, TRUE)) {
+      \Drupal::configFactory()->getEditable("claro.settings")->set('third_party_settings.shortcut.module_link', TRUE)->save(TRUE);
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete().
+   */
+  #[Hook('user_delete')]
+  public function userDelete(EntityInterface $entity) {
+    // Clean up shortcut set mapping of removed user account.
+    \Drupal::entityTypeManager()->getStorage('shortcut_set')->unassignUser($entity);
+  }
+
+}
diff --git a/core/modules/sqlite/sqlite.module b/core/modules/sqlite/sqlite.module
deleted file mode 100644
index 227d26db3cd35304149246667fd1da79519a7bb1..0000000000000000000000000000000000000000
--- a/core/modules/sqlite/sqlite.module
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * The SQLite module provides the connection between Drupal and a SQLite database.
- */
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function sqlite_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.sqlite':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The SQLite module provides the connection between Drupal and a SQLite database. For more information, see the <a href=":sqlite">online documentation for the SQLite module</a>.', [':sqlite' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/sqlite-module']) . '</p>';
-      return $output;
-
-  }
-}
diff --git a/core/modules/sqlite/src/Hook/SqliteHooks.php b/core/modules/sqlite/src/Hook/SqliteHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..d0a6e384fcb4cd0a9b898afacbd9d90e315d5880
--- /dev/null
+++ b/core/modules/sqlite/src/Hook/SqliteHooks.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\sqlite\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for sqlite.
+ */
+class SqliteHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.sqlite':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The SQLite module provides the connection between Drupal and a SQLite database. For more information, see the <a href=":sqlite">online documentation for the SQLite module</a>.', [
+          ':sqlite' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/sqlite-module',
+        ]) . '</p>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/syslog/src/Hook/SyslogHooks.php b/core/modules/syslog/src/Hook/SyslogHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..6862392d5a489277f24ae012ba8c69542ce1908a
--- /dev/null
+++ b/core/modules/syslog/src/Hook/SyslogHooks.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\syslog\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Link;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for syslog.
+ */
+class SyslogHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.syslog':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Syslog module logs events by sending messages to the logging facility of your web server\'s operating system. Syslog is an operating system administrative logging tool that provides valuable information for use in system management and security auditing. Most suited to medium and large sites, Syslog provides filtering tools that allow messages to be routed by type and severity. For more information, see the <a href=":syslog">online documentation for the Syslog module</a>, as well as PHP\'s documentation pages for the <a href="http://php.net/manual/function.openlog.php">openlog</a> and <a href="http://php.net/manual/function.syslog.php">syslog</a> functions.', [':syslog' => 'https://www.drupal.org/documentation/modules/syslog']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Logging for UNIX, Linux, and Mac OS X') . '</dt>';
+        $output .= '<dd>' . t('On UNIX, Linux, and Mac OS X, you will find the configuration in the file <em>/etc/syslog.conf</em>, or in <em>/etc/rsyslog.conf</em> or in the directory <em>/etc/rsyslog.d</em>. These files define the routing configuration. Messages can be flagged with the codes <code>LOG_LOCAL0</code> through <code>LOG_LOCAL7</code>. For information on Syslog facilities, severity levels, and how to set up <em>syslog.conf</em> or <em>rsyslog.conf</em>, see the <em>syslog.conf</em> or <em>rsyslog.conf</em> manual page on your command line.') . '</dd>';
+        $output .= '<dt>' . t('Logging for Microsoft Windows') . '</dt>';
+        $output .= '<dd>' . t('On Microsoft Windows, messages are always sent to the Event Log using the code <code>LOG_USER</code>.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_system_logging_settings_alter')]
+  public function formSystemLoggingSettingsAlter(&$form, FormStateInterface $form_state) : void {
+    $config = \Drupal::configFactory()->getEditable('syslog.settings');
+    $help = \Drupal::moduleHandler()->moduleExists('help') ? ' ' . Link::fromTextAndUrl(t('More information'), Url::fromRoute('help.page', ['name' => 'syslog']))->toString() . '.' : NULL;
+    $form['syslog_identity'] = [
+      '#type' => 'textfield',
+      '#title' => t('Syslog identity'),
+      '#default_value' => $config->get('identity'),
+      '#description' => t('A string that will be prepended to every message logged to Syslog. If you have multiple sites logging to the same Syslog log file, a unique identity per site makes it easy to tell the log entries apart.') . $help,
+    ];
+    if (defined('LOG_LOCAL0')) {
+      $form['syslog_facility'] = [
+        '#type' => 'select',
+        '#title' => t('Syslog facility'),
+        '#default_value' => $config->get('facility'),
+        '#options' => syslog_facility_list(),
+        '#description' => t('Depending on the system configuration, Syslog and other logging tools use this code to identify or filter messages from within the entire system log.') . $help,
+      ];
+    }
+    $form['syslog_format'] = [
+      '#type' => 'textarea',
+      '#title' => t('Syslog format'),
+      '#default_value' => $config->get('format'),
+      '#required' => TRUE,
+      '#description' => t('Specify the format of the syslog entry. Available variables are: <dl><dt><code>!base_url</code></dt><dd>Base URL of the site.</dd><dt><code>!timestamp</code></dt><dd>Unix timestamp of the log entry.</dd><dt><code>!type</code></dt><dd>The category to which this message belongs.</dd><dt><code>!ip</code></dt><dd>IP address of the user triggering the message.</dd><dt><code>!request_uri</code></dt><dd>The requested URI.</dd><dt><code>!referer</code></dt><dd>HTTP Referer if available.</dd><dt><code>!severity</code></dt><dd>The severity level of the event; ranges from 0 (Emergency) to 7 (Debug).</dd><dt><code>!uid</code></dt><dd>User ID.</dd><dt><code>!link</code></dt><dd>A link to associate with the message.</dd><dt><code>!message</code></dt><dd>The message to store in the log.</dd></dl>'),
+    ];
+    $form['#submit'][] = 'syslog_logging_settings_submit';
+  }
+
+}
diff --git a/core/modules/syslog/syslog.module b/core/modules/syslog/syslog.module
index a46c8082e202fb886a54591c8880e6335e4256fc..140f3065ccd742684e144a7a24f2be96a08fc78c 100644
--- a/core/modules/syslog/syslog.module
+++ b/core/modules/syslog/syslog.module
@@ -2,65 +2,9 @@
 
 /**
  * @file
- * Redirects logging messages to syslog.
  */
 
-use Drupal\Core\Link;
-use Drupal\Core\Url;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function syslog_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.syslog':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Syslog module logs events by sending messages to the logging facility of your web server\'s operating system. Syslog is an operating system administrative logging tool that provides valuable information for use in system management and security auditing. Most suited to medium and large sites, Syslog provides filtering tools that allow messages to be routed by type and severity. For more information, see the <a href=":syslog">online documentation for the Syslog module</a>, as well as PHP\'s documentation pages for the <a href="http://php.net/manual/function.openlog.php">openlog</a> and <a href="http://php.net/manual/function.syslog.php">syslog</a> functions.', [':syslog' => 'https://www.drupal.org/documentation/modules/syslog']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Logging for UNIX, Linux, and Mac OS X') . '</dt>';
-      $output .= '<dd>' . t('On UNIX, Linux, and Mac OS X, you will find the configuration in the file <em>/etc/syslog.conf</em>, or in <em>/etc/rsyslog.conf</em> or in the directory <em>/etc/rsyslog.d</em>. These files define the routing configuration. Messages can be flagged with the codes <code>LOG_LOCAL0</code> through <code>LOG_LOCAL7</code>. For information on Syslog facilities, severity levels, and how to set up <em>syslog.conf</em> or <em>rsyslog.conf</em>, see the <em>syslog.conf</em> or <em>rsyslog.conf</em> manual page on your command line.') . '</dd>';
-      $output .= '<dt>' . t('Logging for Microsoft Windows') . '</dt>';
-      $output .= '<dd>' . t('On Microsoft Windows, messages are always sent to the Event Log using the code <code>LOG_USER</code>.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function syslog_form_system_logging_settings_alter(&$form, FormStateInterface $form_state): void {
-  $config = \Drupal::configFactory()->getEditable('syslog.settings');
-  $help = \Drupal::moduleHandler()->moduleExists('help') ? ' ' . Link::fromTextAndUrl(t('More information'), Url::fromRoute('help.page', ['name' => 'syslog']))->toString() . '.' : NULL;
-  $form['syslog_identity'] = [
-    '#type'          => 'textfield',
-    '#title'         => t('Syslog identity'),
-    '#default_value' => $config->get('identity'),
-    '#description'   => t('A string that will be prepended to every message logged to Syslog. If you have multiple sites logging to the same Syslog log file, a unique identity per site makes it easy to tell the log entries apart.') . $help,
-  ];
-  if (defined('LOG_LOCAL0')) {
-    $form['syslog_facility'] = [
-      '#type'          => 'select',
-      '#title'         => t('Syslog facility'),
-      '#default_value' => $config->get('facility'),
-      '#options'       => syslog_facility_list(),
-      '#description'   => t('Depending on the system configuration, Syslog and other logging tools use this code to identify or filter messages from within the entire system log.') . $help,
-    ];
-  }
-  $form['syslog_format'] = [
-    '#type'          => 'textarea',
-    '#title'         => t('Syslog format'),
-    '#default_value' => $config->get('format'),
-    '#required'      => TRUE,
-    '#description'   => t('Specify the format of the syslog entry. Available variables are: <dl><dt><code>!base_url</code></dt><dd>Base URL of the site.</dd><dt><code>!timestamp</code></dt><dd>Unix timestamp of the log entry.</dd><dt><code>!type</code></dt><dd>The category to which this message belongs.</dd><dt><code>!ip</code></dt><dd>IP address of the user triggering the message.</dd><dt><code>!request_uri</code></dt><dd>The requested URI.</dd><dt><code>!referer</code></dt><dd>HTTP Referer if available.</dd><dt><code>!severity</code></dt><dd>The severity level of the event; ranges from 0 (Emergency) to 7 (Debug).</dd><dt><code>!uid</code></dt><dd>User ID.</dd><dt><code>!link</code></dt><dd>A link to associate with the message.</dd><dt><code>!message</code></dt><dd>The message to store in the log.</dd></dl>'),
-  ];
-
-  $form['#submit'][] = 'syslog_logging_settings_submit';
-}
 
 /**
  * Form submission handler for system_logging_settings().
diff --git a/core/modules/system/src/Hook/SystemHooks.php b/core/modules/system/src/Hook/SystemHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..242844830defae215a3ecd1fd52187358435fe71
--- /dev/null
+++ b/core/modules/system/src/Hook/SystemHooks.php
@@ -0,0 +1,540 @@
+<?php
+
+namespace Drupal\system\Hook;
+
+use Drupal\Core\StreamWrapper\StreamWrapperManager;
+use Drupal\Core\StreamWrapper\LocalStream;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\Link;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Block\BlockPluginInterface;
+use Drupal\Component\Render\PlainTextOutput;
+use Drupal\Core\Queue\QueueGarbageCollectionInterface;
+use Drupal\Core\KeyValueStore\KeyValueDatabaseExpirableFactory;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Extension\Extension;
+use Drupal\Component\Gettext\PoItem;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Routing\StackedRouteMatchInterface;
+use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for system.
+ */
+class SystemHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.system':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The System module is integral to the site: it provides user interfaces for many core systems and settings, as well as the basic administrative menu structure. For more information, see the <a href=":system">online documentation for the System module</a>.', [':system' => 'https://www.drupal.org/documentation/modules/system']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Managing modules') . '</dt>';
+        $output .= '<dd>' . t('Users with appropriate permission can install and uninstall modules from the <a href=":modules">Extend page</a>. Depending on which distribution or installation profile you choose when you install your site, several modules are installed and others are provided but not installed. Each module provides a discrete set of features; modules may be installed or uninstalled depending on the needs of the site. Many additional modules contributed by members of the Drupal community are available for download from the <a href=":drupal-modules">Drupal.org module page</a>. Note that uninstalling a module is a destructive action: when you uninstall a module, you will permanently lose all data connected to the module.', [
+          ':modules' => Url::fromRoute('system.modules_list')->toString(),
+          ':drupal-modules' => 'https://www.drupal.org/project/modules',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Managing themes') . '</dt>';
+        $output .= '<dd>' . t('Users with appropriate permission can install and uninstall themes on the <a href=":themes">Appearance page</a>. Themes determine the design and presentation of your site. Depending on which distribution or installation profile you choose when you install your site, a default theme is installed, and possibly a different theme for administration pages. Other themes are provided but not installed, and additional contributed themes are available at the <a href=":drupal-themes">Drupal.org theme page</a>.', [
+          ':themes' => Url::fromRoute('system.themes_page')->toString(),
+          ':drupal-themes' => 'https://www.drupal.org/project/themes',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Disabling drag-and-drop functionality') . '</dt>';
+        $output .= '<dd>' . t('The default drag-and-drop user interface for ordering tables in the administrative interface presents a challenge for some users, including users of screen readers and other assistive technology. The drag-and-drop interface can be disabled in a table by clicking a link labeled "Show row weights" above the table. The replacement interface allows users to order the table by choosing numerical weights instead of dragging table rows.') . '</dd>';
+        $output .= '<dt>' . t('Configuring basic site settings') . '</dt>';
+        $output .= '<dd>' . t('The System module provides pages for managing basic site configuration, including <a href=":date-time-settings">Date and time formats</a> and <a href=":site-info">Basic site settings</a> (site name, email address to send mail from, home page, and error pages). Additional configuration pages are listed on the main <a href=":config">Configuration page</a>.', [
+          ':date-time-settings' => Url::fromRoute('entity.date_format.collection')->toString(),
+          ':site-info' => Url::fromRoute('system.site_information_settings')->toString(),
+          ':config' => Url::fromRoute('system.admin_config')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Checking site status') . '</dt>';
+        $output .= '<dd>' . t('The <a href=":status">Status report</a> provides an overview of the configuration, status, and health of your site. Review this report to make sure there are not any problems to address, and to find information about the software your site and web server are using.', [':status' => Url::fromRoute('system.status')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Using maintenance mode') . '</dt>';
+        $output .= '<dd>' . t('When you are performing site maintenance, you can prevent non-administrative users (including anonymous visitors) from viewing your site by putting it in <a href=":maintenance-mode">Maintenance mode</a>. This will prevent unauthorized users from making changes to the site while you are performing maintenance, or from seeing a broken site while updates are in progress.', [
+          ':maintenance-mode' => Url::fromRoute('system.site_maintenance_mode')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Configuring for performance') . '</dt>';
+        $output .= '<dd>' . t('On the <a href=":performance-page">Performance page</a>, the site can be configured to aggregate CSS and JavaScript files, making the total request size smaller. Note that, for small- to medium-sized websites, the <a href=":page-cache">Internal Page Cache module</a> should be installed so that pages are efficiently cached and reused for anonymous users. Finally, for websites of all sizes, the <a href=":dynamic-page-cache">Dynamic Page Cache module</a> should also be installed so that the non-personalized parts of pages are efficiently cached (for all users).', [
+          ':performance-page' => Url::fromRoute('system.performance_settings')->toString(),
+          ':page-cache' => \Drupal::moduleHandler()->moduleExists('page_cache') ? Url::fromRoute('help.page', [
+            'name' => 'page_cache',
+          ])->toString() : '#',
+          ':dynamic-page-cache' => \Drupal::moduleHandler()->moduleExists('dynamic_page_cache') ? Url::fromRoute('help.page', [
+            'name' => 'dynamic_page_cache',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Configuring cron') . '</dt>';
+        $output .= '<dd>' . t('In order for the site and its modules to continue to operate well, a set of routine administrative operations must run on a regular basis; these operations are known as <em>cron</em> tasks. On the <a href=":cron">Cron page</a>, you can configure cron to run periodically as part of server responses by installing the <em>Automated Cron</em> module, or you can turn this off and trigger cron from an outside process on your web server. You can verify the status of cron tasks by visiting the <a href=":status">Status report page</a>. For more information, see the <a href=":handbook">online documentation for configuring cron jobs</a>.', [
+          ':status' => Url::fromRoute('system.status')->toString(),
+          ':handbook' => 'https://www.drupal.org/docs/administering-a-drupal-site/cron-automated-tasks/cron-automated-tasks-overview',
+          ':cron' => Url::fromRoute('system.cron_settings')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Configuring the file system') . '</dt>';
+        $output .= '<dd>' . t('Your site has several file directories, which are used to store and process uploaded and generated files. The <em>public</em> file directory, which is configured in your settings.php file, is the default place for storing uploaded files. Links to files in this directory contain the direct file URL, so when the files are requested, the web server will send them directly without invoking your site code. This means that the files can be downloaded by anyone with the file URL, so requests are not access-controlled but they are efficient. The <em>private</em> file directory, also configured in your settings.php file and ideally located outside the site web root, is access controlled. Links to files in this directory are not direct, so requests to these files are mediated by your site code. This means that your site can check file access permission for each file before deciding to fulfill the request, so the requests are more secure, but less efficient. You should only use the private storage for files that need access control, not for files like your site logo and background images used on every page. The <em>temporary</em> file directory is used internally by your site code for various operations, and is configured on the <a href=":file-system">File system settings</a> page. You can also see the configured public and private file directories on this page, and choose whether public or private should be the default for uploaded files.', [
+          ':file-system' => Url::fromRoute('system.file_system_settings')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Configuring the image toolkit') . '</dt>';
+        $output .= '<dd>' . t('On the <a href=":toolkit">Image toolkit page</a>, you can select and configure the PHP toolkit used to manipulate images. Depending on which distribution or installation profile you choose when you install your site, the GD2 toolkit and possibly others are included; other toolkits may be provided by contributed modules.', [
+          ':toolkit' => Url::fromRoute('system.image_toolkit_settings')->toString(),
+        ]) . '</dd>';
+        if (\Drupal::currentUser()->hasPermission('administer site configuration')) {
+          $output .= '<dt id="security-advisories">' . t('Critical security advisories') . '</dt>';
+          $output .= '<dd>' . t('The System module displays highly critical and time-sensitive security announcements to site administrators. Some security announcements will be displayed until a critical security update is installed. Announcements that are not associated with a specific release will appear for a fixed period of time. <a href=":handbook">More information on critical security advisories</a>.', [
+            ':handbook' => 'https://www.drupal.org/docs/updating-drupal/responding-to-critical-security-update-advisories',
+          ]) . '</dd>';
+          $output .= '<dd>' . t('Only the most highly critical security announcements will be shown. <a href=":advisories-list">View all security announcements</a>.', [':advisories-list' => 'https://www.drupal.org/security']) . '</dd>';
+        }
+        $output .= '</dl>';
+        return $output;
+
+      case 'system.admin_index':
+        return '<p>' . t('This page shows you all available administration tasks for each module.') . '</p>';
+
+      case 'system.themes_page':
+        $output = '<p>' . t('Set and configure the default theme for your website.  Alternative <a href=":themes">themes</a> are available.', [':themes' => 'https://www.drupal.org/project/themes']) . '</p>';
+        if (\Drupal::moduleHandler()->moduleExists('block')) {
+          $output .= '<p>' . t('You can place blocks for each theme on the <a href=":blocks">block layout</a> page.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
+        }
+        return $output;
+
+      case 'system.theme_settings_theme':
+        $theme_list = \Drupal::service('theme_handler')->listInfo();
+        $theme = $theme_list[$route_match->getParameter('theme')];
+        return '<p>' . t('These options control the display settings for the %name theme. When your site is displayed using this theme, these settings will be used.', ['%name' => $theme->info['name']]) . '</p>';
+
+      case 'system.theme_settings':
+        return '<p>' . t('Control default display settings for your site, across all themes. Use theme-specific settings to override these defaults.') . '</p>';
+
+      case 'system.modules_list':
+        $output = '<p>' . t('Add <a href=":modules">contributed modules</a> to extend your site\'s functionality.', [':modules' => 'https://www.drupal.org/project/modules']) . '</p>';
+        if (!\Drupal::moduleHandler()->moduleExists('update')) {
+          $output .= '<p>' . t('Regularly review available updates and update as required to maintain a secure and current site. Always run the <a href=":update-php">update script</a> each time a module is updated. Install the <a href=":update-manager">Update Manager module</a> to update modules and themes.', [
+            ':update-php' => Url::fromRoute('system.db_update')->toString(),
+            ':update-manager' => Url::fromRoute('system.modules_list', [], [
+              'fragment' => 'module-update',
+            ])->toString(),
+          ]) . '</p>';
+        }
+        return $output;
+
+      case 'system.modules_uninstall':
+        return '<p>' . t('The uninstall process removes all data related to a module.') . '</p>';
+
+      case 'entity.block.edit_form':
+        if (($block = $route_match->getParameter('block')) && $block->getPluginId() == 'system_powered_by_block') {
+          return '<p>' . t('The <em>Powered by Drupal</em> block is an optional link to the home page of the Drupal project. While there is absolutely no requirement that sites feature this link, it may be used to show support for Drupal.') . '</p>';
+        }
+        break;
+
+      case 'block.admin_add':
+        if ($route_match->getParameter('plugin_id') == 'system_powered_by_block') {
+          return '<p>' . t('The <em>Powered by Drupal</em> block is an optional link to the home page of the Drupal project. While there is absolutely no requirement that sites feature this link, it may be used to show support for Drupal.') . '</p>';
+        }
+        break;
+
+      case 'system.site_maintenance_mode':
+        if (\Drupal::currentUser()->id() == 1) {
+          return '<p>' . t('Use maintenance mode when making major updates, particularly if the updates could disrupt visitors or the update process. Examples include upgrading, importing or exporting content, modifying a theme, modifying content types, and making backups.') . '</p>';
+        }
+        break;
+
+      case 'system.status':
+        return '<p>' . t("Here you can find a short overview of your site's parameters as well as any problems detected with your installation. It may be useful to copy and paste this information into support requests filed on Drupal.org's support forums and project issue queues. Before filing a support request, ensure that your web server meets the <a href=\":system-requirements\">system requirements.</a>", [':system-requirements' => 'https://www.drupal.org/docs/system-requirements']) . '</p>';
+    }
+  }
+
+  /**
+   * @} End of "defgroup authorize".
+   */
+
+  /**
+   * Implements hook_updater_info().
+   */
+  #[Hook('updater_info')]
+  public function updaterInfo() {
+    return [
+      'module' => [
+        'class' => 'Drupal\Core\Updater\Module',
+        'name' => t('Update modules'),
+        'weight' => 0,
+      ],
+      'theme' => [
+        'class' => 'Drupal\Core\Updater\Theme',
+        'name' => t('Update themes'),
+        'weight' => 0,
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_filetransfer_info().
+   */
+  #[Hook('filetransfer_info')]
+  public function filetransferInfo() {
+    $backends = [];
+    // This is the default, will be available on most systems.
+    if (function_exists('ftp_connect')) {
+      $backends['ftp'] = ['title' => t('FTP'), 'class' => 'Drupal\Core\FileTransfer\FTP', 'weight' => 0];
+    }
+    // SSH2 lib connection is only available if the proper PHP extension is
+    // installed.
+    if (function_exists('ssh2_connect')) {
+      $backends['ssh'] = ['title' => t('SSH'), 'class' => 'Drupal\Core\FileTransfer\SSH', 'weight' => 20];
+    }
+    return $backends;
+  }
+
+  /**
+   * Implements hook_page_attachments().
+   *
+   * @see template_preprocess_maintenance_page()
+   * @see \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter
+   */
+  #[Hook('page_attachments')]
+  public function pageAttachments(array &$page) {
+    _system_page_attachments($page);
+  }
+
+  /**
+   * Implements hook_js_settings_build().
+   *
+   * Sets values for the core/drupal.ajax library, which just depends on the
+   * active theme but no other request-dependent values.
+   */
+  #[Hook('js_settings_build')]
+  public function jsSettingsBuild(&$settings, AttachedAssetsInterface $assets) {
+    // Generate the values for the core/drupal.ajax library.
+    // We need to send ajaxPageState settings for core/drupal.ajax if:
+    // - ajaxPageState is being loaded in this Response, in which case it will
+    //   already exist at $settings['ajaxPageState'] (because the core/drupal.ajax
+    //   library definition specifies a placeholder 'ajaxPageState' setting).
+    // - core/drupal.ajax already has been loaded and hence this is an AJAX
+    //   Response in which we must send the list of extra asset libraries that are
+    //   being added in this AJAX Response.
+    /** @var \Drupal\Core\Asset\LibraryDependencyResolver $library_dependency_resolver */
+    $library_dependency_resolver = \Drupal::service('library.dependency_resolver');
+    if (isset($settings['ajaxPageState']) || in_array('core/drupal.ajax', $library_dependency_resolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()))) {
+      // Provide the page with information about the theme that's used, so that
+      // a later AJAX request can be rendered using the same theme.
+      // @see \Drupal\Core\Theme\AjaxBasePageNegotiator
+      $theme_key = \Drupal::theme()->getActiveTheme()->getName();
+      $settings['ajaxPageState']['theme'] = $theme_key;
+    }
+  }
+
+  /**
+   * Implements hook_js_settings_alter().
+   *
+   * Sets values which depend on the current request, like core/drupalSettings
+   * as well as theme_token ajax state.
+   */
+  #[Hook('js_settings_alter')]
+  public function jsSettingsAlter(&$settings, AttachedAssetsInterface $assets) {
+    // As this is being output in the final response always use the main request.
+    $request = \Drupal::requestStack()->getMainRequest();
+    $current_query = $request->query->all();
+    // Let output path processors set a prefix.
+    /** @var \Drupal\Core\PathProcessor\OutboundPathProcessorInterface $path_processor */
+    $path_processor = \Drupal::service('path_processor_manager');
+    $options = ['prefix' => ''];
+    $path_processor->processOutbound('/', $options);
+    $pathPrefix = $options['prefix'];
+    $route_match = \Drupal::routeMatch();
+    if ($route_match instanceof StackedRouteMatchInterface) {
+      $route_match = $route_match->getMasterRouteMatch();
+    }
+    $current_path = $route_match->getRouteName() ? Url::fromRouteMatch($route_match)->getInternalPath() : '';
+    $current_path_is_admin = \Drupal::service('router.admin_context')->isAdminRoute($route_match->getRouteObject());
+    $path_settings = [
+      'baseUrl' => $request->getBaseUrl() . '/',
+      'pathPrefix' => $pathPrefix,
+      'currentPath' => $current_path,
+      'currentPathIsAdmin' => $current_path_is_admin,
+      'isFront' => \Drupal::service('path.matcher')->isFrontPage(),
+      'currentLanguage' => \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId(),
+    ];
+    if (!empty($current_query)) {
+      ksort($current_query);
+      $path_settings['currentQuery'] = (object) $current_query;
+    }
+    // Only set core/drupalSettings values that haven't been set already.
+    foreach ($path_settings as $key => $value) {
+      if (!isset($settings['path'][$key])) {
+        $settings['path'][$key] = $value;
+      }
+    }
+    if (!isset($settings['pluralDelimiter'])) {
+      $settings['pluralDelimiter'] = PoItem::DELIMITER;
+    }
+    // Add the theme token to ajaxPageState, ensuring the database is available
+    // before doing so. Also add the loaded libraries to ajaxPageState.
+    /** @var \Drupal\Core\Asset\LibraryDependencyResolver $library_dependency_resolver */
+    $library_dependency_resolver = \Drupal::service('library.dependency_resolver');
+    if (isset($settings['ajaxPageState']) || in_array('core/drupal.ajax', $library_dependency_resolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()))) {
+      if (!defined('MAINTENANCE_MODE')) {
+        // The theme token is only validated when the theme requested is not the
+        // default, so don't generate it unless necessary.
+        // @see \Drupal\Core\Theme\AjaxBasePageNegotiator::determineActiveTheme()
+        $active_theme_key = \Drupal::theme()->getActiveTheme()->getName();
+        if ($active_theme_key !== \Drupal::service('theme_handler')->getDefault()) {
+          $settings['ajaxPageState']['theme_token'] = \Drupal::csrfToken()->get($active_theme_key);
+        }
+      }
+      // Provide the page with information about the individual asset libraries
+      // used, information not otherwise available when aggregation is enabled.
+      $minimal_libraries = $library_dependency_resolver->getMinimalRepresentativeSubset(array_unique(array_merge($assets->getLibraries(), $assets->getAlreadyLoadedLibraries())));
+      sort($minimal_libraries);
+      $settings['ajaxPageState']['libraries'] = implode(',', $minimal_libraries);
+    }
+  }
+
+  /**
+   * Implements hook_system_info_alter().
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(&$info, Extension $file, $type) {
+    // Remove page-top and page-bottom from the blocks UI since they are reserved for
+    // modules to populate from outside the blocks system.
+    if ($type == 'theme') {
+      $info['regions_hidden'][] = 'page_top';
+      $info['regions_hidden'][] = 'page_bottom';
+    }
+  }
+
+  /**
+   * Implements hook_cron().
+   *
+   * Remove older rows from flood, batch cache and expirable keyvalue tables. Also
+   * ensure files directories have .htaccess files.
+   */
+  #[Hook('cron')]
+  public function cron() {
+    // Clean up the flood.
+    \Drupal::flood()->garbageCollection();
+    foreach (Cache::getBins() as $cache_backend) {
+      $cache_backend->garbageCollection();
+    }
+    // Clean up the expirable key value database store.
+    if (\Drupal::service('keyvalue.expirable.database') instanceof KeyValueDatabaseExpirableFactory) {
+      \Drupal::service('keyvalue.expirable.database')->garbageCollection();
+    }
+    // Clean up any garbage in the queue service.
+    $queue_worker_manager = \Drupal::service('plugin.manager.queue_worker');
+    $queue_factory = \Drupal::service('queue');
+    foreach (array_keys($queue_worker_manager->getDefinitions()) as $queue_name) {
+      $queue = $queue_factory->get($queue_name);
+      if ($queue instanceof QueueGarbageCollectionInterface) {
+        $queue->garbageCollection();
+      }
+    }
+    // Ensure that all of Drupal's standard directories (e.g., the public files
+    // directory and config directory) have appropriate .htaccess files.
+    \Drupal::service('file.htaccess_writer')->ensure();
+    if (\Drupal::config('system.advisories')->get('enabled')) {
+      // Fetch the security advisories so that they will be pre-fetched during
+      // _system_advisories_requirements() and system_page_top().
+      /** @var \Drupal\system\SecurityAdvisories\SecurityAdvisoriesFetcher $fetcher */
+      $fetcher = \Drupal::service('system.sa_fetcher');
+      $fetcher->getSecurityAdvisories();
+    }
+  }
+
+  /**
+   * Implements hook_mail().
+   */
+  #[Hook('mail')]
+  public function mail($key, &$message, $params) {
+    $token_service = \Drupal::token();
+    $context = $params['context'];
+    $subject = PlainTextOutput::renderFromHtml($token_service->replace($context['subject'], $context));
+    $body = $token_service->replace($context['message'], $context);
+    $message['subject'] .= str_replace(["\r", "\n"], '', $subject);
+    $message['body'][] = $body;
+  }
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public function entityTypeBuild(array &$entity_types) {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    $entity_types['date_format']->setFormClass('add', 'Drupal\system\Form\DateFormatAddForm')->setFormClass('edit', 'Drupal\system\Form\DateFormatEditForm')->setFormClass('delete', 'Drupal\system\Form\DateFormatDeleteForm')->setListBuilderClass('Drupal\system\DateFormatListBuilder')->setLinkTemplate('edit-form', '/admin/config/regional/date-time/formats/manage/{date_format}')->setLinkTemplate('delete-form', '/admin/config/regional/date-time/formats/manage/{date_format}/delete')->setLinkTemplate('collection', '/admin/config/regional/date-time/formats');
+  }
+
+  /**
+   * Implements hook_block_view_BASE_BLOCK_ID_alter().
+   */
+  #[Hook('block_view_system_main_block_alter')]
+  public function blockViewSystemMainBlockAlter(array &$build, BlockPluginInterface $block) {
+    // Contextual links on the system_main block would basically duplicate the
+    // tabs/local tasks, so reduce the clutter.
+    unset($build['#contextual_links']);
+  }
+
+  /**
+   * Implements hook_query_TAG_alter() for entity reference selection handlers.
+   */
+  #[Hook('query_entity_reference_alter')]
+  public function queryEntityReferenceAlter(AlterableInterface $query) {
+    $handler = $query->getMetadata('entity_reference_selection_handler');
+    $handler->entityQueryAlter($query);
+  }
+
+  /**
+   * Implements hook_element_info_alter().
+   */
+  #[Hook('element_info_alter')]
+  public function elementInfoAlter(&$type) {
+    if (isset($type['page'])) {
+      $type['page']['#theme_wrappers']['off_canvas_page_wrapper'] = ['#weight' => -1000];
+    }
+  }
+
+  /**
+   * Implements hook_modules_uninstalled().
+   */
+  #[Hook('modules_uninstalled')]
+  public function modulesUninstalled($modules) {
+    // @todo Remove this when modules are able to maintain their revision metadata
+    //   keys.
+    //   @see https://www.drupal.org/project/drupal/issues/3074333
+    if (!in_array('workspaces', $modules, TRUE)) {
+      return;
+    }
+    $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
+    foreach ($entity_definition_update_manager->getEntityTypes() as $entity_type) {
+      if ($entity_type instanceof ContentEntityTypeInterface && $entity_type->hasRevisionMetadataKey('workspace')) {
+        $entity_type->setRevisionMetadataKey('workspace', NULL);
+        $entity_definition_update_manager->updateEntityType($entity_type);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_library_info_alter().
+   */
+  #[Hook('library_info_alter')]
+  public function libraryInfoAlter(&$libraries, $extension) {
+    // If Claro is the admin theme but not the active theme, grant Claro the
+    // ability to override the toolbar library with its own assets.
+    if ($extension === 'toolbar' && _system_is_claro_admin_and_not_active()) {
+      require_once DRUPAL_ROOT . '/core/themes/claro/claro.theme';
+      claro_system_module_invoked_library_info_alter($libraries, $extension);
+    }
+  }
+
+  /**
+   * Implements hook_theme_registry_alter().
+   */
+  #[Hook('theme_registry_alter')]
+  public function themeRegistryAlter(array &$theme_registry) {
+    // If Claro is the admin theme but not the active theme, use Claro's toolbar
+    // templates.
+    if (_system_is_claro_admin_and_not_active()) {
+      require_once DRUPAL_ROOT . '/core/themes/claro/claro.theme';
+      claro_system_module_invoked_theme_registry_alter($theme_registry);
+    }
+  }
+
+  /**
+   * Implements hook_page_top().
+   */
+  #[Hook('page_top')]
+  public function pageTop() {
+    /** @var \Drupal\Core\Routing\AdminContext $admin_context */
+    $admin_context = \Drupal::service('router.admin_context');
+    if ($admin_context->isAdminRoute() && \Drupal::currentUser()->hasPermission('administer site configuration')) {
+      $route_match = \Drupal::routeMatch();
+      $route_name = $route_match->getRouteName();
+      if ($route_name !== 'system.status' && \Drupal::config('system.advisories')->get('enabled')) {
+        /** @var \Drupal\system\SecurityAdvisories\SecurityAdvisoriesFetcher $fetcher */
+        $fetcher = \Drupal::service('system.sa_fetcher');
+        $advisories = $fetcher->getSecurityAdvisories(FALSE);
+        if ($advisories) {
+          $messenger = \Drupal::messenger();
+          $display_as_errors = FALSE;
+          $links = [];
+          foreach ($advisories as $advisory) {
+            // To ensure that all the advisory messages are grouped together on
+            // the page, they must all be warnings or all be errors. If any
+            // advisories are not public service announcements, then display all
+            // the messages as errors because security advisories already tied to
+            // a specific release are immediately actionable by upgrading to a
+            // secure version of a project.
+            $display_as_errors = $display_as_errors ? TRUE : !$advisory->isPsa();
+            $links[] = new Link($advisory->getTitle(), Url::fromUri($advisory->getUrl()));
+          }
+          foreach ($links as $link) {
+            $display_as_errors ? $messenger->addError($link) : $messenger->addWarning($link);
+          }
+          if (\Drupal::moduleHandler()->moduleExists('help')) {
+            $help_link = t('(<a href=":system-help">What are critical security announcements?</a>)', [
+              ':system-help' => Url::fromRoute('help.page', [
+                'name' => 'system',
+              ], [
+                'fragment' => 'security-advisories',
+              ])->toString(),
+            ]);
+            $display_as_errors ? $messenger->addError($help_link) : $messenger->addWarning($help_link);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_file_download().
+   */
+  #[Hook('file_download')]
+  public function fileDownload($uri) {
+    $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
+    $scheme = $stream_wrapper_manager->getScheme($uri);
+    if ($stream_wrapper_manager->isValidScheme($scheme)) {
+      $target = $stream_wrapper_manager->getTarget($uri);
+      if ($target !== FALSE) {
+        if (!in_array($scheme, Settings::get('file_sa_core_2023_005_schemes', []))) {
+          if (DIRECTORY_SEPARATOR !== '/') {
+            $class = $stream_wrapper_manager->getClass($scheme);
+            if (is_subclass_of($class, LocalStream::class)) {
+              $target = str_replace(DIRECTORY_SEPARATOR, '/', $target);
+            }
+          }
+          $parts = explode('/', $target);
+          if (array_intersect($parts, ['.', '..'])) {
+            return -1;
+          }
+        }
+      }
+    }
+    $core_schemes = ['public', 'private', 'temporary'];
+    $additional_public_schemes = array_diff(Settings::get('file_additional_public_schemes', []), $core_schemes);
+    if ($additional_public_schemes) {
+      $scheme = StreamWrapperManager::getScheme($uri);
+      if (in_array($scheme, $additional_public_schemes, TRUE)) {
+        return ['Cache-Control' => 'public'];
+      }
+    }
+  }
+
+  /**
+   * Implements hook_archiver_info_alter().
+   */
+  #[Hook('archiver_info_alter')]
+  public function archiverInfoAlter(&$info) {
+    if (!class_exists(\ZipArchive::class)) {
+      // PHP Zip extension is missing.
+      unset($info['Zip']);
+    }
+  }
+
+}
diff --git a/core/modules/system/src/Hook/SystemTokensHooks.php b/core/modules/system/src/Hook/SystemTokensHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c28da24e8e55a597ba91e9431bc9c0dca05b42ca
--- /dev/null
+++ b/core/modules/system/src/Hook/SystemTokensHooks.php
@@ -0,0 +1,209 @@
+<?php
+
+namespace Drupal\system\Hook;
+
+use Drupal\Core\Datetime\Entity\DateFormat;
+use Drupal\Core\Url;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for system.
+ */
+class SystemTokensHooks {
+
+  /**
+   * Implements hook_token_info().
+   */
+  #[Hook('token_info')]
+  public function tokenInfo() {
+    $types['site'] = [
+      'name' => t("Site information"),
+      'description' => t("Tokens for site-wide settings and other global information."),
+    ];
+    $types['date'] = ['name' => t("Dates"), 'description' => t("Tokens related to times and dates.")];
+    // Site-wide global tokens.
+    $site['name'] = ['name' => t("Name"), 'description' => t("The name of the site.")];
+    $site['slogan'] = ['name' => t("Slogan"), 'description' => t("The slogan of the site.")];
+    $site['mail'] = [
+      'name' => t("Email"),
+      'description' => t("The administrative email address for the site."),
+    ];
+    $site['base-url'] = [
+      'name' => t("Base URL"),
+      'description' => t("The base URL of the site, currently: @base_url", [
+        '@base_url' => \Drupal::service('router.request_context')->getCompleteBaseUrl(),
+      ]),
+    ];
+    $site['base-path'] = [
+      'name' => t("Base path"),
+      'description' => t("The base path of the site, currently: @base_path", [
+        '@base_path' => \Drupal::request()->getBasePath(),
+      ]),
+    ];
+    $site['url'] = [
+      'name' => t("URL"),
+      'description' => t("The URL of the site's front page with the language prefix, if it exists."),
+    ];
+    $site['url-brief'] = [
+      'name' => t("URL (brief)"),
+      'description' => t("The URL of the site's front page without the protocol."),
+    ];
+    $site['login-url'] = [
+      'name' => t("Login page"),
+      'description' => t("The URL of the site's login page."),
+    ];
+    /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
+    $date_formatter = \Drupal::service('date.formatter');
+    // Date related tokens.
+    $request_time = \Drupal::time()->getRequestTime();
+    $date['short'] = [
+      'name' => t("Short format"),
+      'description' => t("The current date in 'short' format. (%date)", [
+        '%date' => $date_formatter->format($request_time, 'short'),
+      ]),
+    ];
+    $date['medium'] = [
+      'name' => t("Medium format"),
+      'description' => t("The current date in 'medium' format. (%date)", [
+        '%date' => $date_formatter->format($request_time, 'medium'),
+      ]),
+    ];
+    $date['long'] = [
+      'name' => t("Long format"),
+      'description' => t("The current date in 'long' format. (%date)", [
+        '%date' => $date_formatter->format($request_time, 'long'),
+      ]),
+    ];
+    $date['custom'] = [
+      'name' => t("Custom format"),
+      'description' => t('The current date in a custom format. See <a href="https://www.php.net/manual/datetime.format.php#refsect1-datetime.format-parameters">the PHP documentation</a> for details.'),
+    ];
+    $date['since'] = [
+      'name' => t("Time-since"),
+      'description' => t("The current date in 'time-since' format. (%date)", [
+        '%date' => $date_formatter->formatTimeDiffSince($request_time - 360),
+      ]),
+    ];
+    $date['raw'] = [
+      'name' => t("Raw timestamp"),
+      'description' => t("The current date in UNIX timestamp format (%date)", [
+        '%date' => $request_time,
+      ]),
+    ];
+    return ['types' => $types, 'tokens' => ['site' => $site, 'date' => $date]];
+  }
+
+  /**
+   * Implements hook_tokens().
+   */
+  #[Hook('tokens')]
+  public function tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+    $token_service = \Drupal::token();
+    $url_options = ['absolute' => TRUE];
+    if (isset($options['langcode'])) {
+      $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
+      $langcode = $options['langcode'];
+    }
+    else {
+      $langcode = NULL;
+    }
+    $replacements = [];
+    if ($type == 'site') {
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          case 'name':
+            $config = \Drupal::config('system.site');
+            $bubbleable_metadata->addCacheableDependency($config);
+            $site_name = $config->get('name');
+            $replacements[$original] = $site_name;
+            break;
+
+          case 'slogan':
+            $config = \Drupal::config('system.site');
+            $bubbleable_metadata->addCacheableDependency($config);
+            $slogan = $config->get('slogan');
+            $build = ['#markup' => $slogan];
+            // @todo Fix in https://www.drupal.org/node/2577827
+            $replacements[$original] = \Drupal::service('renderer')->renderInIsolation($build);
+            break;
+
+          case 'mail':
+            $config = \Drupal::config('system.site');
+            $bubbleable_metadata->addCacheableDependency($config);
+            $replacements[$original] = $config->get('mail');
+            break;
+
+          case 'base-url':
+            $bubbleable_metadata->addCacheContexts(['url.site']);
+            $replacements[$original] = \Drupal::service('router.request_context')->getCompleteBaseUrl();
+            break;
+
+          case 'base-path':
+            $bubbleable_metadata->addCacheContexts(['url.site']);
+            $replacements[$original] = \Drupal::request()->getBasePath();
+            break;
+
+          case 'url':
+            /** @var \Drupal\Core\GeneratedUrl $result */
+            $result = Url::fromRoute('<front>', [], $url_options)->toString(TRUE);
+            $bubbleable_metadata->addCacheableDependency($result);
+            $replacements[$original] = $result->getGeneratedUrl();
+            break;
+
+          case 'url-brief':
+            /** @var \Drupal\Core\GeneratedUrl $result */
+            $result = Url::fromRoute('<front>', [], $url_options)->toString(TRUE);
+            $bubbleable_metadata->addCacheableDependency($result);
+            $replacements[$original] = preg_replace(['!^https?://!', '!/$!'], '', $result->getGeneratedUrl());
+            break;
+
+          case 'login-url':
+            /** @var \Drupal\Core\GeneratedUrl $result */
+            $result = Url::fromRoute('user.page', [], $url_options)->toString(TRUE);
+            $bubbleable_metadata->addCacheableDependency($result);
+            $replacements[$original] = $result->getGeneratedUrl();
+            break;
+        }
+      }
+    }
+    elseif ($type == 'date') {
+      if (empty($data['date'])) {
+        $date = \Drupal::time()->getRequestTime();
+        // We depend on the current request time, so the tokens are not cacheable
+        // at all.
+        $bubbleable_metadata->setCacheMaxAge(0);
+      }
+      else {
+        $date = $data['date'];
+      }
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          case 'short':
+          case 'medium':
+          case 'long':
+            $date_format = DateFormat::load($name);
+            $bubbleable_metadata->addCacheableDependency($date_format);
+            $replacements[$original] = \Drupal::service('date.formatter')->format($date, $name, '', NULL, $langcode);
+            break;
+
+          case 'since':
+            $replacements[$original] = \Drupal::service('date.formatter')->formatTimeDiffSince($date, ['langcode' => $langcode]);
+            $bubbleable_metadata->setCacheMaxAge(0);
+            break;
+
+          case 'raw':
+            $replacements[$original] = $date;
+            break;
+        }
+      }
+      if ($created_tokens = $token_service->findWithPrefix($tokens, 'custom')) {
+        foreach ($created_tokens as $name => $original) {
+          $replacements[$original] = \Drupal::service('date.formatter')->format($date, 'custom', $name, NULL, $langcode);
+        }
+      }
+    }
+    return $replacements;
+  }
+
+}
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index b9fa2f7c32ecf97d3dafdbe5f6cdcb9b1a61cd62..dc5961b2e254f35619121d3074c59450c5cbdb6e 100644
--- a/core/modules/system/system.admin.inc
+++ b/core/modules/system/system.admin.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Admin page callbacks for the system module.
  */
 
 use Drupal\Component\Utility\Html;
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index ab34a12863c9658bd37def010c237ed93b9e5430..af698a011cc6a664c89f31d54681ef363fc0ba7c 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -2,29 +2,12 @@
 
 /**
  * @file
- * Configuration system that lets administrators modify the workings of the site.
  */
 
 use Drupal\Component\FileSecurity\FileSecurity;
-use Drupal\Component\Gettext\PoItem;
-use Drupal\Component\Render\PlainTextOutput;
 use Drupal\Component\Utility\UrlHelper;
-use Drupal\Core\Asset\AttachedAssetsInterface;
-use Drupal\Core\Block\BlockPluginInterface;
-use Drupal\Core\Cache\Cache;
-use Drupal\Core\Database\Query\AlterableInterface;
-use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Extension\Extension;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\KeyValueStore\KeyValueDatabaseExpirableFactory;
-use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Link;
-use Drupal\Core\Queue\QueueGarbageCollectionInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Routing\StackedRouteMatchInterface;
-use Drupal\Core\Site\Settings;
-use Drupal\Core\StreamWrapper\LocalStream;
-use Drupal\Core\StreamWrapper\StreamWrapperManager;
 use Drupal\Core\Url;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
@@ -58,96 +41,6 @@
  */
 const REGIONS_ALL = 'all';
 
-/**
- * Implements hook_help().
- */
-function system_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.system':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The System module is integral to the site: it provides user interfaces for many core systems and settings, as well as the basic administrative menu structure. For more information, see the <a href=":system">online documentation for the System module</a>.', [':system' => 'https://www.drupal.org/documentation/modules/system']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Managing modules') . '</dt>';
-      $output .= '<dd>' . t('Users with appropriate permission can install and uninstall modules from the <a href=":modules">Extend page</a>. Depending on which distribution or installation profile you choose when you install your site, several modules are installed and others are provided but not installed. Each module provides a discrete set of features; modules may be installed or uninstalled depending on the needs of the site. Many additional modules contributed by members of the Drupal community are available for download from the <a href=":drupal-modules">Drupal.org module page</a>. Note that uninstalling a module is a destructive action: when you uninstall a module, you will permanently lose all data connected to the module.', [':modules' => Url::fromRoute('system.modules_list')->toString(), ':drupal-modules' => 'https://www.drupal.org/project/modules']) . '</dd>';
-      $output .= '<dt>' . t('Managing themes') . '</dt>';
-      $output .= '<dd>' . t('Users with appropriate permission can install and uninstall themes on the <a href=":themes">Appearance page</a>. Themes determine the design and presentation of your site. Depending on which distribution or installation profile you choose when you install your site, a default theme is installed, and possibly a different theme for administration pages. Other themes are provided but not installed, and additional contributed themes are available at the <a href=":drupal-themes">Drupal.org theme page</a>.', [':themes' => Url::fromRoute('system.themes_page')->toString(), ':drupal-themes' => 'https://www.drupal.org/project/themes']) . '</dd>';
-      $output .= '<dt>' . t('Disabling drag-and-drop functionality') . '</dt>';
-      $output .= '<dd>' . t('The default drag-and-drop user interface for ordering tables in the administrative interface presents a challenge for some users, including users of screen readers and other assistive technology. The drag-and-drop interface can be disabled in a table by clicking a link labeled "Show row weights" above the table. The replacement interface allows users to order the table by choosing numerical weights instead of dragging table rows.') . '</dd>';
-      $output .= '<dt>' . t('Configuring basic site settings') . '</dt>';
-      $output .= '<dd>' . t('The System module provides pages for managing basic site configuration, including <a href=":date-time-settings">Date and time formats</a> and <a href=":site-info">Basic site settings</a> (site name, email address to send mail from, home page, and error pages). Additional configuration pages are listed on the main <a href=":config">Configuration page</a>.', [':date-time-settings' => Url::fromRoute('entity.date_format.collection')->toString(), ':site-info' => Url::fromRoute('system.site_information_settings')->toString(), ':config' => Url::fromRoute('system.admin_config')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Checking site status') . '</dt>';
-      $output .= '<dd>' . t('The <a href=":status">Status report</a> provides an overview of the configuration, status, and health of your site. Review this report to make sure there are not any problems to address, and to find information about the software your site and web server are using.', [':status' => Url::fromRoute('system.status')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Using maintenance mode') . '</dt>';
-      $output .= '<dd>' . t('When you are performing site maintenance, you can prevent non-administrative users (including anonymous visitors) from viewing your site by putting it in <a href=":maintenance-mode">Maintenance mode</a>. This will prevent unauthorized users from making changes to the site while you are performing maintenance, or from seeing a broken site while updates are in progress.', [':maintenance-mode' => Url::fromRoute('system.site_maintenance_mode')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Configuring for performance') . '</dt>';
-      $output .= '<dd>' . t('On the <a href=":performance-page">Performance page</a>, the site can be configured to aggregate CSS and JavaScript files, making the total request size smaller. Note that, for small- to medium-sized websites, the <a href=":page-cache">Internal Page Cache module</a> should be installed so that pages are efficiently cached and reused for anonymous users. Finally, for websites of all sizes, the <a href=":dynamic-page-cache">Dynamic Page Cache module</a> should also be installed so that the non-personalized parts of pages are efficiently cached (for all users).', [':performance-page' => Url::fromRoute('system.performance_settings')->toString(), ':page-cache' => (\Drupal::moduleHandler()->moduleExists('page_cache')) ? Url::fromRoute('help.page', ['name' => 'page_cache'])->toString() : '#', ':dynamic-page-cache' => (\Drupal::moduleHandler()->moduleExists('dynamic_page_cache')) ? Url::fromRoute('help.page', ['name' => 'dynamic_page_cache'])->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Configuring cron') . '</dt>';
-      $output .= '<dd>' . t('In order for the site and its modules to continue to operate well, a set of routine administrative operations must run on a regular basis; these operations are known as <em>cron</em> tasks. On the <a href=":cron">Cron page</a>, you can configure cron to run periodically as part of server responses by installing the <em>Automated Cron</em> module, or you can turn this off and trigger cron from an outside process on your web server. You can verify the status of cron tasks by visiting the <a href=":status">Status report page</a>. For more information, see the <a href=":handbook">online documentation for configuring cron jobs</a>.', [':status' => Url::fromRoute('system.status')->toString(), ':handbook' => 'https://www.drupal.org/docs/administering-a-drupal-site/cron-automated-tasks/cron-automated-tasks-overview', ':cron' => Url::fromRoute('system.cron_settings')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Configuring the file system') . '</dt>';
-      $output .= '<dd>' . t('Your site has several file directories, which are used to store and process uploaded and generated files. The <em>public</em> file directory, which is configured in your settings.php file, is the default place for storing uploaded files. Links to files in this directory contain the direct file URL, so when the files are requested, the web server will send them directly without invoking your site code. This means that the files can be downloaded by anyone with the file URL, so requests are not access-controlled but they are efficient. The <em>private</em> file directory, also configured in your settings.php file and ideally located outside the site web root, is access controlled. Links to files in this directory are not direct, so requests to these files are mediated by your site code. This means that your site can check file access permission for each file before deciding to fulfill the request, so the requests are more secure, but less efficient. You should only use the private storage for files that need access control, not for files like your site logo and background images used on every page. The <em>temporary</em> file directory is used internally by your site code for various operations, and is configured on the <a href=":file-system">File system settings</a> page. You can also see the configured public and private file directories on this page, and choose whether public or private should be the default for uploaded files.', [':file-system' => Url::fromRoute('system.file_system_settings')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Configuring the image toolkit') . '</dt>';
-      $output .= '<dd>' . t('On the <a href=":toolkit">Image toolkit page</a>, you can select and configure the PHP toolkit used to manipulate images. Depending on which distribution or installation profile you choose when you install your site, the GD2 toolkit and possibly others are included; other toolkits may be provided by contributed modules.', [':toolkit' => Url::fromRoute('system.image_toolkit_settings')->toString()]) . '</dd>';
-      if (\Drupal::currentUser()->hasPermission('administer site configuration')) {
-        $output .= '<dt id="security-advisories">' . t('Critical security advisories') . '</dt>';
-        $output .= '<dd>' . t('The System module displays highly critical and time-sensitive security announcements to site administrators. Some security announcements will be displayed until a critical security update is installed. Announcements that are not associated with a specific release will appear for a fixed period of time. <a href=":handbook">More information on critical security advisories</a>.', [':handbook' => 'https://www.drupal.org/docs/updating-drupal/responding-to-critical-security-update-advisories']) . '</dd>';
-        $output .= '<dd>' . t('Only the most highly critical security announcements will be shown. <a href=":advisories-list">View all security announcements</a>.', [':advisories-list' => 'https://www.drupal.org/security']) . '</dd>';
-      }
-      $output .= '</dl>';
-      return $output;
-
-    case 'system.admin_index':
-      return '<p>' . t('This page shows you all available administration tasks for each module.') . '</p>';
-
-    case 'system.themes_page':
-      $output = '<p>' . t('Set and configure the default theme for your website.  Alternative <a href=":themes">themes</a> are available.', [':themes' => 'https://www.drupal.org/project/themes']) . '</p>';
-      if (\Drupal::moduleHandler()->moduleExists('block')) {
-        $output .= '<p>' . t('You can place blocks for each theme on the <a href=":blocks">block layout</a> page.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
-      }
-      return $output;
-
-    case 'system.theme_settings_theme':
-      $theme_list = \Drupal::service('theme_handler')->listInfo();
-      $theme = $theme_list[$route_match->getParameter('theme')];
-      return '<p>' . t('These options control the display settings for the %name theme. When your site is displayed using this theme, these settings will be used.', ['%name' => $theme->info['name']]) . '</p>';
-
-    case 'system.theme_settings':
-      return '<p>' . t('Control default display settings for your site, across all themes. Use theme-specific settings to override these defaults.') . '</p>';
-
-    case 'system.modules_list':
-      $output = '<p>' . t('Add <a href=":modules">contributed modules</a> to extend your site\'s functionality.', [':modules' => 'https://www.drupal.org/project/modules']) . '</p>';
-      if (!\Drupal::moduleHandler()->moduleExists('update')) {
-        $output .= '<p>' . t('Regularly review available updates and update as required to maintain a secure and current site. Always run the <a href=":update-php">update script</a> each time a module is updated. Install the <a href=":update-manager">Update Manager module</a> to update modules and themes.', [':update-php' => Url::fromRoute('system.db_update')->toString(), ':update-manager' => Url::fromRoute('system.modules_list', [], ['fragment' => 'module-update'])->toString()]) . '</p>';
-      }
-      return $output;
-
-    case 'system.modules_uninstall':
-      return '<p>' . t('The uninstall process removes all data related to a module.') . '</p>';
-
-    case 'entity.block.edit_form':
-      if (($block = $route_match->getParameter('block')) && $block->getPluginId() == 'system_powered_by_block') {
-        return '<p>' . t('The <em>Powered by Drupal</em> block is an optional link to the home page of the Drupal project. While there is absolutely no requirement that sites feature this link, it may be used to show support for Drupal.') . '</p>';
-      }
-      break;
-
-    case 'block.admin_add':
-      if ($route_match->getParameter('plugin_id') == 'system_powered_by_block') {
-        return '<p>' . t('The <em>Powered by Drupal</em> block is an optional link to the home page of the Drupal project. While there is absolutely no requirement that sites feature this link, it may be used to show support for Drupal.') . '</p>';
-      }
-      break;
-
-    case 'system.site_maintenance_mode':
-      if (\Drupal::currentUser()->id() == 1) {
-        return '<p>' . t('Use maintenance mode when making major updates, particularly if the updates could disrupt visitors or the update process. Examples include upgrading, importing or exporting content, modifying a theme, modifying content types, and making backups.') . '</p>';
-      }
-      break;
-
-    case 'system.status':
-      return '<p>' . t("Here you can find a short overview of your site's parameters as well as any problems detected with your installation. It may be useful to copy and paste this information into support requests filed on Drupal.org's support forums and project issue queues. Before filing a support request, ensure that your web server meets the <a href=\":system-requirements\">system requirements.</a>", [':system-requirements' => 'https://www.drupal.org/docs/system-requirements']) . '</p>';
-  }
-}
-
 /**
  * Implements hook_theme().
  */
@@ -524,62 +417,13 @@ function system_authorized_batch_process() {
   return batch_process($finish_url->setAbsolute()->toString(), $process_url);
 }
 
-/**
- * @} End of "defgroup authorize".
- */
-
-/**
- * Implements hook_updater_info().
- */
-function system_updater_info() {
-  return [
-    'module' => [
-      'class' => 'Drupal\Core\Updater\Module',
-      'name' => t('Update modules'),
-      'weight' => 0,
-    ],
-    'theme' => [
-      'class' => 'Drupal\Core\Updater\Theme',
-      'name' => t('Update themes'),
-      'weight' => 0,
-    ],
-  ];
-}
-
-/**
- * Implements hook_filetransfer_info().
- */
-function system_filetransfer_info() {
-  $backends = [];
-
-  // This is the default, will be available on most systems.
-  if (function_exists('ftp_connect')) {
-    $backends['ftp'] = [
-      'title' => t('FTP'),
-      'class' => 'Drupal\Core\FileTransfer\FTP',
-      'weight' => 0,
-    ];
-  }
-
-  // SSH2 lib connection is only available if the proper PHP extension is
-  // installed.
-  if (function_exists('ssh2_connect')) {
-    $backends['ssh'] = [
-      'title' => t('SSH'),
-      'class' => 'Drupal\Core\FileTransfer\SSH',
-      'weight' => 20,
-    ];
-  }
-  return $backends;
-}
-
 /**
  * Implements hook_page_attachments().
  *
  * @see template_preprocess_maintenance_page()
  * @see \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter
  */
-function system_page_attachments(array &$page) {
+function _system_page_attachments(array &$page) {
   // Ensure the same CSS is loaded in template_preprocess_maintenance_page().
   $page['#attached']['library'][] = 'system/base';
   if (\Drupal::service('router.admin_context')->isAdminRoute()) {
@@ -668,104 +512,6 @@ function system_page_attachments(array &$page) {
   }
 }
 
-/**
- * Implements hook_js_settings_build().
- *
- * Sets values for the core/drupal.ajax library, which just depends on the
- * active theme but no other request-dependent values.
- */
-function system_js_settings_build(&$settings, AttachedAssetsInterface $assets) {
-  // Generate the values for the core/drupal.ajax library.
-  // We need to send ajaxPageState settings for core/drupal.ajax if:
-  // - ajaxPageState is being loaded in this Response, in which case it will
-  //   already exist at $settings['ajaxPageState'] (because the core/drupal.ajax
-  //   library definition specifies a placeholder 'ajaxPageState' setting).
-  // - core/drupal.ajax already has been loaded and hence this is an AJAX
-  //   Response in which we must send the list of extra asset libraries that are
-  //   being added in this AJAX Response.
-  /** @var \Drupal\Core\Asset\LibraryDependencyResolver $library_dependency_resolver */
-  $library_dependency_resolver = \Drupal::service('library.dependency_resolver');
-  if (isset($settings['ajaxPageState']) || in_array('core/drupal.ajax', $library_dependency_resolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()))) {
-    // Provide the page with information about the theme that's used, so that
-    // a later AJAX request can be rendered using the same theme.
-    // @see \Drupal\Core\Theme\AjaxBasePageNegotiator
-    $theme_key = \Drupal::theme()->getActiveTheme()->getName();
-    $settings['ajaxPageState']['theme'] = $theme_key;
-  }
-}
-
-/**
- * Implements hook_js_settings_alter().
- *
- * Sets values which depend on the current request, like core/drupalSettings
- * as well as theme_token ajax state.
- */
-function system_js_settings_alter(&$settings, AttachedAssetsInterface $assets) {
-  // As this is being output in the final response always use the main request.
-  $request = \Drupal::requestStack()->getMainRequest();
-  $current_query = $request->query->all();
-
-  // Let output path processors set a prefix.
-  /** @var \Drupal\Core\PathProcessor\OutboundPathProcessorInterface $path_processor */
-  $path_processor = \Drupal::service('path_processor_manager');
-  $options = ['prefix' => ''];
-  $path_processor->processOutbound('/', $options);
-  $pathPrefix = $options['prefix'];
-
-  $route_match = \Drupal::routeMatch();
-  if ($route_match instanceof StackedRouteMatchInterface) {
-    $route_match = $route_match->getMasterRouteMatch();
-  }
-  $current_path = $route_match->getRouteName() ? Url::fromRouteMatch($route_match)->getInternalPath() : '';
-  $current_path_is_admin = \Drupal::service('router.admin_context')->isAdminRoute($route_match->getRouteObject());
-  $path_settings = [
-    'baseUrl' => $request->getBaseUrl() . '/',
-    'pathPrefix' => $pathPrefix,
-    'currentPath' => $current_path,
-    'currentPathIsAdmin' => $current_path_is_admin,
-    'isFront' => \Drupal::service('path.matcher')->isFrontPage(),
-    'currentLanguage' => \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId(),
-  ];
-  if (!empty($current_query)) {
-    ksort($current_query);
-    $path_settings['currentQuery'] = (object) $current_query;
-  }
-
-  // Only set core/drupalSettings values that haven't been set already.
-  foreach ($path_settings as $key => $value) {
-    if (!isset($settings['path'][$key])) {
-      $settings['path'][$key] = $value;
-    }
-  }
-  if (!isset($settings['pluralDelimiter'])) {
-    $settings['pluralDelimiter'] = PoItem::DELIMITER;
-  }
-  // Add the theme token to ajaxPageState, ensuring the database is available
-  // before doing so. Also add the loaded libraries to ajaxPageState.
-  /** @var \Drupal\Core\Asset\LibraryDependencyResolver $library_dependency_resolver */
-  $library_dependency_resolver = \Drupal::service('library.dependency_resolver');
-  if (isset($settings['ajaxPageState']) || in_array('core/drupal.ajax', $library_dependency_resolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()))) {
-    if (!defined('MAINTENANCE_MODE')) {
-      // The theme token is only validated when the theme requested is not the
-      // default, so don't generate it unless necessary.
-      // @see \Drupal\Core\Theme\AjaxBasePageNegotiator::determineActiveTheme()
-      $active_theme_key = \Drupal::theme()->getActiveTheme()->getName();
-      if ($active_theme_key !== \Drupal::service('theme_handler')->getDefault()) {
-        $settings['ajaxPageState']['theme_token'] = \Drupal::csrfToken()
-          ->get($active_theme_key);
-      }
-    }
-    // Provide the page with information about the individual asset libraries
-    // used, information not otherwise available when aggregation is enabled.
-    $minimal_libraries = $library_dependency_resolver->getMinimalRepresentativeSubset(array_unique(array_merge(
-      $assets->getLibraries(),
-      $assets->getAlreadyLoadedLibraries()
-    )));
-    sort($minimal_libraries);
-    $settings['ajaxPageState']['libraries'] = implode(',', $minimal_libraries);
-  }
-}
-
 /**
  * Implements hook_preprocess_HOOK() for block templates.
  */
@@ -885,18 +631,6 @@ function system_sort_themes($a, $b) {
   return strcasecmp($a->info['name'], $b->info['name']);
 }
 
-/**
- * Implements hook_system_info_alter().
- */
-function system_system_info_alter(&$info, Extension $file, $type) {
-  // Remove page-top and page-bottom from the blocks UI since they are reserved for
-  // modules to populate from outside the blocks system.
-  if ($type == 'theme') {
-    $info['regions_hidden'][] = 'page_top';
-    $info['regions_hidden'][] = 'page_bottom';
-  }
-}
-
 /**
  * Gets the name of the default region for a given theme.
  *
@@ -934,126 +668,6 @@ function system_admin_compact_mode() {
   return \Drupal::request()->cookies->get('Drupal_visitor_admin_compact_mode', \Drupal::config('system.site')->get('admin_compact_mode'));
 }
 
-/**
- * Implements hook_cron().
- *
- * Remove older rows from flood, batch cache and expirable keyvalue tables. Also
- * ensure files directories have .htaccess files.
- */
-function system_cron() {
-  // Clean up the flood.
-  \Drupal::flood()->garbageCollection();
-
-  foreach (Cache::getBins() as $cache_backend) {
-    $cache_backend->garbageCollection();
-  }
-
-  // Clean up the expirable key value database store.
-  if (\Drupal::service('keyvalue.expirable.database') instanceof KeyValueDatabaseExpirableFactory) {
-    \Drupal::service('keyvalue.expirable.database')->garbageCollection();
-  }
-
-  // Clean up any garbage in the queue service.
-  $queue_worker_manager = \Drupal::service('plugin.manager.queue_worker');
-  $queue_factory = \Drupal::service('queue');
-
-  foreach (array_keys($queue_worker_manager->getDefinitions()) as $queue_name) {
-    $queue = $queue_factory->get($queue_name);
-
-    if ($queue instanceof QueueGarbageCollectionInterface) {
-      $queue->garbageCollection();
-    }
-  }
-
-  // Ensure that all of Drupal's standard directories (e.g., the public files
-  // directory and config directory) have appropriate .htaccess files.
-  \Drupal::service('file.htaccess_writer')->ensure();
-
-  if (\Drupal::config('system.advisories')->get('enabled')) {
-    // Fetch the security advisories so that they will be pre-fetched during
-    // _system_advisories_requirements() and system_page_top().
-    /** @var \Drupal\system\SecurityAdvisories\SecurityAdvisoriesFetcher $fetcher */
-    $fetcher = \Drupal::service('system.sa_fetcher');
-    $fetcher->getSecurityAdvisories();
-  }
-}
-
-/**
- * Implements hook_mail().
- */
-function system_mail($key, &$message, $params) {
-  $token_service = \Drupal::token();
-
-  $context = $params['context'];
-
-  $subject = PlainTextOutput::renderFromHtml($token_service->replace($context['subject'], $context));
-  $body = $token_service->replace($context['message'], $context);
-
-  $message['subject'] .= str_replace(["\r", "\n"], '', $subject);
-  $message['body'][] = $body;
-}
-
-/**
- * Implements hook_entity_type_build().
- */
-function system_entity_type_build(array &$entity_types) {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  $entity_types['date_format']
-    ->setFormClass('add', 'Drupal\system\Form\DateFormatAddForm')
-    ->setFormClass('edit', 'Drupal\system\Form\DateFormatEditForm')
-    ->setFormClass('delete', 'Drupal\system\Form\DateFormatDeleteForm')
-    ->setListBuilderClass('Drupal\system\DateFormatListBuilder')
-    ->setLinkTemplate('edit-form', '/admin/config/regional/date-time/formats/manage/{date_format}')
-    ->setLinkTemplate('delete-form', '/admin/config/regional/date-time/formats/manage/{date_format}/delete')
-    ->setLinkTemplate('collection', '/admin/config/regional/date-time/formats');
-}
-
-/**
- * Implements hook_block_view_BASE_BLOCK_ID_alter().
- */
-function system_block_view_system_main_block_alter(array &$build, BlockPluginInterface $block) {
-  // Contextual links on the system_main block would basically duplicate the
-  // tabs/local tasks, so reduce the clutter.
-  unset($build['#contextual_links']);
-}
-
-/**
- * Implements hook_query_TAG_alter() for entity reference selection handlers.
- */
-function system_query_entity_reference_alter(AlterableInterface $query) {
-  $handler = $query->getMetadata('entity_reference_selection_handler');
-  $handler->entityQueryAlter($query);
-}
-
-/**
- * Implements hook_element_info_alter().
- */
-function system_element_info_alter(&$type) {
-  if (isset($type['page'])) {
-    $type['page']['#theme_wrappers']['off_canvas_page_wrapper'] = ['#weight' => -1000];
-  }
-}
-
-/**
- * Implements hook_modules_uninstalled().
- */
-function system_modules_uninstalled($modules) {
-  // @todo Remove this when modules are able to maintain their revision metadata
-  //   keys.
-  //   @see https://www.drupal.org/project/drupal/issues/3074333
-  if (!in_array('workspaces', $modules, TRUE)) {
-    return;
-  }
-
-  $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
-  foreach ($entity_definition_update_manager->getEntityTypes() as $entity_type) {
-    if ($entity_type instanceof ContentEntityTypeInterface && $entity_type->hasRevisionMetadataKey('workspace')) {
-      $entity_type->setRevisionMetadataKey('workspace', NULL);
-      $entity_definition_update_manager->updateEntityType($entity_type);
-    }
-  }
-}
-
 /**
  * Determines if Claro is the admin theme but not the active theme.
  *
@@ -1066,18 +680,6 @@ function _system_is_claro_admin_and_not_active() {
   return $active_theme !== 'claro' && $admin_theme === 'claro';
 }
 
-/**
- * Implements hook_library_info_alter().
- */
-function system_library_info_alter(&$libraries, $extension) {
-  // If Claro is the admin theme but not the active theme, grant Claro the
-  // ability to override the toolbar library with its own assets.
-  if ($extension === 'toolbar' && _system_is_claro_admin_and_not_active()) {
-    require_once DRUPAL_ROOT . '/core/themes/claro/claro.theme';
-    claro_system_module_invoked_library_info_alter($libraries, $extension);
-  }
-}
-
 /**
  * Implements hook_preprocess_toolbar().
  */
@@ -1095,104 +697,3 @@ function system_preprocess_toolbar(array &$variables, $hook, $info) {
     claro_preprocess_toolbar($variables, $hook, $info);
   }
 }
-
-/**
- * Implements hook_theme_registry_alter().
- */
-function system_theme_registry_alter(array &$theme_registry) {
-  // If Claro is the admin theme but not the active theme, use Claro's toolbar
-  // templates.
-  if (_system_is_claro_admin_and_not_active()) {
-    require_once DRUPAL_ROOT . '/core/themes/claro/claro.theme';
-    claro_system_module_invoked_theme_registry_alter($theme_registry);
-  }
-}
-
-/**
- * Implements hook_page_top().
- */
-function system_page_top() {
-  /** @var \Drupal\Core\Routing\AdminContext $admin_context */
-  $admin_context = \Drupal::service('router.admin_context');
-  if ($admin_context->isAdminRoute() && \Drupal::currentUser()->hasPermission('administer site configuration')) {
-    $route_match = \Drupal::routeMatch();
-    $route_name = $route_match->getRouteName();
-    if ($route_name !== 'system.status' && \Drupal::config('system.advisories')->get('enabled')) {
-      /** @var \Drupal\system\SecurityAdvisories\SecurityAdvisoriesFetcher $fetcher */
-      $fetcher = \Drupal::service('system.sa_fetcher');
-      $advisories = $fetcher->getSecurityAdvisories(FALSE);
-      if ($advisories) {
-        $messenger = \Drupal::messenger();
-        $display_as_errors = FALSE;
-        $links = [];
-        foreach ($advisories as $advisory) {
-          // To ensure that all the advisory messages are grouped together on
-          // the page, they must all be warnings or all be errors. If any
-          // advisories are not public service announcements, then display all
-          // the messages as errors because security advisories already tied to
-          // a specific release are immediately actionable by upgrading to a
-          // secure version of a project.
-          $display_as_errors = $display_as_errors ? TRUE : !$advisory->isPsa();
-          $links[] = new Link($advisory->getTitle(), Url::fromUri($advisory->getUrl()));
-        }
-        foreach ($links as $link) {
-          $display_as_errors ? $messenger->addError($link) : $messenger->addWarning($link);
-        }
-        if (\Drupal::moduleHandler()->moduleExists('help')) {
-          $help_link = t('(<a href=":system-help">What are critical security announcements?</a>)', [
-            ':system-help' => Url::fromRoute('help.page', ['name' => 'system'], ['fragment' => 'security-advisories'])->toString(),
-          ]);
-          $display_as_errors ? $messenger->addError($help_link) : $messenger->addWarning($help_link);
-        }
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_file_download().
- */
-function system_file_download($uri) {
-  $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
-  $scheme = $stream_wrapper_manager->getScheme($uri);
-  if ($stream_wrapper_manager->isValidScheme($scheme)) {
-    $target = $stream_wrapper_manager->getTarget($uri);
-    if ($target !== FALSE) {
-      if (!in_array($scheme, Settings::get('file_sa_core_2023_005_schemes', []))) {
-        if (DIRECTORY_SEPARATOR !== '/') {
-          $class = $stream_wrapper_manager->getClass($scheme);
-          if (is_subclass_of($class, LocalStream::class)) {
-            $target = str_replace(DIRECTORY_SEPARATOR, '/', $target);
-          }
-        }
-        $parts = explode('/', $target);
-        if (array_intersect($parts, ['.', '..'])) {
-          return -1;
-        }
-      }
-    }
-  }
-
-  $core_schemes = ['public', 'private', 'temporary'];
-  $additional_public_schemes = array_diff(Settings::get('file_additional_public_schemes', []), $core_schemes);
-  if ($additional_public_schemes) {
-    $scheme = StreamWrapperManager::getScheme($uri);
-    if (in_array($scheme, $additional_public_schemes, TRUE)) {
-      return [
-        // Returning any header grants access, and setting the 'Cache-Control'
-        // header is appropriate for public files.
-        'Cache-Control' => 'public',
-      ];
-    }
-  }
-}
-
-/**
- * Implements hook_archiver_info_alter().
- */
-function system_archiver_info_alter(&$info) {
-  if (!class_exists(\ZipArchive::class)) {
-    // PHP Zip extension is missing.
-    unset($info['Zip']);
-  }
-}
diff --git a/core/modules/system/system.theme.inc b/core/modules/system/system.theme.inc
index fc88b4155642e4987fa4b02596083c18705bc68b..b7a1259e4017f6bcbc3e91c54f970d3e19180687 100644
--- a/core/modules/system/system.theme.inc
+++ b/core/modules/system/system.theme.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Preprocess functions for the System module.
  */
 
 use Drupal\Core\Url;
diff --git a/core/modules/system/system.tokens.inc b/core/modules/system/system.tokens.inc
deleted file mode 100644
index 1938f83d2ee8865d0c1e1b3b7c843def4953f69a..0000000000000000000000000000000000000000
--- a/core/modules/system/system.tokens.inc
+++ /dev/null
@@ -1,220 +0,0 @@
-<?php
-
-/**
- * @file
- * Builds placeholder replacement tokens system-wide data.
- *
- * This file handles tokens for the global 'site' and 'date' tokens.
- */
-
-use Drupal\Core\Url;
-use Drupal\Core\Datetime\Entity\DateFormat;
-use Drupal\Core\Render\BubbleableMetadata;
-
-/**
- * Implements hook_token_info().
- */
-function system_token_info() {
-  $types['site'] = [
-    'name' => t("Site information"),
-    'description' => t("Tokens for site-wide settings and other global information."),
-  ];
-  $types['date'] = [
-    'name' => t("Dates"),
-    'description' => t("Tokens related to times and dates."),
-  ];
-
-  // Site-wide global tokens.
-  $site['name'] = [
-    'name' => t("Name"),
-    'description' => t("The name of the site."),
-  ];
-  $site['slogan'] = [
-    'name' => t("Slogan"),
-    'description' => t("The slogan of the site."),
-  ];
-  $site['mail'] = [
-    'name' => t("Email"),
-    'description' => t("The administrative email address for the site."),
-  ];
-  $site['base-url'] = [
-    'name' => t("Base URL"),
-    'description' => t("The base URL of the site, currently: @base_url",
-      [
-        '@base_url' => \Drupal::service('router.request_context')->getCompleteBaseUrl(),
-      ]),
-  ];
-  $site['base-path'] = [
-    'name' => t("Base path"),
-    'description' => t("The base path of the site, currently: @base_path", ['@base_path' => \Drupal::request()->getBasePath()]),
-  ];
-  $site['url'] = [
-    'name' => t("URL"),
-    'description' => t("The URL of the site's front page with the language prefix, if it exists."),
-  ];
-  $site['url-brief'] = [
-    'name' => t("URL (brief)"),
-    'description' => t("The URL of the site's front page without the protocol."),
-  ];
-  $site['login-url'] = [
-    'name' => t("Login page"),
-    'description' => t("The URL of the site's login page."),
-  ];
-
-  /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
-  $date_formatter = \Drupal::service('date.formatter');
-
-  // Date related tokens.
-  $request_time = \Drupal::time()->getRequestTime();
-  $date['short'] = [
-    'name' => t("Short format"),
-    'description' => t("The current date in 'short' format. (%date)", ['%date' => $date_formatter->format($request_time, 'short')]),
-  ];
-  $date['medium'] = [
-    'name' => t("Medium format"),
-    'description' => t("The current date in 'medium' format. (%date)", ['%date' => $date_formatter->format($request_time, 'medium')]),
-  ];
-  $date['long'] = [
-    'name' => t("Long format"),
-    'description' => t("The current date in 'long' format. (%date)", ['%date' => $date_formatter->format($request_time, 'long')]),
-  ];
-  $date['custom'] = [
-    'name' => t("Custom format"),
-    'description' => t('The current date in a custom format. See <a href="https://www.php.net/manual/datetime.format.php#refsect1-datetime.format-parameters">the PHP documentation</a> for details.'),
-  ];
-  $date['since'] = [
-    'name' => t("Time-since"),
-    'description' => t("The current date in 'time-since' format. (%date)", ['%date' => $date_formatter->formatTimeDiffSince($request_time - 360)]),
-  ];
-  $date['raw'] = [
-    'name' => t("Raw timestamp"),
-    'description' => t("The current date in UNIX timestamp format (%date)", ['%date' => $request_time]),
-  ];
-
-  return [
-    'types' => $types,
-    'tokens' => [
-      'site' => $site,
-      'date' => $date,
-    ],
-  ];
-}
-
-/**
- * Implements hook_tokens().
- */
-function system_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
-  $token_service = \Drupal::token();
-
-  $url_options = ['absolute' => TRUE];
-  if (isset($options['langcode'])) {
-    $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
-    $langcode = $options['langcode'];
-  }
-  else {
-    $langcode = NULL;
-  }
-  $replacements = [];
-
-  if ($type == 'site') {
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'name':
-          $config = \Drupal::config('system.site');
-          $bubbleable_metadata->addCacheableDependency($config);
-          $site_name = $config->get('name');
-          $replacements[$original] = $site_name;
-          break;
-
-        case 'slogan':
-          $config = \Drupal::config('system.site');
-          $bubbleable_metadata->addCacheableDependency($config);
-          $slogan = $config->get('slogan');
-          $build = [
-            '#markup' => $slogan,
-          ];
-          // @todo Fix in https://www.drupal.org/node/2577827
-          $replacements[$original] = \Drupal::service('renderer')->renderInIsolation($build);
-          break;
-
-        case 'mail':
-          $config = \Drupal::config('system.site');
-          $bubbleable_metadata->addCacheableDependency($config);
-          $replacements[$original] = $config->get('mail');
-          break;
-
-        case 'base-url':
-          $bubbleable_metadata->addCacheContexts(['url.site']);
-          $replacements[$original] = \Drupal::service('router.request_context')->getCompleteBaseUrl();
-          break;
-
-        case 'base-path':
-          $bubbleable_metadata->addCacheContexts(['url.site']);
-          $replacements[$original] = \Drupal::request()->getBasePath();
-          break;
-
-        case 'url':
-          /** @var \Drupal\Core\GeneratedUrl $result */
-          $result = Url::fromRoute('<front>', [], $url_options)->toString(TRUE);
-          $bubbleable_metadata->addCacheableDependency($result);
-          $replacements[$original] = $result->getGeneratedUrl();
-          break;
-
-        case 'url-brief':
-          /** @var \Drupal\Core\GeneratedUrl $result */
-          $result = Url::fromRoute('<front>', [], $url_options)->toString(TRUE);
-          $bubbleable_metadata->addCacheableDependency($result);
-          $replacements[$original] = preg_replace(['!^https?://!', '!/$!'], '', $result->getGeneratedUrl());
-          break;
-
-        case 'login-url':
-          /** @var \Drupal\Core\GeneratedUrl $result */
-          $result = Url::fromRoute('user.page', [], $url_options)->toString(TRUE);
-          $bubbleable_metadata->addCacheableDependency($result);
-          $replacements[$original] = $result->getGeneratedUrl();
-          break;
-      }
-    }
-  }
-
-  elseif ($type == 'date') {
-    if (empty($data['date'])) {
-      $date = \Drupal::time()->getRequestTime();
-      // We depend on the current request time, so the tokens are not cacheable
-      // at all.
-      $bubbleable_metadata->setCacheMaxAge(0);
-    }
-    else {
-      $date = $data['date'];
-    }
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'short':
-        case 'medium':
-        case 'long':
-          $date_format = DateFormat::load($name);
-          $bubbleable_metadata->addCacheableDependency($date_format);
-          $replacements[$original] = \Drupal::service('date.formatter')->format($date, $name, '', NULL, $langcode);
-          break;
-
-        case 'since':
-          $replacements[$original] = \Drupal::service('date.formatter')->formatTimeDiffSince($date, ['langcode' => $langcode]);
-          $bubbleable_metadata->setCacheMaxAge(0);
-          break;
-
-        case 'raw':
-          $replacements[$original] = $date;
-          break;
-      }
-    }
-
-    if ($created_tokens = $token_service->findWithPrefix($tokens, 'custom')) {
-      foreach ($created_tokens as $name => $original) {
-        $replacements[$original] = \Drupal::service('date.formatter')->format($date, 'custom', $name, NULL, $langcode);
-      }
-    }
-  }
-
-  return $replacements;
-}
diff --git a/core/modules/system/tests/modules/advisory_feed_test/advisory_feed_test.module b/core/modules/system/tests/modules/advisory_feed_test/advisory_feed_test.module
deleted file mode 100644
index 2d7341da5f60c15a2c4ea420a212952b196324d3..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/advisory_feed_test/advisory_feed_test.module
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * @file
- * Module for testing the display of security advisories.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Extension\Extension;
-
-/**
- * Implements hook_system_info_alter().
- */
-function advisory_feed_test_system_info_alter(&$info, Extension $file) {
-  // Alter the 'generic_module1_test' module to use the 'generic_module1_project'
-  // project name.  This ensures that for an extension where the 'name' and
-  // the 'project' properties do not match, 'project' is used for matching
-  // 'project' in the JSON feed.
-  $system_info = [
-    'generic_module1_test' => [
-      'project' => 'generic_module1_project',
-      'version' => '8.x-1.1',
-      'hidden' => FALSE,
-    ],
-    'generic_module2_test' => [
-      'project' => 'generic_module2_project',
-      'version' => '8.x-1.1',
-      'hidden' => FALSE,
-    ],
-  ];
-  if (!empty($system_info[$file->getName()])) {
-    foreach ($system_info[$file->getName()] as $key => $value) {
-      $info[$key] = $value;
-    }
-  }
-}
diff --git a/core/modules/system/tests/modules/advisory_feed_test/src/Hook/AdvisoryFeedTestHooks.php b/core/modules/system/tests/modules/advisory_feed_test/src/Hook/AdvisoryFeedTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..48aa82e01996a343c04268379897b1ea96bfbda0
--- /dev/null
+++ b/core/modules/system/tests/modules/advisory_feed_test/src/Hook/AdvisoryFeedTestHooks.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\advisory_feed_test\Hook;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for advisory_feed_test.
+ */
+class AdvisoryFeedTestHooks {
+
+  /**
+   * Implements hook_system_info_alter().
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(&$info, Extension $file) {
+    // Alter the 'generic_module1_test' module to use the 'generic_module1_project'
+    // project name.  This ensures that for an extension where the 'name' and
+    // the 'project' properties do not match, 'project' is used for matching
+    // 'project' in the JSON feed.
+    $system_info = [
+      'generic_module1_test' => [
+        'project' => 'generic_module1_project',
+        'version' => '8.x-1.1',
+        'hidden' => FALSE,
+      ],
+      'generic_module2_test' => [
+        'project' => 'generic_module2_project',
+        'version' => '8.x-1.1',
+        'hidden' => FALSE,
+      ],
+    ];
+    if (!empty($system_info[$file->getName()])) {
+      foreach ($system_info[$file->getName()] as $key => $value) {
+        $info[$key] = $value;
+      }
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module
index 3c62ad003c19b4cea48a622907e4ea1c3ed99bd0..3adcaacf29d07fa47abd7f77291e3439b1096316 100644
--- a/core/modules/system/tests/modules/common_test/common_test.module
+++ b/core/modules/system/tests/modules/common_test/common_test.module
@@ -7,40 +7,6 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Asset\AttachedAssetsInterface;
-use Drupal\Core\Language\LanguageInterface;
-
-/**
- * Implements hook_TYPE_alter().
- */
-function common_test_drupal_alter_alter(&$data, &$arg2 = NULL, &$arg3 = NULL) {
-  // Alter first argument.
-  if (is_array($data)) {
-    $data['foo'] = 'Drupal';
-  }
-  elseif (is_object($data)) {
-    $data->foo = 'Drupal';
-  }
-  // Alter second argument, if present.
-  if (isset($arg2)) {
-    if (is_array($arg2)) {
-      $arg2['foo'] = 'Drupal';
-    }
-    elseif (is_object($arg2)) {
-      $arg2->foo = 'Drupal';
-    }
-  }
-  // Try to alter third argument, if present.
-  if (isset($arg3)) {
-    if (is_array($arg3)) {
-      $arg3['foo'] = 'Drupal';
-    }
-    elseif (is_object($arg3)) {
-      $arg3->foo = 'Drupal';
-    }
-  }
-}
-
 /**
  * Implements hook_TYPE_alter().
  *
@@ -75,18 +41,6 @@ function olivero_drupal_alter_alter(&$data, &$arg2 = NULL, &$arg3 = NULL) {
   }
 }
 
-/**
- * Implements hook_TYPE_alter().
- *
- * This is to verify that
- * \Drupal::moduleHandler()->alter(array(TYPE1, TYPE2), ...) allows
- * hook_module_implements_alter() to affect the order in which module
- * implementations are executed.
- */
-function block_drupal_alter_foo_alter(&$data, &$arg2 = NULL, &$arg3 = NULL) {
-  $data['foo'] .= ' block';
-}
-
 /**
  * Implements hook_module_implements_alter().
  *
@@ -105,20 +59,6 @@ function common_test_module_implements_alter(&$implementations, $hook) {
   }
 }
 
-/**
- * Implements hook_theme().
- */
-function common_test_theme(): array {
-  return [
-    'common_test_foo' => [
-      'variables' => ['foo' => 'foo', 'bar' => 'bar'],
-    ],
-    'common_test_render_element' => [
-      'render element' => 'foo',
-    ],
-  ];
-}
-
 /**
  * Implements MODULE_preprocess().
  *
@@ -142,134 +82,3 @@ function common_test_preprocess_common_test_render_element(&$variables) {
   }
   $variables['#attached']['library'][] = 'test/specific_preprocess';
 }
-
-/**
- * Implements hook_library_info_build().
- */
-function common_test_library_info_build() {
-  $libraries = [];
-  if (\Drupal::state()->get('common_test.library_info_build_test')) {
-    $libraries['dynamic_library'] = [
-      'version' => '1.0',
-      'css' => [
-        'base' => [
-          'common_test.css' => [],
-        ],
-      ],
-    ];
-  }
-  return $libraries;
-}
-
-/**
- * Implements hook_library_info_alter().
- */
-function common_test_library_info_alter(&$libraries, $module) {
-  if ($module === 'core' && isset($libraries['loadjs'])) {
-    // Change the version of loadjs to 0.0.
-    $libraries['loadjs']['version'] = '0.0';
-    // Make loadjs depend on jQuery Form to test library dependencies.
-    $libraries['loadjs']['dependencies'][] = 'core/internal.jquery.form';
-  }
-
-  // Alter the dynamically registered library definition.
-  if ($module === 'common_test' && isset($libraries['dynamic_library'])) {
-    $libraries['dynamic_library']['dependencies'] = [
-      'core/jquery',
-    ];
-  }
-}
-
-/**
- * Implements hook_cron().
- *
- * System module should handle if a module does not catch an exception and keep
- * cron going.
- *
- * @see common_test_cron_helper()
- */
-function common_test_cron() {
-  throw new Exception('Uncaught exception');
-}
-
-/**
- * Implements hook_page_attachments().
- *
- * @see \Drupal\system\Tests\Common\PageRenderTest::assertPageRenderHookExceptions()
- */
-function common_test_page_attachments(array &$page) {
-  $page['#attached']['library'][] = 'core/foo';
-  $page['#attached']['library'][] = 'core/bar';
-  $page['#cache']['tags'] = ['example'];
-  $page['#cache']['contexts'] = ['user.permissions'];
-
-  if (\Drupal::state()->get('common_test.hook_page_attachments.descendant_attached', FALSE)) {
-    $page['content']['#attached']['library'][] = 'core/jquery';
-  }
-
-  if (\Drupal::state()->get('common_test.hook_page_attachments.render_array', FALSE)) {
-    $page['something'] = [
-      '#markup' => 'test',
-    ];
-  }
-
-  if (\Drupal::state()->get('common_test.hook_page_attachments.early_rendering', FALSE)) {
-    // Do some early rendering.
-    $element = ['#markup' => '123'];
-    \Drupal::service('renderer')->render($element);
-  }
-}
-
-/**
- * Implements hook_page_attachments_alter().
- *
- * @see \Drupal\system\Tests\Common\PageRenderTest::assertPageRenderHookExceptions()
- */
-function common_test_page_attachments_alter(array &$page) {
-  // Remove a library that was added in common_test_page_attachments(), to test
-  // that this hook can do what it claims to do.
-  if (isset($page['#attached']['library']) && ($index = array_search('core/bar', $page['#attached']['library'])) && $index !== FALSE) {
-    unset($page['#attached']['library'][$index]);
-  }
-  $page['#attached']['library'][] = 'core/baz';
-  $page['#cache']['tags'] = ['example'];
-  $page['#cache']['contexts'] = ['user.permissions'];
-
-  if (\Drupal::state()->get('common_test.hook_page_attachments_alter.descendant_attached', FALSE)) {
-    $page['content']['#attached']['library'][] = 'core/jquery';
-  }
-
-  if (\Drupal::state()->get('common_test.hook_page_attachments_alter.render_array', FALSE)) {
-    $page['something'] = [
-      '#markup' => 'test',
-    ];
-  }
-}
-
-/**
- * Implements hook_js_alter().
- *
- * @see \Drupal\KernelTests\Core\Asset\AttachedAssetsTest::testAlter()
- */
-function common_test_js_alter(&$javascript, AttachedAssetsInterface $assets, LanguageInterface $language) {
-  // Attach alter.js above tableselect.js.
-  $alter_js = \Drupal::service('extension.list.module')->getPath('common_test') . '/alter.js';
-  if (array_key_exists($alter_js, $javascript) && array_key_exists('core/misc/tableselect.js', $javascript)) {
-    $javascript[$alter_js]['weight'] = $javascript['core/misc/tableselect.js']['weight'] - 1;
-  }
-}
-
-/**
- * Implements hook_js_settings_alter().
- *
- * @see \Drupal\system\Tests\Common\JavaScriptTest::testHeaderSetting()
- */
-function common_test_js_settings_alter(&$settings, AttachedAssetsInterface $assets) {
-  // Modify an existing setting.
-  if (array_key_exists('pluralDelimiter', $settings)) {
-    $settings['pluralDelimiter'] = '☃';
-  }
-
-  // Add a setting.
-  $settings['foo'] = 'bar';
-}
diff --git a/core/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php b/core/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..bbd1a6328792a0760cbf115c12db6e8c2aed586b
--- /dev/null
+++ b/core/modules/system/tests/modules/common_test/src/Hook/CommonTestHooks.php
@@ -0,0 +1,197 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\common_test\Hook;
+
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for common_test.
+ */
+class CommonTestHooks {
+
+  /**
+   * Implements hook_TYPE_alter().
+   */
+  #[Hook('drupal_alter_alter')]
+  public function drupalAlterAlter(&$data, &$arg2 = NULL, &$arg3 = NULL) {
+    // Alter first argument.
+    if (is_array($data)) {
+      $data['foo'] = 'Drupal';
+    }
+    elseif (is_object($data)) {
+      $data->foo = 'Drupal';
+    }
+    // Alter second argument, if present.
+    if (isset($arg2)) {
+      if (is_array($arg2)) {
+        $arg2['foo'] = 'Drupal';
+      }
+      elseif (is_object($arg2)) {
+        $arg2->foo = 'Drupal';
+      }
+    }
+    // Try to alter third argument, if present.
+    if (isset($arg3)) {
+      if (is_array($arg3)) {
+        $arg3['foo'] = 'Drupal';
+      }
+      elseif (is_object($arg3)) {
+        $arg3->foo = 'Drupal';
+      }
+    }
+  }
+
+  /**
+   * Implements hook_TYPE_alter().
+   *
+   * This is to verify that
+   * \Drupal::moduleHandler()->alter(array(TYPE1, TYPE2), ...) allows
+   * hook_module_implements_alter() to affect the order in which module
+   * implementations are executed.
+   */
+  #[Hook('drupal_alter_foo_alter', module: 'block')]
+  public function blockDrupalAlterFooAlter(&$data, &$arg2 = NULL, &$arg3 = NULL) {
+    $data['foo'] .= ' block';
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'common_test_foo' => [
+        'variables' => [
+          'foo' => 'foo',
+          'bar' => 'bar',
+        ],
+      ],
+      'common_test_render_element' => [
+        'render element' => 'foo',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_library_info_build().
+   */
+  #[Hook('library_info_build')]
+  public function libraryInfoBuild() {
+    $libraries = [];
+    if (\Drupal::state()->get('common_test.library_info_build_test')) {
+      $libraries['dynamic_library'] = ['version' => '1.0', 'css' => ['base' => ['common_test.css' => []]]];
+    }
+    return $libraries;
+  }
+
+  /**
+   * Implements hook_library_info_alter().
+   */
+  #[Hook('library_info_alter')]
+  public function libraryInfoAlter(&$libraries, $module) {
+    if ($module === 'core' && isset($libraries['loadjs'])) {
+      // Change the version of loadjs to 0.0.
+      $libraries['loadjs']['version'] = '0.0';
+      // Make loadjs depend on jQuery Form to test library dependencies.
+      $libraries['loadjs']['dependencies'][] = 'core/internal.jquery.form';
+    }
+    // Alter the dynamically registered library definition.
+    if ($module === 'common_test' && isset($libraries['dynamic_library'])) {
+      $libraries['dynamic_library']['dependencies'] = ['core/jquery'];
+    }
+  }
+
+  /**
+   * Implements hook_cron().
+   *
+   * System module should handle if a module does not catch an exception and keep
+   * cron going.
+   *
+   * @see common_test_cron_helper()
+   */
+  #[Hook('cron')]
+  public function cron() {
+    throw new \Exception('Uncaught exception');
+  }
+
+  /**
+   * Implements hook_page_attachments().
+   *
+   * @see \Drupal\system\Tests\Common\PageRenderTest::assertPageRenderHookExceptions()
+   */
+  #[Hook('page_attachments')]
+  public function pageAttachments(array &$page) {
+    $page['#attached']['library'][] = 'core/foo';
+    $page['#attached']['library'][] = 'core/bar';
+    $page['#cache']['tags'] = ['example'];
+    $page['#cache']['contexts'] = ['user.permissions'];
+    if (\Drupal::state()->get('common_test.hook_page_attachments.descendant_attached', FALSE)) {
+      $page['content']['#attached']['library'][] = 'core/jquery';
+    }
+    if (\Drupal::state()->get('common_test.hook_page_attachments.render_array', FALSE)) {
+      $page['something'] = ['#markup' => 'test'];
+    }
+    if (\Drupal::state()->get('common_test.hook_page_attachments.early_rendering', FALSE)) {
+      // Do some early rendering.
+      $element = ['#markup' => '123'];
+      \Drupal::service('renderer')->render($element);
+    }
+  }
+
+  /**
+   * Implements hook_page_attachments_alter().
+   *
+   * @see \Drupal\system\Tests\Common\PageRenderTest::assertPageRenderHookExceptions()
+   */
+  #[Hook('page_attachments_alter')]
+  public function pageAttachmentsAlter(array &$page) {
+    // Remove a library that was added in common_test_page_attachments(), to test
+    // that this hook can do what it claims to do.
+    if (isset($page['#attached']['library']) && ($index = array_search('core/bar', $page['#attached']['library'])) && $index !== FALSE) {
+      unset($page['#attached']['library'][$index]);
+    }
+    $page['#attached']['library'][] = 'core/baz';
+    $page['#cache']['tags'] = ['example'];
+    $page['#cache']['contexts'] = ['user.permissions'];
+    if (\Drupal::state()->get('common_test.hook_page_attachments_alter.descendant_attached', FALSE)) {
+      $page['content']['#attached']['library'][] = 'core/jquery';
+    }
+    if (\Drupal::state()->get('common_test.hook_page_attachments_alter.render_array', FALSE)) {
+      $page['something'] = ['#markup' => 'test'];
+    }
+  }
+
+  /**
+   * Implements hook_js_alter().
+   *
+   * @see \Drupal\KernelTests\Core\Asset\AttachedAssetsTest::testAlter()
+   */
+  #[Hook('js_alter')]
+  public function jsAlter(&$javascript, AttachedAssetsInterface $assets, LanguageInterface $language) {
+    // Attach alter.js above tableselect.js.
+    $alter_js = \Drupal::service('extension.list.module')->getPath('common_test') . '/alter.js';
+    if (array_key_exists($alter_js, $javascript) && array_key_exists('core/misc/tableselect.js', $javascript)) {
+      $javascript[$alter_js]['weight'] = $javascript['core/misc/tableselect.js']['weight'] - 1;
+    }
+  }
+
+  /**
+   * Implements hook_js_settings_alter().
+   *
+   * @see \Drupal\system\Tests\Common\JavaScriptTest::testHeaderSetting()
+   */
+  #[Hook('js_settings_alter')]
+  public function jsSettingsAlter(&$settings, AttachedAssetsInterface $assets) {
+    // Modify an existing setting.
+    if (array_key_exists('pluralDelimiter', $settings)) {
+      $settings['pluralDelimiter'] = '☃';
+    }
+    // Add a setting.
+    $settings['foo'] = 'bar';
+  }
+
+}
diff --git a/core/modules/system/tests/modules/common_test_cron_helper/common_test_cron_helper.module b/core/modules/system/tests/modules/common_test_cron_helper/common_test_cron_helper.module
deleted file mode 100644
index fbaa815fb8aec6f597f77820dfd4753ea1067e0b..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/common_test_cron_helper/common_test_cron_helper.module
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the testCronExceptions in addition to common_test module.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_cron().
- *
- * Function common_test_cron() throws an exception, but the execution should
- * reach this function as well.
- *
- * @see common_test_cron()
- */
-function common_test_cron_helper_cron() {
-  \Drupal::state()->set('common_test.cron', 'success');
-}
diff --git a/core/modules/system/tests/modules/common_test_cron_helper/src/Hook/CommonTestCronHelperHooks.php b/core/modules/system/tests/modules/common_test_cron_helper/src/Hook/CommonTestCronHelperHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..024669c3da7225c5004087c6082097431b1d19a6
--- /dev/null
+++ b/core/modules/system/tests/modules/common_test_cron_helper/src/Hook/CommonTestCronHelperHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\common_test_cron_helper\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for common_test_cron_helper.
+ */
+class CommonTestCronHelperHooks {
+
+  /**
+   * Implements hook_cron().
+   *
+   * Function common_test_cron() throws an exception, but the execution should
+   * reach this function as well.
+   *
+   * @see common_test_cron()
+   */
+  #[Hook('cron')]
+  public function cron() {
+    \Drupal::state()->set('common_test.cron', 'success');
+  }
+
+}
diff --git a/core/modules/system/tests/modules/container_rebuild_test/src/TestController.php b/core/modules/system/tests/modules/container_rebuild_test/src/TestController.php
index 0b2fa1adcbdc408415e14e44b5911d7738f87f42..bc7704eb45dccaeb667e4479561ac91e7dee6729 100644
--- a/core/modules/system/tests/modules/container_rebuild_test/src/TestController.php
+++ b/core/modules/system/tests/modules/container_rebuild_test/src/TestController.php
@@ -38,7 +38,7 @@ public function showModuleInfo(string $module, string $function) {
     else {
       $module_message .= 'not installed';
     }
-    $function_message = $function . ': ' . var_export(function_exists($function), TRUE);
+    $function_message = $function . ': ' . var_export(\Drupal::moduleHandler()->hasImplementations($function, $module), TRUE);
 
     return [
       '#theme' => 'item_list',
diff --git a/core/modules/system/tests/modules/css_disable_transitions_test/css_disable_transitions_test.module b/core/modules/system/tests/modules/css_disable_transitions_test/css_disable_transitions_test.module
deleted file mode 100644
index a67362edaa1ae73b4fe6748fb8224c4a9fd50eec..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/css_disable_transitions_test/css_disable_transitions_test.module
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for disabling animations in tests.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_page_attachments().
- */
-function css_disable_transitions_test_page_attachments(array &$attachments) {
-  // Unconditionally attach an asset to the page.
-  $attachments['#attached']['library'][] = 'css_disable_transitions_test/testing.css_disable_transitions_test';
-}
diff --git a/core/modules/system/tests/modules/css_disable_transitions_test/src/Hook/CssDisableTransitionsTestHooks.php b/core/modules/system/tests/modules/css_disable_transitions_test/src/Hook/CssDisableTransitionsTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..a48fe1d4e89998d9e8afc85b4d44303f28662076
--- /dev/null
+++ b/core/modules/system/tests/modules/css_disable_transitions_test/src/Hook/CssDisableTransitionsTestHooks.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\css_disable_transitions_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for css_disable_transitions_test.
+ */
+class CssDisableTransitionsTestHooks {
+
+  /**
+   * Implements hook_page_attachments().
+   */
+  #[Hook('page_attachments')]
+  public function pageAttachments(array &$attachments) {
+    // Unconditionally attach an asset to the page.
+    $attachments['#attached']['library'][] = 'css_disable_transitions_test/testing.css_disable_transitions_test';
+  }
+
+}
diff --git a/core/modules/system/tests/modules/database_test/database_test.module b/core/modules/system/tests/modules/database_test/database_test.module
deleted file mode 100644
index 45e8dbe5afdff4bb47f5cf20bb12956d075caa7e..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/database_test/database_test.module
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-
-/**
- * @file
- * Database test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Database\Query\AlterableInterface;
-
-/**
- * Implements hook_query_alter().
- */
-function database_test_query_alter(AlterableInterface $query) {
-
-  if ($query->hasTag('database_test_alter_add_range')) {
-    $query->range(0, 2);
-  }
-
-  if ($query->hasTag('database_test_alter_add_join')) {
-    $people_alias = $query->join('test', 'people', "[test_task].[pid] = [%alias].[id]");
-    $query->addField($people_alias, 'name', 'name');
-    $query->condition($people_alias . '.id', 2);
-  }
-
-  if ($query->hasTag('database_test_alter_change_conditional')) {
-    $conditions =& $query->conditions();
-    $conditions[0]['value'] = 2;
-  }
-
-  if ($query->hasTag('database_test_alter_change_fields')) {
-    $fields =& $query->getFields();
-    unset($fields['age']);
-  }
-
-  if ($query->hasTag('database_test_alter_change_expressions')) {
-    $expressions =& $query->getExpressions();
-    $expressions['double_age']['expression'] = '[age]*3';
-  }
-}
-
-/**
- * Implements hook_query_TAG_alter().
- *
- * Called by DatabaseTestCase::testAlterRemoveRange.
- */
-function database_test_query_database_test_alter_remove_range_alter(AlterableInterface $query) {
-  $query->range();
-}
diff --git a/core/modules/system/tests/modules/database_test/src/Hook/DatabaseTestHooks.php b/core/modules/system/tests/modules/database_test/src/Hook/DatabaseTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..2c01f000426a8dac1c868b72ea9d8e9aa4b59b1e
--- /dev/null
+++ b/core/modules/system/tests/modules/database_test/src/Hook/DatabaseTestHooks.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\database_test\Hook;
+
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for database_test.
+ */
+class DatabaseTestHooks {
+
+  /**
+   * Implements hook_query_alter().
+   */
+  #[Hook('query_alter')]
+  public function queryAlter(AlterableInterface $query) {
+    if ($query->hasTag('database_test_alter_add_range')) {
+      $query->range(0, 2);
+    }
+    if ($query->hasTag('database_test_alter_add_join')) {
+      $people_alias = $query->join('test', 'people', "[test_task].[pid] = [%alias].[id]");
+      $query->addField($people_alias, 'name', 'name');
+      $query->condition($people_alias . '.id', 2);
+    }
+    if ($query->hasTag('database_test_alter_change_conditional')) {
+      $conditions =& $query->conditions();
+      $conditions[0]['value'] = 2;
+    }
+    if ($query->hasTag('database_test_alter_change_fields')) {
+      $fields =& $query->getFields();
+      unset($fields['age']);
+    }
+    if ($query->hasTag('database_test_alter_change_expressions')) {
+      $expressions =& $query->getExpressions();
+      $expressions['double_age']['expression'] = '[age]*3';
+    }
+  }
+
+  /**
+   * Implements hook_query_TAG_alter().
+   *
+   * Called by DatabaseTestCase::testAlterRemoveRange.
+   */
+  #[Hook('query_database_test_alter_remove_range_alter')]
+  public function queryDatabaseTestAlterRemoveRangeAlter(AlterableInterface $query) {
+    $query->range();
+  }
+
+}
diff --git a/core/modules/system/tests/modules/decoupled_menus_test/decoupled_menus_test.module b/core/modules/system/tests/modules/decoupled_menus_test/decoupled_menus_test.module
deleted file mode 100644
index bbe6cb1052dd84550876c91dbab3ff5c26da96d4..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/decoupled_menus_test/decoupled_menus_test.module
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-/**
- * @file
- * Support module for decoupled_menus endpoint in tests.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_menu_links_discovered_alter().
- */
-function decoupled_menus_test_menu_links_discovered_alter(&$links) {
-  // Sets a custom link relation type on a menu item.
-  // @see https://tools.ietf.org/id/draft-pot-authentication-link-01.html
-  $links['user.page']['options']['attributes']['rel'] = 'authenticated-as';
-}
diff --git a/core/modules/system/tests/modules/decoupled_menus_test/src/Hook/DecoupledMenusTestHooks.php b/core/modules/system/tests/modules/decoupled_menus_test/src/Hook/DecoupledMenusTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..2e3ae6c12ef2f7ed7963ad12bde5b281fef51a26
--- /dev/null
+++ b/core/modules/system/tests/modules/decoupled_menus_test/src/Hook/DecoupledMenusTestHooks.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\decoupled_menus_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for decoupled_menus_test.
+ */
+class DecoupledMenusTestHooks {
+
+  /**
+   * Implements hook_menu_links_discovered_alter().
+   */
+  #[Hook('menu_links_discovered_alter')]
+  public function menuLinksDiscoveredAlter(&$links) {
+    // Sets a custom link relation type on a menu item.
+    // @see https://tools.ietf.org/id/draft-pot-authentication-link-01.html
+    $links['user.page']['options']['attributes']['rel'] = 'authenticated-as';
+  }
+
+}
diff --git a/core/modules/system/tests/modules/delay_cache_tags_invalidation/delay_cache_tags_invalidation.module b/core/modules/system/tests/modules/delay_cache_tags_invalidation/delay_cache_tags_invalidation.module
deleted file mode 100644
index 1c47ca4a0da93e31eb9ae4d8c94cdd8697e45459..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/delay_cache_tags_invalidation/delay_cache_tags_invalidation.module
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-/**
- * @file
- * Functions to test delayed cache tags invalidation.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Cache\Cache;
-use Drupal\entity_test\Entity\EntityTest;
-use Drupal\user\Entity\User;
-use Drupal\user\UserInterface;
-
-/**
- * Implements hook_ENTITY_TYPE_insert().
- */
-function delay_cache_tags_invalidation_entity_test_insert(EntityTest $entity) {
-  if (\Drupal::state()->get('delay_cache_tags_invalidation_exception')) {
-    throw new \Exception('Abort entity save to trigger transaction rollback.');
-  }
-
-  // Read the pre-transaction cache writes.
-  // @see \Drupal\KernelTests\Core\Cache\EndOfTransactionQueriesTest::testEntitySave()
-  \Drupal::state()->set(__FUNCTION__ . '__pretransaction_foobar', \Drupal::cache()->get('test_cache_pretransaction_foobar'));
-  \Drupal::state()->set(__FUNCTION__ . '__pretransaction_entity_test_list', \Drupal::cache()->get('test_cache_pretransaction_entity_test_list'));
-
-  // Write during the transaction.
-  \Drupal::cache()->set(__FUNCTION__ . '__during_transaction_foobar', 'something', Cache::PERMANENT, ['foobar']);
-  \Drupal::cache()->set(__FUNCTION__ . '__during_transaction_entity_test_list', 'something', Cache::PERMANENT, ['entity_test_list']);
-
-  // Trigger a nested entity save and hence a nested transaction.
-  User::create([
-    'name' => 'john doe',
-    'status' => 1,
-  ])->save();
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert().
- */
-function delay_cache_tags_invalidation_user_insert(UserInterface $entity) {
-  if ($entity->getAccountName() === 'john doe') {
-    // Read the in-transaction cache writes.
-    // @see  delay_cache_tags_invalidation_entity_test_insert()
-    \Drupal::state()->set(__FUNCTION__ . '__during_transaction_foobar', \Drupal::cache()->get('delay_cache_tags_invalidation_entity_test_insert__during_transaction_foobar'));
-    \Drupal::state()->set(__FUNCTION__ . '__during_transaction_entity_test_list', \Drupal::cache()->get('delay_cache_tags_invalidation_entity_test_insert__during_transaction_entity_test_list'));
-  }
-}
diff --git a/core/modules/system/tests/modules/delay_cache_tags_invalidation/src/Hook/DelayCacheTagsInvalidationHooks.php b/core/modules/system/tests/modules/delay_cache_tags_invalidation/src/Hook/DelayCacheTagsInvalidationHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..d7ad7c9af75b337528c12076cbc590fa8799b036
--- /dev/null
+++ b/core/modules/system/tests/modules/delay_cache_tags_invalidation/src/Hook/DelayCacheTagsInvalidationHooks.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\delay_cache_tags_invalidation\Hook;
+
+use Drupal\user\UserInterface;
+use Drupal\user\Entity\User;
+use Drupal\Core\Cache\Cache;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for delay_cache_tags_invalidation.
+ */
+class DelayCacheTagsInvalidationHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert().
+   */
+  #[Hook('entity_test_insert')]
+  public function entityTestInsert(EntityTest $entity) {
+    if (\Drupal::state()->get('delay_cache_tags_invalidation_exception')) {
+      throw new \Exception('Abort entity save to trigger transaction rollback.');
+    }
+    // Read the pre-transaction cache writes.
+    // @see \Drupal\KernelTests\Core\Cache\EndOfTransactionQueriesTest::testEntitySave()
+    \Drupal::state()->set('delay_cache_tags_invalidation_entity_test_insert' . '__pretransaction_foobar', \Drupal::cache()->get('test_cache_pretransaction_foobar'));
+    \Drupal::state()->set('delay_cache_tags_invalidation_entity_test_insert' . '__pretransaction_entity_test_list', \Drupal::cache()->get('test_cache_pretransaction_entity_test_list'));
+    // Write during the transaction.
+    \Drupal::cache()->set('delay_cache_tags_invalidation_entity_test_insert' . '__during_transaction_foobar', 'something', Cache::PERMANENT, ['foobar']);
+    \Drupal::cache()->set('delay_cache_tags_invalidation_entity_test_insert' . '__during_transaction_entity_test_list', 'something', Cache::PERMANENT, ['entity_test_list']);
+    // Trigger a nested entity save and hence a nested transaction.
+    User::create(['name' => 'john doe', 'status' => 1])->save();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert().
+   */
+  #[Hook('user_insert')]
+  public function userInsert(UserInterface $entity) {
+    if ($entity->getAccountName() === 'john doe') {
+      // Read the in-transaction cache writes.
+      // @see  delay_cache_tags_invalidation_entity_test_insert()
+      \Drupal::state()->set('delay_cache_tags_invalidation_user_insert' . '__during_transaction_foobar', \Drupal::cache()->get('delay_cache_tags_invalidation_entity_test_insert__during_transaction_foobar'));
+      \Drupal::state()->set('delay_cache_tags_invalidation_user_insert' . '__during_transaction_entity_test_list', \Drupal::cache()->get('delay_cache_tags_invalidation_entity_test_insert__during_transaction_entity_test_list'));
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/dependency_version_test/dependency_version_test.module b/core/modules/system/tests/modules/dependency_version_test/dependency_version_test.module
deleted file mode 100644
index 924d9ec9ee1c48e9a3293509df733bd2f13c8291..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/dependency_version_test/dependency_version_test.module
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-/**
- * @file
- * Module for testing the dependency version comparisons.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Extension\Extension;
-
-/**
- * Implements hook_system_info_alter().
- */
-function dependency_version_test_system_info_alter(&$info, Extension $file, $type) {
-  // Simulate that the core version for Views module contains the string '8.x'.
-  if ($file->getName() == 'views') {
-    $info['version'] = '9.8.x-dev';
-  }
-
-  // Make the test_module require Views 9.2, which should be compatible with
-  // core version 9.8.x-dev from above.
-  if ($file->getName() == 'test_module') {
-    $info['dependencies'] = ['drupal:views (>=9.2)'];
-  }
-
-}
diff --git a/core/modules/system/tests/modules/dependency_version_test/src/Hook/DependencyVersionTestHooks.php b/core/modules/system/tests/modules/dependency_version_test/src/Hook/DependencyVersionTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..2e468e53ffffc4a08c691e49840057f7ca9007cc
--- /dev/null
+++ b/core/modules/system/tests/modules/dependency_version_test/src/Hook/DependencyVersionTestHooks.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\dependency_version_test\Hook;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for dependency_version_test.
+ */
+class DependencyVersionTestHooks {
+
+  /**
+   * Implements hook_system_info_alter().
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(&$info, Extension $file, $type) {
+    // Simulate that the core version for Views module contains the string '8.x'.
+    if ($file->getName() == 'views') {
+      $info['version'] = '9.8.x-dev';
+    }
+    // Make the test_module require Views 9.2, which should be compatible with
+    // core version 9.8.x-dev from above.
+    if ($file->getName() == 'test_module') {
+      $info['dependencies'] = ['drupal:views (>=9.2)'];
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/deprecated_module_test/deprecated_module_test.module b/core/modules/system/tests/modules/deprecated_module_test/deprecated_module_test.module
deleted file mode 100644
index 79044a7c10fc8d32631a6baf451a52bccefd0036..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/deprecated_module_test/deprecated_module_test.module
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * Deprecated module test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Extension\Extension;
-
-/**
- * Implements hook_system_info_alter().
- */
-function deprecated_module_test_system_info_alter(array &$info, Extension $file, $type) {
-  // Make the 'deprecated_module_contrib' look like it isn't part of core.
-  if ($type === 'module' && $info['name'] === 'Deprecated module contrib') {
-    $file->origin = 'sites/all';
-  }
-}
diff --git a/core/modules/system/tests/modules/deprecated_module_test/src/Hook/DeprecatedModuleTestHooks.php b/core/modules/system/tests/modules/deprecated_module_test/src/Hook/DeprecatedModuleTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c4129261c964ec5f37a9f346a45edf46b7b715f1
--- /dev/null
+++ b/core/modules/system/tests/modules/deprecated_module_test/src/Hook/DeprecatedModuleTestHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\deprecated_module_test\Hook;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for deprecated_module_test.
+ */
+class DeprecatedModuleTestHooks {
+
+  /**
+   * Implements hook_system_info_alter().
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(array &$info, Extension $file, $type) {
+    // Make the 'deprecated_module_contrib' look like it isn't part of core.
+    if ($type === 'module' && $info['name'] === 'Deprecated module contrib') {
+      $file->origin = 'sites/all';
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/deprecated_twig_template/deprecated_twig_template.module b/core/modules/system/tests/modules/deprecated_twig_template/deprecated_twig_template.module
deleted file mode 100644
index cdac30dab658ab27606fa50c3eba33b243c14376..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/deprecated_twig_template/deprecated_twig_template.module
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * This is a test module for deprecated twig templates.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_theme().
- */
-function deprecated_twig_template_theme(): array {
-  return [
-    'deprecated_template' => [
-      'variables' => [
-        'message' => NULL,
-      ],
-      'deprecated' => 'The "deprecated-template.html.twig" template is deprecated in drupal:X.0.0 and is removed from drupal:Y.0.0. Use another template instead. See https://www.example.com',
-    ],
-  ];
-}
diff --git a/core/modules/system/tests/modules/deprecated_twig_template/src/Hook/DeprecatedTwigTemplateHooks.php b/core/modules/system/tests/modules/deprecated_twig_template/src/Hook/DeprecatedTwigTemplateHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..73a4106209c98baed68197195fa1dd5c82e8df6b
--- /dev/null
+++ b/core/modules/system/tests/modules/deprecated_twig_template/src/Hook/DeprecatedTwigTemplateHooks.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\deprecated_twig_template\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for deprecated_twig_template.
+ */
+class DeprecatedTwigTemplateHooks {
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'deprecated_template' => [
+        'variables' => [
+          'message' => NULL,
+        ],
+        'deprecated' => 'The "deprecated-template.html.twig" template is deprecated in drupal:X.0.0 and is removed from drupal:Y.0.0. Use another template instead. See https://www.example.com',
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/system/tests/modules/element_info_test/element_info_test.module b/core/modules/system/tests/modules/element_info_test/element_info_test.module
deleted file mode 100644
index 20e5c171cafee8b3a8d6aa8e33c40cd0d751709b..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/element_info_test/element_info_test.module
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-/**
- * @file
- * Element info test.
- */
-
-declare(strict_types=1);
-
-use Drupal\element_info_test\ElementInfoTestNumberBuilder;
-
-/**
- * Implements hook_element_info_alter().
- */
-function element_info_test_element_info_alter(array &$info) {
-  $info['number'] += ['#pre_render' => []];
-  /* @see \Drupal\KernelTests\Core\Render\Element\WeightTest::testProcessWeightSelectMax() */
-  $info['number']['#pre_render'][] = [ElementInfoTestNumberBuilder::class, 'preRender'];
-}
-
-/**
- * Implements hook_element_plugin_alter().
- */
-function element_info_test_element_plugin_alter(array &$definitions) {
-  if (\Drupal::state()->get('hook_element_plugin_alter:remove_weight', FALSE)) {
-    unset($definitions['weight']);
-  }
-}
diff --git a/core/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php b/core/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c7ff010deacc8ccffe83ad8f912fee91c91f66a2
--- /dev/null
+++ b/core/modules/system/tests/modules/element_info_test/src/Hook/ElementInfoTestHooks.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\element_info_test\Hook;
+
+use Drupal\element_info_test\ElementInfoTestNumberBuilder;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for element_info_test.
+ */
+class ElementInfoTestHooks {
+
+  /**
+   * Implements hook_element_info_alter().
+   */
+  #[Hook('element_info_alter')]
+  public function elementInfoAlter(array &$info) {
+    $info['number'] += ['#pre_render' => []];
+    /* @see \Drupal\KernelTests\Core\Render\Element\WeightTest::testProcessWeightSelectMax() */
+    $info['number']['#pre_render'][] = [ElementInfoTestNumberBuilder::class, 'preRender'];
+  }
+
+  /**
+   * Implements hook_element_plugin_alter().
+   */
+  #[Hook('element_plugin_alter')]
+  public function elementPluginAlter(array &$definitions) {
+    if (\Drupal::state()->get('hook_element_plugin_alter:remove_weight', FALSE)) {
+      unset($definitions['weight']);
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module b/core/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module
deleted file mode 100644
index c7b4c81b01f8ec8704736e9166054f4f7a849927..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/entity_crud_hook_test/entity_crud_hook_test.module
+++ /dev/null
@@ -1,409 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module for the Entity CRUD API.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityInterface;
-
-/**
- * Implements hook_entity_create().
- */
-function entity_crud_hook_test_entity_create(EntityInterface $entity) {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called for type ' . $entity->getEntityTypeId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_create() for block entities.
- */
-function entity_crud_hook_test_block_create() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_create() for comment entities.
- */
-function entity_crud_hook_test_comment_create() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_create() for file entities.
- */
-function entity_crud_hook_test_file_create() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_create() for node entities.
- */
-function entity_crud_hook_test_node_create() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_create() for taxonomy_term entities.
- */
-function entity_crud_hook_test_taxonomy_term_create() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_create() for taxonomy_vocabulary entities.
- */
-function entity_crud_hook_test_taxonomy_vocabulary_create() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_create() for user entities.
- */
-function entity_crud_hook_test_user_create() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_entity_presave().
- */
-function entity_crud_hook_test_entity_presave(EntityInterface $entity) {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called for type ' . $entity->getEntityTypeId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for block entities.
- */
-function entity_crud_hook_test_block_presave() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for comment entities.
- */
-function entity_crud_hook_test_comment_presave() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for file entities.
- */
-function entity_crud_hook_test_file_presave() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for node entities.
- */
-function entity_crud_hook_test_node_presave() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for taxonomy_term entities.
- */
-function entity_crud_hook_test_taxonomy_term_presave() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for taxonomy_vocabulary entities.
- */
-function entity_crud_hook_test_taxonomy_vocabulary_presave() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for user entities.
- */
-function entity_crud_hook_test_user_presave() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_entity_insert().
- */
-function entity_crud_hook_test_entity_insert(EntityInterface $entity) {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called for type ' . $entity->getEntityTypeId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for block entities.
- */
-function entity_crud_hook_test_block_insert() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for comment entities.
- */
-function entity_crud_hook_test_comment_insert() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for file entities.
- */
-function entity_crud_hook_test_file_insert() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for node entities.
- */
-function entity_crud_hook_test_node_insert() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for taxonomy_term entities.
- */
-function entity_crud_hook_test_taxonomy_term_insert() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for taxonomy_vocabulary entities.
- */
-function entity_crud_hook_test_taxonomy_vocabulary_insert() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for user entities.
- */
-function entity_crud_hook_test_user_insert() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_entity_preload().
- */
-function entity_crud_hook_test_entity_preload(array $entities, $type) {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called for type ' . $type);
-}
-
-/**
- * Implements hook_entity_load().
- */
-function entity_crud_hook_test_entity_load(array $entities, $type) {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called for type ' . $type);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_load() for block entities.
- */
-function entity_crud_hook_test_block_load() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_load() for comment entities.
- */
-function entity_crud_hook_test_comment_load() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_load() for file entities.
- */
-function entity_crud_hook_test_file_load() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_load() for node entities.
- */
-function entity_crud_hook_test_node_load() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_load() for taxonomy_term entities.
- */
-function entity_crud_hook_test_taxonomy_term_load() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_load() for taxonomy_vocabulary entities.
- */
-function entity_crud_hook_test_taxonomy_vocabulary_load() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_load() for user entities.
- */
-function entity_crud_hook_test_user_load() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_entity_update().
- */
-function entity_crud_hook_test_entity_update(EntityInterface $entity) {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called for type ' . $entity->getEntityTypeId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for block entities.
- */
-function entity_crud_hook_test_block_update() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for comment entities.
- */
-function entity_crud_hook_test_comment_update() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for file entities.
- */
-function entity_crud_hook_test_file_update() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for node entities.
- */
-function entity_crud_hook_test_node_update() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for taxonomy_term entities.
- */
-function entity_crud_hook_test_taxonomy_term_update() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for taxonomy_vocabulary entities.
- */
-function entity_crud_hook_test_taxonomy_vocabulary_update() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for user entities.
- */
-function entity_crud_hook_test_user_update() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_entity_predelete().
- */
-function entity_crud_hook_test_entity_predelete(EntityInterface $entity) {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called for type ' . $entity->getEntityTypeId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for block entities.
- */
-function entity_crud_hook_test_block_predelete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for comment entities.
- */
-function entity_crud_hook_test_comment_predelete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for file entities.
- */
-function entity_crud_hook_test_file_predelete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for node entities.
- */
-function entity_crud_hook_test_node_predelete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for taxonomy_term entities.
- */
-function entity_crud_hook_test_taxonomy_term_predelete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for taxonomy_vocabulary entities.
- */
-function entity_crud_hook_test_taxonomy_vocabulary_predelete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for user entities.
- */
-function entity_crud_hook_test_user_predelete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_entity_delete().
- */
-function entity_crud_hook_test_entity_delete(EntityInterface $entity) {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called for type ' . $entity->getEntityTypeId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for block entities.
- */
-function entity_crud_hook_test_block_delete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for comment entities.
- */
-function entity_crud_hook_test_comment_delete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for file entities.
- */
-function entity_crud_hook_test_file_delete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for node entities.
- */
-function entity_crud_hook_test_node_delete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for taxonomy_term entities.
- */
-function entity_crud_hook_test_taxonomy_term_delete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for taxonomy_vocabulary entities.
- */
-function entity_crud_hook_test_taxonomy_vocabulary_delete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for user entities.
- */
-function entity_crud_hook_test_user_delete() {
-  $GLOBALS['entity_crud_hook_test'][] = (__FUNCTION__ . ' called');
-}
diff --git a/core/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php b/core/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..df236b8060a344899e145eccfcc87c8531bdd590
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_crud_hook_test/src/Hook/EntityCrudHookTestHooks.php
@@ -0,0 +1,471 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\entity_crud_hook_test\Hook;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for entity_crud_hook_test.
+ */
+class EntityCrudHookTestHooks {
+
+  /**
+   * Implements hook_entity_create().
+   */
+  #[Hook('entity_create')]
+  public function entityCreate(EntityInterface $entity) {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_entity_create' . ' called for type ' . $entity->getEntityTypeId();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_create() for block entities.
+   */
+  #[Hook('block_create')]
+  public function blockCreate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_block_create' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_create() for comment entities.
+   */
+  #[Hook('comment_create')]
+  public function commentCreate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_comment_create' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_create() for file entities.
+   */
+  #[Hook('file_create')]
+  public function fileCreate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_file_create' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_create() for node entities.
+   */
+  #[Hook('node_create')]
+  public function nodeCreate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_node_create' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_create() for taxonomy_term entities.
+   */
+  #[Hook('taxonomy_term_create')]
+  public function taxonomyTermCreate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_term_create' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_create() for taxonomy_vocabulary entities.
+   */
+  #[Hook('taxonomy_vocabulary_create')]
+  public function taxonomyVocabularyCreate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_vocabulary_create' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_create() for user entities.
+   */
+  #[Hook('user_create')]
+  public function userCreate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_user_create' . ' called';
+  }
+
+  /**
+   * Implements hook_entity_presave().
+   */
+  #[Hook('entity_presave')]
+  public function entityPresave(EntityInterface $entity) {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_entity_presave' . ' called for type ' . $entity->getEntityTypeId();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for block entities.
+   */
+  #[Hook('block_presave')]
+  public function blockPresave() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_block_presave' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for comment entities.
+   */
+  #[Hook('comment_presave')]
+  public function commentPresave() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_comment_presave' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for file entities.
+   */
+  #[Hook('file_presave')]
+  public function filePresave() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_file_presave' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for node entities.
+   */
+  #[Hook('node_presave')]
+  public function nodePresave() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_node_presave' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for taxonomy_term entities.
+   */
+  #[Hook('taxonomy_term_presave')]
+  public function taxonomyTermPresave() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_term_presave' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for taxonomy_vocabulary entities.
+   */
+  #[Hook('taxonomy_vocabulary_presave')]
+  public function taxonomyVocabularyPresave() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_vocabulary_presave' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for user entities.
+   */
+  #[Hook('user_presave')]
+  public function userPresave() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_user_presave' . ' called';
+  }
+
+  /**
+   * Implements hook_entity_insert().
+   */
+  #[Hook('entity_insert')]
+  public function entityInsert(EntityInterface $entity) {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_entity_insert' . ' called for type ' . $entity->getEntityTypeId();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for block entities.
+   */
+  #[Hook('block_insert')]
+  public function blockInsert() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_block_insert' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for comment entities.
+   */
+  #[Hook('comment_insert')]
+  public function commentInsert() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_comment_insert' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for file entities.
+   */
+  #[Hook('file_insert')]
+  public function fileInsert() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_file_insert' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for node entities.
+   */
+  #[Hook('node_insert')]
+  public function nodeInsert() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_node_insert' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for taxonomy_term entities.
+   */
+  #[Hook('taxonomy_term_insert')]
+  public function taxonomyTermInsert() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_term_insert' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for taxonomy_vocabulary entities.
+   */
+  #[Hook('taxonomy_vocabulary_insert')]
+  public function taxonomyVocabularyInsert() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_vocabulary_insert' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for user entities.
+   */
+  #[Hook('user_insert')]
+  public function userInsert() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_user_insert' . ' called';
+  }
+
+  /**
+   * Implements hook_entity_preload().
+   */
+  #[Hook('entity_preload')]
+  public function entityPreload(array $entities, $type) {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_entity_preload' . ' called for type ' . $type;
+  }
+
+  /**
+   * Implements hook_entity_load().
+   */
+  #[Hook('entity_load')]
+  public function entityLoad(array $entities, $type) {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_entity_load' . ' called for type ' . $type;
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_load() for block entities.
+   */
+  #[Hook('block_load')]
+  public function blockLoad() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_block_load' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_load() for comment entities.
+   */
+  #[Hook('comment_load')]
+  public function commentLoad() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_comment_load' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_load() for file entities.
+   */
+  #[Hook('file_load')]
+  public function fileLoad() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_file_load' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_load() for node entities.
+   */
+  #[Hook('node_load')]
+  public function nodeLoad() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_node_load' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_load() for taxonomy_term entities.
+   */
+  #[Hook('taxonomy_term_load')]
+  public function taxonomyTermLoad() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_term_load' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_load() for taxonomy_vocabulary entities.
+   */
+  #[Hook('taxonomy_vocabulary_load')]
+  public function taxonomyVocabularyLoad() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_vocabulary_load' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_load() for user entities.
+   */
+  #[Hook('user_load')]
+  public function userLoad() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_user_load' . ' called';
+  }
+
+  /**
+   * Implements hook_entity_update().
+   */
+  #[Hook('entity_update')]
+  public function entityUpdate(EntityInterface $entity) {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_entity_update' . ' called for type ' . $entity->getEntityTypeId();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for block entities.
+   */
+  #[Hook('block_update')]
+  public function blockUpdate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_block_update' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for comment entities.
+   */
+  #[Hook('comment_update')]
+  public function commentUpdate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_comment_update' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for file entities.
+   */
+  #[Hook('file_update')]
+  public function fileUpdate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_file_update' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for node entities.
+   */
+  #[Hook('node_update')]
+  public function nodeUpdate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_node_update' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for taxonomy_term entities.
+   */
+  #[Hook('taxonomy_term_update')]
+  public function taxonomyTermUpdate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_term_update' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for taxonomy_vocabulary entities.
+   */
+  #[Hook('taxonomy_vocabulary_update')]
+  public function taxonomyVocabularyUpdate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_vocabulary_update' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for user entities.
+   */
+  #[Hook('user_update')]
+  public function userUpdate() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_user_update' . ' called';
+  }
+
+  /**
+   * Implements hook_entity_predelete().
+   */
+  #[Hook('entity_predelete')]
+  public function entityPredelete(EntityInterface $entity) {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_entity_predelete' . ' called for type ' . $entity->getEntityTypeId();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for block entities.
+   */
+  #[Hook('block_predelete')]
+  public function blockPredelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_block_predelete' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for comment entities.
+   */
+  #[Hook('comment_predelete')]
+  public function commentPredelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_comment_predelete' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for file entities.
+   */
+  #[Hook('file_predelete')]
+  public function filePredelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_file_predelete' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for node entities.
+   */
+  #[Hook('node_predelete')]
+  public function nodePredelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_node_predelete' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for taxonomy_term entities.
+   */
+  #[Hook('taxonomy_term_predelete')]
+  public function taxonomyTermPredelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_term_predelete' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for taxonomy_vocabulary entities.
+   */
+  #[Hook('taxonomy_vocabulary_predelete')]
+  public function taxonomyVocabularyPredelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_vocabulary_predelete' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for user entities.
+   */
+  #[Hook('user_predelete')]
+  public function userPredelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_user_predelete' . ' called';
+  }
+
+  /**
+   * Implements hook_entity_delete().
+   */
+  #[Hook('entity_delete')]
+  public function entityDelete(EntityInterface $entity) {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_entity_delete' . ' called for type ' . $entity->getEntityTypeId();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for block entities.
+   */
+  #[Hook('block_delete')]
+  public function blockDelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_block_delete' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for comment entities.
+   */
+  #[Hook('comment_delete')]
+  public function commentDelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_comment_delete' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for file entities.
+   */
+  #[Hook('file_delete')]
+  public function fileDelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_file_delete' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for node entities.
+   */
+  #[Hook('node_delete')]
+  public function nodeDelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_node_delete' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for taxonomy_term entities.
+   */
+  #[Hook('taxonomy_term_delete')]
+  public function taxonomyTermDelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_term_delete' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for taxonomy_vocabulary entities.
+   */
+  #[Hook('taxonomy_vocabulary_delete')]
+  public function taxonomyVocabularyDelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_taxonomy_vocabulary_delete' . ' called';
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for user entities.
+   */
+  #[Hook('user_delete')]
+  public function userDelete() {
+    $GLOBALS['entity_crud_hook_test'][] = 'entity_crud_hook_test_user_delete' . ' called';
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_reference_test/entity_reference_test.module b/core/modules/system/tests/modules/entity_reference_test/entity_reference_test.module
deleted file mode 100644
index 1ad183dd0078b214e6e02b9e13a6406cc4f922fc..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/entity_reference_test/entity_reference_test.module
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the Entity Reference tests.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Field\BaseFieldDefinition;
-
-/**
- * Implements hook_entity_base_field_info().
- */
-function entity_reference_test_entity_base_field_info(EntityTypeInterface $entity_type) {
-  $fields = [];
-
-  if ($entity_type->id() === 'entity_test') {
-    $fields['user_role'] = BaseFieldDefinition::create('entity_reference')
-      ->setLabel(t('User role'))
-      ->setDescription(t('The role of the associated user.'))
-      ->setSetting('target_type', 'user_role')
-      ->setSetting('handler', 'default');
-  }
-
-  return $fields;
-}
-
-/**
- * Implements hook_entity_base_field_info_alter().
- */
-function entity_reference_test_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
-  if ($entity_type->id() === 'entity_test') {
-    // Allow user_id field to use configurable widget.
-    $fields['user_id']
-      ->setSetting('handler', 'default')
-      ->setDisplayOptions('form', [
-        'type' => 'entity_reference_autocomplete',
-        'weight' => 0,
-      ])
-      ->setDisplayConfigurable('form', TRUE);
-  }
-}
diff --git a/core/modules/system/tests/modules/entity_reference_test/src/Hook/EntityReferenceTestHooks.php b/core/modules/system/tests/modules/entity_reference_test/src/Hook/EntityReferenceTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..be7ee5b4c1490a523ca779e3a5cb480ce78bf17a
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_reference_test/src/Hook/EntityReferenceTestHooks.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\entity_reference_test\Hook;
+
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for entity_reference_test.
+ */
+class EntityReferenceTestHooks {
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    $fields = [];
+    if ($entity_type->id() === 'entity_test') {
+      $fields['user_role'] = BaseFieldDefinition::create('entity_reference')->setLabel(t('User role'))->setDescription(t('The role of the associated user.'))->setSetting('target_type', 'user_role')->setSetting('handler', 'default');
+    }
+    return $fields;
+  }
+
+  /**
+   * Implements hook_entity_base_field_info_alter().
+   */
+  #[Hook('entity_base_field_info_alter')]
+  public function entityBaseFieldInfoAlter(&$fields, EntityTypeInterface $entity_type) {
+    if ($entity_type->id() === 'entity_test') {
+      // Allow user_id field to use configurable widget.
+      $fields['user_id']->setSetting('handler', 'default')->setDisplayOptions('form', ['type' => 'entity_reference_autocomplete', 'weight' => 0])->setDisplayConfigurable('form', TRUE);
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module
deleted file mode 100644
index 3ab28e368c40fc35bdda0ed17dcd1c3b4033728b..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module
+++ /dev/null
@@ -1,104 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module for the entity API providing a bundle field.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\ContentEntityTypeInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\Core\Field\FieldDefinition;
-use Drupal\entity_test\FieldStorageDefinition;
-use Drupal\entity_test\Entity\EntityTestMulRev;
-
-/**
- * Implements hook_entity_type_alter().
- */
-function entity_schema_test_entity_type_alter(array &$entity_types): void {
-  // Allow a test to tell us whether or not to alter the entity type.
-  if (\Drupal::state()->get('entity_schema_update')) {
-    $entity_type = $entity_types['entity_test_update'];
-    if ($entity_type instanceof ContentEntityTypeInterface) {
-      $entity_type->set('translatable', TRUE);
-      $entity_type->set('data_table', 'entity_test_update_data');
-      // Update the keys with a revision ID.
-      $keys = $entity_type->getKeys();
-      $keys['revision'] = 'revision_id';
-      $entity_type->set('entity_keys', $keys);
-
-      $entity_type->setRevisionMetadataKey('revision_log_message', 'revision_log');
-    }
-  }
-}
-
-/**
- * Implements hook_entity_base_field_info().
- */
-function entity_schema_test_entity_base_field_info(EntityTypeInterface $entity_type) {
-  if ($entity_type->id() == 'entity_test_update') {
-    $definitions['custom_base_field'] = BaseFieldDefinition::create('string')
-      ->setName('custom_base_field')
-      ->setLabel(t('A custom base field'));
-    if (\Drupal::state()->get('entity_schema_update')) {
-      $definitions += EntityTestMulRev::baseFieldDefinitions($entity_type);
-      // And add a revision log.
-      $definitions['revision_log'] = BaseFieldDefinition::create('string_long')
-        ->setLabel(t('Revision log message'))
-        ->setDescription(t('The log entry explaining the changes in this revision.'))
-        ->setRevisionable(TRUE);
-    }
-    return $definitions;
-  }
-}
-
-/**
- * Implements hook_entity_field_storage_info().
- */
-function entity_schema_test_entity_field_storage_info(EntityTypeInterface $entity_type) {
-  if ($entity_type->id() == 'entity_test_update') {
-    $definitions['custom_bundle_field'] = FieldStorageDefinition::create('string')
-      ->setName('custom_bundle_field')
-      ->setLabel(t('A custom bundle field'))
-      ->setRevisionable(TRUE)
-      ->setTargetEntityTypeId($entity_type->id());
-    return $definitions;
-  }
-}
-
-/**
- * Implements hook_entity_bundle_field_info().
- */
-function entity_schema_test_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle) {
-  if ($entity_type->id() == 'entity_test_update' && $bundle == 'custom') {
-    /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $custom_bundle_field_storage */
-    $custom_bundle_field_storage = entity_schema_test_entity_field_storage_info($entity_type)['custom_bundle_field'];
-    $definitions[$custom_bundle_field_storage->getName()] = FieldDefinition::createFromFieldStorageDefinition($custom_bundle_field_storage);
-    return $definitions;
-  }
-}
-
-/**
- * Implements hook_entity_bundle_create().
- */
-function entity_schema_test_entity_bundle_create($entity_type_id, $bundle) {
-  if ($entity_type_id == 'entity_test_update' && $bundle == 'custom') {
-    $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle);
-    // Notify the entity storage that we just created a new field.
-    \Drupal::service('field_definition.listener')->onFieldDefinitionCreate($field_definitions['custom_bundle_field']);
-  }
-}
-
-/**
- * Implements hook_entity_bundle_delete().
- */
-function entity_schema_test_entity_bundle_delete($entity_type_id, $bundle) {
-  if ($entity_type_id == 'entity_test_update' && $bundle == 'custom') {
-    $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle);
-    // Notify the entity storage that our field is gone.
-    \Drupal::service('field_definition.listener')->onFieldDefinitionDelete($field_definitions['custom_bundle_field']);
-    \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionDelete($field_definitions['custom_bundle_field']->getFieldStorageDefinition());
-  }
-}
diff --git a/core/modules/system/tests/modules/entity_schema_test/src/Hook/EntitySchemaTestHooks.php b/core/modules/system/tests/modules/entity_schema_test/src/Hook/EntitySchemaTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..620bca0a9e6f9281215199cb73c16a576f44e247
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_schema_test/src/Hook/EntitySchemaTestHooks.php
@@ -0,0 +1,105 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\entity_schema_test\Hook;
+
+use Drupal\Core\Field\FieldDefinition;
+use Drupal\entity_test\FieldStorageDefinition;
+use Drupal\entity_test\Entity\EntityTestMulRev;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for entity_schema_test.
+ */
+class EntitySchemaTestHooks {
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    // Allow a test to tell us whether or not to alter the entity type.
+    if (\Drupal::state()->get('entity_schema_update')) {
+      $entity_type = $entity_types['entity_test_update'];
+      if ($entity_type instanceof ContentEntityTypeInterface) {
+        $entity_type->set('translatable', TRUE);
+        $entity_type->set('data_table', 'entity_test_update_data');
+        // Update the keys with a revision ID.
+        $keys = $entity_type->getKeys();
+        $keys['revision'] = 'revision_id';
+        $entity_type->set('entity_keys', $keys);
+        $entity_type->setRevisionMetadataKey('revision_log_message', 'revision_log');
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    if ($entity_type->id() == 'entity_test_update') {
+      $definitions['custom_base_field'] = BaseFieldDefinition::create('string')->setName('custom_base_field')->setLabel(t('A custom base field'));
+      if (\Drupal::state()->get('entity_schema_update')) {
+        $definitions += EntityTestMulRev::baseFieldDefinitions($entity_type);
+        // And add a revision log.
+        $definitions['revision_log'] = BaseFieldDefinition::create('string_long')->setLabel(t('Revision log message'))->setDescription(t('The log entry explaining the changes in this revision.'))->setRevisionable(TRUE);
+      }
+      return $definitions;
+    }
+  }
+
+  /**
+   * Implements hook_entity_field_storage_info().
+   */
+  #[Hook('entity_field_storage_info')]
+  public function entityFieldStorageInfo(EntityTypeInterface $entity_type) {
+    if ($entity_type->id() == 'entity_test_update') {
+      $definitions['custom_bundle_field'] = FieldStorageDefinition::create('string')->setName('custom_bundle_field')->setLabel(t('A custom bundle field'))->setRevisionable(TRUE)->setTargetEntityTypeId($entity_type->id());
+      return $definitions;
+    }
+  }
+
+  /**
+   * Implements hook_entity_bundle_field_info().
+   */
+  #[Hook('entity_bundle_field_info')]
+  public function entityBundleFieldInfo(EntityTypeInterface $entity_type, $bundle) {
+    if ($entity_type->id() == 'entity_test_update' && $bundle == 'custom') {
+      /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $custom_bundle_field_storage */
+      $custom_bundle_field_storage = $this->entityFieldStorageInfo($entity_type)['custom_bundle_field'];
+      $definitions[$custom_bundle_field_storage->getName()] = FieldDefinition::createFromFieldStorageDefinition($custom_bundle_field_storage);
+      return $definitions;
+    }
+  }
+
+  /**
+   * Implements hook_entity_bundle_create().
+   */
+  #[Hook('entity_bundle_create')]
+  public function entityBundleCreate($entity_type_id, $bundle) {
+    if ($entity_type_id == 'entity_test_update' && $bundle == 'custom') {
+      $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle);
+      // Notify the entity storage that we just created a new field.
+      \Drupal::service('field_definition.listener')->onFieldDefinitionCreate($field_definitions['custom_bundle_field']);
+    }
+  }
+
+  /**
+   * Implements hook_entity_bundle_delete().
+   */
+  #[Hook('entity_bundle_delete')]
+  public function entityBundleDelete($entity_type_id, $bundle) {
+    if ($entity_type_id == 'entity_test_update' && $bundle == 'custom') {
+      $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle);
+      // Notify the entity storage that our field is gone.
+      \Drupal::service('field_definition.listener')->onFieldDefinitionDelete($field_definitions['custom_bundle_field']);
+      \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionDelete($field_definitions['custom_bundle_field']->getFieldStorageDefinition());
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index f4af3b4f99190df9a4bcd6950c6cad58e5eecb1b..387830f246432015ea2bc9aa022dcd5e128a0402 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -7,21 +7,9 @@
 
 declare(strict_types=1);
 
-use Drupal\Component\Render\FormattableMarkup;
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Database\Query\AlterableInterface;
-use Drupal\Core\Entity\ContentEntityInterface;
-use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Entity\Entity\EntityFormDisplay;
-use Drupal\Core\Url;
 
 /**
  * Filter that limits test entity list to revisable ones.
@@ -83,111 +71,6 @@ function entity_test_entity_types($filter = NULL) {
   return array_combine($types, $types);
 }
 
-/**
- * Implements hook_entity_type_alter().
- */
-function entity_test_entity_type_alter(array &$entity_types): void {
-  $state = \Drupal::state();
-
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  foreach (entity_test_entity_types() as $entity_type) {
-    // Optionally specify a translation handler for testing translations.
-    if ($state->get('entity_test.translation')) {
-      $translation = $entity_types[$entity_type]->get('translation');
-      $translation[$entity_type] = TRUE;
-      $entity_types[$entity_type]->set('translation', $translation);
-    }
-  }
-
-  // Allow entity_test_rev tests to override the entity type definition.
-  $entity_types['entity_test_rev'] = $state->get('entity_test_rev.entity_type', $entity_types['entity_test_rev']);
-  $entity_types['entity_test_revpub'] = $state->get('entity_test_revpub.entity_type', $entity_types['entity_test_revpub']);
-
-  // Enable the entity_test_new only when needed.
-  if (!$state->get('entity_test_new')) {
-    unset($entity_types['entity_test_new']);
-  }
-  else {
-    // Allow tests to override the entity type definition.
-    $entity_types['entity_test_new'] = \Drupal::state()->get('entity_test_new.entity_type', $entity_types['entity_test_new']);
-  }
-
-  $entity_test_definition = $entity_types['entity_test'];
-  $entity_test_definition->set('entity_keys', $state->get('entity_test.entity_keys', []) + $entity_test_definition->getKeys());
-
-  // Allow tests to alter the permission granularity of entity_test_mul.
-  $entity_types['entity_test_mul']->set('permission_granularity', \Drupal::state()->get('entity_test_mul.permission_granularity', 'entity_type'));
-}
-
-/**
- * Implements hook_entity_base_field_info().
- */
-function entity_test_entity_base_field_info(EntityTypeInterface $entity_type) {
-  $fields = [];
-
-  if ($entity_type->id() === 'entity_test' && \Drupal::state()->get('entity_test.internal_field')) {
-    $fields['internal_string_field'] = BaseFieldDefinition::create('string')
-      ->setLabel('Internal field')
-      ->setInternal(TRUE);
-  }
-  if ($entity_type->id() === 'entity_test_mul' && \Drupal::state()->get('entity_test.required_default_field')) {
-    $fields['required_default_field'] = BaseFieldDefinition::create('string')
-      ->setLabel('Required field with default value')
-      ->setRequired(TRUE)
-      ->setDefaultValue('this is a default value');
-  }
-  if ($entity_type->id() === 'entity_test_mul' && \Drupal::state()->get('entity_test.required_multi_default_field')) {
-    $fields['required_multi_default_field'] = BaseFieldDefinition::create('string')
-      ->setLabel('Required field with default value')
-      ->setRequired(TRUE)
-      ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
-      ->setDefaultValue([
-        ['value' => 'this is the first default field item'],
-        ['value' => 'this is the second default value'],
-        ['value' => 'you get the idea...'],
-      ]);
-  }
-  if ($entity_type->id() == 'entity_test_mulrev' && \Drupal::state()->get('entity_test.field_test_item')) {
-    $fields['field_test_item'] = BaseFieldDefinition::create('field_test')
-      ->setLabel(t('Field test'))
-      ->setDescription(t('A field test.'))
-      ->setRevisionable(TRUE)
-      ->setTranslatable(TRUE);
-  }
-  if ($entity_type->id() == 'entity_test_mulrev' && \Drupal::state()->get('entity_test.multi_column')) {
-    $fields['description'] = BaseFieldDefinition::create('shape')
-      ->setLabel(t('Some custom description'))
-      ->setTranslatable(TRUE);
-  }
-
-  return $fields;
-}
-
-/**
- * Implements hook_entity_base_field_info_alter().
- */
-function entity_test_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
-  $state = \Drupal::state();
-  if ($entity_type->id() == 'entity_test_mulrev' && ($names = $state->get('entity_test.field_definitions.translatable'))) {
-    foreach ($names as $name => $value) {
-      $fields[$name]->setTranslatable($value);
-    }
-  }
-  if ($entity_type->id() == 'node' && $state->get('entity_test.node_remove_status_field')) {
-    unset($fields['status']);
-  }
-  if ($entity_type->id() == 'entity_test' && $state->get('entity_test.remove_name_field')) {
-    unset($fields['name']);
-  }
-  // In 8001 we are assuming that a new definition with multiple cardinality has
-  // been deployed.
-  // @todo Remove this if we end up using state definitions at runtime. See
-  //   https://www.drupal.org/node/2554235.
-  if ($entity_type->id() == 'entity_test' && $state->get('entity_test.db_updates.entity_definition_updates') == 8001) {
-    $fields['user_id']->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
-  }
-}
-
 /**
  * Creates a new bundle for entity_test entities.
  *
@@ -225,128 +108,6 @@ function entity_test_delete_bundle($bundle, $entity_type = 'entity_test') {
   \Drupal::service('entity_bundle.listener')->onBundleDelete($bundle, $entity_type);
 }
 
-/**
- * Implements hook_entity_bundle_info().
- */
-function entity_test_entity_bundle_info() {
-  $bundles = [];
-  $entity_types = \Drupal::entityTypeManager()->getDefinitions();
-  foreach ($entity_types as $entity_type_id => $entity_type) {
-    if ($entity_type->getProvider() == 'entity_test' && !in_array($entity_type_id, ['entity_test_with_bundle', 'entity_test_mul_with_bundle'], TRUE)) {
-      $bundles[$entity_type_id] = \Drupal::state()->get($entity_type_id . '.bundles', [$entity_type_id => ['label' => 'Entity Test Bundle']]);
-    }
-  }
-  return $bundles;
-}
-
-/**
- * Implements hook_entity_bundle_info_alter().
- */
-function entity_test_entity_bundle_info_alter(&$bundles) {
-  $entity_info = \Drupal::entityTypeManager()->getDefinitions();
-  $state = \Drupal::state();
-  foreach ($bundles as $entity_type_id => &$all_bundle_info) {
-    if ($entity_info[$entity_type_id]->getProvider() == 'entity_test') {
-      if ($state->get('entity_test.translation') && $entity_info[$entity_type_id]->isTranslatable()) {
-        foreach ($all_bundle_info as &$bundle_info) {
-          $bundle_info['translatable'] = TRUE;
-          if ($state->get('entity_test.untranslatable_fields.default_translation_affected')) {
-            $bundle_info['untranslatable_fields.default_translation_affected'] = TRUE;
-          }
-        }
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_entity_view_mode_info_alter().
- */
-function entity_test_entity_view_mode_info_alter(&$view_modes) {
-  $entity_info = \Drupal::entityTypeManager()->getDefinitions();
-  foreach ($entity_info as $entity_type => $info) {
-    if ($entity_info[$entity_type]->getProvider() == 'entity_test' && !isset($view_modes[$entity_type])) {
-      $view_modes[$entity_type] = [
-        'full' => [
-          'label' => t('Full object'),
-          'status' => TRUE,
-          'cache' => TRUE,
-        ],
-        'teaser' => [
-          'label' => t('Teaser'),
-          'status' => TRUE,
-          'cache' => TRUE,
-        ],
-      ];
-    }
-  }
-}
-
-/**
- * Implements hook_entity_form_mode_info_alter().
- */
-function entity_test_entity_form_mode_info_alter(&$form_modes) {
-  $entity_info = \Drupal::entityTypeManager()->getDefinitions();
-  foreach ($entity_info as $entity_type => $info) {
-    if ($entity_info[$entity_type]->getProvider() == 'entity_test') {
-      $form_modes[$entity_type]['compact'] = [
-        'label' => t('Compact version'),
-        'status' => TRUE,
-      ];
-    }
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_view_mode_alter().
- */
-function entity_test_entity_test_view_mode_alter(string &$view_mode, EntityInterface $entity): void {
-  if ($view_mode == 'entity_test.vm_alter_test') {
-    $view_mode = 'entity_test.vm_alter_full';
-  }
-}
-
-/**
- * Implements hook_entity_extra_field_info().
- */
-function entity_test_entity_extra_field_info() {
-  $extra['entity_test']['bundle_with_extra_fields'] = [
-    'display' => [
-      // Note: those extra fields do not currently display anything, they are
-      // just used in \Drupal\Tests\field_ui\Kernel\EntityDisplayTest to test
-      // the behavior of entity display objects.
-      'display_extra_field' => [
-        'label' => t('Display extra field'),
-        'description' => t('An extra field on the display side.'),
-        'weight' => 5,
-        'visible' => TRUE,
-      ],
-      'display_extra_field_hidden' => [
-        'label' => t('Display extra field (hidden)'),
-        'description' => t('An extra field on the display side, hidden by default.'),
-        'visible' => FALSE,
-      ],
-    ],
-  ];
-
-  return $extra;
-}
-
-/**
- * Implements hook_form_BASE_FORM_ID_alter().
- */
-function entity_test_form_entity_test_form_alter(&$form): void {
-  switch (\Drupal::state()->get('entity_test.form.validate.test')) {
-    case 'form-level':
-      $form['#validate'][] = 'entity_test_form_entity_test_form_validate';
-      $form['#validate'][] = 'entity_test_form_entity_test_form_validate_check';
-      break;
-
-    case 'button-level':
-      $form['actions']['submit']['#validate'][] = 'entity_test_form_entity_test_form_validate';
-  }
-}
-
 /**
  * Validation handler for the entity_test entity form.
  */
@@ -363,293 +124,6 @@ function entity_test_form_entity_test_form_validate_check(array &$form, FormStat
   }
 }
 
-/**
- * Implements hook_form_BASE_FORM_ID_alter().
- */
-function entity_test_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  $langcode = $form_state->getFormObject()->getFormLangcode($form_state);
-  \Drupal::state()->set('entity_test.form_langcode', $langcode);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for 'entity_test'.
- */
-function entity_test_entity_test_insert($entity) {
-  if ($entity->name->value == 'fail_insert') {
-    throw new Exception("Test exception rollback.");
-  }
-}
-
-/**
- * Implements hook_entity_insert().
- */
-function entity_test_entity_insert(EntityInterface $entity) {
-  if ($entity->getEntityTypeId() == 'entity_test_mulrev' && $entity->label() == 'EntityLoadedRevisionTest') {
-    $entity->setNewRevision(FALSE);
-    $entity->save();
-  }
-}
-
-/**
- * Implements hook_entity_update().
- */
-function entity_test_entity_update(EntityInterface $entity) {
-  if ($entity instanceof ContentEntityInterface) {
-    \Drupal::state()->set('entity_test.loadedRevisionId', $entity->getLoadedRevisionId());
-  }
-}
-
-/**
- * Implements hook_entity_field_access().
- *
- * @see \Drupal\system\Tests\Entity\FieldAccessTest::testFieldAccess()
- */
-function entity_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
-  if ($field_definition->getName() == 'field_test_text') {
-    if ($items) {
-      if ($items->value == 'no access value') {
-        return AccessResult::forbidden()->addCacheableDependency($items->getEntity());
-      }
-      elseif ($items->value == 'custom cache tag value') {
-        return AccessResult::allowed()->addCacheableDependency($items->getEntity())->addCacheTags(['entity_test_access:field_test_text']);
-      }
-      elseif ($operation == 'edit' && $items->value == 'no edit access value') {
-        return AccessResult::forbidden()->addCacheableDependency($items->getEntity());
-      }
-    }
-  }
-  if ($field = \Drupal::state()->get('views_field_access_test-field')) {
-    if ($field_definition->getName() === $field) {
-      $result = AccessResult::allowedIfHasPermission($account, 'view test entity field');
-      // For test purposes we want to actively deny access.
-      if ($result->isNeutral()) {
-        $result = AccessResult::forbidden();
-      }
-      return $result;
-    }
-  }
-
-  // No opinion.
-  return AccessResult::neutral();
-}
-
-/**
- * Implements hook_entity_field_access_alter().
- *
- * @see \Drupal\system\Tests\Entity\FieldAccessTest::testFieldAccess()
- */
-function entity_test_entity_field_access_alter(array &$grants, array $context) {
-  if ($context['field_definition']->getName() == 'field_test_text' && $context['items']->value == 'access alter value') {
-    $grants[':default'] = AccessResult::forbidden()->inheritCacheability($grants[':default'])->addCacheableDependency($context['items']->getEntity());
-  }
-}
-
-/**
- * Implements hook_entity_form_mode_alter().
- */
-function entity_test_entity_form_mode_alter(&$form_mode, EntityInterface $entity) {
-  if ($entity->getEntityTypeId() === 'entity_test' && $entity->get('name')->value === 'compact_form_mode') {
-    $form_mode = 'compact';
-  }
-}
-
-/**
- * Implements hook_entity_form_display_alter().
- */
-function entity_test_entity_form_display_alter(EntityFormDisplay $form_display, $context) {
-  // Make the field_test_text field 42 characters for entity_test_mul.
-  if ($context['entity_type'] == 'entity_test') {
-    if ($component_options = $form_display->getComponent('field_test_text')) {
-      $component_options['settings']['size'] = 42;
-      $form_display->setComponent('field_test_text', $component_options);
-    }
-  }
-}
-
-/**
- * Implements hook_entity_presave().
- */
-function entity_test_entity_presave(EntityInterface $entity) {
-  if (isset($GLOBALS['entity_test_throw_exception'])) {
-    throw new Exception('Entity presave exception', 1);
-  }
-
-  if ($entity->getEntityType()->id() == 'entity_view_display') {
-    $entity->setThirdPartySetting('entity_test', 'foo', 'bar');
-  }
-}
-
-/**
- * Implements hook_entity_predelete().
- */
-function entity_test_entity_predelete(EntityInterface $entity) {
-  if (isset($GLOBALS['entity_test_throw_exception'])) {
-    throw new Exception('Entity predelete exception', 2);
-  }
-}
-
-/**
- * Implements hook_entity_operation_alter().
- */
-function entity_test_entity_operation_alter(array &$operations, EntityInterface $entity) {
-  $valid_entity_type_ids = [
-    'user_role',
-    'block',
-  ];
-  if (in_array($entity->getEntityTypeId(), $valid_entity_type_ids)) {
-    if (\Drupal::service('router.route_provider')->getRouteByName("entity.{$entity->getEntityTypeId()}.test_operation")) {
-      $operations['test_operation'] = [
-        'title' => new FormattableMarkup('Test Operation: @label', ['@label' => $entity->label()]),
-        'url' => Url::fromRoute("entity.{$entity->getEntityTypeId()}.test_operation", [$entity->getEntityTypeId() => $entity->id()]),
-        'weight' => 50,
-      ];
-    }
-  }
-}
-
-/**
- * Implements hook_entity_translation_create().
- */
-function entity_test_entity_translation_create(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_translation_create', $translation->language()->getId());
-}
-
-/**
- * Implements hook_entity_translation_insert().
- */
-function entity_test_entity_translation_insert(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_translation_insert', $translation->language()->getId());
-}
-
-/**
- * Implements hook_entity_translation_delete().
- */
-function entity_test_entity_translation_delete(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_translation_delete', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_create() for 'entity_test_mul'.
- */
-function entity_test_entity_test_mul_translation_create(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mul_translation_create', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_insert() for 'entity_test_mul'.
- */
-function entity_test_entity_test_mul_translation_insert(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mul_translation_insert', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_delete() for 'entity_test_mul'.
- */
-function entity_test_entity_test_mul_translation_delete(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mul_translation_delete', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_create() for 'entity_test_mul_changed'.
- */
-function entity_test_entity_test_mul_changed_translation_create(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mul_changed_translation_create', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_insert() for 'entity_test_mul_changed'.
- */
-function entity_test_entity_test_mul_changed_translation_insert(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mul_changed_translation_insert', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_delete().
- */
-function entity_test_entity_test_mul_changed_translation_delete(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mul_changed_translation_delete', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_create().
- */
-function entity_test_entity_test_mulrev_translation_create(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mulrev_translation_create', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_insert().
- */
-function entity_test_entity_test_mulrev_translation_insert(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mulrev_translation_insert', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_delete() for 'entity_test_mulrev'.
- */
-function entity_test_entity_test_mulrev_translation_delete(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mulrev_translation_delete', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_create() for 'entity_test_mulrev_changed'.
- */
-function entity_test_entity_test_mulrev_changed_translation_create(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mulrev_changed_translation_create', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_insert() for 'entity_test_mulrev'.
- */
-function entity_test_entity_test_mulrev_changed_translation_insert(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mulrev_changed_translation_insert', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_delete().
- */
-function entity_test_entity_test_mulrev_changed_translation_delete(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mulrev_changed_translation_delete', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_create() for 'entity_test_mul_langcode_key'.
- */
-function entity_test_entity_test_mul_langcode_key_translation_create(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mul_langcode_key_translation_create', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_insert() for 'entity_test_mul_langcode_key'.
- */
-function entity_test_entity_test_mul_langcode_key_translation_insert(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mul_langcode_key_translation_insert', $translation->language()->getId());
-}
-
-/**
- * Implements hook_ENTITY_TYPE_translation_delete() for 'entity_test_mul_langcode_key'.
- */
-function entity_test_entity_test_mul_langcode_key_translation_delete(EntityInterface $translation) {
-  _entity_test_record_hooks('entity_test_mul_langcode_key_translation_delete', $translation->language()->getId());
-}
-
-/**
- * Implements hook_entity_revision_create().
- */
-function entity_test_entity_revision_create(EntityInterface $new_revision, EntityInterface $entity, $keep_untranslatable_fields) {
-  _entity_test_record_hooks('entity_revision_create', ['new_revision' => $new_revision, 'entity' => $entity, 'keep_untranslatable_fields' => $keep_untranslatable_fields]);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_revision_create() for 'entity_test_mulrev'.
- */
-function entity_test_entity_test_mulrev_revision_create(EntityInterface $new_revision, EntityInterface $entity, $keep_untranslatable_fields) {
-  if ($new_revision->get('name')->value == 'revision_create_test_it') {
-    $new_revision->set('name', 'revision_create_test_it_altered');
-  }
-  _entity_test_record_hooks('entity_test_mulrev_revision_create', ['new_revision' => $new_revision, 'entity' => $entity, 'keep_untranslatable_fields' => $keep_untranslatable_fields]);
-}
-
 /**
  * Field default value callback.
  *
@@ -696,138 +170,3 @@ function _entity_test_record_hooks($hook, $data) {
   $hooks[$hook] = $data;
   $state->set($key, $hooks);
 }
-
-/**
- * Implements hook_entity_prepare_view().
- */
-function entity_test_entity_prepare_view($entity_type, array $entities, array $displays) {
-  if ($entity_type == 'entity_test') {
-    foreach ($entities as $entity) {
-      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
-
-      // Add field item attributes on field_test_text if it exists.
-      // See \Drupal\Tests\system\Functional\Entity\EntityViewControllerTest::testFieldItemAttributes().
-      if ($entity->hasField('field_test_text') && $displays[$entity->bundle()]->getComponent('field_test_text')) {
-        foreach ($entity->get('field_test_text') as $item) {
-          $item->_attributes += [
-            'data-field-item-attr' => 'foobar',
-            'property' => 'schema:text',
-          ];
-        }
-      }
-
-      // Add an item attribute on daterange fields if they exist.
-      $fields = $entity->getFieldDefinitions();
-      foreach ($fields as $field) {
-        if ($field->getType() === 'daterange') {
-          $item = $entity->get($field->getName());
-          $item->_attributes += ['data-field-item-attr' => 'foobar'];
-        }
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_entity_display_build_alter().
- */
-function entity_test_entity_display_build_alter(&$build, $context) {
-  /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
-  $entity = $context['entity'];
-  if ($entity->getEntityTypeId() == 'entity_test' && $entity->bundle() == 'display_build_alter_bundle') {
-    $build['entity_display_build_alter']['#markup'] = 'Content added in hook_entity_display_build_alter for entity id ' . $entity->id();
-  }
-}
-
-/**
- * Implements hook_entity_access().
- */
-function entity_test_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  // Only apply to the 'entity_test' entities.
-  if ($entity->getEntityType()->getProvider() != 'entity_test') {
-    return AccessResult::neutral();
-  }
-  \Drupal::state()->set('entity_test_entity_access', TRUE);
-
-  // Attempt to allow access to entities with the title forbid_access,
-  // this will be overridden by
-  // \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess().
-  if ($entity->label() == 'forbid_access') {
-    return AccessResult::allowed();
-  }
-
-  // Create specific labels to allow or deny access based on certain test
-  // conditions.
-  // @see \Drupal\KernelTests\Core\Entity\EntityAccessControlHandlerTest
-  if ($entity->label() == 'Accessible') {
-    return AccessResult::allowed();
-  }
-  if ($entity->label() == 'Inaccessible') {
-    return AccessResult::forbidden();
-  }
-
-  // Uncacheable because the access result depends on a State key-value pair and
-  // might therefore change at any time.
-  $condition = \Drupal::state()->get("entity_test_entity_access.{$operation}." . $entity->id(), FALSE);
-  return AccessResult::allowedIf($condition)->setCacheMaxAge(0);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_access() for 'entity_test'.
- */
-function entity_test_entity_test_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  \Drupal::state()->set('entity_test_entity_test_access', TRUE);
-
-  // No opinion.
-  return AccessResult::neutral();
-}
-
-/**
- * Implements hook_entity_create_access().
- */
-function entity_test_entity_create_access(AccountInterface $account, $context, $entity_bundle) {
-  \Drupal::state()->set('entity_test_entity_create_access', TRUE);
-  \Drupal::state()->set('entity_test_entity_create_access_context', $context);
-
-  if ($entity_bundle === 'forbidden_access_bundle') {
-    // We need to cover a case in which a bundle is specifically forbidden
-    // from creation (as opposed to neutral access).
-    return AccessResult::forbidden();
-  }
-
-  // No opinion.
-  return AccessResult::neutral();
-}
-
-/**
- * Implements hook_ENTITY_TYPE_create_access() for 'entity_test'.
- */
-function entity_test_entity_test_create_access(AccountInterface $account, $context, $entity_bundle) {
-  \Drupal::state()->set('entity_test_entity_test_create_access', TRUE);
-
-  // No opinion.
-  return AccessResult::neutral();
-}
-
-/**
- * Implements hook_query_entity_test_access_alter().
- */
-function entity_test_query_entity_test_access_alter(AlterableInterface $query) {
-  if (!\Drupal::state()->get('entity_test_query_access')) {
-    return;
-  }
-
-  /** @var \Drupal\Core\Database\Query\Select|\Drupal\Core\Database\Query\AlterableInterface $query */
-  if (!\Drupal::currentUser()->hasPermission('view all entity_test_query_access entities')) {
-    $query->condition('entity_test_query_access.name', 'published entity');
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_form_mode_alter().
- */
-function entity_test_entity_test_form_mode_alter(&$form_mode, EntityInterface $entity): void {
-  if ($entity->getEntityTypeId() === 'entity_test' && $entity->get('name')->value === 'test_entity_type_form_mode_alter') {
-    $form_mode = 'compact';
-  }
-}
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.views.inc b/core/modules/system/tests/modules/entity_test/entity_test.views.inc
deleted file mode 100644
index 51693229608f06e9a655e2de4ff639325f682808..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/entity_test/entity_test.views.inc
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-/**
- * @file
- * Include file for test module.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_views_data_alter().
- */
-function entity_test_views_data_alter(&$data) {
-  $data['entity_test']['name_alias'] = $data['entity_test']['name'];
-  $data['entity_test']['name_alias']['field']['real field'] = 'name';
-}
diff --git a/core/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php b/core/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..dea5aaf0a47aa8b9c1233da2ffabe5adad046516
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Hook/EntityTestHooks.php
@@ -0,0 +1,700 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\entity_test\Hook;
+
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Url;
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for entity_test.
+ */
+class EntityTestHooks {
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    $state = \Drupal::state();
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    foreach (entity_test_entity_types() as $entity_type) {
+      // Optionally specify a translation handler for testing translations.
+      if ($state->get('entity_test.translation')) {
+        $translation = $entity_types[$entity_type]->get('translation');
+        $translation[$entity_type] = TRUE;
+        $entity_types[$entity_type]->set('translation', $translation);
+      }
+    }
+    // Allow entity_test_rev tests to override the entity type definition.
+    $entity_types['entity_test_rev'] = $state->get('entity_test_rev.entity_type', $entity_types['entity_test_rev']);
+    $entity_types['entity_test_revpub'] = $state->get('entity_test_revpub.entity_type', $entity_types['entity_test_revpub']);
+    // Enable the entity_test_new only when needed.
+    if (!$state->get('entity_test_new')) {
+      unset($entity_types['entity_test_new']);
+    }
+    else {
+      // Allow tests to override the entity type definition.
+      $entity_types['entity_test_new'] = \Drupal::state()->get('entity_test_new.entity_type', $entity_types['entity_test_new']);
+    }
+    $entity_test_definition = $entity_types['entity_test'];
+    $entity_test_definition->set('entity_keys', $state->get('entity_test.entity_keys', []) + $entity_test_definition->getKeys());
+    // Allow tests to alter the permission granularity of entity_test_mul.
+    $entity_types['entity_test_mul']->set('permission_granularity', \Drupal::state()->get('entity_test_mul.permission_granularity', 'entity_type'));
+  }
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    $fields = [];
+    if ($entity_type->id() === 'entity_test' && \Drupal::state()->get('entity_test.internal_field')) {
+      $fields['internal_string_field'] = BaseFieldDefinition::create('string')->setLabel('Internal field')->setInternal(TRUE);
+    }
+    if ($entity_type->id() === 'entity_test_mul' && \Drupal::state()->get('entity_test.required_default_field')) {
+      $fields['required_default_field'] = BaseFieldDefinition::create('string')->setLabel('Required field with default value')->setRequired(TRUE)->setDefaultValue('this is a default value');
+    }
+    if ($entity_type->id() === 'entity_test_mul' && \Drupal::state()->get('entity_test.required_multi_default_field')) {
+      $fields['required_multi_default_field'] = BaseFieldDefinition::create('string')->setLabel('Required field with default value')->setRequired(TRUE)->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)->setDefaultValue([
+            [
+              'value' => 'this is the first default field item',
+            ],
+            [
+              'value' => 'this is the second default value',
+            ],
+            [
+              'value' => 'you get the idea...',
+            ],
+      ]);
+    }
+    if ($entity_type->id() == 'entity_test_mulrev' && \Drupal::state()->get('entity_test.field_test_item')) {
+      $fields['field_test_item'] = BaseFieldDefinition::create('field_test')->setLabel(t('Field test'))->setDescription(t('A field test.'))->setRevisionable(TRUE)->setTranslatable(TRUE);
+    }
+    if ($entity_type->id() == 'entity_test_mulrev' && \Drupal::state()->get('entity_test.multi_column')) {
+      $fields['description'] = BaseFieldDefinition::create('shape')->setLabel(t('Some custom description'))->setTranslatable(TRUE);
+    }
+    return $fields;
+  }
+
+  /**
+   * Implements hook_entity_base_field_info_alter().
+   */
+  #[Hook('entity_base_field_info_alter')]
+  public function entityBaseFieldInfoAlter(&$fields, EntityTypeInterface $entity_type) {
+    $state = \Drupal::state();
+    if ($entity_type->id() == 'entity_test_mulrev' && ($names = $state->get('entity_test.field_definitions.translatable'))) {
+      foreach ($names as $name => $value) {
+        $fields[$name]->setTranslatable($value);
+      }
+    }
+    if ($entity_type->id() == 'node' && $state->get('entity_test.node_remove_status_field')) {
+      unset($fields['status']);
+    }
+    if ($entity_type->id() == 'entity_test' && $state->get('entity_test.remove_name_field')) {
+      unset($fields['name']);
+    }
+    // In 8001 we are assuming that a new definition with multiple cardinality has
+    // been deployed.
+    // @todo Remove this if we end up using state definitions at runtime. See
+    //   https://www.drupal.org/node/2554235.
+    if ($entity_type->id() == 'entity_test' && $state->get('entity_test.db_updates.entity_definition_updates') == 8001) {
+      $fields['user_id']->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+    }
+  }
+
+  /**
+   * Implements hook_entity_bundle_info().
+   */
+  #[Hook('entity_bundle_info')]
+  public function entityBundleInfo() {
+    $bundles = [];
+    $entity_types = \Drupal::entityTypeManager()->getDefinitions();
+    foreach ($entity_types as $entity_type_id => $entity_type) {
+      if ($entity_type->getProvider() == 'entity_test' && !in_array($entity_type_id, ['entity_test_with_bundle', 'entity_test_mul_with_bundle'], TRUE)) {
+        $bundles[$entity_type_id] = \Drupal::state()->get($entity_type_id . '.bundles', [$entity_type_id => ['label' => 'Entity Test Bundle']]);
+      }
+    }
+    return $bundles;
+  }
+
+  /**
+   * Implements hook_entity_bundle_info_alter().
+   */
+  #[Hook('entity_bundle_info_alter')]
+  public function entityBundleInfoAlter(&$bundles) {
+    $entity_info = \Drupal::entityTypeManager()->getDefinitions();
+    $state = \Drupal::state();
+    foreach ($bundles as $entity_type_id => &$all_bundle_info) {
+      if ($entity_info[$entity_type_id]->getProvider() == 'entity_test') {
+        if ($state->get('entity_test.translation') && $entity_info[$entity_type_id]->isTranslatable()) {
+          foreach ($all_bundle_info as &$bundle_info) {
+            $bundle_info['translatable'] = TRUE;
+            if ($state->get('entity_test.untranslatable_fields.default_translation_affected')) {
+              $bundle_info['untranslatable_fields.default_translation_affected'] = TRUE;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_view_mode_info_alter().
+   */
+  #[Hook('entity_view_mode_info_alter')]
+  public function entityViewModeInfoAlter(&$view_modes) {
+    $entity_info = \Drupal::entityTypeManager()->getDefinitions();
+    foreach ($entity_info as $entity_type => $info) {
+      if ($entity_info[$entity_type]->getProvider() == 'entity_test' && !isset($view_modes[$entity_type])) {
+        $view_modes[$entity_type] = [
+          'full' => [
+            'label' => t('Full object'),
+            'status' => TRUE,
+            'cache' => TRUE,
+          ],
+          'teaser' => [
+            'label' => t('Teaser'),
+            'status' => TRUE,
+            'cache' => TRUE,
+          ],
+        ];
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_form_mode_info_alter().
+   */
+  #[Hook('entity_form_mode_info_alter')]
+  public function entityFormModeInfoAlter(&$form_modes) {
+    $entity_info = \Drupal::entityTypeManager()->getDefinitions();
+    foreach ($entity_info as $entity_type => $info) {
+      if ($entity_info[$entity_type]->getProvider() == 'entity_test') {
+        $form_modes[$entity_type]['compact'] = ['label' => t('Compact version'), 'status' => TRUE];
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_view_mode_alter().
+   */
+  #[Hook('entity_test_view_mode_alter')]
+  public function entityTestViewModeAlter(string &$view_mode, EntityInterface $entity) : void {
+    if ($view_mode == 'entity_test.vm_alter_test') {
+      $view_mode = 'entity_test.vm_alter_full';
+    }
+  }
+
+  /**
+   * Implements hook_entity_extra_field_info().
+   */
+  #[Hook('entity_extra_field_info')]
+  public function entityExtraFieldInfo() {
+    $extra['entity_test']['bundle_with_extra_fields'] = [
+      'display' => [
+              // Note: those extra fields do not currently display anything, they are
+              // just used in \Drupal\Tests\field_ui\Kernel\EntityDisplayTest to test
+              // the behavior of entity display objects.
+        'display_extra_field' => [
+          'label' => t('Display extra field'),
+          'description' => t('An extra field on the display side.'),
+          'weight' => 5,
+          'visible' => TRUE,
+        ],
+        'display_extra_field_hidden' => [
+          'label' => t('Display extra field (hidden)'),
+          'description' => t('An extra field on the display side, hidden by default.'),
+          'visible' => FALSE,
+        ],
+      ],
+    ];
+    return $extra;
+  }
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter().
+   */
+  #[Hook('form_entity_test_form_alter')]
+  public function formEntityTestFormAlter(&$form) : void {
+    switch (\Drupal::state()->get('entity_test.form.validate.test')) {
+      case 'form-level':
+        $form['#validate'][] = 'entity_test_form_entity_test_form_validate';
+        $form['#validate'][] = 'entity_test_form_entity_test_form_validate_check';
+        break;
+
+      case 'button-level':
+        $form['actions']['submit']['#validate'][] = 'entity_test_form_entity_test_form_validate';
+    }
+  }
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter().
+   */
+  #[Hook('form_node_form_alter')]
+  public function formNodeFormAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    $langcode = $form_state->getFormObject()->getFormLangcode($form_state);
+    \Drupal::state()->set('entity_test.form_langcode', $langcode);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for 'entity_test'.
+   */
+  #[Hook('entity_test_insert')]
+  public function entityTestInsert($entity) {
+    if ($entity->name->value == 'fail_insert') {
+      throw new \Exception("Test exception rollback.");
+    }
+  }
+
+  /**
+   * Implements hook_entity_insert().
+   */
+  #[Hook('entity_insert')]
+  public function entityInsert(EntityInterface $entity) {
+    if ($entity->getEntityTypeId() == 'entity_test_mulrev' && $entity->label() == 'EntityLoadedRevisionTest') {
+      $entity->setNewRevision(FALSE);
+      $entity->save();
+    }
+  }
+
+  /**
+   * Implements hook_entity_update().
+   */
+  #[Hook('entity_update')]
+  public function entityUpdate(EntityInterface $entity) {
+    if ($entity instanceof ContentEntityInterface) {
+      \Drupal::state()->set('entity_test.loadedRevisionId', $entity->getLoadedRevisionId());
+    }
+  }
+
+  /**
+   * Implements hook_entity_field_access().
+   *
+   * @see \Drupal\system\Tests\Entity\FieldAccessTest::testFieldAccess()
+   */
+  #[Hook('entity_field_access')]
+  public function entityFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
+    if ($field_definition->getName() == 'field_test_text') {
+      if ($items) {
+        if ($items->value == 'no access value') {
+          return AccessResult::forbidden()->addCacheableDependency($items->getEntity());
+        }
+        elseif ($items->value == 'custom cache tag value') {
+          return AccessResult::allowed()->addCacheableDependency($items->getEntity())->addCacheTags(['entity_test_access:field_test_text']);
+        }
+        elseif ($operation == 'edit' && $items->value == 'no edit access value') {
+          return AccessResult::forbidden()->addCacheableDependency($items->getEntity());
+        }
+      }
+    }
+    if ($field = \Drupal::state()->get('views_field_access_test-field')) {
+      if ($field_definition->getName() === $field) {
+        $result = AccessResult::allowedIfHasPermission($account, 'view test entity field');
+        // For test purposes we want to actively deny access.
+        if ($result->isNeutral()) {
+          $result = AccessResult::forbidden();
+        }
+        return $result;
+      }
+    }
+    // No opinion.
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Implements hook_entity_field_access_alter().
+   *
+   * @see \Drupal\system\Tests\Entity\FieldAccessTest::testFieldAccess()
+   */
+  #[Hook('entity_field_access_alter')]
+  public function entityFieldAccessAlter(array &$grants, array $context) {
+    if ($context['field_definition']->getName() == 'field_test_text' && $context['items']->value == 'access alter value') {
+      $grants[':default'] = AccessResult::forbidden()->inheritCacheability($grants[':default'])->addCacheableDependency($context['items']->getEntity());
+    }
+  }
+
+  /**
+   * Implements hook_entity_form_mode_alter().
+   */
+  #[Hook('entity_form_mode_alter')]
+  public function entityFormModeAlter(&$form_mode, EntityInterface $entity) {
+    if ($entity->getEntityTypeId() === 'entity_test' && $entity->get('name')->value === 'compact_form_mode') {
+      $form_mode = 'compact';
+    }
+  }
+
+  /**
+   * Implements hook_entity_form_display_alter().
+   */
+  #[Hook('entity_form_display_alter')]
+  public function entityFormDisplayAlter(EntityFormDisplay $form_display, $context) {
+    // Make the field_test_text field 42 characters for entity_test_mul.
+    if ($context['entity_type'] == 'entity_test') {
+      if ($component_options = $form_display->getComponent('field_test_text')) {
+        $component_options['settings']['size'] = 42;
+        $form_display->setComponent('field_test_text', $component_options);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_presave().
+   */
+  #[Hook('entity_presave')]
+  public function entityPresave(EntityInterface $entity) {
+    if (isset($GLOBALS['entity_test_throw_exception'])) {
+      throw new \Exception('Entity presave exception', 1);
+    }
+    if ($entity->getEntityType()->id() == 'entity_view_display') {
+      $entity->setThirdPartySetting('entity_test', 'foo', 'bar');
+    }
+  }
+
+  /**
+   * Implements hook_entity_predelete().
+   */
+  #[Hook('entity_predelete')]
+  public function entityPredelete(EntityInterface $entity) {
+    if (isset($GLOBALS['entity_test_throw_exception'])) {
+      throw new \Exception('Entity predelete exception', 2);
+    }
+  }
+
+  /**
+   * Implements hook_entity_operation_alter().
+   */
+  #[Hook('entity_operation_alter')]
+  public function entityOperationAlter(array &$operations, EntityInterface $entity) {
+    $valid_entity_type_ids = ['user_role', 'block'];
+    if (in_array($entity->getEntityTypeId(), $valid_entity_type_ids)) {
+      if (\Drupal::service('router.route_provider')->getRouteByName("entity.{$entity->getEntityTypeId()}.test_operation")) {
+        $operations['test_operation'] = [
+          'title' => new FormattableMarkup('Test Operation: @label', [
+            '@label' => $entity->label(),
+          ]),
+          'url' => Url::fromRoute("entity.{$entity->getEntityTypeId()}.test_operation", [
+            $entity->getEntityTypeId() => $entity->id(),
+          ]),
+          'weight' => 50,
+        ];
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_translation_create().
+   */
+  #[Hook('entity_translation_create')]
+  public function entityTranslationCreate(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_translation_create', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_entity_translation_insert().
+   */
+  #[Hook('entity_translation_insert')]
+  public function entityTranslationInsert(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_translation_insert', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_entity_translation_delete().
+   */
+  #[Hook('entity_translation_delete')]
+  public function entityTranslationDelete(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_translation_delete', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_create() for 'entity_test_mul'.
+   */
+  #[Hook('entity_test_mul_translation_create')]
+  public function entityTestMulTranslationCreate(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mul_translation_create', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_insert() for 'entity_test_mul'.
+   */
+  #[Hook('entity_test_mul_translation_insert')]
+  public function entityTestMulTranslationInsert(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mul_translation_insert', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_delete() for 'entity_test_mul'.
+   */
+  #[Hook('entity_test_mul_translation_delete')]
+  public function entityTestMulTranslationDelete(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mul_translation_delete', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_create() for 'entity_test_mul_changed'.
+   */
+  #[Hook('entity_test_mul_changed_translation_create')]
+  public function entityTestMulChangedTranslationCreate(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mul_changed_translation_create', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_insert() for 'entity_test_mul_changed'.
+   */
+  #[Hook('entity_test_mul_changed_translation_insert')]
+  public function entityTestMulChangedTranslationInsert(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mul_changed_translation_insert', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_delete().
+   */
+  #[Hook('entity_test_mul_changed_translation_delete')]
+  public function entityTestMulChangedTranslationDelete(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mul_changed_translation_delete', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_create().
+   */
+  #[Hook('entity_test_mulrev_translation_create')]
+  public function entityTestMulrevTranslationCreate(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mulrev_translation_create', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_insert().
+   */
+  #[Hook('entity_test_mulrev_translation_insert')]
+  public function entityTestMulrevTranslationInsert(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mulrev_translation_insert', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_delete() for 'entity_test_mulrev'.
+   */
+  #[Hook('entity_test_mulrev_translation_delete')]
+  public function entityTestMulrevTranslationDelete(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mulrev_translation_delete', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_create() for 'entity_test_mulrev_changed'.
+   */
+  #[Hook('entity_test_mulrev_changed_translation_create')]
+  public function entityTestMulrevChangedTranslationCreate(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mulrev_changed_translation_create', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_insert() for 'entity_test_mulrev'.
+   */
+  #[Hook('entity_test_mulrev_changed_translation_insert')]
+  public function entityTestMulrevChangedTranslationInsert(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mulrev_changed_translation_insert', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_delete().
+   */
+  #[Hook('entity_test_mulrev_changed_translation_delete')]
+  public function entityTestMulrevChangedTranslationDelete(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mulrev_changed_translation_delete', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_create() for 'entity_test_mul_langcode_key'.
+   */
+  #[Hook('entity_test_mul_langcode_key_translation_create')]
+  public function entityTestMulLangcodeKeyTranslationCreate(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mul_langcode_key_translation_create', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_insert() for 'entity_test_mul_langcode_key'.
+   */
+  #[Hook('entity_test_mul_langcode_key_translation_insert')]
+  public function entityTestMulLangcodeKeyTranslationInsert(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mul_langcode_key_translation_insert', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_translation_delete() for 'entity_test_mul_langcode_key'.
+   */
+  #[Hook('entity_test_mul_langcode_key_translation_delete')]
+  public function entityTestMulLangcodeKeyTranslationDelete(EntityInterface $translation) {
+    _entity_test_record_hooks('entity_test_mul_langcode_key_translation_delete', $translation->language()->getId());
+  }
+
+  /**
+   * Implements hook_entity_revision_create().
+   */
+  #[Hook('entity_revision_create')]
+  public function entityRevisionCreate(EntityInterface $new_revision, EntityInterface $entity, $keep_untranslatable_fields) {
+    _entity_test_record_hooks('entity_revision_create', [
+      'new_revision' => $new_revision,
+      'entity' => $entity,
+      'keep_untranslatable_fields' => $keep_untranslatable_fields,
+    ]);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_revision_create() for 'entity_test_mulrev'.
+   */
+  #[Hook('entity_test_mulrev_revision_create')]
+  public function entityTestMulrevRevisionCreate(EntityInterface $new_revision, EntityInterface $entity, $keep_untranslatable_fields) {
+    if ($new_revision->get('name')->value == 'revision_create_test_it') {
+      $new_revision->set('name', 'revision_create_test_it_altered');
+    }
+    _entity_test_record_hooks('entity_test_mulrev_revision_create', [
+      'new_revision' => $new_revision,
+      'entity' => $entity,
+      'keep_untranslatable_fields' => $keep_untranslatable_fields,
+    ]);
+  }
+
+  /**
+   * Implements hook_entity_prepare_view().
+   */
+  #[Hook('entity_prepare_view')]
+  public function entityPrepareView($entity_type, array $entities, array $displays) {
+    if ($entity_type == 'entity_test') {
+      foreach ($entities as $entity) {
+        /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+        // Add field item attributes on field_test_text if it exists.
+        // See \Drupal\Tests\system\Functional\Entity\EntityViewControllerTest::testFieldItemAttributes().
+        if ($entity->hasField('field_test_text') && $displays[$entity->bundle()]->getComponent('field_test_text')) {
+          foreach ($entity->get('field_test_text') as $item) {
+            $item->_attributes += ['data-field-item-attr' => 'foobar', 'property' => 'schema:text'];
+          }
+        }
+        // Add an item attribute on daterange fields if they exist.
+        $fields = $entity->getFieldDefinitions();
+        foreach ($fields as $field) {
+          if ($field->getType() === 'daterange') {
+            $item = $entity->get($field->getName());
+            $item->_attributes += ['data-field-item-attr' => 'foobar'];
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_display_build_alter().
+   */
+  #[Hook('entity_display_build_alter')]
+  public function entityDisplayBuildAlter(&$build, $context) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $context['entity'];
+    if ($entity->getEntityTypeId() == 'entity_test' && $entity->bundle() == 'display_build_alter_bundle') {
+      $build['entity_display_build_alter']['#markup'] = 'Content added in hook_entity_display_build_alter for entity id ' . $entity->id();
+    }
+  }
+
+  /**
+   * Implements hook_entity_access().
+   */
+  #[Hook('entity_access')]
+  public function entityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    // Only apply to the 'entity_test' entities.
+    if ($entity->getEntityType()->getProvider() != 'entity_test') {
+      return AccessResult::neutral();
+    }
+    \Drupal::state()->set('entity_test_entity_access', TRUE);
+    // Attempt to allow access to entities with the title forbid_access,
+    // this will be overridden by
+    // \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess().
+    if ($entity->label() == 'forbid_access') {
+      return AccessResult::allowed();
+    }
+    // Create specific labels to allow or deny access based on certain test
+    // conditions.
+    // @see \Drupal\KernelTests\Core\Entity\EntityAccessControlHandlerTest
+    if ($entity->label() == 'Accessible') {
+      return AccessResult::allowed();
+    }
+    if ($entity->label() == 'Inaccessible') {
+      return AccessResult::forbidden();
+    }
+    // Uncacheable because the access result depends on a State key-value pair and
+    // might therefore change at any time.
+    $condition = \Drupal::state()->get("entity_test_entity_access.{$operation}." . $entity->id(), FALSE);
+    return AccessResult::allowedIf($condition)->setCacheMaxAge(0);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_access() for 'entity_test'.
+   */
+  #[Hook('entity_test_access')]
+  public function entityTestAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    \Drupal::state()->set('entity_test_entity_test_access', TRUE);
+    // No opinion.
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Implements hook_entity_create_access().
+   */
+  #[Hook('entity_create_access')]
+  public function entityCreateAccess(AccountInterface $account, $context, $entity_bundle) {
+    \Drupal::state()->set('entity_test_entity_create_access', TRUE);
+    \Drupal::state()->set('entity_test_entity_create_access_context', $context);
+    if ($entity_bundle === 'forbidden_access_bundle') {
+      // We need to cover a case in which a bundle is specifically forbidden
+      // from creation (as opposed to neutral access).
+      return AccessResult::forbidden();
+    }
+    // No opinion.
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_create_access() for 'entity_test'.
+   */
+  #[Hook('entity_test_create_access')]
+  public function entityTestCreateAccess(AccountInterface $account, $context, $entity_bundle) {
+    \Drupal::state()->set('entity_test_entity_test_create_access', TRUE);
+    // No opinion.
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Implements hook_query_entity_test_access_alter().
+   */
+  #[Hook('query_entity_test_access_alter')]
+  public function queryEntityTestAccessAlter(AlterableInterface $query) {
+    if (!\Drupal::state()->get('entity_test_query_access')) {
+      return;
+    }
+    /** @var \Drupal\Core\Database\Query\Select|\Drupal\Core\Database\Query\AlterableInterface $query */
+    if (!\Drupal::currentUser()->hasPermission('view all entity_test_query_access entities')) {
+      $query->condition('entity_test_query_access.name', 'published entity');
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_form_mode_alter().
+   */
+  #[Hook('entity_test_form_mode_alter')]
+  public function entityTestFormModeAlter(&$form_mode, EntityInterface $entity) : void {
+    if ($entity->getEntityTypeId() === 'entity_test' && $entity->get('name')->value === 'test_entity_type_form_mode_alter') {
+      $form_mode = 'compact';
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/Hook/EntityTestViewsHooks.php b/core/modules/system/tests/modules/entity_test/src/Hook/EntityTestViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..933840b71470fea5d01e06d484274649d3fa0e1c
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Hook/EntityTestViewsHooks.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\entity_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for entity_test.
+ */
+class EntityTestViewsHooks {
+
+  /**
+   * Implements hook_views_data_alter().
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(&$data) {
+    $data['entity_test']['name_alias'] = $data['entity_test']['name'];
+    $data['entity_test']['name_alias']['field']['real field'] = 'name';
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test_bundle_class/entity_test_bundle_class.module b/core/modules/system/tests/modules/entity_test_bundle_class/entity_test_bundle_class.module
deleted file mode 100644
index dc699381a7afe3d657728fe71388011f4c16b899..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/entity_test_bundle_class/entity_test_bundle_class.module
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-/**
- * @file
- * Support module for testing entity bundle classes.
- */
-
-declare(strict_types=1);
-
-use Drupal\entity_test\Entity\EntityTest;
-use Drupal\entity_test_bundle_class\Entity\EntityTestAmbiguousBundleClass;
-use Drupal\entity_test_bundle_class\Entity\EntityTestBundleClass;
-use Drupal\entity_test_bundle_class\Entity\EntityTestUserClass;
-use Drupal\entity_test_bundle_class\Entity\EntityTestVariant;
-use Drupal\entity_test_bundle_class\Entity\NonInheritingBundleClass;
-use Drupal\entity_test_bundle_class\Entity\SharedEntityTestBundleClassA;
-use Drupal\entity_test_bundle_class\Entity\SharedEntityTestBundleClassB;
-
-/**
- * Implements hook_entity_bundle_info_alter().
- */
-function entity_test_bundle_class_entity_bundle_info_alter(&$bundles) {
-  if (!empty($bundles['entity_test']['bundle_class'])) {
-    $bundles['entity_test']['bundle_class']['class'] = EntityTestBundleClass::class;
-  }
-
-  if (\Drupal::state()->get('entity_test_bundle_class_enable_ambiguous_entity_types', FALSE)) {
-    $bundles['entity_test']['bundle_class_2']['class'] = EntityTestBundleClass::class;
-    $bundles['entity_test']['entity_test_no_label']['class'] = EntityTestAmbiguousBundleClass::class;
-    $bundles['entity_test_no_label']['entity_test_no_label']['class'] = EntityTestAmbiguousBundleClass::class;
-  }
-
-  if (\Drupal::state()->get('entity_test_bundle_class_non_inheriting', FALSE)) {
-    $bundles['entity_test']['bundle_class']['class'] = NonInheritingBundleClass::class;
-  }
-
-  if (\Drupal::state()->get('entity_test_bundle_class_enable_user_class', FALSE)) {
-    $bundles['user']['user']['class'] = EntityTestUserClass::class;
-  }
-
-  if (\Drupal::state()->get('entity_test_bundle_class_does_not_exist', FALSE)) {
-    $bundles['entity_test']['bundle_class']['class'] = '\Drupal\Core\NonExistentClass';
-  }
-
-  // Have two bundles share the same base entity class.
-  $bundles['shared_type']['bundle_a'] = [
-    'label' => 'Bundle A',
-    'class' => SharedEntityTestBundleClassA::class,
-  ];
-  $bundles['shared_type']['bundle_b'] = [
-    'label' => 'Bundle B',
-    'class' => SharedEntityTestBundleClassB::class,
-  ];
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function entity_test_bundle_class_entity_type_alter(&$entity_types): void {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  if (\Drupal::state()->get('entity_test_bundle_class_override_base_class', FALSE) && isset($entity_types['entity_test'])) {
-    $entity_types['entity_test']->setClass(EntityTestVariant::class);
-  }
-}
-
-/**
- * Implements hook_entity_type_build().
- */
-function entity_test_bundle_class_entity_type_build(array &$entity_types): void {
-
-  // Have multiple entity types share the same class as Entity Test.
-  // This allows us to test that AmbiguousBundleClassException does not
-  // get thrown when sharing classes.
-  /** @var \Drupal\Core\Entity\ContentEntityType $original_type */
-  $cloned_type = clone $entity_types['entity_test'];
-  $cloned_type->set('bundle_of', 'entity_test');
-  $entity_types['shared_type'] = $cloned_type;
-  $entity_types['shared_type']->setClass(EntityTest::class);
-}
diff --git a/core/modules/system/tests/modules/entity_test_bundle_class/src/Hook/EntityTestBundleClassHooks.php b/core/modules/system/tests/modules/entity_test_bundle_class/src/Hook/EntityTestBundleClassHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..b5ec82ddbccaa0af2e7ab4ef3f5478a7349615fd
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_bundle_class/src/Hook/EntityTestBundleClassHooks.php
@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\entity_test_bundle_class\Hook;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\entity_test_bundle_class\Entity\EntityTestVariant;
+use Drupal\entity_test_bundle_class\Entity\SharedEntityTestBundleClassB;
+use Drupal\entity_test_bundle_class\Entity\SharedEntityTestBundleClassA;
+use Drupal\entity_test_bundle_class\Entity\EntityTestUserClass;
+use Drupal\entity_test_bundle_class\Entity\NonInheritingBundleClass;
+use Drupal\entity_test_bundle_class\Entity\EntityTestAmbiguousBundleClass;
+use Drupal\entity_test_bundle_class\Entity\EntityTestBundleClass;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for entity_test_bundle_class.
+ */
+class EntityTestBundleClassHooks {
+
+  /**
+   * Implements hook_entity_bundle_info_alter().
+   */
+  #[Hook('entity_bundle_info_alter')]
+  public function entityBundleInfoAlter(&$bundles) {
+    if (!empty($bundles['entity_test']['bundle_class'])) {
+      $bundles['entity_test']['bundle_class']['class'] = EntityTestBundleClass::class;
+    }
+    if (\Drupal::state()->get('entity_test_bundle_class_enable_ambiguous_entity_types', FALSE)) {
+      $bundles['entity_test']['bundle_class_2']['class'] = EntityTestBundleClass::class;
+      $bundles['entity_test']['entity_test_no_label']['class'] = EntityTestAmbiguousBundleClass::class;
+      $bundles['entity_test_no_label']['entity_test_no_label']['class'] = EntityTestAmbiguousBundleClass::class;
+    }
+    if (\Drupal::state()->get('entity_test_bundle_class_non_inheriting', FALSE)) {
+      $bundles['entity_test']['bundle_class']['class'] = NonInheritingBundleClass::class;
+    }
+    if (\Drupal::state()->get('entity_test_bundle_class_enable_user_class', FALSE)) {
+      $bundles['user']['user']['class'] = EntityTestUserClass::class;
+    }
+    if (\Drupal::state()->get('entity_test_bundle_class_does_not_exist', FALSE)) {
+      $bundles['entity_test']['bundle_class']['class'] = '\Drupal\Core\NonExistentClass';
+    }
+    // Have two bundles share the same base entity class.
+    $bundles['shared_type']['bundle_a'] = [
+      'label' => 'Bundle A',
+      'class' => SharedEntityTestBundleClassA::class,
+    ];
+    $bundles['shared_type']['bundle_b'] = [
+      'label' => 'Bundle B',
+      'class' => SharedEntityTestBundleClassB::class,
+    ];
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(&$entity_types) : void {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    if (\Drupal::state()->get('entity_test_bundle_class_override_base_class', FALSE) && isset($entity_types['entity_test'])) {
+      $entity_types['entity_test']->setClass(EntityTestVariant::class);
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public function entityTypeBuild(array &$entity_types) : void {
+    // Have multiple entity types share the same class as Entity Test.
+    // This allows us to test that AmbiguousBundleClassException does not
+    // get thrown when sharing classes.
+    /** @var \Drupal\Core\Entity\ContentEntityType $original_type */
+    $cloned_type = clone $entity_types['entity_test'];
+    $cloned_type->set('bundle_of', 'entity_test');
+    $entity_types['shared_type'] = $cloned_type;
+    $entity_types['shared_type']->setClass(EntityTest::class);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test_constraints/entity_test_constraints.module b/core/modules/system/tests/modules/entity_test_constraints/entity_test_constraints.module
deleted file mode 100644
index 15f0cba2a0aef82bcaad41bb26b3ff3249a39e64..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/entity_test_constraints/entity_test_constraints.module
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module file.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_entity_type_build().
- */
-function entity_test_constraints_entity_type_build(array &$entity_types) {
-  if ($extra = \Drupal::state()->get('entity_test_constraints.build')) {
-    foreach ($extra as $id => $option) {
-      $entity_types['entity_test_constraints']->addConstraint($id, $option);
-    }
-  }
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function entity_test_constraints_entity_type_alter(array &$entity_types): void {
-  if ($alter = \Drupal::state()->get('entity_test_constraints.alter')) {
-    $entity_types['entity_test_constraints']->setConstraints($alter);
-  }
-}
diff --git a/core/modules/system/tests/modules/entity_test_constraints/src/Hook/EntityTestConstraintsHooks.php b/core/modules/system/tests/modules/entity_test_constraints/src/Hook/EntityTestConstraintsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..beaab922040e04b8f4011016f2c0d5a3fc60ea71
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_constraints/src/Hook/EntityTestConstraintsHooks.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\entity_test_constraints\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for entity_test_constraints.
+ */
+class EntityTestConstraintsHooks {
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public function entityTypeBuild(array &$entity_types) {
+    if ($extra = \Drupal::state()->get('entity_test_constraints.build')) {
+      foreach ($extra as $id => $option) {
+        $entity_types['entity_test_constraints']->addConstraint($id, $option);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    if ($alter = \Drupal::state()->get('entity_test_constraints.alter')) {
+      $entity_types['entity_test_constraints']->setConstraints($alter);
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.module b/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.module
deleted file mode 100644
index 115ec0272c647aa8f412f3b308d453179228dac8..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.module
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module for the entity API providing several extra fields for testing.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityTypeInterface;
-
-/**
- * Implements hook_entity_base_field_info().
- */
-function entity_test_extra_entity_base_field_info(EntityTypeInterface $entity_type) {
-  return \Drupal::state()->get($entity_type->id() . '.additional_base_field_definitions', []);
-}
-
-/**
- * Implements hook_entity_field_storage_info().
- */
-function entity_test_extra_entity_field_storage_info(EntityTypeInterface $entity_type) {
-  return \Drupal::state()->get($entity_type->id() . '.additional_field_storage_definitions', []);
-}
-
-/**
- * Implements hook_entity_bundle_field_info().
- */
-function entity_test_extra_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
-  return \Drupal::state()->get($entity_type->id() . '.' . $bundle . '.additional_bundle_field_definitions', []);
-}
diff --git a/core/modules/system/tests/modules/entity_test_extra/src/Hook/EntityTestExtraHooks.php b/core/modules/system/tests/modules/entity_test_extra/src/Hook/EntityTestExtraHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..595b21c7b167dabe3d3297f69e160a14520b44f5
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_extra/src/Hook/EntityTestExtraHooks.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\entity_test_extra\Hook;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for entity_test_extra.
+ */
+class EntityTestExtraHooks {
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    return \Drupal::state()->get($entity_type->id() . '.additional_base_field_definitions', []);
+  }
+
+  /**
+   * Implements hook_entity_field_storage_info().
+   */
+  #[Hook('entity_field_storage_info')]
+  public function entityFieldStorageInfo(EntityTypeInterface $entity_type) {
+    return \Drupal::state()->get($entity_type->id() . '.additional_field_storage_definitions', []);
+  }
+
+  /**
+   * Implements hook_entity_bundle_field_info().
+   */
+  #[Hook('entity_bundle_field_info')]
+  public function entityBundleFieldInfo(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
+    return \Drupal::state()->get($entity_type->id() . '.' . $bundle . '.additional_bundle_field_definitions', []);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test_operation/entity_test_operation.module b/core/modules/system/tests/modules/entity_test_operation/entity_test_operation.module
deleted file mode 100644
index fd5dd45377c8d3668ec583a2f14089345afef6b8..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/entity_test_operation/entity_test_operation.module
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains hook implementations for Entity Operation Test Module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_entity_operation().
- */
-function entity_test_operation_entity_operation(EntityInterface $entity) {
-  return [
-    'test' => [
-      'title' => t('Front page'),
-      'url' => Url::fromRoute('<front>'),
-      'weight' => 0,
-    ],
-  ];
-}
diff --git a/core/modules/system/tests/modules/entity_test_operation/src/Hook/EntityTestOperationHooks.php b/core/modules/system/tests/modules/entity_test_operation/src/Hook/EntityTestOperationHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c815d8aefa034982961b9f55eb044b911b9097f1
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_operation/src/Hook/EntityTestOperationHooks.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\entity_test_operation\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for entity_test_operation.
+ */
+class EntityTestOperationHooks {
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $entity) {
+    return [
+      'test' => [
+        'title' => t('Front page'),
+        'url' => Url::fromRoute('<front>'),
+        'weight' => 0,
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test_update/entity_test_update.module b/core/modules/system/tests/modules/entity_test_update/entity_test_update.module
deleted file mode 100644
index 556e09a490b55044ad46b24bbea8465c37742dfb..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/entity_test_update/entity_test_update.module
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides an entity type for testing definition and schema updates.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
-
-/**
- * Implements hook_entity_base_field_info().
- */
-function entity_test_update_entity_base_field_info(EntityTypeInterface $entity_type) {
-  // Add a base field that will be used to test that fields added through
-  // hook_entity_base_field_info() are handled correctly during a schema
-  // conversion (e.g. from non-revisionable to revisionable).
-  if ($entity_type->id() == 'entity_test_update') {
-    $fields = [];
-    $fields['test_entity_base_field_info'] = BaseFieldDefinition::create('string')
-      ->setLabel(new TranslatableMarkup('Field added by hook_entity_base_field_info()'))
-      ->setTranslatable(TRUE)
-      ->setRevisionable(TRUE);
-
-    return $fields;
-  }
-}
-
-/**
- * Implements hook_entity_field_storage_info().
- */
-function entity_test_update_entity_field_storage_info(EntityTypeInterface $entity_type) {
-  if ($entity_type->id() == 'entity_test_update') {
-    return \Drupal::state()->get('entity_test_update.additional_field_storage_definitions', []);
-  }
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function entity_test_update_entity_type_alter(array &$entity_types): void {
-  // Allow entity_test_update tests to override the entity type definition.
-  $entity_type = \Drupal::state()->get('entity_test_update.entity_type', $entity_types['entity_test_update']);
-  if ($entity_type !== 'null') {
-    $entity_types['entity_test_update'] = $entity_type;
-  }
-  else {
-    unset($entity_types['entity_test_update']);
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for the 'view' entity type.
- */
-function entity_test_update_view_presave(EntityInterface $entity) {
-  if (\Drupal::state()->get('entity_test_update.throw_view_exception') === $entity->id()) {
-    throw new \LogicException('The view could not be saved.');
-  }
-}
diff --git a/core/modules/system/tests/modules/entity_test_update/src/Hook/EntityTestUpdateHooks.php b/core/modules/system/tests/modules/entity_test_update/src/Hook/EntityTestUpdateHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..587a66021329759db4e6c425fde4038be258d082
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test_update/src/Hook/EntityTestUpdateHooks.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\entity_test_update\Hook;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for entity_test_update.
+ */
+class EntityTestUpdateHooks {
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    // Add a base field that will be used to test that fields added through
+    // hook_entity_base_field_info() are handled correctly during a schema
+    // conversion (e.g. from non-revisionable to revisionable).
+    if ($entity_type->id() == 'entity_test_update') {
+      $fields = [];
+      $fields['test_entity_base_field_info'] = BaseFieldDefinition::create('string')->setLabel(new TranslatableMarkup('Field added by hook_entity_base_field_info()'))->setTranslatable(TRUE)->setRevisionable(TRUE);
+      return $fields;
+    }
+  }
+
+  /**
+   * Implements hook_entity_field_storage_info().
+   */
+  #[Hook('entity_field_storage_info')]
+  public function entityFieldStorageInfo(EntityTypeInterface $entity_type) {
+    if ($entity_type->id() == 'entity_test_update') {
+      return \Drupal::state()->get('entity_test_update.additional_field_storage_definitions', []);
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    // Allow entity_test_update tests to override the entity type definition.
+    $entity_type = \Drupal::state()->get('entity_test_update.entity_type', $entity_types['entity_test_update']);
+    if ($entity_type !== 'null') {
+      $entity_types['entity_test_update'] = $entity_type;
+    }
+    else {
+      unset($entity_types['entity_test_update']);
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for the 'view' entity type.
+   */
+  #[Hook('view_presave')]
+  public function viewPresave(EntityInterface $entity) {
+    if (\Drupal::state()->get('entity_test_update.throw_view_exception') === $entity->id()) {
+      throw new \LogicException('The view could not be saved.');
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/experimental_module_requirements_test/experimental_module_requirements_test.module b/core/modules/system/tests/modules/experimental_module_requirements_test/experimental_module_requirements_test.module
deleted file mode 100644
index cd399da49574adfc41f5279cbb6e3d2d7b49e8f8..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/experimental_module_requirements_test/experimental_module_requirements_test.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Experimental Test Requirements module to test hook_requirements().
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_help().
- */
-function experimental_module_requirements_test_help($route_name) {
-  switch ($route_name) {
-    case 'help.page.experimental_module_requirements_test':
-      // Make the help text conform to core standards.
-      return t('The Experimental Requirements Test module is not done yet. It may eat your data, but you can read the <a href=":url">online documentation for the Experimental Requirements Test module</a>.', [':url' => 'http://www.example.com']);
-  }
-}
diff --git a/core/modules/system/tests/modules/experimental_module_requirements_test/src/Hook/ExperimentalModuleRequirementsTestHooks.php b/core/modules/system/tests/modules/experimental_module_requirements_test/src/Hook/ExperimentalModuleRequirementsTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..cfd042c50e36ab98cbb2d75b57c6707aa8ef1bf8
--- /dev/null
+++ b/core/modules/system/tests/modules/experimental_module_requirements_test/src/Hook/ExperimentalModuleRequirementsTestHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\experimental_module_requirements_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for experimental_module_requirements_test.
+ */
+class ExperimentalModuleRequirementsTestHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name) {
+    switch ($route_name) {
+      case 'help.page.experimental_module_requirements_test':
+        // Make the help text conform to core standards.
+        return t('The Experimental Requirements Test module is not done yet. It may eat your data, but you can read the <a href=":url">online documentation for the Experimental Requirements Test module</a>.', [':url' => 'http://www.example.com']);
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/experimental_module_test/experimental_module_test.module b/core/modules/system/tests/modules/experimental_module_test/experimental_module_test.module
deleted file mode 100644
index 981b4191999af1ecfc25ad1a524b8d6645d9da43..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/experimental_module_test/experimental_module_test.module
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-/**
- * @file
- * Experimental Test module to test the Core (Experimental) package.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function experimental_module_test_help($route_name, RouteMatchInterface $route_match) {
-
-  switch ($route_name) {
-    case 'help.page.experimental_module_test':
-      // Make the help text conform to core standards.
-      return t('The Experimental Test module is not done yet. It may eat your data, but you can read the <a href=":url">online documentation for the Experimental Test module</a>.', [':url' => 'http://www.example.com']);
-  }
-
-}
diff --git a/core/modules/system/tests/modules/experimental_module_test/src/Hook/ExperimentalModuleTestHooks.php b/core/modules/system/tests/modules/experimental_module_test/src/Hook/ExperimentalModuleTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..6dbd3f5b1a6f8ce08a38f260e52493727e6e971b
--- /dev/null
+++ b/core/modules/system/tests/modules/experimental_module_test/src/Hook/ExperimentalModuleTestHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\experimental_module_test\Hook;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for experimental_module_test.
+ */
+class ExperimentalModuleTestHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.experimental_module_test':
+        // Make the help text conform to core standards.
+        return t('The Experimental Test module is not done yet. It may eat your data, but you can read the <a href=":url">online documentation for the Experimental Test module</a>.', [':url' => 'http://www.example.com']);
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module
index 1a740d199b1911d886c76e5364d4bbb828f0ce6f..8ccfc8e48d93e57ab9702bd67c806dd1f1f9f918 100644
--- a/core/modules/system/tests/modules/form_test/form_test.module
+++ b/core/modules/system/tests/modules/form_test/form_test.module
@@ -9,36 +9,6 @@
 
 use Drupal\Core\Form\FormStateInterface;
 
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function block_form_form_test_alter_form_alter(&$form, FormStateInterface $form_state): void {
-  \Drupal::messenger()->addStatus('block_form_form_test_alter_form_alter() executed.');
-}
-
-/**
- * Implements hook_form_alter().
- */
-function form_test_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  if ($form_id == 'form_test_alter_form') {
-    \Drupal::messenger()->addStatus('form_test_form_alter() executed.');
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function form_test_form_form_test_alter_form_alter(&$form, FormStateInterface $form_state): void {
-  \Drupal::messenger()->addStatus('form_test_form_form_test_alter_form_alter() executed.');
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- */
-function system_form_form_test_alter_form_alter(&$form, FormStateInterface $form_state): void {
-  \Drupal::messenger()->addStatus('system_form_form_test_alter_form_alter() executed.');
-}
-
 /**
  * Create a header and options array. Helper function for callbacks.
  */
@@ -77,17 +47,6 @@ function _form_test_tableselect_get_data() {
   return [$header, $options];
 }
 
-/**
- * Implements hook_form_FORM_ID_alter() for the registration form.
- */
-function form_test_form_user_register_form_alter(&$form, FormStateInterface $form_state): void {
-  $form['test_rebuild'] = [
-    '#type' => 'submit',
-    '#value' => t('Rebuild'),
-    '#submit' => ['form_test_user_register_form_rebuild'],
-  ];
-}
-
 /**
  * Submit callback that just lets the form rebuild.
  */
@@ -96,17 +55,6 @@ function form_test_user_register_form_rebuild($form, FormStateInterface $form_st
   $form_state->setRebuild();
 }
 
-/**
- * Implements hook_form_FORM_ID_alter() for form_test_vertical_tabs_access_form().
- */
-function form_test_form_form_test_vertical_tabs_access_form_alter(&$form, &$form_state, $form_id): void {
-  $form['vertical_tabs1']['#access'] = FALSE;
-  $form['vertical_tabs2']['#access'] = FALSE;
-  $form['tabs3']['#access'] = TRUE;
-  $form['fieldset1']['#access'] = FALSE;
-  $form['container']['#access'] = FALSE;
-}
-
 /**
  * Ajax callback that returns the form element.
  */
diff --git a/core/modules/system/tests/modules/form_test/src/Hook/FormTestHooks.php b/core/modules/system/tests/modules/form_test/src/Hook/FormTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..140ddb02dcdb21e9d331f7d4b2a19c2d1cb64aa6
--- /dev/null
+++ b/core/modules/system/tests/modules/form_test/src/Hook/FormTestHooks.php
@@ -0,0 +1,75 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\form_test\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for form_test.
+ */
+class FormTestHooks {
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_form_test_alter_form_alter', module: 'block')]
+  public function blockFormFormTestAlterFormAlter(&$form, FormStateInterface $form_state) : void {
+    \Drupal::messenger()->addStatus('block_form_form_test_alter_form_alter() executed.');
+  }
+
+  /**
+   * Implements hook_form_alter().
+   */
+  #[Hook('form_alter')]
+  public function formAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    if ($form_id == 'form_test_alter_form') {
+      \Drupal::messenger()->addStatus('form_test_form_alter() executed.');
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_form_test_alter_form_alter')]
+  public function formFormTestAlterFormAlter(&$form, FormStateInterface $form_state) : void {
+    \Drupal::messenger()->addStatus('form_test_form_form_test_alter_form_alter() executed.');
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter().
+   */
+  #[Hook('form_form_test_alter_form_alter', module: 'system')]
+  public function systemFormFormTestAlterFormAlter(&$form, FormStateInterface $form_state) : void {
+    \Drupal::messenger()->addStatus('system_form_form_test_alter_form_alter() executed.');
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for the registration form.
+   */
+  #[Hook('form_user_register_form_alter')]
+  public function formUserRegisterFormAlter(&$form, FormStateInterface $form_state) : void {
+    $form['test_rebuild'] = [
+      '#type' => 'submit',
+      '#value' => t('Rebuild'),
+      '#submit' => [
+        'form_test_user_register_form_rebuild',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for form_test_vertical_tabs_access_form().
+   */
+  #[Hook('form_form_test_vertical_tabs_access_form_alter')]
+  public function formFormTestVerticalTabsAccessFormAlter(&$form, &$form_state, $form_id) : void {
+    $form['vertical_tabs1']['#access'] = FALSE;
+    $form['vertical_tabs2']['#access'] = FALSE;
+    $form['tabs3']['#access'] = TRUE;
+    $form['fieldset1']['#access'] = FALSE;
+    $form['container']['#access'] = FALSE;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/jquery_keyevent_polyfill_test/jquery_keyevent_polyfill_test.module b/core/modules/system/tests/modules/jquery_keyevent_polyfill_test/jquery_keyevent_polyfill_test.module
deleted file mode 100644
index 7c643f423fea575ee877041454ac1ad809233e3a..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/jquery_keyevent_polyfill_test/jquery_keyevent_polyfill_test.module
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for jQuery 3.6 FunctionalJavascript test compatibility.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_library_info_alter().
- */
-function jquery_keyevent_polyfill_test_library_info_alter(&$libraries, $module) {
-  if ($module == 'core' && isset($libraries['jquery'])) {
-    $libraries['jquery']['dependencies'][] = 'jquery_keyevent_polyfill_test/jquery.keyevent.polyfill';
-  }
-}
diff --git a/core/modules/system/tests/modules/jquery_keyevent_polyfill_test/src/Hook/JqueryKeyeventPolyfillTestHooks.php b/core/modules/system/tests/modules/jquery_keyevent_polyfill_test/src/Hook/JqueryKeyeventPolyfillTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..64d23aef929920c40f34329401d9816abe35bc52
--- /dev/null
+++ b/core/modules/system/tests/modules/jquery_keyevent_polyfill_test/src/Hook/JqueryKeyeventPolyfillTestHooks.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\jquery_keyevent_polyfill_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for jquery_keyevent_polyfill_test.
+ */
+class JqueryKeyeventPolyfillTestHooks {
+
+  /**
+   * Implements hook_library_info_alter().
+   */
+  #[Hook('library_info_alter')]
+  public function libraryInfoAlter(&$libraries, $module) {
+    if ($module == 'core' && isset($libraries['jquery'])) {
+      $libraries['jquery']['dependencies'][] = 'jquery_keyevent_polyfill_test/jquery.keyevent.polyfill';
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.module b/core/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.module
deleted file mode 100644
index be3ef20c301c6cac0f7365c6848d6bcc36e5931c..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.module
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the JavaScript deprecation tests.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_js_settings_alter().
- */
-function js_deprecation_test_js_settings_alter(&$settings) {
-  $settings['suppressDeprecationErrors'] = FALSE;
-}
diff --git a/core/modules/system/tests/modules/js_deprecation_test/src/Hook/JsDeprecationTestHooks.php b/core/modules/system/tests/modules/js_deprecation_test/src/Hook/JsDeprecationTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..191382e24bfea74118dc27646771bcd12f68b09a
--- /dev/null
+++ b/core/modules/system/tests/modules/js_deprecation_test/src/Hook/JsDeprecationTestHooks.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\js_deprecation_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for js_deprecation_test.
+ */
+class JsDeprecationTestHooks {
+
+  /**
+   * Implements hook_js_settings_alter().
+   */
+  #[Hook('js_settings_alter')]
+  public function jsSettingsAlter(&$settings) {
+    $settings['suppressDeprecationErrors'] = FALSE;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/js_testing_ajax_request_test/js_testing_ajax_request_test.module b/core/modules/system/tests/modules/js_testing_ajax_request_test/js_testing_ajax_request_test.module
deleted file mode 100644
index 56e443667aab969378db048f392de1e8cdc1b908..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/js_testing_ajax_request_test/js_testing_ajax_request_test.module
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the JavaScript AJAX request tests.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_page_attachments().
- */
-function js_testing_ajax_request_test_page_attachments(array &$attachments) {
-  // Unconditionally attach an asset to the page.
-  $attachments['#attached']['library'][] = 'js_testing_ajax_request_test/track_ajax_requests';
-}
diff --git a/core/modules/system/tests/modules/js_testing_ajax_request_test/src/Hook/JsTestingAjaxRequestTestHooks.php b/core/modules/system/tests/modules/js_testing_ajax_request_test/src/Hook/JsTestingAjaxRequestTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f7bf260b478368aec7c21c26fb5b6026eeb16cab
--- /dev/null
+++ b/core/modules/system/tests/modules/js_testing_ajax_request_test/src/Hook/JsTestingAjaxRequestTestHooks.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\js_testing_ajax_request_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for js_testing_ajax_request_test.
+ */
+class JsTestingAjaxRequestTestHooks {
+
+  /**
+   * Implements hook_page_attachments().
+   */
+  #[Hook('page_attachments')]
+  public function pageAttachments(array &$attachments) {
+    // Unconditionally attach an asset to the page.
+    $attachments['#attached']['library'][] = 'js_testing_ajax_request_test/track_ajax_requests';
+  }
+
+}
diff --git a/core/modules/system/tests/modules/js_testing_log_test/js_testing_log_test.module b/core/modules/system/tests/modules/js_testing_log_test/js_testing_log_test.module
deleted file mode 100644
index 92f683a3c99219a5df7169211420ca742f35fc05..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/js_testing_log_test/js_testing_log_test.module
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the JavaScript deprecation tests.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_page_attachments().
- */
-function js_testing_log_test_page_attachments(array &$attachments) {
-  // Unconditionally attach an asset to the page.
-  $attachments['#attached']['library'][] = 'js_testing_log_test/deprecation_log';
-}
-
-/**
- * Implements hook_js_settings_alter().
- */
-function js_testing_log_test_js_settings_alter(&$settings) {
-  $settings['suppressDeprecationErrors'] = FALSE;
-}
diff --git a/core/modules/system/tests/modules/js_testing_log_test/src/Hook/JsTestingLogTestHooks.php b/core/modules/system/tests/modules/js_testing_log_test/src/Hook/JsTestingLogTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..278651fa889ce4e49d0e60748ffd074b357f7ca9
--- /dev/null
+++ b/core/modules/system/tests/modules/js_testing_log_test/src/Hook/JsTestingLogTestHooks.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\js_testing_log_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for js_testing_log_test.
+ */
+class JsTestingLogTestHooks {
+
+  /**
+   * Implements hook_page_attachments().
+   */
+  #[Hook('page_attachments')]
+  public function pageAttachments(array &$attachments) {
+    // Unconditionally attach an asset to the page.
+    $attachments['#attached']['library'][] = 'js_testing_log_test/deprecation_log';
+  }
+
+  /**
+   * Implements hook_js_settings_alter().
+   */
+  #[Hook('js_settings_alter')]
+  public function jsSettingsAlter(&$settings) {
+    $settings['suppressDeprecationErrors'] = FALSE;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.module b/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.module
deleted file mode 100644
index 23feadb057fc3e6b879b8d792c4f23932e50fa79..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/keyvalue_test/keyvalue_test.module
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-/**
- * @file
- * Sets up the key value entity storage.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_entity_type_alter().
- */
-function keyvalue_test_entity_type_alter(array &$entity_types): void {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  if (isset($entity_types['entity_test_label'])) {
-    $entity_types['entity_test_label']->setStorageClass('Drupal\Core\Entity\KeyValueStore\KeyValueContentEntityStorage');
-    $entity_keys = $entity_types['entity_test_label']->getKeys();
-    $entity_types['entity_test_label']->set('entity_keys', $entity_keys + ['uuid' => 'uuid']);
-    $entity_types['entity_test_label']->set('provider', 'keyvalue_test');
-  }
-}
diff --git a/core/modules/system/tests/modules/keyvalue_test/src/Hook/KeyvalueTestHooks.php b/core/modules/system/tests/modules/keyvalue_test/src/Hook/KeyvalueTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c8c919165ed93af635de46f893660351604b0119
--- /dev/null
+++ b/core/modules/system/tests/modules/keyvalue_test/src/Hook/KeyvalueTestHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\keyvalue_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for keyvalue_test.
+ */
+class KeyvalueTestHooks {
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    if (isset($entity_types['entity_test_label'])) {
+      $entity_types['entity_test_label']->setStorageClass('Drupal\Core\Entity\KeyValueStore\KeyValueContentEntityStorage');
+      $entity_keys = $entity_types['entity_test_label']->getKeys();
+      $entity_types['entity_test_label']->set('entity_keys', $entity_keys + ['uuid' => 'uuid']);
+      $entity_types['entity_test_label']->set('provider', 'keyvalue_test');
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/lazy_route_provider_install_test/lazy_route_provider_install_test.module b/core/modules/system/tests/modules/lazy_route_provider_install_test/lazy_route_provider_install_test.module
deleted file mode 100644
index b611fe08e376bc13c0e55e0865e1a763609af8fd..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/lazy_route_provider_install_test/lazy_route_provider_install_test.module
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the lazy route provider tests.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_menu_links_discovered_alter().
- */
-function lazy_route_provider_install_test_menu_links_discovered_alter(&$links) {
-  $message = \Drupal::state()->get(__FUNCTION__, 'success');
-  try {
-    // Ensure that calling this does not cause a recursive rebuild.
-    \Drupal::service('router.route_provider')->getAllRoutes();
-  }
-  catch (\RuntimeException) {
-    $message = 'failed';
-  }
-  \Drupal::state()->set(__FUNCTION__, $message);
-}
diff --git a/core/modules/system/tests/modules/lazy_route_provider_install_test/src/Hook/LazyRouteProviderInstallTestHooks.php b/core/modules/system/tests/modules/lazy_route_provider_install_test/src/Hook/LazyRouteProviderInstallTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..bae8f9a8fc2cb98b66271e0168790ba77ecd1cc2
--- /dev/null
+++ b/core/modules/system/tests/modules/lazy_route_provider_install_test/src/Hook/LazyRouteProviderInstallTestHooks.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\lazy_route_provider_install_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for lazy_route_provider_install_test.
+ */
+class LazyRouteProviderInstallTestHooks {
+
+  /**
+   * Implements hook_menu_links_discovered_alter().
+   */
+  #[Hook('menu_links_discovered_alter')]
+  public function menuLinksDiscoveredAlter(&$links) {
+    $message = \Drupal::state()->get('lazy_route_provider_install_test_menu_links_discovered_alter', 'success');
+    try {
+      // Ensure that calling this does not cause a recursive rebuild.
+      \Drupal::service('router.route_provider')->getAllRoutes();
+    }
+    catch (\RuntimeException) {
+      $message = 'failed';
+    }
+    \Drupal::state()->set('lazy_route_provider_install_test_menu_links_discovered_alter', $message);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/link_generation_test/link_generation_test.module b/core/modules/system/tests/modules/link_generation_test/link_generation_test.module
deleted file mode 100644
index 3fb66e1ab0634836837ba21313babcff7fff8691..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/link_generation_test/link_generation_test.module
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the link generation tests.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_link_alter().
- */
-function link_generation_test_link_alter(&$variables) {
-  if (\Drupal::state()->get('link_generation_test_link_alter', FALSE)) {
-    // Add a text to the end of links.
-    if (\Drupal::state()->get('link_generation_test_link_alter_safe', FALSE)) {
-      $variables['text'] = t('@text <strong>Test!</strong>', ['@text' => $variables['text']]);
-    }
-    else {
-      $variables['text'] .= ' <strong>Test!</strong>';
-    }
-  }
-}
diff --git a/core/modules/system/tests/modules/link_generation_test/src/Hook/LinkGenerationTestHooks.php b/core/modules/system/tests/modules/link_generation_test/src/Hook/LinkGenerationTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..c402e6a21287b32b1096309474e803692712b1f6
--- /dev/null
+++ b/core/modules/system/tests/modules/link_generation_test/src/Hook/LinkGenerationTestHooks.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\link_generation_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for link_generation_test.
+ */
+class LinkGenerationTestHooks {
+
+  /**
+   * Implements hook_link_alter().
+   */
+  #[Hook('link_alter')]
+  public function linkAlter(&$variables) {
+    if (\Drupal::state()->get('link_generation_test_link_alter', FALSE)) {
+      // Add a text to the end of links.
+      if (\Drupal::state()->get('link_generation_test_link_alter_safe', FALSE)) {
+        $variables['text'] = t('@text <strong>Test!</strong>', ['@text' => $variables['text']]);
+      }
+      else {
+        $variables['text'] .= ' <strong>Test!</strong>';
+      }
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/mail_cancel_test/mail_cancel_test.module b/core/modules/system/tests/modules/mail_cancel_test/mail_cancel_test.module
deleted file mode 100644
index 4e289a842b07e42d9b47860771c5eab87bd4e394..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/mail_cancel_test/mail_cancel_test.module
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the catching mail that needs to be cancelled.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_mail_alter().
- *
- * Aborts sending of messages with ID 'mail_cancel_test_cancel_test'.
- *
- * @see MailTestCase::testCancelMessage()
- */
-function mail_cancel_test_mail_alter(&$message) {
-  if ($message['id'] == 'mail_cancel_test_cancel_test') {
-    $message['send'] = FALSE;
-  }
-}
diff --git a/core/modules/system/tests/modules/mail_cancel_test/src/Hook/MailCancelTestHooks.php b/core/modules/system/tests/modules/mail_cancel_test/src/Hook/MailCancelTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..a95f2763fda8195ffa7efd33705d24e8586799e1
--- /dev/null
+++ b/core/modules/system/tests/modules/mail_cancel_test/src/Hook/MailCancelTestHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\mail_cancel_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for mail_cancel_test.
+ */
+class MailCancelTestHooks {
+
+  /**
+   * Implements hook_mail_alter().
+   *
+   * Aborts sending of messages with ID 'mail_cancel_test_cancel_test'.
+   *
+   * @see MailTestCase::testCancelMessage()
+   */
+  #[Hook('mail_alter')]
+  public function mailAlter(&$message) {
+    if ($message['id'] == 'mail_cancel_test_cancel_test') {
+      $message['send'] = FALSE;
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/mail_html_test/mail_html_test.module b/core/modules/system/tests/modules/mail_html_test/mail_html_test.module
deleted file mode 100644
index b6802095172bf1732b5fdeb5a7e70d409efdf4ba..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/mail_html_test/mail_html_test.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the html mail and URL conversion tests.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_mail().
- */
-function mail_html_test_mail($key, &$message, $params) {
-  switch ($key) {
-    case 'render_from_message_param':
-      $message['body'][] = \Drupal::service('renderer')->renderInIsolation($params['message']);
-      break;
-  }
-}
diff --git a/core/modules/system/tests/modules/mail_html_test/src/Hook/MailHtmlTestHooks.php b/core/modules/system/tests/modules/mail_html_test/src/Hook/MailHtmlTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..ad762ccfe617bdaf668a112a2b13f53a65ff2559
--- /dev/null
+++ b/core/modules/system/tests/modules/mail_html_test/src/Hook/MailHtmlTestHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\mail_html_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for mail_html_test.
+ */
+class MailHtmlTestHooks {
+
+  /**
+   * Implements hook_mail().
+   */
+  #[Hook('mail')]
+  public function mail($key, &$message, $params) {
+    switch ($key) {
+      case 'render_from_message_param':
+        $message['body'][] = \Drupal::service('renderer')->renderInIsolation($params['message']);
+        break;
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/many_assets_test/many_assets_test.module b/core/modules/system/tests/modules/many_assets_test/many_assets_test.module
deleted file mode 100644
index 1621b095619b380b80fb94e4b076ea42d800bc5c..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/many_assets_test/many_assets_test.module
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for asset load order test.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_library_info_build().
- */
-function many_assets_test_library_info_build() {
-  $libraries = [];
-  // Load the local javascript as an "external" asset varied by query string.
-  $base_javascript = \Drupal::request()->getBasePath() . '/' . \Drupal::service('extension.list.module')->getPath('many_assets_test') . '/js/noop.js';
-  $base_css = \Drupal::request()->getBasePath() . '/' . \Drupal::service('extension.list.module')->getPath('many_assets_test') . '/css/noop.css';
-
-  // Build a library dependency containing 100 javascript assets.
-  for ($i = 1; $i <= 150; $i++) {
-    $libraries['many-dependencies']['js'][$base_javascript . '?dep' . $i] = [
-      'type' => 'external',
-    ];
-    $libraries['many-dependencies']['css']['component'][$base_css . '?dep' . $i] = [
-      'type' => 'external',
-    ];
-  }
-
-  return $libraries;
-}
diff --git a/core/modules/system/tests/modules/many_assets_test/src/Hook/ManyAssetsTestHooks.php b/core/modules/system/tests/modules/many_assets_test/src/Hook/ManyAssetsTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..4582934a2ecf1bf37d5c0652be083208b4f9d8ea
--- /dev/null
+++ b/core/modules/system/tests/modules/many_assets_test/src/Hook/ManyAssetsTestHooks.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\many_assets_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for many_assets_test.
+ */
+class ManyAssetsTestHooks {
+
+  /**
+   * Implements hook_library_info_build().
+   */
+  #[Hook('library_info_build')]
+  public function libraryInfoBuild() {
+    $libraries = [];
+    // Load the local javascript as an "external" asset varied by query string.
+    $base_javascript = \Drupal::request()->getBasePath() . '/' . \Drupal::service('extension.list.module')->getPath('many_assets_test') . '/js/noop.js';
+    $base_css = \Drupal::request()->getBasePath() . '/' . \Drupal::service('extension.list.module')->getPath('many_assets_test') . '/css/noop.css';
+    // Build a library dependency containing 100 javascript assets.
+    for ($i = 1; $i <= 150; $i++) {
+      $libraries['many-dependencies']['js'][$base_javascript . '?dep' . $i] = ['type' => 'external'];
+      $libraries['many-dependencies']['css']['component'][$base_css . '?dep' . $i] = ['type' => 'external'];
+    }
+    return $libraries;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module
index d188250500ae300dbf0d15a1ddd852781dd3fcbd..1707582d7fa283cc5587d40dc846298a6de22bdf 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.module
+++ b/core/modules/system/tests/modules/menu_test/menu_test.module
@@ -7,53 +7,6 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_menu_links_discovered_alter().
- */
-function menu_test_menu_links_discovered_alter(&$links) {
-  // Many of the machine names here are slightly different from the route name.
-  // Since the machine name is arbitrary, this helps ensure that core does not
-  // add mistaken assumptions about the correlation.
-  $links['menu_test.menu_name_test']['menu_name'] = menu_test_menu_name();
-  $links['menu_test.context']['title'] = \Drupal::config('menu_test.menu_item')->get('title');
-
-  // Adds a custom menu link.
-  $links['menu_test.custom'] = [
-    'title' => 'Custom link',
-    'route_name' => 'menu_test.custom',
-    'description' => 'Custom link used to check the integrity of manually added menu links.',
-    'parent' => 'menu_test',
-  ];
-}
-
-/**
- * Implements hook_menu_local_tasks_alter().
- */
-function menu_test_menu_local_tasks_alter(&$data, $route_name, RefinableCacheableDependencyInterface &$cacheability) {
-  if (in_array($route_name, ['menu_test.tasks_default'])) {
-    $data['tabs'][0]['foo'] = [
-      '#theme' => 'menu_local_task',
-      '#link' => [
-        'title' => "Task 1 <script>alert('Welcome to the jungle!')</script>",
-        'url' => Url::fromRoute('menu_test.router_test1', ['bar' => '1']),
-      ],
-      '#weight' => 10,
-    ];
-    $data['tabs'][0]['bar'] = [
-      '#theme' => 'menu_local_task',
-      '#link' => [
-        'title' => 'Task 2',
-        'url' => Url::fromRoute('menu_test.router_test2', ['bar' => '2']),
-      ],
-      '#weight' => 20,
-    ];
-  }
-  $cacheability->addCacheTags(['kittens:dwarf-cat']);
-}
-
 /**
  * Sets a static variable for the testMenuName() test.
  *
diff --git a/core/modules/system/tests/modules/menu_test/src/Hook/MenuTestHooks.php b/core/modules/system/tests/modules/menu_test/src/Hook/MenuTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..654c42218de9922168d4f0d486f3037ed7b33931
--- /dev/null
+++ b/core/modules/system/tests/modules/menu_test/src/Hook/MenuTestHooks.php
@@ -0,0 +1,65 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\menu_test\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for menu_test.
+ */
+class MenuTestHooks {
+
+  /**
+   * Implements hook_menu_links_discovered_alter().
+   */
+  #[Hook('menu_links_discovered_alter')]
+  public function menuLinksDiscoveredAlter(&$links) {
+    // Many of the machine names here are slightly different from the route name.
+    // Since the machine name is arbitrary, this helps ensure that core does not
+    // add mistaken assumptions about the correlation.
+    $links['menu_test.menu_name_test']['menu_name'] = menu_test_menu_name();
+    $links['menu_test.context']['title'] = \Drupal::config('menu_test.menu_item')->get('title');
+    // Adds a custom menu link.
+    $links['menu_test.custom'] = [
+      'title' => 'Custom link',
+      'route_name' => 'menu_test.custom',
+      'description' => 'Custom link used to check the integrity of manually added menu links.',
+      'parent' => 'menu_test',
+    ];
+  }
+
+  /**
+   * Implements hook_menu_local_tasks_alter().
+   */
+  #[Hook('menu_local_tasks_alter')]
+  public function menuLocalTasksAlter(&$data, $route_name, RefinableCacheableDependencyInterface &$cacheability) {
+    if (in_array($route_name, ['menu_test.tasks_default'])) {
+      $data['tabs'][0]['foo'] = [
+        '#theme' => 'menu_local_task',
+        '#link' => [
+          'title' => "Task 1 <script>alert('Welcome to the jungle!')</script>",
+          'url' => Url::fromRoute('menu_test.router_test1', [
+            'bar' => '1',
+          ]),
+        ],
+        '#weight' => 10,
+      ];
+      $data['tabs'][0]['bar'] = [
+        '#theme' => 'menu_local_task',
+        '#link' => [
+          'title' => 'Task 2',
+          'url' => Url::fromRoute('menu_test.router_test2', [
+            'bar' => '2',
+          ]),
+        ],
+        '#weight' => 20,
+      ];
+    }
+    $cacheability->addCacheTags(['kittens:dwarf-cat']);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/module_required_test/module_required_test.module b/core/modules/system/tests/modules/module_required_test/module_required_test.module
deleted file mode 100644
index 3bbf43b4eae3854666c25ff8aa716f2581f5d4fd..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/module_required_test/module_required_test.module
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Extension\Extension;
-
-/**
- * Implements hook_system_info_alter().
- *
- * Manipulate module dependencies to test dependency chains.
- */
-function module_required_test_system_info_alter(&$info, Extension $file, $type) {
-  if ($file->getName() == 'module_required_test' && \Drupal::state()->get('module_required_test.hook_system_info_alter')) {
-    $info['required'] = TRUE;
-    $info['explanation'] = 'Testing hook_system_info_alter()';
-  }
-}
diff --git a/core/modules/system/tests/modules/module_required_test/src/Hook/ModuleRequiredTestHooks.php b/core/modules/system/tests/modules/module_required_test/src/Hook/ModuleRequiredTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..29c30b4936085474035cacc95caaf04cf1fd89d2
--- /dev/null
+++ b/core/modules/system/tests/modules/module_required_test/src/Hook/ModuleRequiredTestHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\module_required_test\Hook;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for module_required_test.
+ */
+class ModuleRequiredTestHooks {
+
+  /**
+   * Implements hook_system_info_alter().
+   *
+   * Manipulate module dependencies to test dependency chains.
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(&$info, Extension $file, $type) {
+    if ($file->getName() == 'module_required_test' && \Drupal::state()->get('module_required_test.hook_system_info_alter')) {
+      $info['required'] = TRUE;
+      $info['explanation'] = 'Testing hook_system_info_alter()';
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/off_canvas_test/off_canvas_test.module b/core/modules/system/tests/modules/off_canvas_test/off_canvas_test.module
deleted file mode 100644
index d1ad0923ccd7999aeb89167fcbaa82e8d80ed1c7..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/off_canvas_test/off_canvas_test.module
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains hook implementations for testing the off-canvas area.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_page_attachments().
- */
-function off_canvas_test_page_attachments(array &$attachments) {
-  // This library wraps around the Drupal.offCanvas.resetSize() method and adds
-  // a special data-resize-done attribute to help functional JavaScript tests
-  // use the off-canvas area when it is fully loaded and ready to be interacted
-  // with.
-  // @see \Drupal\Tests\system\Traits\OffCanvasTestTrait::waitForOffCanvasArea()
-  $attachments['#attached']['library'][] = 'off_canvas_test/resize_helper';
-}
diff --git a/core/modules/system/tests/modules/off_canvas_test/src/Hook/OffCanvasTestHooks.php b/core/modules/system/tests/modules/off_canvas_test/src/Hook/OffCanvasTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..56de094eb9e8c059586e9d4b936274a4757053a4
--- /dev/null
+++ b/core/modules/system/tests/modules/off_canvas_test/src/Hook/OffCanvasTestHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\off_canvas_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for off_canvas_test.
+ */
+class OffCanvasTestHooks {
+
+  /**
+   * Implements hook_page_attachments().
+   */
+  #[Hook('page_attachments')]
+  public function pageAttachments(array &$attachments) {
+    // This library wraps around the Drupal.offCanvas.resetSize() method and adds
+    // a special data-resize-done attribute to help functional JavaScript tests
+    // use the off-canvas area when it is fully loaded and ready to be interacted
+    // with.
+    // @see \Drupal\Tests\system\Traits\OffCanvasTestTrait::waitForOffCanvasArea()
+    $attachments['#attached']['library'][] = 'off_canvas_test/resize_helper';
+  }
+
+}
diff --git a/core/modules/system/tests/modules/plugin_test/plugin_test.module b/core/modules/system/tests/modules/plugin_test/plugin_test.module
deleted file mode 100644
index 992720ea888acca09805e8d3bc9ef548d4a83166..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/plugin_test/plugin_test.module
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for the plugin tests.
- */
-
-declare(strict_types=1);
-
-use Drupal\plugin_test\Plugin\plugin_test\fruit\Apple;
-
-/**
- * Implements hook_test_plugin_info().
- */
-function plugin_test_test_plugin_info() {
-  return [
-    'apple' => [
-      'id' => 'apple',
-      'class' => Apple::class,
-    ],
-  ];
-}
-
-/**
- * Implements hook_plugin_test_alter().
- */
-function plugin_test_plugin_test_alter(&$definitions) {
-  foreach ($definitions as &$definition) {
-    $definition['altered'] = TRUE;
-  }
-  $definitions['user_login']['altered_single'] = TRUE;
-}
diff --git a/core/modules/system/tests/modules/plugin_test/src/Hook/PluginTestHooks.php b/core/modules/system/tests/modules/plugin_test/src/Hook/PluginTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..5f9096b7243f1a7ab750be9a497280dd62a1026d
--- /dev/null
+++ b/core/modules/system/tests/modules/plugin_test/src/Hook/PluginTestHooks.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\plugin_test\Hook;
+
+use Drupal\plugin_test\Plugin\plugin_test\fruit\Apple;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for plugin_test.
+ */
+class PluginTestHooks {
+
+  /**
+   * Implements hook_test_plugin_info().
+   */
+  #[Hook('test_plugin_info')]
+  public function testPluginInfo() {
+    return [
+      'apple' => [
+        'id' => 'apple',
+        'class' => Apple::class,
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_plugin_test_alter().
+   */
+  #[Hook('plugin_test_alter')]
+  public function pluginTestAlter(&$definitions) {
+    foreach ($definitions as &$definition) {
+      $definition['altered'] = TRUE;
+    }
+    $definitions['user_login']['altered_single'] = TRUE;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example4.php b/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example4.php
index 0849a53de8187da531c22f5c3233afdbb485495c..d4be1e9adc6bde4f7e07124de5b1050c963352b0 100644
--- a/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example4.php
+++ b/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example4.php
@@ -4,7 +4,7 @@
 
 namespace Drupal\plugin_test\Plugin\plugin_test\custom_annotation;
 
-use Drupal\plugin_test\Plugin;
+use Drupal\plugin_test\Plugin\Attribute\PluginExample;
 
 /**
  * Provides a test plugin with a custom attribute.
@@ -14,7 +14,7 @@
  *
  * @see \Drupal\Component\Annotation\Doctrine\StaticReflectionParser::parse()
  */
-#[Plugin\Attribute\PluginExample(
+#[PluginExample(
   id: "example_4",
   custom: "Example 4"
 )]
diff --git a/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example5.php b/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example5.php
index 08eb4acc42108f60b84c20fceac8bb0da1dbf568..d62fcf484b02d22f5548d85efd96535520350d80 100644
--- a/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example5.php
+++ b/core/modules/system/tests/modules/plugin_test/src/Plugin/plugin_test/custom_annotation/Example5.php
@@ -4,6 +4,8 @@
 
 namespace Drupal\plugin_test\Plugin\plugin_test\custom_annotation;
 
+use Drupal\plugin_test\Plugin\Attribute\PluginExample;
+
 /**
  * Provides a test plugin with a custom attribute.
  *
@@ -13,7 +15,7 @@
  * @see \Drupal\Component\Annotation\Doctrine\StaticReflectionParser::parse()
  */
 #[\Attribute]
-#[\Drupal\plugin_test\Plugin\Attribute\PluginExample(
+#[PluginExample(
   id: "example_5",
   custom: "Example 5"
 )]
diff --git a/core/modules/system/tests/modules/requirements1_test/requirements1_test.module b/core/modules/system/tests/modules/requirements1_test/requirements1_test.module
deleted file mode 100644
index 9e89c58144c18d41f81acb1f3b0cfb052049c02d..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/requirements1_test/requirements1_test.module
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * Hook implementations for requirements1_test module.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_requirements_alter().
- */
-function requirements1_test_requirements_alter(array &$requirements): void {
-  // Change the title.
-  $requirements['requirements1_test_alterable']['title'] = t('Requirements 1 Test - Changed');
-  // Decrease the severity.
-  $requirements['requirements1_test_alterable']['severity'] = REQUIREMENT_WARNING;
-  // Delete 'requirements1_test_deletable',
-  unset($requirements['requirements1_test_deletable']);
-}
diff --git a/core/modules/system/tests/modules/requirements1_test/src/Hook/Requirements1TestHooks.php b/core/modules/system/tests/modules/requirements1_test/src/Hook/Requirements1TestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f122ea913fbdba5782a12ff110c82c1ef28fbb7f
--- /dev/null
+++ b/core/modules/system/tests/modules/requirements1_test/src/Hook/Requirements1TestHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\requirements1_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for requirements1_test.
+ */
+class Requirements1TestHooks {
+
+  /**
+   * Implements hook_requirements_alter().
+   */
+  #[Hook('requirements_alter')]
+  public function requirementsAlter(array &$requirements) : void {
+    // Change the title.
+    $requirements['requirements1_test_alterable']['title'] = t('Requirements 1 Test - Changed');
+    // Decrease the severity.
+    $requirements['requirements1_test_alterable']['severity'] = REQUIREMENT_WARNING;
+    // Delete 'requirements1_test_deletable',
+    unset($requirements['requirements1_test_deletable']);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/router_installer_test/router_installer_test.module b/core/modules/system/tests/modules/router_installer_test/router_installer_test.module
deleted file mode 100644
index c75f307a28422b8155d8e2b7541139885d03af6d..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/router_installer_test/router_installer_test.module
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-/**
- * @file
- * Install, update and uninstall functions for the router_installer_test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Url;
-
-/**
- * Implements hook_modules_installed().
- */
-function router_installer_test_modules_installed($modules) {
-  if (in_array('router_installer_test', $modules, TRUE)) {
-    // Ensure a URL can be generated for routes provided by the module during
-    // installation.
-    \Drupal::state()->set(__FUNCTION__, Url::fromRoute('router_installer_test.1')->toString());
-  }
-}
diff --git a/core/modules/system/tests/modules/router_installer_test/src/Hook/RouterInstallerTestHooks.php b/core/modules/system/tests/modules/router_installer_test/src/Hook/RouterInstallerTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..644f1ada58a85a3d484f74b6ad53b0183a263dda
--- /dev/null
+++ b/core/modules/system/tests/modules/router_installer_test/src/Hook/RouterInstallerTestHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\router_installer_test\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for router_installer_test.
+ */
+class RouterInstallerTestHooks {
+
+  /**
+   * Implements hook_modules_installed().
+   */
+  #[Hook('modules_installed')]
+  public function modulesInstalled($modules) {
+    if (in_array('router_installer_test', $modules, TRUE)) {
+      // Ensure a URL can be generated for routes provided by the module during
+      // installation.
+      \Drupal::state()->set('router_installer_test_modules_installed', Url::fromRoute('router_installer_test.1')->toString());
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/session_exists_cache_context_test/session_exists_cache_context_test.module b/core/modules/system/tests/modules/session_exists_cache_context_test/session_exists_cache_context_test.module
deleted file mode 100644
index 047264ec71addd8b7d0cdb592b67ca838eebe140..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/session_exists_cache_context_test/session_exists_cache_context_test.module
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_page_top().
- */
-function session_exists_cache_context_test_page_top(array &$page_top) {
-  // Ensure this hook is invoked on every page load.
-  $page_top['#cache']['max-age'] = 0;
-
-  $request = \Drupal::request();
-  $session_exists = \Drupal::service('session_configuration')->hasSession($request);
-  $page_top['session_exists_cache_context_test'] = [
-    'label' => [
-      '#markup' => '<p>' . ($session_exists ? 'Session exists!' : 'Session does not exist!') . '</p>',
-    ],
-    'cache_context_value' => [
-      '#markup' => '<code>[session.exists]=' . \Drupal::service('cache_context.session.exists')->getContext() . '</code>',
-    ],
-  ];
-
-  $request = \Drupal::request();
-  if ($request->query->get('trigger_session')) {
-    $request->getSession()->set('session_exists_cache_context_test', TRUE);
-  }
-}
diff --git a/core/modules/system/tests/modules/session_exists_cache_context_test/src/Hook/SessionExistsCacheContextTestHooks.php b/core/modules/system/tests/modules/session_exists_cache_context_test/src/Hook/SessionExistsCacheContextTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..efd440564ae46bfa308025195de694e627c33ae4
--- /dev/null
+++ b/core/modules/system/tests/modules/session_exists_cache_context_test/src/Hook/SessionExistsCacheContextTestHooks.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\session_exists_cache_context_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for session_exists_cache_context_test.
+ */
+class SessionExistsCacheContextTestHooks {
+
+  /**
+   * Implements hook_page_top().
+   */
+  #[Hook('page_top')]
+  public function pageTop(array &$page_top) {
+    // Ensure this hook is invoked on every page load.
+    $page_top['#cache']['max-age'] = 0;
+    $request = \Drupal::request();
+    $session_exists = \Drupal::service('session_configuration')->hasSession($request);
+    $page_top['session_exists_cache_context_test'] = [
+      'label' => [
+        '#markup' => '<p>' . ($session_exists ? 'Session exists!' : 'Session does not exist!') . '</p>',
+      ],
+      'cache_context_value' => [
+        '#markup' => '<code>[session.exists]=' . \Drupal::service('cache_context.session.exists')->getContext() . '</code>',
+      ],
+    ];
+    $request = \Drupal::request();
+    if ($request->query->get('trigger_session')) {
+      $request->getSession()->set('session_exists_cache_context_test', TRUE);
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/session_test/session_test.module b/core/modules/system/tests/modules/session_test/session_test.module
deleted file mode 100644
index 5944510b4e0849cf51e2882c8a51be95f2b2db08..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/session_test/session_test.module
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\user\UserInterface;
-
-/**
- * Implements hook_user_login().
- */
-function session_test_user_login(UserInterface $account) {
-  if ($account->getAccountName() == 'session_test_user') {
-    // Exit so we can verify that the session was regenerated
-    // before hook_user_login() was called.
-    exit;
-  }
-  // Add some data in the session for retrieval testing purpose.
-  \Drupal::request()->getSession()->set("session_test_key", "foobar");
-}
diff --git a/core/modules/system/tests/modules/session_test/src/Hook/SessionTestHooks.php b/core/modules/system/tests/modules/session_test/src/Hook/SessionTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..ab0cd6c59e1f3dec3b6210fa95ef48e74649b43d
--- /dev/null
+++ b/core/modules/system/tests/modules/session_test/src/Hook/SessionTestHooks.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\session_test\Hook;
+
+use Drupal\user\UserInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for session_test.
+ */
+class SessionTestHooks {
+
+  /**
+   * Implements hook_user_login().
+   */
+  #[Hook('user_login')]
+  public function userLogin(UserInterface $account) {
+    if ($account->getAccountName() == 'session_test_user') {
+      // Exit so we can verify that the session was regenerated
+      // before hook_user_login() was called.
+      exit;
+    }
+    // Add some data in the session for retrieval testing purpose.
+    \Drupal::request()->getSession()->set("session_test_key", "foobar");
+  }
+
+}
diff --git a/core/modules/system/tests/modules/system_module_test/src/Hook/SystemModuleTestHooks.php b/core/modules/system/tests/modules/system_module_test/src/Hook/SystemModuleTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..ac5ec35d60857240fa7f245a9797e9f55a264690
--- /dev/null
+++ b/core/modules/system/tests/modules/system_module_test/src/Hook/SystemModuleTestHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\system_module_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for system_module_test.
+ */
+class SystemModuleTestHooks {
+
+  /**
+   * Implements hook_page_attachments_alter().
+   */
+  #[Hook('page_attachments_alter')]
+  public function pageAttachmentsAlter(&$page) {
+    // Remove the HTML5 mobile meta-tags.
+    $meta_tags_to_remove = ['MobileOptimized', 'HandheldFriendly', 'viewport', 'ClearType'];
+    foreach ($page['#attached']['html_head'] as $index => $parts) {
+      if (in_array($parts[1], $meta_tags_to_remove)) {
+        unset($page['#attached']['html_head'][$index]);
+      }
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/system_module_test/system_module_test.module b/core/modules/system/tests/modules/system_module_test/system_module_test.module
deleted file mode 100644
index c2661cea233b1c789089d53b0cb475a6db4c6cd0..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/system_module_test/system_module_test.module
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides System module hook implementations for testing purposes.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_page_attachments_alter().
- */
-function system_module_test_page_attachments_alter(&$page) {
-  // Remove the HTML5 mobile meta-tags.
-  $meta_tags_to_remove = ['MobileOptimized', 'HandheldFriendly', 'viewport', 'ClearType'];
-  foreach ($page['#attached']['html_head'] as $index => $parts) {
-    if (in_array($parts[1], $meta_tags_to_remove)) {
-      unset($page['#attached']['html_head'][$index]);
-    }
-  }
-
-}
diff --git a/core/modules/system/tests/modules/system_test/src/Hook/SystemTestHooks.php b/core/modules/system/tests/modules/system_test/src/Hook/SystemTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..31d7da782bf7a3623a33d164017dd319fcc0ac4b
--- /dev/null
+++ b/core/modules/system/tests/modules/system_test/src/Hook/SystemTestHooks.php
@@ -0,0 +1,154 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\system_test\Hook;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for system_test.
+ */
+class SystemTestHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.system_test':
+        $output = '';
+        $output .= '<h2>' . t('Test Help Page') . '</h2>';
+        $output .= '<p>' . t('This is a test help page for the system_test module for the purpose of testing if the "Help" link displays properly.') . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_modules_installed().
+   */
+  #[Hook('modules_installed')]
+  public function modulesInstalled($modules) {
+    if (\Drupal::state()->get('system_test.verbose_module_hooks')) {
+      foreach ($modules as $module) {
+        \Drupal::messenger()->addStatus(t('hook_modules_installed fired for @module', ['@module' => $module]));
+      }
+    }
+  }
+
+  /**
+   * Implements hook_modules_uninstalled().
+   */
+  #[Hook('modules_uninstalled')]
+  public function modulesUninstalled($modules, $is_syncing) {
+    if (\Drupal::state()->get('system_test.verbose_module_hooks')) {
+      foreach ($modules as $module) {
+        \Drupal::messenger()->addStatus(t('hook_modules_uninstalled fired for @module', ['@module' => $module]));
+      }
+    }
+    // Save the config.installer isSyncing() value to state to check that it is
+    // correctly set when installing module during config import.
+    \Drupal::state()->set('system_test_modules_uninstalled_config_installer_syncing', \Drupal::service('config.installer')->isSyncing());
+    // Save the $is_syncing parameter value to state to check that it is correctly
+    // set when installing module during config import.
+    \Drupal::state()->set('system_test_modules_uninstalled_syncing_param', $is_syncing);
+  }
+
+  /**
+   * Implements hook_system_info_alter().
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(&$info, Extension $file, $type) {
+    // We need a static otherwise the last test will fail to alter common_test.
+    static $test;
+    if (($dependencies = \Drupal::state()->get('system_test.dependencies')) || $test) {
+      if ($file->getName() == 'module_test') {
+        $info['hidden'] = FALSE;
+        $info['dependencies'][] = array_shift($dependencies);
+        \Drupal::state()->set('system_test.dependencies', $dependencies);
+        $test = TRUE;
+      }
+      if ($file->getName() == 'common_test') {
+        $info['hidden'] = FALSE;
+        $info['version'] = '8.x-2.4-beta3';
+      }
+    }
+    // Make the system_dependencies_test visible by default.
+    if ($file->getName() == 'system_dependencies_test') {
+      $info['hidden'] = FALSE;
+    }
+    if (in_array($file->getName(), [
+      'system_incompatible_module_version_dependencies_test',
+      'system_incompatible_core_version_dependencies_test',
+      'system_incompatible_module_version_test',
+    ])) {
+      $info['hidden'] = FALSE;
+    }
+    if ($file->getName() == 'requirements1_test' || $file->getName() == 'requirements2_test') {
+      $info['hidden'] = FALSE;
+    }
+    if ($file->getName() == 'system_test') {
+      $info['hidden'] = \Drupal::state()->get('system_test.module_hidden', TRUE);
+    }
+  }
+
+  /**
+   * Implements hook_page_attachments().
+   */
+  #[Hook('page_attachments')]
+  public function pageAttachments(array &$page) {
+    // Used by FrontPageTestCase to get the results of
+    // \Drupal::service('path.matcher')->isFrontPage().
+    $frontpage = \Drupal::state()->get('system_test.front_page_output', 0);
+    if ($frontpage && \Drupal::service('path.matcher')->isFrontPage()) {
+      \Drupal::messenger()->addStatus(t('On front page.'));
+    }
+  }
+
+  /**
+   * Implements hook_filetransfer_info().
+   */
+  #[Hook('filetransfer_info')]
+  public function filetransferInfo() {
+    return [
+      'system_test' => [
+        'title' => t('System Test FileTransfer'),
+        'class' => 'Drupal\system_test\MockFileTransfer',
+        'weight' => -10,
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_module_preinstall().
+   */
+  #[Hook('module_preinstall')]
+  public function modulePreinstall($module, bool $is_syncing) {
+    \Drupal::messenger()->addStatus('system_test_preinstall_module called');
+    \Drupal::state()->set('system_test_preinstall_module', $module);
+    // Save the config.installer isSyncing() value to state to check that it is
+    // correctly set when installing module during config import.
+    \Drupal::state()->set('system_test_preinstall_module_config_installer_syncing', \Drupal::service('config.installer')->isSyncing());
+    // Save the $is_syncing parameter value to state to check that it is correctly
+    // set when installing module during config import.
+    \Drupal::state()->set('system_test_preinstall_module_syncing_param', $is_syncing);
+  }
+
+  /**
+   * Implements hook_module_preuninstall().
+   */
+  #[Hook('module_preuninstall')]
+  public function modulePreuninstall($module, bool $is_syncing) {
+    \Drupal::state()->set('system_test_preuninstall_module', $module);
+    // Save the config.installer isSyncing() value to state to check that it is
+    // correctly set when uninstalling module during config import.
+    \Drupal::state()->set('system_test_preuninstall_module_config_installer_syncing', \Drupal::service('config.installer')->isSyncing());
+    // Save the $is_syncing parameter value to state to check that it is correctly
+    // set when installing module during config import.
+    \Drupal::state()->set('system_test_preuninstall_module_syncing_param', $is_syncing);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module
index 6ba1e0c5e26407a06eaca39c85b9520cffb9a7ad..bf00733722ec5e9d5862457d15b63243172267d1 100644
--- a/core/modules/system/tests/modules/system_test/system_test.module
+++ b/core/modules/system/tests/modules/system_test/system_test.module
@@ -7,102 +7,6 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Extension\Extension;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function system_test_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.system_test':
-      $output = '';
-      $output .= '<h2>' . t('Test Help Page') . '</h2>';
-      $output .= '<p>' . t('This is a test help page for the system_test module for the purpose of testing if the "Help" link displays properly.') . '</p>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_modules_installed().
- */
-function system_test_modules_installed($modules) {
-  if (\Drupal::state()->get('system_test.verbose_module_hooks')) {
-    foreach ($modules as $module) {
-      \Drupal::messenger()->addStatus(t('hook_modules_installed fired for @module', ['@module' => $module]));
-    }
-  }
-}
-
-/**
- * Implements hook_modules_uninstalled().
- */
-function system_test_modules_uninstalled($modules, $is_syncing) {
-  if (\Drupal::state()->get('system_test.verbose_module_hooks')) {
-    foreach ($modules as $module) {
-      \Drupal::messenger()->addStatus(t('hook_modules_uninstalled fired for @module', ['@module' => $module]));
-    }
-  }
-
-  // Save the config.installer isSyncing() value to state to check that it is
-  // correctly set when installing module during config import.
-  \Drupal::state()->set('system_test_modules_uninstalled_config_installer_syncing', \Drupal::service('config.installer')->isSyncing());
-
-  // Save the $is_syncing parameter value to state to check that it is correctly
-  // set when installing module during config import.
-  \Drupal::state()->set('system_test_modules_uninstalled_syncing_param', $is_syncing);
-}
-
-/**
- * Implements hook_system_info_alter().
- */
-function system_test_system_info_alter(&$info, Extension $file, $type) {
-  // We need a static otherwise the last test will fail to alter common_test.
-  static $test;
-  if (($dependencies = \Drupal::state()->get('system_test.dependencies')) || $test) {
-    if ($file->getName() == 'module_test') {
-      $info['hidden'] = FALSE;
-      $info['dependencies'][] = array_shift($dependencies);
-      \Drupal::state()->set('system_test.dependencies', $dependencies);
-      $test = TRUE;
-    }
-    if ($file->getName() == 'common_test') {
-      $info['hidden'] = FALSE;
-      $info['version'] = '8.x-2.4-beta3';
-    }
-  }
-
-  // Make the system_dependencies_test visible by default.
-  if ($file->getName() == 'system_dependencies_test') {
-    $info['hidden'] = FALSE;
-  }
-  if (in_array($file->getName(), [
-    'system_incompatible_module_version_dependencies_test',
-    'system_incompatible_core_version_dependencies_test',
-    'system_incompatible_module_version_test',
-  ])) {
-    $info['hidden'] = FALSE;
-  }
-  if ($file->getName() == 'requirements1_test' || $file->getName() == 'requirements2_test') {
-    $info['hidden'] = FALSE;
-  }
-  if ($file->getName() == 'system_test') {
-    $info['hidden'] = \Drupal::state()->get('system_test.module_hidden', TRUE);
-  }
-}
-
-/**
- * Implements hook_page_attachments().
- */
-function system_test_page_attachments(array &$page) {
-  // Used by FrontPageTestCase to get the results of
-  // \Drupal::service('path.matcher')->isFrontPage().
-  $frontpage = \Drupal::state()->get('system_test.front_page_output', 0);
-  if ($frontpage && \Drupal::service('path.matcher')->isFrontPage()) {
-    \Drupal::messenger()->addStatus(t('On front page.'));
-  }
-}
-
 /**
  * Dummy shutdown function which registers another shutdown function.
  */
@@ -125,47 +29,3 @@ function _system_test_second_shutdown_function($arg1, $arg2) {
   // \Drupal\Core\Utility\Error::renderExceptionSafe() if possible.
   throw new Exception('Drupal is <blink>awesome</blink>.');
 }
-
-/**
- * Implements hook_filetransfer_info().
- */
-function system_test_filetransfer_info() {
-  return [
-    'system_test' => [
-      'title' => t('System Test FileTransfer'),
-      'class' => 'Drupal\system_test\MockFileTransfer',
-      'weight' => -10,
-    ],
-  ];
-}
-
-/**
- * Implements hook_module_preinstall().
- */
-function system_test_module_preinstall($module, bool $is_syncing) {
-  \Drupal::messenger()->addStatus('system_test_preinstall_module called');
-  \Drupal::state()->set('system_test_preinstall_module', $module);
-
-  // Save the config.installer isSyncing() value to state to check that it is
-  // correctly set when installing module during config import.
-  \Drupal::state()->set('system_test_preinstall_module_config_installer_syncing', \Drupal::service('config.installer')->isSyncing());
-
-  // Save the $is_syncing parameter value to state to check that it is correctly
-  // set when installing module during config import.
-  \Drupal::state()->set('system_test_preinstall_module_syncing_param', $is_syncing);
-}
-
-/**
- * Implements hook_module_preuninstall().
- */
-function system_test_module_preuninstall($module, bool $is_syncing) {
-  \Drupal::state()->set('system_test_preuninstall_module', $module);
-
-  // Save the config.installer isSyncing() value to state to check that it is
-  // correctly set when uninstalling module during config import.
-  \Drupal::state()->set('system_test_preuninstall_module_config_installer_syncing', \Drupal::service('config.installer')->isSyncing());
-
-  // Save the $is_syncing parameter value to state to check that it is correctly
-  // set when installing module during config import.
-  \Drupal::state()->set('system_test_preuninstall_module_syncing_param', $is_syncing);
-}
diff --git a/core/modules/system/tests/modules/theme_page_test/src/Hook/ThemePageTestHooks.php b/core/modules/system/tests/modules/theme_page_test/src/Hook/ThemePageTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..247e958a3a338f5139f5fc43672cea751ffb0e2f
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_page_test/src/Hook/ThemePageTestHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\theme_page_test\Hook;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for theme_page_test.
+ */
+class ThemePageTestHooks {
+
+  /**
+   * Implements hook_system_info_alter().
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(&$info, Extension $file, $type) {
+    // Make sure that all themes are visible on the Appearance form.
+    if ($type === 'theme') {
+      unset($info['hidden']);
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/theme_page_test/theme_page_test.module b/core/modules/system/tests/modules/theme_page_test/theme_page_test.module
deleted file mode 100644
index 7a658902a5838525fbefb4cf9830050371b3e6f1..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/theme_page_test/theme_page_test.module
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Extension\Extension;
-
-/**
- * Implements hook_system_info_alter().
- */
-function theme_page_test_system_info_alter(&$info, Extension $file, $type) {
-  // Make sure that all themes are visible on the Appearance form.
-  if ($type === 'theme') {
-    unset($info['hidden']);
-  }
-}
diff --git a/core/modules/system/tests/modules/theme_suggestions_test/src/Hook/ThemeSuggestionsTestHooks.php b/core/modules/system/tests/modules/theme_suggestions_test/src/Hook/ThemeSuggestionsTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..19895acb91955c644a17d4c55d023ac65974cfff
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_suggestions_test/src/Hook/ThemeSuggestionsTestHooks.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\theme_suggestions_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for theme_suggestions_test.
+ */
+class ThemeSuggestionsTestHooks {
+
+  /**
+   * Implements hook_theme_suggestions_alter().
+   */
+  #[Hook('theme_suggestions_alter')]
+  public function themeSuggestionsAlter(array &$suggestions, array &$variables, $hook) {
+    \Drupal::messenger()->addStatus('theme_suggestions_test_theme_suggestions_alter' . '() executed.');
+    if ($hook == 'theme_test_general_suggestions') {
+      $suggestions[] = $hook . '__module_override';
+      $variables['module_hook'] = 'theme_suggestions_test_theme_suggestions_alter';
+    }
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK_alter().
+   */
+  #[Hook('theme_suggestions_theme_test_suggestions_alter')]
+  public function themeSuggestionsThemeTestSuggestionsAlter(array &$suggestions, array $variables) {
+    \Drupal::messenger()->addStatus('theme_suggestions_test_theme_suggestions_theme_test_suggestions_alter' . '() executed.');
+    $suggestions[] = 'theme_test_suggestions__module_override';
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK_alter().
+   */
+  #[Hook('theme_suggestions_theme_test_specific_suggestions_alter')]
+  public function themeSuggestionsThemeTestSpecificSuggestionsAlter(array &$suggestions, array $variables) {
+    $suggestions[] = 'theme_test_specific_suggestions__variant__foo';
+  }
+
+}
diff --git a/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module b/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module
deleted file mode 100644
index 3c7a55ce426960fcec3ad76faf6f4c4ae520f09b..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/theme_suggestions_test/theme_suggestions_test.module
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-/**
- * @file
- * Support module for testing theme suggestions.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_theme_suggestions_alter().
- */
-function theme_suggestions_test_theme_suggestions_alter(array &$suggestions, array &$variables, $hook) {
-  \Drupal::messenger()->addStatus(__FUNCTION__ . '() executed.');
-  if ($hook == 'theme_test_general_suggestions') {
-    $suggestions[] = $hook . '__module_override';
-    $variables['module_hook'] = 'theme_suggestions_test_theme_suggestions_alter';
-  }
-}
-
-/**
- * Implements hook_theme_suggestions_HOOK_alter().
- */
-function theme_suggestions_test_theme_suggestions_theme_test_suggestions_alter(array &$suggestions, array $variables) {
-  \Drupal::messenger()->addStatus(__FUNCTION__ . '() executed.');
-  $suggestions[] = 'theme_test_suggestions__module_override';
-}
-
-/**
- * Implements hook_theme_suggestions_HOOK_alter().
- */
-function theme_suggestions_test_theme_suggestions_theme_test_specific_suggestions_alter(array &$suggestions, array $variables) {
-  $suggestions[] = 'theme_test_specific_suggestions__variant__foo';
-}
diff --git a/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestHooks.php b/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..3e93c19c2f92e9f7028893341c1b96c2c4ef3eac
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestHooks.php
@@ -0,0 +1,120 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\theme_test\Hook;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for theme_test.
+ */
+class ThemeTestHooks {
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme($existing, $type, $theme, $path) : array {
+    $items['theme_test'] = ['file' => 'theme_test.inc', 'variables' => ['foo' => '']];
+    $items['theme_test_template_test'] = ['template' => 'theme_test.template_test'];
+    $items['theme_test_template_test_2'] = ['template' => 'theme_test.template_test'];
+    $items['theme_test_suggestion_provided'] = ['variables' => []];
+    $items['theme_test_specific_suggestions'] = ['variables' => []];
+    $items['theme_test_suggestions'] = ['variables' => []];
+    $items['theme_test_general_suggestions'] = ['variables' => ['module_hook' => 'theme_test_theme', 'theme_hook' => 'none']];
+    $items['theme_test_foo'] = ['variables' => ['foo' => NULL]];
+    $items['theme_test_render_element'] = ['render element' => 'elements'];
+    $items['theme_test_render_element_children'] = ['render element' => 'element'];
+    $items['theme_test_preprocess_suggestions'] = ['variables' => ['foo' => '', 'bar' => '']];
+    $items['theme_test_preprocess_callback'] = ['variables' => ['foo' => '']];
+    $items['theme_test_registered_by_module'] = ['render element' => 'content', 'base hook' => 'container'];
+    $items['theme_test_theme_class'] = ['variables' => ['message' => '']];
+    $items['theme_test_deprecations_preprocess'] = [
+      'variables' => [
+        'foo' => '',
+        'bar' => '',
+        'gaz' => '',
+        'set_var' => '',
+        'for_var' => '',
+        'contents' => [],
+      ],
+    ];
+    $items['theme_test_deprecations_child'] = ['variables' => ['foo' => '', 'bar' => '', 'gaz' => '']];
+    $items['theme_test_deprecations_hook_theme'] = [
+      'variables' => [
+        'foo' => '',
+        'bar' => '',
+        'deprecations' => [
+          'foo' => "'foo' is deprecated in drupal:X.0.0 and is removed from drupal:Y.0.0. Use 'new_foo' instead. See https://www.example.com.",
+          'bar' => "'bar' is deprecated in drupal:X.0.0 and is removed from drupal:Y.0.0. Use 'new_bar' instead. See https://www.example.com.",
+        ],
+      ],
+    ];
+    return $items;
+  }
+
+  /**
+   * Implements hook_theme_registry_alter().
+   */
+  #[Hook('theme_registry_alter')]
+  public function themeRegistryAlter(&$registry) {
+    $registry['theme_test_preprocess_callback']['preprocess functions'][] = ['\Drupal\theme_test\ThemeTestPreprocess', 'preprocess'];
+  }
+
+  /**
+   * Implements hook_page_bottom().
+   */
+  #[Hook('page_bottom')]
+  public function pageBottom(array &$page_bottom) {
+    $page_bottom['theme_test_page_bottom'] = ['#markup' => 'theme test page bottom markup'];
+  }
+
+  /**
+   * Implements hook_theme_suggestions_alter().
+   */
+  #[Hook('theme_suggestions_alter')]
+  public function themeSuggestionsAlter(array &$suggestions, array $variables, $hook) {
+    \Drupal::messenger()->addStatus('theme_test_theme_suggestions_alter' . '() executed for ' . $hook . '.');
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK_alter().
+   */
+  #[Hook('theme_suggestions_theme_test_suggestions_alter')]
+  public function themeSuggestionsThemeTestSuggestionsAlter(array &$suggestions, array $variables) {
+    \Drupal::messenger()->addStatus('theme_test_theme_suggestions_theme_test_suggestions_alter' . '() executed.');
+  }
+
+  /**
+   * Implements hook_system_info_alter().
+   *
+   * @see \Drupal\system\Tests\Theme\ThemeInfoTest::testChanges()
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(array &$info, Extension $file, $type) {
+    if ($type == 'theme' && $file->getName() == 'test_theme' && \Drupal::state()->get('theme_test.modify_info_files')) {
+      // Add a library to see if the system picks it up.
+      $info += ['libraries' => []];
+      $info['libraries'][] = 'core/once';
+    }
+  }
+
+  /**
+   * Implements hook_library_info_alter().
+   */
+  #[Hook('library_info_alter')]
+  public function libraryInfoAlter(array &$libraries, string $extension) : void {
+    // Allow test code to simulate library changes in a particular extension by
+    // setting a state key in the form `theme_test_library_info_alter $extension`,
+    // whose values is an array containing everything that should be recursively
+    // merged into the given extension's library definitions.
+    $info = \Drupal::state()->get('theme_test_library_info_alter' . " {$extension}");
+    if (is_array($info)) {
+      $libraries = NestedArray::mergeDeep($libraries, $info);
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module
index 1f4dca68c3d52162d4bd4d65750fdb08285f6528..dd430ba201d60b19591ea44093af4fc491a026cb 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.module
+++ b/core/modules/system/tests/modules/theme_test/theme_test.module
@@ -7,101 +7,6 @@
 
 declare(strict_types=1);
 
-use Drupal\Component\Utility\NestedArray;
-use Drupal\Core\Extension\Extension;
-
-/**
- * Implements hook_theme().
- */
-function theme_test_theme($existing, $type, $theme, $path): array {
-  $items['theme_test'] = [
-    'file' => 'theme_test.inc',
-    'variables' => ['foo' => ''],
-  ];
-  $items['theme_test_template_test'] = [
-    'template' => 'theme_test.template_test',
-  ];
-  $items['theme_test_template_test_2'] = [
-    'template' => 'theme_test.template_test',
-  ];
-  $items['theme_test_suggestion_provided'] = [
-    'variables' => [],
-  ];
-  $items['theme_test_specific_suggestions'] = [
-    'variables' => [],
-  ];
-  $items['theme_test_suggestions'] = [
-    'variables' => [],
-  ];
-  $items['theme_test_general_suggestions'] = [
-    'variables' => ['module_hook' => 'theme_test_theme', 'theme_hook' => 'none'],
-  ];
-  $items['theme_test_foo'] = [
-    'variables' => ['foo' => NULL],
-  ];
-  $items['theme_test_render_element'] = [
-    'render element' => 'elements',
-  ];
-  $items['theme_test_render_element_children'] = [
-    'render element' => 'element',
-  ];
-  $items['theme_test_preprocess_suggestions'] = [
-    'variables' => [
-      'foo' => '',
-      'bar' => '',
-    ],
-  ];
-  $items['theme_test_preprocess_callback'] = [
-    'variables' => [
-      'foo' => '',
-    ],
-  ];
-  $items['theme_test_registered_by_module'] = [
-    'render element' => 'content',
-    'base hook' => 'container',
-  ];
-  $items['theme_test_theme_class'] = [
-    'variables' => [
-      'message' => '',
-    ],
-  ];
-  $items['theme_test_deprecations_preprocess'] = [
-    'variables' => [
-      'foo' => '',
-      'bar' => '',
-      'gaz' => '',
-      'set_var' => '',
-      'for_var' => '',
-      'contents' => [],
-    ],
-  ];
-  $items['theme_test_deprecations_child'] = [
-    'variables' => [
-      'foo' => '',
-      'bar' => '',
-      'gaz' => '',
-    ],
-  ];
-  $items['theme_test_deprecations_hook_theme'] = [
-    'variables' => [
-      'foo' => '',
-      'bar' => '',
-      'deprecations' => [
-        'foo' => "'foo' is deprecated in drupal:X.0.0 and is removed from drupal:Y.0.0. Use 'new_foo' instead. See https://www.example.com.",
-        'bar' => "'bar' is deprecated in drupal:X.0.0 and is removed from drupal:Y.0.0. Use 'new_bar' instead. See https://www.example.com.",
-      ],
-    ],
-  ];
-  return $items;
-}
-
-/**
- * Implements hook_theme_registry_alter().
- */
-function theme_test_theme_registry_alter(&$registry) {
-  $registry['theme_test_preprocess_callback']['preprocess functions'][] = ['\Drupal\theme_test\ThemeTestPreprocess', 'preprocess'];
-}
-
 /**
  * Implements hook_preprocess_HOOK() for HTML document templates.
  */
@@ -112,13 +17,6 @@ function theme_test_preprocess_html(&$variables) {
   $variables['attributes']['theme_test_page_variable'] = 'Page variable is an array.';
 }
 
-/**
- * Implements hook_page_bottom().
- */
-function theme_test_page_bottom(array &$page_bottom) {
-  $page_bottom['theme_test_page_bottom'] = ['#markup' => 'theme test page bottom markup'];
-}
-
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
@@ -160,33 +58,6 @@ function theme_test_theme_suggestions_theme_test_suggestion_provided(array $vari
   return ['theme_test_suggestion_provided__foo'];
 }
 
-/**
- * Implements hook_theme_suggestions_alter().
- */
-function theme_test_theme_suggestions_alter(array &$suggestions, array $variables, $hook) {
-  \Drupal::messenger()->addStatus(__FUNCTION__ . '() executed for ' . $hook . '.');
-}
-
-/**
- * Implements hook_theme_suggestions_HOOK_alter().
- */
-function theme_test_theme_suggestions_theme_test_suggestions_alter(array &$suggestions, array $variables) {
-  \Drupal::messenger()->addStatus(__FUNCTION__ . '() executed.');
-}
-
-/**
- * Implements hook_system_info_alter().
- *
- * @see \Drupal\system\Tests\Theme\ThemeInfoTest::testChanges()
- */
-function theme_test_system_info_alter(array &$info, Extension $file, $type) {
-  if ($type == 'theme' && $file->getName() == 'test_theme' && \Drupal::state()->get('theme_test.modify_info_files')) {
-    // Add a library to see if the system picks it up.
-    $info += ['libraries' => []];
-    $info['libraries'][] = 'core/once';
-  }
-}
-
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
@@ -214,18 +85,3 @@ function template_preprocess_theme_test_registered_by_module() {
 function template_preprocess_theme_test_deprecations_preprocess(array &$variables) {
   $variables = array_merge($variables, \Drupal::state()->get('theme_test.theme_test_deprecations_preprocess'));
 }
-
-/**
- * Implements hook_library_info_alter().
- */
-function theme_test_library_info_alter(array &$libraries, string $extension): void {
-  // Allow test code to simulate library changes in a particular extension by
-  // setting a state key in the form `theme_test_library_info_alter $extension`,
-  // whose values is an array containing everything that should be recursively
-  // merged into the given extension's library definitions.
-  $info = \Drupal::state()->get(__FUNCTION__ . " $extension");
-
-  if (is_array($info)) {
-    $libraries = NestedArray::mergeDeep($libraries, $info);
-  }
-}
diff --git a/core/modules/system/tests/modules/twig_extension_test/src/Hook/TwigExtensionTestHooks.php b/core/modules/system/tests/modules/twig_extension_test/src/Hook/TwigExtensionTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..3c255419d187de10663739d79ef353da54dba09f
--- /dev/null
+++ b/core/modules/system/tests/modules/twig_extension_test/src/Hook/TwigExtensionTestHooks.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\twig_extension_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for twig_extension_test.
+ */
+class TwigExtensionTestHooks {
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme($existing, $type, $theme, $path) : array {
+    return [
+      'twig_extension_test_filter' => [
+        'variables' => [
+          'message' => NULL,
+          'safe_join_items' => NULL,
+        ],
+        'template' => 'twig_extension_test.filter',
+      ],
+      'twig_extension_test_function' => [
+        'render element' => 'element',
+        'template' => 'twig_extension_test.function',
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/system/tests/modules/twig_extension_test/twig_extension_test.module b/core/modules/system/tests/modules/twig_extension_test/twig_extension_test.module
deleted file mode 100644
index 44e8d9ecf8e191d14802076bbe538db40bd93d38..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/twig_extension_test/twig_extension_test.module
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for Twig extension tests.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_theme().
- */
-function twig_extension_test_theme($existing, $type, $theme, $path): array {
-  return [
-    'twig_extension_test_filter' => [
-      'variables' => ['message' => NULL, 'safe_join_items' => NULL],
-      'template' => 'twig_extension_test.filter',
-    ],
-    'twig_extension_test_function' => [
-      'render element' => 'element',
-      'template' => 'twig_extension_test.function',
-    ],
-  ];
-}
diff --git a/core/modules/system/tests/modules/twig_theme_test/src/Hook/TwigThemeTestHooks.php b/core/modules/system/tests/modules/twig_theme_test/src/Hook/TwigThemeTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..b71dc37e35cdc864b825295441a9a7d5e64f2e49
--- /dev/null
+++ b/core/modules/system/tests/modules/twig_theme_test/src/Hook/TwigThemeTestHooks.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\twig_theme_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for twig_theme_test.
+ */
+class TwigThemeTestHooks {
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme($existing, $type, $theme, $path) : array {
+    $items['twig_theme_test_filter'] = [
+      'variables' => [
+        'quote' => [],
+        'attributes' => [],
+      ],
+      'template' => 'twig_theme_test.filter',
+    ];
+    $items['twig_theme_test_php_variables'] = ['template' => 'twig_theme_test.php_variables'];
+    $items['twig_theme_test_trans'] = ['variables' => [], 'template' => 'twig_theme_test.trans'];
+    $items['twig_theme_test_placeholder_outside_trans'] = [
+      'variables' => [
+        'var' => '',
+      ],
+      'template' => 'twig_theme_test.placeholder_outside_trans',
+    ];
+    $items['twig_namespace_test'] = ['variables' => [], 'template' => 'twig_namespace_test'];
+    $items['twig_registry_loader_test'] = ['variables' => []];
+    $items['twig_registry_loader_test_include'] = ['variables' => []];
+    $items['twig_registry_loader_test_extend'] = ['variables' => []];
+    $items['twig_raw_test'] = ['variables' => ['script' => '']];
+    $items['twig_autoescape_test'] = ['variables' => ['script' => '']];
+    $items['twig_theme_test_url_generator'] = ['variables' => [], 'template' => 'twig_theme_test.url_generator'];
+    $items['twig_theme_test_link_generator'] = [
+      'variables' => [
+        'test_url' => NULL,
+        'test_url_attribute' => NULL,
+        'attributes' => [],
+      ],
+      'template' => 'twig_theme_test.link_generator',
+    ];
+    $items['twig_theme_test_url_to_string'] = [
+      'variables' => [
+        'test_url' => NULL,
+      ],
+      'template' => 'twig_theme_test.url_to_string',
+    ];
+    $items['twig_theme_test_file_url'] = ['variables' => [], 'template' => 'twig_theme_test.file_url'];
+    $items['twig_theme_test_attach_library'] = ['variables' => [], 'template' => 'twig_theme_test.attach_library'];
+    $items['twig_theme_test_renderable'] = [
+      'variables' => [
+        'renderable' => NULL,
+      ],
+      'template' => 'twig_theme_test.renderable',
+    ];
+    $items['twig_theme_test_embed_tag'] = ['variables' => [], 'template' => 'twig_theme_test.embed_tag'];
+    $items['twig_theme_test_dump'] = ['variables' => [], 'template' => 'twig_theme_test.dump'];
+    return $items;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module
index 70893327557bed75f74afa17ceb63e94a565d523..e74aa96183784ef20b40b3dd39f5d381d69380d7 100644
--- a/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module
+++ b/core/modules/system/tests/modules/twig_theme_test/twig_theme_test.module
@@ -7,85 +7,6 @@
 
 declare(strict_types=1);
 
-/**
- * Implements hook_theme().
- */
-function twig_theme_test_theme($existing, $type, $theme, $path): array {
-  $items['twig_theme_test_filter'] = [
-    'variables' => ['quote' => [], 'attributes' => []],
-    'template' => 'twig_theme_test.filter',
-  ];
-  $items['twig_theme_test_php_variables'] = [
-    'template' => 'twig_theme_test.php_variables',
-  ];
-  $items['twig_theme_test_trans'] = [
-    'variables' => [],
-    'template' => 'twig_theme_test.trans',
-  ];
-  $items['twig_theme_test_placeholder_outside_trans'] = [
-    'variables' => ['var' => ''],
-    'template' => 'twig_theme_test.placeholder_outside_trans',
-  ];
-  $items['twig_namespace_test'] = [
-    'variables' => [],
-    'template' => 'twig_namespace_test',
-  ];
-  $items['twig_registry_loader_test'] = [
-    'variables' => [],
-  ];
-  $items['twig_registry_loader_test_include'] = [
-    'variables' => [],
-  ];
-  $items['twig_registry_loader_test_extend'] = [
-    'variables' => [],
-  ];
-  $items['twig_raw_test'] = [
-    'variables' => ['script' => ''],
-  ];
-  $items['twig_autoescape_test'] = [
-    'variables' => ['script' => ''],
-  ];
-  $items['twig_theme_test_url_generator'] = [
-    'variables' => [],
-    'template' => 'twig_theme_test.url_generator',
-  ];
-  $items['twig_theme_test_link_generator'] = [
-    'variables' => [
-      'test_url' => NULL,
-      'test_url_attribute' => NULL,
-      'attributes' => [],
-    ],
-    'template' => 'twig_theme_test.link_generator',
-  ];
-  $items['twig_theme_test_url_to_string'] = [
-    'variables' => ['test_url' => NULL],
-    'template' => 'twig_theme_test.url_to_string',
-  ];
-  $items['twig_theme_test_file_url'] = [
-    'variables' => [],
-    'template' => 'twig_theme_test.file_url',
-  ];
-  $items['twig_theme_test_attach_library'] = [
-    'variables' => [],
-    'template' => 'twig_theme_test.attach_library',
-  ];
-  $items['twig_theme_test_renderable'] = [
-    'variables' => [
-      'renderable' => NULL,
-    ],
-    'template' => 'twig_theme_test.renderable',
-  ];
-  $items['twig_theme_test_embed_tag'] = [
-    'variables' => [],
-    'template' => 'twig_theme_test.embed_tag',
-  ];
-  $items['twig_theme_test_dump'] = [
-    'variables' => [],
-    'template' => 'twig_theme_test.dump',
-  ];
-  return $items;
-}
-
 /**
  * Helper function to test PHP variables in the Twig engine.
  */
diff --git a/core/modules/system/tests/modules/unique_field_constraint_test/src/Hook/UniqueFieldConstraintTestHooks.php b/core/modules/system/tests/modules/unique_field_constraint_test/src/Hook/UniqueFieldConstraintTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..5f3921e183d6c9ddd2c7ffacc09ebc68596a8d55
--- /dev/null
+++ b/core/modules/system/tests/modules/unique_field_constraint_test/src/Hook/UniqueFieldConstraintTestHooks.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\unique_field_constraint_test\Hook;
+
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for unique_field_constraint_test.
+ */
+class UniqueFieldConstraintTestHooks {
+
+  /**
+   * Implements hook_entity_base_field_info_alter().
+   */
+  #[Hook('entity_base_field_info_alter')]
+  public function entityBaseFieldInfoAlter(&$fields, EntityTypeInterface $entity_type) {
+    if ($entity_type->id() === 'entity_test_string_id') {
+      /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
+      $fields['name']->addConstraint('UniqueField');
+    }
+    if ($entity_type->id() === 'entity_test') {
+      /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
+      $fields['name']->addConstraint('UniqueField');
+    }
+  }
+
+  /**
+   * Implements hook_query_entity_test_access_alter().
+   */
+  #[Hook('query_entity_test_access_alter')]
+  public function queryEntityTestAccessAlter(AlterableInterface $query) {
+    // Set an impossible condition to filter out all entities.
+    /** @var \Drupal\Core\Database\Query\Select|\Drupal\Core\Database\Query\AlterableInterface $query */
+    $query->condition('entity_test.id', 0);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/unique_field_constraint_test/unique_field_constraint_test.module b/core/modules/system/tests/modules/unique_field_constraint_test/unique_field_constraint_test.module
deleted file mode 100644
index 0f47b136d320697dfd77c666fba1a8086cc36e82..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/unique_field_constraint_test/unique_field_constraint_test.module
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains unique_field_constraint_test.module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Database\Query\AlterableInterface;
-
-/**
- * Implements hook_entity_base_field_info_alter().
- */
-function unique_field_constraint_test_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
-  if ($entity_type->id() === 'entity_test_string_id') {
-    /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
-    $fields['name']->addConstraint('UniqueField');
-  }
-  if ($entity_type->id() === 'entity_test') {
-    /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
-    $fields['name']->addConstraint('UniqueField');
-  }
-}
-
-/**
- * Implements hook_query_entity_test_access_alter().
- */
-function unique_field_constraint_test_query_entity_test_access_alter(AlterableInterface $query) {
-  // Set an impossible condition to filter out all entities.
-  /** @var \Drupal\Core\Database\Query\Select|\Drupal\Core\Database\Query\AlterableInterface $query */
-  $query->condition('entity_test.id', 0);
-}
diff --git a/core/modules/system/tests/modules/update_script_test/src/Hook/UpdateScriptTestHooks.php b/core/modules/system/tests/modules/update_script_test/src/Hook/UpdateScriptTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..b88d617fca0249e883fc37ef59b004fd4fab25aa
--- /dev/null
+++ b/core/modules/system/tests/modules/update_script_test/src/Hook/UpdateScriptTestHooks.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\update_script_test\Hook;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for update_script_test.
+ */
+class UpdateScriptTestHooks {
+
+  /**
+   * Implements hook_cache_flush().
+   *
+   * This sets a message to confirm that all caches are cleared whenever
+   * update.php completes.
+   *
+   * @see UpdateScriptFunctionalTest::testRequirements()
+   */
+  #[Hook('cache_flush')]
+  public function cacheFlush() {
+    \Drupal::messenger()->addStatus(t('hook_cache_flush() invoked for update_script_test.module.'));
+  }
+
+  /**
+   * Implements hook_system_info_alter().
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(array &$info, Extension $file, $type) {
+    $new_info = \Drupal::state()->get('update_script_test.system_info_alter');
+    if ($new_info) {
+      if ($file->getName() == 'update_script_test') {
+        $info = $new_info + $info;
+      }
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/update_script_test/update_script_test.module b/core/modules/system/tests/modules/update_script_test/update_script_test.module
deleted file mode 100644
index 0a8ea41d867bcaffbc6ae2d4c4818411c2d28f8f..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/update_script_test/update_script_test.module
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-/**
- * @file
- * This file provides testing functionality for update.php.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Extension\Extension;
-
-/**
- * Implements hook_cache_flush().
- *
- * This sets a message to confirm that all caches are cleared whenever
- * update.php completes.
- *
- * @see UpdateScriptFunctionalTest::testRequirements()
- */
-function update_script_test_cache_flush() {
-  \Drupal::messenger()->addStatus(t('hook_cache_flush() invoked for update_script_test.module.'));
-}
-
-/**
- * Implements hook_system_info_alter().
- */
-function update_script_test_system_info_alter(array &$info, Extension $file, $type) {
-  $new_info = \Drupal::state()->get('update_script_test.system_info_alter');
-  if ($new_info) {
-    if ($file->getName() == 'update_script_test') {
-      $info = $new_info + $info;
-    }
-  }
-}
diff --git a/core/modules/system/tests/modules/update_test_broken_theme_hook/src/Hook/UpdateTestBrokenThemeHookHooks.php b/core/modules/system/tests/modules/update_test_broken_theme_hook/src/Hook/UpdateTestBrokenThemeHookHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..fb77696942e056852a19d8d4e90f47359bff40a2
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_broken_theme_hook/src/Hook/UpdateTestBrokenThemeHookHooks.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\update_test_broken_theme_hook\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for update_test_broken_theme_hook.
+ */
+class UpdateTestBrokenThemeHookHooks {
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme($existing, $type, $theme, $path) : array {
+    throw new \Exception('This mimics an exception caused by unstable dependencies.');
+  }
+
+}
diff --git a/core/modules/system/tests/modules/update_test_broken_theme_hook/update_test_broken_theme_hook.module b/core/modules/system/tests/modules/update_test_broken_theme_hook/update_test_broken_theme_hook.module
deleted file mode 100644
index 9bbc73f8b75327652e7c3727a9e78fc678ee24d5..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/modules/update_test_broken_theme_hook/update_test_broken_theme_hook.module
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-/**
- * @file
- * Hook implementations for the update_test_broken_theme_hook module.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_theme().
- */
-function update_test_broken_theme_hook_theme($existing, $type, $theme, $path): array {
-  throw new \Exception('This mimics an exception caused by unstable dependencies.');
-}
diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/RebuildScriptTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/RebuildScriptTest.php
index 77d15a813e5bf60b2b11785ee9f20139c831b941..484f520da0b5f68baef2e704f84273259815a236 100644
--- a/core/modules/system/tests/src/Functional/UpdateSystem/RebuildScriptTest.php
+++ b/core/modules/system/tests/src/Functional/UpdateSystem/RebuildScriptTest.php
@@ -51,9 +51,9 @@ public function testRebuild(): void {
     $this->assertFalse($cache->get('rebuild_test'));
     $this->refreshVariables();
     $this->assertSame(1, \Drupal::state()->get('container_rebuild_test.count', 0));
-    $this->drupalGet('/container_rebuild_test/module_test/module_test_system_info_alter');
+    $this->drupalGet('/container_rebuild_test/module_test/system_info_alter');
     $this->assertSession()->pageTextContains('module_test: core/modules/system/tests/modules/module_test');
-    $this->assertSession()->pageTextContains('module_test_system_info_alter: true');
+    $this->assertSession()->pageTextContains('system_info_alter: true');
 
     // Move a module to ensure it does not break the rebuild.
     $file_system = new Filesystem();
@@ -63,9 +63,9 @@ public function testRebuild(): void {
     $this->assertSession()->addressEquals(new Url('<front>'));
     $this->refreshVariables();
     $this->assertSame(1, \Drupal::state()->get('container_rebuild_test.count', 0));
-    $this->drupalGet('/container_rebuild_test/module_test/module_test_system_info_alter');
+    $this->drupalGet('/container_rebuild_test/module_test/system_info_alter');
     $this->assertSession()->pageTextContains('module_test: ' . $this->siteDirectory . '/modules/module_test');
-    $this->assertSession()->pageTextContains('module_test_system_info_alter: true');
+    $this->assertSession()->pageTextContains('system_info_alter: true');
 
     // Disable a module by writing to the core.extension list.
     $this->config('core.extension')->clear('module.module_test')->save();
@@ -74,9 +74,9 @@ public function testRebuild(): void {
     $this->assertSession()->addressEquals(new Url('<front>'));
     $this->refreshVariables();
     $this->assertSame(1, \Drupal::state()->get('container_rebuild_test.count', 0));
-    $this->drupalGet('/container_rebuild_test/module_test/module_test_system_info_alter');
+    $this->drupalGet('/container_rebuild_test/module_test/system_info_alter');
     $this->assertSession()->pageTextContains('module_test: not installed');
-    $this->assertSession()->pageTextContains('module_test_system_info_alter: false');
+    $this->assertSession()->pageTextContains('system_info_alter: false');
 
     // Enable a module by writing to the core.extension list.
     $modules = $this->config('core.extension')->get('module');
@@ -87,9 +87,9 @@ public function testRebuild(): void {
     $this->assertSession()->addressEquals(new Url('<front>'));
     $this->refreshVariables();
     $this->assertSame(1, \Drupal::state()->get('container_rebuild_test.count', 0));
-    $this->drupalGet('/container_rebuild_test/module_test/module_test_system_info_alter');
+    $this->drupalGet('/container_rebuild_test/module_test/system_info_alter');
     $this->assertSession()->pageTextContains('module_test: ' . $this->siteDirectory . '/modules/module_test');
-    $this->assertSession()->pageTextContains('module_test_system_info_alter: true');
+    $this->assertSession()->pageTextContains('system_info_alter: true');
 
     // Test how many container rebuild occur when there is no cached container.
     \Drupal::state()->set('container_rebuild_test.count', 0);
diff --git a/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/src/Hook/TestModuleRequiredByThemeHooks.php b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/src/Hook/TestModuleRequiredByThemeHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..b9eca990f184e9b1828ae9ed60b5e852e500c080
--- /dev/null
+++ b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/src/Hook/TestModuleRequiredByThemeHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\test_module_required_by_theme\Hook;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for test_module_required_by_theme.
+ */
+class TestModuleRequiredByThemeHooks {
+
+  /**
+   * Implements hook_system_info_alter().
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(array &$info, Extension $file, $type) {
+    if ($file->getName() == 'test_theme_depending_on_modules') {
+      $new_info = \Drupal::state()->get('test_theme_depending_on_modules.system_info_alter');
+      if ($new_info) {
+        $info = $new_info + $info;
+      }
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/test_module_required_by_theme.module b/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/test_module_required_by_theme.module
deleted file mode 100644
index ef2f6c7988e6dac467d4a7e9a98d6ae3d9234165..0000000000000000000000000000000000000000
--- a/core/modules/system/tests/themes/test_theme_depending_on_modules/test_module_required_by_theme/test_module_required_by_theme.module
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-/**
- * @file
- * This file provides testing functionality for update.php.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Extension\Extension;
-
-/**
- * Implements hook_system_info_alter().
- */
-function test_module_required_by_theme_system_info_alter(array &$info, Extension $file, $type) {
-  if ($file->getName() == 'test_theme_depending_on_modules') {
-    $new_info = \Drupal::state()->get('test_theme_depending_on_modules.system_info_alter');
-    if ($new_info) {
-      $info = $new_info + $info;
-    }
-  }
-}
diff --git a/core/modules/taxonomy/src/Hook/TaxonomyHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..9447038c3141a487edd5629b48f45ca5ed6f3467
--- /dev/null
+++ b/core/modules/taxonomy/src/Hook/TaxonomyHooks.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Drupal\taxonomy\Hook;
+
+use Drupal\taxonomy\Entity\Term;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for taxonomy.
+ */
+class TaxonomyHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.taxonomy':
+        $field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Taxonomy module allows users who have permission to create and edit content to categorize (tag) content of that type. Users who have the <em>Administer vocabularies and terms</em> <a href=":permissions" title="Taxonomy module permissions">permission</a> can add <em>vocabularies</em> that contain a set of related <em>terms</em>. The terms in a vocabulary can either be pre-set by an administrator or built gradually as content is added and edited. Terms may be organized hierarchically if desired.', [
+          ':permissions' => Url::fromRoute('user.admin_permissions.module', [
+            'modules' => 'taxonomy',
+          ])->toString(),
+        ]) . '</p>';
+        $output .= '<p>' . t('For more information, see the <a href=":taxonomy">online documentation for the Taxonomy module</a>.', [':taxonomy' => 'https://www.drupal.org/docs/8/core/modules/taxonomy']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Managing vocabularies') . '</dt>';
+        $output .= '<dd>' . t('Users who have the <em>Administer vocabularies and terms</em> permission can add and edit vocabularies from the <a href=":taxonomy_admin">Taxonomy administration page</a>. Vocabularies can be deleted from their <em>Edit vocabulary</em> page. Users with the <em>Taxonomy term: Administer fields</em> permission may add additional fields for terms in that vocabulary using the <a href=":field_ui">Field UI module</a>.', [
+          ':taxonomy_admin' => Url::fromRoute('entity.taxonomy_vocabulary.collection')->toString(),
+          ':field_ui' => $field_ui_url,
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Managing terms') . '</dt>';
+        $output .= '<dd>' . t('Users who have the <em>Administer vocabularies and terms</em> permission or the <em>Edit terms</em> permission for a particular vocabulary can add, edit, and organize the terms in a vocabulary from a vocabulary\'s term listing page, which can be accessed by going to the <a href=":taxonomy_admin">Taxonomy administration page</a> and clicking <em>List terms</em> in the <em>Operations</em> column. Users must have the <em>Administer vocabularies and terms</em> permission or the <em>Delete terms</em> permission for a particular vocabulary to delete terms.', [
+          ':taxonomy_admin' => Url::fromRoute('entity.taxonomy_vocabulary.collection')->toString(),
+        ]) . ' </dd>';
+        $output .= '<dt>' . t('Classifying entity content') . '</dt>';
+        $output .= '<dd>' . t('A user with the <em>Administer fields</em> permission for a certain entity type may add <em>Taxonomy term</em> reference fields to the entity type, which will allow entities to be classified using taxonomy terms. See the <a href=":entity_reference">Entity Reference help</a> for more information about reference fields. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them.', [
+          ':field_ui' => $field_ui_url,
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+          ':entity_reference' => Url::fromRoute('help.page', [
+            'name' => 'entity_reference',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Adding new terms during content creation') . '</dt>';
+        $output .= '<dd>' . t("Allowing users to add new terms gradually builds a vocabulary as content is added and edited. Users can add new terms if either of the two <em>Autocomplete</em> widgets is chosen for the Taxonomy term reference field in the <em>Manage form display</em> page for the field. You will also need to enable the <em>Create referenced entities if they don't already exist</em> option, and restrict the field to one vocabulary.") . '</dd>';
+        $output .= '<dt>' . t('Configuring displays and form displays') . '</dt>';
+        $output .= '<dd>' . t('See the <a href=":entity_reference">Entity Reference help</a> page for the field widgets and formatters that can be configured for any reference field on the <em>Manage display</em> and <em>Manage form display</em> pages. Taxonomy additionally provides an <em>RSS category</em> formatter that displays nothing when the entity item is displayed as HTML, but displays an RSS category instead of a list when the entity item is displayed in an RSS feed.', [
+          ':entity_reference' => Url::fromRoute('help.page', [
+            'name' => 'entity_reference',
+          ])->toString(),
+        ]) . '</li>';
+        $output .= '</ul>';
+        $output .= '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'entity.taxonomy_vocabulary.collection':
+        $output = '<p>' . t('Taxonomy is for categorizing content. Terms are grouped into vocabularies. For example, a vocabulary called "Fruit" would contain the terms "Apple" and "Banana".') . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return ['taxonomy_term' => ['render element' => 'elements']];
+  }
+
+  /**
+   * Implements hook_local_tasks_alter().
+   *
+   * @todo Evaluate removing as part of https://www.drupal.org/node/2358923.
+   */
+  #[Hook('local_tasks_alter')]
+  public function localTasksAlter(&$local_tasks) {
+    $local_task_key = 'config_translation.local_tasks:entity.taxonomy_vocabulary.config_translation_overview';
+    if (isset($local_tasks[$local_task_key])) {
+      // The config_translation module expects the base route to be
+      // entity.taxonomy_vocabulary.edit_form like it is for other configuration
+      // entities. Taxonomy uses the overview_form as the base route.
+      $local_tasks[$local_task_key]['base_route'] = 'entity.taxonomy_vocabulary.overview_form';
+    }
+  }
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $term) {
+    $operations = [];
+    if ($term instanceof Term && $term->access('create')) {
+      $operations['add-child'] = [
+        'title' => t('Add child'),
+        'weight' => 10,
+        'url' => Url::fromRoute('entity.taxonomy_term.add_form', [
+          'taxonomy_vocabulary' => $term->bundle(),
+        ], [
+          'query' => [
+            'parent' => $term->id(),
+          ],
+        ]),
+      ];
+    }
+    return $operations;
+  }
+
+  /**
+   * @defgroup taxonomy_index Taxonomy indexing
+   * @{
+   * Functions to maintain taxonomy indexing.
+   *
+   * Taxonomy uses default field storage to store canonical relationships
+   * between terms and fieldable entities. However its most common use case
+   * requires listing all content associated with a term or group of terms
+   * sorted by creation date. To avoid slow queries due to joining across
+   * multiple node and field tables with various conditions and order by criteria,
+   * we maintain a denormalized table with all relationships between terms,
+   * published nodes and common sort criteria such as status, sticky and created.
+   * When using other field storage engines or alternative methods of
+   * denormalizing this data you should set the
+   * taxonomy.settings:maintain_index_table to '0' to avoid unnecessary writes in
+   * SQL.
+   */
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for node entities.
+   */
+  #[Hook('node_insert')]
+  public function nodeInsert(EntityInterface $node) {
+    // Add taxonomy index entries for the node.
+    taxonomy_build_node_index($node);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for node entities.
+   */
+  #[Hook('node_update')]
+  public function nodeUpdate(EntityInterface $node) {
+    // If we're not dealing with the default revision of the node, do not make any
+    // change to the taxonomy index.
+    if (!$node->isDefaultRevision()) {
+      return;
+    }
+    taxonomy_delete_node_index($node);
+    taxonomy_build_node_index($node);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for node entities.
+   */
+  #[Hook('node_predelete')]
+  public function nodePredelete(EntityInterface $node) {
+    // Clean up the {taxonomy_index} table when nodes are deleted.
+    taxonomy_delete_node_index($node);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for taxonomy_term entities.
+   */
+  #[Hook('taxonomy_term_delete')]
+  public function taxonomyTermDelete(Term $term) {
+    if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) {
+      // Clean up the {taxonomy_index} table when terms are deleted.
+      \Drupal::database()->delete('taxonomy_index')->condition('tid', $term->id())->execute();
+    }
+  }
+
+}
diff --git a/core/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..07f5e6b9f9c6349b632a50e74e26da8afb95c41b
--- /dev/null
+++ b/core/modules/taxonomy/src/Hook/TaxonomyTokensHooks.php
@@ -0,0 +1,202 @@
+<?php
+
+namespace Drupal\taxonomy\Hook;
+
+use Drupal\Core\Datetime\Entity\DateFormat;
+use Drupal\taxonomy\Entity\Vocabulary;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for taxonomy.
+ */
+class TaxonomyTokensHooks {
+
+  /**
+   * Implements hook_token_info().
+   */
+  #[Hook('token_info')]
+  public function tokenInfo() {
+    $types['term'] = [
+      'name' => t("Taxonomy terms"),
+      'description' => t("Tokens related to taxonomy terms."),
+      'needs-data' => 'term',
+    ];
+    $types['vocabulary'] = [
+      'name' => t("Vocabularies"),
+      'description' => t("Tokens related to taxonomy vocabularies."),
+      'needs-data' => 'vocabulary',
+    ];
+    // Taxonomy term related variables.
+    $term['tid'] = [
+      'name' => t("Term ID"),
+      'description' => t("The unique ID of the taxonomy term."),
+    ];
+    $term['uuid'] = ['name' => t('UUID'), 'description' => t("The UUID of the taxonomy term.")];
+    $term['name'] = ['name' => t("Name"), 'description' => t("The name of the taxonomy term.")];
+    $term['description'] = [
+      'name' => t("Description"),
+      'description' => t("The optional description of the taxonomy term."),
+    ];
+    $term['node-count'] = [
+      'name' => t("Node count"),
+      'description' => t("The number of nodes tagged with the taxonomy term."),
+    ];
+    $term['url'] = ['name' => t("URL"), 'description' => t("The URL of the taxonomy term.")];
+    // Taxonomy vocabulary related variables.
+    $vocabulary['vid'] = [
+      'name' => t("Vocabulary ID"),
+      'description' => t("The unique ID of the taxonomy vocabulary."),
+    ];
+    $vocabulary['name'] = ['name' => t("Name"), 'description' => t("The name of the taxonomy vocabulary.")];
+    $vocabulary['description'] = [
+      'name' => t("Description"),
+      'description' => t("The optional description of the taxonomy vocabulary."),
+    ];
+    $vocabulary['node-count'] = [
+      'name' => t("Node count"),
+      'description' => t("The number of nodes tagged with terms belonging to the taxonomy vocabulary."),
+    ];
+    $vocabulary['term-count'] = [
+      'name' => t("Term count"),
+      'description' => t("The number of terms belonging to the taxonomy vocabulary."),
+    ];
+    // Chained tokens for taxonomies
+    $term['vocabulary'] = [
+      'name' => t("Vocabulary"),
+      'description' => t("The vocabulary the taxonomy term belongs to."),
+      'type' => 'vocabulary',
+    ];
+    $term['parent'] = [
+      'name' => t("Parent term"),
+      'description' => t("The parent term of the taxonomy term, if one exists."),
+      'type' => 'term',
+    ];
+    $term['changed'] = [
+      'name' => t("Date changed"),
+      'description' => t("The date the taxonomy was most recently updated."),
+      'type' => 'date',
+    ];
+    return ['types' => $types, 'tokens' => ['term' => $term, 'vocabulary' => $vocabulary]];
+  }
+
+  /**
+   * Implements hook_tokens().
+   */
+  #[Hook('tokens')]
+  public function tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+    $token_service = \Drupal::token();
+    if (isset($options['langcode'])) {
+      $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
+      $langcode = $options['langcode'];
+    }
+    else {
+      $langcode = LanguageInterface::LANGCODE_DEFAULT;
+    }
+    $replacements = [];
+    if ($type == 'term' && !empty($data['term'])) {
+      $term = $data['term'];
+      $term = \Drupal::service('entity.repository')->getTranslationFromContext($term, $options['langcode'] ?? NULL);
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          case 'tid':
+            $replacements[$original] = $term->id();
+            break;
+
+          case 'uuid':
+            $replacements[$original] = $term->uuid();
+            break;
+
+          case 'name':
+            $replacements[$original] = $term->label();
+            break;
+
+          case 'description':
+            // "processed" returns a \Drupal\Component\Render\MarkupInterface via
+            // check_markup().
+            $replacements[$original] = $term->description->processed;
+            break;
+
+          case 'url':
+            $replacements[$original] = $term->toUrl('canonical', ['absolute' => TRUE])->toString();
+            break;
+
+          case 'node-count':
+            $query = \Drupal::database()->select('taxonomy_index');
+            $query->condition('tid', $term->id());
+            $query->addTag('term_node_count');
+            $count = $query->countQuery()->execute()->fetchField();
+            $replacements[$original] = $count;
+            break;
+
+          case 'vocabulary':
+            $vocabulary = Vocabulary::load($term->bundle());
+            $bubbleable_metadata->addCacheableDependency($vocabulary);
+            $replacements[$original] = $vocabulary->label();
+            break;
+
+          case 'parent':
+            $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
+            if ($parents = $taxonomy_storage->loadParents($term->id())) {
+              $parent = array_pop($parents);
+              $bubbleable_metadata->addCacheableDependency($parent);
+              $replacements[$original] = $parent->getName();
+            }
+            break;
+
+          case 'changed':
+            $date_format = DateFormat::load('medium');
+            $bubbleable_metadata->addCacheableDependency($date_format);
+            $replacements[$original] = \Drupal::service('date.formatter')->format($term->getChangedTime(), 'medium', '', NULL, $langcode);
+            break;
+        }
+      }
+      if ($vocabulary_tokens = $token_service->findWithPrefix($tokens, 'vocabulary')) {
+        $vocabulary = Vocabulary::load($term->bundle());
+        $replacements += $token_service->generate('vocabulary', $vocabulary_tokens, ['vocabulary' => $vocabulary], $options, $bubbleable_metadata);
+      }
+      if ($vocabulary_tokens = $token_service->findWithPrefix($tokens, 'parent')) {
+        $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
+        if ($parents = $taxonomy_storage->loadParents($term->id())) {
+          $parent = array_pop($parents);
+          $replacements += $token_service->generate('term', $vocabulary_tokens, ['term' => $parent], $options, $bubbleable_metadata);
+        }
+      }
+      if ($changed_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
+        $replacements += $token_service->generate('date', $changed_tokens, ['date' => $term->getChangedTime()], $options, $bubbleable_metadata);
+      }
+    }
+    elseif ($type == 'vocabulary' && !empty($data['vocabulary'])) {
+      $vocabulary = $data['vocabulary'];
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          case 'vid':
+            $replacements[$original] = $vocabulary->id();
+            break;
+
+          case 'name':
+            $replacements[$original] = $vocabulary->label();
+            break;
+
+          case 'description':
+            $build = ['#markup' => $vocabulary->getDescription()];
+            // @todo Fix in https://www.drupal.org/node/2577827
+            $replacements[$original] = \Drupal::service('renderer')->renderInIsolation($build);
+            break;
+
+          case 'term-count':
+            $replacements[$original] = \Drupal::entityQuery('taxonomy_term')->accessCheck(TRUE)->condition('vid', $vocabulary->id())->addTag('vocabulary_term_count')->count()->execute();
+            break;
+
+          case 'node-count':
+            $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
+            $replacements[$original] = $taxonomy_storage->nodeCount($vocabulary->id());
+            break;
+        }
+      }
+    }
+    return $replacements;
+  }
+
+}
diff --git a/core/modules/taxonomy/src/Hook/TaxonomyViewsHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..a446864caf9c50575a990b1abc4cec25f4cb81f0
--- /dev/null
+++ b/core/modules/taxonomy/src/Hook/TaxonomyViewsHooks.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\taxonomy\Hook;
+
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for taxonomy.
+ */
+class TaxonomyViewsHooks {
+
+  /**
+   * Implements hook_views_data_alter().
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(&$data) {
+    $data['node_field_data']['term_node_tid'] = [
+      'title' => t('Taxonomy terms on node'),
+      'help' => t('Relate nodes to taxonomy terms, specifying which vocabulary or vocabularies to use. This relationship will cause duplicated records if there are multiple terms.'),
+      'relationship' => [
+        'id' => 'node_term_data',
+        'label' => t('term'),
+        'base' => 'taxonomy_term_field_data',
+      ],
+      'field' => [
+        'title' => t('All taxonomy terms'),
+        'help' => t('Display all taxonomy terms associated with a node from specified vocabularies.'),
+        'id' => 'taxonomy_index_tid',
+        'no group by' => TRUE,
+        'click sortable' => FALSE,
+      ],
+    ];
+    $data['node_field_data']['term_node_tid_depth'] = [
+      'help' => t('Display content if it has the selected taxonomy terms, or children of the selected terms. Due to additional complexity, this has fewer options than the versions without depth.'),
+      'real field' => 'nid',
+      'argument' => [
+        'title' => t('Has taxonomy term ID (with depth)'),
+        'id' => 'taxonomy_index_tid_depth',
+        'accept depth modifier' => TRUE,
+      ],
+      'filter' => [
+        'title' => t('Has taxonomy terms (with depth)'),
+        'id' => 'taxonomy_index_tid_depth',
+      ],
+    ];
+    $data['node_field_data']['term_node_tid_depth_modifier'] = [
+      'title' => t('Has taxonomy term ID depth modifier'),
+      'help' => t('Allows the "depth" for Taxonomy: Term ID (with depth) to be modified via an additional contextual filter value.'),
+      'argument' => [
+        'id' => 'taxonomy_index_tid_depth_modifier',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_field_views_data_alter().
+   *
+   * Views integration for entity reference fields which reference taxonomy terms.
+   * Adds a term relationship to the default field data.
+   *
+   * @see views_field_default_views_data()
+   */
+  #[Hook('field_views_data_alter')]
+  public function fieldViewsDataAlter(array &$data, FieldStorageConfigInterface $field_storage) {
+    if ($field_storage->getType() == 'entity_reference' && $field_storage->getSetting('target_type') == 'taxonomy_term') {
+      foreach ($data as $table_name => $table_data) {
+        foreach ($table_data as $field_name => $field_data) {
+          if (isset($field_data['filter']) && $field_name != 'delta') {
+            $data[$table_name][$field_name]['filter']['id'] = 'taxonomy_index_tid';
+          }
+        }
+      }
+    }
+  }
+
+}
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 0dbfae0cb5c92794b0f7922034c00f50022a8006..6ba925c9f498671e836e4b003ba0ba2f8c21d4ee 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -2,61 +2,13 @@
 
 /**
  * @file
- * Enables the organization of content into categories.
  */
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
 use Drupal\Core\Render\Element;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
 use Drupal\taxonomy\Entity\Term;
 
-/**
- * Implements hook_help().
- */
-function taxonomy_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.taxonomy':
-      $field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Taxonomy module allows users who have permission to create and edit content to categorize (tag) content of that type. Users who have the <em>Administer vocabularies and terms</em> <a href=":permissions" title="Taxonomy module permissions">permission</a> can add <em>vocabularies</em> that contain a set of related <em>terms</em>. The terms in a vocabulary can either be pre-set by an administrator or built gradually as content is added and edited. Terms may be organized hierarchically if desired.', [':permissions' => Url::fromRoute('user.admin_permissions.module', ['modules' => 'taxonomy'])->toString()]) . '</p>';
-      $output .= '<p>' . t('For more information, see the <a href=":taxonomy">online documentation for the Taxonomy module</a>.', [':taxonomy' => 'https://www.drupal.org/docs/8/core/modules/taxonomy']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Managing vocabularies') . '</dt>';
-      $output .= '<dd>' . t('Users who have the <em>Administer vocabularies and terms</em> permission can add and edit vocabularies from the <a href=":taxonomy_admin">Taxonomy administration page</a>. Vocabularies can be deleted from their <em>Edit vocabulary</em> page. Users with the <em>Taxonomy term: Administer fields</em> permission may add additional fields for terms in that vocabulary using the <a href=":field_ui">Field UI module</a>.', [':taxonomy_admin' => Url::fromRoute('entity.taxonomy_vocabulary.collection')->toString(), ':field_ui' => $field_ui_url]) . '</dd>';
-      $output .= '<dt>' . t('Managing terms') . '</dt>';
-      $output .= '<dd>' . t('Users who have the <em>Administer vocabularies and terms</em> permission or the <em>Edit terms</em> permission for a particular vocabulary can add, edit, and organize the terms in a vocabulary from a vocabulary\'s term listing page, which can be accessed by going to the <a href=":taxonomy_admin">Taxonomy administration page</a> and clicking <em>List terms</em> in the <em>Operations</em> column. Users must have the <em>Administer vocabularies and terms</em> permission or the <em>Delete terms</em> permission for a particular vocabulary to delete terms.', [':taxonomy_admin' => Url::fromRoute('entity.taxonomy_vocabulary.collection')->toString()]) . ' </dd>';
-      $output .= '<dt>' . t('Classifying entity content') . '</dt>';
-      $output .= '<dd>' . t('A user with the <em>Administer fields</em> permission for a certain entity type may add <em>Taxonomy term</em> reference fields to the entity type, which will allow entities to be classified using taxonomy terms. See the <a href=":entity_reference">Entity Reference help</a> for more information about reference fields. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them.', [':field_ui' => $field_ui_url, ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':entity_reference' => Url::fromRoute('help.page', ['name' => 'entity_reference'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Adding new terms during content creation') . '</dt>';
-      $output .= '<dd>' . t("Allowing users to add new terms gradually builds a vocabulary as content is added and edited. Users can add new terms if either of the two <em>Autocomplete</em> widgets is chosen for the Taxonomy term reference field in the <em>Manage form display</em> page for the field. You will also need to enable the <em>Create referenced entities if they don't already exist</em> option, and restrict the field to one vocabulary.") . '</dd>';
-      $output .= '<dt>' . t('Configuring displays and form displays') . '</dt>';
-      $output .= '<dd>' . t('See the <a href=":entity_reference">Entity Reference help</a> page for the field widgets and formatters that can be configured for any reference field on the <em>Manage display</em> and <em>Manage form display</em> pages. Taxonomy additionally provides an <em>RSS category</em> formatter that displays nothing when the entity item is displayed as HTML, but displays an RSS category instead of a list when the entity item is displayed in an RSS feed.', [':entity_reference' => Url::fromRoute('help.page', ['name' => 'entity_reference'])->toString()]) . '</li>';
-      $output .= '</ul>';
-      $output .= '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'entity.taxonomy_vocabulary.collection':
-      $output = '<p>' . t('Taxonomy is for categorizing content. Terms are grouped into vocabularies. For example, a vocabulary called "Fruit" would contain the terms "Apple" and "Banana".') . '</p>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function taxonomy_theme(): array {
-  return [
-    'taxonomy_term' => [
-      'render element' => 'elements',
-    ],
-  ];
-}
-
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
@@ -72,21 +24,6 @@ function taxonomy_theme_suggestions_taxonomy_term(array $variables) {
   return $suggestions;
 }
 
-/**
- * Implements hook_local_tasks_alter().
- *
- * @todo Evaluate removing as part of https://www.drupal.org/node/2358923.
- */
-function taxonomy_local_tasks_alter(&$local_tasks) {
-  $local_task_key = 'config_translation.local_tasks:entity.taxonomy_vocabulary.config_translation_overview';
-  if (isset($local_tasks[$local_task_key])) {
-    // The config_translation module expects the base route to be
-    // entity.taxonomy_vocabulary.edit_form like it is for other configuration
-    // entities. Taxonomy uses the overview_form as the base route.
-    $local_tasks[$local_task_key]['base_route'] = 'entity.taxonomy_vocabulary.overview_form';
-  }
-}
-
 /**
  * Prepares variables for taxonomy term templates.
  *
@@ -139,26 +76,6 @@ function template_preprocess_taxonomy_term(&$variables) {
   }
 }
 
-/**
- * Implements hook_entity_operation().
- */
-function taxonomy_entity_operation(EntityInterface $term) {
-  $operations = [];
-  if ($term instanceof Term && $term->access('create')) {
-    $operations['add-child'] = [
-      'title' => t('Add child'),
-      'weight' => 10,
-      'url' => Url::fromRoute(
-        'entity.taxonomy_term.add_form',
-        ['taxonomy_vocabulary' => $term->bundle()],
-        ['query' => ['parent' => $term->id()]],
-      ),
-    ];
-  }
-
-  return $operations;
-}
-
 /**
  * Returns whether the current page is the page of the passed-in term.
  *
@@ -172,32 +89,6 @@ function taxonomy_term_is_page(Term $term) {
   return FALSE;
 }
 
-/**
- * @defgroup taxonomy_index Taxonomy indexing
- * @{
- * Functions to maintain taxonomy indexing.
- *
- * Taxonomy uses default field storage to store canonical relationships
- * between terms and fieldable entities. However its most common use case
- * requires listing all content associated with a term or group of terms
- * sorted by creation date. To avoid slow queries due to joining across
- * multiple node and field tables with various conditions and order by criteria,
- * we maintain a denormalized table with all relationships between terms,
- * published nodes and common sort criteria such as status, sticky and created.
- * When using other field storage engines or alternative methods of
- * denormalizing this data you should set the
- * taxonomy.settings:maintain_index_table to '0' to avoid unnecessary writes in
- * SQL.
- */
-
-/**
- * Implements hook_ENTITY_TYPE_insert() for node entities.
- */
-function taxonomy_node_insert(EntityInterface $node) {
-  // Add taxonomy index entries for the node.
-  taxonomy_build_node_index($node);
-}
-
 /**
  * Builds and inserts taxonomy index entries for a given node.
  *
@@ -248,27 +139,6 @@ function taxonomy_build_node_index($node) {
   }
 }
 
-/**
- * Implements hook_ENTITY_TYPE_update() for node entities.
- */
-function taxonomy_node_update(EntityInterface $node) {
-  // If we're not dealing with the default revision of the node, do not make any
-  // change to the taxonomy index.
-  if (!$node->isDefaultRevision()) {
-    return;
-  }
-  taxonomy_delete_node_index($node);
-  taxonomy_build_node_index($node);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_predelete() for node entities.
- */
-function taxonomy_node_predelete(EntityInterface $node) {
-  // Clean up the {taxonomy_index} table when nodes are deleted.
-  taxonomy_delete_node_index($node);
-}
-
 /**
  * Deletes taxonomy index entries for a given node.
  *
@@ -281,16 +151,6 @@ function taxonomy_delete_node_index(EntityInterface $node) {
   }
 }
 
-/**
- * Implements hook_ENTITY_TYPE_delete() for taxonomy_term entities.
- */
-function taxonomy_taxonomy_term_delete(Term $term) {
-  if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) {
-    // Clean up the {taxonomy_index} table when terms are deleted.
-    \Drupal::database()->delete('taxonomy_index')->condition('tid', $term->id())->execute();
-  }
-}
-
 /**
  * @} End of "defgroup taxonomy_index".
  */
diff --git a/core/modules/taxonomy/taxonomy.tokens.inc b/core/modules/taxonomy/taxonomy.tokens.inc
deleted file mode 100644
index df64096803270ad3725c3fa21eb91b2be9173150..0000000000000000000000000000000000000000
--- a/core/modules/taxonomy/taxonomy.tokens.inc
+++ /dev/null
@@ -1,231 +0,0 @@
-<?php
-
-/**
- * @file
- * Builds placeholder replacement tokens for taxonomy terms and vocabularies.
- */
-
-use Drupal\Core\Datetime\Entity\DateFormat;
-use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\Render\BubbleableMetadata;
-use Drupal\taxonomy\Entity\Vocabulary;
-
-/**
- * Implements hook_token_info().
- */
-function taxonomy_token_info() {
-  $types['term'] = [
-    'name' => t("Taxonomy terms"),
-    'description' => t("Tokens related to taxonomy terms."),
-    'needs-data' => 'term',
-  ];
-  $types['vocabulary'] = [
-    'name' => t("Vocabularies"),
-    'description' => t("Tokens related to taxonomy vocabularies."),
-    'needs-data' => 'vocabulary',
-  ];
-
-  // Taxonomy term related variables.
-  $term['tid'] = [
-    'name' => t("Term ID"),
-    'description' => t("The unique ID of the taxonomy term."),
-  ];
-  $term['uuid'] = [
-    'name' => t('UUID'),
-    'description' => t("The UUID of the taxonomy term."),
-  ];
-  $term['name'] = [
-    'name' => t("Name"),
-    'description' => t("The name of the taxonomy term."),
-  ];
-  $term['description'] = [
-    'name' => t("Description"),
-    'description' => t("The optional description of the taxonomy term."),
-  ];
-  $term['node-count'] = [
-    'name' => t("Node count"),
-    'description' => t("The number of nodes tagged with the taxonomy term."),
-  ];
-  $term['url'] = [
-    'name' => t("URL"),
-    'description' => t("The URL of the taxonomy term."),
-  ];
-
-  // Taxonomy vocabulary related variables.
-  $vocabulary['vid'] = [
-    'name' => t("Vocabulary ID"),
-    'description' => t("The unique ID of the taxonomy vocabulary."),
-  ];
-  $vocabulary['name'] = [
-    'name' => t("Name"),
-    'description' => t("The name of the taxonomy vocabulary."),
-  ];
-  $vocabulary['description'] = [
-    'name' => t("Description"),
-    'description' => t("The optional description of the taxonomy vocabulary."),
-  ];
-  $vocabulary['node-count'] = [
-    'name' => t("Node count"),
-    'description' => t("The number of nodes tagged with terms belonging to the taxonomy vocabulary."),
-  ];
-  $vocabulary['term-count'] = [
-    'name' => t("Term count"),
-    'description' => t("The number of terms belonging to the taxonomy vocabulary."),
-  ];
-
-  // Chained tokens for taxonomies
-  $term['vocabulary'] = [
-    'name' => t("Vocabulary"),
-    'description' => t("The vocabulary the taxonomy term belongs to."),
-    'type' => 'vocabulary',
-  ];
-  $term['parent'] = [
-    'name' => t("Parent term"),
-    'description' => t("The parent term of the taxonomy term, if one exists."),
-    'type' => 'term',
-  ];
-  $term['changed'] = [
-    'name' => t("Date changed"),
-    'description' => t("The date the taxonomy was most recently updated."),
-    'type' => 'date',
-  ];
-
-  return [
-    'types' => $types,
-    'tokens' => [
-      'term' => $term,
-      'vocabulary' => $vocabulary,
-    ],
-  ];
-}
-
-/**
- * Implements hook_tokens().
- */
-function taxonomy_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
-  $token_service = \Drupal::token();
-
-  if (isset($options['langcode'])) {
-    $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
-    $langcode = $options['langcode'];
-  }
-  else {
-    $langcode = LanguageInterface::LANGCODE_DEFAULT;
-  }
-
-  $replacements = [];
-  if ($type == 'term' && !empty($data['term'])) {
-    $term = $data['term'];
-    $term = \Drupal::service('entity.repository')->getTranslationFromContext($term, $options['langcode'] ?? NULL);
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'tid':
-          $replacements[$original] = $term->id();
-          break;
-
-        case 'uuid':
-          $replacements[$original] = $term->uuid();
-          break;
-
-        case 'name':
-          $replacements[$original] = $term->label();
-          break;
-
-        case 'description':
-          // "processed" returns a \Drupal\Component\Render\MarkupInterface via
-          // check_markup().
-          $replacements[$original] = $term->description->processed;
-          break;
-
-        case 'url':
-          $replacements[$original] = $term->toUrl('canonical', ['absolute' => TRUE])->toString();
-          break;
-
-        case 'node-count':
-          $query = \Drupal::database()->select('taxonomy_index');
-          $query->condition('tid', $term->id());
-          $query->addTag('term_node_count');
-          $count = $query->countQuery()->execute()->fetchField();
-          $replacements[$original] = $count;
-          break;
-
-        case 'vocabulary':
-          $vocabulary = Vocabulary::load($term->bundle());
-          $bubbleable_metadata->addCacheableDependency($vocabulary);
-          $replacements[$original] = $vocabulary->label();
-          break;
-
-        case 'parent':
-          $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
-          if ($parents = $taxonomy_storage->loadParents($term->id())) {
-            $parent = array_pop($parents);
-            $bubbleable_metadata->addCacheableDependency($parent);
-            $replacements[$original] = $parent->getName();
-          }
-          break;
-
-        case 'changed':
-          $date_format = DateFormat::load('medium');
-          $bubbleable_metadata->addCacheableDependency($date_format);
-          $replacements[$original] = \Drupal::service('date.formatter')->format($term->getChangedTime(), 'medium', '', NULL, $langcode);
-          break;
-      }
-    }
-
-    if ($vocabulary_tokens = $token_service->findWithPrefix($tokens, 'vocabulary')) {
-      $vocabulary = Vocabulary::load($term->bundle());
-      $replacements += $token_service->generate('vocabulary', $vocabulary_tokens, ['vocabulary' => $vocabulary], $options, $bubbleable_metadata);
-    }
-
-    if (($vocabulary_tokens = $token_service->findWithPrefix($tokens, 'parent'))) {
-      $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
-      if ($parents = $taxonomy_storage->loadParents($term->id())) {
-        $parent = array_pop($parents);
-        $replacements += $token_service->generate('term', $vocabulary_tokens, ['term' => $parent], $options, $bubbleable_metadata);
-      }
-    }
-
-    if ($changed_tokens = $token_service->findWithPrefix($tokens, 'changed')) {
-      $replacements += $token_service->generate('date', $changed_tokens, ['date' => $term->getChangedTime()], $options, $bubbleable_metadata);
-    }
-  }
-
-  elseif ($type == 'vocabulary' && !empty($data['vocabulary'])) {
-    $vocabulary = $data['vocabulary'];
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'vid':
-          $replacements[$original] = $vocabulary->id();
-          break;
-
-        case 'name':
-          $replacements[$original] = $vocabulary->label();
-          break;
-
-        case 'description':
-          $build = ['#markup' => $vocabulary->getDescription()];
-          // @todo Fix in https://www.drupal.org/node/2577827
-          $replacements[$original] = \Drupal::service('renderer')->renderInIsolation($build);
-          break;
-
-        case 'term-count':
-          $replacements[$original] = \Drupal::entityQuery('taxonomy_term')
-            ->accessCheck(TRUE)
-            ->condition('vid', $vocabulary->id())
-            ->addTag('vocabulary_term_count')
-            ->count()
-            ->execute();
-          break;
-
-        case 'node-count':
-          $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
-          $replacements[$original] = $taxonomy_storage->nodeCount($vocabulary->id());
-          break;
-      }
-    }
-  }
-
-  return $replacements;
-}
diff --git a/core/modules/taxonomy/taxonomy.views.inc b/core/modules/taxonomy/taxonomy.views.inc
deleted file mode 100644
index 2f5718d12e4bc3d9e6652a575675b56014433855..0000000000000000000000000000000000000000
--- a/core/modules/taxonomy/taxonomy.views.inc
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides views data for taxonomy.module.
- */
-
-use Drupal\field\FieldStorageConfigInterface;
-
-/**
- * Implements hook_views_data_alter().
- */
-function taxonomy_views_data_alter(&$data) {
-  $data['node_field_data']['term_node_tid'] = [
-    'title' => t('Taxonomy terms on node'),
-    'help' => t('Relate nodes to taxonomy terms, specifying which vocabulary or vocabularies to use. This relationship will cause duplicated records if there are multiple terms.'),
-    'relationship' => [
-      'id' => 'node_term_data',
-      'label' => t('term'),
-      'base' => 'taxonomy_term_field_data',
-    ],
-    'field' => [
-      'title' => t('All taxonomy terms'),
-      'help' => t('Display all taxonomy terms associated with a node from specified vocabularies.'),
-      'id' => 'taxonomy_index_tid',
-      'no group by' => TRUE,
-      'click sortable' => FALSE,
-    ],
-  ];
-
-  $data['node_field_data']['term_node_tid_depth'] = [
-    'help' => t('Display content if it has the selected taxonomy terms, or children of the selected terms. Due to additional complexity, this has fewer options than the versions without depth.'),
-    'real field' => 'nid',
-    'argument' => [
-      'title' => t('Has taxonomy term ID (with depth)'),
-      'id' => 'taxonomy_index_tid_depth',
-      'accept depth modifier' => TRUE,
-    ],
-    'filter' => [
-      'title' => t('Has taxonomy terms (with depth)'),
-      'id' => 'taxonomy_index_tid_depth',
-    ],
-  ];
-
-  $data['node_field_data']['term_node_tid_depth_modifier'] = [
-    'title' => t('Has taxonomy term ID depth modifier'),
-    'help' => t('Allows the "depth" for Taxonomy: Term ID (with depth) to be modified via an additional contextual filter value.'),
-    'argument' => [
-      'id' => 'taxonomy_index_tid_depth_modifier',
-    ],
-  ];
-}
-
-/**
- * Implements hook_field_views_data_alter().
- *
- * Views integration for entity reference fields which reference taxonomy terms.
- * Adds a term relationship to the default field data.
- *
- * @see views_field_default_views_data()
- */
-function taxonomy_field_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
-  if ($field_storage->getType() == 'entity_reference' && $field_storage->getSetting('target_type') == 'taxonomy_term') {
-    foreach ($data as $table_name => $table_data) {
-      foreach ($table_data as $field_name => $field_data) {
-        if (isset($field_data['filter']) && $field_name != 'delta') {
-          $data[$table_name][$field_name]['filter']['id'] = 'taxonomy_index_tid';
-        }
-      }
-    }
-  }
-}
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_crud/src/Hook/TaxonomyCrudHooks.php b/core/modules/taxonomy/tests/modules/taxonomy_crud/src/Hook/TaxonomyCrudHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..80848fd77687cff20670cbf9faaa0db1008e1b25
--- /dev/null
+++ b/core/modules/taxonomy/tests/modules/taxonomy_crud/src/Hook/TaxonomyCrudHooks.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\taxonomy_crud\Hook;
+
+use Drupal\taxonomy\VocabularyInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for taxonomy_crud.
+ */
+class TaxonomyCrudHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for taxonomy_vocabulary entities.
+   */
+  #[Hook('taxonomy_vocabulary_presave')]
+  public function taxonomyVocabularyPresave(VocabularyInterface $vocabulary) {
+    $vocabulary->setThirdPartySetting('taxonomy_crud', 'foo', 'bar');
+  }
+
+}
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_crud/taxonomy_crud.module b/core/modules/taxonomy/tests/modules/taxonomy_crud/taxonomy_crud.module
deleted file mode 100644
index 5af9fc1603b51315f1d955408cde726df70731b3..0000000000000000000000000000000000000000
--- a/core/modules/taxonomy/tests/modules/taxonomy_crud/taxonomy_crud.module
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides hook implementations for testing purposes.
- */
-
-declare(strict_types=1);
-
-use Drupal\taxonomy\VocabularyInterface;
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for taxonomy_vocabulary entities.
- */
-function taxonomy_crud_taxonomy_vocabulary_presave(VocabularyInterface $vocabulary) {
-  $vocabulary->setThirdPartySetting('taxonomy_crud', 'foo', 'bar');
-}
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_term_display_configurable_test/src/Hook/TaxonomyTermDisplayConfigurableTestHooks.php b/core/modules/taxonomy/tests/modules/taxonomy_term_display_configurable_test/src/Hook/TaxonomyTermDisplayConfigurableTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..444677420dfef1ff85996dff57cc563498d75c91
--- /dev/null
+++ b/core/modules/taxonomy/tests/modules/taxonomy_term_display_configurable_test/src/Hook/TaxonomyTermDisplayConfigurableTestHooks.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\taxonomy_term_display_configurable_test\Hook;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for taxonomy_term_display_configurable_test.
+ */
+class TaxonomyTermDisplayConfigurableTestHooks {
+
+  /**
+   * Implements hook_entity_base_field_info_alter().
+   */
+  #[Hook('entity_base_field_info_alter')]
+  public function entityBaseFieldInfoAlter(&$base_field_definitions, EntityTypeInterface $entity_type) {
+    if ($entity_type->id() === 'taxonomy_term') {
+      /** @var \Drupal\Core\Field\BaseFieldDefinition[] $base_field_definitions */
+      $base_field_definitions['name']->setDisplayConfigurable('view', TRUE);
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public function entityTypeBuild(array &$entity_types) {
+    // Allow skipping of extra preprocessing for configurable display.
+    $entity_types['taxonomy_term']->set('enable_base_field_custom_preprocess_skipping', TRUE);
+  }
+
+}
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_term_display_configurable_test/taxonomy_term_display_configurable_test.module b/core/modules/taxonomy/tests/modules/taxonomy_term_display_configurable_test/taxonomy_term_display_configurable_test.module
deleted file mode 100644
index c4247f13ce92d4898ddd816f5655f74bbe0ffb93..0000000000000000000000000000000000000000
--- a/core/modules/taxonomy/tests/modules/taxonomy_term_display_configurable_test/taxonomy_term_display_configurable_test.module
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-/**
- * @file
- * Tests configurable displays for taxonomy_term base fields.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Entity\EntityTypeInterface;
-
-/**
- * Implements hook_entity_base_field_info_alter().
- */
-function taxonomy_term_display_configurable_test_entity_base_field_info_alter(&$base_field_definitions, EntityTypeInterface $entity_type) {
-  if ($entity_type->id() === 'taxonomy_term') {
-    /** @var \Drupal\Core\Field\BaseFieldDefinition[] $base_field_definitions */
-    $base_field_definitions['name']->setDisplayConfigurable('view', TRUE);
-  }
-}
-
-/**
- * Implements hook_entity_type_build().
- */
-function taxonomy_term_display_configurable_test_entity_type_build(array &$entity_types) {
-  // Allow skipping of extra preprocessing for configurable display.
-  $entity_types['taxonomy_term']->set('enable_base_field_custom_preprocess_skipping', TRUE);
-}
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test/src/Hook/TaxonomyTestHooks.php b/core/modules/taxonomy/tests/modules/taxonomy_test/src/Hook/TaxonomyTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..8f467f849e261940ffd133f97c1d76247e80bcac
--- /dev/null
+++ b/core/modules/taxonomy/tests/modules/taxonomy_test/src/Hook/TaxonomyTestHooks.php
@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\taxonomy_test\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\taxonomy\TermInterface;
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for taxonomy_test.
+ */
+class TaxonomyTestHooks {
+
+  /**
+   * Implements hook_entity_access().
+   */
+  #[Hook('entity_access')]
+  public function entityAccess(EntityInterface $entity, string $operation, AccountInterface $account) : AccessResultInterface {
+    if ($entity instanceof TermInterface) {
+      $parts = explode(' ', (string) $entity->label());
+      if (in_array('Inaccessible', $parts, TRUE) && in_array($operation, $parts, TRUE)) {
+        return AccessResult::forbidden();
+      }
+    }
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Implements hook_query_alter().
+   */
+  #[Hook('query_alter')]
+  public function queryAlter(AlterableInterface $query) {
+    $value = \Drupal::state()->get('taxonomy_test_query_alter');
+    if (isset($value)) {
+      \Drupal::state()->set('taxonomy_test_query_alter', ++$value);
+    }
+  }
+
+  /**
+   * Implements hook_query_TAG_alter().
+   */
+  #[Hook('query_term_access_alter')]
+  public function queryTermAccessAlter(AlterableInterface $query) {
+    $value = \Drupal::state()->get('taxonomy_test_query_term_access_alter');
+    if (isset($value)) {
+      \Drupal::state()->set('taxonomy_test_query_term_access_alter', ++$value);
+    }
+  }
+
+  /**
+   * Implements hook_query_TAG_alter().
+   */
+  #[Hook('query_taxonomy_term_access_alter')]
+  public function queryTaxonomyTermAccessAlter(AlterableInterface $query) {
+    $value = \Drupal::state()->get('taxonomy_test_query_taxonomy_term_access_alter');
+    if (isset($value)) {
+      \Drupal::state()->set('taxonomy_test_query_taxonomy_term_access_alter', ++$value);
+    }
+  }
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter() for the taxonomy term form.
+   */
+  #[Hook('form_taxonomy_term_form_alter')]
+  public function formTaxonomyTermFormAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    if (\Drupal::state()->get('taxonomy_test.disable_parent_form_element', FALSE)) {
+      $form['relations']['parent']['#disabled'] = TRUE;
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_load() for the taxonomy term.
+   */
+  #[Hook('taxonomy_term_load')]
+  public function taxonomyTermLoad($entities) {
+    $value = \Drupal::state()->get('taxonomy_test_taxonomy_term_load');
+    // Only record loaded terms is the test has set this to an empty array.
+    if (is_array($value)) {
+      $value = array_merge($value, array_keys($entities));
+      \Drupal::state()->set('taxonomy_test_taxonomy_term_load', array_unique($value));
+    }
+  }
+
+}
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test/taxonomy_test.module b/core/modules/taxonomy/tests/modules/taxonomy_test/taxonomy_test.module
deleted file mode 100644
index f78e281c346ee9f46809a897e45d7e8532683e0c..0000000000000000000000000000000000000000
--- a/core/modules/taxonomy/tests/modules/taxonomy_test/taxonomy_test.module
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides test hook implementations for taxonomy tests.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Access\AccessResultInterface;
-use Drupal\Core\Database\Query\AlterableInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\taxonomy\TermInterface;
-
-/**
- * Implements hook_entity_access().
- */
-function taxonomy_test_entity_access(EntityInterface $entity, string $operation, AccountInterface $account): AccessResultInterface {
-  if ($entity instanceof TermInterface) {
-    $parts = explode(' ', (string) $entity->label());
-    if (in_array('Inaccessible', $parts, TRUE) && in_array($operation, $parts, TRUE)) {
-      return AccessResult::forbidden();
-    }
-  }
-
-  return AccessResult::neutral();
-}
-
-/**
- * Implements hook_query_alter().
- */
-function taxonomy_test_query_alter(AlterableInterface $query) {
-  $value = \Drupal::state()->get(__FUNCTION__);
-  if (isset($value)) {
-    \Drupal::state()->set(__FUNCTION__, ++$value);
-  }
-}
-
-/**
- * Implements hook_query_TAG_alter().
- */
-function taxonomy_test_query_term_access_alter(AlterableInterface $query) {
-  $value = \Drupal::state()->get(__FUNCTION__);
-  if (isset($value)) {
-    \Drupal::state()->set(__FUNCTION__, ++$value);
-  }
-}
-
-/**
- * Implements hook_query_TAG_alter().
- */
-function taxonomy_test_query_taxonomy_term_access_alter(AlterableInterface $query) {
-  $value = \Drupal::state()->get(__FUNCTION__);
-  if (isset($value)) {
-    \Drupal::state()->set(__FUNCTION__, ++$value);
-  }
-}
-
-/**
- * Implements hook_form_BASE_FORM_ID_alter() for the taxonomy term form.
- */
-function taxonomy_test_form_taxonomy_term_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  if (\Drupal::state()->get('taxonomy_test.disable_parent_form_element', FALSE)) {
-    $form['relations']['parent']['#disabled'] = TRUE;
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_load() for the taxonomy term.
- */
-function taxonomy_test_taxonomy_term_load($entities) {
-  $value = \Drupal::state()->get(__FUNCTION__);
-  // Only record loaded terms is the test has set this to an empty array.
-  if (is_array($value)) {
-    $value = array_merge($value, array_keys($entities));
-    \Drupal::state()->set(__FUNCTION__, array_unique($value));
-  }
-}
diff --git a/core/modules/telephone/src/Hook/TelephoneHooks.php b/core/modules/telephone/src/Hook/TelephoneHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..fafd3f1c07c710e13ed517886aa365e03f5985ec
--- /dev/null
+++ b/core/modules/telephone/src/Hook/TelephoneHooks.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Drupal\telephone\Hook;
+
+use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for telephone.
+ */
+class TelephoneHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.telephone':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Telephone module allows you to create fields that contain telephone numbers. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":telephone_documentation">online documentation for the Telephone module</a>.', [
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+          ':telephone_documentation' => 'https://www.drupal.org/documentation/modules/telephone',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Managing and displaying telephone fields') . '</dt>';
+        $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the telephone field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Displaying telephone numbers as links') . '</dt>';
+        $output .= '<dd>' . t('Telephone numbers can be displayed as links with the scheme name <em>tel:</em> by choosing the <em>Telephone</em> display format on the <em>Manage display</em> page. Any spaces will be stripped out of the link text. This semantic markup improves the user experience on mobile and assistive technology devices.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_field_formatter_info_alter().
+   */
+  #[Hook('field_formatter_info_alter')]
+  public function fieldFormatterInfoAlter(&$info) {
+    $info['string']['field_types'][] = 'telephone';
+  }
+
+  /**
+   * Implements hook_field_type_category_info_alter().
+   */
+  #[Hook('field_type_category_info_alter')]
+  public function fieldTypeCategoryInfoAlter(&$definitions) {
+    // The `telephone` field type belongs in the `general` category, so the
+    // libraries need to be attached using an alter hook.
+    $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'telephone/drupal.telephone-icon';
+  }
+
+}
diff --git a/core/modules/telephone/telephone.module b/core/modules/telephone/telephone.module
deleted file mode 100644
index 3c65366168f0dda87fdc9a00179b8cfb077a28f8..0000000000000000000000000000000000000000
--- a/core/modules/telephone/telephone.module
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-/**
- * @file
- * Defines a simple telephone number field type.
- */
-
-use Drupal\Core\Field\FieldTypeCategoryManagerInterface;
-use Drupal\Core\Url;
-use Drupal\Core\Routing\RouteMatchInterface;
-
-/**
- * Implements hook_help().
- */
-function telephone_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.telephone':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Telephone module allows you to create fields that contain telephone numbers. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":telephone_documentation">online documentation for the Telephone module</a>.', [':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':telephone_documentation' => 'https://www.drupal.org/documentation/modules/telephone']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Managing and displaying telephone fields') . '</dt>';
-      $output .= '<dd>' . t('The <em>settings</em> and the <em>display</em> of the telephone field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Displaying telephone numbers as links') . '</dt>';
-      $output .= '<dd>' . t('Telephone numbers can be displayed as links with the scheme name <em>tel:</em> by choosing the <em>Telephone</em> display format on the <em>Manage display</em> page. Any spaces will be stripped out of the link text. This semantic markup improves the user experience on mobile and assistive technology devices.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_field_formatter_info_alter().
- */
-function telephone_field_formatter_info_alter(&$info) {
-  $info['string']['field_types'][] = 'telephone';
-}
-
-/**
- * Implements hook_field_type_category_info_alter().
- */
-function telephone_field_type_category_info_alter(&$definitions) {
-  // The `telephone` field type belongs in the `general` category, so the
-  // libraries need to be attached using an alter hook.
-  $definitions[FieldTypeCategoryManagerInterface::FALLBACK_CATEGORY]['libraries'][] = 'telephone/drupal.telephone-icon';
-}
diff --git a/core/modules/text/src/Hook/TextHooks.php b/core/modules/text/src/Hook/TextHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..356052ad658d8eefc11ed9bd84513f2f8771a946
--- /dev/null
+++ b/core/modules/text/src/Hook/TextHooks.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\text\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for text.
+ */
+class TextHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.text':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Text module allows you to create short and long text fields with optional summaries. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":text_documentation">online documentation for the Text module</a>.', [
+          ':field' => Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString(),
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+          ':text_documentation' => 'https://www.drupal.org/documentation/modules/text',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Managing and displaying text fields') . '</dt>';
+        $output .= '<dd>' . t('The <em>settings</em> and <em>display</em> of the text field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [
+          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
+            'name' => 'field_ui',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Creating short text fields') . '</dt>';
+        $output .= '<dd>' . t('If you choose <em>Text (plain)</em> or <em>Text (formatted)</em> as the field type on the <em>Manage fields</em> page, then a field with a single row is displayed. You can change the maximum text length in the <em>Field settings</em> when you set up the field.') . '</dd>';
+        $output .= '<dt>' . t('Creating long text fields') . '</dt>';
+        $output .= '<dd>' . t('If you choose <em>Text (plain, long)</em>, <em>Text (formatted, long)</em>, or <em>Text (formatted, long, with summary)</em> on the <em>Manage fields</em> page, then users can insert text of unlimited length. On the <em>Manage form display</em> page, you can set the number of rows that are displayed to users.') . '</dd>';
+        $output .= '<dt>' . t('Trimming the text length') . '</dt>';
+        $output .= '<dd>' . t('On the <em>Manage display</em> page you can choose to display a trimmed version of the text, and if so, where to cut off the text.') . '</dd>';
+        $output .= '<dt>' . t('Displaying summaries instead of trimmed text') . '</dt>';
+        $output .= '<dd>' . t('As an alternative to using a trimmed version of the text, you can enter a separate summary by choosing the <em>Text (formatted, long, with summary)</em> field type on the <em>Manage fields</em> page. Even when <em>Summary input</em> is enabled, and summaries are provided, you can display <em>trimmed</em> text nonetheless by choosing the appropriate format on the <em>Manage display</em> page.') . '</dd>';
+        $output .= '<dt>' . t('Using text formats and editors') . '</dt>';
+        $output .= '<dd>' . t('If you choose <em>Text (plain)</em> or <em>Text (plain, long)</em> you restrict the input to <em>Plain text</em> only. If you choose <em>Text (formatted)</em>, <em>Text (formatted, long)</em>, or <em>Text (formatted, long with summary)</em> you allow users to write formatted text. Which options are available to individual users depends on the settings on the <a href=":formats">Text formats and editors page</a>.', [':formats' => Url::fromRoute('filter.admin_overview')->toString()]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/text/text.module b/core/modules/text/text.module
index 5a6b85235d54d6ec025481f3fa4ae8a83171a35f..b7bc4208561ce49d320c1a5607ef82e599b8976a 100644
--- a/core/modules/text/text.module
+++ b/core/modules/text/text.module
@@ -2,43 +2,12 @@
 
 /**
  * @file
- * Defines simple text field types.
  */
 
-use Drupal\Core\Url;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Unicode;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\filter\Entity\FilterFormat;
 
-/**
- * Implements hook_help().
- */
-function text_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.text':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Text module allows you to create short and long text fields with optional summaries. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":text_documentation">online documentation for the Text module</a>.', [':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString(), ':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':text_documentation' => 'https://www.drupal.org/documentation/modules/text']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Managing and displaying text fields') . '</dt>';
-      $output .= '<dd>' . t('The <em>settings</em> and <em>display</em> of the text field can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#']) . '</dd>';
-      $output .= '<dt>' . t('Creating short text fields') . '</dt>';
-      $output .= '<dd>' . t('If you choose <em>Text (plain)</em> or <em>Text (formatted)</em> as the field type on the <em>Manage fields</em> page, then a field with a single row is displayed. You can change the maximum text length in the <em>Field settings</em> when you set up the field.') . '</dd>';
-      $output .= '<dt>' . t('Creating long text fields') . '</dt>';
-      $output .= '<dd>' . t('If you choose <em>Text (plain, long)</em>, <em>Text (formatted, long)</em>, or <em>Text (formatted, long, with summary)</em> on the <em>Manage fields</em> page, then users can insert text of unlimited length. On the <em>Manage form display</em> page, you can set the number of rows that are displayed to users.') . '</dd>';
-      $output .= '<dt>' . t('Trimming the text length') . '</dt>';
-      $output .= '<dd>' . t('On the <em>Manage display</em> page you can choose to display a trimmed version of the text, and if so, where to cut off the text.') . '</dd>';
-      $output .= '<dt>' . t('Displaying summaries instead of trimmed text') . '</dt>';
-      $output .= '<dd>' . t('As an alternative to using a trimmed version of the text, you can enter a separate summary by choosing the <em>Text (formatted, long, with summary)</em> field type on the <em>Manage fields</em> page. Even when <em>Summary input</em> is enabled, and summaries are provided, you can display <em>trimmed</em> text nonetheless by choosing the appropriate format on the <em>Manage display</em> page.') . '</dd>';
-      $output .= '<dt>' . t('Using text formats and editors') . '</dt>';
-      $output .= '<dd>' . t('If you choose <em>Text (plain)</em> or <em>Text (plain, long)</em> you restrict the input to <em>Plain text</em> only. If you choose <em>Text (formatted)</em>, <em>Text (formatted, long)</em>, or <em>Text (formatted, long with summary)</em> you allow users to write formatted text. Which options are available to individual users depends on the settings on the <a href=":formats">Text formats and editors page</a>.', [':formats' => Url::fromRoute('filter.admin_overview')->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
 /**
  * Generates a trimmed, formatted version of a text field value.
  *
diff --git a/core/modules/toolbar/src/Hook/ToolbarHooks.php b/core/modules/toolbar/src/Hook/ToolbarHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..883b733cd334dc48c41df34cddad9e2b1c90153f
--- /dev/null
+++ b/core/modules/toolbar/src/Hook/ToolbarHooks.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace Drupal\toolbar\Hook;
+
+use Drupal\toolbar\Controller\ToolbarController;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for toolbar.
+ */
+class ToolbarHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.toolbar':
+        $output = '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Toolbar module provides a toolbar for site administrators, which displays tabs and trays provided by the Toolbar module itself and other modules. For more information, see the <a href=":toolbar_docs">online documentation for the Toolbar module</a>.', [':toolbar_docs' => 'https://www.drupal.org/docs/8/core/modules/toolbar']) . '</p>';
+        $output .= '<h4>' . t('Terminology') . '</h4>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Tabs') . '</dt>';
+        $output .= '<dd>' . t('Tabs are buttons, displayed in a bar across the top of the screen. Some tabs execute an action (such as starting Edit mode), while other tabs toggle which tray is open.') . '</dd>';
+        $output .= '<dt>' . t('Trays') . '</dt>';
+        $output .= '<dd>' . t('Trays are usually lists of links, which can be hierarchical like a menu. If a tray has been toggled open, it is displayed either vertically or horizontally below the tab bar, depending on the browser width. Only one tray may be open at a time. If you click another tab, that tray will replace the tray being displayed. In wide browser widths, the user has the ability to toggle from vertical to horizontal, using a link at the bottom or right of the tray. Hierarchical menus only have open/close behavior in vertical mode; if you display a tray containing a hierarchical menu horizontally, only the top-level links will be available.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme($existing, $type, $theme, $path) : array {
+    $items['toolbar'] = ['render element' => 'element'];
+    $items['menu__toolbar'] = [
+      'base hook' => 'menu',
+      'variables' => [
+        'menu_name' => NULL,
+        'items' => [],
+        'attributes' => [],
+      ],
+    ];
+    return $items;
+  }
+
+  /**
+   * Implements hook_page_top().
+   *
+   * Add admin toolbar to the top of the page automatically.
+   */
+  #[Hook('page_top')]
+  public function pageTop(array &$page_top) {
+    $page_top['toolbar'] = [
+      '#type' => 'toolbar',
+      '#access' => \Drupal::currentUser()->hasPermission('access toolbar'),
+      '#cache' => [
+        'keys' => [
+          'toolbar',
+        ],
+        'contexts' => [
+          'user.permissions',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_toolbar().
+   */
+  #[Hook('toolbar')]
+  public function toolbar() {
+    // The 'Home' tab is a simple link, with no corresponding tray.
+    $items['home'] = [
+      '#type' => 'toolbar_item',
+      'tab' => [
+        '#type' => 'link',
+        '#title' => t('Back to site'),
+        '#url' => Url::fromRoute('<front>'),
+        '#attributes' => [
+          'title' => t('Return to site content'),
+          'class' => [
+            'toolbar-icon',
+            'toolbar-icon-escape-admin',
+          ],
+          'data-toolbar-escape-admin' => TRUE,
+        ],
+      ],
+      '#wrapper_attributes' => [
+        'class' => [
+          'home-toolbar-tab',
+        ],
+      ],
+      '#attached' => [
+        'library' => [
+          'toolbar/toolbar.escapeAdmin',
+        ],
+      ],
+      '#weight' => -20,
+    ];
+    // To conserve bandwidth, we only include the top-level links in the HTML.
+    // The subtrees are fetched through a JSONP script that is generated at the
+    // toolbar_subtrees route. We provide the JavaScript requesting that JSONP
+    // script here with the hash parameter that is needed for that route.
+    // @see toolbar_subtrees_jsonp()
+    [$hash, $hash_cacheability] = _toolbar_get_subtrees_hash();
+    $subtrees_attached['drupalSettings']['toolbar'] = ['subtreesHash' => $hash];
+    // The administration element has a link that is themed to correspond to
+    // a toolbar tray. The tray contains the full administrative menu of the site.
+    $items['administration'] = [
+      '#type' => 'toolbar_item',
+      'tab' => [
+        '#type' => 'link',
+        '#title' => t('Manage'),
+        '#url' => Url::fromRoute('system.admin'),
+        '#attributes' => [
+          'title' => t('Admin menu'),
+          'class' => [
+            'toolbar-icon',
+            'toolbar-icon-menu',
+          ],
+                  // A data attribute that indicates to the client to defer loading of
+                  // the admin menu subtrees until this tab is activated. Admin menu
+                  // subtrees will not render to the DOM if this attribute is removed.
+                  // The value of the attribute is intentionally left blank. Only the
+                  // presence of the attribute is necessary.
+          'data-drupal-subtrees' => '',
+        ],
+      ],
+      'tray' => [
+        '#heading' => t('Administration menu'),
+        '#attached' => $subtrees_attached,
+        'toolbar_administration' => [
+          '#pre_render' => [
+                      [
+                        ToolbarController::class,
+                        'preRenderAdministrationTray',
+                      ],
+          ],
+          '#type' => 'container',
+          '#attributes' => [
+            'class' => [
+              'toolbar-menu-administration',
+            ],
+          ],
+        ],
+      ],
+      '#weight' => -15,
+    ];
+    $hash_cacheability->applyTo($items['administration']);
+    return $items;
+  }
+
+}
diff --git a/core/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/src/Hook/ToolbarDisableUserToolbarHooks.php b/core/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/src/Hook/ToolbarDisableUserToolbarHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f2be064f9de5effbb5d1302dbb82cf78e23715cd
--- /dev/null
+++ b/core/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/src/Hook/ToolbarDisableUserToolbarHooks.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\toolbar_disable_user_toolbar\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for toolbar_disable_user_toolbar.
+ */
+class ToolbarDisableUserToolbarHooks {
+
+  /**
+   * Implements hook_toolbar_alter().
+   */
+  #[Hook('toolbar_alter')]
+  public function toolbarAlter(&$items) {
+    unset($items['user']);
+  }
+
+}
diff --git a/core/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/toolbar_disable_user_toolbar.module b/core/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/toolbar_disable_user_toolbar.module
deleted file mode 100644
index 27a86623e301b525bcd7510e8768d5a531a3e344..0000000000000000000000000000000000000000
--- a/core/modules/toolbar/tests/modules/toolbar_disable_user_toolbar/toolbar_disable_user_toolbar.module
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-/**
- * @file
- * Test module.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_toolbar_alter().
- */
-function toolbar_disable_user_toolbar_toolbar_alter(&$items) {
-  unset($items['user']);
-}
diff --git a/core/modules/toolbar/tests/modules/toolbar_test/src/Hook/ToolbarTestHooks.php b/core/modules/toolbar/tests/modules/toolbar_test/src/Hook/ToolbarTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..a6d2b69bcd6ce5c9e5ff90bfef6582b55d389ce0
--- /dev/null
+++ b/core/modules/toolbar/tests/modules/toolbar_test/src/Hook/ToolbarTestHooks.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\toolbar_test\Hook;
+
+use Drupal\Core\Link;
+use Drupal\Core\Url;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for toolbar_test.
+ */
+class ToolbarTestHooks {
+
+  /**
+   * Implements hook_toolbar().
+   */
+  #[Hook('toolbar')]
+  public function toolbar() {
+    $items['testing'] = [
+      '#type' => 'toolbar_item',
+      'tab' => [
+        '#type' => 'link',
+        '#title' => t('Test tab'),
+        '#url' => Url::fromRoute('<front>'),
+        '#options' => [
+          'attributes' => [
+            'id' => 'toolbar-tab-testing',
+            'title' => t('Test tab'),
+          ],
+        ],
+      ],
+      'tray' => [
+        '#heading' => t('Test tray'),
+        '#wrapper_attributes' => [
+          'id' => 'toolbar-tray-testing',
+        ],
+        'content' => [
+          '#theme' => 'item_list',
+          '#items' => [
+            Link::fromTextAndUrl(t('link 1'), Url::fromRoute('<front>', [], [
+              'attributes' => [
+                'title' => 'Test link 1 title',
+              ],
+            ]))->toRenderable(),
+            Link::fromTextAndUrl(t('link 2'), Url::fromRoute('<front>', [], [
+              'attributes' => [
+                'title' => 'Test link 2 title',
+              ],
+            ]))->toRenderable(),
+            Link::fromTextAndUrl(t('link 3'), Url::fromRoute('<front>', [], [
+              'attributes' => [
+                'title' => 'Test link 3 title',
+              ],
+            ]))->toRenderable(),
+          ],
+          '#attributes' => [
+            'class' => [
+              'toolbar-menu',
+            ],
+          ],
+        ],
+      ],
+      '#weight' => 50,
+    ];
+    $items['empty'] = ['#type' => 'toolbar_item'];
+    return $items;
+  }
+
+}
diff --git a/core/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module b/core/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module
index bea8d993054f69ad68273c8df8769a37c0b76f05..9085a1b6f60e817cd2f9a48d888af862d662f53f 100644
--- a/core/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module
+++ b/core/modules/toolbar/tests/modules/toolbar_test/toolbar_test.module
@@ -7,53 +7,6 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Link;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_toolbar().
- */
-function toolbar_test_toolbar() {
-
-  $items['testing'] = [
-    '#type' => 'toolbar_item',
-    'tab' => [
-      '#type' => 'link',
-      '#title' => t('Test tab'),
-      '#url' => Url::fromRoute('<front>'),
-      '#options' => [
-        'attributes' => [
-          'id' => 'toolbar-tab-testing',
-          'title' => t('Test tab'),
-        ],
-      ],
-    ],
-    'tray' => [
-      '#heading' => t('Test tray'),
-      '#wrapper_attributes' => [
-        'id' => 'toolbar-tray-testing',
-      ],
-      'content' => [
-        '#theme' => 'item_list',
-        '#items' => [
-          Link::fromTextAndUrl(t('link 1'), Url::fromRoute('<front>', [], ['attributes' => ['title' => 'Test link 1 title']]))->toRenderable(),
-          Link::fromTextAndUrl(t('link 2'), Url::fromRoute('<front>', [], ['attributes' => ['title' => 'Test link 2 title']]))->toRenderable(),
-          Link::fromTextAndUrl(t('link 3'), Url::fromRoute('<front>', [], ['attributes' => ['title' => 'Test link 3 title']]))->toRenderable(),
-        ],
-        '#attributes' => [
-          'class' => ['toolbar-menu'],
-        ],
-      ],
-    ],
-    '#weight' => 50,
-  ];
-  $items['empty'] = [
-    '#type' => 'toolbar_item',
-  ];
-
-  return $items;
-}
-
 /**
  * Implements hook_preprocess_HOOK().
  */
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index 57dc701ef882bd74e7c94b721aa0871a6ec90486..285c1bc28e659f73abe73380176ae17eadb37e09 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -2,68 +2,15 @@
 
 /**
  * @file
- * Administration toolbar for quick access to top level administration items.
  */
 
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\RenderContext;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Template\Attribute;
 use Drupal\Component\Utility\Crypt;
-use Drupal\Core\Url;
 use Drupal\toolbar\Controller\ToolbarController;
 
-/**
- * Implements hook_help().
- */
-function toolbar_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.toolbar':
-      $output = '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Toolbar module provides a toolbar for site administrators, which displays tabs and trays provided by the Toolbar module itself and other modules. For more information, see the <a href=":toolbar_docs">online documentation for the Toolbar module</a>.', [':toolbar_docs' => 'https://www.drupal.org/docs/8/core/modules/toolbar']) . '</p>';
-      $output .= '<h4>' . t('Terminology') . '</h4>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Tabs') . '</dt>';
-      $output .= '<dd>' . t('Tabs are buttons, displayed in a bar across the top of the screen. Some tabs execute an action (such as starting Edit mode), while other tabs toggle which tray is open.') . '</dd>';
-      $output .= '<dt>' . t('Trays') . '</dt>';
-      $output .= '<dd>' . t('Trays are usually lists of links, which can be hierarchical like a menu. If a tray has been toggled open, it is displayed either vertically or horizontally below the tab bar, depending on the browser width. Only one tray may be open at a time. If you click another tab, that tray will replace the tray being displayed. In wide browser widths, the user has the ability to toggle from vertical to horizontal, using a link at the bottom or right of the tray. Hierarchical menus only have open/close behavior in vertical mode; if you display a tray containing a hierarchical menu horizontally, only the top-level links will be available.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function toolbar_theme($existing, $type, $theme, $path): array {
-  $items['toolbar'] = [
-    'render element' => 'element',
-  ];
-  $items['menu__toolbar'] = [
-    'base hook' => 'menu',
-    'variables' => ['menu_name' => NULL, 'items' => [], 'attributes' => []],
-  ];
-
-  return $items;
-}
-
-/**
- * Implements hook_page_top().
- *
- * Add admin toolbar to the top of the page automatically.
- */
-function toolbar_page_top(array &$page_top) {
-  $page_top['toolbar'] = [
-    '#type' => 'toolbar',
-    '#access' => \Drupal::currentUser()->hasPermission('access toolbar'),
-    '#cache' => [
-      'keys' => ['toolbar'],
-      'contexts' => ['user.permissions'],
-    ],
-  ];
-}
-
 /**
  * Prepares variables for administration toolbar templates.
  *
@@ -132,81 +79,6 @@ function template_preprocess_toolbar(&$variables) {
   }
 }
 
-/**
- * Implements hook_toolbar().
- */
-function toolbar_toolbar() {
-  // The 'Home' tab is a simple link, with no corresponding tray.
-  $items['home'] = [
-    '#type' => 'toolbar_item',
-    'tab' => [
-      '#type' => 'link',
-      '#title' => t('Back to site'),
-      '#url' => Url::fromRoute('<front>'),
-      '#attributes' => [
-        'title' => t('Return to site content'),
-        'class' => ['toolbar-icon', 'toolbar-icon-escape-admin'],
-        'data-toolbar-escape-admin' => TRUE,
-      ],
-    ],
-    '#wrapper_attributes' => [
-      'class' => ['home-toolbar-tab'],
-    ],
-    '#attached' => [
-      'library' => [
-        'toolbar/toolbar.escapeAdmin',
-      ],
-    ],
-    '#weight' => -20,
-  ];
-
-  // To conserve bandwidth, we only include the top-level links in the HTML.
-  // The subtrees are fetched through a JSONP script that is generated at the
-  // toolbar_subtrees route. We provide the JavaScript requesting that JSONP
-  // script here with the hash parameter that is needed for that route.
-  // @see toolbar_subtrees_jsonp()
-  [$hash, $hash_cacheability] = _toolbar_get_subtrees_hash();
-  $subtrees_attached['drupalSettings']['toolbar'] = [
-    'subtreesHash' => $hash,
-  ];
-
-  // The administration element has a link that is themed to correspond to
-  // a toolbar tray. The tray contains the full administrative menu of the site.
-  $items['administration'] = [
-    '#type' => 'toolbar_item',
-    'tab' => [
-      '#type' => 'link',
-      '#title' => t('Manage'),
-      '#url' => Url::fromRoute('system.admin'),
-      '#attributes' => [
-        'title' => t('Admin menu'),
-        'class' => ['toolbar-icon', 'toolbar-icon-menu'],
-        // A data attribute that indicates to the client to defer loading of
-        // the admin menu subtrees until this tab is activated. Admin menu
-        // subtrees will not render to the DOM if this attribute is removed.
-        // The value of the attribute is intentionally left blank. Only the
-        // presence of the attribute is necessary.
-        'data-drupal-subtrees' => '',
-      ],
-    ],
-    'tray' => [
-      '#heading' => t('Administration menu'),
-      '#attached' => $subtrees_attached,
-      'toolbar_administration' => [
-        '#pre_render' => [[ToolbarController::class, 'preRenderAdministrationTray']],
-        '#type' => 'container',
-        '#attributes' => [
-          'class' => ['toolbar-menu-administration'],
-        ],
-      ],
-    ],
-    '#weight' => -15,
-  ];
-  $hash_cacheability->applyTo($items['administration']);
-
-  return $items;
-}
-
 /**
  * Adds toolbar-specific attributes to the menu link tree.
  *
diff --git a/core/modules/update/src/Hook/UpdateHooks.php b/core/modules/update/src/Hook/UpdateHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..999a4e4d630e2c29eb9165e7786b662ea3ace874
--- /dev/null
+++ b/core/modules/update/src/Hook/UpdateHooks.php
@@ -0,0 +1,330 @@
+<?php
+
+namespace Drupal\update\Hook;
+
+use Drupal\update\UpdateManagerInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for update.
+ */
+class UpdateHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.update':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Update Manager module periodically checks for new versions of your site\'s software (including contributed modules and themes), and alerts administrators to available updates. The Update Manager system is also used by some other modules to manage updates and downloads; for example, the Interface Translation module uses the Update Manager to download translations from the localization server. Note that whenever the Update Manager system is used, anonymous usage statistics are sent to Drupal.org. If desired, you may uninstall the Update Manager module from the <a href=":modules">Extend page</a>; if you do so, functionality that depends on the Update Manager system will not work. For more information, see the <a href=":update">online documentation for the Update Manager module</a>.', [
+          ':update' => 'https://www.drupal.org/documentation/modules/update',
+          ':modules' => Url::fromRoute('system.modules_list')->toString(),
+        ]) . '</p>';
+        // Only explain the Update manager if it has not been uninstalled.
+        if (_update_manager_access()) {
+          $output .= '<p>' . t('The Update Manager also allows administrators to add and update modules and themes through the administration interface.') . '</p>';
+        }
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Checking for available updates') . '</dt>';
+        $output .= '<dd>' . t('The <a href=":update-report">Available updates report</a> displays core, contributed modules, and themes for which there are new releases available for download. On the report page, you can also check manually for updates. You can configure the frequency of update checks, which are performed during cron runs, and whether notifications are sent on the <a href=":update-settings">Update Manager settings page</a>.', [
+          ':update-report' => Url::fromRoute('update.status')->toString(),
+          ':update-settings' => Url::fromRoute('update.settings')->toString(),
+        ]) . '</dd>';
+        // Only explain the Update manager if it has not been uninstalled.
+        if (_update_manager_access()) {
+          $output .= '<dt>' . t('Performing updates through the Update page') . '</dt>';
+          $output .= '<dd>' . t('The Update Manager module allows administrators to perform updates directly from the <a href=":update-page">Update page</a>. It lists all available updates, and you can confirm whether you want to download them. If you don\'t have sufficient access rights to your web server, you could be prompted for your FTP/SSH password. Afterwards the files are transferred into your site installation, overwriting your old files. Direct links to the Update page are also displayed on the <a href=":modules_page">Extend page</a> and the <a href=":themes_page">Appearance page</a>.', [
+            ':modules_page' => Url::fromRoute('system.modules_list')->toString(),
+            ':themes_page' => Url::fromRoute('system.themes_page')->toString(),
+            ':update-page' => Url::fromRoute('update.report_update')->toString(),
+          ]) . '</dd>';
+        }
+        $output .= '</dl>';
+        return $output;
+
+      case 'update.status':
+        return '<p>' . t('Here you can find information about available updates for your installed modules and themes. Note that each module or theme is part of a "project", which may or may not have the same name, and might include multiple modules or themes within it.') . '</p>';
+
+      case 'system.modules_list':
+        return '<p>' . t('Regularly review <a href=":updates">available updates</a> and update as required to maintain a secure and current site. Always run the <a href=":update-php">update script</a> each time you update software.', [
+          ':update-php' => Url::fromRoute('system.db_update')->toString(),
+          ':updates' => Url::fromRoute('update.status')->toString(),
+        ]) . '</p>';
+    }
+  }
+
+  /**
+   * Implements hook_page_top().
+   */
+  #[Hook('page_top')]
+  public function pageTop() {
+    /** @var \Drupal\Core\Routing\AdminContext $admin_context */
+    $admin_context = \Drupal::service('router.admin_context');
+    $route_match = \Drupal::routeMatch();
+    if ($admin_context->isAdminRoute($route_match->getRouteObject()) && \Drupal::currentUser()->hasPermission('view update notifications')) {
+      $route_name = \Drupal::routeMatch()->getRouteName();
+      switch ($route_name) {
+        // These pages don't need additional nagging.
+        case 'update.theme_update':
+        case 'update.module_update':
+        case 'update.status':
+        case 'update.report_update':
+        case 'update.settings':
+        case 'system.status':
+        case 'system.theme_install':
+        case 'update.confirmation_page':
+        case 'system.batch_page.html':
+          return;
+
+        // If we are on the appearance or modules list, display a detailed report
+        // of the update status.
+        case 'system.themes_page':
+        case 'system.modules_list':
+          $verbose = TRUE;
+          break;
+      }
+      \Drupal::moduleHandler()->loadInclude('update', 'install');
+      $status = update_requirements('runtime');
+      foreach (['core', 'contrib'] as $report_type) {
+        $type = 'update_' . $report_type;
+        // hook_requirements() supports render arrays therefore we need to render
+        // them before using
+        // \Drupal\Core\Messenger\MessengerInterface::addStatus().
+        if (isset($status[$type]['description']) && is_array($status[$type]['description'])) {
+          $status[$type]['description'] = \Drupal::service('renderer')->renderInIsolation($status[$type]['description']);
+        }
+        if (!empty($verbose)) {
+          if (isset($status[$type]['severity'])) {
+            if ($status[$type]['severity'] == REQUIREMENT_ERROR) {
+              \Drupal::messenger()->addError($status[$type]['description']);
+            }
+            elseif ($status[$type]['severity'] == REQUIREMENT_WARNING) {
+              \Drupal::messenger()->addWarning($status[$type]['description']);
+            }
+          }
+        }
+        else {
+          if (isset($status[$type]) && isset($status[$type]['reason']) && $status[$type]['reason'] === UpdateManagerInterface::NOT_SECURE) {
+            \Drupal::messenger()->addError($status[$type]['description']);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'update_last_check' => [
+        'variables' => [
+          'last' => 0,
+        ],
+      ],
+      'update_report' => [
+        'variables' => [
+          'data' => NULL,
+        ],
+        'file' => 'update.report.inc',
+      ],
+      'update_project_status' => [
+        'variables' => [
+          'project' => [],
+        ],
+        'file' => 'update.report.inc',
+      ],
+          // We are using template instead of '#type' => 'table' here to keep markup
+          // out of preprocess and allow for easier changes to markup.
+      'update_version' => [
+        'variables' => [
+          'version' => NULL,
+          'title' => NULL,
+          'attributes' => [],
+        ],
+        'file' => 'update.report.inc',
+      ],
+      'update_fetch_error_message' => [
+        'file' => 'update.report.inc',
+        'render element' => 'element',
+        'variables' => [
+          'error_message' => [],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_cron().
+   */
+  #[Hook('cron')]
+  public function cron() {
+    $update_config = \Drupal::config('update.settings');
+    $frequency = $update_config->get('check.interval_days');
+    $interval = 60 * 60 * 24 * $frequency;
+    $last_check = \Drupal::state()->get('update.last_check', 0);
+    $request_time = \Drupal::time()->getRequestTime();
+    if ($request_time - $last_check > $interval) {
+      // If the configured update interval has elapsed, we want to invalidate
+      // the data for all projects, attempt to re-fetch, and trigger any
+      // configured notifications about the new status.
+      update_refresh();
+      update_fetch_data();
+    }
+    else {
+      // Otherwise, see if any individual projects are now stale or still
+      // missing data, and if so, try to fetch the data.
+      update_get_available(TRUE);
+    }
+    $last_email_notice = \Drupal::state()->get('update.last_email_notification', 0);
+    if ($request_time - $last_email_notice > $interval) {
+      // If configured time between notifications elapsed, send email about
+      // updates possibly available.
+      \Drupal::moduleHandler()->loadInclude('update', 'inc', 'update.fetch');
+      _update_cron_notify();
+    }
+    // Clear garbage from disk.
+    update_clear_update_disk_cache();
+  }
+
+  /**
+   * Implements hook_themes_installed().
+   *
+   * If themes are installed, we invalidate the information of available updates.
+   */
+  #[Hook('themes_installed')]
+  public function themesInstalled($themes) {
+    // Clear all update module data.
+    update_storage_clear();
+  }
+
+  /**
+   * Implements hook_themes_uninstalled().
+   *
+   * If themes are uninstalled, we invalidate the information of available updates.
+   */
+  #[Hook('themes_uninstalled')]
+  public function themesUninstalled($themes) {
+    // Clear all update module data.
+    update_storage_clear();
+  }
+
+  /**
+   * Implements hook_modules_installed().
+   *
+   * If modules are installed, we invalidate the information of available updates.
+   */
+  #[Hook('modules_installed')]
+  public function modulesInstalled($modules) {
+    // Clear all update module data.
+    update_storage_clear();
+  }
+
+  /**
+   * Implements hook_modules_uninstalled().
+   *
+   * If modules are uninstalled, we invalidate the information of available updates.
+   */
+  #[Hook('modules_uninstalled')]
+  public function modulesUninstalled($modules) {
+    // Clear all update module data.
+    update_storage_clear();
+  }
+
+  /**
+   * Implements hook_mail().
+   *
+   * Constructs the email notification message when the site is out of date.
+   *
+   * @see \Drupal\Core\Mail\MailManagerInterface::mail()
+   * @see _update_cron_notify()
+   * @see _update_message_text()
+   * @see \Drupal\update\UpdateManagerInterface
+   */
+  #[Hook('mail')]
+  public function mail($key, &$message, $params) {
+    $langcode = $message['langcode'];
+    $language = \Drupal::languageManager()->getLanguage($langcode);
+    $message['subject'] .= t('New release(s) available for @site_name', ['@site_name' => \Drupal::config('system.site')->get('name')], ['langcode' => $langcode]);
+    foreach ($params as $msg_type => $msg_reason) {
+      $message['body'][] = _update_message_text($msg_type, $msg_reason, $langcode);
+    }
+    $message['body'][] = t('See the available updates page for more information:', [], ['langcode' => $langcode]) . "\n" . Url::fromRoute('update.status', [], ['absolute' => TRUE, 'language' => $language])->toString();
+    if (_update_manager_access()) {
+      $message['body'][] = t('You can automatically download your missing updates using the Update manager:', [], ['langcode' => $langcode]) . "\n" . Url::fromRoute('update.report_update', [], ['absolute' => TRUE, 'language' => $language])->toString();
+    }
+    $settings_url = Url::fromRoute('update.settings', [], ['absolute' => TRUE])->toString();
+    if (\Drupal::config('update.settings')->get('notification.threshold') == 'all') {
+      $message['body'][] = t('Your site is currently configured to send these emails when any updates are available. To get notified only for security updates, @url.', ['@url' => $settings_url]);
+    }
+    else {
+      $message['body'][] = t('Your site is currently configured to send these emails only when security updates are available. To get notified for any available updates, @url.', ['@url' => $settings_url]);
+    }
+  }
+
+  /**
+   * Implements hook_verify_update_archive().
+   *
+   * First, we ensure that the archive isn't a copy of Drupal core, which the
+   * update manager does not yet support. See https://www.drupal.org/node/606592.
+   *
+   * Then, we make sure that at least one module included in the archive file has
+   * an .info.yml file which claims that the code is compatible with the current
+   * version of Drupal core.
+   *
+   * @see \Drupal\Core\Extension\ExtensionDiscovery
+   */
+  #[Hook('verify_update_archive')]
+  public function verifyUpdateArchive($project, $archive_file, $directory) {
+    $errors = [];
+    // Make sure this isn't a tarball of Drupal core.
+    if (file_exists("{$directory}/{$project}/index.php") && file_exists("{$directory}/{$project}/core/install.php") && file_exists("{$directory}/{$project}/core/includes/bootstrap.inc") && file_exists("{$directory}/{$project}/core/modules/node/node.module") && file_exists("{$directory}/{$project}/core/modules/system/system.module")) {
+      return [
+        'no-core' => t('Automatic updating of Drupal core is not supported. See the <a href=":update-guide">Updating Drupal guide</a> for information on how to update Drupal core manually.', [
+          ':update-guide' => 'https://www.drupal.org/docs/updating-drupal',
+        ]),
+      ];
+    }
+    // Parse all the .info.yml files and make sure at least one is compatible with
+    // this version of Drupal core. If one is compatible, then the project as a
+    // whole is considered compatible (since, for example, the project may ship
+    // with some out-of-date modules that are not necessary for its overall
+    // functionality).
+    $compatible_project = FALSE;
+    $incompatible = [];
+    /** @var \Drupal\Core\File\FileSystemInterface $file_system */
+    $file_system = \Drupal::service('file_system');
+    $files = $file_system->scanDirectory("{$directory}/{$project}", '/.*\.info.yml$/', ['key' => 'name', 'min_depth' => 0]);
+    foreach ($files as $file) {
+      // Get the .info.yml file for the module or theme this file belongs to.
+      $info = \Drupal::service('info_parser')->parse($file->uri);
+      // If the module or theme is incompatible with Drupal core, set an error.
+      if ($info['core_incompatible']) {
+        $incompatible[] = !empty($info['name']) ? $info['name'] : t('Unknown');
+      }
+      else {
+        $compatible_project = TRUE;
+        break;
+      }
+    }
+    if (empty($files)) {
+      $errors[] = t('%archive_file does not contain any .info.yml files.', ['%archive_file' => $file_system->basename($archive_file)]);
+    }
+    elseif (!$compatible_project) {
+      $errors[] = \Drupal::translation()->formatPlural(count($incompatible), '%archive_file contains a version of %names that is not compatible with Drupal @version.', '%archive_file contains versions of modules or themes that are not compatible with Drupal @version: %names', [
+        '@version' => \Drupal::VERSION,
+        '%archive_file' => $file_system->basename($archive_file),
+        '%names' => implode(', ', $incompatible),
+      ]);
+    }
+    return $errors;
+  }
+
+}
diff --git a/core/modules/update/tests/modules/update_test/src/Hook/UpdateTestHooks.php b/core/modules/update/tests/modules/update_test/src/Hook/UpdateTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..32100571a7b742e3ce604168407d4f6059a1809b
--- /dev/null
+++ b/core/modules/update/tests/modules/update_test/src/Hook/UpdateTestHooks.php
@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\update_test\Hook;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for update_test.
+ */
+class UpdateTestHooks {
+
+  /**
+   * Implements hook_system_info_alter().
+   *
+   * Checks the 'update_test.settings:system_info' configuration and sees if we
+   * need to alter the system info for the given $file based on the setting. The
+   * setting is expected to be a nested associative array. If the key '#all' is
+   * defined, its subarray will include .info.yml keys and values for all modules
+   * and themes on the system. Otherwise, the settings array is keyed by the
+   * module or theme short name ($file->name) and the subarrays contain settings
+   * just for that module or theme.
+   */
+  #[Hook('system_info_alter')]
+  public function systemInfoAlter(&$info, Extension $file) {
+    $setting = \Drupal::config('update_test.settings')->get('system_info');
+    foreach (['#all', $file->getName()] as $id) {
+      if (!empty($setting[$id])) {
+        foreach ($setting[$id] as $key => $value) {
+          $info[$key] = $value;
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_update_status_alter().
+   *
+   * Checks the 'update_test.settings:update_status' configuration and sees if we
+   * need to alter the update status for the given project based on the setting.
+   * The setting is expected to be a nested associative array. If the key '#all'
+   * is defined, its subarray will include .info.yml keys and values for all modules
+   * and themes on the system. Otherwise, the settings array is keyed by the
+   * module or theme short name and the subarrays contain settings just for that
+   * module or theme.
+   */
+  #[Hook('update_status_alter')]
+  public function updateStatusAlter(&$projects) {
+    $setting = \Drupal::config('update_test.settings')->get('update_status');
+    if (!empty($setting)) {
+      foreach ($projects as $project_name => &$project) {
+        foreach (['#all', $project_name] as $id) {
+          if (!empty($setting[$id])) {
+            foreach ($setting[$id] as $key => $value) {
+              $project[$key] = $value;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_filetransfer_info().
+   */
+  #[Hook('filetransfer_info')]
+  public function filetransferInfo() {
+    // Define a test file transfer method, to ensure that there will always be at
+    // least one method available in the user interface (regardless of the
+    // environment in which the update manager tests are run).
+    return [
+      'system_test' => [
+        'title' => t('Update Test FileTransfer'),
+        'class' => 'Drupal\update_test\TestFileTransferWithSettingsForm',
+        'weight' => -20,
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/update/tests/modules/update_test/update_test.module b/core/modules/update/tests/modules/update_test/update_test.module
deleted file mode 100644
index be38cd832b00c1fff3684603b9b6204e92a69698..0000000000000000000000000000000000000000
--- a/core/modules/update/tests/modules/update_test/update_test.module
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php
-
-/**
- * @file
- * Module for testing Update Manager functionality.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Extension\Extension;
-
-/**
- * Implements hook_system_info_alter().
- *
- * Checks the 'update_test.settings:system_info' configuration and sees if we
- * need to alter the system info for the given $file based on the setting. The
- * setting is expected to be a nested associative array. If the key '#all' is
- * defined, its subarray will include .info.yml keys and values for all modules
- * and themes on the system. Otherwise, the settings array is keyed by the
- * module or theme short name ($file->name) and the subarrays contain settings
- * just for that module or theme.
- */
-function update_test_system_info_alter(&$info, Extension $file) {
-  $setting = \Drupal::config('update_test.settings')->get('system_info');
-  foreach (['#all', $file->getName()] as $id) {
-    if (!empty($setting[$id])) {
-      foreach ($setting[$id] as $key => $value) {
-        $info[$key] = $value;
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_update_status_alter().
- *
- * Checks the 'update_test.settings:update_status' configuration and sees if we
- * need to alter the update status for the given project based on the setting.
- * The setting is expected to be a nested associative array. If the key '#all'
- * is defined, its subarray will include .info.yml keys and values for all modules
- * and themes on the system. Otherwise, the settings array is keyed by the
- * module or theme short name and the subarrays contain settings just for that
- * module or theme.
- */
-function update_test_update_status_alter(&$projects) {
-  $setting = \Drupal::config('update_test.settings')->get('update_status');
-  if (!empty($setting)) {
-    foreach ($projects as $project_name => &$project) {
-      foreach (['#all', $project_name] as $id) {
-        if (!empty($setting[$id])) {
-          foreach ($setting[$id] as $key => $value) {
-            $project[$key] = $value;
-          }
-        }
-      }
-    }
-  }
-}
-
-/**
- * Implements hook_filetransfer_info().
- */
-function update_test_filetransfer_info() {
-  // Define a test file transfer method, to ensure that there will always be at
-  // least one method available in the user interface (regardless of the
-  // environment in which the update manager tests are run).
-  return [
-    'system_test' => [
-      'title' => t('Update Test FileTransfer'),
-      'class' => 'Drupal\update_test\TestFileTransferWithSettingsForm',
-      'weight' => -20,
-    ],
-  ];
-}
diff --git a/core/modules/update/tests/src/Unit/UpdateMailTest.php b/core/modules/update/tests/src/Unit/UpdateMailTest.php
index ca3f73891d265fcf00b91a11e609080f3b83eca4..b8caa9e5f675c3fc65e063518695a5d8f4a63150 100644
--- a/core/modules/update/tests/src/Unit/UpdateMailTest.php
+++ b/core/modules/update/tests/src/Unit/UpdateMailTest.php
@@ -7,6 +7,7 @@
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Tests\UnitTestCase;
 use Drupal\update\UpdateManagerInterface;
+use Drupal\update\Hook\UpdateHooks;
 
 /**
  * Tests text of update email.
@@ -147,7 +148,8 @@ public function testUpdateEmail($notification_threshold, $params, $authorized, a
     \Drupal::setContainer($this->container);
 
     // Generate the email message.
-    update_mail($key, $message, $params);
+    $updateMail = new UpdateHooks();
+    $updateMail->mail($key, $message, $params);
 
     // Confirm the subject.
     $this->assertSame("New release(s) available for $site_name", $message['subject']);
diff --git a/core/modules/update/update.authorize.inc b/core/modules/update/update.authorize.inc
index 9be6690af407121d1d50d3a5d791c29c61a4ac7e..15f09badfcf0e98c84d61c8bc076799798f46494 100644
--- a/core/modules/update/update.authorize.inc
+++ b/core/modules/update/update.authorize.inc
@@ -2,12 +2,6 @@
 
 /**
  * @file
- * Callbacks and related functions invoked by authorize.php to update projects.
- *
- * We use the Batch API to actually update each individual project on the site.
- * All of the code in this file is run at a low bootstrap level (modules are not
- * loaded), so these functions cannot assume access to the rest of the code of
- * the Update Manager module.
  */
 
 use Drupal\Core\Batch\BatchBuilder;
diff --git a/core/modules/update/update.compare.inc b/core/modules/update/update.compare.inc
index ed909ea7b3e08eac4d87e5dc81bbd3d066794fc2..4cca7e4000f8f27742ac9eb8422b405da30cdf72 100644
--- a/core/modules/update/update.compare.inc
+++ b/core/modules/update/update.compare.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Code required only when comparing available updates to existing data.
  */
 
 use Drupal\Core\Extension\ExtensionVersion;
diff --git a/core/modules/update/update.fetch.inc b/core/modules/update/update.fetch.inc
index 4ae279da24194a6e1ba0aa9f18d596249bd27210..0056bb8ea3e4ffb8244b929567454c1b582beb9a 100644
--- a/core/modules/update/update.fetch.inc
+++ b/core/modules/update/update.fetch.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Code required only when fetching information about available updates.
  */
 
 use Drupal\update\UpdateManagerInterface;
diff --git a/core/modules/update/update.manager.inc b/core/modules/update/update.manager.inc
index 87330e337c3e3af315f03035230093cf716d9c63..02554db90acb8053172399dd0cb15acae0909d37 100644
--- a/core/modules/update/update.manager.inc
+++ b/core/modules/update/update.manager.inc
@@ -2,38 +2,6 @@
 
 /**
  * @file
- * Administrative screens and processing functions of the Update Manager module.
- *
- * This allows site administrators with the 'administer software updates'
- * permission to either upgrade existing projects, or download and install new
- * ones, so long as the kill switch setting ('allow_authorize_operations') is
- * not FALSE.
- *
- * To install new code, the administrator is prompted for either the URL of an
- * archive file, or to directly upload the archive file. The archive is loaded
- * into a temporary location, extracted, and verified. If everything is
- * successful, the user is redirected to authorize.php to type in file transfer
- * credentials and authorize the installation to proceed with elevated
- * privileges, such that the extracted files can be copied out of the temporary
- * location and into the live web root.
- *
- * Updating existing code is a more elaborate process. The first step is a
- * selection form where the user is presented with a table of installed projects
- * that are missing newer releases. The user selects which projects they wish to
- * update, and presses the "Download updates" button to continue. This sets up a
- * batch to fetch all the selected releases, and redirects to
- * admin/update/download to display the batch progress bar as it runs. Each
- * batch operation is responsible for downloading a single file, extracting the
- * archive, and verifying the contents. If there are any errors, the user is
- * redirected back to the first page with the error messages. If all downloads
- * were extracted and verified, the user is instead redirected to
- * admin/update/ready, a landing page which reminds them to backup their
- * database and asks if they want to put the site offline during the update.
- * Once the user presses the "Install updates" button, they are redirected to
- * authorize.php to supply their web root file access credentials. The
- * authorized operation (which lives in update.authorize.inc) sets up a batch to
- * copy each extracted update from the temporary location into the live web
- * root.
  */
 
 use Drupal\Core\File\Exception\FileException;
diff --git a/core/modules/update/update.module b/core/modules/update/update.module
index 9bd58f98e140fb38322ed0e67bbddee8d535fbbf..f12aecaa74eb33b6bede31f2d4e5c59eacf59ae2 100644
--- a/core/modules/update/update.module
+++ b/core/modules/update/update.module
@@ -2,119 +2,15 @@
 
 /**
  * @file
- * Handles updates of Drupal core and contributed projects.
- *
- * The module checks for available updates of Drupal core and any installed
- * contributed modules and themes. It warns site administrators if newer
- * releases are available via the system status report (admin/reports/status),
- * the module and theme pages, and optionally via email.
  */
 
 use Drupal\Core\File\Exception\FileException;
 use Drupal\Core\Link;
 use Drupal\Core\Url;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Site\Settings;
 use Drupal\update\UpdateFetcherInterface;
 use Drupal\update\UpdateManagerInterface;
 
-/**
- * Implements hook_help().
- */
-function update_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.update':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Update Manager module periodically checks for new versions of your site\'s software (including contributed modules and themes), and alerts administrators to available updates. The Update Manager system is also used by some other modules to manage updates and downloads; for example, the Interface Translation module uses the Update Manager to download translations from the localization server. Note that whenever the Update Manager system is used, anonymous usage statistics are sent to Drupal.org. If desired, you may uninstall the Update Manager module from the <a href=":modules">Extend page</a>; if you do so, functionality that depends on the Update Manager system will not work. For more information, see the <a href=":update">online documentation for the Update Manager module</a>.', [':update' => 'https://www.drupal.org/documentation/modules/update', ':modules' => Url::fromRoute('system.modules_list')->toString()]) . '</p>';
-      // Only explain the Update manager if it has not been uninstalled.
-      if (_update_manager_access()) {
-        $output .= '<p>' . t('The Update Manager also allows administrators to add and update modules and themes through the administration interface.') . '</p>';
-      }
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Checking for available updates') . '</dt>';
-      $output .= '<dd>' . t('The <a href=":update-report">Available updates report</a> displays core, contributed modules, and themes for which there are new releases available for download. On the report page, you can also check manually for updates. You can configure the frequency of update checks, which are performed during cron runs, and whether notifications are sent on the <a href=":update-settings">Update Manager settings page</a>.', [':update-report' => Url::fromRoute('update.status')->toString(), ':update-settings' => Url::fromRoute('update.settings')->toString()]) . '</dd>';
-      // Only explain the Update manager if it has not been uninstalled.
-      if (_update_manager_access()) {
-        $output .= '<dt>' . t('Performing updates through the Update page') . '</dt>';
-        $output .= '<dd>' . t('The Update Manager module allows administrators to perform updates directly from the <a href=":update-page">Update page</a>. It lists all available updates, and you can confirm whether you want to download them. If you don\'t have sufficient access rights to your web server, you could be prompted for your FTP/SSH password. Afterwards the files are transferred into your site installation, overwriting your old files. Direct links to the Update page are also displayed on the <a href=":modules_page">Extend page</a> and the <a href=":themes_page">Appearance page</a>.', [':modules_page' => Url::fromRoute('system.modules_list')->toString(), ':themes_page' => Url::fromRoute('system.themes_page')->toString(), ':update-page' => Url::fromRoute('update.report_update')->toString()]) . '</dd>';
-      }
-      $output .= '</dl>';
-      return $output;
-
-    case 'update.status':
-      return '<p>' . t('Here you can find information about available updates for your installed modules and themes. Note that each module or theme is part of a "project", which may or may not have the same name, and might include multiple modules or themes within it.') . '</p>';
-
-    case 'system.modules_list':
-      return '<p>' . t('Regularly review <a href=":updates">available updates</a> and update as required to maintain a secure and current site. Always run the <a href=":update-php">update script</a> each time you update software.', [':update-php' => Url::fromRoute('system.db_update')->toString(), ':updates' => Url::fromRoute('update.status')->toString()]) . '</p>';
-
-  }
-}
-
-/**
- * Implements hook_page_top().
- */
-function update_page_top() {
-  /** @var \Drupal\Core\Routing\AdminContext $admin_context */
-  $admin_context = \Drupal::service('router.admin_context');
-  $route_match = \Drupal::routeMatch();
-  if ($admin_context->isAdminRoute($route_match->getRouteObject()) && \Drupal::currentUser()->hasPermission('view update notifications')) {
-    $route_name = \Drupal::routeMatch()->getRouteName();
-    switch ($route_name) {
-      // These pages don't need additional nagging.
-      case 'update.theme_update':
-      case 'update.module_update':
-      case 'update.status':
-      case 'update.report_update':
-      case 'update.settings':
-      case 'system.status':
-      case 'system.theme_install':
-      case 'update.confirmation_page':
-      case 'system.batch_page.html':
-        return;
-
-      // If we are on the appearance or modules list, display a detailed report
-      // of the update status.
-      case 'system.themes_page':
-      case 'system.modules_list':
-        $verbose = TRUE;
-        break;
-
-    }
-    \Drupal::moduleHandler()->loadInclude('update', 'install');
-    $status = update_requirements('runtime');
-    foreach (['core', 'contrib'] as $report_type) {
-      $type = 'update_' . $report_type;
-      // hook_requirements() supports render arrays therefore we need to render
-      // them before using
-      // \Drupal\Core\Messenger\MessengerInterface::addStatus().
-      if (isset($status[$type]['description']) && is_array($status[$type]['description'])) {
-        $status[$type]['description'] = \Drupal::service('renderer')->renderInIsolation($status[$type]['description']);
-      }
-      if (!empty($verbose)) {
-        if (isset($status[$type]['severity'])) {
-          if ($status[$type]['severity'] == REQUIREMENT_ERROR) {
-            \Drupal::messenger()->addError($status[$type]['description']);
-          }
-          elseif ($status[$type]['severity'] == REQUIREMENT_WARNING) {
-            \Drupal::messenger()->addWarning($status[$type]['description']);
-          }
-        }
-      }
-      // Otherwise, if we're on *any* admin page and there's a security
-      // update missing, print an error message about it.
-      else {
-        if (isset($status[$type])
-            && isset($status[$type]['reason'])
-            && $status[$type]['reason'] === UpdateManagerInterface::NOT_SECURE) {
-          \Drupal::messenger()->addError($status[$type]['description']);
-        }
-      }
-    }
-  }
-}
-
 /**
  * Resolves if the current user can access updater menu items.
  *
@@ -129,109 +25,6 @@ function _update_manager_access() {
   return Settings::get('allow_authorize_operations', TRUE) && \Drupal::currentUser()->hasPermission('administer software updates');
 }
 
-/**
- * Implements hook_theme().
- */
-function update_theme(): array {
-  return [
-    'update_last_check' => [
-      'variables' => ['last' => 0],
-    ],
-    'update_report' => [
-      'variables' => ['data' => NULL],
-      'file' => 'update.report.inc',
-    ],
-    'update_project_status' => [
-      'variables' => ['project' => []],
-      'file' => 'update.report.inc',
-    ],
-    // We are using template instead of '#type' => 'table' here to keep markup
-    // out of preprocess and allow for easier changes to markup.
-    'update_version' => [
-      'variables' => ['version' => NULL, 'title' => NULL, 'attributes' => []],
-      'file' => 'update.report.inc',
-    ],
-    'update_fetch_error_message' => [
-      'file' => 'update.report.inc',
-      'render element' => 'element',
-      'variables' => ['error_message' => []],
-    ],
-  ];
-}
-
-/**
- * Implements hook_cron().
- */
-function update_cron() {
-  $update_config = \Drupal::config('update.settings');
-  $frequency = $update_config->get('check.interval_days');
-  $interval = 60 * 60 * 24 * $frequency;
-  $last_check = \Drupal::state()->get('update.last_check', 0);
-  $request_time = \Drupal::time()->getRequestTime();
-  if (($request_time - $last_check) > $interval) {
-    // If the configured update interval has elapsed, we want to invalidate
-    // the data for all projects, attempt to re-fetch, and trigger any
-    // configured notifications about the new status.
-    update_refresh();
-    update_fetch_data();
-  }
-  else {
-    // Otherwise, see if any individual projects are now stale or still
-    // missing data, and if so, try to fetch the data.
-    update_get_available(TRUE);
-  }
-  $last_email_notice = \Drupal::state()->get('update.last_email_notification', 0);
-  if (($request_time - $last_email_notice) > $interval) {
-    // If configured time between notifications elapsed, send email about
-    // updates possibly available.
-    \Drupal::moduleHandler()->loadInclude('update', 'inc', 'update.fetch');
-    _update_cron_notify();
-  }
-
-  // Clear garbage from disk.
-  update_clear_update_disk_cache();
-}
-
-/**
- * Implements hook_themes_installed().
- *
- * If themes are installed, we invalidate the information of available updates.
- */
-function update_themes_installed($themes) {
-  // Clear all update module data.
-  update_storage_clear();
-}
-
-/**
- * Implements hook_themes_uninstalled().
- *
- * If themes are uninstalled, we invalidate the information of available updates.
- */
-function update_themes_uninstalled($themes) {
-  // Clear all update module data.
-  update_storage_clear();
-}
-
-/**
- * Implements hook_modules_installed().
- *
- * If modules are installed, we invalidate the information of available updates.
- */
-function update_modules_installed($modules) {
-  // Clear all update module data.
-  update_storage_clear();
-}
-
-/**
- * Implements hook_modules_uninstalled().
- *
- * If modules are uninstalled, we invalidate the information of available updates.
- */
-function update_modules_uninstalled($modules) {
-  // Clear all update module data.
-  update_storage_clear();
-}
-
 /**
  * Returns a warning message when there is no data about available updates.
  */
@@ -374,36 +167,6 @@ function update_fetch_data_finished($success, $results) {
   }
 }
 
-/**
- * Implements hook_mail().
- *
- * Constructs the email notification message when the site is out of date.
- *
- * @see \Drupal\Core\Mail\MailManagerInterface::mail()
- * @see _update_cron_notify()
- * @see _update_message_text()
- * @see \Drupal\update\UpdateManagerInterface
- */
-function update_mail($key, &$message, $params) {
-  $langcode = $message['langcode'];
-  $language = \Drupal::languageManager()->getLanguage($langcode);
-  $message['subject'] .= t('New release(s) available for @site_name', ['@site_name' => \Drupal::config('system.site')->get('name')], ['langcode' => $langcode]);
-  foreach ($params as $msg_type => $msg_reason) {
-    $message['body'][] = _update_message_text($msg_type, $msg_reason, $langcode);
-  }
-  $message['body'][] = t('See the available updates page for more information:', [], ['langcode' => $langcode]) . "\n" . Url::fromRoute('update.status', [], ['absolute' => TRUE, 'language' => $language])->toString();
-  if (_update_manager_access()) {
-    $message['body'][] = t('You can automatically download your missing updates using the Update manager:', [], ['langcode' => $langcode]) . "\n" . Url::fromRoute('update.report_update', [], ['absolute' => TRUE, 'language' => $language])->toString();
-  }
-  $settings_url = Url::fromRoute('update.settings', [], ['absolute' => TRUE])->toString();
-  if (\Drupal::config('update.settings')->get('notification.threshold') == 'all') {
-    $message['body'][] = t('Your site is currently configured to send these emails when any updates are available. To get notified only for security updates, @url.', ['@url' => $settings_url]);
-  }
-  else {
-    $message['body'][] = t('Your site is currently configured to send these emails only when security updates are available. To get notified for any available updates, @url.', ['@url' => $settings_url]);
-  }
-}
-
 /**
  * Returns the appropriate message text when site is out of date or not secure.
  *
@@ -512,77 +275,6 @@ function template_preprocess_update_last_check(&$variables) {
   $variables['link'] = Link::fromTextAndUrl(t('Check manually'), Url::fromRoute('update.manual_status', [], ['query' => \Drupal::destination()->getAsArray()]))->toString();
 }
 
-/**
- * Implements hook_verify_update_archive().
- *
- * First, we ensure that the archive isn't a copy of Drupal core, which the
- * update manager does not yet support. See https://www.drupal.org/node/606592.
- *
- * Then, we make sure that at least one module included in the archive file has
- * an .info.yml file which claims that the code is compatible with the current
- * version of Drupal core.
- *
- * @see \Drupal\Core\Extension\ExtensionDiscovery
- */
-function update_verify_update_archive($project, $archive_file, $directory) {
-  $errors = [];
-
-  // Make sure this isn't a tarball of Drupal core.
-  if (
-    file_exists("$directory/$project/index.php")
-    && file_exists("$directory/$project/core/install.php")
-    && file_exists("$directory/$project/core/includes/bootstrap.inc")
-    && file_exists("$directory/$project/core/modules/node/node.module")
-    && file_exists("$directory/$project/core/modules/system/system.module")
-  ) {
-    return [
-      'no-core' => t('Automatic updating of Drupal core is not supported. See the <a href=":update-guide">Updating Drupal guide</a> for information on how to update Drupal core manually.', [':update-guide' => 'https://www.drupal.org/docs/updating-drupal']),
-    ];
-  }
-
-  // Parse all the .info.yml files and make sure at least one is compatible with
-  // this version of Drupal core. If one is compatible, then the project as a
-  // whole is considered compatible (since, for example, the project may ship
-  // with some out-of-date modules that are not necessary for its overall
-  // functionality).
-  $compatible_project = FALSE;
-  $incompatible = [];
-  /** @var \Drupal\Core\File\FileSystemInterface $file_system */
-  $file_system = \Drupal::service('file_system');
-  $files = $file_system->scanDirectory("$directory/$project", '/.*\.info.yml$/', ['key' => 'name', 'min_depth' => 0]);
-  foreach ($files as $file) {
-    // Get the .info.yml file for the module or theme this file belongs to.
-    $info = \Drupal::service('info_parser')->parse($file->uri);
-
-    // If the module or theme is incompatible with Drupal core, set an error.
-    if ($info['core_incompatible']) {
-      $incompatible[] = !empty($info['name']) ? $info['name'] : t('Unknown');
-    }
-    else {
-      $compatible_project = TRUE;
-      break;
-    }
-  }
-
-  if (empty($files)) {
-    $errors[] = t('%archive_file does not contain any .info.yml files.', ['%archive_file' => $file_system->basename($archive_file)]);
-  }
-  elseif (!$compatible_project) {
-    $errors[] = \Drupal::translation()->formatPlural(
-      count($incompatible),
-      '%archive_file contains a version of %names that is not compatible with Drupal @version.',
-      '%archive_file contains versions of modules or themes that are not compatible with Drupal @version: %names',
-      [
-        '@version' => \Drupal::VERSION,
-        '%archive_file' => $file_system->basename($archive_file),
-        '%names' => implode(', ', $incompatible),
-      ]
-    );
-  }
-
-  return $errors;
-}
-
 /**
  * Invalidates stored data relating to update status.
  */
diff --git a/core/modules/update/update.report.inc b/core/modules/update/update.report.inc
index 8dbb17e094fff69a9a421a0d2baa63bdec4392c5..ec8bc36115e7b79e8e09bf0c0b001c20f824c179 100644
--- a/core/modules/update/update.report.inc
+++ b/core/modules/update/update.report.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Code required only when rendering the available updates report.
  */
 
 use Drupal\Core\Template\Attribute;
diff --git a/core/modules/user/src/Hook/UserHooks.php b/core/modules/user/src/Hook/UserHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..ecfc5a1555fcf5ea7d3eb737b9461724403ddcbc
--- /dev/null
+++ b/core/modules/user/src/Hook/UserHooks.php
@@ -0,0 +1,522 @@
+<?php
+
+namespace Drupal\user\Hook;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\user\Entity\Role;
+use Drupal\filter\FilterFormatInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\system\Entity\Action;
+use Drupal\Component\Assertion\Inspector;
+use Drupal\user\RoleInterface;
+use Drupal\Component\Render\PlainTextOutput;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\image\Plugin\Field\FieldType\ImageItem;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\user\UserInterface;
+use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for user.
+ */
+class UserHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.user':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The User module allows users to register, log in, and log out. It also allows users with proper permissions to manage user roles and permissions. For more information, see the <a href=":user_docs">online documentation for the User module</a>.', [':user_docs' => 'https://www.drupal.org/documentation/modules/user']) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Creating and managing users') . '</dt>';
+        $output .= '<dd>' . t('Through the <a href=":people">People administration page</a> you can add and cancel user accounts and assign users to roles. By editing one particular user you can change their username, email address, password, and information in other fields.', [':people' => Url::fromRoute('entity.user.collection')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Configuring user roles') . '</dt>';
+        $output .= '<dd>' . t('<em>Roles</em> are used to group and classify users; each user can be assigned one or more roles. Typically there are two pre-defined roles: <em>Anonymous user</em> (users that are not logged in), and <em>Authenticated user</em> (users that are registered and logged in). Depending on how your site was set up, an <em>Administrator</em> role may also be available: users with this role will automatically be assigned any new permissions whenever a module is installed. You can create additional roles on the <a href=":roles">Roles administration page</a>.', [
+          ':roles' => Url::fromRoute('entity.user_role.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Setting permissions') . '</dt>';
+        $output .= '<dd>' . t('After creating roles, you can set permissions for each role on the <a href=":permissions_user">Permissions page</a>. Granting a permission allows users who have been assigned a particular role to perform an action on the site, such as viewing content, editing or creating  a particular type of content, administering settings for a particular module, or using a particular function of the site (such as search).', [
+          ':permissions_user' => Url::fromRoute('user.admin_permissions')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Other permissions pages') . '</dt>';
+        $output .= '<dd>' . t('The main Permissions page can be overwhelming, so each module that defines permissions has its own page for setting them. There are links to these pages on the <a href=":modules">Extend page</a>. When editing a content type, vocabulary, etc., there is also a Manage permissions tab for permissions related to that configuration.', [':modules' => Url::fromRoute('system.modules_list')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Managing account settings') . '</dt>';
+        $output .= '<dd>' . t('The <a href=":accounts">Account settings page</a> allows you to manage settings for the displayed name of the Anonymous user role, personal contact forms, user registration settings, and account cancellation settings. On this page you can also manage settings for account personalization, and adapt the text for the email messages that users receive when they register or request a password recovery. You may also set which role is automatically assigned new permissions whenever a module is installed (the Administrator role).', [':accounts' => Url::fromRoute('entity.user.admin_form')->toString()]) . '</dd>';
+        $output .= '<dt>' . t('Managing user account fields') . '</dt>';
+        $output .= '<dd>' . t('Because User accounts are an entity type, you can extend them by adding fields through the Manage fields tab on the <a href=":accounts">Account settings page</a>. By adding fields for e.g., a picture, a biography, or address, you can a create a custom profile for the users of the website. For background information on entities and fields, see the <a href=":field_help">Field module help page</a>.', [
+          ':field_help' => \Drupal::moduleHandler()->moduleExists('field') ? Url::fromRoute('help.page', [
+            'name' => 'field',
+          ])->toString() : '#',
+          ':accounts' => Url::fromRoute('entity.user.admin_form')->toString(),
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+
+      case 'user.admin_create':
+        return '<p>' . t("This web page allows administrators to register new users. Users' email addresses and usernames must be unique.") . '</p>';
+
+      case 'user.admin_permissions':
+        return '<p>' . t('Permissions let you control what users can do and see on your site. You can define a specific set of permissions for each role. (See the <a href=":role">Roles</a> page to create a role.) Any permissions granted to the Authenticated user role will be given to any user who is logged in to your site. On the <a href=":settings">Role settings</a> page, you can make any role into an Administrator role for the site, meaning that role will be granted all permissions. You should be careful to ensure that only trusted users are given this access and level of control of your site.', [
+          ':role' => Url::fromRoute('entity.user_role.collection')->toString(),
+          ':settings' => Url::fromRoute('user.role.settings')->toString(),
+        ]) . '</p>';
+
+      case 'entity.user_role.collection':
+        return '<p>' . t('A role defines a group of users that have certain privileges. These privileges are defined on the <a href=":permissions">Permissions page</a>. Here, you can define the names and the display sort order of the roles on your site. It is recommended to order roles from least permissive (for example, Anonymous user) to most permissive (for example, Administrator user). Users who are not logged in have the Anonymous user role. Users who are logged in have the Authenticated user role, plus any other roles granted to their user account.', [
+          ':permissions' => Url::fromRoute('user.admin_permissions')->toString(),
+        ]) . '</p>';
+
+      case 'entity.user.field_ui_fields':
+        return '<p>' . t('This form lets administrators add and edit fields for storing user data.') . '</p>';
+
+      case 'entity.entity_form_display.user.default':
+        return '<p>' . t('This form lets administrators configure how form fields should be displayed when editing a user profile.') . '</p>';
+
+      case 'entity.entity_view_display.user.default':
+        return '<p>' . t('This form lets administrators configure how fields should be displayed when rendering a user profile page.') . '</p>';
+    }
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+      'user' => [
+        'render element' => 'elements',
+      ],
+      'username' => [
+        'variables' => [
+          'account' => NULL,
+          'attributes' => [],
+          'link_options' => [],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_js_settings_alter().
+   */
+  #[Hook('js_settings_alter')]
+  public function jsSettingsAlter(&$settings, AttachedAssetsInterface $assets) {
+    // Provide the user ID in drupalSettings to allow JavaScript code to customize
+    // the experience for the end user, rather than the server side, which would
+    // break the render cache.
+    // Similarly, provide a permissions hash, so that permission-dependent data
+    // can be reliably cached on the client side.
+    $user = \Drupal::currentUser();
+    $settings['user']['uid'] = $user->id();
+    $settings['user']['permissionsHash'] = \Drupal::service('user_permissions_hash_generator')->generate($user);
+  }
+
+  /**
+   * Implements hook_entity_extra_field_info().
+   */
+  #[Hook('entity_extra_field_info')]
+  public function entityExtraFieldInfo() {
+    $fields['user']['user']['form']['account'] = [
+      'label' => t('User name and password'),
+      'description' => t('User module account form elements.'),
+      'weight' => -10,
+    ];
+    $fields['user']['user']['form']['language'] = [
+      'label' => t('Language settings'),
+      'description' => t('User module form element.'),
+      'weight' => 0,
+    ];
+    if (\Drupal::config('system.date')->get('timezone.user.configurable')) {
+      $fields['user']['user']['form']['timezone'] = [
+        'label' => t('Timezone'),
+        'description' => t('System module form element.'),
+        'weight' => 6,
+      ];
+    }
+    $fields['user']['user']['display']['member_for'] = [
+      'label' => t('Member for'),
+      'description' => t("User module 'member for' view element."),
+      'weight' => 5,
+    ];
+    return $fields;
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave() for user entities.
+   *
+   * @todo https://www.drupal.org/project/drupal/issues/3112704 Move to
+   *   \Drupal\user\Entity\User::preSave().
+   */
+  #[Hook('user_presave')]
+  public function userPresave(UserInterface $account) {
+    $config = \Drupal::config('system.date');
+    if ($config->get('timezone.user.configurable') && !$account->getTimeZone() && !$config->get('timezone.user.default')) {
+      $account->timezone = $config->get('timezone.default');
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_view() for user entities.
+   */
+  #[Hook('user_view')]
+  public function userView(array &$build, UserInterface $account, EntityViewDisplayInterface $display) {
+    if ($account->isAuthenticated() && $display->getComponent('member_for')) {
+      $build['member_for'] = [
+        '#type' => 'item',
+        '#markup' => '<h4 class="label">' . t('Member for') . '</h4> ' . \Drupal::service('date.formatter')->formatTimeDiffSince($account->getCreatedTime()),
+      ];
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_view_alter() for user entities.
+   *
+   * This function adds a default alt tag to the user_picture field to maintain
+   * accessibility.
+   */
+  #[Hook('user_view_alter')]
+  public function userViewAlter(array &$build, UserInterface $account, EntityViewDisplayInterface $display) {
+    if (!empty($build['user_picture']) && user_picture_enabled()) {
+      foreach (Element::children($build['user_picture']) as $key) {
+        if (!isset($build['user_picture'][$key]['#item']) || !$build['user_picture'][$key]['#item'] instanceof ImageItem) {
+          // User picture field is provided by standard profile install. If the
+          // display is configured to use a different formatter, the #item render
+          // key may not exist, or may not be an image field.
+          continue;
+        }
+        /** @var \Drupal\image\Plugin\Field\FieldType\ImageItem $item */
+        $item = $build['user_picture'][$key]['#item'];
+        if (!$item->get('alt')->getValue()) {
+          $item->get('alt')->setValue(\Drupal::translation()->translate('Profile picture for user @username', ['@username' => $account->getAccountName()]));
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_template_preprocess_default_variables_alter().
+   *
+   * @see user_user_login()
+   * @see user_user_logout()
+   */
+  #[Hook('template_preprocess_default_variables_alter')]
+  public function templatePreprocessDefaultVariablesAlter(&$variables) {
+    $user = \Drupal::currentUser();
+    $variables['user'] = clone $user;
+    // Remove password and session IDs, since themes should not need nor see them.
+    unset($variables['user']->pass, $variables['user']->sid, $variables['user']->ssid);
+    $variables['is_admin'] = $user->hasPermission('access administration pages');
+    $variables['logged_in'] = $user->isAuthenticated();
+  }
+
+  /**
+   * Implements hook_user_login().
+   */
+  #[Hook('user_login')]
+  public function userLogin(UserInterface $account) {
+    // Reset static cache of default variables in template_preprocess() to reflect
+    // the new user.
+    drupal_static_reset('template_preprocess');
+    // If the user has a NULL time zone, notify them to set a time zone.
+    $config = \Drupal::config('system.date');
+    if (!$account->getTimezone() && $config->get('timezone.user.configurable') && $config->get('timezone.user.warn')) {
+      \Drupal::messenger()->addStatus(t('Configure your <a href=":user-edit">account time zone setting</a>.', [
+        ':user-edit' => $account->toUrl('edit-form', [
+          'query' => \Drupal::destination()->getAsArray(),
+          'fragment' => 'edit-timezone',
+        ])->toString(),
+      ]));
+    }
+  }
+
+  /**
+   * Implements hook_user_logout().
+   */
+  #[Hook('user_logout')]
+  public function userLogout(AccountInterface $account) {
+    // Reset static cache of default variables in template_preprocess() to reflect
+    // the new user.
+    drupal_static_reset('template_preprocess');
+  }
+
+  /**
+   * Implements hook_mail().
+   */
+  #[Hook('mail')]
+  public function mail($key, &$message, $params) {
+    $token_service = \Drupal::token();
+    $language_manager = \Drupal::languageManager();
+    $langcode = $message['langcode'];
+    $variables = ['user' => $params['account']];
+    $language = $language_manager->getLanguage($langcode);
+    $original_language = $language_manager->getConfigOverrideLanguage();
+    $language_manager->setConfigOverrideLanguage($language);
+    $mail_config = \Drupal::config('user.mail');
+    $token_options = ['langcode' => $langcode, 'callback' => 'user_mail_tokens', 'clear' => TRUE];
+    $message['subject'] .= PlainTextOutput::renderFromHtml($token_service->replace($mail_config->get($key . '.subject'), $variables, $token_options));
+    $message['body'][] = $token_service->replace($mail_config->get($key . '.body'), $variables, $token_options);
+    $language_manager->setConfigOverrideLanguage($original_language);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for user_role entities.
+   */
+  #[Hook('user_role_insert')]
+  public function userRoleInsert(RoleInterface $role) {
+    // Ignore the authenticated and anonymous roles or the role is being synced.
+    if (in_array($role->id(), [
+      RoleInterface::AUTHENTICATED_ID,
+      RoleInterface::ANONYMOUS_ID,
+    ]) || $role->isSyncing()) {
+      return;
+    }
+    assert(Inspector::assertStringable($role->label()), 'Role label is expected to be a string.');
+    $add_id = 'user_add_role_action.' . $role->id();
+    if (!Action::load($add_id)) {
+      $action = Action::create([
+        'id' => $add_id,
+        'type' => 'user',
+        'label' => t('Add the @label role to the selected user(s)', [
+          '@label' => $role->label(),
+        ]),
+        'configuration' => [
+          'rid' => $role->id(),
+        ],
+        'plugin' => 'user_add_role_action',
+      ]);
+      $action->trustData()->save();
+    }
+    $remove_id = 'user_remove_role_action.' . $role->id();
+    if (!Action::load($remove_id)) {
+      $action = Action::create([
+        'id' => $remove_id,
+        'type' => 'user',
+        'label' => t('Remove the @label role from the selected user(s)', [
+          '@label' => $role->label(),
+        ]),
+        'configuration' => [
+          'rid' => $role->id(),
+        ],
+        'plugin' => 'user_remove_role_action',
+      ]);
+      $action->trustData()->save();
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for user_role entities.
+   */
+  #[Hook('user_role_delete')]
+  public function userRoleDelete(RoleInterface $role) {
+    // Delete role references for all users.
+    $user_storage = \Drupal::entityTypeManager()->getStorage('user');
+    $user_storage->deleteRoleReferences([$role->id()]);
+    // Ignore the authenticated and anonymous roles or the role is being synced.
+    if (in_array($role->id(), [
+      RoleInterface::AUTHENTICATED_ID,
+      RoleInterface::ANONYMOUS_ID,
+    ]) || $role->isSyncing()) {
+      return;
+    }
+    $actions = Action::loadMultiple(['user_add_role_action.' . $role->id(), 'user_remove_role_action.' . $role->id()]);
+    foreach ($actions as $action) {
+      $action->delete();
+    }
+  }
+
+  /**
+   * Implements hook_element_info_alter().
+   */
+  #[Hook('element_info_alter')]
+  public function elementInfoAlter(array &$types) {
+    if (isset($types['password_confirm'])) {
+      $types['password_confirm']['#process'][] = 'user_form_process_password_confirm';
+    }
+  }
+
+  /**
+   * Implements hook_modules_uninstalled().
+   */
+  #[Hook('modules_uninstalled')]
+  public function modulesUninstalled($modules) {
+    // Remove any potentially orphan module data stored for users.
+    \Drupal::service('user.data')->delete($modules);
+  }
+
+  /**
+   * Implements hook_toolbar().
+   */
+  #[Hook('toolbar')]
+  public function toolbar() {
+    $user = \Drupal::currentUser();
+    $items['user'] = [
+      '#type' => 'toolbar_item',
+      'tab' => [
+        '#type' => 'link',
+        '#title' => $user->getDisplayName(),
+        '#url' => Url::fromRoute('user.page'),
+        '#attributes' => [
+          'title' => t('My account'),
+          'class' => [
+            'toolbar-icon',
+            'toolbar-icon-user',
+          ],
+        ],
+        '#cache' => [
+                  // Vary cache for anonymous and authenticated users.
+          'contexts' => [
+            'user.roles:anonymous',
+          ],
+        ],
+      ],
+      'tray' => [
+        '#heading' => t('User account actions'),
+      ],
+      '#weight' => 100,
+      '#attached' => [
+        'library' => [
+          'user/drupal.user.icons',
+        ],
+      ],
+    ];
+    if ($user->isAnonymous()) {
+      $links = [
+        'login' => [
+          'title' => t('Log in'),
+          'url' => Url::fromRoute('user.page'),
+        ],
+      ];
+      $items['user']['tray']['user_links'] = [
+        '#theme' => 'links__toolbar_user',
+        '#links' => $links,
+        '#attributes' => [
+          'class' => [
+            'toolbar-menu',
+          ],
+        ],
+      ];
+    }
+    else {
+      $items['user']['tab']['#title'] = [
+        '#lazy_builder' => [
+          'user.toolbar_link_builder:renderDisplayName',
+                [],
+        ],
+        '#create_placeholder' => TRUE,
+        '#lazy_builder_preview' => [
+                // Add a line of whitespace to the placeholder to ensure the icon is
+                // positioned in the same place it will be when the lazy loaded content
+                // appears.
+          '#markup' => '&nbsp;',
+        ],
+      ];
+      $items['user']['tray']['user_links'] = [
+        '#lazy_builder' => [
+          'user.toolbar_link_builder:renderToolbarLinks',
+                [],
+        ],
+        '#create_placeholder' => TRUE,
+        '#lazy_builder_preview' => [
+          '#markup' => '<a href="#" class="toolbar-tray-lazy-placeholder-link">&nbsp;</a>',
+        ],
+      ];
+    }
+    return $items;
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for \Drupal\system\Form\RegionalForm.
+   */
+  #[Hook('form_system_regional_settings_alter')]
+  public function formSystemRegionalSettingsAlter(&$form, FormStateInterface $form_state) : void {
+    $config = \Drupal::config('system.date');
+    $form['timezone']['configurable_timezones'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Users may set their own time zone'),
+      '#default_value' => $config->get('timezone.user.configurable'),
+    ];
+    $form['timezone']['configurable_timezones_wrapper'] = [
+      '#type' => 'container',
+      '#states' => [
+              // Hide the user configured timezone settings when users are forced to use
+              // the default setting.
+        'invisible' => [
+          'input[name="configurable_timezones"]' => [
+            'checked' => FALSE,
+          ],
+        ],
+      ],
+    ];
+    $form['timezone']['configurable_timezones_wrapper']['empty_timezone_message'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Remind users at login if their time zone is not set'),
+      '#default_value' => $config->get('timezone.user.warn'),
+      '#description' => t('Only applied if users may set their own time zone.'),
+    ];
+    $form['timezone']['configurable_timezones_wrapper']['user_default_timezone'] = [
+      '#type' => 'radios',
+      '#title' => t('Time zone for new users'),
+      '#default_value' => $config->get('timezone.user.default'),
+      '#options' => [
+        UserInterface::TIMEZONE_DEFAULT => t('Default time zone'),
+        UserInterface::TIMEZONE_EMPTY => t('Empty time zone'),
+        UserInterface::TIMEZONE_SELECT => t('Users may set their own time zone at registration'),
+      ],
+      '#description' => t('Only applied if users may set their own time zone.'),
+    ];
+    $form['#submit'][] = 'user_form_system_regional_settings_submit';
+  }
+
+  /**
+   * Implements hook_filter_format_disable().
+   */
+  #[Hook('filter_format_disable')]
+  public function filterFormatDisable(FilterFormatInterface $filter_format) {
+    // Remove the permission from any roles.
+    $permission = $filter_format->getPermissionName();
+    /** @var \Drupal\user\Entity\Role $role */
+    foreach (Role::loadMultiple() as $role) {
+      if ($role->hasPermission($permission)) {
+        $role->revokePermission($permission)->save();
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $entity) {
+    // Add Manage permissions link if this entity type defines the permissions
+    // link template.
+    if (!$entity->hasLinkTemplate('entity-permissions-form')) {
+      return [];
+    }
+    $bundle_entity_type = $entity->bundle();
+    $route = "entity.{$bundle_entity_type}.entity_permissions_form";
+    if (empty(\Drupal::service('router.route_provider')->getRoutesByNames([$route]))) {
+      return [];
+    }
+    $url = Url::fromRoute($route, [$bundle_entity_type => $entity->id()]);
+    if (!$url->access()) {
+      return [];
+    }
+    return [
+      'manage-permissions' => [
+        'title' => t('Manage permissions'),
+        'weight' => 50,
+        'url' => $url,
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/user/src/Hook/UserTokensHooks.php b/core/modules/user/src/Hook/UserTokensHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f52898cffd8f04d51374f4021441cb0c241c878c
--- /dev/null
+++ b/core/modules/user/src/Hook/UserTokensHooks.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Drupal\user\Hook;
+
+use Drupal\user\Entity\User;
+use Drupal\Core\Datetime\Entity\DateFormat;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for user.
+ */
+class UserTokensHooks {
+
+  /**
+   * Implements hook_token_info().
+   */
+  #[Hook('token_info')]
+  public function tokenInfo() {
+    $types['user'] = [
+      'name' => t('Users'),
+      'description' => t('Tokens related to individual user accounts.'),
+      'needs-data' => 'user',
+    ];
+    $types['current-user'] = [
+      'name' => t('Current user'),
+      'description' => t('Tokens related to the currently logged in user.'),
+      'type' => 'user',
+    ];
+    $user['uid'] = ['name' => t('User ID'), 'description' => t("The unique ID of the user account.")];
+    $user['uuid'] = ['name' => t('UUID'), 'description' => t("The UUID of the user account.")];
+    $user['name'] = [
+      'name' => t("Deprecated: User Name"),
+      'description' => t("Deprecated: Use account-name or display-name instead."),
+    ];
+    $user['account-name'] = [
+      'name' => t("Account Name"),
+      'description' => t("The login name of the user account."),
+    ];
+    $user['display-name'] = [
+      'name' => t("Display Name"),
+      'description' => t("The display name of the user account."),
+    ];
+    $user['mail'] = [
+      'name' => t("Email"),
+      'description' => t("The email address of the user account."),
+    ];
+    $user['url'] = ['name' => t("URL"), 'description' => t("The URL of the account profile page.")];
+    $user['edit-url'] = ['name' => t("Edit URL"), 'description' => t("The URL of the account edit page.")];
+    $user['last-login'] = [
+      'name' => t("Last login"),
+      'description' => t("The date the user last logged in to the site."),
+      'type' => 'date',
+    ];
+    $user['created'] = [
+      'name' => t("Created"),
+      'description' => t("The date the user account was created."),
+      'type' => 'date',
+    ];
+    return ['types' => $types, 'tokens' => ['user' => $user]];
+  }
+
+  /**
+   * Implements hook_tokens().
+   */
+  #[Hook('tokens')]
+  public function tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+    $token_service = \Drupal::token();
+    $url_options = ['absolute' => TRUE];
+    if (isset($options['langcode'])) {
+      $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
+      $langcode = $options['langcode'];
+    }
+    else {
+      $langcode = NULL;
+    }
+    $replacements = [];
+    if ($type == 'user' && !empty($data['user'])) {
+      /** @var \Drupal\user\UserInterface $account */
+      $account = $data['user'];
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          // Basic user account information.
+          case 'uid':
+            // In the case of hook user_presave uid is not set yet.
+            $replacements[$original] = $account->id() ?: t('not yet assigned');
+            break;
+
+          case 'uuid':
+            $replacements[$original] = $account->uuid();
+            break;
+
+          case 'display-name':
+            $replacements[$original] = $account->getDisplayName();
+            if ($account->isAnonymous()) {
+              $bubbleable_metadata->addCacheableDependency(\Drupal::config('user.settings'));
+            }
+            break;
+
+          case 'name':
+          case 'account-name':
+            $display_name = $account->getAccountName();
+            $replacements[$original] = $display_name;
+            if ($account->isAnonymous()) {
+              $bubbleable_metadata->addCacheableDependency(\Drupal::config('user.settings'));
+            }
+            break;
+
+          case 'mail':
+            $replacements[$original] = $account->getEmail();
+            break;
+
+          case 'url':
+            $replacements[$original] = $account->id() ? $account->toUrl('canonical', $url_options)->toString() : t('not yet assigned');
+            break;
+
+          case 'edit-url':
+            $replacements[$original] = $account->id() ? $account->toUrl('edit-form', $url_options)->toString() : t('not yet assigned');
+            break;
+
+          // These tokens are default variations on the chained tokens handled below.
+          case 'last-login':
+            $date_format = DateFormat::load('medium');
+            $bubbleable_metadata->addCacheableDependency($date_format);
+            $replacements[$original] = $account->getLastLoginTime() ? \Drupal::service('date.formatter')->format($account->getLastLoginTime(), 'medium', '', NULL, $langcode) : t('never');
+            break;
+
+          case 'created':
+            $date_format = DateFormat::load('medium');
+            $bubbleable_metadata->addCacheableDependency($date_format);
+            // In the case of user_presave the created date may not yet be set.
+            $replacements[$original] = $account->getCreatedTime() ? \Drupal::service('date.formatter')->format($account->getCreatedTime(), 'medium', '', NULL, $langcode) : t('not yet created');
+            break;
+        }
+      }
+      if ($login_tokens = $token_service->findWithPrefix($tokens, 'last-login')) {
+        $replacements += $token_service->generate('date', $login_tokens, ['date' => $account->getLastLoginTime()], $options, $bubbleable_metadata);
+      }
+      if ($registered_tokens = $token_service->findWithPrefix($tokens, 'created')) {
+        $replacements += $token_service->generate('date', $registered_tokens, ['date' => $account->getCreatedTime()], $options, $bubbleable_metadata);
+      }
+    }
+    if ($type == 'current-user') {
+      $account = User::load(\Drupal::currentUser()->id());
+      $bubbleable_metadata->addCacheContexts(['user']);
+      $replacements += $token_service->generate('user', $tokens, ['user' => $account], $options, $bubbleable_metadata);
+    }
+    return $replacements;
+  }
+
+}
diff --git a/core/modules/user/src/Hook/UserViewsExecutionHooks.php b/core/modules/user/src/Hook/UserViewsExecutionHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..0acca99b3ea2d471757d43484a25eee4417424d5
--- /dev/null
+++ b/core/modules/user/src/Hook/UserViewsExecutionHooks.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\user\Hook;
+
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for user.
+ */
+class UserViewsExecutionHooks {
+
+  /**
+   * Implements hook_views_query_substitutions().
+   *
+   * Allow replacement of current user ID so we can cache these queries.
+   */
+  #[Hook('views_query_substitutions')]
+  public function viewsQuerySubstitutions(ViewExecutable $view) {
+    return ['***CURRENT_USER***' => \Drupal::currentUser()->id()];
+  }
+
+}
diff --git a/core/modules/user/src/Hook/UserViewsHooks.php b/core/modules/user/src/Hook/UserViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..6d3e3e3fb0f95c0aa209f96a2b73e6dda6cf5a03
--- /dev/null
+++ b/core/modules/user/src/Hook/UserViewsHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\user\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for user.
+ */
+class UserViewsHooks {
+  /**
+   * @file
+   * Provide views data for user.module.
+   */
+
+  /**
+   * Implements hook_views_plugins_argument_validator_alter().
+   */
+  #[Hook('views_plugins_argument_validator_alter')]
+  public function viewsPluginsArgumentValidatorAlter(array &$plugins) {
+    $plugins['entity:user']['title'] = t('User ID');
+    $plugins['entity:user']['class'] = 'Drupal\user\Plugin\views\argument_validator\User';
+    $plugins['entity:user']['provider'] = 'user';
+  }
+
+}
diff --git a/core/modules/user/tests/modules/user_access_test/src/Hook/UserAccessTestHooks.php b/core/modules/user/tests/modules/user_access_test/src/Hook/UserAccessTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..a346f6d40cca177acbf50c0f9a2c8ea25c343b2d
--- /dev/null
+++ b/core/modules/user/tests/modules/user_access_test/src/Hook/UserAccessTestHooks.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\user_access_test\Hook;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\user\Entity\User;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for user_access_test.
+ */
+class UserAccessTestHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_access() for entity type "user".
+   */
+  #[Hook('user_access')]
+  public function userAccess(User $entity, $operation, $account) {
+    if ($entity->getAccountName() == "no_edit" && $operation == "update") {
+      // Deny edit access.
+      return AccessResult::forbidden();
+    }
+    if ($entity->getAccountName() == "no_delete" && $operation == "delete") {
+      // Deny delete access.
+      return AccessResult::forbidden();
+    }
+    // Account with role sub-admin can manage users with no roles.
+    if (count($entity->getRoles()) == 1) {
+      return AccessResult::allowedIfHasPermission($account, 'sub-admin');
+    }
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Implements hook_entity_create_access().
+   */
+  #[Hook('entity_create_access')]
+  public function entityCreateAccess(AccountInterface $account, array $context, $entity_bundle) {
+    if ($context['entity_type_id'] != 'user') {
+      return AccessResult::neutral();
+    }
+    // Account with role sub-admin can create users.
+    return AccessResult::allowedIfHasPermission($account, 'sub-admin');
+  }
+
+  /**
+   * Implements hook_entity_field_access().
+   */
+  #[Hook('entity_field_access')]
+  public function entityFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
+    // Account with role sub-admin can view the status, init and mail fields for
+    // user with no roles.
+    if ($field_definition->getTargetEntityTypeId() == 'user' && $operation === 'view' && in_array($field_definition->getName(), ['status', 'init', 'mail'])) {
+      if ($items == NULL || count($items->getEntity()->getRoles()) == 1) {
+        return AccessResult::allowedIfHasPermission($account, 'sub-admin');
+      }
+    }
+    if (\Drupal::state()->get('user_access_test_forbid_mail_edit', FALSE)) {
+      if ($operation === 'edit' && $items && $items->getEntity()->getEntityTypeId() === 'user' && $field_definition->getName() === 'mail') {
+        return AccessResult::forbidden();
+      }
+    }
+    return AccessResult::neutral();
+  }
+
+}
diff --git a/core/modules/user/tests/modules/user_access_test/user_access_test.module b/core/modules/user/tests/modules/user_access_test/user_access_test.module
deleted file mode 100644
index d202abd1bab5ea9937c5684ebd10684818a92daf..0000000000000000000000000000000000000000
--- a/core/modules/user/tests/modules/user_access_test/user_access_test.module
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-/**
- * @file
- * Dummy module implementing hook_user_access() to test if entity access is respected.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\user\Entity\User;
-
-/**
- * Implements hook_ENTITY_TYPE_access() for entity type "user".
- */
-function user_access_test_user_access(User $entity, $operation, $account) {
-  if ($entity->getAccountName() == "no_edit" && $operation == "update") {
-    // Deny edit access.
-    return AccessResult::forbidden();
-  }
-  if ($entity->getAccountName() == "no_delete" && $operation == "delete") {
-    // Deny delete access.
-    return AccessResult::forbidden();
-  }
-
-  // Account with role sub-admin can manage users with no roles.
-  if (count($entity->getRoles()) == 1) {
-    return AccessResult::allowedIfHasPermission($account, 'sub-admin');
-  }
-
-  return AccessResult::neutral();
-}
-
-/**
- * Implements hook_entity_create_access().
- */
-function user_access_test_entity_create_access(AccountInterface $account, array $context, $entity_bundle) {
-  if ($context['entity_type_id'] != 'user') {
-    return AccessResult::neutral();
-  }
-
-  // Account with role sub-admin can create users.
-  return AccessResult::allowedIfHasPermission($account, 'sub-admin');
-}
-
-/**
- * Implements hook_entity_field_access().
- */
-function user_access_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
-  // Account with role sub-admin can view the status, init and mail fields for
-  // user with no roles.
-  if ($field_definition->getTargetEntityTypeId() == 'user' && $operation === 'view' && in_array($field_definition->getName(), ['status', 'init', 'mail'])) {
-    if (($items == NULL) || (count($items->getEntity()->getRoles()) == 1)) {
-      return AccessResult::allowedIfHasPermission($account, 'sub-admin');
-    }
-  }
-
-  if (\Drupal::state()->get('user_access_test_forbid_mail_edit', FALSE)) {
-    if ($operation === 'edit' && $items && $items->getEntity()->getEntityTypeId() === 'user' && $field_definition->getName() === 'mail') {
-      return AccessResult::forbidden();
-    }
-  }
-
-  return AccessResult::neutral();
-}
diff --git a/core/modules/user/tests/modules/user_form_test/src/Hook/UserFormTestHooks.php b/core/modules/user/tests/modules/user_form_test/src/Hook/UserFormTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f49008597f10aeb6aab1e9b198c9c4414ded3435
--- /dev/null
+++ b/core/modules/user/tests/modules/user_form_test/src/Hook/UserFormTestHooks.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\user_form_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for user_form_test.
+ */
+class UserFormTestHooks {
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for user_cancel_form().
+   */
+  #[Hook('form_user_cancel_form_alter')]
+  public function formUserCancelFormAlter(&$form, &$form_state) : void {
+    $form['user_cancel_confirm']['#default_value'] = FALSE;
+    $form['access']['#value'] = \Drupal::currentUser()->hasPermission('cancel other accounts');
+  }
+
+}
diff --git a/core/modules/user/tests/modules/user_form_test/user_form_test.module b/core/modules/user/tests/modules/user_form_test/user_form_test.module
deleted file mode 100644
index 5ac37190ee8bf9779bc9a6cdc037f87db5547224..0000000000000000000000000000000000000000
--- a/core/modules/user/tests/modules/user_form_test/user_form_test.module
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-/**
- * @file
- * Support module for user form testing.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_form_FORM_ID_alter() for user_cancel_form().
- */
-function user_form_test_form_user_cancel_form_alter(&$form, &$form_state): void {
-  $form['user_cancel_confirm']['#default_value'] = FALSE;
-  $form['access']['#value'] = \Drupal::currentUser()->hasPermission('cancel other accounts');
-}
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 8b956f5b05e7806df533c72b9b3fdae7c83cf692..98fb803fe130d111977fa32070c02c8f046b9042 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -2,107 +2,22 @@
 
 /**
  * @file
- * Enables the user registration and login system.
  */
 
-use Drupal\Component\Assertion\Inspector;
 use Drupal\Component\Utility\Crypt;
-use Drupal\Component\Render\PlainTextOutput;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Access\AccessibleInterface;
-use Drupal\Core\Asset\AttachedAssetsInterface;
 use Drupal\Core\Batch\BatchBuilder;
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
-use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\Element;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Session\AnonymousUserSession;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\Url;
-use Drupal\image\Plugin\Field\FieldType\ImageItem;
-use Drupal\filter\FilterFormatInterface;
-use Drupal\system\Entity\Action;
 use Drupal\user\Entity\Role;
 use Drupal\user\Entity\User;
-use Drupal\user\RoleInterface;
 use Drupal\user\UserInterface;
 
-/**
- * Implements hook_help().
- */
-function user_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.user':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The User module allows users to register, log in, and log out. It also allows users with proper permissions to manage user roles and permissions. For more information, see the <a href=":user_docs">online documentation for the User module</a>.', [':user_docs' => 'https://www.drupal.org/documentation/modules/user']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Creating and managing users') . '</dt>';
-      $output .= '<dd>' . t('Through the <a href=":people">People administration page</a> you can add and cancel user accounts and assign users to roles. By editing one particular user you can change their username, email address, password, and information in other fields.', [':people' => Url::fromRoute('entity.user.collection')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Configuring user roles') . '</dt>';
-      $output .= '<dd>' . t('<em>Roles</em> are used to group and classify users; each user can be assigned one or more roles. Typically there are two pre-defined roles: <em>Anonymous user</em> (users that are not logged in), and <em>Authenticated user</em> (users that are registered and logged in). Depending on how your site was set up, an <em>Administrator</em> role may also be available: users with this role will automatically be assigned any new permissions whenever a module is installed. You can create additional roles on the <a href=":roles">Roles administration page</a>.', [':roles' => Url::fromRoute('entity.user_role.collection')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Setting permissions') . '</dt>';
-      $output .= '<dd>' . t('After creating roles, you can set permissions for each role on the <a href=":permissions_user">Permissions page</a>. Granting a permission allows users who have been assigned a particular role to perform an action on the site, such as viewing content, editing or creating  a particular type of content, administering settings for a particular module, or using a particular function of the site (such as search).', [':permissions_user' => Url::fromRoute('user.admin_permissions')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Other permissions pages') . '</dt>';
-      $output .= '<dd>' . t('The main Permissions page can be overwhelming, so each module that defines permissions has its own page for setting them. There are links to these pages on the <a href=":modules">Extend page</a>. When editing a content type, vocabulary, etc., there is also a Manage permissions tab for permissions related to that configuration.', [':modules' => Url::fromRoute('system.modules_list')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Managing account settings') . '</dt>';
-      $output .= '<dd>' . t('The <a href=":accounts">Account settings page</a> allows you to manage settings for the displayed name of the Anonymous user role, personal contact forms, user registration settings, and account cancellation settings. On this page you can also manage settings for account personalization, and adapt the text for the email messages that users receive when they register or request a password recovery. You may also set which role is automatically assigned new permissions whenever a module is installed (the Administrator role).', [':accounts' => Url::fromRoute('entity.user.admin_form')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Managing user account fields') . '</dt>';
-      $output .= '<dd>' . t('Because User accounts are an entity type, you can extend them by adding fields through the Manage fields tab on the <a href=":accounts">Account settings page</a>. By adding fields for e.g., a picture, a biography, or address, you can a create a custom profile for the users of the website. For background information on entities and fields, see the <a href=":field_help">Field module help page</a>.', [':field_help' => (\Drupal::moduleHandler()->moduleExists('field')) ? Url::fromRoute('help.page', ['name' => 'field'])->toString() : '#', ':accounts' => Url::fromRoute('entity.user.admin_form')->toString()]) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-
-    case 'user.admin_create':
-      return '<p>' . t("This web page allows administrators to register new users. Users' email addresses and usernames must be unique.") . '</p>';
-
-    case 'user.admin_permissions':
-      return '<p>' . t('Permissions let you control what users can do and see on your site. You can define a specific set of permissions for each role. (See the <a href=":role">Roles</a> page to create a role.) Any permissions granted to the Authenticated user role will be given to any user who is logged in to your site. On the <a href=":settings">Role settings</a> page, you can make any role into an Administrator role for the site, meaning that role will be granted all permissions. You should be careful to ensure that only trusted users are given this access and level of control of your site.', [':role' => Url::fromRoute('entity.user_role.collection')->toString(), ':settings' => Url::fromRoute('user.role.settings')->toString()]) . '</p>';
-
-    case 'entity.user_role.collection':
-      return '<p>' . t('A role defines a group of users that have certain privileges. These privileges are defined on the <a href=":permissions">Permissions page</a>. Here, you can define the names and the display sort order of the roles on your site. It is recommended to order roles from least permissive (for example, Anonymous user) to most permissive (for example, Administrator user). Users who are not logged in have the Anonymous user role. Users who are logged in have the Authenticated user role, plus any other roles granted to their user account.', [':permissions' => Url::fromRoute('user.admin_permissions')->toString()]) . '</p>';
-
-    case 'entity.user.field_ui_fields':
-      return '<p>' . t('This form lets administrators add and edit fields for storing user data.') . '</p>';
-
-    case 'entity.entity_form_display.user.default':
-      return '<p>' . t('This form lets administrators configure how form fields should be displayed when editing a user profile.') . '</p>';
-
-    case 'entity.entity_view_display.user.default':
-      return '<p>' . t('This form lets administrators configure how fields should be displayed when rendering a user profile page.') . '</p>';
-  }
-}
-
-/**
- * Implements hook_theme().
- */
-function user_theme(): array {
-  return [
-    'user' => [
-      'render element' => 'elements',
-    ],
-    'username' => [
-      'variables' => ['account' => NULL, 'attributes' => [], 'link_options' => []],
-    ],
-  ];
-}
-
-/**
- * Implements hook_js_settings_alter().
- */
-function user_js_settings_alter(&$settings, AttachedAssetsInterface $assets) {
-  // Provide the user ID in drupalSettings to allow JavaScript code to customize
-  // the experience for the end user, rather than the server side, which would
-  // break the render cache.
-  // Similarly, provide a permissions hash, so that permission-dependent data
-  // can be reliably cached on the client side.
-  $user = \Drupal::currentUser();
-  $settings['user']['uid'] = $user->id();
-  $settings['user']['permissionsHash'] = \Drupal::service('user_permissions_hash_generator')->generate($user);
-}
-
 /**
  * Returns whether this site supports the default user picture feature.
  *
@@ -116,50 +31,6 @@ function user_picture_enabled() {
   return isset($field_definitions['user_picture']);
 }
 
-/**
- * Implements hook_entity_extra_field_info().
- */
-function user_entity_extra_field_info() {
-  $fields['user']['user']['form']['account'] = [
-    'label' => t('User name and password'),
-    'description' => t('User module account form elements.'),
-    'weight' => -10,
-  ];
-  $fields['user']['user']['form']['language'] = [
-    'label' => t('Language settings'),
-    'description' => t('User module form element.'),
-    'weight' => 0,
-  ];
-  if (\Drupal::config('system.date')->get('timezone.user.configurable')) {
-    $fields['user']['user']['form']['timezone'] = [
-      'label' => t('Timezone'),
-      'description' => t('System module form element.'),
-      'weight' => 6,
-    ];
-  }
-
-  $fields['user']['user']['display']['member_for'] = [
-    'label' => t('Member for'),
-    'description' => t("User module 'member for' view element."),
-    'weight' => 5,
-  ];
-
-  return $fields;
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave() for user entities.
- *
- * @todo https://www.drupal.org/project/drupal/issues/3112704 Move to
- *   \Drupal\user\Entity\User::preSave().
- */
-function user_user_presave(UserInterface $account) {
-  $config = \Drupal::config('system.date');
-  if ($config->get('timezone.user.configurable') && !$account->getTimeZone() && !$config->get('timezone.user.default')) {
-    $account->timezone = $config->get('timezone.default');
-  }
-}
-
 /**
  * Fetches a user object by email address.
  *
@@ -241,42 +112,6 @@ function user_is_blocked($name) {
     ->execute();
 }
 
-/**
- * Implements hook_ENTITY_TYPE_view() for user entities.
- */
-function user_user_view(array &$build, UserInterface $account, EntityViewDisplayInterface $display) {
-  if ($account->isAuthenticated() && $display->getComponent('member_for')) {
-    $build['member_for'] = [
-      '#type' => 'item',
-      '#markup' => '<h4 class="label">' . t('Member for') . '</h4> ' . \Drupal::service('date.formatter')->formatTimeDiffSince($account->getCreatedTime()),
-    ];
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_view_alter() for user entities.
- *
- * This function adds a default alt tag to the user_picture field to maintain
- * accessibility.
- */
-function user_user_view_alter(array &$build, UserInterface $account, EntityViewDisplayInterface $display) {
-  if (!empty($build['user_picture']) && user_picture_enabled()) {
-    foreach (Element::children($build['user_picture']) as $key) {
-      if (!isset($build['user_picture'][$key]['#item']) || !($build['user_picture'][$key]['#item'] instanceof ImageItem)) {
-        // User picture field is provided by standard profile install. If the
-        // display is configured to use a different formatter, the #item render
-        // key may not exist, or may not be an image field.
-        continue;
-      }
-      /** @var \Drupal\image\Plugin\Field\FieldType\ImageItem $item */
-      $item = $build['user_picture'][$key]['#item'];
-      if (!$item->get('alt')->getValue()) {
-        $item->get('alt')->setValue(\Drupal::translation()->translate('Profile picture for user @username', ['@username' => $account->getAccountName()]));
-      }
-    }
-  }
-}
-
 /**
  * Implements hook_preprocess_HOOK() for block templates.
  */
@@ -290,23 +125,6 @@ function user_preprocess_block(&$variables) {
   }
 }
 
-/**
- * Implements hook_template_preprocess_default_variables_alter().
- *
- * @see user_user_login()
- * @see user_user_logout()
- */
-function user_template_preprocess_default_variables_alter(&$variables) {
-  $user = \Drupal::currentUser();
-
-  $variables['user'] = clone $user;
-  // Remove password and session IDs, since themes should not need nor see them.
-  unset($variables['user']->pass, $variables['user']->sid, $variables['user']->ssid);
-
-  $variables['is_admin'] = $user->hasPermission('access administration pages');
-  $variables['logged_in'] = $user->isAuthenticated();
-}
-
 /**
  * Prepares variables for username templates.
  *
@@ -419,36 +237,6 @@ function user_login_finalize(UserInterface $account) {
   \Drupal::moduleHandler()->invokeAll('user_login', [$account]);
 }
 
-/**
- * Implements hook_user_login().
- */
-function user_user_login(UserInterface $account) {
-  // Reset static cache of default variables in template_preprocess() to reflect
-  // the new user.
-  drupal_static_reset('template_preprocess');
-
-  // If the user has a NULL time zone, notify them to set a time zone.
-  $config = \Drupal::config('system.date');
-  if (!$account->getTimezone() && $config->get('timezone.user.configurable') && $config->get('timezone.user.warn')) {
-    \Drupal::messenger()
-      ->addStatus(t('Configure your <a href=":user-edit">account time zone setting</a>.', [
-        ':user-edit' => $account->toUrl('edit-form', [
-          'query' => \Drupal::destination()->getAsArray(),
-          'fragment' => 'edit-timezone',
-        ])->toString(),
-      ]));
-  }
-}
-
-/**
- * Implements hook_user_logout().
- */
-function user_user_logout(AccountInterface $account) {
-  // Reset static cache of default variables in template_preprocess() to reflect
-  // the new user.
-  drupal_static_reset('template_preprocess');
-}
-
 /**
  * Generates a unique URL for a user to log in and reset their password.
  *
@@ -715,28 +503,6 @@ function user_cancel_methods() {
   return $form;
 }
 
-/**
- * Implements hook_mail().
- */
-function user_mail($key, &$message, $params) {
-  $token_service = \Drupal::token();
-  $language_manager = \Drupal::languageManager();
-  $langcode = $message['langcode'];
-  $variables = ['user' => $params['account']];
-
-  $language = $language_manager->getLanguage($langcode);
-  $original_language = $language_manager->getConfigOverrideLanguage();
-  $language_manager->setConfigOverrideLanguage($language);
-  $mail_config = \Drupal::config('user.mail');
-
-  $token_options = ['langcode' => $langcode, 'callback' => 'user_mail_tokens', 'clear' => TRUE];
-  $message['subject'] .= PlainTextOutput::renderFromHtml($token_service->replace($mail_config->get($key . '.subject'), $variables, $token_options));
-  $message['body'][] = $token_service->replace($mail_config->get($key . '.body'), $variables, $token_options);
-
-  $language_manager->setConfigOverrideLanguage($original_language);
-
-}
-
 /**
  * Token callback to add unsafe tokens for user mails.
  *
@@ -764,67 +530,6 @@ function user_mail_tokens(&$replacements, $data, $options) {
   }
 }
 
-/**
- * Implements hook_ENTITY_TYPE_insert() for user_role entities.
- */
-function user_user_role_insert(RoleInterface $role) {
-  // Ignore the authenticated and anonymous roles or the role is being synced.
-  if (in_array($role->id(), [RoleInterface::AUTHENTICATED_ID, RoleInterface::ANONYMOUS_ID]) || $role->isSyncing()) {
-    return;
-  }
-
-  assert(Inspector::assertStringable($role->label()), 'Role label is expected to be a string.');
-
-  $add_id = 'user_add_role_action.' . $role->id();
-  if (!Action::load($add_id)) {
-    $action = Action::create([
-      'id' => $add_id,
-      'type' => 'user',
-      'label' => t('Add the @label role to the selected user(s)', ['@label' => $role->label()]),
-      'configuration' => [
-        'rid' => $role->id(),
-      ],
-      'plugin' => 'user_add_role_action',
-    ]);
-    $action->trustData()->save();
-  }
-  $remove_id = 'user_remove_role_action.' . $role->id();
-  if (!Action::load($remove_id)) {
-    $action = Action::create([
-      'id' => $remove_id,
-      'type' => 'user',
-      'label' => t('Remove the @label role from the selected user(s)', ['@label' => $role->label()]),
-      'configuration' => [
-        'rid' => $role->id(),
-      ],
-      'plugin' => 'user_remove_role_action',
-    ]);
-    $action->trustData()->save();
-  }
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for user_role entities.
- */
-function user_user_role_delete(RoleInterface $role) {
-  // Delete role references for all users.
-  $user_storage = \Drupal::entityTypeManager()->getStorage('user');
-  $user_storage->deleteRoleReferences([$role->id()]);
-
-  // Ignore the authenticated and anonymous roles or the role is being synced.
-  if (in_array($role->id(), [RoleInterface::AUTHENTICATED_ID, RoleInterface::ANONYMOUS_ID]) || $role->isSyncing()) {
-    return;
-  }
-
-  $actions = Action::loadMultiple([
-    'user_add_role_action.' . $role->id(),
-    'user_remove_role_action.' . $role->id(),
-  ]);
-  foreach ($actions as $action) {
-    $action->delete();
-  }
-}
-
 /**
  * Change permissions for a user role.
  *
@@ -959,15 +664,6 @@ function _user_mail_notify($op, AccountInterface $account) {
   return empty($mail) ? NULL : $mail['result'];
 }
 
-/**
- * Implements hook_element_info_alter().
- */
-function user_element_info_alter(array &$types) {
-  if (isset($types['password_confirm'])) {
-    $types['password_confirm']['#process'][] = 'user_form_process_password_confirm';
-  }
-}
-
 /**
  * Form element process handler for client-side password validation.
  *
@@ -1008,14 +704,6 @@ function user_form_process_password_confirm($element) {
   return $element;
 }
 
-/**
- * Implements hook_modules_uninstalled().
- */
-function user_modules_uninstalled($modules) {
-  // Remove any potentially orphan module data stored for users.
-  \Drupal::service('user.data')->delete($modules);
-}
-
 /**
  * Saves visitor information as a cookie so it can be reused.
  *
@@ -1040,76 +728,6 @@ function user_cookie_delete($cookie_name) {
   setrawcookie('Drupal.visitor.' . $cookie_name, '', \Drupal::time()->getRequestTime() - 3600, '/');
 }
 
-/**
- * Implements hook_toolbar().
- */
-function user_toolbar() {
-  $user = \Drupal::currentUser();
-
-  $items['user'] = [
-    '#type' => 'toolbar_item',
-    'tab' => [
-      '#type' => 'link',
-      '#title' => $user->getDisplayName(),
-      '#url' => Url::fromRoute('user.page'),
-      '#attributes' => [
-        'title' => t('My account'),
-        'class' => ['toolbar-icon', 'toolbar-icon-user'],
-      ],
-      '#cache' => [
-        // Vary cache for anonymous and authenticated users.
-        'contexts' => ['user.roles:anonymous'],
-      ],
-    ],
-    'tray' => [
-      '#heading' => t('User account actions'),
-    ],
-    '#weight' => 100,
-    '#attached' => [
-      'library' => [
-        'user/drupal.user.icons',
-      ],
-    ],
-  ];
-
-  if ($user->isAnonymous()) {
-    $links = [
-      'login' => [
-        'title' => t('Log in'),
-        'url' => Url::fromRoute('user.page'),
-      ],
-    ];
-    $items['user']['tray']['user_links'] = [
-      '#theme' => 'links__toolbar_user',
-      '#links' => $links,
-      '#attributes' => [
-        'class' => ['toolbar-menu'],
-      ],
-    ];
-  }
-  else {
-    $items['user']['tab']['#title'] = [
-      '#lazy_builder' => ['user.toolbar_link_builder:renderDisplayName', []],
-      '#create_placeholder' => TRUE,
-      '#lazy_builder_preview' => [
-        // Add a line of whitespace to the placeholder to ensure the icon is
-        // positioned in the same place it will be when the lazy loaded content
-        // appears.
-        '#markup' => '&nbsp;',
-      ],
-    ];
-    $items['user']['tray']['user_links'] = [
-      '#lazy_builder' => ['user.toolbar_link_builder:renderToolbarLinks', []],
-      '#create_placeholder' => TRUE,
-      '#lazy_builder_preview' => [
-        '#markup' => '<a href="#" class="toolbar-tray-lazy-placeholder-link">&nbsp;</a>',
-      ],
-    ];
-  }
-
-  return $items;
-}
-
 /**
  * Logs the current user out.
  */
@@ -1150,50 +768,6 @@ function template_preprocess_user(&$variables) {
   }
 }
 
-/**
- * Implements hook_form_FORM_ID_alter() for \Drupal\system\Form\RegionalForm.
- */
-function user_form_system_regional_settings_alter(&$form, FormStateInterface $form_state): void {
-  $config = \Drupal::config('system.date');
-
-  $form['timezone']['configurable_timezones'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Users may set their own time zone'),
-    '#default_value' => $config->get('timezone.user.configurable'),
-  ];
-
-  $form['timezone']['configurable_timezones_wrapper'] = [
-    '#type' => 'container',
-    '#states' => [
-      // Hide the user configured timezone settings when users are forced to use
-      // the default setting.
-      'invisible' => [
-        'input[name="configurable_timezones"]' => ['checked' => FALSE],
-      ],
-    ],
-  ];
-  $form['timezone']['configurable_timezones_wrapper']['empty_timezone_message'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Remind users at login if their time zone is not set'),
-    '#default_value' => $config->get('timezone.user.warn'),
-    '#description' => t('Only applied if users may set their own time zone.'),
-  ];
-
-  $form['timezone']['configurable_timezones_wrapper']['user_default_timezone'] = [
-    '#type' => 'radios',
-    '#title' => t('Time zone for new users'),
-    '#default_value' => $config->get('timezone.user.default'),
-    '#options' => [
-      UserInterface::TIMEZONE_DEFAULT => t('Default time zone'),
-      UserInterface::TIMEZONE_EMPTY   => t('Empty time zone'),
-      UserInterface::TIMEZONE_SELECT  => t('Users may set their own time zone at registration'),
-    ],
-    '#description' => t('Only applied if users may set their own time zone.'),
-  ];
-
-  $form['#submit'][] = 'user_form_system_regional_settings_submit';
-}
-
 /**
  * Additional submit handler for \Drupal\system\Form\RegionalForm.
  */
@@ -1204,48 +778,3 @@ function user_form_system_regional_settings_submit($form, FormStateInterface $fo
     ->set('timezone.user.default', $form_state->getValue('user_default_timezone'))
     ->save();
 }
-
-/**
- * Implements hook_filter_format_disable().
- */
-function user_filter_format_disable(FilterFormatInterface $filter_format) {
-  // Remove the permission from any roles.
-  $permission = $filter_format->getPermissionName();
-  /** @var \Drupal\user\Entity\Role $role */
-  foreach (Role::loadMultiple() as $role) {
-    if ($role->hasPermission($permission)) {
-      $role->revokePermission($permission)->save();
-    }
-  }
-}
-
-/**
- * Implements hook_entity_operation().
- */
-function user_entity_operation(EntityInterface $entity) {
-  // Add Manage permissions link if this entity type defines the permissions
-  // link template.
-  if (!$entity->hasLinkTemplate('entity-permissions-form')) {
-    return [];
-  }
-
-  $bundle_entity_type = $entity->bundle();
-  $route = "entity.$bundle_entity_type.entity_permissions_form";
-  if (empty(\Drupal::service('router.route_provider')->getRoutesByNames([$route]))) {
-    return [];
-  }
-
-  $url = Url::fromRoute($route, [$bundle_entity_type => $entity->id()]);
-  if (!$url->access()) {
-    return [];
-  }
-
-  return [
-    'manage-permissions' => [
-      'title' => t('Manage permissions'),
-      'weight' => 50,
-      'url' => $url,
-    ],
-  ];
-
-}
diff --git a/core/modules/user/user.tokens.inc b/core/modules/user/user.tokens.inc
deleted file mode 100644
index 897df8b5626304aed6e5b44060dca91a70d61bb4..0000000000000000000000000000000000000000
--- a/core/modules/user/user.tokens.inc
+++ /dev/null
@@ -1,168 +0,0 @@
-<?php
-
-/**
- * @file
- * Builds placeholder replacement tokens for user-related data.
- */
-
-use Drupal\Core\Datetime\Entity\DateFormat;
-use Drupal\Core\Render\BubbleableMetadata;
-use Drupal\user\Entity\User;
-
-/**
- * Implements hook_token_info().
- */
-function user_token_info() {
-  $types['user'] = [
-    'name' => t('Users'),
-    'description' => t('Tokens related to individual user accounts.'),
-    'needs-data' => 'user',
-  ];
-  $types['current-user'] = [
-    'name' => t('Current user'),
-    'description' => t('Tokens related to the currently logged in user.'),
-    'type' => 'user',
-  ];
-
-  $user['uid'] = [
-    'name' => t('User ID'),
-    'description' => t("The unique ID of the user account."),
-  ];
-  $user['uuid'] = [
-    'name' => t('UUID'),
-    'description' => t("The UUID of the user account."),
-  ];
-  $user['name'] = [
-    'name' => t("Deprecated: User Name"),
-    'description' => t("Deprecated: Use account-name or display-name instead."),
-  ];
-  $user['account-name'] = [
-    'name' => t("Account Name"),
-    'description' => t("The login name of the user account."),
-  ];
-  $user['display-name'] = [
-    'name' => t("Display Name"),
-    'description' => t("The display name of the user account."),
-  ];
-  $user['mail'] = [
-    'name' => t("Email"),
-    'description' => t("The email address of the user account."),
-  ];
-  $user['url'] = [
-    'name' => t("URL"),
-    'description' => t("The URL of the account profile page."),
-  ];
-  $user['edit-url'] = [
-    'name' => t("Edit URL"),
-    'description' => t("The URL of the account edit page."),
-  ];
-
-  $user['last-login'] = [
-    'name' => t("Last login"),
-    'description' => t("The date the user last logged in to the site."),
-    'type' => 'date',
-  ];
-  $user['created'] = [
-    'name' => t("Created"),
-    'description' => t("The date the user account was created."),
-    'type' => 'date',
-  ];
-
-  return [
-    'types' => $types,
-    'tokens' => ['user' => $user],
-  ];
-}
-
-/**
- * Implements hook_tokens().
- */
-function user_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
-
-  $token_service = \Drupal::token();
-  $url_options = ['absolute' => TRUE];
-  if (isset($options['langcode'])) {
-    $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']);
-    $langcode = $options['langcode'];
-  }
-  else {
-    $langcode = NULL;
-  }
-  $replacements = [];
-
-  if ($type == 'user' && !empty($data['user'])) {
-    /** @var \Drupal\user\UserInterface $account */
-    $account = $data['user'];
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        // Basic user account information.
-        case 'uid':
-          // In the case of hook user_presave uid is not set yet.
-          $replacements[$original] = $account->id() ?: t('not yet assigned');
-          break;
-
-        case 'uuid':
-          $replacements[$original] = $account->uuid();
-          break;
-
-        case 'display-name':
-          $replacements[$original] = $account->getDisplayName();
-          if ($account->isAnonymous()) {
-            $bubbleable_metadata->addCacheableDependency(\Drupal::config('user.settings'));
-          }
-          break;
-
-        case 'name':
-        case 'account-name':
-          $display_name = $account->getAccountName();
-          $replacements[$original] = $display_name;
-          if ($account->isAnonymous()) {
-            $bubbleable_metadata->addCacheableDependency(\Drupal::config('user.settings'));
-          }
-          break;
-
-        case 'mail':
-          $replacements[$original] = $account->getEmail();
-          break;
-
-        case 'url':
-          $replacements[$original] = $account->id() ? $account->toUrl('canonical', $url_options)->toString() : t('not yet assigned');
-          break;
-
-        case 'edit-url':
-          $replacements[$original] = $account->id() ? $account->toUrl('edit-form', $url_options)->toString() : t('not yet assigned');
-          break;
-
-        // These tokens are default variations on the chained tokens handled below.
-        case 'last-login':
-          $date_format = DateFormat::load('medium');
-          $bubbleable_metadata->addCacheableDependency($date_format);
-          $replacements[$original] = $account->getLastLoginTime() ? \Drupal::service('date.formatter')->format($account->getLastLoginTime(), 'medium', '', NULL, $langcode) : t('never');
-          break;
-
-        case 'created':
-          $date_format = DateFormat::load('medium');
-          $bubbleable_metadata->addCacheableDependency($date_format);
-          // In the case of user_presave the created date may not yet be set.
-          $replacements[$original] = $account->getCreatedTime() ? \Drupal::service('date.formatter')->format($account->getCreatedTime(), 'medium', '', NULL, $langcode) : t('not yet created');
-          break;
-      }
-    }
-
-    if ($login_tokens = $token_service->findWithPrefix($tokens, 'last-login')) {
-      $replacements += $token_service->generate('date', $login_tokens, ['date' => $account->getLastLoginTime()], $options, $bubbleable_metadata);
-    }
-
-    if ($registered_tokens = $token_service->findWithPrefix($tokens, 'created')) {
-      $replacements += $token_service->generate('date', $registered_tokens, ['date' => $account->getCreatedTime()], $options, $bubbleable_metadata);
-    }
-  }
-
-  if ($type == 'current-user') {
-    $account = User::load(\Drupal::currentUser()->id());
-    $bubbleable_metadata->addCacheContexts(['user']);
-    $replacements += $token_service->generate('user', $tokens, ['user' => $account], $options, $bubbleable_metadata);
-  }
-
-  return $replacements;
-}
diff --git a/core/modules/user/user.views.inc b/core/modules/user/user.views.inc
deleted file mode 100644
index 5e3bd251e02cf242fb0ee1aa3a9757c15b757468..0000000000000000000000000000000000000000
--- a/core/modules/user/user.views.inc
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views data for user.module.
- */
-
-/**
- * Implements hook_views_plugins_argument_validator_alter().
- */
-function user_views_plugins_argument_validator_alter(array &$plugins) {
-  $plugins['entity:user']['title'] = t('User ID');
-  $plugins['entity:user']['class'] = 'Drupal\user\Plugin\views\argument_validator\User';
-  $plugins['entity:user']['provider'] = 'user';
-}
diff --git a/core/modules/user/user.views_execution.inc b/core/modules/user/user.views_execution.inc
deleted file mode 100644
index e0c5c0c635d19f5fbf91a826cdda2116d8ba9fa9..0000000000000000000000000000000000000000
--- a/core/modules/user/user.views_execution.inc
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views runtime hooks for user.module.
- */
-
-use Drupal\views\ViewExecutable;
-
-/**
- * Implements hook_views_query_substitutions().
- *
- * Allow replacement of current user ID so we can cache these queries.
- */
-function user_views_query_substitutions(ViewExecutable $view) {
-  return ['***CURRENT_USER***' => \Drupal::currentUser()->id()];
-}
diff --git a/core/modules/views/src/Hook/ViewsHooks.php b/core/modules/views/src/Hook/ViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..ee64d34d60b5afa41ce7ed6cf6eb34567eb68efc
--- /dev/null
+++ b/core/modules/views/src/Hook/ViewsHooks.php
@@ -0,0 +1,378 @@
+<?php
+
+namespace Drupal\views\Hook;
+
+use Drupal\views\ViewsConfigUpdater;
+use Drupal\views\ViewEntityInterface;
+use Drupal\views\Plugin\Derivative\ViewsLocalTask;
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\views\ViewExecutable;
+use Drupal\views\Views;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views.
+ */
+class ViewsHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.views':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Views module provides a back end to fetch information from content, user accounts, taxonomy terms, and other entities from the database and present it to the user as a grid, HTML list, table, unformatted list, etc. The resulting displays are known generally as <em>views</em>.') . '</p>';
+        $output .= '<p>' . t('For more information, see the <a href=":views">online documentation for the Views module</a>.', [':views' => 'https://www.drupal.org/documentation/modules/views']) . '</p>';
+        $output .= '<p>' . t('In order to create and modify your own views using the administration and configuration user interface, you will need to install either the Views UI module in core or a contributed module that provides a user interface for Views. See the <a href=":views-ui">Views UI module help page</a> for more information.', [
+          ':views-ui' => \Drupal::moduleHandler()->moduleExists('views_ui') ? Url::fromRoute('help.page', [
+            'name' => 'views_ui',
+          ])->toString() : '#',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Adding functionality to administrative pages') . '</dt>';
+        $output .= '<dd>' . t('The Views module adds functionality to some core administration pages. For example, <em>admin/content</em> uses Views to filter and sort content. With Views uninstalled, <em>admin/content</em> is more limited.') . '</dd>';
+        $output .= '<dt>' . t('Expanding Views functionality') . '</dt>';
+        $output .= '<dd>' . t('Contributed projects that support the Views module can be found in the <a href=":node">online documentation for Views-related contributed modules</a>.', [':node' => 'https://www.drupal.org/documentation/modules/views/add-ons']) . '</dd>';
+        $output .= '<dt>' . t('Improving table accessibility') . '</dt>';
+        $output .= '<dd>' . t('Views tables include semantic markup to improve accessibility. Data cells are automatically associated with header cells through id and header attributes. To improve the accessibility of your tables you can add descriptive elements within the Views table settings. The <em>caption</em> element can introduce context for a table, making it easier to understand. The <em>summary</em> element can provide an overview of how the data has been organized and how to navigate the table. Both the caption and summary are visible by default and also implemented according to HTML5 guidelines.') . '</dd>';
+        $output .= '<dt>' . t('Working with multilingual views') . '</dt>';
+        $output .= '<dd>' . t('If your site has multiple languages and translated entities, each result row in a view will contain one translation of each involved entity (a view can involve multiple entities if it uses relationships). You can use a filter to restrict your view to one language: without filtering, if an entity has three translations it will add three rows to the results; if you filter by language, at most one result will appear (it could be zero if that particular entity does not have a translation matching your language filter choice). If a view uses relationships, each entity in the relationship needs to be filtered separately. You can filter a view to a fixed language choice, such as English or Spanish, or to the language selected by the page the view is displayed on (the language that is selected for the page by the language detection settings either for Content or User interface).') . '</dd>';
+        $output .= '<dd>' . t('Because each result row contains a specific translation of each entity, field-level filters are also relative to these entity translations. For example, if your view has a filter that specifies that the entity title should contain a particular English word, you will presumably filter out all rows containing Chinese translations, since they will not contain the English word. If your view also has a second filter specifying that the title should contain a particular Chinese word, and if you are using "And" logic for filtering, you will presumably end up with no results in the view, because there are probably not any entity translations containing both the English and Chinese words in the title.') . '</dd>';
+        $output .= '<dd>' . t('Independent of filtering, you can choose the display language (the language used to display the entities and their fields) via a setting on the display. Your language choices are the same as the filter language choices, with an additional choice of "Content language of view row" and "Original language of content in view row", which means to display each entity in the result row using the language that entity has or in which it was originally created. In theory, this would give you the flexibility to filter to French translations, for instance, and then display the results in Spanish. The more usual choices would be to use the same language choices for the display language and each entity filter in the view, or to use the Row language setting for the display.') . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_views_pre_render().
+   */
+  #[Hook('views_pre_render')]
+  public function viewsPreRender($view) {
+    // If using AJAX, send identifying data about this view.
+    if ($view->ajaxEnabled() && empty($view->is_attachment) && empty($view->live_preview)) {
+      $view->element['#attached']['drupalSettings']['views'] = [
+        'ajax_path' => Url::fromRoute('views.ajax')->toString(),
+        'ajaxViews' => [
+          'views_dom_id:' . $view->dom_id => [
+            'view_name' => $view->storage->id(),
+            'view_display_id' => $view->current_display,
+            'view_args' => Html::escape(implode('/', $view->args)),
+            'view_path' => Html::escape(\Drupal::service('path.current')->getPath()),
+            'view_base_path' => $view->getPath(),
+            'view_dom_id' => $view->dom_id,
+                    // To fit multiple views on a page, the programmer may have
+                    // overridden the display's pager_element.
+            'pager_element' => isset($view->pager) ? $view->pager->getPagerId() : 0,
+          ],
+        ],
+      ];
+      $view->element['#attached']['library'][] = 'views/views.ajax';
+    }
+    return $view;
+  }
+
+  /**
+   * Implements hook_theme().
+   *
+   * Register views theming functions and those that are defined via views plugin
+   * definitions.
+   */
+  #[Hook('theme')]
+  public function theme($existing, $type, $theme, $path) : array {
+    \Drupal::moduleHandler()->loadInclude('views', 'inc', 'views.theme');
+    // Some quasi clever array merging here.
+    $base = ['file' => 'views.theme.inc'];
+    // Our extra version of pager
+    $hooks['views_mini_pager'] = $base + [
+      'variables' => [
+        'tags' => [],
+        'quantity' => 9,
+        'element' => 0,
+        'pagination_heading_level' => 'h4',
+        'parameters' => [],
+      ],
+    ];
+    $variables = [
+          // For displays, we pass in a dummy array as the first parameter, since
+          // $view is an object but the core contextual_preprocess() function only
+          // attaches contextual links when the primary theme argument is an array.
+      'display' => [
+        'view_array' => [],
+        'view' => NULL,
+        'rows' => [],
+        'header' => [],
+        'footer' => [],
+        'empty' => [],
+        'exposed' => [],
+        'more' => [],
+        'feed_icons' => [],
+        'pager' => [],
+        'title' => '',
+        'attachment_before' => [],
+        'attachment_after' => [],
+      ],
+      'style' => [
+        'view' => NULL,
+        'options' => NULL,
+        'rows' => NULL,
+        'title' => NULL,
+      ],
+      'row' => [
+        'view' => NULL,
+        'options' => NULL,
+        'row' => NULL,
+        'field_alias' => NULL,
+      ],
+      'exposed_form' => [
+        'view' => NULL,
+        'options' => NULL,
+      ],
+      'pager' => [
+        'view' => NULL,
+        'options' => NULL,
+        'tags' => [],
+        'quantity' => 9,
+        'element' => 0,
+        'pagination_heading_level' => 'h4',
+        'parameters' => [],
+      ],
+    ];
+    // Default view themes
+    $hooks['views_view_field'] = $base + ['variables' => ['view' => NULL, 'field' => NULL, 'row' => NULL]];
+    $hooks['views_view_grouping'] = $base + [
+      'variables' => [
+        'view' => NULL,
+        'grouping' => NULL,
+        'grouping_level' => NULL,
+        'rows' => NULL,
+        'title' => NULL,
+      ],
+    ];
+    // Only display, pager, row, and style plugins can provide theme hooks.
+    $plugin_types = ['display', 'pager', 'row', 'style', 'exposed_form'];
+    $plugins = [];
+    foreach ($plugin_types as $plugin_type) {
+      $plugins[$plugin_type] = Views::pluginManager($plugin_type)->getDefinitions();
+    }
+    $module_handler = \Drupal::moduleHandler();
+    // Register theme functions for all style plugins. It provides a basic auto
+    // implementation of theme functions or template files by using the plugin
+    // definitions (theme, theme_file, module, register_theme). Template files are
+    // assumed to be located in the templates folder.
+    foreach ($plugins as $type => $info) {
+      foreach ($info as $def) {
+        // Not all plugins have theme functions, and they can also explicitly
+        // prevent a theme function from being registered automatically.
+        if (!isset($def['theme']) || empty($def['register_theme'])) {
+          continue;
+        }
+        // For each theme registration, we have a base directory to check for the
+        // templates folder. This will be relative to the root of the given module
+        // folder, so we always need a module definition.
+        // @todo Watchdog or exception?
+        if (!isset($def['provider']) || !$module_handler->moduleExists($def['provider'])) {
+          continue;
+        }
+        $hooks[$def['theme']] = ['variables' => $variables[$type]];
+        // We always use the module directory as base dir.
+        $module_dir = \Drupal::service('extension.list.module')->getPath($def['provider']);
+        $hooks[$def['theme']]['path'] = $module_dir;
+        // For the views module we ensure views.theme.inc is included.
+        if ($def['provider'] == 'views') {
+          if (!isset($hooks[$def['theme']]['includes'])) {
+            $hooks[$def['theme']]['includes'] = [];
+          }
+          if (!in_array('views.theme.inc', $hooks[$def['theme']]['includes'])) {
+            $hooks[$def['theme']]['includes'][] = $module_dir . '/views.theme.inc';
+          }
+        }
+        elseif (!empty($def['theme_file'])) {
+          $hooks[$def['theme']]['file'] = $def['theme_file'];
+        }
+        // Whenever we have a theme file, we include it directly so we can
+        // auto-detect the theme function.
+        if (isset($def['theme_file'])) {
+          $include = \Drupal::root() . '/' . $module_dir . '/' . $def['theme_file'];
+          if (is_file($include)) {
+            require_once $include;
+          }
+        }
+        // By default any templates for a module are located in the /templates
+        // directory of the module's folder. If a module wants to define its own
+        // location it has to set register_theme of the plugin to FALSE and
+        // implement hook_theme() by itself.
+        $hooks[$def['theme']]['path'] .= '/templates';
+        $hooks[$def['theme']]['template'] = Html::cleanCssIdentifier($def['theme']);
+      }
+    }
+    $hooks['views_form_views_form'] = $base + ['render element' => 'form'];
+    $hooks['views_exposed_form'] = $base + ['render element' => 'form'];
+    return $hooks;
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK_alter().
+   */
+  #[Hook('theme_suggestions_node_alter')]
+  public function themeSuggestionsNodeAlter(array &$suggestions, array $variables) {
+    $node = $variables['elements']['#node'];
+    if (!empty($node->view) && $node->view->storage->id()) {
+      $suggestions[] = 'node__view__' . $node->view->storage->id();
+      if (!empty($node->view->current_display)) {
+        $suggestions[] = 'node__view__' . $node->view->storage->id() . '__' . $node->view->current_display;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK_alter().
+   */
+  #[Hook('theme_suggestions_comment_alter')]
+  public function themeSuggestionsCommentAlter(array &$suggestions, array $variables) {
+    $comment = $variables['elements']['#comment'];
+    if (!empty($comment->view) && $comment->view->storage->id()) {
+      $suggestions[] = 'comment__view__' . $comment->view->storage->id();
+      if (!empty($comment->view->current_display)) {
+        $suggestions[] = 'comment__view__' . $comment->view->storage->id() . '__' . $comment->view->current_display;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK_alter().
+   */
+  #[Hook('theme_suggestions_container_alter')]
+  public function themeSuggestionsContainerAlter(array &$suggestions, array $variables) {
+    if (!empty($variables['element']['#type']) && $variables['element']['#type'] == 'more_link' && !empty($variables['element']['#view']) && $variables['element']['#view'] instanceof ViewExecutable) {
+      $suggestions = array_merge(
+            $suggestions,
+            // Theme suggestions use the reverse order compared to #theme hooks.
+            array_reverse($variables['element']['#view']->buildThemeFunctions('container__more_link'))
+        );
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for 'field_config'.
+   */
+  #[Hook('field_config_insert')]
+  public function fieldConfigInsert(EntityInterface $field) {
+    Views::viewsData()->clear();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for 'field_config'.
+   */
+  #[Hook('field_config_update')]
+  public function fieldConfigUpdate(EntityInterface $entity) {
+    Views::viewsData()->clear();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for 'field_config'.
+   */
+  #[Hook('field_config_delete')]
+  public function fieldConfigDelete(EntityInterface $entity) {
+    Views::viewsData()->clear();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert().
+   */
+  #[Hook('base_field_override_insert')]
+  public function baseFieldOverrideInsert(EntityInterface $entity) {
+    Views::viewsData()->clear();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update().
+   */
+  #[Hook('base_field_override_update')]
+  public function baseFieldOverrideUpdate(EntityInterface $entity) {
+    Views::viewsData()->clear();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete().
+   */
+  #[Hook('base_field_override_delete')]
+  public function baseFieldOverrideDelete(EntityInterface $entity) {
+    Views::viewsData()->clear();
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for the exposed form.
+   *
+   * Since the exposed form is a GET form, we don't want it to send a wide
+   * variety of information.
+   */
+  #[Hook('form_views_exposed_form_alter')]
+  public function formViewsExposedFormAlter(&$form, FormStateInterface $form_state) : void {
+    $form['form_build_id']['#access'] = FALSE;
+    $form['form_token']['#access'] = FALSE;
+    $form['form_id']['#access'] = FALSE;
+  }
+
+  /**
+   * Implements hook_query_TAG_alter().
+   *
+   * This is the hook_query_alter() for queries tagged by Views and is used to
+   * add in substitutions from hook_views_query_substitutions().
+   */
+  #[Hook('query_views_alter')]
+  public function queryViewsAlter(AlterableInterface $query) {
+    $substitutions = $query->getMetaData('views_substitutions');
+    $tables =& $query->getTables();
+    $where =& $query->conditions();
+    // Replaces substitutions in tables.
+    foreach ($tables as $table_name => $table_metadata) {
+      foreach ($table_metadata['arguments'] as $replacement_key => $value) {
+        if (!is_array($value)) {
+          if (isset($substitutions[$value])) {
+            $tables[$table_name]['arguments'][$replacement_key] = $substitutions[$value];
+          }
+        }
+        else {
+          foreach ($value as $sub_key => $sub_value) {
+            if (isset($substitutions[$sub_value])) {
+              $tables[$table_name]['arguments'][$replacement_key][$sub_key] = $substitutions[$sub_value];
+            }
+          }
+        }
+      }
+    }
+    // Replaces substitutions in filter criteria.
+    _views_query_tag_alter_condition($query, $where, $substitutions);
+  }
+
+  /**
+   * Implements hook_local_tasks_alter().
+   */
+  #[Hook('local_tasks_alter')]
+  public function localTasksAlter(&$local_tasks) {
+    $container = \Drupal::getContainer();
+    $local_task = ViewsLocalTask::create($container, 'views_view');
+    $local_task->alterLocalTasks($local_tasks);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_presave().
+   */
+  #[Hook('view_presave')]
+  public function viewPresave(ViewEntityInterface $view) {
+    /** @var \Drupal\views\ViewsConfigUpdater $config_updater */
+    $config_updater = \Drupal::classResolver(ViewsConfigUpdater::class);
+    $config_updater->updateAll($view);
+  }
+
+}
diff --git a/core/modules/views/src/Hook/ViewsTokensHooks.php b/core/modules/views/src/Hook/ViewsTokensHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..2609e5bb1dbef7bea4f8d69e4460a3a973576b4b
--- /dev/null
+++ b/core/modules/views/src/Hook/ViewsTokensHooks.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace Drupal\views\Hook;
+
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views.
+ */
+class ViewsTokensHooks {
+
+  /**
+   * Implements hook_token_info().
+   */
+  #[Hook('token_info')]
+  public function tokenInfo() {
+    $info['types']['view'] = [
+      'name' => t('View', [], [
+        'context' => 'View entity type',
+      ]),
+      'description' => t('Tokens related to views.'),
+      'needs-data' => 'view',
+    ];
+    $info['tokens']['view']['label'] = ['name' => t('Label'), 'description' => t('The label of the view.')];
+    $info['tokens']['view']['description'] = ['name' => t('Description'), 'description' => t('The description of the view.')];
+    $info['tokens']['view']['id'] = ['name' => t('ID'), 'description' => t('The machine-readable ID of the view.')];
+    $info['tokens']['view']['title'] = [
+      'name' => t('Title'),
+      'description' => t('The title of current display of the view.'),
+    ];
+    $info['tokens']['view']['url'] = ['name' => t('URL'), 'description' => t('The URL of the view.'), 'type' => 'url'];
+    $info['tokens']['view']['base-table'] = [
+      'name' => t('Base table'),
+      'description' => t('The base table used for this view.'),
+    ];
+    $info['tokens']['view']['base-field'] = [
+      'name' => t('Base field'),
+      'description' => t('The base field used for this view.'),
+    ];
+    $info['tokens']['view']['total-rows'] = [
+      'name' => t('Total rows'),
+      'description' => t('The total amount of results returned from the view. The current display will be used.'),
+    ];
+    $info['tokens']['view']['items-per-page'] = [
+      'name' => t('Items per page'),
+      'description' => t('The number of items per page.'),
+    ];
+    $info['tokens']['view']['current-page'] = [
+      'name' => t('Current page'),
+      'description' => t('The current page of results the view is on.'),
+    ];
+    $info['tokens']['view']['page-count'] = ['name' => t('Page count'), 'description' => t('The total page count.')];
+    return $info;
+  }
+
+  /**
+   * Implements hook_tokens().
+   */
+  #[Hook('tokens')]
+  public function tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
+    $url_options = ['absolute' => TRUE];
+    if (isset($options['language'])) {
+      $url_options['language'] = $options['language'];
+    }
+    $replacements = [];
+    if ($type == 'view' && !empty($data['view'])) {
+      /** @var \Drupal\views\ViewExecutable $view */
+      $view = $data['view'];
+      $bubbleable_metadata->addCacheableDependency($view->storage);
+      foreach ($tokens as $name => $original) {
+        switch ($name) {
+          case 'label':
+            $replacements[$original] = $view->storage->label();
+            break;
+
+          case 'description':
+            $replacements[$original] = $view->storage->get('description');
+            break;
+
+          case 'id':
+            $replacements[$original] = $view->storage->id();
+            break;
+
+          case 'title':
+            $title = $view->getTitle();
+            $replacements[$original] = $title;
+            break;
+
+          case 'url':
+            try {
+              if ($url = $view->getUrl()) {
+                $replacements[$original] = $url->setOptions($url_options)->toString();
+              }
+            }
+            catch (\InvalidArgumentException) {
+              // The view has no URL so we leave the value empty.
+              $replacements[$original] = '';
+            }
+            break;
+
+          case 'base-table':
+            $replacements[$original] = $view->storage->get('base_table');
+            break;
+
+          case 'base-field':
+            $replacements[$original] = $view->storage->get('base_field');
+            break;
+
+          case 'total-rows':
+            $replacements[$original] = (int) $view->total_rows;
+            break;
+
+          case 'items-per-page':
+            $replacements[$original] = (int) $view->getItemsPerPage();
+            break;
+
+          case 'current-page':
+            $replacements[$original] = (int) $view->getCurrentPage() + 1;
+            break;
+
+          case 'page-count':
+            // If there are no items per page, set this to 1 for the division.
+            $per_page = $view->getItemsPerPage() ?: 1;
+            $replacements[$original] = max(1, (int) ceil($view->total_rows / $per_page));
+            break;
+        }
+      }
+    }
+    return $replacements;
+  }
+
+}
diff --git a/core/modules/views/src/Hook/ViewsViewsExecutionHooks.php b/core/modules/views/src/Hook/ViewsViewsExecutionHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..62b4b91e4936315c0b09f189253ec58c63898d63
--- /dev/null
+++ b/core/modules/views/src/Hook/ViewsViewsExecutionHooks.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\views\Hook;
+
+use Drupal\views\Plugin\views\PluginBase;
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views.
+ */
+class ViewsViewsExecutionHooks {
+
+  /**
+   * Implements hook_views_query_substitutions().
+   *
+   * Makes the following substitutions:
+   * - Current time.
+   * - Drupal version.
+   * - Special language codes; see
+   *   \Drupal\views\Plugin\views\PluginBase::listLanguages().
+   */
+  #[Hook('views_query_substitutions')]
+  public function viewsQuerySubstitutions(ViewExecutable $view) {
+    $substitutions = [
+      '***CURRENT_VERSION***' => \Drupal::VERSION,
+      '***CURRENT_TIME***' => \Drupal::time()->getRequestTime(),
+    ] + PluginBase::queryLanguageSubstitutions();
+    return $substitutions;
+  }
+
+  /**
+   * Implements hook_views_form_substitutions().
+   */
+  #[Hook('views_form_substitutions')]
+  public function viewsFormSubstitutions() {
+    $select_all = [
+      '#type' => 'checkbox',
+      '#default_value' => FALSE,
+      '#attributes' => [
+        'class' => [
+          'action-table-select-all',
+        ],
+      ],
+    ];
+    return [
+      '<!--action-bulk-form-select-all-->' => \Drupal::service('renderer')->render($select_all),
+    ];
+  }
+
+}
diff --git a/core/modules/views/src/Hook/ViewsViewsHooks.php b/core/modules/views/src/Hook/ViewsViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..70af0d1f00329df8f0c5dcd56576389c0593a6b4
--- /dev/null
+++ b/core/modules/views/src/Hook/ViewsViewsHooks.php
@@ -0,0 +1,291 @@
+<?php
+
+namespace Drupal\views\Hook;
+
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\system\ActionConfigEntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views.
+ */
+class ViewsViewsHooks {
+
+  /**
+   * Implements hook_views_data().
+   */
+  #[Hook('views_data')]
+  public function viewsData() {
+    $data['views']['table']['group'] = t('Global');
+    $data['views']['table']['join'] = ['#global' => []];
+    $data['views']['random'] = [
+      'title' => t('Random'),
+      'help' => t('Randomize the display order.'),
+      'sort' => [
+        'id' => 'random',
+      ],
+    ];
+    $data['views']['null'] = [
+      'title' => t('Null'),
+      'help' => t('Allow a contextual filter value to be ignored. The query will not be altered by this contextual filter value. Can be used when contextual filter values come from the URL, and a part of the URL needs to be ignored.'),
+      'argument' => [
+        'id' => 'null',
+      ],
+    ];
+    $data['views']['nothing'] = [
+      'title' => t('Custom text'),
+      'help' => t('Provide custom text or link.'),
+      'field' => [
+        'id' => 'custom',
+        'click sortable' => FALSE,
+      ],
+    ];
+    $data['views']['counter'] = [
+      'title' => t('View result counter'),
+      'help' => t('Displays the actual position of the view result'),
+      'field' => [
+        'id' => 'counter',
+      ],
+    ];
+    $data['views']['area'] = [
+      'title' => t('Text area'),
+      'help' => t('Provide markup for the area using any available text format.'),
+      'area' => [
+        'id' => 'text',
+      ],
+    ];
+    $data['views']['area_text_custom'] = [
+      'title' => t('Unfiltered text'),
+      'help' => t('Provide markup for the area with minimal filtering.'),
+      'area' => [
+        'id' => 'text_custom',
+      ],
+    ];
+    $data['views']['title'] = [
+      'title' => t('Title override'),
+      'help' => t('Override the default view title for this view. This is useful to display an alternative title when a view is empty.'),
+      'area' => [
+        'id' => 'title',
+        'sub_type' => 'empty',
+      ],
+    ];
+    $data['views']['view'] = [
+      'title' => t('View area'),
+      'help' => t('Insert a view inside an area.'),
+      'area' => [
+        'id' => 'view',
+      ],
+    ];
+    $data['views']['result'] = [
+      'title' => t('Result summary'),
+      'help' => t('Shows result summary, for example the items per page.'),
+      'area' => [
+        'id' => 'result',
+      ],
+    ];
+    $data['views']['messages'] = [
+      'title' => t('Messages'),
+      'help' => t('Displays messages in an area.'),
+      'area' => [
+        'id' => 'messages',
+      ],
+    ];
+    $data['views']['http_status_code'] = [
+      'title' => t('Response status code'),
+      'help' => t('Alter the HTTP response status code used by this view, mostly helpful for empty results.'),
+      'area' => [
+        'id' => 'http_status_code',
+      ],
+    ];
+    $data['views']['combine'] = [
+      'title' => t('Combine fields filter'),
+      'help' => t('Combine multiple fields together and search by them.'),
+      'filter' => [
+        'id' => 'combine',
+      ],
+    ];
+    $data['views']['dropbutton'] = [
+      'title' => t('Dropbutton'),
+      'help' => t('Display fields in a dropbutton.'),
+      'field' => [
+        'id' => 'dropbutton',
+      ],
+    ];
+    $data['views']['display_link'] = [
+      'title' => t('Link to display'),
+      'help' => t('Displays a link to a path-based display of this view while keeping the filter criteria, sort criteria, pager settings and contextual filters.'),
+      'area' => [
+        'id' => 'display_link',
+      ],
+    ];
+    // Registers an entity area handler per entity type.
+    foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
+      // Excludes entity types, which cannot be rendered.
+      if ($entity_type->hasViewBuilderClass()) {
+        $label = $entity_type->getLabel();
+        $data['views']['entity_' . $entity_type_id] = [
+          'title' => t('Rendered entity - @label', [
+            '@label' => $label,
+          ]),
+          'help' => t('Displays a rendered @label entity in an area.', [
+            '@label' => $label,
+          ]),
+          'area' => [
+            'entity_type' => $entity_type_id,
+            'id' => 'entity',
+          ],
+        ];
+      }
+    }
+    // Registers an action bulk form per entity.
+    foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type => $entity_info) {
+      $actions = array_filter(\Drupal::entityTypeManager()->getStorage('action')->loadMultiple(), function (ActionConfigEntityInterface $action) use ($entity_type) {
+          return $action->getType() == $entity_type;
+      });
+      if (empty($actions)) {
+        continue;
+      }
+      $data[$entity_info->getBaseTable()][$entity_type . '_bulk_form'] = [
+        'title' => t('Bulk update'),
+        'help' => t('Allows users to apply an action to one or more items.'),
+        'field' => [
+          'id' => 'bulk_form',
+        ],
+      ];
+    }
+    // Registers views data for the entity itself.
+    foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
+      if ($entity_type->hasHandlerClass('views_data')) {
+        /** @var \Drupal\views\EntityViewsDataInterface $views_data */
+        $views_data = \Drupal::entityTypeManager()->getHandler($entity_type_id, 'views_data');
+        $data = NestedArray::mergeDeep($data, $views_data->getViewsData());
+      }
+    }
+    // Field modules can implement hook_field_views_data() to override the default
+    // behavior for adding fields.
+    $module_handler = \Drupal::moduleHandler();
+    $entity_type_manager = \Drupal::entityTypeManager();
+    if ($entity_type_manager->hasDefinition('field_storage_config')) {
+      /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
+      foreach ($entity_type_manager->getStorage('field_storage_config')->loadMultiple() as $field_storage) {
+        if (_views_field_get_entity_type_storage($field_storage)) {
+          $provider = $field_storage->getTypeProvider();
+          $result = (array) $module_handler->invoke($provider === 'core' ? 'views' : $provider, 'field_views_data', [$field_storage]);
+          if (empty($result)) {
+            $result = views_field_default_views_data($field_storage);
+          }
+          $module_handler->alter('field_views_data', $result, $field_storage);
+          if (is_array($result)) {
+            $data = NestedArray::mergeDeep($result, $data);
+          }
+        }
+      }
+    }
+    return $data;
+  }
+
+  /**
+   * Implements hook_views_data_alter().
+   *
+   * Field modules can implement hook_field_views_data_views_data_alter() to
+   * alter the views data on a per field basis. This is weirdly named so as
+   * not to conflict with the \Drupal::moduleHandler()->alter('field_views_data')
+   * in views_views_data().
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(&$data) {
+    $entity_type_manager = \Drupal::entityTypeManager();
+    if (!$entity_type_manager->hasDefinition('field_storage_config')) {
+      return;
+    }
+    /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
+    foreach ($entity_type_manager->getStorage('field_storage_config')->loadMultiple() as $field_storage) {
+      if (_views_field_get_entity_type_storage($field_storage)) {
+        \Drupal::moduleHandler()->invoke($field_storage->getTypeProvider(), 'field_views_data_views_data_alter', [&$data, $field_storage]);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_field_views_data().
+   *
+   * The function implements the hook on behalf of 'core' because it adds a
+   * relationship and a reverse relationship to entity_reference field type, which
+   * is provided by core. This function also provides an argument plugin for
+   * entity_reference fields that handles title token replacement.
+   */
+  #[Hook('field_views_data')]
+  public function fieldViewsData(FieldStorageConfigInterface $field_storage) {
+    $data = views_field_default_views_data($field_storage);
+    // The code below only deals with the Entity reference field type.
+    if ($field_storage->getType() != 'entity_reference') {
+      return $data;
+    }
+    $entity_type_manager = \Drupal::entityTypeManager();
+    $entity_type_id = $field_storage->getTargetEntityTypeId();
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
+    $table_mapping = $entity_type_manager->getStorage($entity_type_id)->getTableMapping();
+    foreach ($data as $table_name => $table_data) {
+      // Add a relationship to the target entity type.
+      $target_entity_type_id = $field_storage->getSetting('target_type');
+      $target_entity_type = $entity_type_manager->getDefinition($target_entity_type_id);
+      $entity_type_id = $field_storage->getTargetEntityTypeId();
+      $entity_type = $entity_type_manager->getDefinition($entity_type_id);
+      $target_base_table = $target_entity_type->getDataTable() ?: $target_entity_type->getBaseTable();
+      $field_name = $field_storage->getName();
+      if ($target_entity_type instanceof ContentEntityTypeInterface) {
+        // Provide a relationship for the entity type with the entity reference
+        // field.
+        $args = ['@label' => $target_entity_type->getLabel(), '@field_name' => $field_name];
+        $data[$table_name][$field_name]['relationship'] = [
+          'title' => t('@label referenced from @field_name', $args),
+          'label' => t('@field_name: @label', $args),
+          'group' => $entity_type->getLabel(),
+          'help' => t('Appears in: @bundles.', [
+            '@bundles' => implode(', ', $field_storage->getBundles()),
+          ]),
+          'id' => 'standard',
+          'base' => $target_base_table,
+          'entity type' => $target_entity_type_id,
+          'base field' => $target_entity_type->getKey('id'),
+          'relationship field' => $field_name . '_target_id',
+        ];
+        // Provide a reverse relationship for the entity type that is referenced by
+        // the field.
+        $args['@entity'] = $entity_type->getLabel();
+        $args['@label'] = $target_entity_type->getSingularLabel();
+        $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name;
+        $data[$target_base_table][$pseudo_field_name]['relationship'] = [
+          'title' => t('@entity using @field_name', $args),
+          'label' => t('@field_name', [
+            '@field_name' => $field_name,
+          ]),
+          'group' => $target_entity_type->getLabel(),
+          'help' => t('Relate each @entity with a @field_name set to the @label.', $args),
+          'id' => 'entity_reverse',
+          'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
+          'entity_type' => $entity_type_id,
+          'base field' => $entity_type->getKey('id'),
+          'field_name' => $field_name,
+          'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
+          'field field' => $field_name . '_target_id',
+          'join_extra' => [
+                  [
+                    'field' => 'deleted',
+                    'value' => 0,
+                    'numeric' => TRUE,
+                  ],
+          ],
+        ];
+      }
+      // Provide an argument plugin that has a meaningful titleQuery()
+      // implementation getting the entity label.
+      $data[$table_name][$field_name . '_target_id']['argument']['id'] = 'entity_target_id';
+      $data[$table_name][$field_name . '_target_id']['argument']['target_entity_type_id'] = $target_entity_type_id;
+    }
+    return $data;
+  }
+
+}
diff --git a/core/modules/views/tests/modules/views_entity_test/src/Hook/ViewsEntityTestHooks.php b/core/modules/views/tests/modules/views_entity_test/src/Hook/ViewsEntityTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..27c861c8a910410da197438399021ea99e83d440
--- /dev/null
+++ b/core/modules/views/tests/modules/views_entity_test/src/Hook/ViewsEntityTestHooks.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\views_entity_test\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_entity_test.
+ */
+class ViewsEntityTestHooks {
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    if ($entity_type->id() == 'entity_test') {
+      $definitions['test_text_access'] = BaseFieldDefinition::create('string')->setLabel(t('Test access'))->setTranslatable(FALSE)->setSetting('max_length', 64)->setDisplayOptions('form', ['type' => 'string_textfield', 'weight' => 10]);
+      return $definitions;
+    }
+  }
+
+  /**
+   * Implements hook_entity_field_access().
+   *
+   * @see \Drupal\system\Tests\Entity\FieldAccessTest::testFieldAccess()
+   */
+  #[Hook('entity_field_access')]
+  public function entityFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
+    if ($field_definition->getName() == 'test_text_access') {
+      if ($items) {
+        if ($items->value == 'no access value') {
+          return AccessResult::forbidden()->addCacheableDependency($items->getEntity());
+        }
+      }
+    }
+    // No opinion.
+    return AccessResult::neutral();
+  }
+
+  /**
+   * Implements hook_entity_load().
+   *
+   * @see \Drupal\Tests\views\Kernel\Handler\FieldFieldTest::testSimpleExecute()
+   */
+  #[Hook('entity_load')]
+  public function entityLoad(array $entities, $entity_type_id) {
+    if ($entity_type_id === 'entity_test') {
+      // Cast the value of an entity field to be something else than a string so
+      // we can check that
+      // \Drupal\views\Tests\ViewResultAssertionTrait::assertIdenticalResultsetHelper()
+      // takes care of converting all field values to strings.
+      foreach ($entities as $entity) {
+        $entity->user_id->target_id = (int) $entity->user_id->target_id;
+      }
+    }
+  }
+
+}
diff --git a/core/modules/views/tests/modules/views_entity_test/views_entity_test.module b/core/modules/views/tests/modules/views_entity_test/views_entity_test.module
deleted file mode 100644
index aa8a06b3432e33078d15c8c044b0a89fcb3dabb3..0000000000000000000000000000000000000000
--- a/core/modules/views/tests/modules/views_entity_test/views_entity_test.module
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains main module functionality.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Implements hook_entity_base_field_info().
- */
-function views_entity_test_entity_base_field_info(EntityTypeInterface $entity_type) {
-  if ($entity_type->id() == 'entity_test') {
-    $definitions['test_text_access'] = BaseFieldDefinition::create('string')
-      ->setLabel(t('Test access'))
-      ->setTranslatable(FALSE)
-      ->setSetting('max_length', 64)
-      ->setDisplayOptions('form', [
-        'type' => 'string_textfield',
-        'weight' => 10,
-      ]);
-    return $definitions;
-  }
-}
-
-/**
- * Implements hook_entity_field_access().
- *
- * @see \Drupal\system\Tests\Entity\FieldAccessTest::testFieldAccess()
- */
-function views_entity_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, ?FieldItemListInterface $items = NULL) {
-  if ($field_definition->getName() == 'test_text_access') {
-    if ($items) {
-      if ($items->value == 'no access value') {
-        return AccessResult::forbidden()->addCacheableDependency($items->getEntity());
-      }
-    }
-  }
-  // No opinion.
-  return AccessResult::neutral();
-}
-
-/**
- * Implements hook_entity_load().
- *
- * @see \Drupal\Tests\views\Kernel\Handler\FieldFieldTest::testSimpleExecute()
- */
-function views_entity_test_entity_load(array $entities, $entity_type_id) {
-  if ($entity_type_id === 'entity_test') {
-    // Cast the value of an entity field to be something else than a string so
-    // we can check that
-    // \Drupal\views\Tests\ViewResultAssertionTrait::assertIdenticalResultsetHelper()
-    // takes care of converting all field values to strings.
-    foreach ($entities as $entity) {
-      $entity->user_id->target_id = (int) $entity->user_id->target_id;
-    }
-  }
-}
diff --git a/core/modules/views/tests/modules/views_form_test/src/Hook/ViewsFormTestHooks.php b/core/modules/views/tests/modules/views_form_test/src/Hook/ViewsFormTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..5a15d7f85163cc10ce4a7a95289c6586bc095849
--- /dev/null
+++ b/core/modules/views/tests/modules/views_form_test/src/Hook/ViewsFormTestHooks.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\views_form_test\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_form_test.
+ */
+class ViewsFormTestHooks {
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter().
+   */
+  #[Hook('form_views_form_media_media_page_list_alter')]
+  public function formViewsFormMediaMediaPageListAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    $state = \Drupal::state();
+    $count = $state->get('hook_form_BASE_FORM_ID_alter_count', 0);
+    $state->set('hook_form_BASE_FORM_ID_alter_count', $count + 1);
+  }
+
+}
diff --git a/core/modules/views/tests/modules/views_form_test/views_form_test.module b/core/modules/views/tests/modules/views_form_test/views_form_test.module
deleted file mode 100644
index 9cc51882091c8c6d30b85b4cc38eda777cd1deee..0000000000000000000000000000000000000000
--- a/core/modules/views/tests/modules/views_form_test/views_form_test.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Hook implementations for this module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * Implements hook_form_BASE_FORM_ID_alter().
- */
-function views_form_test_form_views_form_media_media_page_list_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  $state = \Drupal::state();
-  $count = $state->get('hook_form_BASE_FORM_ID_alter_count', 0);
-  $state->set('hook_form_BASE_FORM_ID_alter_count', $count + 1);
-}
diff --git a/core/modules/views/tests/modules/views_test_config/src/Hook/ViewsTestConfigHooks.php b/core/modules/views/tests/modules/views_test_config/src/Hook/ViewsTestConfigHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..ae21dd62c260b12260dbf298cb7746610d0a6fa1
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_config/src/Hook/ViewsTestConfigHooks.php
@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\views_test_config\Hook;
+
+use Drupal\views\Plugin\views\cache\CachePluginBase;
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_test_config.
+ */
+class ViewsTestConfigHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_load().
+   */
+  #[Hook('view_load')]
+  public function viewLoad(array $views) {
+    // Emulate a severely broken view: this kind of view configuration cannot be
+    // saved, it can likely be returned only by a corrupt active configuration.
+    $broken_view_id = \Drupal::state()->get('views_test_config.broken_view');
+    if (isset($views[$broken_view_id])) {
+      $display =& $views[$broken_view_id]->getDisplay('default');
+      $display['display_options']['fields']['id_broken'] = NULL;
+    }
+  }
+
+  /**
+   * Implements hook_views_post_render().
+   */
+  #[Hook('views_post_render')]
+  public function viewsPostRender(ViewExecutable $view, &$output, CachePluginBase $cache) {
+    if (\Drupal::state()->get('views_test_config.views_post_render_cache_tag')) {
+      \Drupal::state()->set('views_test_config.views_post_render_called', TRUE);
+      // Set a cache key on output to ensure ViewsSelection::stripAdminAndAnchorTagsFromResults
+      // correctly handles elements that aren't result rows.
+      $output['#cache']['tags'][] = 'foo';
+    }
+  }
+
+  /**
+   * Implements hook_views_plugins_area_alter().
+   */
+  #[Hook('views_plugins_area_alter')]
+  public function viewsPluginsAreaAlter(array &$definitions) : void {
+    _views_test_config_disable_broken_handler($definitions, 'area');
+  }
+
+  /**
+   * Implements hook_views_plugins_argument_alter().
+   */
+  #[Hook('views_plugins_argument_alter')]
+  public function viewsPluginsArgumentAlter(array &$definitions) : void {
+    _views_test_config_disable_broken_handler($definitions, 'argument');
+  }
+
+  /**
+   * Implements hook_views_plugins_field_alter().
+   */
+  #[Hook('views_plugins_field_alter')]
+  public function viewsPluginsFieldAlter(array &$definitions) : void {
+    _views_test_config_disable_broken_handler($definitions, 'field');
+  }
+
+  /**
+   * Implements hook_views_plugins_filter_alter().
+   */
+  #[Hook('views_plugins_filter_alter')]
+  public function viewsPluginsFilterAlter(array &$definitions) : void {
+    _views_test_config_disable_broken_handler($definitions, 'filter');
+  }
+
+  /**
+   * Implements hook_views_plugins_relationship_alter().
+   */
+  #[Hook('views_plugins_relationship_alter')]
+  public function viewsPluginsRelationshipAlter(array &$definitions) : void {
+    _views_test_config_disable_broken_handler($definitions, 'relationship');
+  }
+
+  /**
+   * Implements hook_views_plugins_sort_alter().
+   */
+  #[Hook('views_plugins_sort_alter')]
+  public function viewsPluginsSortAlter(array &$definitions) : void {
+    _views_test_config_disable_broken_handler($definitions, 'sort');
+  }
+
+}
diff --git a/core/modules/views/tests/modules/views_test_config/views_test_config.module b/core/modules/views/tests/modules/views_test_config/views_test_config.module
index 500a8f32068fbfa840d7d43e221ca66f4682ba67..71a25f9b017ac0b5a4e5bf3be40541efe32a03de 100644
--- a/core/modules/views/tests/modules/views_test_config/views_test_config.module
+++ b/core/modules/views/tests/modules/views_test_config/views_test_config.module
@@ -7,78 +7,8 @@
 
 declare(strict_types=1);
 
-use Drupal\views\Plugin\views\cache\CachePluginBase;
-use Drupal\views\ViewExecutable;
-
-/**
- * Implements hook_ENTITY_TYPE_load().
- */
-function views_test_config_view_load(array $views) {
-  // Emulate a severely broken view: this kind of view configuration cannot be
-  // saved, it can likely be returned only by a corrupt active configuration.
-  $broken_view_id = \Drupal::state()->get('views_test_config.broken_view');
-  if (isset($views[$broken_view_id])) {
-    $display =& $views[$broken_view_id]->getDisplay('default');
-    $display['display_options']['fields']['id_broken'] = NULL;
-  }
-}
-
-/**
- * Implements hook_views_post_render().
- */
-function views_test_config_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) {
-  if (\Drupal::state()->get('views_test_config.views_post_render_cache_tag')) {
-    \Drupal::state()->set('views_test_config.views_post_render_called', TRUE);
-    // Set a cache key on output to ensure ViewsSelection::stripAdminAndAnchorTagsFromResults
-    // correctly handles elements that aren't result rows.
-    $output['#cache']['tags'][] = 'foo';
-  }
-}
-
 function _views_test_config_disable_broken_handler(array &$definitions, string $handler_type): void {
   if (in_array($handler_type, \Drupal::state()->get('views_test_config_disable_broken_handler', []))) {
     unset($definitions['broken']);
   }
 }
-
-/**
- * Implements hook_views_plugins_area_alter().
- */
-function views_test_config_views_plugins_area_alter(array &$definitions): void {
-  _views_test_config_disable_broken_handler($definitions, 'area');
-}
-
-/**
- * Implements hook_views_plugins_argument_alter().
- */
-function views_test_config_views_plugins_argument_alter(array &$definitions): void {
-  _views_test_config_disable_broken_handler($definitions, 'argument');
-}
-
-/**
- * Implements hook_views_plugins_field_alter().
- */
-function views_test_config_views_plugins_field_alter(array &$definitions): void {
-  _views_test_config_disable_broken_handler($definitions, 'field');
-}
-
-/**
- * Implements hook_views_plugins_filter_alter().
- */
-function views_test_config_views_plugins_filter_alter(array &$definitions): void {
-  _views_test_config_disable_broken_handler($definitions, 'filter');
-}
-
-/**
- * Implements hook_views_plugins_relationship_alter().
- */
-function views_test_config_views_plugins_relationship_alter(array &$definitions): void {
-  _views_test_config_disable_broken_handler($definitions, 'relationship');
-}
-
-/**
- * Implements hook_views_plugins_sort_alter().
- */
-function views_test_config_views_plugins_sort_alter(array &$definitions): void {
-  _views_test_config_disable_broken_handler($definitions, 'sort');
-}
diff --git a/core/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataHooks.php b/core/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..14be3dfaf4f331b4dedea2c7fcafd6f07abc41ce
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataHooks.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\views_test_data\Hook;
+
+use Drupal\views\ViewEntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_test_data.
+ */
+class ViewsTestDataHooks {
+
+  /**
+   * Implements hook_form_BASE_FORM_ID_alter().
+   */
+  #[Hook('form_views_form_test_form_multiple_default_alter')]
+  public function formViewsFormTestFormMultipleDefaultAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    \Drupal::messenger()->addStatus(t('Test base form ID with Views forms and arguments.'));
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for the 'view' entity type.
+   */
+  #[Hook('view_update')]
+  public function viewUpdate(ViewEntityInterface $view) {
+    // Use state to keep track of how many times a file is saved.
+    $view_save_count = \Drupal::state()->get('views_test_data.view_save_count', []);
+    $view_save_count[$view->id()] = isset($view_save_count[$view->id()]) ? $view_save_count[$view->id()] + 1 : 1;
+    \Drupal::state()->set('views_test_data.view_save_count', $view_save_count);
+  }
+
+}
diff --git a/core/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php b/core/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..624bb6b21e269559634fd8b3132eaa4ce0d0741c
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsExecutionHooks.php
@@ -0,0 +1,146 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\views_test_data\Hook;
+
+use Drupal\views\Plugin\views\cache\CachePluginBase;
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_test_data.
+ */
+class ViewsTestDataViewsExecutionHooks {
+
+  /**
+   * Implements hook_views_query_substitutions().
+   */
+  #[Hook('views_query_substitutions')]
+  public function viewsQuerySubstitutions(ViewExecutable $view) {
+    \Drupal::state()->set('views_hook_test_views_query_substitutions', TRUE);
+  }
+
+  /**
+   * Implements hook_views_form_substitutions().
+   */
+  #[Hook('views_form_substitutions')]
+  public function viewsFormSubstitutions() {
+    \Drupal::state()->set('views_hook_test_views_form_substitutions', TRUE);
+    $render = ['#markup' => '<em>unescaped</em>'];
+    return [
+      '<!--will-be-escaped-->' => '<em>escaped</em>',
+      '<!--will-be-not-escaped-->' => \Drupal::service('renderer')->renderInIsolation($render),
+    ];
+  }
+
+  /**
+   * Implements hook_field_views_data().
+   */
+  #[Hook('field_views_data')]
+  public function fieldViewsData(FieldStorageConfigInterface $field_storage) {
+    \Drupal::state()->set('views_hook_test_field_views_data', TRUE);
+  }
+
+  /**
+   * Implements hook_field_views_data_alter().
+   */
+  #[Hook('field_views_data_alter')]
+  public function fieldViewsDataAlter(&$data, FieldStorageConfigInterface $field_storage, $module) {
+    \Drupal::state()->set('views_hook_test_field_views_data_alter', TRUE);
+  }
+
+  /**
+   * Implements hook_views_pre_render().
+   *
+   * @see \Drupal\views\Tests\Plugin\CacheTest
+   * @see \Drupal\views\Tests\Plugin\RenderTest
+   */
+  #[Hook('views_pre_render')]
+  public function viewsPreRender(ViewExecutable $view) {
+    \Drupal::state()->set('views_hook_test_views_pre_render', TRUE);
+    if (isset($view) && $view->storage->id() == 'test_cache_header_storage') {
+      $view->element['#attached']['library'][] = 'views_test_data/test';
+      $view->element['#attached']['drupalSettings']['foo'] = 'bar';
+      $view->element['#attached']['placeholders']['non-existing-placeholder-just-for-testing-purposes']['#lazy_builder'] = [
+        'Drupal\views_test_data\Controller\ViewsTestDataController::placeholderLazyBuilder',
+            [
+              'bar',
+            ],
+      ];
+      $view->element['#cache']['tags'][] = 'views_test_data:1';
+      $view->build_info['pre_render_called'] = TRUE;
+    }
+  }
+
+  /**
+   * Implements hook_views_post_render().
+   */
+  #[Hook('views_post_render')]
+  public function viewsPostRender(ViewExecutable $view, &$output, CachePluginBase $cache) {
+    \Drupal::state()->set('views_hook_test_views_post_render', TRUE);
+    if ($view->storage->id() === 'test_page_display' && $view->current_display === 'empty_row') {
+      for ($i = 0; $i < 5; $i++) {
+        $output['#rows'][0]['#rows'][] = [];
+      }
+    }
+  }
+
+  /**
+   * Implements hook_views_pre_build().
+   */
+  #[Hook('views_pre_build')]
+  public function viewsPreBuild(ViewExecutable $view) {
+    \Drupal::state()->set('views_hook_test_views_pre_build', TRUE);
+  }
+
+  /**
+   * Implements hook_views_post_build().
+   */
+  #[Hook('views_post_build')]
+  public function viewsPostBuild(ViewExecutable $view) {
+    \Drupal::state()->set('views_hook_test_views_post_build', TRUE);
+    if (isset($view) && $view->storage->id() == 'test_page_display') {
+      if ($view->current_display == 'page_1') {
+        $view->build_info['denied'] = TRUE;
+      }
+      elseif ($view->current_display == 'page_2') {
+        $view->build_info['fail'] = TRUE;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_views_pre_view().
+   */
+  #[Hook('views_pre_view')]
+  public function viewsPreView(ViewExecutable $view) {
+    \Drupal::state()->set('views_hook_test_views_pre_view', TRUE);
+  }
+
+  /**
+   * Implements hook_views_pre_execute().
+   */
+  #[Hook('views_pre_execute')]
+  public function viewsPreExecute(ViewExecutable $view) {
+    \Drupal::state()->set('views_hook_test_views_pre_execute', TRUE);
+  }
+
+  /**
+   * Implements hook_views_post_execute().
+   */
+  #[Hook('views_post_execute')]
+  public function viewsPostExecute(ViewExecutable $view) {
+    \Drupal::state()->set('views_hook_test_views_post_execute', TRUE);
+  }
+
+  /**
+   * Implements hook_views_query_alter().
+   */
+  #[Hook('views_query_alter')]
+  public function viewsQueryAlter(ViewExecutable $view) {
+    \Drupal::state()->set('views_hook_test_views_query_alter', TRUE);
+  }
+
+}
diff --git a/core/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsHooks.php b/core/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..a795434a6fc84f656a45c0708649b480e77a07a1
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_data/src/Hook/ViewsTestDataViewsHooks.php
@@ -0,0 +1,64 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\views_test_data\Hook;
+
+use Drupal\views\Analyzer;
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_test_data.
+ */
+class ViewsTestDataViewsHooks {
+
+  /**
+   * Implements hook_views_data().
+   */
+  #[Hook('views_data')]
+  public function viewsData() {
+    $state = \Drupal::service('state');
+    $state->set('views_hook_test_views_data', TRUE);
+    // We use a state variable to keep track of how many times this function is
+    // called so we can assert that calls to
+    // \Drupal\views\ViewsData::delete() trigger a rebuild of views data.
+    if (!($count = $state->get('views_test_data_views_data_count'))) {
+      $count = 0;
+    }
+    $count++;
+    $state->set('views_test_data_views_data_count', $count);
+    return $state->get('views_test_data_views_data', []);
+  }
+
+  /**
+   * Implements hook_views_data_alter().
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(array &$data) {
+    \Drupal::state()->set('views_hook_test_views_data_alter', TRUE);
+    \Drupal::state()->set('views_hook_test_views_data_alter_data', $data);
+  }
+
+  /**
+   * Implements hook_views_analyze().
+   */
+  #[Hook('views_analyze')]
+  public function viewsAnalyze(ViewExecutable $view) {
+    \Drupal::state()->set('views_hook_test_views_analyze', TRUE);
+    $ret = [];
+    $ret[] = Analyzer::formatMessage(t('Test ok message'), 'ok');
+    $ret[] = Analyzer::formatMessage(t('Test warning message'), 'warning');
+    $ret[] = Analyzer::formatMessage(t('Test error message'), 'error');
+    return $ret;
+  }
+
+  /**
+   * Implements hook_views_invalidate_cache().
+   */
+  #[Hook('views_invalidate_cache')]
+  public function viewsInvalidateCache() {
+    \Drupal::state()->set('views_hook_test_views_invalidate_cache', TRUE);
+  }
+
+}
diff --git a/core/modules/views/tests/modules/views_test_data/views_test_data.module b/core/modules/views/tests/modules/views_test_data/views_test_data.module
index 6fc06500a97645fae4ff00a07784f1af179492cf..3d49926c628c3e747156b80c9d42436e2047f69d 100644
--- a/core/modules/views/tests/modules/views_test_data/views_test_data.module
+++ b/core/modules/views/tests/modules/views_test_data/views_test_data.module
@@ -7,9 +7,6 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\views\ViewEntityInterface;
-
 /**
  * Access callback for the generic handler test.
  *
@@ -113,20 +110,3 @@ function template_preprocess_views_view_mapping_test(&$variables) {
     }
   }
 }
-
-/**
- * Implements hook_form_BASE_FORM_ID_alter().
- */
-function views_test_data_form_views_form_test_form_multiple_default_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  \Drupal::messenger()->addStatus(t('Test base form ID with Views forms and arguments.'));
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for the 'view' entity type.
- */
-function views_test_data_view_update(ViewEntityInterface $view) {
-  // Use state to keep track of how many times a file is saved.
-  $view_save_count = \Drupal::state()->get('views_test_data.view_save_count', []);
-  $view_save_count[$view->id()] = isset($view_save_count[$view->id()]) ? $view_save_count[$view->id()] + 1 : 1;
-  \Drupal::state()->set('views_test_data.view_save_count', $view_save_count);
-}
diff --git a/core/modules/views/tests/modules/views_test_data/views_test_data.views.inc b/core/modules/views/tests/modules/views_test_data/views_test_data.views.inc
deleted file mode 100644
index 2c704f7f7a50a1373bae2e813f5e7080769070bd..0000000000000000000000000000000000000000
--- a/core/modules/views/tests/modules/views_test_data/views_test_data.views.inc
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides views data and hooks for views_test_data module.
- */
-
-declare(strict_types=1);
-
-use Drupal\views\ViewExecutable;
-use Drupal\views\Analyzer;
-
-/**
- * Implements hook_views_data().
- */
-function views_test_data_views_data() {
-  $state = \Drupal::service('state');
-  $state->set('views_hook_test_views_data', TRUE);
-  // We use a state variable to keep track of how many times this function is
-  // called so we can assert that calls to
-  // \Drupal\views\ViewsData::delete() trigger a rebuild of views data.
-  if (!($count = $state->get('views_test_data_views_data_count'))) {
-    $count = 0;
-  }
-  $count++;
-  $state->set('views_test_data_views_data_count', $count);
-
-  return $state->get('views_test_data_views_data', []);
-}
-
-/**
- * Implements hook_views_data_alter().
- */
-function views_test_data_views_data_alter(array &$data) {
-  \Drupal::state()->set('views_hook_test_views_data_alter', TRUE);
-  \Drupal::state()->set('views_hook_test_views_data_alter_data', $data);
-}
-
-/**
- * Implements hook_views_analyze().
- */
-function views_test_data_views_analyze(ViewExecutable $view) {
-  \Drupal::state()->set('views_hook_test_views_analyze', TRUE);
-
-  $ret = [];
-
-  $ret[] = Analyzer::formatMessage(t('Test ok message'), 'ok');
-  $ret[] = Analyzer::formatMessage(t('Test warning message'), 'warning');
-  $ret[] = Analyzer::formatMessage(t('Test error message'), 'error');
-
-  return $ret;
-}
-
-/**
- * Implements hook_views_invalidate_cache().
- */
-function views_test_data_views_invalidate_cache() {
-  \Drupal::state()->set('views_hook_test_views_invalidate_cache', TRUE);
-}
diff --git a/core/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc b/core/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc
deleted file mode 100644
index 380a853eaa48946c08a0f20dc8c61c406490aa75..0000000000000000000000000000000000000000
--- a/core/modules/views/tests/modules/views_test_data/views_test_data.views_execution.inc
+++ /dev/null
@@ -1,127 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides views data and hooks for views_test_data module.
- */
-
-declare(strict_types=1);
-
-use Drupal\field\FieldStorageConfigInterface;
-use Drupal\views\Plugin\views\cache\CachePluginBase;
-use Drupal\views\ViewExecutable;
-
-/**
- * Implements hook_views_query_substitutions().
- */
-function views_test_data_views_query_substitutions(ViewExecutable $view) {
-  \Drupal::state()->set('views_hook_test_views_query_substitutions', TRUE);
-}
-
-/**
- * Implements hook_views_form_substitutions().
- */
-function views_test_data_views_form_substitutions() {
-  \Drupal::state()->set('views_hook_test_views_form_substitutions', TRUE);
-  $render = ['#markup' => '<em>unescaped</em>'];
-  return [
-    '<!--will-be-escaped-->' => '<em>escaped</em>',
-    '<!--will-be-not-escaped-->' => \Drupal::service('renderer')->renderInIsolation($render),
-  ];
-}
-
-/**
- * Implements hook_field_views_data().
- */
-function views_test_data_field_views_data(FieldStorageConfigInterface $field_storage) {
-  \Drupal::state()->set('views_hook_test_field_views_data', TRUE);
-}
-
-/**
- * Implements hook_field_views_data_alter().
- */
-function views_test_data_field_views_data_alter(&$data, FieldStorageConfigInterface $field_storage, $module) {
-  \Drupal::state()->set('views_hook_test_field_views_data_alter', TRUE);
-}
-
-/**
- * Implements hook_views_pre_render().
- *
- * @see \Drupal\views\Tests\Plugin\CacheTest
- * @see \Drupal\views\Tests\Plugin\RenderTest
- */
-function views_test_data_views_pre_render(ViewExecutable $view) {
-  \Drupal::state()->set('views_hook_test_views_pre_render', TRUE);
-
-  if (isset($view) && ($view->storage->id() == 'test_cache_header_storage')) {
-    $view->element['#attached']['library'][] = 'views_test_data/test';
-    $view->element['#attached']['drupalSettings']['foo'] = 'bar';
-    $view->element['#attached']['placeholders']['non-existing-placeholder-just-for-testing-purposes']['#lazy_builder'] = ['Drupal\views_test_data\Controller\ViewsTestDataController::placeholderLazyBuilder', ['bar']];
-    $view->element['#cache']['tags'][] = 'views_test_data:1';
-    $view->build_info['pre_render_called'] = TRUE;
-  }
-
-}
-
-/**
- * Implements hook_views_post_render().
- */
-function views_test_data_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) {
-  \Drupal::state()->set('views_hook_test_views_post_render', TRUE);
-  if ($view->storage->id() === 'test_page_display' && $view->current_display === 'empty_row') {
-    for ($i = 0; $i < 5; $i++) {
-      $output['#rows'][0]['#rows'][] = [];
-    }
-  }
-}
-
-/**
- * Implements hook_views_pre_build().
- */
-function views_test_data_views_pre_build(ViewExecutable $view) {
-  \Drupal::state()->set('views_hook_test_views_pre_build', TRUE);
-}
-
-/**
- * Implements hook_views_post_build().
- */
-function views_test_data_views_post_build(ViewExecutable $view) {
-  \Drupal::state()->set('views_hook_test_views_post_build', TRUE);
-
-  if (isset($view) && ($view->storage->id() == 'test_page_display')) {
-    if ($view->current_display == 'page_1') {
-      $view->build_info['denied'] = TRUE;
-    }
-    elseif ($view->current_display == 'page_2') {
-      $view->build_info['fail'] = TRUE;
-    }
-  }
-}
-
-/**
- * Implements hook_views_pre_view().
- */
-function views_test_data_views_pre_view(ViewExecutable $view) {
-  \Drupal::state()->set('views_hook_test_views_pre_view', TRUE);
-}
-
-/**
- * Implements hook_views_pre_execute().
- */
-function views_test_data_views_pre_execute(ViewExecutable $view) {
-  \Drupal::state()->set('views_hook_test_views_pre_execute', TRUE);
-}
-
-/**
- * Implements hook_views_post_execute().
- */
-function views_test_data_views_post_execute(ViewExecutable $view) {
-  \Drupal::state()->set('views_hook_test_views_post_execute', TRUE);
-}
-
-/**
- * Implements hook_views_query_alter().
- */
-function views_test_data_views_query_alter(ViewExecutable $view) {
-  \Drupal::state()->set('views_hook_test_views_query_alter', TRUE);
-}
diff --git a/core/modules/views/tests/modules/views_test_entity_reference/src/Hook/ViewsTestEntityReferenceHooks.php b/core/modules/views/tests/modules/views_test_entity_reference/src/Hook/ViewsTestEntityReferenceHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..8add41eef5a4887e21f1838652efd68fc586e2d3
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_entity_reference/src/Hook/ViewsTestEntityReferenceHooks.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\views_test_entity_reference\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_test_entity_reference.
+ */
+class ViewsTestEntityReferenceHooks {
+
+  /**
+   * Implements hook_views_data_alter().
+   */
+  #[Hook('views_data_alter')]
+  public function viewsDataAlter(&$data) {
+    $manager = \Drupal::entityTypeManager();
+    $field_config_storage = $manager->getStorage('field_config');
+    /** @var \Drupal\field\FieldConfigInterface[] $field_configs */
+    $field_configs = $field_config_storage->loadByProperties(['field_type' => 'entity_reference']);
+    foreach ($field_configs as $field_config) {
+      $table_name = $field_config->getTargetEntityTypeId() . '__' . $field_config->getName();
+      $column_name = $field_config->getName() . '_target_id';
+      if (isset($data[$table_name][$column_name]['filter']['id']) && in_array($data[$table_name][$column_name]['filter']['id'], ['numeric', 'string'])) {
+        $data[$table_name][$column_name]['filter']['id'] = 'entity_reference';
+      }
+    }
+  }
+
+}
diff --git a/core/modules/views/tests/modules/views_test_entity_reference/views_test_entity_reference.module b/core/modules/views/tests/modules/views_test_entity_reference/views_test_entity_reference.module
deleted file mode 100644
index a4075cf4ab6c82c7e361904ff21fe919aa896705..0000000000000000000000000000000000000000
--- a/core/modules/views/tests/modules/views_test_entity_reference/views_test_entity_reference.module
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-/**
- * @file
- * Views data altering to test use of the entity reference plugin.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_views_data_alter().
- */
-function views_test_entity_reference_views_data_alter(&$data) {
-  $manager = \Drupal::entityTypeManager();
-  $field_config_storage = $manager->getStorage('field_config');
-  /** @var \Drupal\field\FieldConfigInterface[] $field_configs */
-  $field_configs = $field_config_storage->loadByProperties([
-    'field_type' => 'entity_reference',
-  ]);
-  foreach ($field_configs as $field_config) {
-    $table_name = $field_config->getTargetEntityTypeId() . '__' . $field_config->getName();
-    $column_name = $field_config->getName() . '_target_id';
-    if (
-      isset($data[$table_name][$column_name]['filter']['id'])
-      && in_array($data[$table_name][$column_name]['filter']['id'], ['numeric', 'string'])
-    ) {
-      $data[$table_name][$column_name]['filter']['id'] = 'entity_reference';
-    }
-  }
-}
diff --git a/core/modules/views/tests/modules/views_test_query_access/src/Hook/ViewsTestQueryAccessHooks.php b/core/modules/views/tests/modules/views_test_query_access/src/Hook/ViewsTestQueryAccessHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..14f298de4b3501d0b5ab7c9356f101125db3b5f2
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_query_access/src/Hook/ViewsTestQueryAccessHooks.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\views_test_query_access\Hook;
+
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_test_query_access.
+ */
+class ViewsTestQueryAccessHooks {
+
+  /**
+   * Implements hook_query_TAG_alter() for the 'media_access' query tag.
+   */
+  #[Hook('query_media_access_alter')]
+  public function queryMediaAccessAlter(AlterableInterface $query) {
+    _views_test_query_access_restrict_by_uuid($query);
+  }
+
+  /**
+   * Implements hook_query_TAG_alter() for the 'block_content_access' query tag.
+   */
+  #[Hook('query_block_content_access_alter')]
+  public function queryBlockContentAccessAlter(AlterableInterface $query) {
+    _views_test_query_access_restrict_by_uuid($query);
+  }
+
+}
diff --git a/core/modules/views/tests/modules/views_test_query_access/views_test_query_access.module b/core/modules/views/tests/modules/views_test_query_access/views_test_query_access.module
index 47c77c4cfd578cf64e8c857e57a5618d40168cec..ab1a1c11fe94a6a0885719ce4d786ae909d179a2 100644
--- a/core/modules/views/tests/modules/views_test_query_access/views_test_query_access.module
+++ b/core/modules/views/tests/modules/views_test_query_access/views_test_query_access.module
@@ -12,20 +12,6 @@
 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
 use Drupal\Core\Entity\Sql\DefaultTableMapping;
 
-/**
- * Implements hook_query_TAG_alter() for the 'media_access' query tag.
- */
-function views_test_query_access_query_media_access_alter(AlterableInterface $query) {
-  _views_test_query_access_restrict_by_uuid($query);
-}
-
-/**
- * Implements hook_query_TAG_alter() for the 'block_content_access' query tag.
- */
-function views_test_query_access_query_block_content_access_alter(AlterableInterface $query) {
-  _views_test_query_access_restrict_by_uuid($query);
-}
-
 /**
  * Excludes entities with the 'hidden-ENTITY_TYPE_ID' UUID from a query.
  *
diff --git a/core/modules/views/tests/src/Kernel/ViewsTemplateTest.php b/core/modules/views/tests/src/Kernel/ViewsTemplateTest.php
index f8ca7a1b912601d2842932554d2b3ceb7a9c25d7..5775163aa2eb39473a240c8337e95881ee3803a1 100644
--- a/core/modules/views/tests/src/Kernel/ViewsTemplateTest.php
+++ b/core/modules/views/tests/src/Kernel/ViewsTemplateTest.php
@@ -45,7 +45,7 @@ public function testTemplate(): void {
   }
 
   /**
-   * @covers views_theme_suggestions_container_alter
+   * @covers \Drupal\views\Hook\ViewsHooks::themeSuggestionsContainerAlter
    * @throws \Exception
    */
   public function testThemeSuggestionsContainerAlter(): void {
diff --git a/core/modules/views/views.api.php b/core/modules/views/views.api.php
index ab6f7f63b15a26bd5969af23508071a233f05201..a2185d57a687269025bd27b1768dadb17c5fe4ec 100644
--- a/core/modules/views/views.api.php
+++ b/core/modules/views/views.api.php
@@ -5,6 +5,11 @@
  * Describes hooks and plugins provided by the Views module.
  */
 
+use Drupal\views\Analyzer;
+use Drupal\field\FieldStorageConfigInterface;
+use Drupal\views\Plugin\views\pager\Full;
+use Drupal\views\Plugin\views\cache\Time;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\views\Plugin\views\cache\CachePluginBase;
 use Drupal\views\Plugin\views\PluginBase;
@@ -84,11 +89,11 @@
  *   Array of warning messages built by Analyzer::formatMessage to be displayed
  *   to the user following analysis of the view.
  */
-function hook_views_analyze(\Drupal\views\ViewExecutable $view) {
+function hook_views_analyze(ViewExecutable $view) {
   $messages = [];
 
   if ($view->display_handler->options['pager']['type'] == 'none') {
-    $messages[] = Drupal\views\Analyzer::formatMessage(t('This view has no pager. This could cause performance issues when the view contains many items.'), 'warning');
+    $messages[] = Analyzer::formatMessage(t('This view has no pager. This could cause performance issues when the view contains many items.'), 'warning');
   }
 
   return $messages;
@@ -544,7 +549,7 @@ function hook_views_data_alter(array &$data) {
  * @see hook_field_views_data_alter()
  * @see hook_field_views_data_views_data_alter()
  */
-function hook_field_views_data(\Drupal\field\FieldStorageConfigInterface $field_storage) {
+function hook_field_views_data(FieldStorageConfigInterface $field_storage) {
   $data = views_field_default_views_data($field_storage);
   foreach ($data as $table_name => $table_data) {
     // Add the relationship only on the target_id field.
@@ -577,7 +582,7 @@ function hook_field_views_data(\Drupal\field\FieldStorageConfigInterface $field_
  * @see hook_field_views_data()
  * @see hook_field_views_data_views_data_alter()
  */
-function hook_field_views_data_alter(array &$data, \Drupal\field\FieldStorageConfigInterface $field_storage) {
+function hook_field_views_data_alter(array &$data, FieldStorageConfigInterface $field_storage) {
   $entity_type_id = $field_storage->getTargetEntityTypeId();
   $field_name = $field_storage->getName();
   $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
@@ -633,7 +638,7 @@ function hook_field_views_data_alter(array &$data, \Drupal\field\FieldStorageCon
  * @see hook_field_views_data_alter()
  * @see views_views_data_alter()
  */
-function hook_field_views_data_views_data_alter(array &$data, \Drupal\field\FieldStorageConfigInterface $field) {
+function hook_field_views_data_views_data_alter(array &$data, FieldStorageConfigInterface $field) {
   $field_name = $field->getName();
   $data_key = 'field_data_' . $field_name;
   $entity_type_id = $field->getTargetEntityTypeId();
@@ -878,7 +883,7 @@ function hook_views_pre_render(ViewExecutable $view) {
 function hook_views_post_render(ViewExecutable $view, array &$output, CachePluginBase $cache) {
   // When using full pager, disable any time-based caching if there are fewer
   // than 10 results.
-  if ($view->pager instanceof Drupal\views\Plugin\views\pager\Full && $cache instanceof Drupal\views\Plugin\views\cache\Time && count($view->result) < 10) {
+  if ($view->pager instanceof Full && $cache instanceof Time && count($view->result) < 10) {
     $cache->options['results_lifespan'] = 0;
     $cache->options['output_lifespan'] = 0;
   }
@@ -956,7 +961,7 @@ function hook_views_preview_info_alter(array &$rows, ViewExecutable $view) {
  * @see views_invalidate_cache()
  */
 function hook_views_invalidate_cache() {
-  \Drupal\Core\Cache\Cache::invalidateTags(['views']);
+  Cache::invalidateTags(['views']);
 }
 
 /**
diff --git a/core/modules/views/views.module b/core/modules/views/views.module
index 74dc5726b57eaa6ee3b4f3ac3a42b7569d080d25..59a4f0b47430936198a77572f9f7406622d4fd91 100644
--- a/core/modules/views/views.module
+++ b/core/modules/views/views.module
@@ -2,228 +2,13 @@
 
 /**
  * @file
- * Primarily Drupal hooks and global API functions to manipulate views.
  */
 
-use Drupal\Component\Utility\Html;
 use Drupal\Core\Database\Query\AlterableInterface;
-use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
-use Drupal\views\Plugin\Derivative\ViewsLocalTask;
-use Drupal\views\ViewEntityInterface;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Entity\View;
 use Drupal\views\Views;
-use Drupal\views\ViewsConfigUpdater;
-
-/**
- * Implements hook_help().
- */
-function views_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.views':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Views module provides a back end to fetch information from content, user accounts, taxonomy terms, and other entities from the database and present it to the user as a grid, HTML list, table, unformatted list, etc. The resulting displays are known generally as <em>views</em>.') . '</p>';
-      $output .= '<p>' . t('For more information, see the <a href=":views">online documentation for the Views module</a>.', [':views' => 'https://www.drupal.org/documentation/modules/views']) . '</p>';
-      $output .= '<p>' . t('In order to create and modify your own views using the administration and configuration user interface, you will need to install either the Views UI module in core or a contributed module that provides a user interface for Views. See the <a href=":views-ui">Views UI module help page</a> for more information.', [':views-ui' => (\Drupal::moduleHandler()->moduleExists('views_ui')) ? Url::fromRoute('help.page', ['name' => 'views_ui'])->toString() : '#']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Adding functionality to administrative pages') . '</dt>';
-      $output .= '<dd>' . t('The Views module adds functionality to some core administration pages. For example, <em>admin/content</em> uses Views to filter and sort content. With Views uninstalled, <em>admin/content</em> is more limited.') . '</dd>';
-      $output .= '<dt>' . t('Expanding Views functionality') . '</dt>';
-      $output .= '<dd>' . t('Contributed projects that support the Views module can be found in the <a href=":node">online documentation for Views-related contributed modules</a>.', [':node' => 'https://www.drupal.org/documentation/modules/views/add-ons']) . '</dd>';
-      $output .= '<dt>' . t('Improving table accessibility') . '</dt>';
-      $output .= '<dd>' . t('Views tables include semantic markup to improve accessibility. Data cells are automatically associated with header cells through id and header attributes. To improve the accessibility of your tables you can add descriptive elements within the Views table settings. The <em>caption</em> element can introduce context for a table, making it easier to understand. The <em>summary</em> element can provide an overview of how the data has been organized and how to navigate the table. Both the caption and summary are visible by default and also implemented according to HTML5 guidelines.') . '</dd>';
-      $output .= '<dt>' . t('Working with multilingual views') . '</dt>';
-      $output .= '<dd>' . t('If your site has multiple languages and translated entities, each result row in a view will contain one translation of each involved entity (a view can involve multiple entities if it uses relationships). You can use a filter to restrict your view to one language: without filtering, if an entity has three translations it will add three rows to the results; if you filter by language, at most one result will appear (it could be zero if that particular entity does not have a translation matching your language filter choice). If a view uses relationships, each entity in the relationship needs to be filtered separately. You can filter a view to a fixed language choice, such as English or Spanish, or to the language selected by the page the view is displayed on (the language that is selected for the page by the language detection settings either for Content or User interface).') . '</dd>';
-      $output .= '<dd>' . t('Because each result row contains a specific translation of each entity, field-level filters are also relative to these entity translations. For example, if your view has a filter that specifies that the entity title should contain a particular English word, you will presumably filter out all rows containing Chinese translations, since they will not contain the English word. If your view also has a second filter specifying that the title should contain a particular Chinese word, and if you are using "And" logic for filtering, you will presumably end up with no results in the view, because there are probably not any entity translations containing both the English and Chinese words in the title.') . '</dd>';
-      $output .= '<dd>' . t('Independent of filtering, you can choose the display language (the language used to display the entities and their fields) via a setting on the display. Your language choices are the same as the filter language choices, with an additional choice of "Content language of view row" and "Original language of content in view row", which means to display each entity in the result row using the language that entity has or in which it was originally created. In theory, this would give you the flexibility to filter to French translations, for instance, and then display the results in Spanish. The more usual choices would be to use the same language choices for the display language and each entity filter in the view, or to use the Row language setting for the display.') . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_views_pre_render().
- */
-function views_views_pre_render($view) {
-  // If using AJAX, send identifying data about this view.
-  if ($view->ajaxEnabled() && empty($view->is_attachment) && empty($view->live_preview)) {
-    $view->element['#attached']['drupalSettings']['views'] = [
-      'ajax_path' => Url::fromRoute('views.ajax')->toString(),
-      'ajaxViews' => [
-        'views_dom_id:' . $view->dom_id => [
-          'view_name' => $view->storage->id(),
-          'view_display_id' => $view->current_display,
-          'view_args' => Html::escape(implode('/', $view->args)),
-          'view_path' => Html::escape(\Drupal::service('path.current')->getPath()),
-          'view_base_path' => $view->getPath(),
-          'view_dom_id' => $view->dom_id,
-          // To fit multiple views on a page, the programmer may have
-          // overridden the display's pager_element.
-          'pager_element' => isset($view->pager) ? $view->pager->getPagerId() : 0,
-        ],
-      ],
-    ];
-    $view->element['#attached']['library'][] = 'views/views.ajax';
-  }
-
-  return $view;
-}
-
-/**
- * Implements hook_theme().
- *
- * Register views theming functions and those that are defined via views plugin
- * definitions.
- */
-function views_theme($existing, $type, $theme, $path): array {
-  \Drupal::moduleHandler()->loadInclude('views', 'inc', 'views.theme');
-
-  // Some quasi clever array merging here.
-  $base = [
-    'file' => 'views.theme.inc',
-  ];
-
-  // Our extra version of pager
-  $hooks['views_mini_pager'] = $base + [
-    'variables' => [
-      'tags' => [],
-      'quantity' => 9,
-      'element' => 0,
-      'pagination_heading_level' => 'h4',
-      'parameters' => [],
-    ],
-  ];
-
-  $variables = [
-    // For displays, we pass in a dummy array as the first parameter, since
-    // $view is an object but the core contextual_preprocess() function only
-    // attaches contextual links when the primary theme argument is an array.
-    'display' => [
-      'view_array' => [],
-      'view' => NULL,
-      'rows' => [],
-      'header' => [],
-      'footer' => [],
-      'empty' => [],
-      'exposed' => [],
-      'more' => [],
-      'feed_icons' => [],
-      'pager' => [],
-      'title' => '',
-      'attachment_before' => [],
-      'attachment_after' => [],
-    ],
-    'style' => ['view' => NULL, 'options' => NULL, 'rows' => NULL, 'title' => NULL],
-    'row' => ['view' => NULL, 'options' => NULL, 'row' => NULL, 'field_alias' => NULL],
-    'exposed_form' => ['view' => NULL, 'options' => NULL],
-    'pager' => [
-      'view' => NULL,
-      'options' => NULL,
-      'tags' => [],
-      'quantity' => 9,
-      'element' => 0,
-      'pagination_heading_level' => 'h4',
-      'parameters' => [],
-    ],
-  ];
-
-  // Default view themes
-  $hooks['views_view_field'] = $base + [
-    'variables' => ['view' => NULL, 'field' => NULL, 'row' => NULL],
-  ];
-  $hooks['views_view_grouping'] = $base + [
-    'variables' => ['view' => NULL, 'grouping' => NULL, 'grouping_level' => NULL, 'rows' => NULL, 'title' => NULL],
-  ];
-
-  // Only display, pager, row, and style plugins can provide theme hooks.
-  $plugin_types = [
-    'display',
-    'pager',
-    'row',
-    'style',
-    'exposed_form',
-  ];
-  $plugins = [];
-  foreach ($plugin_types as $plugin_type) {
-    $plugins[$plugin_type] = Views::pluginManager($plugin_type)->getDefinitions();
-  }
-
-  $module_handler = \Drupal::moduleHandler();
-
-  // Register theme functions for all style plugins. It provides a basic auto
-  // implementation of theme functions or template files by using the plugin
-  // definitions (theme, theme_file, module, register_theme). Template files are
-  // assumed to be located in the templates folder.
-  foreach ($plugins as $type => $info) {
-    foreach ($info as $def) {
-      // Not all plugins have theme functions, and they can also explicitly
-      // prevent a theme function from being registered automatically.
-      if (!isset($def['theme']) || empty($def['register_theme'])) {
-        continue;
-      }
-      // For each theme registration, we have a base directory to check for the
-      // templates folder. This will be relative to the root of the given module
-      // folder, so we always need a module definition.
-      // @todo Watchdog or exception?
-      if (!isset($def['provider']) || !$module_handler->moduleExists($def['provider'])) {
-        continue;
-      }
-
-      $hooks[$def['theme']] = [
-        'variables' => $variables[$type],
-      ];
-
-      // We always use the module directory as base dir.
-      $module_dir = \Drupal::service('extension.list.module')->getPath($def['provider']);
-      $hooks[$def['theme']]['path'] = $module_dir;
-
-      // For the views module we ensure views.theme.inc is included.
-      if ($def['provider'] == 'views') {
-        if (!isset($hooks[$def['theme']]['includes'])) {
-          $hooks[$def['theme']]['includes'] = [];
-        }
-        if (!in_array('views.theme.inc', $hooks[$def['theme']]['includes'])) {
-          $hooks[$def['theme']]['includes'][] = $module_dir . '/views.theme.inc';
-        }
-      }
-      // The theme_file definition is always relative to the modules directory.
-      elseif (!empty($def['theme_file'])) {
-        $hooks[$def['theme']]['file'] = $def['theme_file'];
-      }
-
-      // Whenever we have a theme file, we include it directly so we can
-      // auto-detect the theme function.
-      if (isset($def['theme_file'])) {
-        $include = \Drupal::root() . '/' . $module_dir . '/' . $def['theme_file'];
-        if (is_file($include)) {
-          require_once $include;
-        }
-      }
-
-      // By default any templates for a module are located in the /templates
-      // directory of the module's folder. If a module wants to define its own
-      // location it has to set register_theme of the plugin to FALSE and
-      // implement hook_theme() by itself.
-      $hooks[$def['theme']]['path'] .= '/templates';
-      $hooks[$def['theme']]['template'] = Html::cleanCssIdentifier($def['theme']);
-    }
-  }
-
-  $hooks['views_form_views_form'] = $base + [
-    'render element' => 'form',
-  ];
-
-  $hooks['views_exposed_form'] = $base + [
-    'render element' => 'form',
-  ];
-
-  return $hooks;
-}
 
 /**
  * Allows view-based node templates if called from a view.
@@ -252,19 +37,6 @@ function views_preprocess_node(&$variables) {
   }
 }
 
-/**
- * Implements hook_theme_suggestions_HOOK_alter().
- */
-function views_theme_suggestions_node_alter(array &$suggestions, array $variables) {
-  $node = $variables['elements']['#node'];
-  if (!empty($node->view) && $node->view->storage->id()) {
-    $suggestions[] = 'node__view__' . $node->view->storage->id();
-    if (!empty($node->view->current_display)) {
-      $suggestions[] = 'node__view__' . $node->view->storage->id() . '__' . $node->view->current_display;
-    }
-  }
-}
-
 /**
  * Allows view-based comment templates if called from a view.
  */
@@ -276,32 +48,6 @@ function views_preprocess_comment(&$variables) {
   }
 }
 
-/**
- * Implements hook_theme_suggestions_HOOK_alter().
- */
-function views_theme_suggestions_comment_alter(array &$suggestions, array $variables) {
-  $comment = $variables['elements']['#comment'];
-  if (!empty($comment->view) && $comment->view->storage->id()) {
-    $suggestions[] = 'comment__view__' . $comment->view->storage->id();
-    if (!empty($comment->view->current_display)) {
-      $suggestions[] = 'comment__view__' . $comment->view->storage->id() . '__' . $comment->view->current_display;
-    }
-  }
-}
-
-/**
- * Implements hook_theme_suggestions_HOOK_alter().
- */
-function views_theme_suggestions_container_alter(array &$suggestions, array $variables) {
-  if (!empty($variables['element']['#type']) && $variables['element']['#type'] == 'more_link' && !empty($variables['element']['#view']) && $variables['element']['#view'] instanceof ViewExecutable) {
-    $suggestions = array_merge(
-      $suggestions,
-      // Theme suggestions use the reverse order compared to #theme hooks.
-      array_reverse($variables['element']['#view']->buildThemeFunctions('container__more_link'))
-    );
-  }
-}
-
 /**
  * Adds contextual links associated with a view display to a renderable array.
  *
@@ -444,48 +190,6 @@ function views_add_contextual_links(&$render_element, $location, $display_id, ?a
   }
 }
 
-/**
- * Implements hook_ENTITY_TYPE_insert() for 'field_config'.
- */
-function views_field_config_insert(EntityInterface $field) {
-  Views::viewsData()->clear();
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for 'field_config'.
- */
-function views_field_config_update(EntityInterface $entity) {
-  Views::viewsData()->clear();
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete() for 'field_config'.
- */
-function views_field_config_delete(EntityInterface $entity) {
-  Views::viewsData()->clear();
-}
-
-/**
- * Implements hook_ENTITY_TYPE_insert().
- */
-function views_base_field_override_insert(EntityInterface $entity) {
-  Views::viewsData()->clear();
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update().
- */
-function views_base_field_override_update(EntityInterface $entity) {
-  Views::viewsData()->clear();
-}
-
-/**
- * Implements hook_ENTITY_TYPE_delete().
- */
-function views_base_field_override_delete(EntityInterface $entity) {
-  Views::viewsData()->clear();
-}
-
 /**
  * Invalidate the views cache, forcing a rebuild on the next grab of table data.
  */
@@ -626,51 +330,6 @@ function views_disable_view(View $view) {
   $view->disable()->save();
 }
 
-/**
- * Implements hook_form_FORM_ID_alter() for the exposed form.
- *
- * Since the exposed form is a GET form, we don't want it to send a wide
- * variety of information.
- */
-function views_form_views_exposed_form_alter(&$form, FormStateInterface $form_state): void {
-  $form['form_build_id']['#access'] = FALSE;
-  $form['form_token']['#access'] = FALSE;
-  $form['form_id']['#access'] = FALSE;
-}
-
-/**
- * Implements hook_query_TAG_alter().
- *
- * This is the hook_query_alter() for queries tagged by Views and is used to
- * add in substitutions from hook_views_query_substitutions().
- */
-function views_query_views_alter(AlterableInterface $query) {
-  $substitutions = $query->getMetaData('views_substitutions');
-  $tables = &$query->getTables();
-  $where = &$query->conditions();
-
-  // Replaces substitutions in tables.
-  foreach ($tables as $table_name => $table_metadata) {
-    foreach ($table_metadata['arguments'] as $replacement_key => $value) {
-      if (!is_array($value)) {
-        if (isset($substitutions[$value])) {
-          $tables[$table_name]['arguments'][$replacement_key] = $substitutions[$value];
-        }
-      }
-      else {
-        foreach ($value as $sub_key => $sub_value) {
-          if (isset($substitutions[$sub_value])) {
-            $tables[$table_name]['arguments'][$replacement_key][$sub_key] = $substitutions[$sub_value];
-          }
-        }
-      }
-    }
-  }
-
-  // Replaces substitutions in filter criteria.
-  _views_query_tag_alter_condition($query, $where, $substitutions);
-}
-
 /**
  * Replaces the substitutions recursive foreach condition.
  */
@@ -689,7 +348,7 @@ function _views_query_tag_alter_condition(AlterableInterface $query, &$condition
       if (is_object($condition['value'])) {
         $subquery = $condition['value'];
         $subquery->addMetaData('views_substitutions', $query->getMetaData('views_substitutions'));
-        views_query_views_alter($condition['value']);
+        \Drupal::moduleHandler()->invoke('views', 'query_views_alter', [$condition['value']]);
       }
       elseif (isset($condition['value'])) {
         // We can not use a simple str_replace() here because it always returns
@@ -798,21 +457,3 @@ function views_element_validate_tags($element, FormStateInterface $form_state) {
     }
   }
 }
-
-/**
- * Implements hook_local_tasks_alter().
- */
-function views_local_tasks_alter(&$local_tasks) {
-  $container = \Drupal::getContainer();
-  $local_task = ViewsLocalTask::create($container, 'views_view');
-  $local_task->alterLocalTasks($local_tasks);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave().
- */
-function views_view_presave(ViewEntityInterface $view) {
-  /** @var \Drupal\views\ViewsConfigUpdater $config_updater */
-  $config_updater = \Drupal::classResolver(ViewsConfigUpdater::class);
-  $config_updater->updateAll($view);
-}
diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc
index faa7da0d300c08c6bdc02016f98144fb61ea4b72..859ede62114dda024225e51943c8619ebb299b0c 100644
--- a/core/modules/views/views.theme.inc
+++ b/core/modules/views/views.theme.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Preprocessors and helper functions to make theming easier.
  */
 
 use Drupal\Component\Utility\Html;
diff --git a/core/modules/views/views.tokens.inc b/core/modules/views/views.tokens.inc
deleted file mode 100644
index e980580248d8416d449bb244ce8fc58b8f0a3154..0000000000000000000000000000000000000000
--- a/core/modules/views/views.tokens.inc
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-
-/**
- * @file
- * Token integration for the views module.
- */
-
-use Drupal\Core\Render\BubbleableMetadata;
-
-/**
- * Implements hook_token_info().
- */
-function views_token_info() {
-  $info['types']['view'] = [
-    'name' => t('View', [], ['context' => 'View entity type']),
-    'description' => t('Tokens related to views.'),
-    'needs-data' => 'view',
-  ];
-  $info['tokens']['view']['label'] = [
-    'name' => t('Label'),
-    'description' => t('The label of the view.'),
-  ];
-  $info['tokens']['view']['description'] = [
-    'name' => t('Description'),
-    'description' => t('The description of the view.'),
-  ];
-  $info['tokens']['view']['id'] = [
-    'name' => t('ID'),
-    'description' => t('The machine-readable ID of the view.'),
-  ];
-  $info['tokens']['view']['title'] = [
-    'name' => t('Title'),
-    'description' => t('The title of current display of the view.'),
-  ];
-  $info['tokens']['view']['url'] = [
-    'name' => t('URL'),
-    'description' => t('The URL of the view.'),
-    'type' => 'url',
-  ];
-  $info['tokens']['view']['base-table'] = [
-    'name' => t('Base table'),
-    'description' => t('The base table used for this view.'),
-  ];
-  $info['tokens']['view']['base-field'] = [
-    'name' => t('Base field'),
-    'description' => t('The base field used for this view.'),
-  ];
-  $info['tokens']['view']['total-rows'] = [
-    'name' => t('Total rows'),
-    'description' => t('The total amount of results returned from the view. The current display will be used.'),
-  ];
-  $info['tokens']['view']['items-per-page'] = [
-    'name' => t('Items per page'),
-    'description' => t('The number of items per page.'),
-  ];
-  $info['tokens']['view']['current-page'] = [
-    'name' => t('Current page'),
-    'description' => t('The current page of results the view is on.'),
-  ];
-  $info['tokens']['view']['page-count'] = [
-    'name' => t('Page count'),
-    'description' => t('The total page count.'),
-  ];
-
-  return $info;
-}
-
-/**
- * Implements hook_tokens().
- */
-function views_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
-  $url_options = ['absolute' => TRUE];
-  if (isset($options['language'])) {
-    $url_options['language'] = $options['language'];
-  }
-  $replacements = [];
-
-  if ($type == 'view' && !empty($data['view'])) {
-    /** @var \Drupal\views\ViewExecutable $view */
-    $view = $data['view'];
-
-    $bubbleable_metadata->addCacheableDependency($view->storage);
-
-    foreach ($tokens as $name => $original) {
-      switch ($name) {
-        case 'label':
-          $replacements[$original] = $view->storage->label();
-          break;
-
-        case 'description':
-          $replacements[$original] = $view->storage->get('description');
-          break;
-
-        case 'id':
-          $replacements[$original] = $view->storage->id();
-          break;
-
-        case 'title':
-          $title = $view->getTitle();
-          $replacements[$original] = $title;
-          break;
-
-        case 'url':
-          try {
-            if ($url = $view->getUrl()) {
-              $replacements[$original] = $url->setOptions($url_options)
-                ->toString();
-            }
-          }
-          catch (\InvalidArgumentException) {
-            // The view has no URL so we leave the value empty.
-            $replacements[$original] = '';
-          }
-          break;
-
-        case 'base-table':
-          $replacements[$original] = $view->storage->get('base_table');
-          break;
-
-        case 'base-field':
-          $replacements[$original] = $view->storage->get('base_field');
-          break;
-
-        case 'total-rows':
-          $replacements[$original] = (int) $view->total_rows;
-          break;
-
-        case 'items-per-page':
-          $replacements[$original] = (int) $view->getItemsPerPage();
-          break;
-
-        case 'current-page':
-          $replacements[$original] = (int) $view->getCurrentPage() + 1;
-          break;
-
-        case 'page-count':
-          // If there are no items per page, set this to 1 for the division.
-          $per_page = $view->getItemsPerPage() ?: 1;
-          $replacements[$original] = max(1, (int) ceil($view->total_rows / $per_page));
-          break;
-      }
-    }
-  }
-
-  return $replacements;
-}
diff --git a/core/modules/views/views.views.inc b/core/modules/views/views.views.inc
index 50bccc6e22ef52dd46b98c7905a52d31e0d97218..3a08f62c900949377212658a26a4775357601f16 100644
--- a/core/modules/views/views.views.inc
+++ b/core/modules/views/views.views.inc
@@ -2,234 +2,14 @@
 
 /**
  * @file
- * Provide views data that isn't tied to any other module.
  */
 
-use Drupal\Component\Utility\NestedArray;
-use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
 use Drupal\Core\Render\Markup;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\FieldConfigInterface;
 use Drupal\field\FieldStorageConfigInterface;
-use Drupal\system\ActionConfigEntityInterface;
-
-/**
- * Implements hook_views_data().
- */
-function views_views_data() {
-  $data['views']['table']['group'] = t('Global');
-  $data['views']['table']['join'] = [
-  // #global is a special flag which allows a table to appear all the time.
-    '#global' => [],
-  ];
-
-  $data['views']['random'] = [
-    'title' => t('Random'),
-    'help' => t('Randomize the display order.'),
-    'sort' => [
-      'id' => 'random',
-    ],
-  ];
-
-  $data['views']['null'] = [
-    'title' => t('Null'),
-    'help' => t('Allow a contextual filter value to be ignored. The query will not be altered by this contextual filter value. Can be used when contextual filter values come from the URL, and a part of the URL needs to be ignored.'),
-    'argument' => [
-      'id' => 'null',
-    ],
-  ];
-
-  $data['views']['nothing'] = [
-    'title' => t('Custom text'),
-    'help' => t('Provide custom text or link.'),
-    'field' => [
-      'id' => 'custom',
-      'click sortable' => FALSE,
-    ],
-  ];
-
-  $data['views']['counter'] = [
-    'title' => t('View result counter'),
-    'help' => t('Displays the actual position of the view result'),
-    'field' => [
-      'id' => 'counter',
-    ],
-  ];
-
-  $data['views']['area'] = [
-    'title' => t('Text area'),
-    'help' => t('Provide markup for the area using any available text format.'),
-    'area' => [
-      'id' => 'text',
-    ],
-  ];
-
-  $data['views']['area_text_custom'] = [
-    'title' => t('Unfiltered text'),
-    'help' => t('Provide markup for the area with minimal filtering.'),
-    'area' => [
-      'id' => 'text_custom',
-    ],
-  ];
-
-  $data['views']['title'] = [
-    'title' => t('Title override'),
-    'help' => t('Override the default view title for this view. This is useful to display an alternative title when a view is empty.'),
-    'area' => [
-      'id' => 'title',
-      'sub_type' => 'empty',
-    ],
-  ];
-
-  $data['views']['view'] = [
-    'title' => t('View area'),
-    'help' => t('Insert a view inside an area.'),
-    'area' => [
-      'id' => 'view',
-    ],
-  ];
-
-  $data['views']['result'] = [
-    'title' => t('Result summary'),
-    'help' => t('Shows result summary, for example the items per page.'),
-    'area' => [
-      'id' => 'result',
-    ],
-  ];
-
-  $data['views']['messages'] = [
-    'title' => t('Messages'),
-    'help' => t('Displays messages in an area.'),
-    'area' => [
-      'id' => 'messages',
-    ],
-  ];
-
-  $data['views']['http_status_code'] = [
-    'title' => t('Response status code'),
-    'help' => t('Alter the HTTP response status code used by this view, mostly helpful for empty results.'),
-    'area' => [
-      'id' => 'http_status_code',
-    ],
-  ];
-
-  $data['views']['combine'] = [
-    'title' => t('Combine fields filter'),
-    'help' => t('Combine multiple fields together and search by them.'),
-    'filter' => [
-      'id' => 'combine',
-    ],
-  ];
-
-  $data['views']['dropbutton'] = [
-    'title' => t('Dropbutton'),
-    'help' => t('Display fields in a dropbutton.'),
-    'field' => [
-      'id' => 'dropbutton',
-    ],
-  ];
-
-  $data['views']['display_link'] = [
-    'title' => t('Link to display'),
-    'help' => t('Displays a link to a path-based display of this view while keeping the filter criteria, sort criteria, pager settings and contextual filters.'),
-    'area' => [
-      'id' => 'display_link',
-    ],
-  ];
-
-  // Registers an entity area handler per entity type.
-  foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
-    // Excludes entity types, which cannot be rendered.
-    if ($entity_type->hasViewBuilderClass()) {
-      $label = $entity_type->getLabel();
-      $data['views']['entity_' . $entity_type_id] = [
-        'title' => t('Rendered entity - @label', ['@label' => $label]),
-        'help' => t('Displays a rendered @label entity in an area.', ['@label' => $label]),
-        'area' => [
-          'entity_type' => $entity_type_id,
-          'id' => 'entity',
-        ],
-      ];
-    }
-  }
-
-  // Registers an action bulk form per entity.
-  foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type => $entity_info) {
-    $actions = array_filter(\Drupal::entityTypeManager()->getStorage('action')->loadMultiple(), function (ActionConfigEntityInterface $action) use ($entity_type) {
-      return $action->getType() == $entity_type;
-    });
-    if (empty($actions)) {
-      continue;
-    }
-    $data[$entity_info->getBaseTable()][$entity_type . '_bulk_form'] = [
-      'title' => t('Bulk update'),
-      'help' => t('Allows users to apply an action to one or more items.'),
-      'field' => [
-        'id' => 'bulk_form',
-      ],
-    ];
-  }
-
-  // Registers views data for the entity itself.
-  foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
-    if ($entity_type->hasHandlerClass('views_data')) {
-      /** @var \Drupal\views\EntityViewsDataInterface $views_data */
-      $views_data = \Drupal::entityTypeManager()->getHandler($entity_type_id, 'views_data');
-      $data = NestedArray::mergeDeep($data, $views_data->getViewsData());
-    }
-  }
-
-  // Field modules can implement hook_field_views_data() to override the default
-  // behavior for adding fields.
-  $module_handler = \Drupal::moduleHandler();
-
-  $entity_type_manager = \Drupal::entityTypeManager();
-  if ($entity_type_manager->hasDefinition('field_storage_config')) {
-    /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
-    foreach ($entity_type_manager->getStorage('field_storage_config')->loadMultiple() as $field_storage) {
-      if (_views_field_get_entity_type_storage($field_storage)) {
-        $provider = $field_storage->getTypeProvider();
-        $result = (array) $module_handler->invoke($provider === 'core' ? 'views' : $provider, 'field_views_data', [$field_storage]);
-        if (empty($result)) {
-          $result = views_field_default_views_data($field_storage);
-        }
-        $module_handler->alter('field_views_data', $result, $field_storage);
-
-        if (is_array($result)) {
-          $data = NestedArray::mergeDeep($result, $data);
-        }
-      }
-    }
-  }
-
-  return $data;
-}
-
-/**
- * Implements hook_views_data_alter().
- *
- * Field modules can implement hook_field_views_data_views_data_alter() to
- * alter the views data on a per field basis. This is weirdly named so as
- * not to conflict with the \Drupal::moduleHandler()->alter('field_views_data')
- * in views_views_data().
- */
-function views_views_data_alter(&$data) {
-  $entity_type_manager = \Drupal::entityTypeManager();
-  if (!$entity_type_manager->hasDefinition('field_storage_config')) {
-    return;
-  }
-  /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
-  foreach ($entity_type_manager->getStorage('field_storage_config')->loadMultiple() as $field_storage) {
-    if (_views_field_get_entity_type_storage($field_storage)) {
-      $function = $field_storage->getTypeProvider() . '_field_views_data_views_data_alter';
-      if (function_exists($function)) {
-        $function($data, $field_storage);
-      }
-    }
-  }
-}
 
 /**
  * Determines whether the entity type the field appears in is SQL based.
@@ -764,88 +544,3 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora
 
   return $data;
 }
-
-/**
- * Implements hook_field_views_data().
- *
- * The function implements the hook on behalf of 'core' because it adds a
- * relationship and a reverse relationship to entity_reference field type, which
- * is provided by core. This function also provides an argument plugin for
- * entity_reference fields that handles title token replacement.
- */
-function views_field_views_data(FieldStorageConfigInterface $field_storage) {
-  $data = views_field_default_views_data($field_storage);
-
-  // The code below only deals with the Entity reference field type.
-  if ($field_storage->getType() != 'entity_reference') {
-    return $data;
-  }
-
-  $entity_type_manager = \Drupal::entityTypeManager();
-  $entity_type_id = $field_storage->getTargetEntityTypeId();
-  /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
-  $table_mapping = $entity_type_manager->getStorage($entity_type_id)->getTableMapping();
-
-  foreach ($data as $table_name => $table_data) {
-    // Add a relationship to the target entity type.
-    $target_entity_type_id = $field_storage->getSetting('target_type');
-    $target_entity_type = $entity_type_manager->getDefinition($target_entity_type_id);
-    $entity_type_id = $field_storage->getTargetEntityTypeId();
-    $entity_type = $entity_type_manager->getDefinition($entity_type_id);
-    $target_base_table = $target_entity_type->getDataTable() ?: $target_entity_type->getBaseTable();
-    $field_name = $field_storage->getName();
-
-    if ($target_entity_type instanceof ContentEntityTypeInterface) {
-      // Provide a relationship for the entity type with the entity reference
-      // field.
-      $args = [
-        '@label' => $target_entity_type->getLabel(),
-        '@field_name' => $field_name,
-      ];
-      $data[$table_name][$field_name]['relationship'] = [
-        'title' => t('@label referenced from @field_name', $args),
-        'label' => t('@field_name: @label', $args),
-        'group' => $entity_type->getLabel(),
-        'help' => t('Appears in: @bundles.', ['@bundles' => implode(', ', $field_storage->getBundles())]),
-        'id' => 'standard',
-        'base' => $target_base_table,
-        'entity type' => $target_entity_type_id,
-        'base field' => $target_entity_type->getKey('id'),
-        'relationship field' => $field_name . '_target_id',
-      ];
-
-      // Provide a reverse relationship for the entity type that is referenced by
-      // the field.
-      $args['@entity'] = $entity_type->getLabel();
-      $args['@label'] = $target_entity_type->getSingularLabel();
-      $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name;
-      $data[$target_base_table][$pseudo_field_name]['relationship'] = [
-        'title' => t('@entity using @field_name', $args),
-        'label' => t('@field_name', ['@field_name' => $field_name]),
-        'group' => $target_entity_type->getLabel(),
-        'help' => t('Relate each @entity with a @field_name set to the @label.', $args),
-        'id' => 'entity_reverse',
-        'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
-        'entity_type' => $entity_type_id,
-        'base field' => $entity_type->getKey('id'),
-        'field_name' => $field_name,
-        'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
-        'field field' => $field_name . '_target_id',
-        'join_extra' => [
-          [
-            'field' => 'deleted',
-            'value' => 0,
-            'numeric' => TRUE,
-          ],
-        ],
-      ];
-    }
-
-    // Provide an argument plugin that has a meaningful titleQuery()
-    // implementation getting the entity label.
-    $data[$table_name][$field_name . '_target_id']['argument']['id'] = 'entity_target_id';
-    $data[$table_name][$field_name . '_target_id']['argument']['target_entity_type_id'] = $target_entity_type_id;
-  }
-
-  return $data;
-}
diff --git a/core/modules/views/views.views_execution.inc b/core/modules/views/views.views_execution.inc
deleted file mode 100644
index 3647f52abccc5dcb3f262edd497fead992f51526..0000000000000000000000000000000000000000
--- a/core/modules/views/views.views_execution.inc
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides views runtime hooks for views.module.
- */
-
-use Drupal\views\ViewExecutable;
-use Drupal\views\Plugin\views\PluginBase;
-
-/**
- * Implements hook_views_query_substitutions().
- *
- * Makes the following substitutions:
- * - Current time.
- * - Drupal version.
- * - Special language codes; see
- *   \Drupal\views\Plugin\views\PluginBase::listLanguages().
- */
-function views_views_query_substitutions(ViewExecutable $view) {
-  $substitutions = [
-    '***CURRENT_VERSION***' => \Drupal::VERSION,
-    '***CURRENT_TIME***' => \Drupal::time()->getRequestTime(),
-  ] + PluginBase::queryLanguageSubstitutions();
-
-  return $substitutions;
-}
-
-/**
- * Implements hook_views_form_substitutions().
- */
-function views_views_form_substitutions() {
-  $select_all = [
-    '#type' => 'checkbox',
-    '#default_value' => FALSE,
-    '#attributes' => ['class' => ['action-table-select-all']],
-  ];
-  return [
-    '<!--action-bulk-form-select-all-->' => \Drupal::service('renderer')->render($select_all),
-  ];
-}
diff --git a/core/modules/views_ui/admin.inc b/core/modules/views_ui/admin.inc
index 2bff41b07c78eb63092866a0e9f1d3551f1efbc0..d9ba648c72e92a3154345d610773126cf69e5dc7 100644
--- a/core/modules/views_ui/admin.inc
+++ b/core/modules/views_ui/admin.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Provides the Views' administrative interface.
  */
 
 use Drupal\Component\Utility\NestedArray;
diff --git a/core/modules/views_ui/src/Hook/ViewsUiHooks.php b/core/modules/views_ui/src/Hook/ViewsUiHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..94d0dc78a702a986ba2757d487b01069273a72f0
--- /dev/null
+++ b/core/modules/views_ui/src/Hook/ViewsUiHooks.php
@@ -0,0 +1,246 @@
+<?php
+
+namespace Drupal\views_ui\Hook;
+
+use Drupal\views\Entity\View;
+use Drupal\block\BlockInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\views\Analyzer;
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_ui.
+ */
+class ViewsUiHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.views_ui':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Views UI module provides an interface for managing views for the <a href=":views">Views module</a>. For more information, see the <a href=":handbook">online documentation for the Views UI module</a>.', [
+          ':views' => Url::fromRoute('help.page', [
+            'name' => 'views',
+          ])->toString(),
+          ':handbook' => 'https://www.drupal.org/documentation/modules/views_ui',
+        ]) . '</p>';
+        $output .= '<h2>' . t('Uses') . '</h2>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Creating and managing views') . '</dt>';
+        $output .= '<dd>' . t('Views can be created from the <a href=":list">Views list page</a> by using the "Add view" action. Existing views can be managed from the <a href=":list">Views list page</a> by locating the view in the "Enabled" or "Disabled" list and selecting the desired operation action, for example "Edit".', [
+          ':list' => Url::fromRoute('entity.view.collection', [
+            'name' => 'views_ui',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Enabling and disabling views') . '<dt>';
+        $output .= '<dd>' . t('Views can be enabled or disabled from the <a href=":list">Views list page</a>. To enable a view, find the view within the "Disabled" list and select the "Enable" operation. To disable a view find the view within the "Enabled" list and select the "Disable" operation.', [
+          ':list' => Url::fromRoute('entity.view.collection', [
+            'name' => 'views_ui',
+          ])->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Exporting and importing views') . '</dt>';
+        $output .= '<dd>' . t('Views can be exported and imported as configuration files by using the <a href=":config">Configuration Manager module</a>.', [
+          ':config' => \Drupal::moduleHandler()->moduleExists('config') ? Url::fromRoute('help.page', [
+            'name' => 'config',
+          ])->toString() : '#',
+        ]) . '</dd>';
+        $output .= '</dl>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public function entityTypeBuild(array &$entity_types) {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    $entity_types['view']->setFormClass('edit', 'Drupal\views_ui\ViewEditForm')->setFormClass('add', 'Drupal\views_ui\ViewAddForm')->setFormClass('preview', 'Drupal\views_ui\ViewPreviewForm')->setFormClass('duplicate', 'Drupal\views_ui\ViewDuplicateForm')->setFormClass('delete', 'Drupal\Core\Entity\EntityDeleteForm')->setFormClass('break_lock', 'Drupal\views_ui\Form\BreakLockForm')->setListBuilderClass('Drupal\views_ui\ViewListBuilder')->setLinkTemplate('edit-form', '/admin/structure/views/view/{view}')->setLinkTemplate('edit-display-form', '/admin/structure/views/view/{view}/edit/{display_id}')->setLinkTemplate('preview-form', '/admin/structure/views/view/{view}/preview/{display_id}')->setLinkTemplate('duplicate-form', '/admin/structure/views/view/{view}/duplicate')->setLinkTemplate('delete-form', '/admin/structure/views/view/{view}/delete')->setLinkTemplate('enable', '/admin/structure/views/view/{view}/enable')->setLinkTemplate('disable', '/admin/structure/views/view/{view}/disable')->setLinkTemplate('break-lock-form', '/admin/structure/views/view/{view}/break-lock')->setLinkTemplate('collection', '/admin/structure/views');
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function theme() : array {
+    return [
+          // Edit a view
+      'views_ui_display_tab_setting' => [
+        'variables' => [
+          'description' => '',
+          'link' => '',
+          'settings_links' => [],
+          'overridden' => FALSE,
+          'defaulted' => FALSE,
+          'description_separator' => TRUE,
+          'class' => [],
+        ],
+        'file' => 'views_ui.theme.inc',
+      ],
+      'views_ui_display_tab_bucket' => [
+        'render element' => 'element',
+        'file' => 'views_ui.theme.inc',
+      ],
+      'views_ui_rearrange_filter_form' => [
+        'render element' => 'form',
+        'file' => 'views_ui.theme.inc',
+      ],
+      'views_ui_expose_filter_form' => [
+        'render element' => 'form',
+        'file' => 'views_ui.theme.inc',
+      ],
+          // Legacy theme hook for displaying views info.
+      'views_ui_view_info' => [
+        'variables' => [
+          'view' => NULL,
+          'displays' => NULL,
+        ],
+        'file' => 'views_ui.theme.inc',
+      ],
+          // List views.
+      'views_ui_views_listing_table' => [
+        'variables' => [
+          'headers' => NULL,
+          'rows' => NULL,
+          'attributes' => [],
+        ],
+        'file' => 'views_ui.theme.inc',
+      ],
+      'views_ui_view_displays_list' => [
+        'variables' => [
+          'displays' => [],
+        ],
+      ],
+          // Group of filters.
+      'views_ui_build_group_filter_form' => [
+        'render element' => 'form',
+        'file' => 'views_ui.theme.inc',
+      ],
+          // On behalf of a plugin
+      'views_ui_style_plugin_table' => [
+        'render element' => 'form',
+        'file' => 'views_ui.theme.inc',
+      ],
+          // When previewing a view.
+      'views_ui_view_preview_section' => [
+        'variables' => [
+          'view' => NULL,
+          'section' => NULL,
+          'content' => NULL,
+          'links' => '',
+        ],
+        'file' => 'views_ui.theme.inc',
+      ],
+          // Generic container wrapper, to use instead of theme_container when an id
+          // is not desired.
+      'views_ui_container' => [
+        'variables' => [
+          'children' => NULL,
+          'attributes' => [],
+        ],
+        'file' => 'views_ui.theme.inc',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_views_plugins_display_alter().
+   */
+  #[Hook('views_plugins_display_alter')]
+  public function viewsPluginsDisplayAlter(&$plugins) {
+    // Attach contextual links to each display plugin. The links will point to
+    // paths underneath "admin/structure/views/view/{$view->id()}" (i.e., paths
+    // for editing and performing other contextual actions on the view).
+    foreach ($plugins as &$display) {
+      $display['contextual links']['entity.view.edit_form'] = [
+        'route_name' => 'entity.view.edit_form',
+        'route_parameters_names' => [
+          'view' => 'id',
+        ],
+      ];
+    }
+  }
+
+  /**
+   * Implements hook_contextual_links_view_alter().
+   */
+  #[Hook('contextual_links_view_alter')]
+  public function contextualLinksViewAlter(&$element, $items) {
+    // Remove contextual links from being rendered, when so desired, such as
+    // within a View preview.
+    if (views_ui_contextual_links_suppress()) {
+      $element['#links'] = [];
+    }
+    elseif (!empty($element['#links']['entityviewedit-form'])) {
+      $display_id = $items['entity.view.edit_form']['metadata']['display_id'];
+      $route_parameters = $element['#links']['entityviewedit-form']['url']->getRouteParameters() + ['display_id' => $display_id];
+      $element['#links']['entityviewedit-form']['url'] = Url::fromRoute('entity.view.edit_display_form', $route_parameters);
+    }
+  }
+
+  /**
+   * Implements hook_views_analyze().
+   *
+   * This is the basic views analysis that checks for very minimal problems.
+   * There are other analysis tools in core specific sections, such as
+   * node.views.inc as well.
+   */
+  #[Hook('views_analyze')]
+  public function viewsAnalyze(ViewExecutable $view) {
+    $ret = [];
+    // Check for something other than the default display:
+    if (count($view->displayHandlers) < 2) {
+      $ret[] = Analyzer::formatMessage(t('This view has only a default display and therefore will not be placed anywhere on your site; perhaps you want to add a page or a block display.'), 'warning');
+    }
+    // If a display has a path, check that it does not match an existing path
+    // alias. This results in the path alias not working.
+    foreach ($view->displayHandlers as $display) {
+      if (empty($display)) {
+        continue;
+      }
+      if ($display->hasPath() && ($path = $display->getOption('path'))) {
+        $normal_path = \Drupal::service('path_alias.manager')->getPathByAlias($path);
+        if ($path != $normal_path) {
+          $ret[] = Analyzer::formatMessage(t('You have configured display %display with a path which is an path alias as well. This might lead to unwanted effects so better use an internal path.', ['%display' => $display->display['display_title']]), 'warning');
+        }
+      }
+    }
+    return $ret;
+  }
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $entity) : array {
+    $operations = [];
+    if ($entity instanceof BlockInterface) {
+      $plugin = $entity->getPlugin();
+      if ($plugin->getBaseId() === 'views_block') {
+        $view_id_parts = explode('-', $plugin->getDerivativeId());
+        $view_id = $view_id_parts[0] ?? '';
+        $display_id = $view_id_parts[1] ?? '';
+        $view = View::load($view_id);
+        if ($view && $view->access('edit')) {
+          $operations['view-edit'] = [
+            'title' => t('Edit view'),
+            'url' => Url::fromRoute('entity.view.edit_display_form', [
+              'view' => $view_id,
+              'display_id' => $display_id,
+            ]),
+            'weight' => 50,
+          ];
+        }
+      }
+    }
+    return $operations;
+  }
+
+}
diff --git a/core/modules/views_ui/src/ProxyClass/ParamConverter/ViewUIConverter.php b/core/modules/views_ui/src/ProxyClass/ParamConverter/ViewUIConverter.php
index 9ef21bfa965c0ec746e108631afb2dd3f1330690..5281a69c61873cfb24f39c11e4c79fb4215ae757 100644
--- a/core/modules/views_ui/src/ProxyClass/ParamConverter/ViewUIConverter.php
+++ b/core/modules/views_ui/src/ProxyClass/ParamConverter/ViewUIConverter.php
@@ -7,15 +7,20 @@
 
 namespace Drupal\views_ui\ProxyClass\ParamConverter {
 
+    use Drupal\Core\ParamConverter\ParamConverterInterface;
+    use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+    use Symfony\Component\DependencyInjection\ContainerInterface;
+    use Symfony\Component\Routing\Route;
+
     /**
      * Provides a proxy class for \Drupal\views_ui\ParamConverter\ViewUIConverter.
      *
      * @see \Drupal\Component\ProxyBuilder
      */
-    class ViewUIConverter implements \Drupal\Core\ParamConverter\ParamConverterInterface
+    class ViewUIConverter implements ParamConverterInterface
     {
 
-        use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
+        use DependencySerializationTrait;
 
         /**
          * The id of the original proxied service.
@@ -46,7 +51,7 @@ class ViewUIConverter implements \Drupal\Core\ParamConverter\ParamConverterInter
          * @param string $drupal_proxy_original_service_id
          *   The service ID of the original service.
          */
-        public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
+        public function __construct(ContainerInterface $container, $drupal_proxy_original_service_id)
         {
             $this->container = $container;
             $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
@@ -78,7 +83,7 @@ public function convert($value, $definition, $name, array $defaults)
         /**
          * {@inheritdoc}
          */
-        public function applies($definition, $name, \Symfony\Component\Routing\Route $route)
+        public function applies($definition, $name, Route $route)
         {
             return $this->lazyLoadItself()->applies($definition, $name, $route);
         }
diff --git a/core/modules/views_ui/tests/modules/views_ui_test/src/Hook/ViewsUiTestHooks.php b/core/modules/views_ui/tests/modules/views_ui_test/src/Hook/ViewsUiTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..e564b9637ee77b06ea64797687025982d3d57fff
--- /dev/null
+++ b/core/modules/views_ui/tests/modules/views_ui_test/src/Hook/ViewsUiTestHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\views_ui_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_ui_test.
+ */
+class ViewsUiTestHooks {
+
+  /**
+   * Implements hook_views_preview_info_alter().
+   *
+   * Add a row count row to the live preview area.
+   */
+  #[Hook('views_preview_info_alter')]
+  public function viewsPreviewInfoAlter(&$rows, $view) {
+    $data = ['#markup' => t('Test row count')];
+    $data['#attached']['library'][] = 'views_ui_test/views_ui_test.test';
+    $rows['query'][] = [['data' => $data], count($view->result)];
+  }
+
+}
diff --git a/core/modules/views_ui/tests/modules/views_ui_test/views_ui_test.module b/core/modules/views_ui/tests/modules/views_ui_test/views_ui_test.module
deleted file mode 100644
index d963d7fcc6a37c5e76ed7ca80e22d15c6ce1cf72..0000000000000000000000000000000000000000
--- a/core/modules/views_ui/tests/modules/views_ui_test/views_ui_test.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Helper module for Views UI tests.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_views_preview_info_alter().
- *
- * Add a row count row to the live preview area.
- */
-function views_ui_test_views_preview_info_alter(&$rows, $view) {
-  $data = ['#markup' => t('Test row count')];
-  $data['#attached']['library'][] = 'views_ui_test/views_ui_test.test';
-  $rows['query'][] = [['data' => $data], count($view->result)];
-}
diff --git a/core/modules/views_ui/tests/modules/views_ui_test_field/src/Hook/ViewsUiTestFieldHooks.php b/core/modules/views_ui/tests/modules/views_ui_test_field/src/Hook/ViewsUiTestFieldHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..795982f64b5885a6eaf236c40af0f8a467fab5b6
--- /dev/null
+++ b/core/modules/views_ui/tests/modules/views_ui_test_field/src/Hook/ViewsUiTestFieldHooks.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\views_ui_test_field\Hook;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_ui_test_field.
+ */
+class ViewsUiTestFieldHooks {
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for views_ui_add_handler_form.
+   *
+   * Changes the label for one of the tests fields to validate this label is not
+   * searched on.
+   */
+  #[Hook('form_views_ui_add_handler_form_alter')]
+  public function formViewsUiAddHandlerFormAlter(&$form, FormStateInterface $form_state) : void {
+    $form['options']['name']['#options']['views.views_test_field_1']['title']['data']['#title'] .= ' FIELD_1_LABEL';
+  }
+
+}
diff --git a/core/modules/views_ui/tests/modules/views_ui_test_field/src/Hook/ViewsUiTestFieldViewsHooks.php b/core/modules/views_ui/tests/modules/views_ui_test_field/src/Hook/ViewsUiTestFieldViewsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..def022e945b0826ffb8b5e51597677d6f0874a62
--- /dev/null
+++ b/core/modules/views_ui/tests/modules/views_ui_test_field/src/Hook/ViewsUiTestFieldViewsHooks.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\views_ui_test_field\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for views_ui_test_field.
+ */
+class ViewsUiTestFieldViewsHooks {
+
+  /**
+   * Implements hook_views_data().
+   */
+  #[Hook('views_data')]
+  public function viewsData() {
+    $data['views']['views_test_field_1'] = [
+      'title' => t('Views test field 1 - FIELD_1_TITLE'),
+      'help' => t('Field 1 for testing purposes - FIELD_1_DESCRIPTION'),
+      'field' => [
+        'id' => 'views_test_field_1',
+      ],
+    ];
+    $data['views']['views_test_field_2'] = [
+      'title' => t('Views test field 2 - FIELD_2_TITLE'),
+      'help' => t('Field 2 for testing purposes - FIELD_2_DESCRIPTION'),
+      'field' => [
+        'id' => 'views_test_field_2',
+      ],
+    ];
+    return $data;
+  }
+
+}
diff --git a/core/modules/views_ui/tests/modules/views_ui_test_field/views_ui_test_field.module b/core/modules/views_ui/tests/modules/views_ui_test_field/views_ui_test_field.module
deleted file mode 100644
index 896affa3b19f8eed0b053957b4c08a0fff413e8a..0000000000000000000000000000000000000000
--- a/core/modules/views_ui/tests/modules/views_ui_test_field/views_ui_test_field.module
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * @file
- * ViewsUI Test field module.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * Implements hook_form_FORM_ID_alter() for views_ui_add_handler_form.
- *
- * Changes the label for one of the tests fields to validate this label is not
- * searched on.
- */
-function views_ui_test_field_form_views_ui_add_handler_form_alter(&$form, FormStateInterface $form_state): void {
-  $form['options']['name']['#options']['views.views_test_field_1']['title']['data']['#title'] .= ' FIELD_1_LABEL';
-}
diff --git a/core/modules/views_ui/tests/modules/views_ui_test_field/views_ui_test_field.views.inc b/core/modules/views_ui/tests/modules/views_ui_test_field/views_ui_test_field.views.inc
deleted file mode 100644
index 33cefbf8ef2e2aabbcd88d37d7a827b86035da1c..0000000000000000000000000000000000000000
--- a/core/modules/views_ui/tests/modules/views_ui_test_field/views_ui_test_field.views.inc
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-/**
- * @file
- * Provide views data for testing purposes.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_views_data().
- */
-function views_ui_test_field_views_data() {
-
-  $data['views']['views_test_field_1'] = [
-    'title' => t('Views test field 1 - FIELD_1_TITLE'),
-    'help' => t('Field 1 for testing purposes - FIELD_1_DESCRIPTION'),
-    'field' => [
-      'id' => 'views_test_field_1',
-    ],
-  ];
-  $data['views']['views_test_field_2'] = [
-    'title' => t('Views test field 2 - FIELD_2_TITLE'),
-    'help' => t('Field 2 for testing purposes - FIELD_2_DESCRIPTION'),
-    'field' => [
-      'id' => 'views_test_field_2',
-    ],
-  ];
-
-  return $data;
-}
diff --git a/core/modules/views_ui/tests/src/Kernel/ViewsBlockTest.php b/core/modules/views_ui/tests/src/Kernel/ViewsBlockTest.php
index da792a4047b306e711dda9a74571e4122bca448e..48fe83df982d8cc0e31a3d6fb3ab1dd030d86928 100644
--- a/core/modules/views_ui/tests/src/Kernel/ViewsBlockTest.php
+++ b/core/modules/views_ui/tests/src/Kernel/ViewsBlockTest.php
@@ -9,6 +9,7 @@
 use Drupal\Tests\user\Traits\UserCreationTrait;
 use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
 use Drupal\views\Tests\ViewTestData;
+use Drupal\views_ui\Hook\ViewsUiHooks;
 
 /**
  * Tests ViewsBlock.
@@ -60,7 +61,8 @@ public function testOperationLinks(): void {
     ]);
 
     // The anonymous user doesn't have the "administer block" permission.
-    $this->assertEmpty(views_ui_entity_operation($block));
+    $viewsUiEntityOperation = new ViewsUiHooks();
+    $this->assertEmpty($viewsUiEntityOperation->entityOperation($block));
 
     $this->setUpCurrentUser(['uid' => 1], ['administer views']);
 
@@ -74,7 +76,7 @@ public function testOperationLinks(): void {
         ]),
         'weight' => 50,
       ],
-    ], views_ui_entity_operation($block));
+    ], $viewsUiEntityOperation->entityOperation($block));
   }
 
 }
diff --git a/core/modules/views_ui/views_ui.api.php b/core/modules/views_ui/views_ui.api.php
index d623fd4f4c5121e95f05acbb5db2192f344d15d6..65b14c6fdbe87b8b9916be3e33432463b9f6e427 100644
--- a/core/modules/views_ui/views_ui.api.php
+++ b/core/modules/views_ui/views_ui.api.php
@@ -1,5 +1,11 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\views_ui\ViewUI;
+
 /**
  * @file
  * Describes hooks provided by the Views UI module.
@@ -28,7 +34,7 @@
  *
  * @see \Drupal\views_ui\ViewUI::renderDisplayTop()
  */
-function hook_views_ui_display_top_alter(&$build, \Drupal\views_ui\ViewUI $view, $display_id) {
+function hook_views_ui_display_top_alter(&$build, ViewUI $view, $display_id) {
   $build['custom']['#markup'] = 'This text should always appear';
 }
 
@@ -50,7 +56,7 @@ function hook_views_ui_display_top_alter(&$build, \Drupal\views_ui\ViewUI $view,
  *
  * @see \Drupal\views_ui\ViewEditForm::getDisplayTab()
  */
-function hook_views_ui_display_tab_alter(&$build, \Drupal\views_ui\ViewUI $view, $display_id) {
+function hook_views_ui_display_tab_alter(&$build, ViewUI $view, $display_id) {
   $build['custom']['#markup'] = 'This text should always appear';
 }
 
diff --git a/core/modules/views_ui/views_ui.module b/core/modules/views_ui/views_ui.module
index face0734d402d5c1b389965e1806204e81f2e7cd..f84be5ec62d2634169050cb82d1d2afce17eef21 100644
--- a/core/modules/views_ui/views_ui.module
+++ b/core/modules/views_ui/views_ui.module
@@ -2,132 +2,11 @@
 
 /**
  * @file
- * Provide structure for the administrative interface to Views.
  */
 
-use Drupal\block\BlockInterface;
 use Drupal\Component\Utility\Xss;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
-use Drupal\views\Entity\View;
 use Drupal\views\ViewExecutable;
-use Drupal\views\Analyzer;
-
-/**
- * Implements hook_help().
- */
-function views_ui_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.views_ui':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Views UI module provides an interface for managing views for the <a href=":views">Views module</a>. For more information, see the <a href=":handbook">online documentation for the Views UI module</a>.', [':views' => Url::fromRoute('help.page', ['name' => 'views'])->toString(), ':handbook' => 'https://www.drupal.org/documentation/modules/views_ui']) . '</p>';
-      $output .= '<h2>' . t('Uses') . '</h2>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Creating and managing views') . '</dt>';
-      $output .= '<dd>' . t('Views can be created from the <a href=":list">Views list page</a> by using the "Add view" action. Existing views can be managed from the <a href=":list">Views list page</a> by locating the view in the "Enabled" or "Disabled" list and selecting the desired operation action, for example "Edit".', [':list' => Url::fromRoute('entity.view.collection', ['name' => 'views_ui'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Enabling and disabling views') . '<dt>';
-      $output .= '<dd>' . t('Views can be enabled or disabled from the <a href=":list">Views list page</a>. To enable a view, find the view within the "Disabled" list and select the "Enable" operation. To disable a view find the view within the "Enabled" list and select the "Disable" operation.', [':list' => Url::fromRoute('entity.view.collection', ['name' => 'views_ui'])->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Exporting and importing views') . '</dt>';
-      $output .= '<dd>' . t('Views can be exported and imported as configuration files by using the <a href=":config">Configuration Manager module</a>.', [':config' => (\Drupal::moduleHandler()->moduleExists('config')) ? Url::fromRoute('help.page', ['name' => 'config'])->toString() : '#']) . '</dd>';
-      $output .= '</dl>';
-      return $output;
-  }
-}
-
-/**
- * Implements hook_entity_type_build().
- */
-function views_ui_entity_type_build(array &$entity_types) {
-  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
-  $entity_types['view']
-    ->setFormClass('edit', 'Drupal\views_ui\ViewEditForm')
-    ->setFormClass('add', 'Drupal\views_ui\ViewAddForm')
-    ->setFormClass('preview', 'Drupal\views_ui\ViewPreviewForm')
-    ->setFormClass('duplicate', 'Drupal\views_ui\ViewDuplicateForm')
-    ->setFormClass('delete', 'Drupal\Core\Entity\EntityDeleteForm')
-    ->setFormClass('break_lock', 'Drupal\views_ui\Form\BreakLockForm')
-    ->setListBuilderClass('Drupal\views_ui\ViewListBuilder')
-    ->setLinkTemplate('edit-form', '/admin/structure/views/view/{view}')
-    ->setLinkTemplate('edit-display-form', '/admin/structure/views/view/{view}/edit/{display_id}')
-    ->setLinkTemplate('preview-form', '/admin/structure/views/view/{view}/preview/{display_id}')
-    ->setLinkTemplate('duplicate-form', '/admin/structure/views/view/{view}/duplicate')
-    ->setLinkTemplate('delete-form', '/admin/structure/views/view/{view}/delete')
-    ->setLinkTemplate('enable', '/admin/structure/views/view/{view}/enable')
-    ->setLinkTemplate('disable', '/admin/structure/views/view/{view}/disable')
-    ->setLinkTemplate('break-lock-form', '/admin/structure/views/view/{view}/break-lock')
-    ->setLinkTemplate('collection', '/admin/structure/views');
-}
-
-/**
- * Implements hook_theme().
- */
-function views_ui_theme(): array {
-  return [
-    // Edit a view
-    'views_ui_display_tab_setting' => [
-      'variables' => ['description' => '', 'link' => '', 'settings_links' => [], 'overridden' => FALSE, 'defaulted' => FALSE, 'description_separator' => TRUE, 'class' => []],
-      'file' => 'views_ui.theme.inc',
-    ],
-    'views_ui_display_tab_bucket' => [
-      'render element' => 'element',
-      'file' => 'views_ui.theme.inc',
-    ],
-    'views_ui_rearrange_filter_form' => [
-      'render element' => 'form',
-      'file' => 'views_ui.theme.inc',
-    ],
-    'views_ui_expose_filter_form' => [
-      'render element' => 'form',
-      'file' => 'views_ui.theme.inc',
-    ],
-
-    // Legacy theme hook for displaying views info.
-    'views_ui_view_info' => [
-      'variables' => ['view' => NULL, 'displays' => NULL],
-      'file' => 'views_ui.theme.inc',
-    ],
-
-    // List views.
-    'views_ui_views_listing_table' => [
-      'variables' => [
-        'headers' => NULL,
-        'rows' => NULL,
-        'attributes' => [],
-      ],
-      'file' => 'views_ui.theme.inc',
-    ],
-    'views_ui_view_displays_list' => [
-      'variables' => ['displays' => []],
-    ],
-
-    // Group of filters.
-    'views_ui_build_group_filter_form' => [
-      'render element' => 'form',
-      'file' => 'views_ui.theme.inc',
-    ],
-
-    // On behalf of a plugin
-    'views_ui_style_plugin_table' => [
-      'render element' => 'form',
-      'file' => 'views_ui.theme.inc',
-    ],
-
-    // When previewing a view.
-    'views_ui_view_preview_section' => [
-      'variables' => ['view' => NULL, 'section' => NULL, 'content' => NULL, 'links' => ''],
-      'file' => 'views_ui.theme.inc',
-    ],
-
-    // Generic container wrapper, to use instead of theme_container when an id
-    // is not desired.
-    'views_ui_container' => [
-      'variables' => ['children' => NULL, 'attributes' => []],
-      'file' => 'views_ui.theme.inc',
-    ],
-  ];
-}
 
 /**
  * Implements hook_preprocess_HOOK() for views templates.
@@ -236,40 +115,6 @@ function views_ui_view_preview_section_rows_links(ViewExecutable $view) {
   return $links;
 }
 
-/**
- * Implements hook_views_plugins_display_alter().
- */
-function views_ui_views_plugins_display_alter(&$plugins) {
-  // Attach contextual links to each display plugin. The links will point to
-  // paths underneath "admin/structure/views/view/{$view->id()}" (i.e., paths
-  // for editing and performing other contextual actions on the view).
-  foreach ($plugins as &$display) {
-    $display['contextual links']['entity.view.edit_form'] = [
-      'route_name' => 'entity.view.edit_form',
-      'route_parameters_names' => ['view' => 'id'],
-    ];
-  }
-}
-
-/**
- * Implements hook_contextual_links_view_alter().
- */
-function views_ui_contextual_links_view_alter(&$element, $items) {
-  // Remove contextual links from being rendered, when so desired, such as
-  // within a View preview.
-  if (views_ui_contextual_links_suppress()) {
-    $element['#links'] = [];
-  }
-  // Append the display ID to the Views UI edit links, so that clicking on the
-  // contextual link takes you directly to the correct display tab on the edit
-  // screen.
-  elseif (!empty($element['#links']['entityviewedit-form'])) {
-    $display_id = $items['entity.view.edit_form']['metadata']['display_id'];
-    $route_parameters = $element['#links']['entityviewedit-form']['url']->getRouteParameters() + ['display_id' => $display_id];
-    $element['#links']['entityviewedit-form']['url'] = Url::fromRoute('entity.view.edit_display_form', $route_parameters);
-  }
-}
-
 /**
  * Sets a static variable for controlling whether contextual links are rendered.
  *
@@ -307,61 +152,3 @@ function views_ui_contextual_links_suppress_push() {
 function views_ui_contextual_links_suppress_pop() {
   views_ui_contextual_links_suppress(((int) views_ui_contextual_links_suppress()) - 1);
 }
-
-/**
- * Implements hook_views_analyze().
- *
- * This is the basic views analysis that checks for very minimal problems.
- * There are other analysis tools in core specific sections, such as
- * node.views.inc as well.
- */
-function views_ui_views_analyze(ViewExecutable $view) {
-  $ret = [];
-  // Check for something other than the default display:
-  if (count($view->displayHandlers) < 2) {
-    $ret[] = Analyzer::formatMessage(t('This view has only a default display and therefore will not be placed anywhere on your site; perhaps you want to add a page or a block display.'), 'warning');
-  }
-  // If a display has a path, check that it does not match an existing path
-  // alias. This results in the path alias not working.
-  foreach ($view->displayHandlers as $display) {
-    if (empty($display)) {
-      continue;
-    }
-    if ($display->hasPath() && $path = $display->getOption('path')) {
-      $normal_path = \Drupal::service('path_alias.manager')->getPathByAlias($path);
-      if ($path != $normal_path) {
-        $ret[] = Analyzer::formatMessage(t('You have configured display %display with a path which is an path alias as well. This might lead to unwanted effects so better use an internal path.', ['%display' => $display->display['display_title']]), 'warning');
-      }
-    }
-  }
-
-  return $ret;
-}
-
-/**
- * Implements hook_entity_operation().
- */
-function views_ui_entity_operation(EntityInterface $entity): array {
-  $operations = [];
-  if ($entity instanceof BlockInterface) {
-    $plugin = $entity->getPlugin();
-    if ($plugin->getBaseId() === 'views_block') {
-      $view_id_parts = explode('-', $plugin->getDerivativeId());
-      $view_id = $view_id_parts[0] ?? '';
-      $display_id = $view_id_parts[1] ?? '';
-      $view = View::load($view_id);
-      if ($view && $view->access('edit')) {
-        $operations['view-edit'] = [
-          'title' => t('Edit view'),
-          'url' => Url::fromRoute('entity.view.edit_display_form', [
-            'view' => $view_id,
-            'display_id' => $display_id,
-          ]),
-          'weight' => 50,
-        ];
-      }
-    }
-  }
-
-  return $operations;
-}
diff --git a/core/modules/views_ui/views_ui.theme.inc b/core/modules/views_ui/views_ui.theme.inc
index e94e38e802759d35a056fb96d44e01b81298d93e..996c700aa08d6cc31cba78a0322c48f99f6173df 100644
--- a/core/modules/views_ui/views_ui.theme.inc
+++ b/core/modules/views_ui/views_ui.theme.inc
@@ -2,7 +2,6 @@
 
 /**
  * @file
- * Preprocessors and theme functions for the Views UI.
  */
 
 use Drupal\Component\Render\FormattableMarkup;
diff --git a/core/modules/workflows/src/Hook/WorkflowsHooks.php b/core/modules/workflows/src/Hook/WorkflowsHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..6fe0cf68dca0d8a24bd811151e54c079882baf15
--- /dev/null
+++ b/core/modules/workflows/src/Hook/WorkflowsHooks.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\workflows\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for workflows.
+ */
+class WorkflowsHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'help.page.workflows':
+        $content_moderation_url = NULL;
+        if (\Drupal::moduleHandler()->moduleExists('content_moderation')) {
+          $content_moderation_url = Url::fromRoute('help.page', ['name' => 'content_moderation'])->toString();
+        }
+        $output = '<h2>' . t('About') . '</h2>';
+        if ($content_moderation_url) {
+          $output .= '<p>' . t('The Workflows module provides an API and an interface to create workflows with transitions between different states (for example publication or user status). These have to be provided by other modules such as the <a href=":moderation">Content Moderation module</a>. For more information, see the <a href=":workflow">online documentation for the Workflows module</a>.', [
+            ':moderation' => $content_moderation_url,
+            ':workflow' => 'https://www.drupal.org/documentation/modules/workflows',
+          ]) . '</p>';
+        }
+        else {
+          $output .= '<p>' . t('The Workflows module provides an API and an interface to create workflows with transitions between different states (for example publication or user status). These have to be provided by other modules such as the Content Moderation module. For more information, see the <a href=":workflow">online documentation for the Workflows module</a>.', [':workflow' => 'https://www.drupal.org/documentation/modules/workflows']) . '</p>';
+        }
+        $output .= '<h3>' . t('Uses') . '</h3>';
+        $output .= '<dl>';
+        $output .= '<dt>' . t('Adding workflows') . '</dt>';
+        if ($content_moderation_url) {
+          $output .= '<dd>' . t('You can <em>only</em> add workflows on the <a href=":workflows">Workflows page</a>, after you have installed a module that leverages the API such as the <a href=":moderation">Content Moderation module</a>.', [
+            ':moderation' => $content_moderation_url,
+            ':workflows' => Url::fromRoute('entity.workflow.collection')->toString(),
+          ]) . '</dd>';
+        }
+        else {
+          $output .= '<dd>' . t('You can <em>only</em> add workflows on the <a href=":workflows">Workflows page</a>, after you have installed a module that leverages the API such as the Content Moderation module.', [':workflow' => 'https://www.drupal.org/documentation/modules/workflows']) . '</dd>';
+        }
+        $output .= '<dt>' . t('Adding states') . '<dt>';
+        $output .= '<dd>' . t('A workflow requires at least two states. States can be added when you add or edit a workflow on the <a href=":workflows">Workflows page</a>.', [
+          ':workflows' => Url::fromRoute('entity.workflow.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Adding transitions') . '</dt>';
+        $output .= '<dd>' . t('A transition defines in which state an item can be save as next. It has one destination state, but can have several states <em>from</em> which the transition can be applied. Transitions can be added when you add or edit a workflow on the <a href=":workflows">Workflows page</a>.', [
+          ':workflows' => Url::fromRoute('entity.workflow.collection')->toString(),
+        ]) . '</dd>';
+        $output .= '<dt>' . t('Configuring workflows further') . '</dt>';
+        $output .= '<dd>' . t('Depending on the installed workflow type, additional configuration can be available in the edit form of a workflow.') . '</dd>';
+        $output .= '<dl>';
+        return $output;
+    }
+  }
+
+}
diff --git a/core/modules/workflows/tests/modules/workflow_type_test/src/Hook/WorkflowTypeTestHooks.php b/core/modules/workflows/tests/modules/workflow_type_test/src/Hook/WorkflowTypeTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..30c1ef1713e4c16fa62aaa65f96d904ff7aa3fea
--- /dev/null
+++ b/core/modules/workflows/tests/modules/workflow_type_test/src/Hook/WorkflowTypeTestHooks.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\workflow_type_test\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\workflow_type_test\Plugin\WorkflowType\WorkflowCustomAccessType;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\workflows\WorkflowInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for workflow_type_test.
+ */
+class WorkflowTypeTestHooks {
+
+  /**
+   * Implements hook_workflow_type_info_alter().
+   */
+  #[Hook('workflow_type_info_alter')]
+  public function workflowTypeInfoAlter(&$definitions) {
+    // Allow tests to override the workflow type definitions.
+    $state = \Drupal::state();
+    if ($state->get('workflow_type_test.plugin_definitions') !== NULL) {
+      $definitions = $state->get('workflow_type_test.plugin_definitions');
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_access() for the Workflow entity type.
+   */
+  #[Hook('workflow_access')]
+  public function workflowAccess(WorkflowInterface $entity, $operation, AccountInterface $account) {
+    if ($entity->getTypePlugin()->getPluginId() === 'workflow_custom_access_type') {
+      return WorkflowCustomAccessType::workflowAccess($entity, $operation, $account);
+    }
+    return AccessResult::neutral();
+  }
+
+}
diff --git a/core/modules/workflows/tests/modules/workflow_type_test/workflow_type_test.module b/core/modules/workflows/tests/modules/workflow_type_test/workflow_type_test.module
index ed2b138463f93f642408f736ee72e514c8210419..f74b802cdfb0c73fd34e11a6d7a03ef11a615c51 100644
--- a/core/modules/workflows/tests/modules/workflow_type_test/workflow_type_test.module
+++ b/core/modules/workflows/tests/modules/workflow_type_test/workflow_type_test.module
@@ -7,22 +7,6 @@
 
 declare(strict_types=1);
 
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\workflow_type_test\Plugin\WorkflowType\WorkflowCustomAccessType;
-use Drupal\workflows\WorkflowInterface;
-
-/**
- * Implements hook_workflow_type_info_alter().
- */
-function workflow_type_test_workflow_type_info_alter(&$definitions) {
-  // Allow tests to override the workflow type definitions.
-  $state = \Drupal::state();
-  if ($state->get('workflow_type_test.plugin_definitions') !== NULL) {
-    $definitions = $state->get('workflow_type_test.plugin_definitions');
-  }
-}
-
 /**
  * Sets the type plugin definitions override and clear the cache.
  *
@@ -33,13 +17,3 @@ function workflow_type_test_set_definitions($definitions) {
   \Drupal::state()->set('workflow_type_test.plugin_definitions', $definitions);
   \Drupal::service('plugin.manager.workflows.type')->clearCachedDefinitions();
 }
-
-/**
- * Implements hook_ENTITY_TYPE_access() for the Workflow entity type.
- */
-function workflow_type_test_workflow_access(WorkflowInterface $entity, $operation, AccountInterface $account) {
-  if ($entity->getTypePlugin()->getPluginId() === 'workflow_custom_access_type') {
-    return WorkflowCustomAccessType::workflowAccess($entity, $operation, $account);
-  }
-  return AccessResult::neutral();
-}
diff --git a/core/modules/workflows/workflows.module b/core/modules/workflows/workflows.module
deleted file mode 100644
index 7b29bba3854c787a0e7c87ddfab765917683ded4..0000000000000000000000000000000000000000
--- a/core/modules/workflows/workflows.module
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides hook implementations for the Workflows module.
- */
-
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Url;
-
-/**
- * Implements hook_help().
- */
-function workflows_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'help.page.workflows':
-      $content_moderation_url = NULL;
-      if (\Drupal::moduleHandler()->moduleExists('content_moderation')) {
-        $content_moderation_url = Url::fromRoute('help.page', ['name' => 'content_moderation'])->toString();
-      }
-      $output = '<h2>' . t('About') . '</h2>';
-      if ($content_moderation_url) {
-        $output .= '<p>' . t('The Workflows module provides an API and an interface to create workflows with transitions between different states (for example publication or user status). These have to be provided by other modules such as the <a href=":moderation">Content Moderation module</a>. For more information, see the <a href=":workflow">online documentation for the Workflows module</a>.', [':moderation' => $content_moderation_url, ':workflow' => 'https://www.drupal.org/documentation/modules/workflows']) . '</p>';
-      }
-      else {
-        $output .= '<p>' . t('The Workflows module provides an API and an interface to create workflows with transitions between different states (for example publication or user status). These have to be provided by other modules such as the Content Moderation module. For more information, see the <a href=":workflow">online documentation for the Workflows module</a>.', [':workflow' => 'https://www.drupal.org/documentation/modules/workflows']) . '</p>';
-      }
-      $output .= '<h3>' . t('Uses') . '</h3>';
-      $output .= '<dl>';
-      $output .= '<dt>' . t('Adding workflows') . '</dt>';
-      if ($content_moderation_url) {
-        $output .= '<dd>' . t('You can <em>only</em> add workflows on the <a href=":workflows">Workflows page</a>, after you have installed a module that leverages the API such as the <a href=":moderation">Content Moderation module</a>.', [':moderation' => $content_moderation_url, ':workflows' => Url::fromRoute('entity.workflow.collection')->toString()]) . '</dd>';
-      }
-      else {
-        $output .= '<dd>' . t('You can <em>only</em> add workflows on the <a href=":workflows">Workflows page</a>, after you have installed a module that leverages the API such as the Content Moderation module.', [':workflow' => 'https://www.drupal.org/documentation/modules/workflows']) . '</dd>';
-      }
-      $output .= '<dt>' . t('Adding states') . '<dt>';
-      $output .= '<dd>' . t('A workflow requires at least two states. States can be added when you add or edit a workflow on the <a href=":workflows">Workflows page</a>.', [':workflows' => Url::fromRoute('entity.workflow.collection')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Adding transitions') . '</dt>';
-      $output .= '<dd>' . t('A transition defines in which state an item can be save as next. It has one destination state, but can have several states <em>from</em> which the transition can be applied. Transitions can be added when you add or edit a workflow on the <a href=":workflows">Workflows page</a>.', [':workflows' => Url::fromRoute('entity.workflow.collection')->toString()]) . '</dd>';
-      $output .= '<dt>' . t('Configuring workflows further') . '</dt>';
-      $output .= '<dd>' . t('Depending on the installed workflow type, additional configuration can be available in the edit form of a workflow.') . '</dd>';
-      $output .= '<dl>';
-      return $output;
-  }
-}
diff --git a/core/modules/workspaces/src/Hook/WorkspacesHooks.php b/core/modules/workspaces/src/Hook/WorkspacesHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..a12a66edc7b1821c83359351da6b5ffc4cc9a67a
--- /dev/null
+++ b/core/modules/workspaces/src/Hook/WorkspacesHooks.php
@@ -0,0 +1,289 @@
+<?php
+
+namespace Drupal\workspaces\Hook;
+
+use Drupal\Core\Url;
+use Drupal\workspaces\ViewsQueryAlter;
+use Drupal\views\Plugin\views\query\QueryPluginBase;
+use Drupal\views\ViewExecutable;
+use Drupal\Core\Cache\Cache;
+use Drupal\workspaces\EntityAccess;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\workspaces\FormOperations;
+use Drupal\workspaces\EntityOperations;
+use Drupal\Core\Entity\EntityFormInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\workspaces\EntityTypeInfo;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for workspaces.
+ */
+class WorkspacesHooks {
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      // Main module help for the Workspaces module.
+      case 'help.page.workspaces':
+        $output = '';
+        $output .= '<h2>' . t('About') . '</h2>';
+        $output .= '<p>' . t('The Workspaces module allows workspaces to be defined and switched between. Content is then assigned to the active workspace when created. For more information, see the <a href=":workspaces">online documentation for the Workspaces module</a>.', [':workspaces' => 'https://www.drupal.org/docs/8/core/modules/workspace/overview']) . '</p>';
+        return $output;
+    }
+  }
+
+  /**
+   * Implements hook_module_preinstall().
+   */
+  #[Hook('module_preinstall')]
+  public function modulePreinstall($module) {
+    if ($module !== 'workspaces') {
+      return;
+    }
+
+    /** @var \Drupal\workspaces\WorkspaceInformationInterface $workspace_info */
+    $workspace_info = \Drupal::service('workspaces.information');
+    $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
+    foreach ($entity_definition_update_manager->getEntityTypes() as $entity_type) {
+      if ($workspace_info->isEntityTypeSupported($entity_type)) {
+        $entity_type->setRevisionMetadataKey('workspace', 'workspace');
+        $entity_definition_update_manager->updateEntityType($entity_type);
+      }
+    }
+  }
+
+  /**
+   * Implements hook_entity_type_build().
+   */
+  #[Hook('entity_type_build')]
+  public function entityTypeBuild(array &$entity_types) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class)->entityTypeBuild($entity_types);
+  }
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class)->entityTypeAlter($entity_types);
+  }
+
+  /**
+   * Implements hook_form_alter().
+   */
+  #[Hook('form_alter')]
+  public function formAlter(&$form, FormStateInterface $form_state, $form_id) : void {
+    if ($form_state->getFormObject() instanceof EntityFormInterface) {
+      \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityFormAlter($form, $form_state, $form_id);
+    }
+    \Drupal::service('class_resolver')->getInstanceFromDefinition(FormOperations::class)->formAlter($form, $form_state, $form_id);
+  }
+
+  /**
+   * Implements hook_field_info_alter().
+   */
+  #[Hook('field_info_alter')]
+  public function fieldInfoAlter(&$definitions) {
+    \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class)->fieldInfoAlter($definitions);
+  }
+
+  /**
+   * Implements hook_entity_base_field_info().
+   */
+  #[Hook('entity_base_field_info')]
+  public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class)->entityBaseFieldInfo($entity_type);
+  }
+
+  /**
+   * Implements hook_entity_preload().
+   */
+  #[Hook('entity_preload')]
+  public function entityPreload(array $ids, $entity_type_id) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityPreload($ids, $entity_type_id);
+  }
+
+  /**
+   * Implements hook_entity_presave().
+   */
+  #[Hook('entity_presave')]
+  public function entityPresave(EntityInterface $entity) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityPresave($entity);
+  }
+
+  /**
+   * Implements hook_entity_insert().
+   */
+  #[Hook('entity_insert')]
+  public function entityInsert(EntityInterface $entity) {
+    if ($entity->getEntityTypeId() === 'workspace') {
+      \Drupal::service('workspaces.association')->workspaceInsert($entity);
+      \Drupal::service('workspaces.repository')->resetCache();
+    }
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityInsert($entity);
+  }
+
+  /**
+   * Implements hook_entity_update().
+   */
+  #[Hook('entity_update')]
+  public function entityUpdate(EntityInterface $entity) {
+    if ($entity->getEntityTypeId() === 'workspace') {
+      \Drupal::service('workspaces.repository')->resetCache();
+    }
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityUpdate($entity);
+  }
+
+  /**
+   * Implements hook_entity_translation_insert().
+   */
+  #[Hook('entity_translation_insert')]
+  public function entityTranslationInsert(EntityInterface $translation) : void {
+    \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityTranslationInsert($translation);
+  }
+
+  /**
+   * Implements hook_entity_predelete().
+   */
+  #[Hook('entity_predelete')]
+  public function entityPredelete(EntityInterface $entity) {
+    if ($entity->getEntityTypeId() === 'workspace') {
+      \Drupal::service('workspaces.repository')->resetCache();
+    }
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityOperations::class)->entityPredelete($entity);
+  }
+
+  /**
+   * Implements hook_entity_delete().
+   */
+  #[Hook('entity_delete')]
+  public function entityDelete(EntityInterface $entity) {
+    if (\Drupal::service('workspaces.information')->isEntityTypeSupported($entity->getEntityType())) {
+      \Drupal::service('workspaces.association')->deleteAssociations(NULL, $entity->getEntityTypeId(), [$entity->id()]);
+    }
+  }
+
+  /**
+   * Implements hook_entity_revision_delete().
+   */
+  #[Hook('entity_revision_delete')]
+  public function entityRevisionDelete(EntityInterface $entity) {
+    if (\Drupal::service('workspaces.information')->isEntityTypeSupported($entity->getEntityType())) {
+      \Drupal::service('workspaces.association')->deleteAssociations(NULL, $entity->getEntityTypeId(), [$entity->id()], [$entity->getRevisionId()]);
+    }
+  }
+
+  /**
+   * Implements hook_entity_access().
+   *
+   * @see \Drupal\workspaces\EntityAccess
+   */
+  #[Hook('entity_access')]
+  public function entityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityAccess::class)->entityOperationAccess($entity, $operation, $account);
+  }
+
+  /**
+   * Implements hook_entity_create_access().
+   *
+   * @see \Drupal\workspaces\EntityAccess
+   */
+  #[Hook('entity_create_access')]
+  public function entityCreateAccess(AccountInterface $account, array $context, $entity_bundle) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(EntityAccess::class)->entityCreateAccess($account, $context, $entity_bundle);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for the 'menu_link_content' entity type.
+   */
+  #[Hook('menu_link_content_update')]
+  public function menuLinkContentUpdate(EntityInterface $entity) {
+    /** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
+    if ($entity->getLoadedRevisionId() != $entity->getRevisionId()) {
+      // We are not updating the menu tree definitions when a custom menu link
+      // entity is saved as a pending revision (because the parent can not be
+      // changed), so we need to clear the system menu cache manually. However,
+      // inserting or deleting a custom menu link updates the menu tree
+      // definitions, so we don't have to do anything in those cases.
+      $cache_tags = Cache::buildTags('config:system.menu', [$entity->getMenuName()], '.');
+      \Drupal::service('cache_tags.invalidator')->invalidateTags($cache_tags);
+    }
+  }
+
+  /**
+   * Implements hook_views_query_alter().
+   */
+  #[Hook('views_query_alter')]
+  public function viewsQueryAlter(ViewExecutable $view, QueryPluginBase $query) {
+    return \Drupal::service('class_resolver')->getInstanceFromDefinition(ViewsQueryAlter::class)->alterQuery($view, $query);
+  }
+
+  /**
+   * Implements hook_cron().
+   */
+  #[Hook('cron')]
+  public function cron() {
+    \Drupal::service('workspaces.manager')->purgeDeletedWorkspacesBatch();
+  }
+
+  /**
+   * Implements hook_toolbar().
+   */
+  #[Hook('toolbar')]
+  public function toolbar() {
+    $items['workspace'] = ['#cache' => ['contexts' => ['user.permissions']]];
+    $current_user = \Drupal::currentUser();
+    if (!$current_user->hasPermission('administer workspaces') && !$current_user->hasPermission('view own workspace') && !$current_user->hasPermission('view any workspace')) {
+      return $items;
+    }
+    /** @var \Drupal\workspaces\WorkspaceInterface $active_workspace */
+    $active_workspace = \Drupal::service('workspaces.manager')->getActiveWorkspace();
+    $items['workspace'] += [
+      '#type' => 'toolbar_item',
+      'tab' => [
+        '#lazy_builder' => [
+          'workspaces.lazy_builders:renderToolbarTab',
+                  [],
+        ],
+        '#create_placeholder' => TRUE,
+        '#lazy_builder_preview' => [
+          '#type' => 'link',
+          '#title' => $active_workspace ? $active_workspace->label() : t('Live'),
+          '#url' => Url::fromRoute('entity.workspace.collection'),
+          '#attributes' => [
+            'class' => [
+              'toolbar-tray-lazy-placeholder-link',
+            ],
+          ],
+        ],
+      ],
+      '#wrapper_attributes' => [
+        'class' => [
+          'workspaces-toolbar-tab',
+        ],
+      ],
+      '#weight' => 500,
+    ];
+    // Add a special class to the wrapper if we don't have an active workspace so
+    // we can highlight it with a different color.
+    if (!$active_workspace) {
+      $items['workspace']['#wrapper_attributes']['class'][] = 'workspaces-toolbar-tab--is-default';
+    }
+    // \Drupal\toolbar\Element\ToolbarItem::preRenderToolbarItem adds an
+    // #attributes property to each toolbar item's tab child automatically.
+    // Lazy builders don't support an #attributes property so we need to
+    // add another render callback to remove the #attributes property. We start by
+    // adding the defaults, and then we append our own pre render callback.
+    $items['workspace'] += \Drupal::service('plugin.manager.element_info')->getInfo('toolbar_item');
+    $items['workspace']['#pre_render'][] = 'workspaces.lazy_builders:removeTabAttributes';
+    return $items;
+  }
+
+}
diff --git a/core/modules/workspaces/tests/modules/workspace_access_test/src/Hook/WorkspaceAccessTestHooks.php b/core/modules/workspaces/tests/modules/workspace_access_test/src/Hook/WorkspaceAccessTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..7f47e13df26b46885451a4e8ff7c384ba81ccda1
--- /dev/null
+++ b/core/modules/workspaces/tests/modules/workspace_access_test/src/Hook/WorkspaceAccessTestHooks.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\workspace_access_test\Hook;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for workspace_access_test.
+ */
+class WorkspaceAccessTestHooks {
+
+  /**
+   * Implements hook_ENTITY_TYPE_access() for the 'workspace' entity type.
+   */
+  #[Hook('workspace_access')]
+  public function workspaceAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    return \Drupal::state()->get("workspace_access_test.result.{$operation}", AccessResult::neutral());
+  }
+
+}
diff --git a/core/modules/workspaces/tests/modules/workspace_access_test/workspace_access_test.module b/core/modules/workspaces/tests/modules/workspace_access_test/workspace_access_test.module
deleted file mode 100644
index 8b54f6a99b3ccdbac23e3f18e71b8eafb954b89e..0000000000000000000000000000000000000000
--- a/core/modules/workspaces/tests/modules/workspace_access_test/workspace_access_test.module
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides supporting code for testing access for workspaces.
- */
-
-declare(strict_types=1);
-
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Session\AccountInterface;
-
-/**
- * Implements hook_ENTITY_TYPE_access() for the 'workspace' entity type.
- */
-function workspace_access_test_workspace_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  return \Drupal::state()->get("workspace_access_test.result.$operation", AccessResult::neutral());
-}
diff --git a/core/modules/workspaces/tests/modules/workspaces_test/src/Hook/WorkspacesTestHooks.php b/core/modules/workspaces/tests/modules/workspaces_test/src/Hook/WorkspacesTestHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..09ab54054ff6e5b97888ece8944079cb335fc944
--- /dev/null
+++ b/core/modules/workspaces/tests/modules/workspaces_test/src/Hook/WorkspacesTestHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\workspaces_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Hook implementations for workspaces_test.
+ */
+class WorkspacesTestHooks {
+
+  /**
+   * Implements hook_entity_type_alter().
+   */
+  #[Hook('entity_type_alter')]
+  public function entityTypeAlter(array &$entity_types) : void {
+    $state = \Drupal::state();
+    // Allow all entity types to have their definition changed dynamically for
+    // testing purposes.
+    foreach ($entity_types as $entity_type_id => $entity_type) {
+      $entity_types[$entity_type_id] = $state->get("{$entity_type_id}.entity_type", $entity_types[$entity_type_id]);
+    }
+  }
+
+}
diff --git a/core/modules/workspaces/tests/modules/workspaces_test/workspaces_test.module b/core/modules/workspaces/tests/modules/workspaces_test/workspaces_test.module
deleted file mode 100644
index 3c3f63dacfe662d2409a6c5a32b6309c1948dddd..0000000000000000000000000000000000000000
--- a/core/modules/workspaces/tests/modules/workspaces_test/workspaces_test.module
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-/**
- * @file
- * Provides supporting code for testing workspaces.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_entity_type_alter().
- */
-function workspaces_test_entity_type_alter(array &$entity_types): void {
-  $state = \Drupal::state();
-
-  // Allow all entity types to have their definition changed dynamically for
-  // testing purposes.
-  foreach ($entity_types as $entity_type_id => $entity_type) {
-    $entity_types[$entity_type_id] = $state->get("$entity_type_id.entity_type", $entity_types[$entity_type_id]);
-  }
-}
diff --git a/core/modules/workspaces/workspaces.install b/core/modules/workspaces/workspaces.install
index d89b446a55b4c38591e9b06c545d5eeed6bd7e2c..e4c0a6668b99eed052cdbbcb015924723a165ad9 100644
--- a/core/modules/workspaces/workspaces.install
+++ b/core/modules/workspaces/workspaces.install
@@ -27,25 +27,6 @@ function workspaces_requirements($phase) {
   return $requirements;
 }
 
-/**
- * Implements hook_module_preinstall().
- */
-function workspaces_module_preinstall($module) {
-  if ($module !== 'workspaces') {
-    return;
-  }
-
-  /** @var \Drupal\workspaces\WorkspaceInformationInterface $workspace_info */
-  $workspace_info = \Drupal::service('workspaces.information');
-  $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
-  foreach ($entity_definition_update_manager->getEntityTypes() as $entity_type) {
-    if ($workspace_info->isEntityTypeSupported($entity_type)) {
-      $entity_type->setRevisionMetadataKey('workspace', 'workspace');
-      $entity_definition_update_manager->updateEntityType($entity_type);
-    }
-  }
-}
-
 /**
  * Implements hook_install().
  */
diff --git a/core/modules/workspaces/workspaces.module b/core/modules/workspaces/workspaces.module
index 90fca801f9d6637bfa737f6fccfa44e05f8665a6..39864fd9f3e28a38eaf724942f485fb69a1e5b8f 100644
--- a/core/modules/workspaces/workspaces.module
+++ b/core/modules/workspaces/workspaces.module
@@ -2,39 +2,8 @@
 
 /**
  * @file
- * Provides full-site preview functionality for content staging.
  */
 
-use Drupal\Core\Cache\Cache;
-use Drupal\Core\Entity\EntityFormInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Url;
-use Drupal\views\Plugin\views\query\QueryPluginBase;
-use Drupal\views\ViewExecutable;
-use Drupal\workspaces\EntityAccess;
-use Drupal\workspaces\EntityOperations;
-use Drupal\workspaces\EntityTypeInfo;
-use Drupal\workspaces\FormOperations;
-use Drupal\workspaces\ViewsQueryAlter;
-
-/**
- * Implements hook_help().
- */
-function workspaces_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    // Main module help for the Workspaces module.
-    case 'help.page.workspaces':
-      $output = '';
-      $output .= '<h2>' . t('About') . '</h2>';
-      $output .= '<p>' . t('The Workspaces module allows workspaces to be defined and switched between. Content is then assigned to the active workspace when created. For more information, see the <a href=":workspaces">online documentation for the Workspaces module</a>.', [':workspaces' => 'https://www.drupal.org/docs/8/core/modules/workspace/overview']) . '</p>';
-      return $output;
-  }
-}
-
 /**
  * Implements hook_module_implements_alter().
  */
@@ -57,252 +26,3 @@ function workspaces_module_implements_alter(&$implementations, $hook): void {
     $implementations['workspaces'] = $group;
   }
 }
-
-/**
- * Implements hook_entity_type_build().
- */
-function workspaces_entity_type_build(array &$entity_types) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityTypeInfo::class)
-    ->entityTypeBuild($entity_types);
-}
-
-/**
- * Implements hook_entity_type_alter().
- */
-function workspaces_entity_type_alter(array &$entity_types): void {
-  \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityTypeInfo::class)
-    ->entityTypeAlter($entity_types);
-}
-
-/**
- * Implements hook_form_alter().
- */
-function workspaces_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
-  if ($form_state->getFormObject() instanceof EntityFormInterface) {
-    \Drupal::service('class_resolver')
-      ->getInstanceFromDefinition(EntityOperations::class)
-      ->entityFormAlter($form, $form_state, $form_id);
-  }
-  \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(FormOperations::class)
-    ->formAlter($form, $form_state, $form_id);
-}
-
-/**
- * Implements hook_field_info_alter().
- */
-function workspaces_field_info_alter(&$definitions) {
-  \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityTypeInfo::class)
-    ->fieldInfoAlter($definitions);
-}
-
-/**
- * Implements hook_entity_base_field_info().
- */
-function workspaces_entity_base_field_info(EntityTypeInterface $entity_type) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityTypeInfo::class)
-    ->entityBaseFieldInfo($entity_type);
-}
-
-/**
- * Implements hook_entity_preload().
- */
-function workspaces_entity_preload(array $ids, $entity_type_id) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityPreload($ids, $entity_type_id);
-}
-
-/**
- * Implements hook_entity_presave().
- */
-function workspaces_entity_presave(EntityInterface $entity) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityPresave($entity);
-}
-
-/**
- * Implements hook_entity_insert().
- */
-function workspaces_entity_insert(EntityInterface $entity) {
-  if ($entity->getEntityTypeId() === 'workspace') {
-    \Drupal::service('workspaces.association')->workspaceInsert($entity);
-    \Drupal::service('workspaces.repository')->resetCache();
-  }
-
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityInsert($entity);
-}
-
-/**
- * Implements hook_entity_update().
- */
-function workspaces_entity_update(EntityInterface $entity) {
-  if ($entity->getEntityTypeId() === 'workspace') {
-    \Drupal::service('workspaces.repository')->resetCache();
-  }
-
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityUpdate($entity);
-}
-
-/**
- * Implements hook_entity_translation_insert().
- */
-function workspaces_entity_translation_insert(EntityInterface $translation): void {
-  \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityTranslationInsert($translation);
-}
-
-/**
- * Implements hook_entity_predelete().
- */
-function workspaces_entity_predelete(EntityInterface $entity) {
-  if ($entity->getEntityTypeId() === 'workspace') {
-    \Drupal::service('workspaces.repository')->resetCache();
-  }
-
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityOperations::class)
-    ->entityPredelete($entity);
-}
-
-/**
- * Implements hook_entity_delete().
- */
-function workspaces_entity_delete(EntityInterface $entity) {
-  if (\Drupal::service('workspaces.information')->isEntityTypeSupported($entity->getEntityType())) {
-    \Drupal::service('workspaces.association')
-      ->deleteAssociations(NULL, $entity->getEntityTypeId(), [$entity->id()]);
-  }
-}
-
-/**
- * Implements hook_entity_revision_delete().
- */
-function workspaces_entity_revision_delete(EntityInterface $entity) {
-  if (\Drupal::service('workspaces.information')->isEntityTypeSupported($entity->getEntityType())) {
-    \Drupal::service('workspaces.association')
-      ->deleteAssociations(NULL, $entity->getEntityTypeId(), [$entity->id()], [$entity->getRevisionId()]);
-  }
-}
-
-/**
- * Implements hook_entity_access().
- *
- * @see \Drupal\workspaces\EntityAccess
- */
-function workspaces_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityAccess::class)
-    ->entityOperationAccess($entity, $operation, $account);
-}
-
-/**
- * Implements hook_entity_create_access().
- *
- * @see \Drupal\workspaces\EntityAccess
- */
-function workspaces_entity_create_access(AccountInterface $account, array $context, $entity_bundle) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(EntityAccess::class)
-    ->entityCreateAccess($account, $context, $entity_bundle);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for the 'menu_link_content' entity type.
- */
-function workspaces_menu_link_content_update(EntityInterface $entity) {
-  /** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
-  if ($entity->getLoadedRevisionId() != $entity->getRevisionId()) {
-    // We are not updating the menu tree definitions when a custom menu link
-    // entity is saved as a pending revision (because the parent can not be
-    // changed), so we need to clear the system menu cache manually. However,
-    // inserting or deleting a custom menu link updates the menu tree
-    // definitions, so we don't have to do anything in those cases.
-    $cache_tags = Cache::buildTags('config:system.menu', [$entity->getMenuName()], '.');
-    \Drupal::service('cache_tags.invalidator')->invalidateTags($cache_tags);
-  }
-}
-
-/**
- * Implements hook_views_query_alter().
- */
-function workspaces_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
-  return \Drupal::service('class_resolver')
-    ->getInstanceFromDefinition(ViewsQueryAlter::class)
-    ->alterQuery($view, $query);
-}
-
-/**
- * Implements hook_cron().
- */
-function workspaces_cron() {
-  \Drupal::service('workspaces.manager')->purgeDeletedWorkspacesBatch();
-}
-
-/**
- * Implements hook_toolbar().
- */
-function workspaces_toolbar() {
-  $items['workspace'] = [
-    '#cache' => [
-      'contexts' => [
-        'user.permissions',
-      ],
-    ],
-  ];
-  $current_user = \Drupal::currentUser();
-  if (!$current_user->hasPermission('administer workspaces')
-    && !$current_user->hasPermission('view own workspace')
-    && !$current_user->hasPermission('view any workspace')) {
-    return $items;
-  }
-
-  /** @var \Drupal\workspaces\WorkspaceInterface $active_workspace */
-  $active_workspace = \Drupal::service('workspaces.manager')->getActiveWorkspace();
-
-  $items['workspace'] += [
-    '#type' => 'toolbar_item',
-    'tab' => [
-      '#lazy_builder' => ['workspaces.lazy_builders:renderToolbarTab', []],
-      '#create_placeholder' => TRUE,
-      '#lazy_builder_preview' => [
-        '#type' => 'link',
-        '#title' => $active_workspace ? $active_workspace->label() : t('Live'),
-        '#url' => Url::fromRoute('entity.workspace.collection'),
-        '#attributes' => [
-          'class' => ['toolbar-tray-lazy-placeholder-link'],
-        ],
-      ],
-    ],
-    '#wrapper_attributes' => [
-      'class' => ['workspaces-toolbar-tab'],
-    ],
-    '#weight' => 500,
-  ];
-
-  // Add a special class to the wrapper if we don't have an active workspace so
-  // we can highlight it with a different color.
-  if (!$active_workspace) {
-    $items['workspace']['#wrapper_attributes']['class'][] = 'workspaces-toolbar-tab--is-default';
-  }
-
-  // \Drupal\toolbar\Element\ToolbarItem::preRenderToolbarItem adds an
-  // #attributes property to each toolbar item's tab child automatically.
-  // Lazy builders don't support an #attributes property so we need to
-  // add another render callback to remove the #attributes property. We start by
-  // adding the defaults, and then we append our own pre render callback.
-  $items['workspace'] += \Drupal::service('plugin.manager.element_info')->getInfo('toolbar_item');
-  $items['workspace']['#pre_render'][] = 'workspaces.lazy_builders:removeTabAttributes';
-
-  return $items;
-}
diff --git a/core/profiles/demo_umami/modules/demo_umami_content/demo_umami_content.install b/core/profiles/demo_umami/modules/demo_umami_content/demo_umami_content.install
index 3a0a986c4be423a531f6ec9a1c1acb294762563c..97eff3e068fa2aed576acd89bcdd00a6c0150ef9 100644
--- a/core/profiles/demo_umami/modules/demo_umami_content/demo_umami_content.install
+++ b/core/profiles/demo_umami/modules/demo_umami_content/demo_umami_content.install
@@ -7,17 +7,6 @@
 
 use Drupal\demo_umami_content\InstallHelper;
 
-/**
- * Implements hook_module_preinstall().
- */
-function demo_umami_content_module_preinstall($module) {
-  if ($module === 'demo_umami_content' && !\Drupal::service('config.installer')->isSyncing()) {
-    // Run before importing config so blocks are created with the correct
-    // dependencies.
-    \Drupal::classResolver(InstallHelper::class)->importContent();
-  }
-}
-
 /**
  * Implements hook_install().
  */
diff --git a/core/profiles/demo_umami/modules/demo_umami_content/src/Hook/DemoUmamiContentHooks.php b/core/profiles/demo_umami/modules/demo_umami_content/src/Hook/DemoUmamiContentHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..33b903ff276a7381a7e51ad7205866115c72f2f0
--- /dev/null
+++ b/core/profiles/demo_umami/modules/demo_umami_content/src/Hook/DemoUmamiContentHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\demo_umami_content\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\demo_umami_content\InstallHelper;
+
+/**
+ * Hook implementations for demo_umami_content.
+ */
+class DemoUmamiContentHooks {
+
+  /**
+   * Implements hook_module_preinstall().
+   */
+  #[Hook('module_preinstall')]
+  public function modulePreinstall($module) {
+    if ($module === 'demo_umami_content' && !\Drupal::service('config.installer')->isSyncing()) {
+      // Run before importing config so blocks are created with the correct
+      // dependencies.
+      \Drupal::classResolver(InstallHelper::class)->importContent();
+    }
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Common/DrupalFlushAllCachesTest.php b/core/tests/Drupal/KernelTests/Core/Common/DrupalFlushAllCachesTest.php
index 47c65db6a7b6ae8bdb7d76ef45ebfb1c29058586..9bff112862205cb01dd4b423c858346e8cceb861 100644
--- a/core/tests/Drupal/KernelTests/Core/Common/DrupalFlushAllCachesTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Common/DrupalFlushAllCachesTest.php
@@ -5,6 +5,7 @@
 namespace Drupal\KernelTests\Core\Common;
 
 use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\system_test\Hook\SystemTestHooks;
 use Drupal\KernelTests\KernelTestBase;
 
 /**
@@ -46,7 +47,7 @@ public function testDrupalFlushAllCachesModuleList(): void {
     sort($container_modules);
     $this->assertSame($module_list, $container_modules);
     $this->assertSame(1, $this->containerBuilds);
-    $this->assertTrue(function_exists('system_test_help'));
+    $this->assertTrue(method_exists(SystemTestHooks::class, 'help'));
 
     $core_extension->clear('module.system_test')->save();
     $this->containerBuilds = 0;
diff --git a/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php b/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php
index 8c6490d6e12bdb3fb476a8c9b8588308006fb8a0..38d8003ae23869df674a9942b17f943d9498e5d0 100644
--- a/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php
+++ b/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php
@@ -10,6 +10,7 @@
 use Drupal\Core\File\Exception\FileException;
 use Drupal\Core\File\FileExists;
 use Drupal\Core\File\FileSystemInterface;
+use Drupal\system\Hook\SystemHooks;
 
 /**
  * Tests operations dealing with directories.
@@ -106,7 +107,8 @@ public function testFileCheckDirectoryHandling(): void {
     // Remove .htaccess file again to test that it is re-created by a cron run.
     @$file_system->unlink($default_scheme . '://.htaccess');
     $this->assertFileDoesNotExist($default_scheme . '://.htaccess');
-    system_cron();
+    $systemCron = new SystemHooks();
+    $systemCron->cron();
     $this->assertFileExists($default_scheme . '://.htaccess');
 
     // Verify contents of .htaccess file.
diff --git a/core/tests/Drupal/KernelTests/Core/KeyValueStore/GarbageCollectionTest.php b/core/tests/Drupal/KernelTests/Core/KeyValueStore/GarbageCollectionTest.php
index f6c58cca3af91c6a6cbc7a21fdd20fcede334b71..9a84be9a190191301b0c0e6fee2482a07e8de39d 100644
--- a/core/tests/Drupal/KernelTests/Core/KeyValueStore/GarbageCollectionTest.php
+++ b/core/tests/Drupal/KernelTests/Core/KeyValueStore/GarbageCollectionTest.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\KeyValueStore\DatabaseStorageExpirable;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\system\Hook\SystemHooks;
 
 /**
  * Tests garbage collection for the expirable key-value database storage.
@@ -50,7 +51,8 @@ public function testGarbageCollection(): void {
 
     // Perform a new set operation and then trigger garbage collection.
     $store->setWithExpire('autumn', 'winter', rand(500, 1000000));
-    system_cron();
+    $systemCron = new SystemHooks();
+    $systemCron->cron();
 
     // Query the database and confirm that the stale records were deleted.
     $result = $connection->select('key_value_expire', 'kvp')
diff --git a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php
index 1856945a712a2c4ec91c91608fc2777eacf338f3..f237c09d1caa0a8562760fe02b35b57a61e4d873 100644
--- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php
@@ -9,6 +9,7 @@
 use Drupal\Core\Theme\Registry;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Drupal\theme_test\Hook\ThemeTestHooks;
 
 /**
  * @coversDefaultClass \Drupal\Core\Theme\Registry
@@ -155,10 +156,11 @@ public function testGetRegistryForModule(): void {
     // Include the module and theme files so that hook_theme can be called.
     include_once $this->root . '/core/modules/system/tests/modules/theme_test/theme_test.module';
     include_once $this->root . '/core/tests/fixtures/test_stable/test_stable.theme';
+    $themeTestTheme = new ThemeTestHooks();
     $this->moduleHandler->expects($this->atLeastOnce())
       ->method('invoke')
       ->with('theme_test', 'theme')
-      ->willReturn(theme_test_theme(NULL, NULL, NULL, NULL));
+      ->willReturn($themeTestTheme->theme(NULL, NULL, NULL, NULL));
     $this->moduleHandler->expects($this->atLeastOnce())
       ->method('invokeAllWith')
       ->with('theme')