Поддержка
Медиа

Драйверы

Контракт MediaDriver, встроенный FileDriver и создание собственных драйверов

Обзор

Вся работа с файлами и изображениями в Frame выполняется через медиа-драйверы. Драйвер — это класс, реализующий контракт MediaDriver, который определяет как файлы загружаются, хранятся, привязываются к моделям и удаляются.

По умолчанию используется встроенный FileDriver, который не требует дополнительных зависимостей.

Контракт MediaDriver

Каждый драйвер реализует пять методов:

upload
UploadedFile → array
Загрузка файла. Принимает UploadedFile, возвращает массив метаданных для фронтенда.
delete
int|string → void
Удаление файла по его идентификатору.
resolve
Model, string, array → ?array
Получение списка медиа для поля модели. Принимает модель, имя поля и массив опций (например, conversion).
fill
Model, string, ?array → void
Привязка медиа к модели. Получает массив медиа-данных и сохраняет их в модель.
export
Model, string → ?string
Экспорт значения поля в строковое представление (пути через запятую).

Полный интерфейс:

packages/core/src/Contracts/MediaDriver.php
namespace Frame\Core\Contracts;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\UploadedFile;

interface MediaDriver
{
    public function upload(UploadedFile $file): array;

    public function delete(int|string $id): void;

    public function resolve(Model $model, string $field, array $options = []): ?array;

    public function fill(Model $model, string $field, ?array $value): void;

    public function export(Model $model, string $field): ?string;
}

Глобальный драйвер

Зарегистрируйте драйвер в сервис-провайдере — все поля File и Image будут его использовать:

app/Providers/AppServiceProvider.php
use Frame\Core\Frame;
use App\Drivers\SpatieMediaDriver;

public function boot(): void
{
    Frame::useMediaDriver(SpatieMediaDriver::class);
}

Драйвер на уровне поля

Метод driver() позволяет переопределить драйвер для конкретного поля:

app/Frame/Resources/UserResource.php
use App\Drivers\CloudinaryDriver;
use App\Drivers\SpatieMediaDriver;

public function fields(): array
{
    return [
        Image::make('avatar')
            ->driver(new SpatieMediaDriver()),

        File::make('documents')
            ->driver(new CloudinaryDriver()),
    ];
}
Драйвер поля имеет приоритет над глобальным. Если поле работает в JSON-режиме (->json()), всегда используется встроенный FileDriver.

FileDriver (встроенный)

FileDriver — драйвер по умолчанию:

  • Загружает файлы через Laravel Storage (диск public, директория frame-uploads)
  • Хранит метаданные в JSON-колонке модели
  • Не требует дополнительных пакетов

Настройка диска и директории через сервис-контейнер:

app/Providers/AppServiceProvider.php
use Frame\Laravel\Drivers\FileDriver;

public function register(): void
{
    $this->app->when(FileDriver::class)
        ->needs('$disk')
        ->give('s3');

    $this->app->when(FileDriver::class)
        ->needs('$directory')
        ->give('uploads');
}

Создание своего драйвера

Создайте класс драйвера

app/Drivers/SpatieMediaDriver.php
namespace App\Drivers;

use Frame\Core\Contracts\MediaDriver;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\UploadedFile;

class SpatieMediaDriver implements MediaDriver
{
    public function upload(UploadedFile $file): array
    {
        // Загрузка, оптимизация, thumbhash...
    }

    public function delete(int|string $id): void
    {
        // Удаление медиа
    }

    public function resolve(Model $model, string $field, array $options = []): ?array
    {
        $conversion = $options['conversion'] ?? '';
        // Получение медиа из коллекции
    }

    public function fill(Model $model, string $field, ?array $value): void
    {
        // Привязка: reorder, copy, удаление неиспользуемых
    }

    public function export(Model $model, string $field): ?string
    {
        // Экспорт путей
    }
}

Зарегистрируйте драйвер

app/Providers/AppServiceProvider.php
use Frame\Core\Frame;
use App\Drivers\SpatieMediaDriver;

public function boot(): void
{
    Frame::useMediaDriver(SpatieMediaDriver::class);
}

Структура ответа upload

Метод upload() должен возвращать массив с метаданными:

app/Drivers/SpatieMediaDriver.php
public function upload(UploadedFile $file): array
{
    return [
        'id' => 'unique-id',
        'path' => 'https://.../',
        'size' => 102400,
        'name' => 'document.pdf',
        'type' => 'file',              // 'file' или 'image'
        'preview' => null,             // thumbhash, base64...
        'extension' => 'pdf',
        'collection' => 'default',
        'conversions' => [],
        'properties' => [],
        'order' => 0,
    ];
}

Опции resolve

Массив $options в методе resolve() может содержать:

КлючТипОписание
conversionstringНазвание конверсии (из Image::conversion())
typestringТип поля (file или image) для фильтрации

Browsable-драйверы (медиа-библиотека)

Если драйвер умеет показывать ранее загруженные файлы (медиа-библиотека), он должен дополнительно реализовать Frame\Core\Contracts\BrowsableDriver. У поля File/Image автоматически появится кнопка «Select from library» — флаг browsable в ответе API вычисляется из resolveDriver() instanceof BrowsableDriver.

Контракт

packages/core/src/Contracts/BrowsableDriver.php
namespace Frame\Core\Contracts;

use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;

interface BrowsableDriver extends MediaDriver
{
    public function browse(
        ?string $search = null,
        ?string $folder = null,
        ?string $type = null,
        int $perPage = 24,
    ): LengthAwarePaginator;

    public function folders(?string $parent = null): Collection;

    public function createFolder(string $name, ?string $parent = null): array;

    public function moveToFolder(int|string $id, ?string $folder = null): void;
}

Методы

browse
→ LengthAwarePaginator
Пагинированный список медиа. Параметры: search (подстрока в имени), folder (id текущей папки или null), type (image/file/null для всех), perPage. Элементы — массивы в формате upload-response.
folders
?string → Collection
Список папок для навигации. Каждая — массив с ключами id, name, parent. Верните пустую коллекцию, если папки не поддерживаются.
createFolder
string, ?string → array
Создать папку. Возвращает массив только что созданной папки. Если папки не поддерживаются — выбрасывайте RuntimeException.
moveToFolder
int|string, ?string → void
Переместить медиа в папку (или в корень при null).

Минимальный пример без папок

Если хранилище плоское (нет директорий), folders() возвращает пустую коллекцию, createFolder кидает, moveToFolder — no-op:

app/Drivers/MyBrowsableDriver.php
use Frame\Core\Contracts\BrowsableDriver;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use RuntimeException;

class MyBrowsableDriver implements BrowsableDriver
{
    // ...все методы MediaDriver...

    public function browse(
        ?string $search = null,
        ?string $folder = null,
        ?string $type = null,
        int $perPage = 24,
    ): LengthAwarePaginator {
        $query = Media::query()->latest('id');

        if ($search) {
            $query->where('filename', 'ilike', "%{$search}%");
        }

        if ($type === 'image') {
            $query->where('mime_type', 'like', 'image/%');
        } elseif ($type === 'file') {
            $query->where('mime_type', 'not like', 'image/%');
        }

        return $query->paginate($perPage)
            ->through(fn (Media $m) => $this->toFrameArray($m));
    }

    public function folders(?string $parent = null): Collection
    {
        return collect();
    }

    public function createFolder(string $name, ?string $parent = null): array
    {
        throw new RuntimeException('Folders are not supported.');
    }

    public function moveToFolder(int|string $id, ?string $folder = null): void
    {
        // no-op
    }
}
Фронтенд узнаёт про поддержку библиотеки только по типу-хинту: драйвер должен именно implements BrowsableDriver (не просто MediaDriver + методы). Flag browsable в сериализации поля завязан на instanceof.