> ## Documentation Index
> Fetch the complete documentation index at: https://cloudinary.com/documentation/llms.txt
> Use this file to discover all available pages before exploring further.

# Magento Extension: Developer guide



## Introduction

* The Cloudinary Adobe Commerce (Magento) Extension leverages the [Cloudinary PHP SDK](php_integration) to authenticate, manage assets, and generate dynamic media URLs.
* The extension automatically handles media URL generation for standard Magento entities like products, swatches, and some CMS/Widget blocks. However, custom code and third-party extensions often require manual integration to utilize Cloudinary URLs.
* This guide is for developers aiming to extend Cloudinary support to custom functionalities, integrate with third-party extensions, or enhance the official extension with additional business logic.

## Before adding code

* For simple use cases where you only need a Cloudinary URL for a known asset and don't require Magento's fallback mechanisms or dynamic transformations based on extension settings, consider using the Cloudinary asset URL directly (e.g., from the Cloudinary DAM).
* The primary downside of using raw Cloudinary URLs is the lack of Magento-side fallback to default media URLs if an asset isn't available on Cloudinary or if the extension is disabled.

## Core concepts

### Cloudinary PHP SDK integration

The extension stores your Cloudinary credentials, modifies Magento to serve media assets from Cloudinary, and embeds Cloudinary widgets and players into Magento product pages. The powerful and well-documented Cloudinary PHP SDK is used by the extension to sync assets between Cloudinary and Magento, and for URL generation, among other things.

### Getting Cloudinary configuration

* If your use case is not covered by the extension's `UrlGenerator` class, you may need to use the SDK directly. You'll need to initialize it with your Cloudinary account details (cloud name, API key, API secret) which are stored by the Magento extension.
    * Images synced from Magento to Cloudinary typically use their Magento file path (starting from `media/`) as the public ID (e.g., `catalog/product/s/a/sample.jpg`).
    * You can use the path with the file extension (e.g., `sample.jpg`) or without (`sample`) as the public ID.

### Fallback URLs

The Magento extension includes mechanisms (like its `ImageFactory`) to serve local Magento URLs if the Cloudinary extension is disabled or if a specific image hasn't been synced to Cloudinary. This is crucial for frontend display.

## Automatic setup: Use extension classes (recommended)

When the extension's `UrlGenerator` class is used, the underlying Cloudinary SDK is instantiated for you.

## Manual setup: Initializing a Cloudinary PHP SDK instance

The recommended approach for URL generation is to use the extension's `UrlGenerator` class. However, for more advanced use cases you may need to instantiate and configure the Cloudinary SDK using the credentials stored by the Magento 2 extension. You can do this by injecting the extension's `ConfigurationInterface` and `ConfigurationBuilder` into your class.

### Option 1: Instance-scoped SDK (recommended if you must use the SDK directly)

This approach creates an instance-scoped, encapsulated Cloudinary SDK instance. It's generally preferred for better dependency management and testability. You'll use helper methods on this instance (e.g., `$this->cld->image('public_id')`).

```php
namespace Your\Namespace;

use Cloudinary\Cloudinary\Core\ConfigurationInterface;
use Cloudinary\Cloudinary\Core\ConfigurationBuilder;
use Cloudinary\Cloudinary;
class YourClass {

  private Cloudinary $cld;
  public function __construct(
      ConfigurationInterface $configuration,
      ConfigurationBuilder $configurationBuilder,
  ) {
   if ($configuration->isEnabled()) {
      $this->cld = new Cloudinary($configurationBuilder->build());
   }
  }
  public function generateTag(): string {
      return $this->cld->imageTag('sample')->toTag();
  }
}
```

### Option 2: Application-wide SDK configuration

This approach configures a single, application-wide SDK instance. Use this if you need the SDK in multiple, disparate parts of your code to avoid passing around a Cloudinary object. After this setup, you'll use SDK classes directly (e.g., `new \Cloudinary\Asset\Image('public_id')`).

```php
namespace Your\Namespace;

use Cloudinary\Cloudinary\Core\ConfigurationInterface;
use Cloudinary\Cloudinary\Core\ConfigurationBuilder;
use Cloudinary\Configuration\Configuration;
use Cloudinary\Tag\ImageTag;
class YourClass {

  private bool $_initialized;
  private ConfigurationInterface $configuration;
  private ConfigurationBuilder $configurationBuilder;
  public function __construct(
      ConfigurationInterface $configuration,
      ConfigurationBuilder $configurationBuilder,
  ) {
      $this->configuration = $configuration;
      $this->configurationBuilder = $configurationBuilder;
      $this->configure();
  }
  private function configure()
  {
      if (!$this->_initialized && $this->configuration->isEnabled()) {
          Configuration::instance($this->configurationBuilder->build());
          $this->_initialized = true;
      }
  }
  public function generateTag(): string {
       return (new ImageTag('sample'))->toTag();
  }
}
```

## Generating URLs

### From file paths using the Cloudinary Magento extension's `UrlGenerator` class  (recommended)

The most straightforward way to obtain Cloudinary URLs is using the extension's `UrlGenerator` class.

The benefit of this approach is that the PHP SDK configuration and fallback logic for URL generation are automatically handled.

```php
namespace Your\Namespace;

use Cloudinary\Cloudinary\Core\Image\ImageFactory;
use Cloudinary\Cloudinary\Core\ImageInterface;
use Cloudinary\Cloudinary\Core\UrlGenerator;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Framework\UrlInterface;

class YourClass
{
  private UrlGenerator $urlGenerator;

  private ImageFactory $imageFactory;

  private StoreManagerInterface $storeManager;

  public function __construct(
    StoreManagerInterface $storeManager,
    ImageFactory $imageFactory,
    UrlGenerator $urlGenerator
  ) {
      $this->storeManager = $storeManager;
      $this->imageFactory = $imageFactory;
      $this->urlGenerator = $urlGenerator;
  }


  /**
   * @param $imagePath file path relative to Magento media dir, e.g., "catalog/product/s/a/sample.jpg"
   * @return ImageInterface
   */
  public function getImage(string $imagePath): ImageInterface
  {
      $baseMediaUrl = $this->storeManager->getStore()
        ->getBaseUrl(UrlInterface::URL_TYPE_MEDIA);


      // The fallback anonymous function will be executed when __toString()
      // is called on a LocalImage class, which is returned by the ImageFactory
      // when the Cloudinary extension is disabled or when the image is not synced.
      // The fallback function will usually inherit variables from
      // the parent scope to generate the original image URL.
      $fallback = function() use ($baseMediaUrl, $imagePath) {
          // $imagePath is passed from the parent scope;
          return $baseMediaUrl . $imagePath;
      };


      // Returns Cloudinary\Cloudinary\Core\Image if the Cloudinary extension is enabled
      // and the image is synced, or a LocalImage if the extension is disabled
      // both classes implement the ImageInterface interface.
      //
      /* @var Cloudinary\Cloudinary\Core\ImageInterface $image */
      $image = $this->imageFactory->build(
          $imagePath,
          $fallback,
      );

      return $image;
  }

  public function getImageUrl(string $imagePath): string
  {
      $image = $this->getImage($imagePath);
      return $this->urlGenerator->generateFor($image);
  }
}
```

### From file paths using the extension's `ImageFactory` alongside the SDK

For frontend rendering where you need a reliable URL (either Cloudinary or a local Magento fallback), the extension's `ImageFactory` is invaluable. It checks if the image is synced and if the Cloudinary extension is active.

```php
namespace Your\Namespace;

use Cloudinary\Cloudinary\Core\ConfigurationInterface;
use Cloudinary\Cloudinary\Core\ConfigurationBuilder;
use Cloudinary\Cloudinary\Core\Image\ImageFactory;
use Cloudinary\Cloudinary\Core\Image\LocalImage;
use Cloudinary\Cloudinary\Core\ImageInterface;
use Cloudinary\Cloudinary;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Framework\UrlInterface;

class YourClass
{
  private Cloudinary $cld;

  private ImageFactory $imageFactory;

  private StoreManagerInterface $storeManager;

  public function __construct(
    ConfigurationInterface $configuration,
    ConfigurationBuilder $configurationBuilder,
    StoreManagerInterface $storeManager,
    ImageFactory $imageFactory,
  ) {
      $this->storeManager = $storeManager;
      $this->imageFactory = $imageFactory;

      if ($configuration->isEnabled()) {
        $this->cld = new Cloudinary($configurationBuilder->build());
      }
  }

  public function getImage(string $imagePath): ImageInterface
  {
      $baseMediaUrl = $this->storeManager
                           ->getStore()
                           ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA);

      $fallback = function() use ($baseMediaUrl, $imagePath) {
          return $baseMediaUrl . $imagePath;
      };

      $image = $this->imageFactory->build(
          $imagePath,
          $fallback,
      );

      return $image;
  }

  public function getImageUrl(string $imagePath): string
  {
      $image = $this->getImage($imagePath);

      if (!$this->cld || $image instanceof LocalImage) {
          // If the image is a LocalImage, return the result of the
          // fallback function, which should return the original image URL.
          return (string) $image;
      }


     // There is no need to remove the file extension if it still exists,
     // as the Cloudinary SDK will handle it correctly.
      $publicId = $image->getId();

      return $this->cld->image($publicId)->toUrl();
  }
}
```

### From URLs

On occasion, you'll only have access to the Magento URL and not the file path. In such cases, you'll need to perform string manipulation to extract the original path. 

Examples of string manipulations from the extension source code:

* [Plugin around the `create` method of Catalog\Block\Product\ImageFactory](https://github.com/cloudinary/cloudinary_magento2/blob/af7211d25b0aac214aa1c8e3eb54c88213a49f7a/Plugin/Catalog/Block/Product/ImageFactory.php#L170)
* [Plugin around the `getUrl` method of Catalog\Model\Product\Image\UrlBuilder](https://github.com/cloudinary/cloudinary_magento2/blob/af7211d25b0aac214aa1c8e3eb54c88213a49f7a/Plugin/Catalog/Model/Product/Image/UrlBuilder.php#L140)
* [Plugin around the `mediaDirective` method of Widget\Model\Template\Filter](https://github.com/cloudinary/cloudinary_magento2/blob/af7211d25b0aac214aa1c8e3eb54c88213a49f7a/Plugin/Widget/Model/Template/Filter.php#L85)

## Working with transformations

### Using the extension's default transformations

The Magento extension defines default transformations (e.g., auto-format, auto-quality). 

When the extension's `UrlGenerator` class is used, these default transformations are automatically applied to the generated URL.

For more advanced use cases, you can retrieve these settings and apply them using the SDK.

```php
namespace Your\Namespace;

use Cloudinary\Cloudinary\Core\ConfigurationInterface;
use Cloudinary\Cloudinary\Core\ConfigurationBuilder;
use Cloudinary\Cloudinary\Core\Image\ImageFactory;
use Cloudinary\Cloudinary\Core\ImageInterface;
use Cloudinary\Cloudinary\Core\Image\LocalImage;
use Cloudinary\Cloudinary;
use Magento\Store\Model\StoreManagerInterface;
use Cloudinary\Cloudinary\Core\Image\Transformation\Freeform;
use Cloudinary\Transformation\Transformation;
use Magento\Framework\UrlInterface;

class YourClass
{
 private Cloudinary $cld;

 private ImageFactory $imageFactory;

 private StoreManagerInterface $storeManager;

 private ConfigurationInterface $configuration;

 public function __construct(
   ConfigurationInterface $configuration,
   ConfigurationBuilder $configurationBuilder,
   StoreManagerInterface $storeManager,
   ImageFactory $imageFactory,
 ) {
     $this->configuration = $configuration;
     $this->storeManager = $storeManager;
     $this->imageFactory = $imageFactory;

     if ($configuration->isEnabled()) {
       $this->cld = new Cloudinary($configurationBuilder->build());
     }
 }

 /**
  * @param $imagePath file path relative to Magento media dir, e.g., "catalog/product/s/a/sample.jpg"
  * @return ImageInterface
  */
 public function getImage(string $imagePath): ImageInterface
 {
     $baseMediaUrl = $this->storeManager
       ->getStore()
       ->getBaseUrl(UrlInterface::URL_TYPE_MEDIA);

     $fallback = function() use ($baseMediaUrl, $imagePath) {
         return $baseMediaUrl . $imagePath;
     };

     $image = $this->imageFactory->build(
         $imagePath,
         $fallback,
     );

     return $image;
 }

 /**
  * Get the default transformation for an image.
  *
  * @param bool $isProduct whether to apply custom product transformations
  * @param null|string $freeTransform optional freeform transformation string
  * @return Transformation
  */
 public function getDefaultTransformation(
     bool $isProduct = false,
     ?string $freeTransform = null
 ): Transformation {
     // Returns an extension-specific transformation builder class
     /* @var Cloudinary\Cloudinary\Core\Image\Transformation $transformation */
     $transformation = $this->configuration->getDefaultTransformation($isProduct);
     if ($freeTransform) {
         $transformation->withFreeform(Freeform::fromString($freeTransform));
     }

     // Builds an array of transformation parameters and returns an SDK Transformation class.
     return Transformation::fromParams($transformation->build());
 }

 public function getImageUrl(string $imagePath, $addDefaultTransformation = false): string
 {
     $image = $this->getImage($imagePath);

     if (!$this->cld || $image instanceof LocalImage) {
         return (string) $image;
     }

     // There is no need to remove the file extension if it still exists,
     // as the Cloudinary SDK will handle it correctly.
     $publicId = $image->getId();

     $cloudinaryImage = $this->cld->image($publicId);

     if ($addDefaultTransformation) {
         $cloudinaryImage->setTransformation($this->getDefaultTransformation());
     }

     return $cloudinaryImage->toUrl();
 }
}
```

## Handling videos

The Cloudinary Magento extension stores the raw Cloudinary URLs for product videos with no local copy. The URL is then parsed to extract the public ID and passed to the Cloudinary Product Gallery widget or Video Player. You can store the public ID directly in your own code or store the full URL and leverage the extension's helpers to extract the public ID.

### Using the extension's `parseCloudinaryUrl` method

```php
namespace Your\Namespace;

use Cloudinary\Cloudinary\Core\ConfigurationInterface;
use Cloudinary\Cloudinary\Core\ConfigurationBuilder;
use Cloudinary\Cloudinary;

class YourClass
{
 private ConfigurationInterface $configuration;

 private Cloudinary $cld;

 public function __construct(
   ConfigurationInterface $configuration,
   ConfigurationBuilder $configurationBuilder,
 ) {
     $this->configuration = $configuration;

     if ($configuration->isEnabled()) {
       $this->cld = new Cloudinary($configurationBuilder->build());
     }
 }

 public function getAssetInfo(string $remoteUrl): array {
   $parsed = $this->configuration->parseCloudinaryUrl($remoteUrl);
   return $parsed;
 }

 public function getTag(string $remoteUrl): string {
   $assetInfo = $this->getAssetInfo($remoteUrl);

   $tag = match($assetInfo['type']) {
     'image' => $this->cld->imageTag($assetInfo['publicId']),
     'video' => $this->cld->videoTag($assetInfo['publicId']),
     default => null,
   };

   return (string) $tag;
 }
}
```

## Common strategies for modifying core and third-party code

### Plugins (interceptors)

* Plugins (interceptors) are the most common and Magento-recommended approach for modifying external code behavior.
* Plugins allow you to intercept public methods of other classes. You can change arguments before a method is called (`before`), change the result after it's called (`after`), or wrap the original method entirely (`around`).
* When modifying methods that generate image/video URLs in third-party extensions, you can use the techniques described in [Generating URLs](#generating_urls) within your plugin logic.
* **Caution**: If you resort to parsing HTML returned by a method to replace `<img>` tag `src` attributes, be mindful of performance. Extensive HTML parsing can impact Time To First Byte (TTFB). Ensure your parsing logic is efficient and targeted.

### Dependency injection (preferences)

* You can configure Magento's `ObjectManager` to replace an external class with your own modified class (a subclass or a complete re-implementation).
* This approach is more intrusive than plugins and should be used cautiously. It can fail if the external classes aren't instantiated by Magento's `ObjectManager` (e.g., if they're created directly using the `new` keyword).

## Advanced use

### Direct SDK usage for specific backend tasks

For specific backend tasks, like custom synchronization scripts or batch operations where Magento's fallback or extension-specific logic isn't needed, you can initialize and use the Cloudinary PHP SDK directly. You'd only need to retrieve the Cloudinary configuration from the extension (as shown in the [Setup](#manual_setup_initializing_a_cloudinary_php_sdk_instance) section) and then proceed with full SDK capabilities.

## Troubleshooting / common pitfalls

* **Caching**: Magento relies heavily on caching. After making configuration changes in the Cloudinary extension settings or deploying code changes, always clear relevant Magento caches (e.g., `config`, `block_html`, `full_page`).

## Magento CLI

The Cloudinary integration for Magento includes additional Cloudinary-specific commands that you can run using the Magento CLI.

### Cloudinary commands

| Command | Purpose |
|---------|---------|
| `cloudinary:download:all` | Download images from Cloudinary to your local environment |
| `cloudinary:upload:all` | Upload images to Cloudinary |
| `cloudinary:migration:stop` | Stop the running upload or download process |
| `cloudinary:reset:all` | Reset all module data (destructive) |
| `cloudinary:product-gallery-api-queue:process` | Process the product gallery queue |

### Download images

Download images from Cloudinary to your local `pub/media` directory.

php bin/magento cloudinary:download:all [[options](#download_options)]

#### Options

OPTION | DESCRIPTION
---|---
`-o, --override` | Override existing files
`-f, --force` | Force execution even if the module is disabled
`-e, --env VALUE` | Set the environment (production/staging/dev)
`-i, --include-synchronization` | Download all images, including those already synchronized

#### Examples

Basic download (unsynchronized images only):

```bash
php bin/magento cloudinary:download:all
```

Download and override files in a specific environment:

```bash
php bin/magento cloudinary:download:all -o -e production
```

Download all images, including those already synchronized:

```bash
php bin/magento cloudinary:download:all -i
```

Force download all images with override:

```bash
php bin/magento cloudinary:download:all -f -i -o
```

### Upload images

Upload unsynchronized images to Cloudinary.

php bin/magento cloudinary:upload:all [[options](#upload_options)]

#### Options

OPTION | DESCRIPTION
---|---
`-f, --force` | Force execution even if the module is disabled
`-e, --env VALUE` | Set the environment (production/staging/dev)

#### Example

```bash
php bin/magento cloudinary:upload:all -f -e staging
```

### Stop migration

Stop any running upload or download process.

```bash
php bin/magento cloudinary:migration:stop
```

### Reset module

This command permanently removes ALL Cloudinary data and configuration. This action cannot be undone.

```bash
php bin/magento cloudinary:reset:all
```

* Requires administrator username and password
* Always back up your database before running this command

**After resetting the module, you must:**

1. Clear the cache
2. Reconfigure the module
3. Re-enable the module
4. Run a new migration

### Process product gallery queue

Process pending product gallery tasks.

```bash
php bin/magento cloudinary:product-gallery-api-queue:process
```

This command is typically run via a cron job.

## Additional resources

### Documentation

* [Magento integration documentation](magento_integration)
* [PHP SDK documentation](php_integration)
* [Cloudinary for PHP developers course (two-hour course)](https://training.cloudinary.com/courses/introduction-for-api-users-and-php-developers)

### Source code

* [Cloudinary Magento 2 extension source code](https://github.com/cloudinary/cloudinary_magento2)
* [Cloudinary PHP SDK source code](https://github.com/cloudinary/cloudinary_php)
* [PHP transformation builder SDK source code](https://github.com/cloudinary/php-transformation-builder-sdk)

