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

# Media

> Managing images and files on Lunar models using Spatie's Media Library.

Lunar uses Spatie's Media Library package to manage images and files across models.

## Overview

Lunar uses the [Laravel-medialibrary](https://spatie.be/docs/laravel-medialibrary) package by Spatie to handle media across all models. Rather than reinvent the wheel, Lunar leverages this battle-tested package and adds its own conventions on top, including per-model media definition classes, automatic image conversions, and primary image management.

### Supported Models

The following models support media out of the box via the `Lunar\Core\Models\Concerns\HasMedia` trait:

| Model | Class |
| - | - |
| Product | `Lunar\Core\Models\Product` |
| Product Type | `Lunar\Core\Models\ProductType` |
| Product Option | `Lunar\Core\Models\ProductOption` |
| Product Option Value | `Lunar\Core\Models\ProductOptionValue` |
| Collection | `Lunar\Core\Models\Collection` |
| Brand | `Lunar\Core\Models\Brand` |
| Asset | `Lunar\Core\Models\Asset` |

<Info>
  `Lunar\Core\Models\ProductVariant` does not use the `HasMedia` trait. Instead, it uses a many-to-many relationship with media through a pivot table. See [Product Variant Images](#product-variant-images) for details.
</Info>

## Configuration

The media configuration is published at `config/lunar/media.php`.

```php theme={null}
use Lunar\Core\Media\StandardDefinitions;

return [
    'definitions' => [
        'asset' => StandardDefinitions::class,
        'brand' => StandardDefinitions::class,
        'collection' => StandardDefinitions::class,
        'product' => StandardDefinitions::class,
        'product-option' => StandardDefinitions::class,
        'product-option-value' => StandardDefinitions::class,
        'product_type' => StandardDefinitions::class,
    ],

    'collection' => 'images',

    'max_upload_kb' => 8192,

    'fallback' => [
        'url' => env('FALLBACK_IMAGE_URL', null),
        'path' => env('FALLBACK_IMAGE_PATH', null),
    ],
];
```

| Key | Description |
| - | - |
| `definitions` | Maps a model alias to its media definition class. Each model can have its own definition class to customize collections and conversions |
| `collection` | The default media collection name used across all models |
| `max_upload_kb` | Maximum accepted upload size per image, in kilobytes |
| `fallback.url` | A fallback URL returned when a model has no media |
| `fallback.path` | A fallback file path returned when a model has no media |

The `definitions` keys use aliases derived from the model class name (a snake-cased basename, e.g. `Product` becomes `product`, `ProductOptionValue` becomes `product-option-value`, `ProductType` becomes `product_type`). The fully qualified class name can also be used as the key.

## Adding Media

Adding media to a model follows the standard Spatie Media Library API.

```php theme={null}
use Lunar\Core\Models\Product;

$product = Product::find(123);

$product->addMedia($request->file('image'))->toMediaCollection('images');
```

For more information, see [Associating files](https://spatie.be/docs/laravel-medialibrary/v11/basic-usage/associating-files) in the Spatie documentation.

## Retrieving Media

```php theme={null}
use Lunar\Core\Models\Product;

$product = Product::find(123);

// Get all images
$product->getMedia('images');

// Get the first image URL
$product->getFirstMediaUrl('images');

// Get a specific conversion URL
$product->getFirstMediaUrl('images', 'medium');
```

For more information, see [Retrieving media](https://spatie.be/docs/laravel-medialibrary/v11/basic-usage/retrieving-media) in the Spatie documentation.

## Primary Images

Lunar adds a concept of "primary" images on top of Spatie's media library. Each model can have one image marked as primary per collection, tracked via a `primary` custom property on the media item.

### Thumbnail Relationship

Models using the `HasMedia` trait have a `thumbnail` relationship that returns the primary image, scoped to the configured media collection (`config('lunar.media.collection')`, `images` by default):

```php theme={null}
use Lunar\Core\Models\Product;

$product = Product::find(123);

// Get the primary image via the thumbnail relationship
$product->thumbnail;
```

### Automatic Primary Management

Lunar includes a `Lunar\Core\Observers\MediaObserver` that automatically enforces the following rules for the configured media collection:

* When a media item is marked as primary, all other items in the same collection are unmarked.
* When the primary image is deleted, the first remaining image in the collection is automatically promoted to primary.
* When a new image is added to an empty collection, it is automatically marked as primary.

### Setting the Primary Image

```php theme={null}
$media = $product->getMedia('images')->first();

$media->setCustomProperty('primary', true)->save();
```

## Product Variant Images

`Lunar\Core\Models\ProductVariant` handles media differently from other models. Instead of using the `HasMedia` trait, it uses a many-to-many relationship with the `media` table through a `media_product_variant` pivot table.

```php theme={null}
use Lunar\Core\Models\ProductVariant;

$variant = ProductVariant::find(456);

// Get all images (ordered by position)
$variant->images;

// Get the thumbnail: the pivot-primary image, or the parent product's thumbnail
$variant->getThumbnail();
```

### Relationships

| Relationship | Type | Related Model | Description |
| :- | :- | :- | :- |
| `images` | BelongsToMany | `Spatie\MediaLibrary\MediaCollections\Models\Media` | Images assigned to the variant, ordered by pivot `position` |

### Pivot columns (`media_product_variant`)

| Column | Type | Description |
| - | - | - |
| `primary` | `boolean` | Whether this is the primary image for the variant, default `false` |
| `position` | `smallInteger` | Display order of the image, default `1` |

`getThumbnail()` returns the image whose pivot `primary` flag is set; if none is marked, it falls back to the parent product's `thumbnail`.

## Fallback Images

If a model has no media, calling `getFirstMediaUrl` or `getFirstMediaPath` returns an empty string by default. Fallback images can be configured in `config/lunar/media.php` or via environment variables:

```php theme={null}
'fallback' => [
    'url' => env('FALLBACK_IMAGE_URL', null),
    'path' => env('FALLBACK_IMAGE_PATH', null),
]
```

These values are passed to Spatie's `useFallbackUrl` and `useFallbackPath` methods on the media collection.

## Default Conversions

`Lunar\Core\Media\StandardDefinitions`, the default media definition class, registers the following image conversions:

| Conversion | Width | Height | Notes |
| - | - | - | - |
| `small` | 300 | 300 | Registered globally (runs for all collections). Used by admin panel thumbnails |
| `medium` | 500 | 500 | Registered on the configured media collection |
| `large` | 800 | 800 | Registered on the configured media collection |
| `zoom` | 500 | 500 | Registered on the configured media collection |

All conversions use `Spatie\Image\Enums\Fit::Fill` for sizing, a white background and border, and preserve the original image format.

## Custom Media Definitions

To customize the media collections and conversions for a model, create a class that implements `Lunar\Core\Contracts\MediaDefinitions`:

```php theme={null}
namespace App\Media;

use Lunar\Core\Contracts\MediaDefinitions;
use Spatie\Image\Enums\Fit;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\MediaCollections\MediaCollection;
use Spatie\MediaLibrary\MediaCollections\Models\Media;

class CustomMediaDefinitions implements MediaDefinitions
{
    public function registerMediaConversions(HasMedia $model, ?Media $media = null): void
    {
        $model->addMediaConversion('small')
            ->fit(Fit::Fill, 300, 300)
            ->sharpen(10)
            ->keepOriginalImageFormat();
    }

    public function registerMediaCollections(HasMedia $model): void
    {
        $fallbackUrl = config('lunar.media.fallback.url');
        $fallbackPath = config('lunar.media.fallback.path');

        // Reset to avoid duplication
        $model->mediaCollections = [];

        $collection = $model->addMediaCollection(config('lunar.media.collection'));

        if ($fallbackUrl) {
            $collection = $collection->useFallbackUrl($fallbackUrl);
        }

        if ($fallbackPath) {
            $collection = $collection->useFallbackPath($fallbackPath);
        }

        $this->registerCollectionConversions($collection, $model);
    }

    protected function registerCollectionConversions(MediaCollection $collection, HasMedia $model): void
    {
        $conversions = [
            'zoom' => ['width' => 500, 'height' => 500],
            'large' => ['width' => 800, 'height' => 800],
            'medium' => ['width' => 500, 'height' => 500],
        ];

        $collection->registerMediaConversions(function (Media $media) use ($model, $conversions) {
            foreach ($conversions as $key => $conversion) {
                $model->addMediaConversion($key)
                    ->fit(Fit::Fill, $conversion['width'], $conversion['height'])
                    ->keepOriginalImageFormat();
            }
        });
    }

    public function getMediaCollectionTitles(): array
    {
        return ['images' => 'Images'];
    }

    public function getMediaCollectionDescriptions(): array
    {
        return ['images' => ''];
    }
}
```

Then register the class in `config/lunar/media.php`:

```php theme={null}
'definitions' => [
    'product' => \App\Media\CustomMediaDefinitions::class,
    // ...
],
```

### Regenerating Conversions

After changing conversion definitions, regenerate existing conversions using the Spatie artisan command:

```bash theme={null}
php artisan media-library:regenerate
```

This creates queue jobs for each media entry to be reprocessed. See the [Spatie documentation](https://spatie.be/docs/laravel-medialibrary/v11/converting-images/regenerating-images) for more options.

## Extending Your Own Models

Custom models can be given media support using the Lunar `HasMedia` trait:

```php theme={null}
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Lunar\Core\Models\Concerns\HasMedia;
use Spatie\MediaLibrary\HasMedia as SpatieHasMedia;

class YourCustomModel extends Model implements SpatieHasMedia
{
    use HasMedia;
}
```

This provides access to all Lunar media features, including per-model media definitions and automatic conversions.

<Warning>
  To use Lunar's media definitions and conversions, models must use the `Lunar\Core\Models\Concerns\HasMedia` trait. Using Spatie's `InteractsWithMedia` trait directly bypasses Lunar's media definition system. See the [Spatie documentation](https://spatie.be/docs/laravel-medialibrary/v11/basic-usage/preparing-your-model) for using the library directly.
</Warning>

### Definition Resolution

The `HasMedia` trait resolves the media definition class for a model using the following priority:

1. A snake\_case alias in `config('lunar.media.definitions')` (e.g. `product` for `Product`)
2. The fully qualified class name in the config
3. The parent class name in the config (useful for extended models)
4. Falls back to `Lunar\Core\Media\StandardDefinitions`


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.