diff --git a/collection.module b/collection.module
index 70a3ef2..586ceca 100644
--- a/collection.module
+++ b/collection.module
@@ -4,6 +4,9 @@
  * @file
  * Contains collection.module.
  */
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\collection\Hook\CollectionHooks;
+use Drupal\Component\Utility\DeprecationHelper;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -17,42 +20,26 @@ use Drupal\Core\Access\AccessResult;
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function collection_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    // Main module help for the collection module.
-    case 'help.page.collection':
-      $output = '';
-      $output .= '<h3>' . t('About') . '</h3>';
-      $output .= '<p>' . t('Allows collections of content and configuration entities.') . '</p>';
-      return $output;
-
-    default:
-  }
+  return \Drupal::service(CollectionHooks::class)->help($route_name, $route_match);
 }
 
 /**
  * Implements hook_toolbar_alter().
  */
+#[LegacyHook]
 function collection_toolbar_alter(&$items) {
-  $items['administration']['#attached']['library'][] = 'collection/toolbar';
+  \Drupal::service(CollectionHooks::class)->toolbarAlter($items);
 }
 
 /**
  * Implements hook_theme().
  */
-function collection_theme() {
-  $theme = [];
-  $theme['collection'] = [
-    'render element' => 'elements',
-    'file' => 'collection.page.inc',
-    'template' => 'collection',
-  ];
-  $theme['collection_content_add_list'] = [
-    'render element' => 'content',
-    'variables' => ['content' => NULL],
-    'file' => 'collection.page.inc',
-  ];
-  return $theme;
+#[LegacyHook]
+function collection_theme()
+{
+    return \Drupal::service(CollectionHooks::class)->theme();
 }
 
 /**
@@ -66,80 +53,28 @@ function collection_theme() {
  *
  * @see Drupal\collection\Form\CollectionItemTypeForm
  */
-function collection_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
-  $fields = [];
-
-  // Apply custom settings to different collection item types.
-  if ($entity_type->id() === 'collection_item') {
-    $collection_item_type_storage = \Drupal::service('entity_type.manager')->getStorage('collection_item_type');
-
-    // Get the `allowed_bundles` setting for this collection item type. They
-    // will be an array of items in the form `entity_type_id.bundle` (e.g.
-    // `node.article`).
-    $collection_item_type = $collection_item_type_storage->load($bundle);
-    $entity_bundles = [];
-
-    // Place the allowed bundles in a multidimensional array for later use.
-    foreach ($collection_item_type->getAllowedBundles() as $entity_and_bundle) {
-      list($entity_type_id, $bundle_name) = explode('.', $entity_and_bundle);
-      $entity_bundles[$entity_type_id][$bundle_name] = $bundle_name;
-    }
-
-    // Load the item field for this collection item bundle.
-    $fields['item'] = BaseFieldOverride::createFromBaseFieldDefinition($base_field_definitions['item'], $bundle);
-
-    // Append any non-node entity types to the `entity_type_ids` setting.
-    $entity_type_ids_setting = $fields['item']->getSetting('entity_type_ids');
-    $fields['item']
-      ->setSetting('entity_type_ids', $entity_type_ids_setting + array_combine(array_keys($entity_bundles), array_keys($entity_bundles)));
-
-    // Set config for the allowed entity types, especially the `target_bundles`,
-    // for this collection item type.
-    foreach ($entity_bundles as $entity_type_id => $bundles) {
-      $fields['item']
-        ->setSetting($entity_type_id, [
-          'handler' => 'default:' . $entity_type_id,
-          'handler_settings' => [
-            'target_bundles' => $bundles,
-          ]
-        ]);
-    }
-  }
-
-  return $fields;
+#[LegacyHook]
+function collection_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions)
+{
+    return \Drupal::service(CollectionHooks::class)->entityBundleFieldInfo($entity_type, $bundle, $base_field_definitions);
 }
 
 /**
  * Implements hook_form_BASE_FORM_ID_alter().
  */
-function collection_form_collection_form_alter(&$form, FormStateInterface $form_state) {
-  $form['settings'] = [
-    '#type' => 'details',
-    '#title' => t('Settings'),
-    '#group' => 'advanced',
-    '#weight' => 50,
-    '#optional' => TRUE,
-    '#open' => FALSE,
-  ];
-
-  $form['collectible']['#group'] = 'settings';
-  $form['user_id']['#group'] = 'settings';
+#[LegacyHook]
+function collection_form_collection_form_alter(&$form, FormStateInterface $form_state)
+{
+    \Drupal::service(CollectionHooks::class)->formCollectionFormAlter($form, $form_state);
 }
 
 /**
  * Implements hook_theme_suggestions_HOOK().
  */
-function collection_theme_suggestions_collection(array $variables) {
-  $suggestions = [];
-  $entity = $variables['elements']['#collection'];
-  $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
-
-  $suggestions[] = 'collection__' . $sanitized_view_mode;
-  $suggestions[] = 'collection__' . $entity->bundle();
-  $suggestions[] = 'collection__' . $entity->bundle() . '__' . $sanitized_view_mode;
-  $suggestions[] = 'collection__' . $entity->id();
-  $suggestions[] = 'collection__' . $entity->id() . '__' . $sanitized_view_mode;
-  return $suggestions;
+#[LegacyHook]
+function collection_theme_suggestions_collection(array $variables)
+{
+    return \Drupal::service(CollectionHooks::class)->themeSuggestionsCollection($variables);
 }
 
 /**
@@ -147,28 +82,10 @@ function collection_theme_suggestions_collection(array $variables) {
  *
  * Update the name of collection items when an entity is modified.
  */
-function collection_entity_update(EntityInterface $entity) {
-  // Don't run on collection_items or paragraphs.
-  if (in_array($entity->getEntityTypeId(), ['collection_item', 'paragraph'])) {
-    return;
-  }
-
-  // Don't run if entity label is unchanged.
-  if ($entity->label() === $entity->original->label()) {
-    return;
-  }
-
-  $item_storage = \Drupal::service('entity_type.manager')->getStorage('collection_item');
-  $collection_items = $item_storage->loadByProperties([
-    'item__target_id' => $entity->id(),
-    'item__target_type' => $entity->getEntityTypeId()
-  ]);
-
-  foreach ($collection_items as $collection_item) {
-    // This should trigger CollectionItem::preSave() and update the
-    // collection_item label.
-    $collection_item->save();
-  }
+#[LegacyHook]
+function collection_entity_update(EntityInterface $entity)
+{
+    \Drupal::service(CollectionHooks::class)->entityUpdate($entity);
 }
 
 /**
@@ -176,56 +93,35 @@ function collection_entity_update(EntityInterface $entity) {
  *
  * Delete any corresponding collection items when an entity is deleted.
  */
-function collection_entity_predelete(EntityInterface $entity) {
-  $item_storage = \Drupal::service('entity_type.manager')->getStorage('collection_item');
-  $collection_items = $item_storage->loadByProperties([
-    'item__target_id' => $entity->id(),
-    'item__target_type' => $entity->getEntityTypeId()
-  ]);
-
-  foreach ($collection_items as $collection_item) {
-    $collection_item->delete();
-  }
+#[LegacyHook]
+function collection_entity_predelete(EntityInterface $entity)
+{
+    \Drupal::service(CollectionHooks::class)->entityPredelete($entity);
 }
 
 /**
  * Implements hook_entity_extra_field_info().
  */
+#[LegacyHook]
 function collection_entity_extra_field_info() {
-  return \Drupal::service('collection.content_entity_form.alter')->getExtraFieldInfo();
+  return \Drupal::service(CollectionHooks::class)->entityExtraFieldInfo();
 }
 
 /**
  * Implements hook_form_alter().
  */
+#[LegacyHook]
 function collection_form_alter(&$form, FormStateInterface $form_state, $form_id) {
-  \Drupal::service('collection.content_entity_form.alter')->alterForm($form, $form_state, $form_id);
+  \Drupal::service(CollectionHooks::class)->formAlter($form, $form_state, $form_id);
 }
 
 /**
  * Implements hook_entity_field_access_alter().
  */
-function collection_entity_field_access_alter(array &$grants, array $context) {
-  // Prevent change of collected item reference when editing a collection item.
-  if ($context['field_definition']->getTargetEntityTypeId() === 'collection_item' && $context['field_definition']->getName() === 'item' && $context['operation'] === 'edit') {
-    $grants[':default'] = AccessResult::allowedIf($context['items']->getEntity()->isNew());
-  }
-
-  // Restrict access to collection item attributes.
-  if ($context['field_definition']->getTargetEntityTypeId() === 'collection_item' && $context['field_definition']->getName() === 'attributes' && $context['operation'] === 'edit') {
-    $attributes_access = $context['items']->getEntity()->type->entity->get('attributes_access');
-
-    if ($attributes_access === 'administrators') {
-      $grants[':default'] = AccessResult::allowedIfHasPermission($context['account'], 'administer collections');
-    }
-    elseif ($attributes_access === 'owners') {
-      $is_owner = in_array($context['account']->id(), $context['items']->getEntity()->collection->entity->getOwnerIds());
-      $grants[':default'] = AccessResult::allowedIf($is_owner);
-    }
-    else {
-      $grants[':default'] = AccessResult::forbidden();
-    }
-  }
+#[LegacyHook]
+function collection_entity_field_access_alter(array &$grants, array $context)
+{
+    \Drupal::service(CollectionHooks::class)->entityFieldAccessAlter($grants, $context);
 }
 
 /**
@@ -233,33 +129,10 @@ function collection_entity_field_access_alter(array &$grants, array $context) {
  *
  * Add operation links to collection and collection item entities.
  */
-function collection_entity_operation(EntityInterface $entity) {
-  if ($entity->getEntityTypeId() == 'collection_item' && $entity->item->entity->access('update')) {
-    $item_entity_type = ($entity->item->entity instanceof ContentEntityInterface) ? 'content' : 'configuration';
-    $operations = [];
-    $operations['edit_item'] = [
-      'title' => t('Edit @entity_type', ['@entity_type' => $item_entity_type]),
-      'url' => $entity->item->entity->toUrl('edit-form'),
-      'weight' => -1,
-    ];
-    return $operations;
-  }
-
-  if ($entity->getEntityTypeId() === 'collection' && $entity->access('update')) {
-    $operations = [];
-
-    $collection_items_url = Url::fromRoute(
-      'entity.collection_item.collection',
-      ['collection' => $entity->id()]
-    );
-
-    $operations['view_items'] = [
-      'title' => t('Items'),
-      'url' => $collection_items_url,
-      'weight' => -1,
-    ];
-    return $operations;
-  }
+#[LegacyHook]
+function collection_entity_operation(EntityInterface $entity)
+{
+    return \Drupal::service(CollectionHooks::class)->entityOperation($entity);
 }
 
 /**
@@ -267,16 +140,9 @@ function collection_entity_operation(EntityInterface $entity) {
  *
  * Update the edit and delete operations titles for collection items.
  */
+#[LegacyHook]
 function collection_entity_operation_alter(array &$operations, EntityInterface $entity) {
-  if ($entity->getEntityTypeId() == 'collection_item') {
-    if (isset($operations['edit'])) {
-      $operations['edit']['title'] = t('Edit collection item');
-    }
-
-    if (isset($operations['delete'])) {
-      $operations['delete']['title'] = t('Remove from collection');
-    }
-  }
+  \Drupal::service(CollectionHooks::class)->entityOperationAlter($operations, $entity);
 }
 
 /**
@@ -284,10 +150,9 @@ function collection_entity_operation_alter(array &$operations, EntityInterface $
  *
  * Add a handler if there is a preview action.
  */
+#[LegacyHook]
 function collection_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
-  if (isset($form['actions']['preview'])) {
-    $form['actions']['preview']['#submit'][] = 'collection_node_preview_submit';
-  }
+  \Drupal::service(CollectionHooks::class)->formNodeFormAlter($form, $form_state, $form_id);
 }
 
 /**
diff --git a/collection.services.yml b/collection.services.yml
index 2173047..a7f5b4b 100644
--- a/collection.services.yml
+++ b/collection.services.yml
@@ -22,3 +22,11 @@ services:
     class: Drupal\collection\Routing\CollectionRouteSubscriber
     tags:
       - { name: event_subscriber }
+
+  Drupal\collection\Hook\CollectionHooks:
+    class: Drupal\collection\Hook\CollectionHooks
+    autowire: true
+
+  Drupal\collection\Hook\CollectionViewsHooks:
+    class: Drupal\collection\Hook\CollectionViewsHooks
+    autowire: true
diff --git a/collection.views.inc b/collection.views.inc
index ed01815..537db84 100644
--- a/collection.views.inc
+++ b/collection.views.inc
@@ -1,68 +1,16 @@
 <?php
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\collection\Hook\CollectionViewsHooks;
+
 /**
  * Implements hook_views_data().
  *
  * Add fields to the view that mirror the CollectionItemListBuilder.
  * Add a relationship from nodes to canonical collection items.
  */
-function collection_views_data() {
-  $data['collection_item']['entity_type_label'] = [
-    'title' => t('Collected item type'),
-    'field' => [
-      'id' => 'collection_item_collected_item_entity_type_label',
-    ],
-  ];
-
-  $data['collection_item']['status'] = [
-    'title' => t('Item status'),
-    'field' => [
-      'id' => 'collection_item_collected_item_status',
-    ],
-  ];
-
-  $data['collection_item']['state'] = [
-    'title' => t('Item moderation state'),
-    'field' => [
-      'id' => 'collection_item_collected_item_state',
-    ],
-    'help' => t('Requires the Content Moderation module'),
-  ];
-
-  $data['collection_item']['item_latest_version_link'] = [
-    'title' => t('Item latest version link'),
-    'field' => [
-      'id' => 'collection_item_collected_item_latest_version_link',
-    ],
-    'help' => t('Requires the Content Moderation module'),
-  ];
-
-  // This relationship is special because collection items use dynamic entity
-  // references and therefore the joins need to be more specific.
-  // @todo Consider refactoring this to any content entity that can be in a
-  //   collection. @see comment.views.inc.
-  $data['node']['canonical_collection_item'] = [
-    'title' => t('Canonical collection item'),
-    'help' => t('The canonical collection item for this content'),
-    'relationship' => [
-      'group' => t('Collection'),
-      'label' => t('Canonical Collection Item'),
-      'id' => 'standard',
-      'base' => 'collection_item_field_data',
-      'base field' => 'item__target_id_int',
-      'relationship field' => 'nid',
-      'extra' => [
-        [
-          'field' => 'item__target_type',
-          'value' => 'node',
-        ],
-        [
-          'field' => 'canonical',
-          'value' => 1,
-        ],
-      ],
-    ],
-  ];
-
-  return $data;
+#[LegacyHook]
+function collection_views_data()
+{
+    return \Drupal::service(CollectionViewsHooks::class)->viewsData();
 }
diff --git a/modules/collection_pathauto/collection_pathauto.module b/modules/collection_pathauto/collection_pathauto.module
index e0d1280..3e8710b 100644
--- a/modules/collection_pathauto/collection_pathauto.module
+++ b/modules/collection_pathauto/collection_pathauto.module
@@ -1,5 +1,7 @@
 <?php
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\collection_pathauto\Hook\CollectionPathautoHooks;
 use Drupal\Core\Entity\ContentEntityInterface;
 
 /**
@@ -9,28 +11,8 @@ use Drupal\Core\Entity\ContentEntityInterface;
  * path. It also implements an alter hook so that other modules can further
  * modify the alias.
  */
-function collection_pathauto_pathauto_alias_alter(&$alias, array &$context) {
-  $collection_content_manager = \Drupal::service('collection.content_manager');
-  $path_alias_manager = \Drupal::service('path_alias.manager');
-
-  $entity = reset($context['data']);
-
-  if (empty($entity) || !$entity instanceof ContentEntityInterface) {
-    return;
-  }
-
-  foreach ($collection_content_manager->getCollectionItemsForEntity($entity) as $collection_item) {
-    if ($collection_item->isCanonical()) {
-      $collection_url = $path_alias_manager->getAliasByPath('/collection/' . $collection_item->collection->target_id);
-      $context['original_alias'] = $alias;
-      $alias = $collection_url . $alias;
-
-      // Add relevant collection details to the context and allow other modules to
-      // alter the alias.
-      $context['collection_item'] = $collection_item;
-      $context['canonical_collection'] = $collection_item->collection->entity;
-      \Drupal::moduleHandler()->alter('collection_pathauto_alias', $alias, $context);
-      break;
-    }
-  }
+#[LegacyHook]
+function collection_pathauto_pathauto_alias_alter(&$alias, array &$context)
+{
+    \Drupal::service(CollectionPathautoHooks::class)->pathautoAliasAlter($alias, $context);
 }
diff --git a/modules/collection_pathauto/collection_pathauto.services.yml b/modules/collection_pathauto/collection_pathauto.services.yml
index 5077a68..2635090 100644
--- a/modules/collection_pathauto/collection_pathauto.services.yml
+++ b/modules/collection_pathauto/collection_pathauto.services.yml
@@ -4,3 +4,7 @@ services:
     arguments: ['@pathauto.generator']
     tags:
       - { name: event_subscriber }
+
+  Drupal\collection_pathauto\Hook\CollectionPathautoHooks:
+    class: Drupal\collection_pathauto\Hook\CollectionPathautoHooks
+    autowire: true
diff --git a/modules/collection_pathauto/src/Hook/CollectionPathautoHooks.php b/modules/collection_pathauto/src/Hook/CollectionPathautoHooks.php
new file mode 100644
index 0000000..34df9d4
--- /dev/null
+++ b/modules/collection_pathauto/src/Hook/CollectionPathautoHooks.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\collection_pathauto\Hook;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+/**
+ * Hook implementations for collection_pathauto.
+ */
+class CollectionPathautoHooks
+{
+    /**
+     * Implements hook_pathauto_alias_alter().
+     *
+     * Checks for a canonical collection item and prepends the collection url to the
+     * path. It also implements an alter hook so that other modules can further
+     * modify the alias.
+     */
+    #[Hook('pathauto_alias_alter')]
+    public static function pathautoAliasAlter(&$alias, array &$context)
+    {
+        $collection_content_manager = \Drupal::service('collection.content_manager');
+        $path_alias_manager = \Drupal::service('path_alias.manager');
+        $entity = reset($context['data']);
+        if (empty($entity) || !$entity instanceof \Drupal\Core\Entity\ContentEntityInterface) {
+            return;
+        }
+        foreach ($collection_content_manager->getCollectionItemsForEntity($entity) as $collection_item) {
+            if ($collection_item->isCanonical()) {
+                $collection_url = $path_alias_manager->getAliasByPath('/collection/' . $collection_item->collection->target_id);
+                $context['original_alias'] = $alias;
+                $alias = $collection_url . $alias;
+                // Add relevant collection details to the context and allow other modules to
+                // alter the alias.
+                $context['collection_item'] = $collection_item;
+                $context['canonical_collection'] = $collection_item->collection->entity;
+                \Drupal::moduleHandler()->alter('collection_pathauto_alias', $alias, $context);
+                break;
+            }
+        }
+    }
+}
diff --git a/src/CollectionItemListBuilder.php b/src/CollectionItemListBuilder.php
index 436480d..5af5253 100644
--- a/src/CollectionItemListBuilder.php
+++ b/src/CollectionItemListBuilder.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\collection;
 
+use Drupal\Component\Utility\DeprecationHelper;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\EntityPublishedInterface;
@@ -88,7 +89,7 @@ class CollectionItemListBuilder extends BulkFormEntityListBuilder {
     $type = $entity->item->entity->getEntityType()->getLabel();
 
     if ($entity->item->entity->getEntityTypeId() === 'node') {
-      $type .= ': ' . node_get_type_label($entity->item->entity);
+      $type .= ': ' . DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => $entity->item->entity->getBundleEntity()->label(), fn() => node_get_type_label($entity->item->entity));
     }
     elseif ($entity->item->entity->getEntityType()->get('bundle_entity_type') !== NULL) {
       $bundle_key = $entity->item->entity->getEntityType()->getKey('bundle');
diff --git a/src/Hook/CollectionHooks.php b/src/Hook/CollectionHooks.php
new file mode 100644
index 0000000..59f7293
--- /dev/null
+++ b/src/Hook/CollectionHooks.php
@@ -0,0 +1,301 @@
+<?php
+
+namespace Drupal\collection\Hook;
+
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\ContentEntityForm;
+use Drupal\Core\Field\Entity\BaseFieldOverride;
+use Drupal\Core\Url;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+/**
+ * Hook implementations for collection.
+ */
+class CollectionHooks
+{
+    use StringTranslationTrait;
+    /**
+     * Implements hook_help().
+     */
+    #[Hook('help')]
+    public function help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match)
+    {
+        switch ($route_name) {
+            // Main module help for the collection module.
+            case 'help.page.collection':
+                $output = '';
+                $output .= '<h3>' . $this->t('About') . '</h3>';
+                $output .= '<p>' . $this->t('Allows collections of content and configuration entities.') . '</p>';
+                return $output;
+            default:
+        }
+    }
+    /**
+     * Implements hook_toolbar_alter().
+     */
+    #[Hook('toolbar_alter')]
+    public static function toolbarAlter(&$items)
+    {
+        $items['administration']['#attached']['library'][] = 'collection/toolbar';
+    }
+    /**
+     * Implements hook_theme().
+     */
+    #[Hook('theme')]
+    public static function theme()
+    {
+        $theme = [
+        ];
+        $theme['collection'] = [
+            'render element' => 'elements',
+            'file' => 'collection.page.inc',
+            'template' => 'collection',
+        ];
+        $theme['collection_content_add_list'] = [
+            'render element' => 'content',
+            'variables' => [
+                'content' => NULL,
+            ],
+            'file' => 'collection.page.inc',
+        ];
+        return $theme;
+    }
+    /**
+     * Implements hook_entity_bundle_field_info().
+     *
+     * Update the settings for the collection_item `item` field from collection item
+     * type configuration.
+     *
+     * The dynamic entity reference field on collection items uses these settings on
+     * the form widget.
+     *
+     * @see Drupal\collection\Form\CollectionItemTypeForm
+     */
+    #[Hook('entity_bundle_field_info')]
+    public static function entityBundleFieldInfo(\Drupal\Core\Entity\EntityTypeInterface $entity_type, $bundle, array $base_field_definitions)
+    {
+        $fields = [
+        ];
+        // Apply custom settings to different collection item types.
+        if ($entity_type->id() === 'collection_item') {
+            $collection_item_type_storage = \Drupal::service('entity_type.manager')->getStorage('collection_item_type');
+            // Get the `allowed_bundles` setting for this collection item type. They
+            // will be an array of items in the form `entity_type_id.bundle` (e.g.
+            // `node.article`).
+            $collection_item_type = $collection_item_type_storage->load($bundle);
+            $entity_bundles = [
+            ];
+            // Place the allowed bundles in a multidimensional array for later use.
+            foreach ($collection_item_type->getAllowedBundles() as $entity_and_bundle) {
+                list($entity_type_id, $bundle_name) = explode('.', $entity_and_bundle);
+                $entity_bundles[$entity_type_id][$bundle_name] = $bundle_name;
+            }
+            // Load the item field for this collection item bundle.
+            $fields['item'] = \Drupal\Core\Field\Entity\BaseFieldOverride::createFromBaseFieldDefinition($base_field_definitions['item'], $bundle);
+            // Append any non-node entity types to the `entity_type_ids` setting.
+            $entity_type_ids_setting = $fields['item']->getSetting('entity_type_ids');
+            $fields['item']->setSetting('entity_type_ids', $entity_type_ids_setting + array_combine(array_keys($entity_bundles), array_keys($entity_bundles)));
+            // Set config for the allowed entity types, especially the `target_bundles`,
+            // for this collection item type.
+            foreach ($entity_bundles as $entity_type_id => $bundles) {
+                $fields['item']->setSetting($entity_type_id, [
+                    'handler' => 'default:' . $entity_type_id,
+                    'handler_settings' => [
+                        'target_bundles' => $bundles,
+                    ],
+                ]);
+            }
+        }
+        return $fields;
+    }
+    /**
+     * Implements hook_form_BASE_FORM_ID_alter().
+     */
+    #[Hook('form_collection_form_alter')]
+    public function formCollectionFormAlter(&$form, \Drupal\Core\Form\FormStateInterface $form_state)
+    {
+        $form['settings'] = [
+            '#type' => 'details',
+            '#title' => $this->t('Settings'),
+            '#group' => 'advanced',
+            '#weight' => 50,
+            '#optional' => TRUE,
+            '#open' => FALSE,
+        ];
+        $form['collectible']['#group'] = 'settings';
+        $form['user_id']['#group'] = 'settings';
+    }
+    /**
+     * Implements hook_theme_suggestions_HOOK().
+     */
+    #[Hook('theme_suggestions_collection')]
+    public static function themeSuggestionsCollection(array $variables)
+    {
+        $suggestions = [
+        ];
+        $entity = $variables['elements']['#collection'];
+        $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
+        $suggestions[] = 'collection__' . $sanitized_view_mode;
+        $suggestions[] = 'collection__' . $entity->bundle();
+        $suggestions[] = 'collection__' . $entity->bundle() . '__' . $sanitized_view_mode;
+        $suggestions[] = 'collection__' . $entity->id();
+        $suggestions[] = 'collection__' . $entity->id() . '__' . $sanitized_view_mode;
+        return $suggestions;
+    }
+    /**
+     * Implements hook_entity_update().
+     *
+     * Update the name of collection items when an entity is modified.
+     */
+    #[Hook('entity_update')]
+    public static function entityUpdate(\Drupal\Core\Entity\EntityInterface $entity)
+    {
+        // Don't run on collection_items or paragraphs.
+        if (in_array($entity->getEntityTypeId(), [
+            'collection_item',
+            'paragraph',
+        ])) {
+            return;
+        }
+        // Don't run if entity label is unchanged.
+        if ($entity->label() === \Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => $entity->getOriginal(), fn() => $entity->original)->label()) {
+            return;
+        }
+        $item_storage = \Drupal::service('entity_type.manager')->getStorage('collection_item');
+        $collection_items = $item_storage->loadByProperties([
+            'item__target_id' => $entity->id(),
+            'item__target_type' => $entity->getEntityTypeId(),
+        ]);
+        foreach ($collection_items as $collection_item) {
+            // This should trigger CollectionItem::preSave() and update the
+            // collection_item label.
+            $collection_item->save();
+        }
+    }
+    /**
+     * Implements hook_entity_predelete().
+     *
+     * Delete any corresponding collection items when an entity is deleted.
+     */
+    #[Hook('entity_predelete')]
+    public static function entityPredelete(\Drupal\Core\Entity\EntityInterface $entity)
+    {
+        $item_storage = \Drupal::service('entity_type.manager')->getStorage('collection_item');
+        $collection_items = $item_storage->loadByProperties([
+            'item__target_id' => $entity->id(),
+            'item__target_type' => $entity->getEntityTypeId(),
+        ]);
+        foreach ($collection_items as $collection_item) {
+            $collection_item->delete();
+        }
+    }
+    /**
+     * Implements hook_entity_extra_field_info().
+     */
+    #[Hook('entity_extra_field_info')]
+    public static function entityExtraFieldInfo()
+    {
+        return \Drupal::service('collection.content_entity_form.alter')->getExtraFieldInfo();
+    }
+    /**
+     * Implements hook_form_alter().
+     */
+    #[Hook('form_alter')]
+    public static function formAlter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id)
+    {
+        \Drupal::service('collection.content_entity_form.alter')->alterForm($form, $form_state, $form_id);
+    }
+    /**
+     * Implements hook_entity_field_access_alter().
+     */
+    #[Hook('entity_field_access_alter')]
+    public static function entityFieldAccessAlter(array &$grants, array $context)
+    {
+        // Prevent change of collected item reference when editing a collection item.
+        if ($context['field_definition']->getTargetEntityTypeId() === 'collection_item' && $context['field_definition']->getName() === 'item' && $context['operation'] === 'edit') {
+            $grants[':default'] = \Drupal\Core\Access\AccessResult::allowedIf($context['items']->getEntity()->isNew());
+        }
+        // Restrict access to collection item attributes.
+        if ($context['field_definition']->getTargetEntityTypeId() === 'collection_item' && $context['field_definition']->getName() === 'attributes' && $context['operation'] === 'edit') {
+            $attributes_access = $context['items']->getEntity()->type->entity->get('attributes_access');
+            if ($attributes_access === 'administrators') {
+                $grants[':default'] = \Drupal\Core\Access\AccessResult::allowedIfHasPermission($context['account'], 'administer collections');
+            } elseif ($attributes_access === 'owners') {
+                $is_owner = in_array($context['account']->id(), $context['items']->getEntity()->collection->entity->getOwnerIds());
+                $grants[':default'] = \Drupal\Core\Access\AccessResult::allowedIf($is_owner);
+            } else {
+                $grants[':default'] = \Drupal\Core\Access\AccessResult::forbidden();
+            }
+        }
+    }
+    /**
+     * Implements hook_entity_operation().
+     *
+     * Add operation links to collection and collection item entities.
+     */
+    #[Hook('entity_operation')]
+    public function entityOperation(\Drupal\Core\Entity\EntityInterface $entity)
+    {
+        if ($entity->getEntityTypeId() == 'collection_item' && $entity->item->entity->access('update')) {
+            $item_entity_type = $entity->item->entity instanceof \Drupal\Core\Entity\ContentEntityInterface ? 'content' : 'configuration';
+            $operations = [
+            ];
+            $operations['edit_item'] = [
+                'title' => $this->t('Edit @entity_type', [
+                    '@entity_type' => $item_entity_type,
+                ]),
+                'url' => $entity->item->entity->toUrl('edit-form'),
+                'weight' => -1,
+            ];
+            return $operations;
+        }
+        if ($entity->getEntityTypeId() === 'collection' && $entity->access('update')) {
+            $operations = [
+            ];
+            $collection_items_url = \Drupal\Core\Url::fromRoute('entity.collection_item.collection', [
+                'collection' => $entity->id(),
+            ]);
+            $operations['view_items'] = [
+                'title' => $this->t('Items'),
+                'url' => $collection_items_url,
+                'weight' => -1,
+            ];
+            return $operations;
+        }
+    }
+    /**
+     * Implements hook_entity_operation_alter().
+     *
+     * Update the edit and delete operations titles for collection items.
+     */
+    #[Hook('entity_operation_alter')]
+    public function entityOperationAlter(array &$operations, \Drupal\Core\Entity\EntityInterface $entity)
+    {
+        if ($entity->getEntityTypeId() == 'collection_item') {
+            if (isset($operations['edit'])) {
+                $operations['edit']['title'] = $this->t('Edit collection item');
+            }
+            if (isset($operations['delete'])) {
+                $operations['delete']['title'] = $this->t('Remove from collection');
+            }
+        }
+    }
+    /**
+     * Implements hook_form_BASE_FORM_ID_alter().
+     *
+     * Add a handler if there is a preview action.
+     */
+    #[Hook('form_node_form_alter')]
+    public static function formNodeFormAlter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id)
+    {
+        if (isset($form['actions']['preview'])) {
+            $form['actions']['preview']['#submit'][] = 'collection_node_preview_submit';
+        }
+    }
+}
diff --git a/src/Hook/CollectionViewsHooks.php b/src/Hook/CollectionViewsHooks.php
new file mode 100644
index 0000000..01a4ee9
--- /dev/null
+++ b/src/Hook/CollectionViewsHooks.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\collection\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+/**
+ * Hook implementations for collection.
+ */
+class CollectionViewsHooks
+{
+    use StringTranslationTrait;
+    /**
+     * Implements hook_views_data().
+     *
+     * Add fields to the view that mirror the CollectionItemListBuilder.
+     * Add a relationship from nodes to canonical collection items.
+     */
+    #[Hook('views_data')]
+    public function viewsData()
+    {
+        $data['collection_item']['entity_type_label'] = [
+            'title' => $this->t('Collected item type'),
+            'field' => [
+                'id' => 'collection_item_collected_item_entity_type_label',
+            ],
+        ];
+        $data['collection_item']['status'] = [
+            'title' => $this->t('Item status'),
+            'field' => [
+                'id' => 'collection_item_collected_item_status',
+            ],
+        ];
+        $data['collection_item']['state'] = [
+            'title' => $this->t('Item moderation state'),
+            'field' => [
+                'id' => 'collection_item_collected_item_state',
+            ],
+            'help' => $this->t('Requires the Content Moderation module'),
+        ];
+        $data['collection_item']['item_latest_version_link'] = [
+            'title' => $this->t('Item latest version link'),
+            'field' => [
+                'id' => 'collection_item_collected_item_latest_version_link',
+            ],
+            'help' => $this->t('Requires the Content Moderation module'),
+        ];
+        // This relationship is special because collection items use dynamic entity
+        // references and therefore the joins need to be more specific.
+        // @todo Consider refactoring this to any content entity that can be in a
+        //   collection. @see comment.views.inc.
+        $data['node']['canonical_collection_item'] = [
+            'title' => $this->t('Canonical collection item'),
+            'help' => $this->t('The canonical collection item for this content'),
+            'relationship' => [
+                'group' => $this->t('Collection'),
+                'label' => $this->t('Canonical Collection Item'),
+                'id' => 'standard',
+                'base' => 'collection_item_field_data',
+                'base field' => 'item__target_id_int',
+                'relationship field' => 'nid',
+                'extra' => [
+                    [
+                        'field' => 'item__target_type',
+                        'value' => 'node',
+                    ],
+                    [
+                        'field' => 'canonical',
+                        'value' => 1,
+                    ],
+                ],
+            ],
+        ];
+        return $data;
+    }
+}
diff --git a/src/Plugin/Validation/Constraint/PreventSelfValidator.php b/src/Plugin/Validation/Constraint/PreventSelfValidator.php
index b0eca7c..1ec00c7 100644
--- a/src/Plugin/Validation/Constraint/PreventSelfValidator.php
+++ b/src/Plugin/Validation/Constraint/PreventSelfValidator.php
@@ -13,7 +13,7 @@ class PreventSelfValidator extends ConstraintValidator {
   /**
    * {@inheritdoc}
    */
-  public function validate($collection_item, Constraint $constraint) {
+  public function validate(mixed $collection_item, Constraint $constraint): void {
     if ($collection_item->collection->entity === $collection_item->item->entity) {
       $this->context->addViolation($constraint->inception, [
         '%entity' => $collection_item->item->entity->label(),
diff --git a/src/Plugin/Validation/Constraint/PreventUncollectibleValidator.php b/src/Plugin/Validation/Constraint/PreventUncollectibleValidator.php
index e28f4ac..5a9ecaf 100644
--- a/src/Plugin/Validation/Constraint/PreventUncollectibleValidator.php
+++ b/src/Plugin/Validation/Constraint/PreventUncollectibleValidator.php
@@ -13,7 +13,7 @@ class PreventUncollectibleValidator extends ConstraintValidator {
   /**
    * {@inheritdoc}
    */
-  public function validate($collection_item, Constraint $constraint) {
+  public function validate(mixed $collection_item, Constraint $constraint): void {
     if ($collection_item->item->entity->hasField('collectible') === FALSE) {
       return;
     }
diff --git a/src/Plugin/Validation/Constraint/SingleCanonicalItemValidator.php b/src/Plugin/Validation/Constraint/SingleCanonicalItemValidator.php
index 92d42a1..f54b23b 100644
--- a/src/Plugin/Validation/Constraint/SingleCanonicalItemValidator.php
+++ b/src/Plugin/Validation/Constraint/SingleCanonicalItemValidator.php
@@ -29,7 +29,7 @@ class SingleCanonicalItemValidator extends ConstraintValidator implements Contai
   /**
    * {@inheritdoc}
    */
-  public function validate($collection_item, Constraint $constraint) {
+  public function validate(mixed $collection_item, Constraint $constraint): void {
     // Ensure that we only validate collection items that are set to canonical.
     if (!$collection_item->canonical->value) {
       return;
diff --git a/src/Plugin/Validation/Constraint/UniqueItemValidator.php b/src/Plugin/Validation/Constraint/UniqueItemValidator.php
index 656a802..a8ea8ec 100644
--- a/src/Plugin/Validation/Constraint/UniqueItemValidator.php
+++ b/src/Plugin/Validation/Constraint/UniqueItemValidator.php
@@ -13,7 +13,7 @@ class UniqueItemValidator extends ConstraintValidator {
   /**
    * {@inheritdoc}
    */
-  public function validate($collection_item, Constraint $constraint) {
+  public function validate(mixed $collection_item, Constraint $constraint): void {
     /** @var Drupal\collection\Entity\CollectionItemInterface $collection_item */
     $collection = $collection_item->collection->entity;
 
diff --git a/src/Plugin/views/field/CollectionItemCollectedItemEntityTypeLabel.php b/src/Plugin/views/field/CollectionItemCollectedItemEntityTypeLabel.php
index 6d94784..ecb7dfa 100644
--- a/src/Plugin/views/field/CollectionItemCollectedItemEntityTypeLabel.php
+++ b/src/Plugin/views/field/CollectionItemCollectedItemEntityTypeLabel.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\collection\Plugin\views\field;
 
+use Drupal\Component\Utility\DeprecationHelper;
 use Drupal\views\ResultRow;
 use Drupal\views\Plugin\views\field\FieldPluginBase;
 
@@ -25,7 +26,7 @@ class CollectionItemCollectedItemEntityTypeLabel extends FieldPluginBase {
       $entity_type_label = $collection_item->item->entity->getEntityType()->getLabel();
 
       if ($collection_item->item->entity->getEntityTypeId() === 'node') {
-        $entity_type_label .= ': ' . node_get_type_label($collection_item->item->entity);
+        $entity_type_label .= ': ' . DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => $collection_item->item->entity->getBundleEntity()->label(), fn() => node_get_type_label($collection_item->item->entity));
       }
       elseif ($collection_item->item->entity->getEntityType()->get('bundle_entity_type') !== NULL) {
         $bundle_key = $collection_item->item->entity->getEntityType()->getKey('bundle');
diff --git a/tests/src/Functional/LoadTest.php b/tests/src/Functional/LoadTest.php
index a54c3c9..afa0b48 100644
--- a/tests/src/Functional/LoadTest.php
+++ b/tests/src/Functional/LoadTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\collection\Functional;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\Core\Url;
 use Drupal\Tests\BrowserTestBase;
 
@@ -10,6 +12,8 @@ use Drupal\Tests\BrowserTestBase;
  *
  * @group collection
  */
+#[Group('collection')]
+#[RunTestsInSeparateProcesses]
 class LoadTest extends BrowserTestBase {
 
   /**
