Розробка системи медіа-бібліотеки для CMS
Медіа-бібліотека — централізоване сховище та інтерфейс управління файлами: зображення, відео, документи. Замість завантаження одного файлу для кожного запису, редактор вибирає з уже завантажених файлів — менше дублів, менше витрат на зберігання.
Модель даних
media_files (
id, original_name, file_name, disk, path,
mime_type, size,
width, height, -- для зображень
alt, title, caption, -- SEO та доступність
folder_id,
uploaded_by,
created_at, updated_at
)
media_folders (
id, name, parent_id, created_at
)
media_conversions (
id, media_id, name, -- 'thumb', 'medium', 'large'
file_name, width, height, size, created_at
)
Завантаження файлів через Presigned URL
Завантаження безпосередньо з браузера на S3 — без навантаження на сервер:
// Сервер генерує presigned URL
$s3 = new S3Client([...]);
$cmd = $s3->getCommand('PutObject', [
'Bucket' => env('AWS_BUCKET'),
'Key' => "uploads/temp/{$uuid}/{$filename}",
'ContentType' => $mimeType
]);
$presigned = $s3->createPresignedRequest($cmd, '+15 minutes');
return ['upload_url' => (string)$presigned->getUri(), 'key' => $key];
// Клієнт завантажує безпосередньо на S3
const { upload_url, key } = await getPresignedUrl(file);
await fetch(upload_url, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } });
// Підтвердити завантаження серверу
await confirmUpload(key, { alt: '', title: file.name });
Генерація превью
Після завантаження — асинхронна генерація версій через чергу:
class GenerateImageConversions implements ShouldQueue
{
public function handle(): void
{
$conversions = [
'thumb' => ['width' => 150, 'height' => 150],
'medium' => ['width' => 800, 'height' => null],
'large' => ['width' => 1920, 'height' => null],
'webp' => ['width' => null, 'height' => null, 'format' => 'webp']
];
foreach ($conversions as $name => $params) {
$image = Image::make(Storage::get($this->media->path));
if ($params['width']) {
$image->resize($params['width'], $params['height'], fn($c) => $c->aspectRatio());
}
if (isset($params['format'])) {
$image->encode($params['format'], 85);
}
Storage::put("media/conversions/{$this->media->id}/{$name}", $image->stream());
}
}
}
Інтерфейс медіа-бібліотеки
Основні можливості UI:
- Сітка файлів з превью (або список)
- Фільтри: тип файлу, папка, дата
- Пошук по імені та alt-тексту
- Завантаження drag-and-drop або кнопкою
- Створення папок
- Множинний виділення для масових операцій
- Редагування alt, title, caption прямо в бібліотеці
- Копіювання URL у буфер обміну
Віджет вибору медіа для редактора
function MediaPickerButton({ onSelect, multiple = false }) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Вибрати з бібліотеки</Button>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="max-w-4xl h-[80vh]">
<MediaLibraryGrid
onSelect={(files) => {
onSelect(multiple ? files : files[0]);
setIsOpen(false);
}}
multiple={multiple}
/>
</DialogContent>
</Dialog>
</>
);
}
Очистка невиконаних файлів
// Задача за розкладом: знайти файли без посилань
$unused = MediaFile::whereDoesntHave('usages')
->where('created_at', '<', now()->subDays(30))
->get();
foreach ($unused as $file) {
Storage::delete($file->path);
$file->delete();
}
Термін розробки: 3–4 тижні для повної медіа-бібліотеки з генерацією превью, папками та віджетом вибору.







