Эта статья является продолжением первой части инструкции, где мы подробно рассматривали интеграцию Laravel с S3-хранилищем Timeweb Cloud. В предыдущей части вы узнали, как шаг за шагом настроить подключение к S3 и подготовить приложение для работы с облачным хранилищем.
Теперь мы перейдем к следующему этапу — интеграции пакета Spatie/MediaLibrary, который значительно упрощает управление медиаданными в Laravel. В сочетании с S3 этот инструмент открывает широкие возможности для загрузки, обработки и хранения файлов.
В данной инструкции мы разберем:
Убедитесь, что установлен GD Graphics Library, необходимый для обработки изображений. Выполните команду:
sudo apt-get install php8.x-gd
Замените x
на вашу актуальную версию PHP.
Установите библиотеку с помощью Composer:
composer require "spatie/laravel-medialibrary:*"
Эта команда устанавливает любую доступную версию пакета spatie/laravel-medialibrary
. Символ *
означает, что Composer выберет последнюю доступную стабильную версию, которая соответствует остальным ограничениям версий в вашем проекте.
Опубликуйте миграции для библиотеки MediaLibrary:
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="migrations"
Запустите миграции, чтобы создать необходимые таблицы в базе данных:
php artisan migrate
Для удобной настройки библиотеки опубликуйте её файл конфигурации:
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="config"
cloud
Для корректной работы библиотеки Spatie/MediaLibrary настройте подключение S3 и другие параметры.
В файле конфигурации config/media-library.php
настройте параметры загрузки.
Укажите максимальный размер файла и используемый диск. Например:
'disks' => [
'disk_name' => env('MEDIA_DISK', 'public'),
'max_file_size' => 1024 * 1024 * 10, //10Мб
],
В файле .env
добавьте переменную для указания используемого диска:
MEDIA_DISK=tws3
После этого библиотека будет использовать указанный диск tws3
для хранения медиафайлов.
Добавьте в модель app/Models/User.php
:
use Spatie\MediaLibrary\HasMedia\HasMedia;
use Spatie\MediaLibrary\HasMedia\HasMediaTrait;
use Spatie\MediaLibrary\Models\Media;
use Spatie\Image\Manipulations;
class User extends Model implements HasMedia
{
use HasMediaTrait;
}
Добавьте два метода registerMediaCollections
и registerMediaConversions
в файле app/Models/User.php
:
Этот метод задаёт параметры для хранения медиафайлов, определяя «коллекции». Каждая коллекция описывает, как организовать файлы, связанные с моделью.
public function registerMediaCollections(): void
{
$this
->addMediaCollection('avatars')
->singleFile();
$this
->addMediaCollection('gallery')
->withResponsiveImages();
}
addMediaCollection('avatars')
: Создаёт коллекцию с именем avatars
, которая предназначена для хранения одного изображения (например, аватара).
singleFile()
: Гарантирует, что в коллекции будет храниться только один файл. Если добавить новый файл, предыдущий будет автоматически удалён.
withResponsiveImages()
: Автоматически генерирует адаптивные изображения (разные размеры для разных экранов) для всех файлов в этой коллекции. Это полезно для оптимизации отображения изображений на устройствах с разным разрешением.
Этот метод описывает, какие преобразования (конверсии) изображений нужно выполнить для добавленных файлов. Преобразования выполняются автоматически при добавлении файла в коллекцию.
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion('thumb')
->width(150)
->height(150)
->sharpen(10)
->performOnCollections('avatars','gallery');
$this->addMediaConversion('border')
->width(250)
->height(250)
->sharpen(10)
->sepia()
->border(10, 'black', Manipulations::BORDER_OVERLAY)
->performOnCollections('gallery');
}
addMediaConversion('thumb')
Создаёт конверсию с именем thumb
(миниатюра).
width(150)
и height(150)
: Устанавливают размеры преобразованного изображения — 150x150 пикселей.
sharpen(10)
: Применяет резкость с уровнем 10, чтобы сделать изображение чётче.
performOnCollections('avatars', 'gallery')
: Указывает, что миниатюра должна создаваться для всех изображений, добавленных в коллекции avatars
и gallery
.
addMediaConversion('border')
Создаёт конверсию с именем border
(изображение с рамкой).
width(250)
и height(250)
: Устанавливают размеры изображения с рамкой — 250x250 пикселей.
sepia()
: Накладывает сепию, придавая изображению эффект старины.
border(10, 'black', Manipulations::BORDER_OVERLAY)
: Добавляет чёрную рамку шириной 10 пикселей поверх изображения.
performOnCollections('gallery')
: Указывает, что это преобразование применяется только к изображениям, добавленным в коллекцию gallery
.
Полный листинг файла, который должен получиться:
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\Image\Manipulations;
class User extends Authenticatable implements HasMedia
{
use HasApiTokens, HasFactory, Notifiable, InteractsWithMedia;
public function registerMediaCollections(): void
{
$this
->addMediaCollection('avatars') // Изображение аватара
->singleFile();
$this
->addMediaCollection('gallery')
->withResponsiveImages();
}
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion('thumb')
->width(150)
->height(150)
->sharpen(10)
->performOnCollections('avatars','gallery');
$this->addMediaConversion('border')
->width(250)
->height(250)
->sharpen(10)
->sepia()
->border(10, 'black', Manipulations::BORDER_OVERLAY)
->performOnCollections('gallery');
}
protected $fillable = [
'name',
'email',
'password',
'avatar',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
];
}
Поскольку процесс нарезки изображений может быть длительным, важно обеспечить бесперебойную работу приложения. Для этого Laravel и библиотека Spatie используют очереди, которые позволяют выполнять задачи в фоновом режиме. В этом примере мы настроим очереди с использованием адаптера database
.
Сначала создадим таблицу для хранения задач очереди. Выполните команду:
php artisan queue:table
Запустите миграции для создания таблицы в базе данных:
php artisan migrate
В файле .env
укажите, что для очередей будет использоваться база данных:
QUEUE_CONNECTION=database
Чтобы обработать задачи очереди, запустите следующий процесс:
php artisan queue:work
Теперь задачи нарезки изображений будут выполняться асинхронно, не блокируя работу приложения.
В файле routes/web.php
напишем логику для загрузки изображений:
Route::post('/profile/store', function (Request $request) {
$user = User::find(1);
if ($request->hasFile('avatar')) {
$user->addMediaFromRequest('avatar')->toMediaCollection('avatars');
}
if ($request->hasFile('gallery')) {
$user->addMultipleMediaFromRequest(['gallery'])->each(function ($user) {
$user->toMediaCollection('gallery');
});
}
return redirect('/profile');
});
$user = User::find(1)
: Получается объект пользователя с ID из базы данных.
if ($request->hasFile(<key>)) {}
: Проверяет, был ли передан файл в запросе
addMediaFromRequest(<key>)
: Загружает файл из запроса с указанным ключом
toMediaCollection(<key>)
: Сохраняет файл в медиа-коллекцию. Конверсии изображений (если определены) будут применены автоматически.
addMultipleMediaFromRequest(['gallery'])
: Извлекает файлы из запроса, соответствующие ключу и возвращает коллекцию медиаобъектов для дальнейшей обработки. Метод предназначен для загрузки нескольких файлов за один вызов.
В этом разделе мы настроим шаблон resources/views/profile/create.blade.php
для загрузки аватара пользователя и фотогалереи:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Создание пользователя</title>
</head>
<body>
<form action="/profile/store" method="POST" enctype="multipart/form-data">
@csrf
<label for="avatar">Выберите изображение:</label>
<input type="file" name="avatar" required>
<hr>
<label for="gallery">Выберите фотографии:</label>
<input type="file" name="gallery[]" required multiple>
<button type="submit">Загрузить</button>
</form>
</body>
</html>
Инструкция:
Откройте в браузере: http://localhost:8000/profile/create
.
Для загрузки аватара выберите одно изображение в поле avatar
.
Для загрузки нескольких фотографий в галерею используйте поле gallery[]
.
После выбора файлов нажмите кнопку «Загрузить».
Теперь добавим функционал для отображения загруженных изображений в шаблон resources/views/profile/index.blade.php
.
@if ($user->hasMedia('avatars'))
<img src="{{ $user->getFirstMediaUrl('avatars','thumb') }}"
alt="Аватар"
style="width: 150px; height: 150px; border-radius: 50%;">
@else
<p>Аватар отсутствует.</p>
@endif
$user->hasMedia('avatars')
: Проверяет, есть ли у пользователя медиафайлы в коллекции avatars
.
$user->getFirstMediaUrl('avatars', 'thumb')
: Возвращает URL первого медиафайла в коллекции avatars с преобразованием thumb.
Для вывода изображений из коллекции gallery используем следующие варианты:
Вариант 1. С преобразованием thumb:
@if ($user->hasMedia('gallery'))
@foreach($user->getMedia('gallery') as $media)
<img src="{{ $media->getUrl('thumb') }}"
alt="Изображение"
style="width: 150px; height: 150px;">
@endforeach
@else
<p>Фотографии отсутствуют.</p>
@endif
Вариант 2. С преобразованием border:
@if ($user->hasMedia('gallery'))
@foreach($user->getMedia('gallery') as $media)
<img src="{{ $media->getUrl('border') }}"
alt="Изображение"
style="width: 150px; height: 150px;">
@endforeach
@else
<p>Фотографии отсутствуют.</p>
@endif
Вариант 3. Вывод всех изображений responsive:
@if ($user->hasMedia('gallery'))
@foreach($user->getMedia('gallery') as $media)
{{ $media }}
@endforeach
@else
<p>Фотографии отсутствуют.</p>
@endif
Полный код файла resources/views/profile/index.blade.php:
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Профиль пользователя</title>
</head>
<body>
<h1>Профиль пользователя</h1>
<p>{{$user->name}}</p>
@if ($user->hasMedia('avatars'))
<img src="{{ $user->getFirstMediaUrl('avatars','thumb') }}"
alt="Аватар"
style="width: 150px; height: 150px; border-radius: 50%;">
@else
<p>Аватар отсутствует.</p>
@endif
<h1>Фотографии пользователя с преобразованием thumb</h1>
@if ($user->hasMedia('gallery'))
@foreach($user->getMedia('gallery') as $media)
<img src="{{ $media->getUrl('thumb') }}"
alt="Аватар"
style="width: 150px; height: 150px;">
@endforeach
@else
<p>Фотографии отсутствует.</p>
@endif
<h1>Фотографии пользователя с преобразованием border</h1>
@if ($user->hasMedia('gallery'))
@foreach($user->getMedia('gallery') as $media)
<img src="{{ $media->getUrl('border') }}"
alt="Аватар"
style="width: 150px; height: 150px;">
@endforeach
@else
<p>Фотографии отсутствует.</p>
@endif
<h1>Фотографии пользователя responsive</h1>
@if ($user->hasMedia('gallery'))
@foreach($user->getMedia('gallery') as $media)
{{ $media }}
@endforeach
@else
<p>Фотографии отсутствует.</p>
@endif
</body>
</html>
Для проверки откройте в браузере: http://localhost:8000/profile
.
Страница без изображений:
Страница с изображениями:
Для улучшения пользовательского опыта и удобства работы с медиафайлами можно настроить персонализированный домен для обращения к данным в облачном хранилище. Этот процесс включает несколько этапов.
Для примера будет использоваться домен третьего уровня s3.domain.ru
.
В DNS настройках добавьте CNAME-запись для созданного поддомена, указывающую на адрес s3.timeweb.cloud
.
Свяжите поддомен с вашим бакетом S3 через панель управления Timeweb Cloud.
Подождите 15-20 минут, пока изменения вступят в силу.
В файле .env
задайте переменную окружения:
TW_URL=https://s3.domain.ru
Откройте приложение в браузере http://localhost:8000/profile
и убедитесь, что в HTML-коде атрибут src
у медиафайлов содержит ссылку на ваш домен, например:
<img src="https://s3.domain.ru/path/to/image.jpg" alt="Медиафайл">
Для удаления изображений добавим функционал в маршруты и шаблон.
В файле routes/web.php
добавьте следующий код:
use Spatie\MediaLibrary\MediaCollections\Models\Media;
Route::get('mediaDelete/{media}', function (Media $media){
$media->delete();
return back();
})->name('delete.media');
Добавьте код для удаления изображений в файл resources/views/profile/index.blade.php
:
<h1>Список изображений:</h1>
@if ($user->hasMedia('avatars'))
<p>Коллекция avatars:</p>
@foreach($user->getMedia('avatars') as $media)
<a href="{{route('delete.media',$media)}}">Удалить {{$media->file_name}}</a>
@endforeach
@else
<p>Фотографии отсутствует.</p>
@endif
@if ($user->hasMedia('gallery'))
<p>Коллекция gallery:</p>
@foreach($user->getMedia('gallery') as $media)
<a href="{{route('delete.media',$media)}}">Удалить {{$media->file_name}}</a><br>
@endforeach
@else
<p>Фотографии отсутствует.</p>
@endif
Как работает:
Откройте в браузере: http://localhost:8000/profile
.
При клике по ссылке файл удаляется из облачного хранилища.
Удаление происходит через маршрут delete.media
, который удаляет файл по его идентификатору.
Эта настройка позволяет легко управлять медиафайлами в приложении Laravel.
Выгодные тарифы на облако в Timeweb Cloud
Использование Spatie/MediaLibrary в сочетании с облачным хранилищем S3 открывает широкие возможности для управления медиафайлами в приложении Laravel. Этот подход предлагает:
Удобство загрузки: Простая настройка форм для загрузки единичных изображений (например, аватаров) и галерей.
Гибкость отображения: Возможность настройки преобразований изображений, таких как создание миниатюр, изображений с рамками или адаптивных версий.
Эффективное хранение: Надежное и масштабируемое хранение данных в S3 с поддержкой пользовательских доменов.
Простое управление: Легкость удаления и замены медиафайлов через маршруты и интерфейс приложения.
Масштабируемость: Обработка изображений в очередях гарантирует стабильность работы даже при больших объемах данных.
Следуя предложенным инструкциям, вы сможете настроить функциональность для загрузки, обработки, отображения и управления изображениями, включая удаление файлов. Эта интеграция значительно повышает удобство работы, производительность и масштабируемость вашего приложения.