diff --git a/cloudinary.module b/cloudinary.module
index b69b53b..0c2ad15 100644
--- a/cloudinary.module
+++ b/cloudinary.module
@@ -8,7 +8,8 @@
 /**
  * Sample file on cloudinary for image effect preview.
  */
-
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\cloudinary\Hook\CloudinaryHooks;
 use Cloudinary\Transformation\Transformation;
 use Drupal\image\Entity\ImageStyle;
 
@@ -30,14 +31,9 @@ define('CLOUDINARY_VISIBLE_STATES_CROP', 'fill,crop,thumb,pad,lfill,lpad,mpad');
 /**
  * Implements hook_theme().
  */
+#[LegacyHook]
 function cloudinary_theme() {
-  return [
-    'cloudinary_crop_summary' => [
-      'variables' => [
-        'data' => NULL,
-      ],
-    ],
-  ];
+  return \Drupal::service(CloudinaryHooks::class)->theme();
 }
 
 /**
@@ -63,13 +59,9 @@ function theme_cloudinary_crop_summary($variables) {
 /**
  * Implements hook_theme_registry_alter().
  */
+#[LegacyHook]
 function cloudinary_theme_registry_alter(&$theme_registry) {
-  foreach ($theme_registry['image_style_preview']['preprocess functions'] as &$function) {
-    if ($function == 'template_preprocess_image_style_preview') {
-      $function = 'cloudinary_preprocess_image_style_preview';
-      break;
-    }
-  }
+  \Drupal::service(CloudinaryHooks::class)->themeRegistryAlter($theme_registry);
 }
 
 /**
@@ -179,51 +171,9 @@ function cloudinary_preprocess_image_style_preview(&$variables) {
 /**
  * Implements hook_cloudinary_stream_wrapper_transformation().
  */
+#[LegacyHook]
 function cloudinary_cloudinary_stream_wrapper_transformation() {
-  $path = \Drupal::service('extension.list.module')->getPath('cloudinary');
-
-  return [
-    'cloudinary_crop' => [
-      'title' => t('Cloudinary crop'),
-      'callback' => 'cloudinary_transformation_cloudinary_crop',
-      'file' => $path . '/includes/cloudinary.transformation.cloudinary.inc',
-    ],
-    'cloudinary_named_transformation' => [
-      'title' => t('Cloudinary named transformation'),
-      'callback' => 'cloudinary_transformation_cloudinary_named_transformation',
-      'file' => $path . '/includes/cloudinary.transformation.cloudinary.inc',
-    ],
-    'image_crop' => [
-      'title' => t('Crop'),
-      'callback' => 'cloudinary_transformation_image_crop',
-      'file' => $path . '/includes/cloudinary.transformation.drupal.inc',
-    ],
-    'image_desaturate' => [
-      'title' => t('Desaturate'),
-      'callback' => 'cloudinary_transformation_image_desaturate',
-    ],
-    'image_resize' => [
-      'title' => t('Resize'),
-      'callback' => 'cloudinary_transformation_image_resize',
-    ],
-    'image_rotate' => [
-      'title' => t('Rotate'),
-      'callback' => 'cloudinary_transformation_image_rotate',
-    ],
-    'image_scale' => [
-      'title' => t('Scale'),
-      'callback' => 'cloudinary_transformation_image_scale',
-    ],
-    'image_scale_and_crop' => [
-      'title' => t('Scale and crop'),
-      'callback' => 'cloudinary_transformation_image_scale_and_crop',
-    ],
-    'focal_point_scale_and_crop' => [
-      'title' => t('Focal Point Scale and Crop'),
-      'callback' => 'cloudinary_transformation_image_scale_and_crop',
-      'file' => $path . '/includes/cloudinary.transformation.drupal.inc',
-    ],
-  ];
+  return \Drupal::service(CloudinaryHooks::class)->cloudinaryStreamWrapperTransformation();
 }
 
 /**
diff --git a/cloudinary.services.yml b/cloudinary.services.yml
new file mode 100644
index 0000000..eab2978
--- /dev/null
+++ b/cloudinary.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\cloudinary\Hook\CloudinaryHooks:
+    class: Drupal\cloudinary\Hook\CloudinaryHooks
+    autowire: true
diff --git a/modules/cloudinary_media_library_widget/cloudinary_media_library_widget.module b/modules/cloudinary_media_library_widget/cloudinary_media_library_widget.module
index 3469786..3b236bd 100644
--- a/modules/cloudinary_media_library_widget/cloudinary_media_library_widget.module
+++ b/modules/cloudinary_media_library_widget/cloudinary_media_library_widget.module
@@ -5,16 +5,10 @@
  * Main module file.
  */
 
-use Cloudinary\Api\Admin\AdminApi;
-use Cloudinary\Configuration\Configuration;
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\cloudinary_media_library_widget\Hook\CloudinaryMediaLibraryWidgetHooks;
 use Drupal\cloudinary_media_library_widget\Service\CloudinaryMediaWidgetHelper;
-use Drupal\cloudinary_media_library_widget\Plugin\Field\FieldType\CloudinaryImage;
-use Drupal\cloudinary_sdk\Service\CloudinaryAssetHelper;
-use Drupal\cloudinary_stream_wrapper\StreamWrapper\CloudinaryStreamWrapper;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
-use Drupal\file\FileInterface;
 use Drupal\media\MediaInterface;
 
 /**
@@ -24,129 +18,9 @@ use Drupal\media\MediaInterface;
  *
  * @see cloudinary_sdk_settings()
  */
+#[LegacyHook]
 function cloudinary_media_library_widget_form_cloudinary_sdk_settings_alter(&$form, $form_state) {
-  $config = \Drupal::config('cloudinary_media_library_widget.settings');
-  $form['widget'] = [
-    '#type' => 'fieldset',
-    '#title' => t('Media library widget settings'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-  ];
-
-  $form['widget']['cloudinary_saml_auth'] = [
-    '#type' => 'checkbox',
-    '#title' => t('SAML Authentication'),
-    '#default_value' => $config->get('cloudinary_saml_auth'),
-    '#description' => t('Whether to perform automatic SSO login via SAML for the specified username. This parameter is only needed if the "Enforce SAML login" option is disabled for your account.'),
-  ];
-
-  try {
-    $api = new AdminApi();
-    $root_folders = $api->rootFolders();
-    $folders = [];
-    foreach ($root_folders['folders'] as $folder) {
-      $folders[$folder['path']] = t('Folder: /@path', [
-        '@path' => $folder['path'],
-      ]);
-    }
-  }
-  catch (\Exception $e) {
-    $folders = [];
-  }
-
-  $form['widget']['cloudinary_starting_folder'] = [
-    '#type' => 'select',
-    '#title' => t('Starting folder'),
-    '#options' => $folders,
-    '#empty_value' => '/',
-    '#empty_option' => t('Root'),
-    '#default_value' => $config->get('cloudinary_starting_folder'),
-    '#description' => t('Instructs the widget to open in browse mode, displaying the contents of the specified folder.'),
-  ];
-
-  $form['metadata'] = [
-    '#type' => 'fieldset',
-    '#title' => t('Metadata attributes'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-  ];
-
-  $title_options = [
-    'context.custom.caption' => t('Contextual metadata: Title (caption)'),
-  ];
-  $description_options = [
-    'context.custom.alt' => t('Contextual metadata: Description (alt)'),
-  ];
-
-  try {
-    Configuration::instance()->validate();
-
-    // Fetch list of custom metadata fields.
-    $fields = CloudinaryAssetHelper::cloudinary()->adminApi()->listMetadataFields();
-    foreach ($fields->offsetGet('metadata_fields') as $metadata) {
-      if ($metadata['type'] === 'string') {
-        $title_options[$metadata['external_id']] = $metadata['label'];
-        $description_options[$metadata['external_id']] = $metadata['label'];
-      }
-    }
-  }
-  catch (\Exception $e) {
-    $form['metadata']['#description'] = t('Please set up API connection and save the form to see list of all metadata including structure one.');
-  }
-
-  $form['metadata']['cloudinary_attribute_default_title'] = [
-    '#title' => t('Default title (caption) attribute'),
-    '#type' => 'select',
-    '#description' => t('Use this attribute to automatically populate title attribute for an image and name for a media.'),
-    '#options' => $title_options,
-    '#default_value' => $config->get('cloudinary_attribute_default_title'),
-  ];
-
-  $form['metadata']['cloudinary_attribute_default_description'] = [
-    '#title' => t('Default description (alt) attribute'),
-    '#type' => 'select',
-    '#description' => t('Use this attribute to automatically populate "alt text" for an image and "description" for a video/document.'),
-    '#options' => $description_options,
-    '#default_value' => $config->get('cloudinary_attribute_default_description'),
-  ];
-
-  $form['optimizations'] = [
-    '#type' => 'fieldset',
-    '#title' => t('Optimizations'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-  ];
-
-  $image_optimizations = $config->get('cloudinary_image_optimizations') ?? [];
-  $form['optimizations']['cloudinary_image_optimizations'] = [
-    '#type' => 'textarea',
-    '#title' => t('Default optimizations for image'),
-    '#default_value' => $image_optimizations,
-    '#description' => t('Represents a different component (separated by a "/"), for example: c_scale,w_400/f_auto/q_auto. Check official @docs to get more details.', [
-      '@docs' => Link::fromTextAndUrl(
-        t('docs'),
-        Url::fromUri('https://cloudinary.com/documentation/image_transformations')->setOption('attributes', ['target' => '_blank'])
-      )->toString(),
-    ]),
-    '#rows' => 2,
-    '#weight' => 1,
-  ];
-
-  $breakpoints = $config->get('cloudinary_responsive_breakpoints') ?? [];
-  $form['optimizations']['cloudinary_responsive_breakpoints'] = [
-    '#type' => 'textarea',
-    '#title' => t('Responsive breakpoints'),
-    '#default_value' => CloudinaryMediaWidgetHelper::convertArrayOptionsToString($breakpoints),
-    '#description' => t('Enter one value per line or separate it by comma. Check official @docs to get more details.', [
-      '@docs' => Link::fromTextAndUrl(
-        t('docs'),
-        Url::fromUri('https://cloudinary.com/documentation/responsive_images#overriding_default_values')->setOption('attributes', ['target' => '_blank'])
-      )->toString(),
-    ]),
-    '#weight' => 3,
-  ];
-
-  $form['#submit'][] = 'cloudinary_media_library_widget_settings_submit';
+  \Drupal::service(CloudinaryMediaLibraryWidgetHooks::class)->formCloudinarySdkSettingsAlter($form, $form_state);
 }
 
 /**
@@ -181,16 +55,9 @@ function cloudinary_media_library_widget_settings_submit($form, FormStateInterfa
 /**
  * Implements hook_theme().
  */
+#[LegacyHook]
 function cloudinary_media_library_widget_theme() {
-  return [
-    'cloudinary_image_formatter' => [
-      'variables' => [
-        'item' => NULL,
-        'item_attributes' => NULL,
-        'url' => NULL,
-      ],
-    ],
-  ];
+  return \Drupal::service(CloudinaryMediaLibraryWidgetHooks::class)->theme();
 }
 
 /**
@@ -198,20 +65,9 @@ function cloudinary_media_library_widget_theme() {
  *
  * Apply custom transformation for default image formatter.
  */
+#[LegacyHook]
 function cloudinary_media_library_widget_preprocess_image_formatter(&$variables) {
-  $item = $variables['item'];
-
-  if ($item instanceof CloudinaryImage) {
-    /** @var \Drupal\cloudinary_media_library_widget\Service\CloudinaryFileHelperInterface $file_helper */
-    $file_helper = \Drupal::service('cloudinary_media_library_widget.file_helper');
-
-    /** @var \Drupal\cloudinary_sdk\Service\AssetHelperInterface $asset_helper */
-    $asset_helper = \Drupal::service('cloudinary_sdk.asset_helper');
-
-    $info = $item->getValue();
-    $value = $asset_helper->generateStringValue($info);
-    $variables['image']['#uri'] = $file_helper->generateUriForFileWithTransformation($value);
-  }
+  \Drupal::service(CloudinaryMediaLibraryWidgetHooks::class)->preprocessImageFormatter($variables);
 }
 
 /**
@@ -219,20 +75,9 @@ function cloudinary_media_library_widget_preprocess_image_formatter(&$variables)
  *
  * Apply custom transformation for responsive image formatter.
  */
+#[LegacyHook]
 function cloudinary_media_library_widget_preprocess_responsive_image_formatter(&$variables) {
-  $item = $variables['item'];
-
-  if ($item instanceof CloudinaryImage) {
-    /** @var \Drupal\cloudinary_media_library_widget\Service\CloudinaryFileHelperInterface $file_helper */
-    $file_helper = \Drupal::service('cloudinary_media_library_widget.file_helper');
-
-    /** @var \Drupal\cloudinary_sdk\Service\AssetHelperInterface $asset_helper */
-    $asset_helper = \Drupal::service('cloudinary_sdk.asset_helper');
-
-    $info = $item->getValue();
-    $value = $asset_helper->generateStringValue($info);
-    $variables['responsive_image']['#uri'] = $file_helper->generateUriForFileWithTransformation($value);
-  }
+  \Drupal::service(CloudinaryMediaLibraryWidgetHooks::class)->preprocessResponsiveImageFormatter($variables);
 }
 
 /**
@@ -240,68 +85,17 @@ function cloudinary_media_library_widget_preprocess_responsive_image_formatter(&
  *
  * Apply custom transformation for drimage formatter and improve performance.
  */
+#[LegacyHook]
 function cloudinary_media_library_widget_preprocess_drimage_formatter(&$variables) {
-  $item = $variables['item'];
-
-  // Properly handle aspect ratio behaviour, so it well integrated with
-  // cloudinary behaviour.
-  if ($item->entity instanceof FileInterface) {
-    $uri = $item->entity->getFileUri();
-
-    [$scheme] = explode('://', $uri, 2);
-
-    if ($scheme !== 'cloudinary') {
-      return;
-    }
-
-    $info = CloudinaryStreamWrapper::parseUri($uri);
-
-    // Fix ration and add a placeholder to be replaced in htaccess by drimage
-    // width and height. See README.md for more details about htaccess changes.
-    $ratio = $variables['width'] / $variables['height'];
-    $info['transformation'] = implode('/', array_filter([
-      $info['transformation'],
-      "ar_{$ratio},c_crop",
-      "%replaceme%",
-    ]));
-
-    /** @var \Drupal\cloudinary_media_library_widget\Service\CloudinaryFileHelperInterface $file_helper */
-    $file_helper = \Drupal::service('cloudinary_media_library_widget.file_helper');
-
-    /** @var \Drupal\cloudinary_sdk\Service\AssetHelperInterface $asset_helper */
-    $asset_helper = \Drupal::service('cloudinary_sdk.asset_helper');
-
-    $value = $asset_helper->generateStringValue($info);
-    $uri = $file_helper->generateUriForExternalUrl($value);
-    $variables['data']['original_source'] = \Drupal::service('file_url_generator')->generateAbsoluteString($uri);
-  }
+  \Drupal::service(CloudinaryMediaLibraryWidgetHooks::class)->preprocessDrimageFormatter($variables);
 }
 
 /**
  * Implements hook_field_formatter_info_alter().
  */
+#[LegacyHook]
 function cloudinary_media_library_widget_field_formatter_info_alter(array &$info) {
-  // Support contrib/core image formatters for custom cloudinary field type.
-  $field_formatters = [
-    'cloudinary_image' => [
-      'image',
-      'responsive_image',
-      'drimage',
-    ],
-    'cloudinary_raw' => [
-      'file_default',
-      'file_url_plain',
-      'file_table',
-    ],
-  ];
-
-  foreach ($field_formatters as $field_type => $formatters) {
-    foreach ($formatters as $field_formatter) {
-      if (isset($info[$field_formatter])) {
-        $info[$field_formatter]['field_types'][] = $field_type;
-      }
-    }
-  }
+  \Drupal::service(CloudinaryMediaLibraryWidgetHooks::class)->fieldFormatterInfoAlter($info);
 }
 
 /**
@@ -309,33 +103,7 @@ function cloudinary_media_library_widget_field_formatter_info_alter(array &$info
  *
  * Force updating media thumbnail when uri changes.
  */
+#[LegacyHook]
 function cloudinary_media_library_widget_media_update(MediaInterface $entity) {
-  if ($entity->getSource()->getPluginId() === 'cloudinary' && isset($entity->original)) {
-    $new_uri = $entity->getSource()->getMetadata($entity, 'thumbnail_uri');
-    $old_uri = $entity->getSource()->getMetadata($entity->original, 'thumbnail_uri');
-
-    if ($new_uri === $old_uri) {
-      return;
-    }
-
-    $values = [
-      'uri' => $new_uri,
-    ];
-
-    $file_storage = \Drupal::entityTypeManager()->getStorage('file');
-    if ($existing = $file_storage->loadByProperties($values)) {
-      $file = reset($existing);
-    }
-    else {
-      /** @var \Drupal\file\FileInterface $file */
-      $file = $file_storage->create($values);
-      if ($owner = $entity->getOwner()) {
-        $file->setOwner($owner);
-      }
-      $file->setPermanent();
-      $file->save();
-    }
-
-    $entity->thumbnail->target_id = $file->id();
-  }
+  \Drupal::service(CloudinaryMediaLibraryWidgetHooks::class)->mediaUpdate($entity);
 }
diff --git a/modules/cloudinary_media_library_widget/cloudinary_media_library_widget.services.yml b/modules/cloudinary_media_library_widget/cloudinary_media_library_widget.services.yml
index ab1aeee..55170bc 100644
--- a/modules/cloudinary_media_library_widget/cloudinary_media_library_widget.services.yml
+++ b/modules/cloudinary_media_library_widget/cloudinary_media_library_widget.services.yml
@@ -23,3 +23,7 @@ services:
       - '@entity_type.manager'
       - '@current_user'
 
+
+  Drupal\cloudinary_media_library_widget\Hook\CloudinaryMediaLibraryWidgetHooks:
+    class: Drupal\cloudinary_media_library_widget\Hook\CloudinaryMediaLibraryWidgetHooks
+    autowire: true
diff --git a/modules/cloudinary_media_library_widget/src/Hook/CloudinaryMediaLibraryWidgetHooks.php b/modules/cloudinary_media_library_widget/src/Hook/CloudinaryMediaLibraryWidgetHooks.php
new file mode 100644
index 0000000..783f0d7
--- /dev/null
+++ b/modules/cloudinary_media_library_widget/src/Hook/CloudinaryMediaLibraryWidgetHooks.php
@@ -0,0 +1,292 @@
+<?php
+
+namespace Drupal\cloudinary_media_library_widget\Hook;
+
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\media\MediaInterface;
+use Drupal\cloudinary_stream_wrapper\StreamWrapper\CloudinaryStreamWrapper;
+use Drupal\file\FileInterface;
+use Drupal\cloudinary_media_library_widget\Plugin\Field\FieldType\CloudinaryImage;
+use Drupal\cloudinary_media_library_widget\Service\CloudinaryMediaWidgetHelper;
+use Drupal\Core\Url;
+use Drupal\Core\Link;
+use Drupal\cloudinary_sdk\Service\CloudinaryAssetHelper;
+use Cloudinary\Configuration\Configuration;
+use Cloudinary\Api\Admin\AdminApi;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for cloudinary_media_library_widget.
+ */
+class CloudinaryMediaLibraryWidgetHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for cloudinary_sdk_settings().
+   *
+   * Alters the setting form for Cloudinary settings.
+   *
+   * @see cloudinary_sdk_settings()
+   */
+  #[Hook('form_cloudinary_sdk_settings_alter')]
+  public function formCloudinarySdkSettingsAlter(&$form, $form_state) {
+    $config = \Drupal::config('cloudinary_media_library_widget.settings');
+    $form['widget'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Media library widget settings'),
+      '#collapsible' => TRUE,
+      '#collapsed' => FALSE,
+    ];
+    $form['widget']['cloudinary_saml_auth'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('SAML Authentication'),
+      '#default_value' => $config->get('cloudinary_saml_auth'),
+      '#description' => $this->t('Whether to perform automatic SSO login via SAML for the specified username. This parameter is only needed if the "Enforce SAML login" option is disabled for your account.'),
+    ];
+    try {
+      $api = new AdminApi();
+      $root_folders = $api->rootFolders();
+      $folders = [];
+      foreach ($root_folders['folders'] as $folder) {
+        $folders[$folder['path']] = $this->t('Folder: /@path', [
+          '@path' => $folder['path'],
+        ]);
+      }
+    }
+    catch (\Exception $e) {
+      $folders = [];
+    }
+    $form['widget']['cloudinary_starting_folder'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Starting folder'),
+      '#options' => $folders,
+      '#empty_value' => '/',
+      '#empty_option' => $this->t('Root'),
+      '#default_value' => $config->get('cloudinary_starting_folder'),
+      '#description' => $this->t('Instructs the widget to open in browse mode, displaying the contents of the specified folder.'),
+    ];
+    $form['metadata'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Metadata attributes'),
+      '#collapsible' => TRUE,
+      '#collapsed' => FALSE,
+    ];
+    $title_options = [
+      'context.custom.caption' => $this->t('Contextual metadata: Title (caption)'),
+    ];
+    $description_options = [
+      'context.custom.alt' => $this->t('Contextual metadata: Description (alt)'),
+    ];
+    try {
+      Configuration::instance()->validate();
+      // Fetch list of custom metadata fields.
+      $fields = CloudinaryAssetHelper::cloudinary()->adminApi()->listMetadataFields();
+      foreach ($fields->offsetGet('metadata_fields') as $metadata) {
+        if ($metadata['type'] === 'string') {
+          $title_options[$metadata['external_id']] = $metadata['label'];
+          $description_options[$metadata['external_id']] = $metadata['label'];
+        }
+      }
+    }
+    catch (\Exception $e) {
+      $form['metadata']['#description'] = $this->t('Please set up API connection and save the form to see list of all metadata including structure one.');
+    }
+    $form['metadata']['cloudinary_attribute_default_title'] = [
+      '#title' => $this->t('Default title (caption) attribute'),
+      '#type' => 'select',
+      '#description' => $this->t('Use this attribute to automatically populate title attribute for an image and name for a media.'),
+      '#options' => $title_options,
+      '#default_value' => $config->get('cloudinary_attribute_default_title'),
+    ];
+    $form['metadata']['cloudinary_attribute_default_description'] = [
+      '#title' => $this->t('Default description (alt) attribute'),
+      '#type' => 'select',
+      '#description' => $this->t('Use this attribute to automatically populate "alt text" for an image and "description" for a video/document.'),
+      '#options' => $description_options,
+      '#default_value' => $config->get('cloudinary_attribute_default_description'),
+    ];
+    $form['optimizations'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Optimizations'),
+      '#collapsible' => TRUE,
+      '#collapsed' => FALSE,
+    ];
+    $image_optimizations = $config->get('cloudinary_image_optimizations') ?? [];
+    $form['optimizations']['cloudinary_image_optimizations'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Default optimizations for image'),
+      '#default_value' => $image_optimizations,
+      '#description' => $this->t('Represents a different component (separated by a "/"), for example: c_scale,w_400/f_auto/q_auto. Check official @docs to get more details.', [
+        '@docs' => Link::fromTextAndUrl($this->t('docs'), Url::fromUri('https://cloudinary.com/documentation/image_transformations')->setOption('attributes', [
+          'target' => '_blank',
+        ]))->toString(),
+      ]),
+      '#rows' => 2,
+      '#weight' => 1,
+    ];
+    $breakpoints = $config->get('cloudinary_responsive_breakpoints') ?? [];
+    $form['optimizations']['cloudinary_responsive_breakpoints'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Responsive breakpoints'),
+      '#default_value' => CloudinaryMediaWidgetHelper::convertArrayOptionsToString($breakpoints),
+      '#description' => $this->t('Enter one value per line or separate it by comma. Check official @docs to get more details.', [
+        '@docs' => Link::fromTextAndUrl($this->t('docs'), Url::fromUri('https://cloudinary.com/documentation/responsive_images#overriding_default_values')->setOption('attributes', [
+          'target' => '_blank',
+        ]))->toString(),
+      ]),
+      '#weight' => 3,
+    ];
+    $form['#submit'][] = 'cloudinary_media_library_widget_settings_submit';
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public static function theme() {
+    return [
+      'cloudinary_image_formatter' => [
+        'variables' => [
+          'item' => NULL,
+          'item_attributes' => NULL,
+          'url' => NULL,
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_preprocess_HOOK() for image_formatter.
+   *
+   * Apply custom transformation for default image formatter.
+   */
+  #[Hook('preprocess_image_formatter')]
+  public static function preprocessImageFormatter(&$variables) {
+    $item = $variables['item'];
+    if ($item instanceof CloudinaryImage) {
+      /** @var \Drupal\cloudinary_media_library_widget\Service\CloudinaryFileHelperInterface $file_helper */
+      $file_helper = \Drupal::service('cloudinary_media_library_widget.file_helper');
+      /** @var \Drupal\cloudinary_sdk\Service\AssetHelperInterface $asset_helper */
+      $asset_helper = \Drupal::service('cloudinary_sdk.asset_helper');
+      $info = $item->getValue();
+      $value = $asset_helper->generateStringValue($info);
+      $variables['image']['#uri'] = $file_helper->generateUriForFileWithTransformation($value);
+    }
+  }
+
+  /**
+   * Implements hook_preprocess_HOOK() for responsive_image_formatter.
+   *
+   * Apply custom transformation for responsive image formatter.
+   */
+  #[Hook('preprocess_responsive_image_formatter')]
+  public static function preprocessResponsiveImageFormatter(&$variables) {
+    $item = $variables['item'];
+    if ($item instanceof CloudinaryImage) {
+      /** @var \Drupal\cloudinary_media_library_widget\Service\CloudinaryFileHelperInterface $file_helper */
+      $file_helper = \Drupal::service('cloudinary_media_library_widget.file_helper');
+      /** @var \Drupal\cloudinary_sdk\Service\AssetHelperInterface $asset_helper */
+      $asset_helper = \Drupal::service('cloudinary_sdk.asset_helper');
+      $info = $item->getValue();
+      $value = $asset_helper->generateStringValue($info);
+      $variables['responsive_image']['#uri'] = $file_helper->generateUriForFileWithTransformation($value);
+    }
+  }
+
+  /**
+   * Implements hook_preprocess_HOOK() for drimage_formatter.
+   *
+   * Apply custom transformation for drimage formatter and improve performance.
+   */
+  #[Hook('preprocess_drimage_formatter')]
+  public static function preprocessDrimageFormatter(&$variables) {
+    $item = $variables['item'];
+    // Properly handle aspect ratio behaviour, so it well integrated with
+    // cloudinary behaviour.
+    if ($item->entity instanceof FileInterface) {
+      $uri = $item->entity->getFileUri();
+      [$scheme] = explode('://', $uri, 2);
+      if ($scheme !== 'cloudinary') {
+        return;
+      }
+      $info = CloudinaryStreamWrapper::parseUri($uri);
+      // Fix ration and add a placeholder to be replaced in htaccess by drimage
+      // width and height. See README.md for more details about htaccess changes.
+      $ratio = $variables['width'] / $variables['height'];
+      $info['transformation'] = implode('/', array_filter([
+        $info['transformation'],
+        "ar_{$ratio},c_crop",
+        "%replaceme%",
+      ]));
+      /** @var \Drupal\cloudinary_media_library_widget\Service\CloudinaryFileHelperInterface $file_helper */
+      $file_helper = \Drupal::service('cloudinary_media_library_widget.file_helper');
+      /** @var \Drupal\cloudinary_sdk\Service\AssetHelperInterface $asset_helper */
+      $asset_helper = \Drupal::service('cloudinary_sdk.asset_helper');
+      $value = $asset_helper->generateStringValue($info);
+      $uri = $file_helper->generateUriForExternalUrl($value);
+      $variables['data']['original_source'] = \Drupal::service('file_url_generator')->generateAbsoluteString($uri);
+    }
+  }
+
+  /**
+   * Implements hook_field_formatter_info_alter().
+   */
+  #[Hook('field_formatter_info_alter')]
+  public static function fieldFormatterInfoAlter(array &$info) {
+    // Support contrib/core image formatters for custom cloudinary field type.
+    $field_formatters = [
+      'cloudinary_image' => [
+        'image',
+        'responsive_image',
+        'drimage',
+      ],
+      'cloudinary_raw' => [
+        'file_default',
+        'file_url_plain',
+        'file_table',
+      ],
+    ];
+    foreach ($field_formatters as $field_type => $formatters) {
+      foreach ($formatters as $field_formatter) {
+        if (isset($info[$field_formatter])) {
+          $info[$field_formatter]['field_types'][] = $field_type;
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for media.
+   *
+   * Force updating media thumbnail when uri changes.
+   */
+  #[Hook('media_update')]
+  public static function mediaUpdate(MediaInterface $entity) {
+    if ($entity->getSource()->getPluginId() === 'cloudinary' && DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => $entity->getOriginal() !== NULL, fn() => isset($entity->original))) {
+      $new_uri = $entity->getSource()->getMetadata($entity, 'thumbnail_uri');
+      $old_uri = $entity->getSource()->getMetadata(DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => $entity->getOriginal(), fn() => $entity->original), 'thumbnail_uri');
+      if ($new_uri === $old_uri) {
+        return;
+      }
+      $values = [
+        'uri' => $new_uri,
+      ];
+      $file_storage = \Drupal::entityTypeManager()->getStorage('file');
+      if ($existing = $file_storage->loadByProperties($values)) {
+        $file = reset($existing);
+      }
+      else {
+        /** @var \Drupal\file\FileInterface $file */
+        $file = $file_storage->create($values);
+        if ($owner = $entity->getOwner()) {
+          $file->setOwner($owner);
+        }
+        $file->setPermanent();
+        $file->save();
+      }
+      $entity->thumbnail->target_id = $file->id();
+    }
+  }
+
+}
diff --git a/modules/cloudinary_media_library_widget/src/Plugin/CloudinaryMedia/CloudinaryFile.php b/modules/cloudinary_media_library_widget/src/Plugin/CloudinaryMedia/CloudinaryFile.php
index d7862ec..91f4887 100644
--- a/modules/cloudinary_media_library_widget/src/Plugin/CloudinaryMedia/CloudinaryFile.php
+++ b/modules/cloudinary_media_library_widget/src/Plugin/CloudinaryMedia/CloudinaryFile.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\cloudinary_media_library_widget\Plugin\CloudinaryMedia;
 
+use Drupal\Component\Utility\DeprecationHelper;
 use Drupal\cloudinary_media_library_widget\Model\Asset;
 use Drupal\cloudinary_media_library_widget\Plugin\CloudinaryMediaBase;
 use Drupal\cloudinary_media_library_widget\Plugin\CloudinaryMediaPluginInterface;
@@ -127,7 +128,7 @@ class CloudinaryFile extends CloudinaryMediaBase implements CloudinaryMediaPlugi
 
     /** @var \Drupal\file\FileInterface $file */
     $file = $this->entityTypeManager->getStorage('file')->create([
-      'filename' => $this->fileSystem->basename($uri),
+      'filename' => DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => basename($uri), fn() => $this->fileSystem->basename($uri)),
       'uri' => $uri,
       'filemime' => $this->mimeTypeGuesser->guessMimeType($uri),
       'filesize' => $asset->getBytes(),
diff --git a/modules/cloudinary_media_library_widget/src/Plugin/CloudinaryMedia/CloudinaryImage.php b/modules/cloudinary_media_library_widget/src/Plugin/CloudinaryMedia/CloudinaryImage.php
index d1f21e1..f7c4871 100644
--- a/modules/cloudinary_media_library_widget/src/Plugin/CloudinaryMedia/CloudinaryImage.php
+++ b/modules/cloudinary_media_library_widget/src/Plugin/CloudinaryMedia/CloudinaryImage.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\cloudinary_media_library_widget\Plugin\CloudinaryMedia;
 
+use Drupal\Component\Utility\DeprecationHelper;
 use Drupal\cloudinary_media_library_widget\Model\Asset;
 use Drupal\cloudinary_media_library_widget\Plugin\CloudinaryMediaBase;
 use Drupal\cloudinary_media_library_widget\Plugin\CloudinaryMediaPluginInterface;
@@ -138,7 +139,7 @@ class CloudinaryImage extends CloudinaryMediaBase implements CloudinaryMediaPlug
 
     /** @var \Drupal\file\FileInterface $file */
     $file = $this->entityTypeManager->getStorage('file')->create([
-      'filename' => $this->fileSystem->basename($uri),
+      'filename' => DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.3.0', fn() => basename($uri), fn() => $this->fileSystem->basename($uri)),
       'uri' => $uri,
       'filemime' => $this->mimeTypeGuesser->guessMimeType($uri),
       'filesize' => $asset->getBytes(),
diff --git a/modules/cloudinary_media_library_widget/src/Plugin/Exception/CloudinaryMediaNotFoundException.php b/modules/cloudinary_media_library_widget/src/Plugin/Exception/CloudinaryMediaNotFoundException.php
index a48683e..215fab1 100644
--- a/modules/cloudinary_media_library_widget/src/Plugin/Exception/CloudinaryMediaNotFoundException.php
+++ b/modules/cloudinary_media_library_widget/src/Plugin/Exception/CloudinaryMediaNotFoundException.php
@@ -26,7 +26,7 @@ class CloudinaryMediaNotFoundException extends \Exception {
    *
    * @see \Exception
    */
-  public function __construct($type, $message = '', $code = 0, \Exception $previous = NULL) {
+  public function __construct($type, $message = '', $code = 0, ?\Exception $previous = NULL) {
     if (empty($message)) {
       $message = sprintf("Cloudinary media with resource type '%s' was not found.", $type);
     }
diff --git a/modules/cloudinary_sdk/cloudinary_sdk.install b/modules/cloudinary_sdk/cloudinary_sdk.install
index e6be647..f0c819d 100644
--- a/modules/cloudinary_sdk/cloudinary_sdk.install
+++ b/modules/cloudinary_sdk/cloudinary_sdk.install
@@ -5,6 +5,8 @@
  * Provides installation functions for cloudinary_sdk module.
  */
 
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\Core\Extension\Requirement\RequirementSeverity;
 use Drupal\Core\Url;
 use Drupal\cloudinary_sdk\CloudinarySdkConstantsInterface;
 
@@ -26,15 +28,15 @@ function cloudinary_sdk_requirements($phase) {
     return $requirements;
   }
 
-  list($status, $version, $error_message) = cloudinary_sdk_check(TRUE);
+  [$status, $version, $error_message] = cloudinary_sdk_check(TRUE);
   $r = [
-    'severity' => REQUIREMENT_OK,
+    'severity' => DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => RequirementSeverity::OK, fn() => REQUIREMENT_OK),
     'title' => t('Cloudinary'),
     'value' => $version,
   ];
 
   if ($status == CloudinarySdkConstantsInterface::CLOUDINARY_SDK_NOT_LOADED) {
-    $r['severity'] = REQUIREMENT_ERROR;
+    $r['severity'] = DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => RequirementSeverity::Error, fn() => REQUIREMENT_ERROR);
     $r['value'] = t('Failed to load the Cloudinary SDK.');
     if ($error_message) {
       $r['value'] .= ' ' . $error_message;
@@ -42,7 +44,7 @@ function cloudinary_sdk_requirements($phase) {
     $r['description'] = t('Please make sure the Cloudinary SDK library is installed in the libraries directory.');
   }
   elseif ($status == CloudinarySdkConstantsInterface::CLOUDINARY_SDK_OLD_VERSION) {
-    $r['severity'] = REQUIREMENT_ERROR;
+    $r['severity'] = DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => RequirementSeverity::Error, fn() => REQUIREMENT_ERROR);
     $r['description'] = t(
       'Please make sure the Cloudinary SDK library installed is @version or greater. Current version is @current_version.',
       [
@@ -56,7 +58,7 @@ function cloudinary_sdk_requirements($phase) {
     // Cloud name, API key, API secret.
     $config = cloudinary_sdk_config_load();
     if (empty($config)) {
-      $r['severity'] = REQUIREMENT_WARNING;
+      $r['severity'] = DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => RequirementSeverity::Warning, fn() => REQUIREMENT_WARNING);
       // Add link to configuration form.
       $url = Url::fromRoute('cloudinary_sdk.settings')->toString(TRUE)->getGeneratedUrl();
       $r['description'] = t("The required cloud name, API key and API secret of Cloudinary hasn't been set. <a href=':url'>Configure</a>", [
diff --git a/modules/cloudinary_sdk/cloudinary_sdk.module b/modules/cloudinary_sdk/cloudinary_sdk.module
index cd81269..08cfacc 100644
--- a/modules/cloudinary_sdk/cloudinary_sdk.module
+++ b/modules/cloudinary_sdk/cloudinary_sdk.module
@@ -5,10 +5,10 @@
  * File for the Cloudinary SDK module.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\cloudinary_sdk\Hook\CloudinarySdkHooks;
 use Cloudinary\Cloudinary;
 use Cloudinary\Configuration\Configuration;
-use Drupal\Core\Url;
-use Drupal\Core\Link;
 use Drupal\cloudinary_sdk\CloudinarySdkConstantsInterface;
 
 /**
@@ -92,53 +92,7 @@ function cloudinary_sdk_check($ver = FALSE) {
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function cloudinary_sdk_help($route_name) {
-
-  $renderer = \Drupal::service('renderer');
-
-  $logo = [
-    '#theme' => 'image',
-    '#uri' => \Drupal::service('extension.list.module')->getPath('cloudinary_sdk') . '/images/logo.png',
-    '#alt' => 'Cloudinary Logo',
-    '#width' => '344',
-    '#height' => '76',
-  ];
-  $logo_image = $renderer->render($logo);
-
-  $api = [
-    '#theme' => 'image',
-    '#uri' => \Drupal::service('extension.list.module')->getPath('cloudinary_sdk') . '/images/api.png',
-    '#alt' => 'Cloudinary Logo',
-    '#width' => '600',
-    '#height' => '249',
-  ];
-  $api_image = $renderer->render($api);
-
-
-  $output = '';
-  $url = Url::fromUri('http://www.cloudinary.com/');
-  $url_console = Url::fromUri('http://www.cloudinary.com/console');
-
-  switch ($route_name) {
-    case 'help.page.cloudinary_sdk':
-      $output .= '<p>' . Link::fromTextAndUrl($logo_image, $url)->toString() . '</p>';
-      $output .= '<h3>' . t('Features') . '</h3>';
-      $output .= '<ul>';
-      $output .= '<li>' . t("Image upload - Securely upload images or any other file, at any scale from any source. API for fast upload directly from your user's browsers or mobile apps.") . '</li>';
-      $output .= '<li>' . t('Cloud storage - Store as many images as needed. Our image hosting service stores images privately and safely with automatic backup and historical revisions.') . '</li>';
-      $output .= '<li>' . t('Powerful administration - Manage your media library interactively with our Digital Asset Management solution or via APIs. Gain insights using advanced analytics.') . '</li>';
-      $output .= '<li>' . t('Image manipulation - Manipulate your images dynamically to fit any graphics design. Apply effects, resizing, cropping, face detection, watermarks and tons of image processing capabilities.') . '</li>';
-      $output .= '<li>' . t("Fast delivery - Get your images delivered lightning-fast, responsive and highly optimized for any device in any location. Images are served via Akamai's worldwide CDN.") . '</li>';
-      $output .= '</ul>';
-      break;
-
-    case 'cloudinary_sdk.settings':
-      $output .= '<p>' . Link::fromTextAndUrl($logo_image, $url)->toString() . '</p>';
-      $output .= '<h3>' . t('API account') . '</h3>';
-      $output .= '<p>' . t('You can get the correct cloud name, API key and API secret on your account page.') . '</p>';
-      $output .= '<p>' . Link::fromTextAndUrl($api_image, $url_console)->toString() . '</p>';
-      break;
-  }
-
-  return $output;
+  return \Drupal::service(CloudinarySdkHooks::class)->help($route_name);
 }
diff --git a/modules/cloudinary_sdk/cloudinary_sdk.services.yml b/modules/cloudinary_sdk/cloudinary_sdk.services.yml
index cfc8ff0..04f887f 100644
--- a/modules/cloudinary_sdk/cloudinary_sdk.services.yml
+++ b/modules/cloudinary_sdk/cloudinary_sdk.services.yml
@@ -8,3 +8,7 @@ services:
     class: Drupal\cloudinary_sdk\Service\CloudinaryAssetHelper
     arguments:
       - '@serializer'
+
+  Drupal\cloudinary_sdk\Hook\CloudinarySdkHooks:
+    class: Drupal\cloudinary_sdk\Hook\CloudinarySdkHooks
+    autowire: true
diff --git a/modules/cloudinary_sdk/src/EventSubscriber/InitSubscriber.php b/modules/cloudinary_sdk/src/EventSubscriber/InitSubscriber.php
index 79c8d10..5c6e511 100644
--- a/modules/cloudinary_sdk/src/EventSubscriber/InitSubscriber.php
+++ b/modules/cloudinary_sdk/src/EventSubscriber/InitSubscriber.php
@@ -14,7 +14,7 @@ class InitSubscriber implements EventSubscriberInterface {
   /**
    * {@inheritdoc}
    */
-  public static function getSubscribedEvents() {
+  public static function getSubscribedEvents(): array {
     return [KernelEvents::REQUEST => ['onEvent', 0]];
   }
 
diff --git a/modules/cloudinary_sdk/src/Hook/CloudinarySdkHooks.php b/modules/cloudinary_sdk/src/Hook/CloudinarySdkHooks.php
new file mode 100644
index 0000000..58ca812
--- /dev/null
+++ b/modules/cloudinary_sdk/src/Hook/CloudinarySdkHooks.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\cloudinary_sdk\Hook;
+
+use Drupal\Core\Link;
+use Drupal\Core\Url;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for cloudinary_sdk.
+ */
+class CloudinarySdkHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name) {
+    $renderer = \Drupal::service('renderer');
+    $logo = [
+      '#theme' => 'image',
+      '#uri' => \Drupal::service('extension.list.module')->getPath('cloudinary_sdk') . '/images/logo.png',
+      '#alt' => 'Cloudinary Logo',
+      '#width' => '344',
+      '#height' => '76',
+    ];
+    $logo_image = $renderer->render($logo);
+    $api = [
+      '#theme' => 'image',
+      '#uri' => \Drupal::service('extension.list.module')->getPath('cloudinary_sdk') . '/images/api.png',
+      '#alt' => 'Cloudinary Logo',
+      '#width' => '600',
+      '#height' => '249',
+    ];
+    $api_image = $renderer->render($api);
+    $output = '';
+    $url = Url::fromUri('http://www.cloudinary.com/');
+    $url_console = Url::fromUri('http://www.cloudinary.com/console');
+    switch ($route_name) {
+      case 'help.page.cloudinary_sdk':
+        $output .= '<p>' . Link::fromTextAndUrl($logo_image, $url)->toString() . '</p>';
+        $output .= '<h3>' . $this->t('Features') . '</h3>';
+        $output .= '<ul>';
+        $output .= '<li>' . $this->t("Image upload - Securely upload images or any other file, at any scale from any source. API for fast upload directly from your user's browsers or mobile apps.") . '</li>';
+        $output .= '<li>' . $this->t('Cloud storage - Store as many images as needed. Our image hosting service stores images privately and safely with automatic backup and historical revisions.') . '</li>';
+        $output .= '<li>' . $this->t('Powerful administration - Manage your media library interactively with our Digital Asset Management solution or via APIs. Gain insights using advanced analytics.') . '</li>';
+        $output .= '<li>' . $this->t('Image manipulation - Manipulate your images dynamically to fit any graphics design. Apply effects, resizing, cropping, face detection, watermarks and tons of image processing capabilities.') . '</li>';
+        $output .= '<li>' . $this->t("Fast delivery - Get your images delivered lightning-fast, responsive and highly optimized for any device in any location. Images are served via Akamai's worldwide CDN.") . '</li>';
+        $output .= '</ul>';
+        break;
+
+      case 'cloudinary_sdk.settings':
+        $output .= '<p>' . Link::fromTextAndUrl($logo_image, $url)->toString() . '</p>';
+        $output .= '<h3>' . $this->t('API account') . '</h3>';
+        $output .= '<p>' . $this->t('You can get the correct cloud name, API key and API secret on your account page.') . '</p>';
+        $output .= '<p>' . Link::fromTextAndUrl($api_image, $url_console)->toString() . '</p>';
+        break;
+    }
+    return $output;
+  }
+
+}
diff --git a/modules/cloudinary_storage/cloudinary_storage.module b/modules/cloudinary_storage/cloudinary_storage.module
index 9fb279c..ac18c9a 100644
--- a/modules/cloudinary_storage/cloudinary_storage.module
+++ b/modules/cloudinary_storage/cloudinary_storage.module
@@ -1,5 +1,11 @@
 <?php
-use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * @file
+ */
+
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\cloudinary_storage\Hook\CloudinaryStorageHooks;
 
 /**
  * @file
@@ -55,93 +61,41 @@ function cloudinary_storage_class() {
 /**
  * Implements hook_cloudinary_stream_wrapper_resource_create().
  */
+#[LegacyHook]
 function cloudinary_storage_cloudinary_stream_wrapper_resource_create($resource) {
-  // Update parent path to store new file or folder.
-  if (isset($resource['public_id'])) {
-    if ($storage_class = cloudinary_storage_class()) {
-      $storage = new $storage_class($resource);
-      list($path, $file) = $storage->resourceUpdate();
-      $data = array(CLOUDINARY_STORAGE_NEW => $file);
-
-      if ($resource['mode'] == CLOUDINARY_STREAM_WRAPPER_FILE) {
-        $storage->folderUpdate($path, $data);
-      }
-      elseif ($resource['mode'] == CLOUDINARY_STREAM_WRAPPER_FOLDER) {
-        $storage->folderUpdate($path, NULL, $data);
-      }
-    }
-  }
+  \Drupal::service(CloudinaryStorageHooks::class)->cloudinaryStreamWrapperResourceCreate($resource);
 }
 
 /**
  * Implements hook_cloudinary_stream_wrapper_resource_rename().
  */
+#[LegacyHook]
 function cloudinary_storage_cloudinary_stream_wrapper_resource_rename($src_resource, $dst_resource) {
-  if ($storage_class = cloudinary_storage_class()) {
-    $src_path = $dst_path = FALSE;
-    $src_file = $dst_file = '';
-
-    $src_storage = new $storage_class($src_resource);
-    list($src_path, $src_file) = $src_storage->resourceUpdate(FALSE);
-
-    $dst_storage = new $storage_class($dst_resource);
-    list($dst_path, $dst_file) = $dst_storage->resourceUpdate();
-
-    if ($src_path !== FALSE && $src_path == $dst_path) {
-      $src_storage->folderUpdate($src_path, array(CLOUDINARY_STORAGE_NEW => $dst_file, CLOUDINARY_STORAGE_REMOVE => $src_file));
-    }
-    else {
-      if ($src_path !== FALSE) {
-        $src_storage->folderUpdate($src_path, array(CLOUDINARY_STORAGE_REMOVE => $src_file));
-      }
-
-      if ($dst_path !== FALSE) {
-        $dst_storage->folderUpdate($dst_path, array(CLOUDINARY_STORAGE_NEW => $dst_file));
-      }
-    }
-  }
+  \Drupal::service(CloudinaryStorageHooks::class)->cloudinaryStreamWrapperResourceRename($src_resource, $dst_resource);
 }
 
 /**
  * Implements hook_cloudinary_stream_wrapper_resource_prepare().
  */
+#[LegacyHook]
 function cloudinary_storage_cloudinary_stream_wrapper_resource_prepare($resource) {
-  if (isset($resource['public_id'])) {
-    if ($storage_class = cloudinary_storage_class()) {
-      $storage = new $storage_class($resource, FALSE);
-      $data = $storage->getResource();
-      $resource = array_merge($resource, $data);
-    }
-  }
-
-  return $resource;
+  return \Drupal::service(CloudinaryStorageHooks::class)->cloudinaryStreamWrapperResourcePrepare($resource);
 }
 
 /**
  * Implements hook_cloudinary_stream_wrapper_resource_loaded().
  */
+#[LegacyHook]
 function cloudinary_storage_cloudinary_stream_wrapper_resource_loaded($resource) {
-  // Insert or update resource data which load from remote.
-  if ($storage_class = cloudinary_storage_class()) {
-    $storage = new $storage_class($resource);
-    $storage->resourceUpdate();
-  }
+  \Drupal::service(CloudinaryStorageHooks::class)->cloudinaryStreamWrapperResourceLoaded($resource);
 }
 
 /**
  * Implements hook_cloudinary_stream_wrapper_resource_delete().
  */
+#[LegacyHook]
 function cloudinary_storage_cloudinary_stream_wrapper_resource_delete($resource) {
-  if (isset($resource['public_id'])) {
-    if ($storage_class = cloudinary_storage_class()) {
-      $storage = new $storage_class($resource);
-      list($path, $file) = $storage->resourceUpdate(FALSE);
-
-      if ($resource['mode'] == CLOUDINARY_STREAM_WRAPPER_FILE) {
-        $storage->folderUpdate($path, array(CLOUDINARY_STORAGE_REMOVE => $file));
-      }
-    }
-  }
+  \Drupal::service(CloudinaryStorageHooks::class)->cloudinaryStreamWrapperResourceDelete($resource);
 }
 
 /**
@@ -161,35 +115,9 @@ function cloudinary_storage_clear() {
  *
  * @see cloudinary_sdk_settings()
  */
+#[LegacyHook]
 function cloudinary_storage_form_cloudinary_sdk_settings_alter(&$form, $form_state) {
-  $storages = cloudinary_storage_info();
-
-  if (empty($storages)) {
-    return;
-  }
-
-  $options = array();
-  foreach ($storages as $key => $storage) {
-    $options[$key] = $storage['title'];
-  }
-
-  $form['storage'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Cloudinary storage settings'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-    '#description' => t('Choose one of the storage to reduce network requests and improve loading speed for uploaded Cloudinary files.'),
-  );
-
-  $form['storage']['cloudinary_storage_default'] = array(
-    '#type' => 'radios',
-    '#title' => t('Storage for uploaded cloudinary files'),
-    '#options' => $options,
-    '#default_value' => \Drupal::config('cloudinary_storage.settings')->get('cloudinary_storage_default'),
-  );
-
-  // Unshift exist storage clear checking.
-  array_unshift($form['#submit'], 'cloudinary_storage_settings_submit');
+  \Drupal::service(CloudinaryStorageHooks::class)->formCloudinarySdkSettingsAlter($form, $form_state);
 }
 
 /**
diff --git a/modules/cloudinary_storage/cloudinary_storage.services.yml b/modules/cloudinary_storage/cloudinary_storage.services.yml
index 992f3d9..a0c8063 100644
--- a/modules/cloudinary_storage/cloudinary_storage.services.yml
+++ b/modules/cloudinary_storage/cloudinary_storage.services.yml
@@ -2,3 +2,7 @@ services:
   cloudinary_storage:
     class: Drupal\cloudinary_storage_db\CloudinaryStorage
     #arguments: [@service_container]
+
+  Drupal\cloudinary_storage\Hook\CloudinaryStorageHooks:
+    class: Drupal\cloudinary_storage\Hook\CloudinaryStorageHooks
+    autowire: true
diff --git a/modules/cloudinary_storage/modules/cloudinary_storage_db/cloudinary_storage_db.module b/modules/cloudinary_storage/modules/cloudinary_storage_db/cloudinary_storage_db.module
index 7b83142..0a01ef8 100644
--- a/modules/cloudinary_storage/modules/cloudinary_storage_db/cloudinary_storage_db.module
+++ b/modules/cloudinary_storage/modules/cloudinary_storage_db/cloudinary_storage_db.module
@@ -5,16 +5,13 @@
  * File for the cloudinary_storage_db module.
  */
 
-use Drupal\cloudinary_storage_db\CloudinaryStorageDb;
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\cloudinary_storage_db\Hook\CloudinaryStorageDbHooks;
 
 /**
  * Implements hook_cloudinary_storage_info().
  */
+#[LegacyHook]
 function cloudinary_storage_db_cloudinary_storage_info() {
-  return array(
-    'cloudinary_storage_db' => array(
-      'title' => t('Database'),
-      'class' => '\Drupal\cloudinary_storage_db\CloudinaryStorageDb',
-    ),
-  );
+  return \Drupal::service(CloudinaryStorageDbHooks::class)->cloudinaryStorageInfo();
 }
diff --git a/modules/cloudinary_storage/modules/cloudinary_storage_db/cloudinary_storage_db.services.yml b/modules/cloudinary_storage/modules/cloudinary_storage_db/cloudinary_storage_db.services.yml
index 0a3fc8a..3043fb6 100644
--- a/modules/cloudinary_storage/modules/cloudinary_storage_db/cloudinary_storage_db.services.yml
+++ b/modules/cloudinary_storage/modules/cloudinary_storage_db/cloudinary_storage_db.services.yml
@@ -2,3 +2,7 @@ services:
   cloudinary_storage_db:
     class: Drupal\cloudinary_storage_db\CloudinaryStorageDb
     #arguments: [@service_container]
+
+  Drupal\cloudinary_storage_db\Hook\CloudinaryStorageDbHooks:
+    class: Drupal\cloudinary_storage_db\Hook\CloudinaryStorageDbHooks
+    autowire: true
diff --git a/modules/cloudinary_storage/modules/cloudinary_storage_db/src/Hook/CloudinaryStorageDbHooks.php b/modules/cloudinary_storage/modules/cloudinary_storage_db/src/Hook/CloudinaryStorageDbHooks.php
new file mode 100644
index 0000000..2caec6a
--- /dev/null
+++ b/modules/cloudinary_storage/modules/cloudinary_storage_db/src/Hook/CloudinaryStorageDbHooks.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\cloudinary_storage_db\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for cloudinary_storage_db.
+ */
+class CloudinaryStorageDbHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_cloudinary_storage_info().
+   */
+  #[Hook('cloudinary_storage_info')]
+  public function cloudinaryStorageInfo() {
+    return [
+      'cloudinary_storage_db' => [
+        'title' => $this->t('Database'),
+        'class' => '\Drupal\cloudinary_storage_db\CloudinaryStorageDb',
+      ],
+    ];
+  }
+
+}
diff --git a/modules/cloudinary_storage/src/Hook/CloudinaryStorageHooks.php b/modules/cloudinary_storage/src/Hook/CloudinaryStorageHooks.php
new file mode 100644
index 0000000..932d00e
--- /dev/null
+++ b/modules/cloudinary_storage/src/Hook/CloudinaryStorageHooks.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace Drupal\cloudinary_storage\Hook;
+
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for cloudinary_storage.
+ */
+class CloudinaryStorageHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_cloudinary_stream_wrapper_resource_create().
+   */
+  #[Hook('cloudinary_stream_wrapper_resource_create')]
+  public static function cloudinaryStreamWrapperResourceCreate($resource) {
+    // Update parent path to store new file or folder.
+    if (isset($resource['public_id'])) {
+      if ($storage_class = cloudinary_storage_class()) {
+        $storage = new $storage_class($resource);
+        [$path, $file] = $storage->resourceUpdate();
+        $data = [
+          CLOUDINARY_STORAGE_NEW => $file,
+        ];
+        if ($resource['mode'] == CLOUDINARY_STREAM_WRAPPER_FILE) {
+          $storage->folderUpdate($path, $data);
+        }
+        elseif ($resource['mode'] == CLOUDINARY_STREAM_WRAPPER_FOLDER) {
+          $storage->folderUpdate($path, NULL, $data);
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_cloudinary_stream_wrapper_resource_rename().
+   */
+  #[Hook('cloudinary_stream_wrapper_resource_rename')]
+  public static function cloudinaryStreamWrapperResourceRename($src_resource, $dst_resource) {
+    if ($storage_class = cloudinary_storage_class()) {
+      $src_path = $dst_path = FALSE;
+      $src_file = $dst_file = '';
+      $src_storage = new $storage_class($src_resource);
+      [$src_path, $src_file] = $src_storage->resourceUpdate(FALSE);
+      $dst_storage = new $storage_class($dst_resource);
+      [$dst_path, $dst_file] = $dst_storage->resourceUpdate();
+      if ($src_path !== FALSE && $src_path == $dst_path) {
+        $src_storage->folderUpdate($src_path, [
+          CLOUDINARY_STORAGE_NEW => $dst_file,
+          CLOUDINARY_STORAGE_REMOVE => $src_file,
+        ]);
+      }
+      else {
+        if ($src_path !== FALSE) {
+          $src_storage->folderUpdate($src_path, [
+            CLOUDINARY_STORAGE_REMOVE => $src_file,
+          ]);
+        }
+        if ($dst_path !== FALSE) {
+          $dst_storage->folderUpdate($dst_path, [
+            CLOUDINARY_STORAGE_NEW => $dst_file,
+          ]);
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_cloudinary_stream_wrapper_resource_prepare().
+   */
+  #[Hook('cloudinary_stream_wrapper_resource_prepare')]
+  public static function cloudinaryStreamWrapperResourcePrepare($resource) {
+    if (isset($resource['public_id'])) {
+      if ($storage_class = cloudinary_storage_class()) {
+        $storage = new $storage_class($resource, FALSE);
+        $data = DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.2.0', fn() => $storage->getImage(), fn() => $storage->getResource());
+        $resource = array_merge($resource, $data);
+      }
+    }
+    return $resource;
+  }
+
+  /**
+   * Implements hook_cloudinary_stream_wrapper_resource_loaded().
+   */
+  #[Hook('cloudinary_stream_wrapper_resource_loaded')]
+  public static function cloudinaryStreamWrapperResourceLoaded($resource) {
+    // Insert or update resource data which load from remote.
+    if ($storage_class = cloudinary_storage_class()) {
+      $storage = new $storage_class($resource);
+      $storage->resourceUpdate();
+    }
+  }
+
+  /**
+   * Implements hook_cloudinary_stream_wrapper_resource_delete().
+   */
+  #[Hook('cloudinary_stream_wrapper_resource_delete')]
+  public static function cloudinaryStreamWrapperResourceDelete($resource) {
+    if (isset($resource['public_id'])) {
+      if ($storage_class = cloudinary_storage_class()) {
+        $storage = new $storage_class($resource);
+        [$path, $file] = $storage->resourceUpdate(FALSE);
+        if ($resource['mode'] == CLOUDINARY_STREAM_WRAPPER_FILE) {
+          $storage->folderUpdate($path, [
+            CLOUDINARY_STORAGE_REMOVE => $file,
+          ]);
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for cloudinary_sdk_settings().
+   *
+   * Alters the setting form for Cloudinary settings.
+   *
+   * @see cloudinary_sdk_settings()
+   */
+  #[Hook('form_cloudinary_sdk_settings_alter')]
+  public function formCloudinarySdkSettingsAlter(&$form, $form_state) {
+    $storages = cloudinary_storage_info();
+    if (empty($storages)) {
+      return;
+    }
+    $options = [];
+    foreach ($storages as $key => $storage) {
+      $options[$key] = $storage['title'];
+    }
+    $form['storage'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Cloudinary storage settings'),
+      '#collapsible' => TRUE,
+      '#collapsed' => FALSE,
+      '#description' => $this->t('Choose one of the storage to reduce network requests and improve loading speed for uploaded Cloudinary files.'),
+    ];
+    $form['storage']['cloudinary_storage_default'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Storage for uploaded cloudinary files'),
+      '#options' => $options,
+      '#default_value' => \Drupal::config('cloudinary_storage.settings')->get('cloudinary_storage_default'),
+    ];
+    // Unshift exist storage clear checking.
+    array_unshift($form['#submit'], 'cloudinary_storage_settings_submit');
+  }
+
+}
diff --git a/modules/cloudinary_stream_wrapper/cloudinary_stream_wrapper.api.php b/modules/cloudinary_stream_wrapper/cloudinary_stream_wrapper.api.php
index 34629cc..733afed 100644
--- a/modules/cloudinary_stream_wrapper/cloudinary_stream_wrapper.api.php
+++ b/modules/cloudinary_stream_wrapper/cloudinary_stream_wrapper.api.php
@@ -1,5 +1,11 @@
 <?php
 
+/**
+ * @file
+ */
+
+use Drupal\Component\Utility\DeprecationHelper;
+
 /**
  * @file
  * Hooks provided by the Cloudinary Stream Wrapper.
@@ -18,8 +24,8 @@ function hook_cloudinary_stream_wrapper_resource_create(array $resource) {
   if (isset($resource['public_id'])) {
     if ($storage_class = cloudinary_storage_class()) {
       $storage = new $storage_class($resource);
-      list($path, $file) = $storage->resourceUpdate();
-      $data = array(CLOUDINARY_STORAGE_NEW => $file);
+      [$path, $file] = $storage->resourceUpdate();
+      $data = [CLOUDINARY_STORAGE_NEW => $file];
 
       if ($resource['mode'] == CLOUDINARY_STREAM_WRAPPER_FILE) {
         $storage->folderUpdate($path, $data);
@@ -47,21 +53,21 @@ function hook_cloudinary_stream_wrapper_resource_rename(array $src_resource, arr
     $src_file = $dst_file = '';
 
     $src_storage = new $storage_class($src_resource);
-    list($src_path, $src_file) = $src_storage->resourceUpdate(FALSE);
+    [$src_path, $src_file] = $src_storage->resourceUpdate(FALSE);
 
     $dst_storage = new $storage_class($dst_resource);
-    list($dst_path, $dst_file) = $dst_storage->resourceUpdate();
+    [$dst_path, $dst_file] = $dst_storage->resourceUpdate();
 
     if ($src_path !== FALSE && $src_path == $dst_path) {
-      $src_storage->folderUpdate($src_path, array(CLOUDINARY_STORAGE_NEW => $dst_file, CLOUDINARY_STORAGE_REMOVE => $src_file));
+      $src_storage->folderUpdate($src_path, [CLOUDINARY_STORAGE_NEW => $dst_file, CLOUDINARY_STORAGE_REMOVE => $src_file]);
     }
     else {
       if ($src_path !== FALSE) {
-        $src_storage->folderUpdate($src_path, array(CLOUDINARY_STORAGE_REMOVE => $src_file));
+        $src_storage->folderUpdate($src_path, [CLOUDINARY_STORAGE_REMOVE => $src_file]);
       }
 
       if ($dst_path !== FALSE) {
-        $dst_storage->folderUpdate($dst_path, array(CLOUDINARY_STORAGE_NEW => $dst_file));
+        $dst_storage->folderUpdate($dst_path, [CLOUDINARY_STORAGE_NEW => $dst_file]);
       }
     }
   }
@@ -82,7 +88,7 @@ function hook_cloudinary_stream_wrapper_resource_prepare(array $resource) {
   if (isset($resource['public_id'])) {
     if ($storage_class = cloudinary_storage_class()) {
       $storage = new $storage_class($resource, FALSE);
-      $data = $storage->getResource();
+      $data = DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.2.0', fn() => $storage->getImage(), fn() => $storage->getResource());
       $resource = array_merge($resource, $data);
     }
   }
@@ -118,10 +124,10 @@ function hook_cloudinary_stream_wrapper_resource_delete(array $resource) {
   if (isset($resource['public_id'])) {
     if ($storage_class = cloudinary_storage_class()) {
       $storage = new $storage_class($resource);
-      list($path, $file) = $storage->resourceUpdate(FALSE);
+      [$path, $file] = $storage->resourceUpdate(FALSE);
 
       if ($resource['mode'] == CLOUDINARY_STREAM_WRAPPER_FILE) {
-        $storage->folderUpdate($path, array(CLOUDINARY_STORAGE_REMOVE => $file));
+        $storage->folderUpdate($path, [CLOUDINARY_STORAGE_REMOVE => $file]);
       }
     }
   }
@@ -142,13 +148,13 @@ function hook_cloudinary_stream_wrapper_resource_delete(array $resource) {
 function hook_cloudinary_stream_wrapper_transformation() {
   $path = \Drupal::service('extension.list.module')->getPath('cloudinary');
 
-  return array(
-    'image_crop' => array(
+  return [
+    'image_crop' => [
       'title' => t('Crop'),
       'callback' => 'cloudinary_transformation_image_crop',
       'file' => $path . '/includes/cloudinary.transformation.drupal.inc',
-    ),
-  );
+    ],
+  ];
 }
 
 /**
diff --git a/modules/cloudinary_stream_wrapper/cloudinary_stream_wrapper.module b/modules/cloudinary_stream_wrapper/cloudinary_stream_wrapper.module
index 75507d8..9966848 100644
--- a/modules/cloudinary_stream_wrapper/cloudinary_stream_wrapper.module
+++ b/modules/cloudinary_stream_wrapper/cloudinary_stream_wrapper.module
@@ -5,6 +5,8 @@
  * File for the Cloudinary Stream Wrapper module.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\cloudinary_stream_wrapper\Hook\CloudinaryStreamWrapperHooks;
 use Cloudinary\Api\Admin\AdminApi;
 use Cloudinary\Api\Upload\UploadApi;
 use Cloudinary\Asset\AssetType;
@@ -48,25 +50,9 @@ define('CLOUDINARY_STREAM_WRAPPER_TRANSFORMATION_MULTIPLE', 2);
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function cloudinary_stream_wrapper_help($path, $arg) {
-  $output = '';
-
-  switch ($path) {
-    case 'admin/help#cloudinary_stream_wrapper':
-      $output .= '<h3>' . t('About') . '</h3>';
-      $output .= '<p>' . t("The Cloudinary module allows the local file system to be replaced with Cloudinary. Uploads are saved into the Drupal file table using Drupal 7's file stream wrapper system.") . '</p>';
-      $output .= '<h3>' . t('How to use') . '</h3>';
-      $output .= '<ul>';
-      $output .= '<li>' . t("Image upload - Securely upload images or any other file, at any scale from any source. API for fast upload directly from your user's browsers or mobile apps.") . '</li>';
-      $output .= '<li>' . t('Cloud storage - Store as many images as needed. Our image hosting service stores images privately and safely with automatic backup and historical revisions.') . '</li>';
-      $output .= '<li>' . t('Powerful administration - Manage your media library interactively with our Digital Asset Management solution or via APIs. Gain insights using advanced analytics.') . '</li>';
-      $output .= '<li>' . t('Image manipulation - Manipulate your images dynamically to fit any graphics design. Apply effects, resizing, cropping, face detection, watermarks and tons of image processing capabilities.') . '</li>';
-      $output .= '<li>' . t("Fast delivery - Get your images delivered lightning-fast, responsive and highly optimized for any device in any location. Images are served via Akamai's worldwide CDN.") . '</li>';
-      $output .= '</ul>';
-      break;
-  }
-
-  return $output;
+  return \Drupal::service(CloudinaryStreamWrapperHooks::class)->help($path, $arg);
 }
 
 /**
@@ -76,51 +62,9 @@ function cloudinary_stream_wrapper_help($path, $arg) {
  *
  * @see cloudinary_sdk_settings()
  */
+#[LegacyHook]
 function cloudinary_stream_wrapper_form_cloudinary_sdk_settings_alter(&$form, $form_state) {
-  $config = cloudinary_sdk_config_load();
-
-  if (!$config) {
-    return;
-  }
-
-  // Get Cloudinary SDK configs.
-  $configs = \Drupal::config('cloudinary_stream_wrapper.settings');
-
-  $form['settings']['cloudinary_stream_wrapper_logging'] = [
-    '#type' => 'checkbox',
-    '#title' => t('Enable error logging.'),
-    '#default_value' => (bool) $configs->get('cloudinary_stream_wrapper_logging'),
-    '#description' => t('Enable if you want to logging any errors messages during file processing by cloudinary.'),
-  ];
-
-  $folders = [];
-
-  try {
-    $api = new AdminApi();
-    $root_folders = $api->rootFolders();
-    foreach ($root_folders['folders'] as $folder) {
-      $folders[str_replace('.', '_', $folder['path'])] = t('Folder: /@folder', [
-        '@folder' => "{$folder['path']} [cloudinary.{$folder['path']}]",
-      ]);
-    }
-  } catch (\Exception $e) {
-  }
-
-  $form['folders'] = [
-    '#type' => 'details',
-    '#title' => t('Stream Wrapper Settings'),
-    '#open' => FALSE,
-    '#weight' => 10,
-    '#access' => !empty($folders),
-    '#description' => t('You can enable more stream wrappers for Cloudinary with root folders.'),
-  ];
-
-  $form['folders']['cloudinary_stream_wrapper_folders'] = [
-    '#type' => 'checkboxes',
-    '#title' => t('Cloudinary scheme with root folder'),
-    '#options' => $folders,
-    '#default_value' => $configs->get('cloudinary_stream_wrapper_folders'),
-  ];
+  \Drupal::service(CloudinaryStreamWrapperHooks::class)->formCloudinarySdkSettingsAlter($form, $form_state);
 }
 
 /**
@@ -581,7 +525,7 @@ function cloudinary_stream_wrapper_remote_image_info($url, $filesize = FALSE) {
     if ($filesize) {
       $meta = stream_get_meta_data($handle);
       if (is_array($meta['wrapper_data'])) {
-        $headers = isset($meta['wrapper_data']['headers']) ? $meta['wrapper_data']['headers'] : $meta['wrapper_data'];
+        $headers = $meta['wrapper_data']['headers'] ?? $meta['wrapper_data'];
       }
       elseif (is_object($meta['wrapper_data']) && isset($meta['wrapper_data']->stream_headers)) {
         $headers = $meta['wrapper_data']->stream_headers;
@@ -622,7 +566,7 @@ function cloudinary_stream_wrapper_remote_image_info($url, $filesize = FALSE) {
 /**
  * Logging error messages.
  *
- * @param $message string
+ * @param string $message
  *   Error message.
  * @param array $options
  *   Message options.
diff --git a/modules/cloudinary_stream_wrapper/cloudinary_stream_wrapper.services.yml b/modules/cloudinary_stream_wrapper/cloudinary_stream_wrapper.services.yml
index 3ed7f3e..b1556ce 100644
--- a/modules/cloudinary_stream_wrapper/cloudinary_stream_wrapper.services.yml
+++ b/modules/cloudinary_stream_wrapper/cloudinary_stream_wrapper.services.yml
@@ -6,3 +6,7 @@ services:
   cloudinary_stream_wrapper.service_provider:
     class: Drupal\cloudinary_stream_wrapper\CloudinaryStreamWrapperServiceProvider
     arguments: ['@service_container']
+
+  Drupal\cloudinary_stream_wrapper\Hook\CloudinaryStreamWrapperHooks:
+    class: Drupal\cloudinary_stream_wrapper\Hook\CloudinaryStreamWrapperHooks
+    autowire: true
diff --git a/modules/cloudinary_stream_wrapper/src/Hook/CloudinaryStreamWrapperHooks.php b/modules/cloudinary_stream_wrapper/src/Hook/CloudinaryStreamWrapperHooks.php
new file mode 100644
index 0000000..8ac2754
--- /dev/null
+++ b/modules/cloudinary_stream_wrapper/src/Hook/CloudinaryStreamWrapperHooks.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\cloudinary_stream_wrapper\Hook;
+
+use Cloudinary\Api\Admin\AdminApi;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for cloudinary_stream_wrapper.
+ */
+class CloudinaryStreamWrapperHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($path, $arg) {
+    $output = '';
+    switch ($path) {
+      case 'admin/help#cloudinary_stream_wrapper':
+        $output .= '<h3>' . $this->t('About') . '</h3>';
+        $output .= '<p>' . $this->t("The Cloudinary module allows the local file system to be replaced with Cloudinary. Uploads are saved into the Drupal file table using Drupal 7's file stream wrapper system.") . '</p>';
+        $output .= '<h3>' . $this->t('How to use') . '</h3>';
+        $output .= '<ul>';
+        $output .= '<li>' . $this->t("Image upload - Securely upload images or any other file, at any scale from any source. API for fast upload directly from your user's browsers or mobile apps.") . '</li>';
+        $output .= '<li>' . $this->t('Cloud storage - Store as many images as needed. Our image hosting service stores images privately and safely with automatic backup and historical revisions.') . '</li>';
+        $output .= '<li>' . $this->t('Powerful administration - Manage your media library interactively with our Digital Asset Management solution or via APIs. Gain insights using advanced analytics.') . '</li>';
+        $output .= '<li>' . $this->t('Image manipulation - Manipulate your images dynamically to fit any graphics design. Apply effects, resizing, cropping, face detection, watermarks and tons of image processing capabilities.') . '</li>';
+        $output .= '<li>' . $this->t("Fast delivery - Get your images delivered lightning-fast, responsive and highly optimized for any device in any location. Images are served via Akamai's worldwide CDN.") . '</li>';
+        $output .= '</ul>';
+        break;
+    }
+    return $output;
+  }
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for cloudinary_sdk_settings().
+   *
+   * Alters the setting form for Cloudinary settings.
+   *
+   * @see cloudinary_sdk_settings()
+   */
+  #[Hook('form_cloudinary_sdk_settings_alter')]
+  public function formCloudinarySdkSettingsAlter(&$form, $form_state) {
+    $config = cloudinary_sdk_config_load();
+    if (!$config) {
+      return;
+    }
+    // Get Cloudinary SDK configs.
+    $configs = \Drupal::config('cloudinary_stream_wrapper.settings');
+    $form['settings']['cloudinary_stream_wrapper_logging'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Enable error logging.'),
+      '#default_value' => (bool) $configs->get('cloudinary_stream_wrapper_logging'),
+      '#description' => $this->t('Enable if you want to logging any errors messages during file processing by cloudinary.'),
+    ];
+    $folders = [];
+    try {
+      $api = new AdminApi();
+      $root_folders = $api->rootFolders();
+      foreach ($root_folders['folders'] as $folder) {
+        $folders[str_replace('.', '_', $folder['path'])] = $this->t('Folder: /@folder', [
+          '@folder' => "{$folder['path']} [cloudinary.{$folder['path']}]",
+        ]);
+      }
+    }
+    catch (\Exception $e) {
+    }
+    $form['folders'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Stream Wrapper Settings'),
+      '#open' => FALSE,
+      '#weight' => 10,
+      '#access' => !empty($folders),
+      '#description' => $this->t('You can enable more stream wrappers for Cloudinary with root folders.'),
+    ];
+    $form['folders']['cloudinary_stream_wrapper_folders'] = [
+      '#type' => 'checkboxes',
+      '#title' => $this->t('Cloudinary scheme with root folder'),
+      '#options' => $folders,
+      '#default_value' => $configs->get('cloudinary_stream_wrapper_folders'),
+    ];
+  }
+
+}
diff --git a/modules/cloudinary_stream_wrapper/src/StreamWrapper/CloudinaryStreamWrapper.php b/modules/cloudinary_stream_wrapper/src/StreamWrapper/CloudinaryStreamWrapper.php
index c9aaf5c..b246949 100644
--- a/modules/cloudinary_stream_wrapper/src/StreamWrapper/CloudinaryStreamWrapper.php
+++ b/modules/cloudinary_stream_wrapper/src/StreamWrapper/CloudinaryStreamWrapper.php
@@ -3,6 +3,7 @@
 namespace Drupal\cloudinary_stream_wrapper\StreamWrapper;
 
 // These classes are used to implement a stream wrapper class.
+use Drupal\Core\Utility\Error;
 use Cloudinary\Asset\AssetType;
 use Cloudinary\Asset\DeliveryType;
 use Cloudinary\Configuration\Configuration;
@@ -17,54 +18,54 @@ class CloudinaryStreamWrapper implements StreamWrapperInterface {
    *
    * A stream is referenced as "scheme://target".
    *
-   * @var String
+   * @var string
    */
   protected $uri;
 
   /**
    * Folder name as a prefix name of public_id.
    *
-   * @var String
+   * @var string
    */
   protected $folderName = NULL;
 
   /**
    * The resource type of Cloudinary (image, raw).
    *
-   * @var String
+   * @var string
    */
   protected $resourceType = AssetType::AUTO;
 
   /**
    * The pointer to the next read or write.
    *
-   * @var Int
+   * @var int
    */
   protected $streamPointer = 0;
 
   /**
    * A buffer for reading/writing.
    *
-   * @var String
+   * @var string
    */
   protected $streamData = NULL;
 
   /**
    * This $stream_write property is flagged for data written.
    *
-   * @var Boolean
+   * @var bool
    */
   protected $streamWrite = FALSE;
 
   /**
    * List of files in a given directory.
    */
-  protected $directoryList = array();
+  protected $directoryList = [];
 
   /**
    * A current file resource of Cloudinary.
    *
-   * @var Array
+   * @var array
    */
   protected $resource = NULL;
 
@@ -211,7 +212,7 @@ class CloudinaryStreamWrapper implements StreamWrapperInterface {
       $data[$this->uri] = $request->getBody();
     }
     catch (\Exception $e) {
-      watchdog_exception('cloudinary_stream_wrapper', $e);
+      Error::logException(\Drupal::logger('cloudinary_stream_wrapper'), $e);
     }
 
     return $data[$this->uri];
@@ -232,7 +233,7 @@ class CloudinaryStreamWrapper implements StreamWrapperInterface {
       return FALSE;
     }
 
-    $stat = array();
+    $stat = [];
     $stat[0] = $stat['dev'] = 0;
     $stat[1] = $stat['ino'] = 0;
     $stat[2] = $stat['mode'] = $resource['mode'];
@@ -255,7 +256,7 @@ class CloudinaryStreamWrapper implements StreamWrapperInterface {
    */
   protected function flush() {
     $this->folderName = NULL;
-    $this->directoryList = array();
+    $this->directoryList = [];
     $this->streamData = NULL;
     $this->streamPointer = 0;
     $this->streamWrite = FALSE;
@@ -349,7 +350,7 @@ class CloudinaryStreamWrapper implements StreamWrapperInterface {
    * @return string
    *   A web accessible URL for the resource.
    */
-  public function getExternalUrl(string $uri = NULL) {
+  public function getExternalUrl(?string $uri = NULL) {
     $uri = $uri ?? $this->uri;
     $info = static::parseUri($uri);
 
@@ -540,7 +541,6 @@ class CloudinaryStreamWrapper implements StreamWrapperInterface {
     return TRUE;
   }
 
-
   /**
    * Change stream options.
    *
@@ -686,7 +686,6 @@ class CloudinaryStreamWrapper implements StreamWrapperInterface {
     return $this->streamPointer >= strlen($this->streamData);
   }
 
-
   /**
    * Support for fseek().
    *
@@ -746,11 +745,11 @@ class CloudinaryStreamWrapper implements StreamWrapperInterface {
         $dirname = '';
       }
 
-      $options = array(
+      $options = [
         'public_id' => $public_id,
         AssetType::KEY => $this->resourceType,
         'tags' => CLOUDINARY_STREAM_WRAPPER_FOLDER_TAG_PREFIX . $dirname,
-      );
+      ];
 
       if (cloudinary_stream_wrapper_create_file($base64_data, $options)) {
         return TRUE;
@@ -983,7 +982,7 @@ class CloudinaryStreamWrapper implements StreamWrapperInterface {
     $resource = $this->loadResource($uri);
 
     if ($resource) {
-      $list = array('.', '..');
+      $list = ['.', '..'];
 
       if (isset($this->resource['folders']) && !empty($this->resource['folders'])) {
         $list = array_merge($list, $this->resource['folders']);
@@ -1049,7 +1048,7 @@ class CloudinaryStreamWrapper implements StreamWrapperInterface {
    * @see http://php.net/manual/streamwrapper.dir-closedir.php
    */
   public function dir_closedir() {
-    $this->directoryList = array();
+    $this->directoryList = [];
 
     return TRUE;
   }
diff --git a/modules/cloudinary_video/cloudinary_video.module b/modules/cloudinary_video/cloudinary_video.module
index c476053..e69a3e2 100644
--- a/modules/cloudinary_video/cloudinary_video.module
+++ b/modules/cloudinary_video/cloudinary_video.module
@@ -5,9 +5,9 @@
  * Cloudinary video integration.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\cloudinary_video\Hook\CloudinaryVideoHooks;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
 
 /**
  * Implements hook_form_FORM_ID_alter() for cloudinary_sdk_settings().
@@ -16,86 +16,9 @@ use Drupal\Core\Url;
  *
  * @see cloudinary_sdk_settings()
  */
+#[LegacyHook]
 function cloudinary_video_form_cloudinary_sdk_settings_alter(&$form, $form_state) {
-  $config = \Drupal::config('cloudinary_video.settings');
-
-  $video_optimizations = $config->get('cloudinary_video_optimizations') ?? [];
-  $form['optimizations']['cloudinary_video_optimizations'] = [
-    '#type' => 'textarea',
-    '#title' => t('Default optimizations for video'),
-    '#default_value' => $video_optimizations,
-    '#description' => t('Represents a different component (separated by a "/"), for example: ar_1:1,c_fill,g_auto,w_300/e_blur:50/r_max. Check official @docs to get more details.', [
-      '@docs' => Link::fromTextAndUrl(
-        t('docs'),
-        Url::fromUri('https://cloudinary.com/documentation/video_manipulation_and_delivery')->setOption('attributes', ['target' => '_blank'])
-      )->toString(),
-    ]),
-    '#rows' => 2,
-    '#weight' => 2,
-  ];
-
-  $form['video'] = [
-    '#type' => 'fieldset',
-    '#title' => t('Video playback'),
-    '#collapsible' => TRUE,
-    '#collapsed' => FALSE,
-  ];
-
-  $form['video']['cloudinary_default_player'] = [
-    '#type' => 'radios',
-    '#title' => t('Default player'),
-    '#options' => [
-      'html5' => t('HTML 5 Player'),
-      'cloudinary' => t('Cloudinary Video player'),
-    ],
-    '#default_value' => $config->get('cloudinary_default_player'),
-    '#required' => TRUE,
-  ];
-
-  $form['video']['cloudinary_default_player_config'] = [
-    '#type' => 'textfield',
-    '#title' => t('Default HTML5 player configuration'),
-    '#default_value' => $config->get('cloudinary_default_player_config') ?? '',
-    '#description' => t('Provide list of attributes separated by space, e.g <strong>controls loop preload="auto"</strong>. Check available player attributes @here.', [
-      '@here' => Link::fromTextAndUrl(
-        t('here'),
-        Url::fromUri('https://www.w3schools.com/tags/tag_video.asp')->setOption('attributes', ['target' => '_blank'])
-      )->toString(),
-      '@link' => Link::fromTextAndUrl(
-        t('Video Player studio'),
-        Url::fromUri('https://studio.cloudinary.com')->setOption('attributes', ['target' => '_blank'])
-      )->toString(),
-    ]),
-    '#states' => [
-      'visible' => [
-        'input[name="cloudinary_default_player"]' => ['value' => 'html5'],
-      ],
-    ],
-  ];
-
-  $form['video']['cloudinary_video_player_config'] = [
-    '#type' => 'textarea',
-    '#title' => t('Default Cloudinary player configuration (JSON)'),
-    '#default_value' => $config->get('cloudinary_video_player_config') ?? '',
-    '#description' => t('Check available player options @here, or use the @link to try out different configurations.', [
-      '@here' => Link::fromTextAndUrl(
-        t('here'),
-        Url::fromUri('https://cloudinary.com/documentation/video_player_api_reference#configuration_options')->setOption('attributes', ['target' => '_blank'])
-      )->toString(),
-      '@link' => Link::fromTextAndUrl(
-        t('Video Player studio'),
-        Url::fromUri('https://studio.cloudinary.com')->setOption('attributes', ['target' => '_blank'])
-      )->toString(),
-    ]),
-    '#states' => [
-      'visible' => [
-        'input[name="cloudinary_default_player"]' => ['value' => 'cloudinary'],
-      ],
-    ],
-  ];
-
-  $form['#validate'][] = 'cloudinary_video_settings_validate';
-  $form['#submit'][] = 'cloudinary_video_settings_submit';
+  \Drupal::service(CloudinaryVideoHooks::class)->formCloudinarySdkSettingsAlter($form, $form_state);
 }
 
 /**
@@ -134,23 +57,15 @@ function cloudinary_video_settings_submit($form, FormStateInterface $form_state)
 /**
  * Implements hook_theme().
  */
+#[LegacyHook]
 function cloudinary_video_theme($existing, $type, $theme, $path) {
-  return [
-    'cloudinary_video_player' => [
-      'variables' => [
-        'player_type' => NULL,
-        'responsive' => NULL,
-        'children' => NULL,
-      ],
-    ],
-  ];
+  return \Drupal::service(CloudinaryVideoHooks::class)->theme($existing, $type, $theme, $path);
 }
 
 /**
  * Implements hook_theme_suggestions_HOOK() for cloudinary_video_player.
  */
+#[LegacyHook]
 function cloudinary_video_theme_suggestions_cloudinary_video_player(array $variables) {
-  $suggestions[] = 'cloudinary_video_player__' . $variables['player_type'];
-
-  return $suggestions;
+  return \Drupal::service(CloudinaryVideoHooks::class)->themeSuggestionsCloudinaryVideoPlayer($variables);
 }
diff --git a/modules/cloudinary_video/cloudinary_video.services.yml b/modules/cloudinary_video/cloudinary_video.services.yml
new file mode 100644
index 0000000..6a906d6
--- /dev/null
+++ b/modules/cloudinary_video/cloudinary_video.services.yml
@@ -0,0 +1,5 @@
+
+services:
+  Drupal\cloudinary_video\Hook\CloudinaryVideoHooks:
+    class: Drupal\cloudinary_video\Hook\CloudinaryVideoHooks
+    autowire: true
diff --git a/modules/cloudinary_video/src/Hook/CloudinaryVideoHooks.php b/modules/cloudinary_video/src/Hook/CloudinaryVideoHooks.php
new file mode 100644
index 0000000..74fd672
--- /dev/null
+++ b/modules/cloudinary_video/src/Hook/CloudinaryVideoHooks.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Drupal\cloudinary_video\Hook;
+
+use Drupal\Core\Url;
+use Drupal\Core\Link;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for cloudinary_video.
+ */
+class CloudinaryVideoHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_form_FORM_ID_alter() for cloudinary_sdk_settings().
+   *
+   * Alters the setting form for Cloudinary settings.
+   *
+   * @see cloudinary_sdk_settings()
+   */
+  #[Hook('form_cloudinary_sdk_settings_alter')]
+  public function formCloudinarySdkSettingsAlter(&$form, $form_state) {
+    $config = \Drupal::config('cloudinary_video.settings');
+    $video_optimizations = $config->get('cloudinary_video_optimizations') ?? [];
+    $form['optimizations']['cloudinary_video_optimizations'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Default optimizations for video'),
+      '#default_value' => $video_optimizations,
+      '#description' => $this->t('Represents a different component (separated by a "/"), for example: ar_1:1,c_fill,g_auto,w_300/e_blur:50/r_max. Check official @docs to get more details.', [
+        '@docs' => Link::fromTextAndUrl($this->t('docs'), Url::fromUri('https://cloudinary.com/documentation/video_manipulation_and_delivery')->setOption('attributes', [
+          'target' => '_blank',
+        ]))->toString(),
+      ]),
+      '#rows' => 2,
+      '#weight' => 2,
+    ];
+    $form['video'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Video playback'),
+      '#collapsible' => TRUE,
+      '#collapsed' => FALSE,
+    ];
+    $form['video']['cloudinary_default_player'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Default player'),
+      '#options' => [
+        'html5' => $this->t('HTML 5 Player'),
+        'cloudinary' => $this->t('Cloudinary Video player'),
+      ],
+      '#default_value' => $config->get('cloudinary_default_player'),
+      '#required' => TRUE,
+    ];
+    $form['video']['cloudinary_default_player_config'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Default HTML5 player configuration'),
+      '#default_value' => $config->get('cloudinary_default_player_config') ?? '',
+      '#description' => $this->t('Provide list of attributes separated by space, e.g <strong>controls loop preload="auto"</strong>. Check available player attributes @here.', [
+        '@here' => Link::fromTextAndUrl($this->t('here'), Url::fromUri('https://www.w3schools.com/tags/tag_video.asp')->setOption('attributes', [
+          'target' => '_blank',
+        ]))->toString(),
+        '@link' => Link::fromTextAndUrl($this->t('Video Player studio'), Url::fromUri('https://studio.cloudinary.com')->setOption('attributes', [
+          'target' => '_blank',
+        ]))->toString(),
+      ]),
+      '#states' => [
+        'visible' => [
+          'input[name="cloudinary_default_player"]' => [
+            'value' => 'html5',
+          ],
+        ],
+      ],
+    ];
+    $form['video']['cloudinary_video_player_config'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Default Cloudinary player configuration (JSON)'),
+      '#default_value' => $config->get('cloudinary_video_player_config') ?? '',
+      '#description' => $this->t('Check available player options @here, or use the @link to try out different configurations.', [
+        '@here' => Link::fromTextAndUrl($this->t('here'), Url::fromUri('https://cloudinary.com/documentation/video_player_api_reference#configuration_options')->setOption('attributes', [
+          'target' => '_blank',
+        ]))->toString(),
+        '@link' => Link::fromTextAndUrl($this->t('Video Player studio'), Url::fromUri('https://studio.cloudinary.com')->setOption('attributes', [
+          'target' => '_blank',
+        ]))->toString(),
+      ]),
+      '#states' => [
+        'visible' => [
+          'input[name="cloudinary_default_player"]' => [
+            'value' => 'cloudinary',
+          ],
+        ],
+      ],
+    ];
+    $form['#validate'][] = 'cloudinary_video_settings_validate';
+    $form['#submit'][] = 'cloudinary_video_settings_submit';
+  }
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public static function theme($existing, $type, $theme, $path) {
+    return [
+      'cloudinary_video_player' => [
+        'variables' => [
+          'player_type' => NULL,
+          'responsive' => NULL,
+          'children' => NULL,
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK() for cloudinary_video_player.
+   */
+  #[Hook('theme_suggestions_cloudinary_video_player')]
+  public static function themeSuggestionsCloudinaryVideoPlayer(array $variables) {
+    $suggestions[] = 'cloudinary_video_player__' . $variables['player_type'];
+    return $suggestions;
+  }
+
+}
diff --git a/modules/cloudinary_video/src/Plugin/CloudinaryMedia/CloudinaryVideo.php b/modules/cloudinary_video/src/Plugin/CloudinaryMedia/CloudinaryVideo.php
index a60d7c1..523c849 100644
--- a/modules/cloudinary_video/src/Plugin/CloudinaryMedia/CloudinaryVideo.php
+++ b/modules/cloudinary_video/src/Plugin/CloudinaryMedia/CloudinaryVideo.php
@@ -44,7 +44,7 @@ class CloudinaryVideo extends CloudinaryMediaBase implements CloudinaryMediaPlug
   /**
    * {@inheritdoc}
    */
-  public function createMedia(Asset $asset, string $bundle, FileInterface $file = NULL): MediaInterface {
+  public function createMedia(Asset $asset, string $bundle, ?FileInterface $file = NULL): MediaInterface {
     if ($media = $this->findMedia($asset, $bundle)) {
       return $media;
     }
diff --git a/src/Hook/CloudinaryHooks.php b/src/Hook/CloudinaryHooks.php
new file mode 100644
index 0000000..ca1c972
--- /dev/null
+++ b/src/Hook/CloudinaryHooks.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Drupal\cloudinary\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for cloudinary.
+ */
+class CloudinaryHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public static function theme() {
+    return [
+      'cloudinary_crop_summary' => [
+        'variables' => [
+          'data' => NULL,
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_theme_registry_alter().
+   */
+  #[Hook('theme_registry_alter')]
+  public static function themeRegistryAlter(&$theme_registry) {
+    foreach ($theme_registry['image_style_preview']['preprocess functions'] as &$function) {
+      if ($function == 'template_preprocess_image_style_preview') {
+        $function = 'cloudinary_preprocess_image_style_preview';
+        break;
+      }
+    }
+  }
+
+  /**
+   * Implements hook_cloudinary_stream_wrapper_transformation().
+   */
+  #[Hook('cloudinary_stream_wrapper_transformation')]
+  public function cloudinaryStreamWrapperTransformation() {
+    $path = \Drupal::service('extension.list.module')->getPath('cloudinary');
+    return [
+      'cloudinary_crop' => [
+        'title' => $this->t('Cloudinary crop'),
+        'callback' => 'cloudinary_transformation_cloudinary_crop',
+        'file' => $path . '/includes/cloudinary.transformation.cloudinary.inc',
+      ],
+      'cloudinary_named_transformation' => [
+        'title' => $this->t('Cloudinary named transformation'),
+        'callback' => 'cloudinary_transformation_cloudinary_named_transformation',
+        'file' => $path . '/includes/cloudinary.transformation.cloudinary.inc',
+      ],
+      'image_crop' => [
+        'title' => $this->t('Crop'),
+        'callback' => 'cloudinary_transformation_image_crop',
+        'file' => $path . '/includes/cloudinary.transformation.drupal.inc',
+      ],
+      'image_desaturate' => [
+        'title' => $this->t('Desaturate'),
+        'callback' => 'cloudinary_transformation_image_desaturate',
+      ],
+      'image_resize' => [
+        'title' => $this->t('Resize'),
+        'callback' => 'cloudinary_transformation_image_resize',
+      ],
+      'image_rotate' => [
+        'title' => $this->t('Rotate'),
+        'callback' => 'cloudinary_transformation_image_rotate',
+      ],
+      'image_scale' => [
+        'title' => $this->t('Scale'),
+        'callback' => 'cloudinary_transformation_image_scale',
+      ],
+      'image_scale_and_crop' => [
+        'title' => $this->t('Scale and crop'),
+        'callback' => 'cloudinary_transformation_image_scale_and_crop',
+      ],
+      'focal_point_scale_and_crop' => [
+        'title' => $this->t('Focal Point Scale and Crop'),
+        'callback' => 'cloudinary_transformation_image_scale_and_crop',
+        'file' => $path . '/includes/cloudinary.transformation.drupal.inc',
+      ],
+    ];
+  }
+
+}
