Версія v1.6.5 Попередні версії: документація на GitHub
Файли й зображення
На цій сторінці
- Швидкий старт
- Публічне зображення
- Приватний файл
- Кілька файлів
- Приклад: допис з обкладинкою, вкладеннями й приватним документом
- Диски й видимість
- S3
- Каталог
- Формат зберігання
- Валідація й обмеження
- Заблоковані розширення
- Ліміти PHP
- Приватні файли й підписані посилання
- Кешування
- Видалення й очищення
- Очищення (pruning)
- Одночасне редагування
- Видалення записів
- М'яке видалення
- Власні поля з файлами
- FileSafety для розширень
- Безпека
- Довідник API
- Конфігурація
- Обмеження
Поля File і Image завантажують файли на диск Laravel (config/filesystems.php) і зберігають
шлях у колонці моделі. Image показує мініатюру в списку, попередній перегляд на сторінці запису й
у формі. File показує назву файлу з посиланням. Обидва поля працюють з одним файлом або з кількома
(multiple()), з публічними й приватними дисками, зокрема з S3.
Додаткових залежностей Composer чи npm не потрібно.
Для моделей зі spatie/laravel-medialibrary (колекції,
перетворення, галереї) встановіть окреме доповнення
maxcxam/larsadmin-medialibrary (1.0, потребує Larsadmin 1.6+).
Воно додає поля Media і MediaGallery й описане у власному посібнику,
vendor/maxcxam/larsadmin-medialibrary/docs/guide/medialibrary.md.
Швидкий старт
Публічне зображення
Міграція: звичайна рядкова колонка для шляху.
$table->string('cover')->nullable();
Entity:
use Maxcxam\Larsadmin\Fields\Image;
Image::make('Обкладинка', 'cover')->disk('public'),
Диск public має 'visibility' => 'public', тож файл віддає вебсервер за адресою
Storage::disk('public')->url($path). Для цього потрібне посилання public/storage:
php artisan storage:link
Storage::url() дає абсолютну адресу з url диска, у стандартному конфігу це APP_URL.'/storage'.
Якщо панель відкривається з іншого хоста, ніж APP_URL (наприклад, 127.0.0.1:8000 замість
localhost), зображення не завантажаться. Задайте правильний APP_URL або
filesystems.disks.public.url.
Приватний файл
$table->string('contract')->nullable();
$table->string('contract_name')->nullable();
use Maxcxam\Larsadmin\Fields\File;
File::make('Договір', 'contract')
->types(['pdf'])
->maxSize(5120) // КБ
->storeOriginalName('contract_name'),
Диск не вказано, тож береться larsadmin.files.disk, за замовчуванням local (у Laravel 11+ це
storage/app/private). Такий файл недоступний напряму: панель видає підписане посилання з обмеженим
часом дії і перед кожним завантаженням знову перевіряє права користувача
(див. Приватні файли й підписані посилання).
Кілька файлів
$table->json('attachments')->nullable();
// app/Models/Post.php
protected function casts(): array
{
return ['attachments' => 'array'];
}
File::make('Вкладення', 'attachments')
->multiple()
->maxFiles(5)
->types(['pdf', 'docx', 'xlsx']),
Користувач може додати файли кнопкою «Обрати файли» або перетягнути їх у форму, прибрати окремі файли й повернути прибрані до збереження. Нові файли додаються після наявних. Змінювати порядок файлів у 1.1 не можна.
Приклад: допис з обкладинкою, вкладеннями й приватним документом
// міграція
$table->string('cover')->nullable();
$table->string('cover_name')->nullable();
$table->json('attachments')->nullable();
$table->string('contract')->nullable();
$table->string('contract_name')->nullable();
use Maxcxam\Larsadmin\Fields\File;
use Maxcxam\Larsadmin\Fields\Image;
use Maxcxam\Larsadmin\Layout\Panel;
public function fields(Request $request): array
{
return [
// Публічний диск: файл віддає вебсервер через storage:link.
Image::make('Обкладинка', 'cover')->disk('public')->storeOriginalName('cover_name')->maxSize(2048),
Text::make('Заголовок', 'title')->rules('required', 'max:255'),
Panel::make('Файли', [
// Кілька файлів в одній JSON-колонці на приватному диску local (за замовчуванням).
File::make('Вкладення', 'attachments')->multiple()->maxFiles(5)->maxSize(5120)
->types(['pdf', 'docx', 'xlsx', 'txt', 'zip', 'jpg', 'png']),
// Приватний документ лише для першого користувача.
File::make('Внутрішній документ', 'contract')->private()->types(['pdf', 'docx'])
->storeOriginalName('contract_name')
->hideFromIndex()
->canSee(fn (Request $request) => $request->user()?->getKey() === 1),
]),
];
}
У моделі attachments має cast array. Посилання на contract бачить і може відкрити лише
користувач, для якого canSee() повертає true: для інших маршрут файлу відповідає 404.
Диски й видимість
| Що | Як визначається |
|---|---|
| Диск | ->disk('s3'), інакше larsadmin.files.disk (за замовчуванням local) |
| Публічний чи приватний | ->public() або ->private(); якщо не викликано, публічним вважається диск з 'visibility' => 'public' у config/filesystems.php |
| Каталог | ->path(...), інакше {files.directory}/{uriKey Entity}/{атрибут}, наприклад larsadmin/posts/cover |
Невідомий диск (немає в filesystems.disks) кидає InvalidArgumentException, щойно поле
прив'язується до Entity. Так само поле з ->public() на диску з драйвером local без ключа url
(наприклад, стандартний local): публічного посилання на такий файл немає. Використайте диск
public або ->private().
Як будуються посилання:
| Видимість | Драйвер диска | Посилання |
|---|---|---|
| публічний | будь-який | Storage::disk()->url($path), без терміну дії |
| приватний | не local, підтримує тимчасові URL (S3 і сумісні) |
temporaryUrl() диска з терміном files.url_ttl |
| приватний | local (зокрема з serve => true) або без тимчасових URL |
маршрут панелі з відносним підписом і терміном files.url_ttl |
Файл записується з видимістю public або private відповідно до поля.
S3
Image::make('Фото', 'photo')->disk('s3'), // приватно: тимчасові посилання S3
Image::make('Банер', 'banner')->disk('s3')->public(), // публічно: Storage::url()
Для приватних файлів S3 панель підписує посилання локально, без запиту до S3. У посиланні задаються
ResponseContentType, ResponseContentDisposition і ResponseCacheControl: растрові зображення
відкриваються в браузері, решта файлів завантажується як application/octet-stream і не кешується
(див. Кешування).
Тимчасове посилання S3 перевіряється лише в момент видачі (користувач мав право бачити запис і
поле). Після цього його обмежує тільки термін дії files.url_ttl. Якщо доступ треба перевіряти під
час кожного завантаження, використовуйте диск з драйвером local.
Каталог
File::make('Звіт', 'report')->path('reports'),
File::make('Звіт', 'report')->path(fn (Request $request, Model $record) => 'reports/'.now()->format('Y/m')),
Каталог має бути відносним і складатися з сегментів [A-Za-z0-9._-], без . і ... Інакше
InvalidArgumentException: для рядка одразу, для замикання під час збереження. Під час створення
запис у замиканні ще не збережений, тож ключа в нього немає.
Формат зберігання
Ім'я файлу на диску завжди генерується: 40 випадкових символів і розширення, визначене за вмістом
файлу (bin, якщо тип невідомий). Ім'я з комп'ютера користувача на диску не використовується.
Один файл. Колонка містить шлях відносно кореня диска:
larsadmin/posts/cover/Xk3fq…40 символів….jpg
Розмір файлу не зберігається (розмір і тип зберігає лише multiple()), тип визначається за
розширенням. Оригінальна назва теж не зберігається: без storeOriginalName() файл показується й
завантажується під назвою поля з розширенням, наприклад Договір.pdf. Тому для одного файлу чи
зображення рекомендуємо зберігати оригінальну назву в окремій колонці:
File::make('Договір', 'contract')->storeOriginalName('contract_name'),
Назва очищується: лише останній сегмент шляху, без керівних і форматувальних символів Unicode
(зокрема перемикачів напрямку тексту й символів нульової ширини, якими підробляють розширення) і
"<>:*?|, не довше 200 символів (розширення зберігається). storeOriginalName() разом з multiple() кидає LogicException.
Кілька файлів. Колонка містить JSON-масив об'єктів:
[{"path":"larsadmin/posts/attachments/Q9…40.pdf","name":"Звіт за жовтень.pdf","size":48213,"mime":"application/pdf"}]
size і mime визначає сервер (getSize(), fileinfo), а не браузер. Рекомендуємо колонку json
(або text) і cast array. Якщо модель приводить колонку до array, json, object,
collection, їх encrypted: варіантів, AsArrayObject чи AsCollection, поле записує PHP-масив,
інакше JSON-рядок.
Читання толерантне: null і '' означають «файлів немає», список рядків сприймається як список
шляхів, записи без коректного шляху пропускаються. Запис без назви показується як
{назва поля} (N).{розширення}, де N його позиція. Коректний шлях непорожній, відносний, без ..,
\, : і керівних символів. Некоректні записи не показуються, не віддаються й не видаляються.
Валідація й обмеження
| Обмеження | Як задати | За замовчуванням |
|---|---|---|
| Типи | ->types(['pdf', 'docx']) |
File: будь-який незаблокований тип; Image: jpg, jpeg, png, gif, webp, avif |
| Розмір одного файлу | ->maxSize(5120) (КБ, як правило max Laravel) |
files.max_size = 10240 (10 МБ) |
| Кількість файлів | ->maxFiles(5) (лише з multiple()) |
files.max_files = 10; без multiple() завжди 1 |
| Обов'язковість | ->rules('required'), ->creationRules('required') |
не обов'язкове |
| Заблоковані типи | files.blocked_extensions |
див. Конфігурація |
-
types()перевіряє і вміст (mimes), і розширення імені з браузера (extensions). Це основний засіб контролю типів: задавайтеtypes()для кожного поляFile, а список заблокованих розширень лишайте запобіжником.types([])кидаєInvalidArgumentException; щоб дозволити будь-який незаблокований тип, не викликайтеtypes()взагалі. -
Image::types()приймає лише растрові типи (jpg,jpeg,png,gif,webp,avif), інші, і SVG зокрема, кидаютьInvalidArgumentException. -
requiredозначає, що після збереження в полі має лишитися хоча б один файл. Під час редагування вже завантажений файл задовольняє правило. -
Кількість рахується після збереження: лишені наявні файли плюс нові.
-
Інші ваші правила з
rules(),creationRules(),updateRules()застосовуються до кожного завантаженого файлу окремо. Наприклад, мінімальний розмір зображення:Image::make('Обкладинка', 'cover')->rules('required', 'dimensions:min_width=600'),required,nullable,sometimesіarrayстосуються поля загалом, а не файлів.nullable()приймається, але нічого не змінює: порожнє поле визначаєrequired.
Помилки показуються біля поля, а помилка конкретного файлу біля цього файлу. Браузер теж перевіряє розмір, тип і кількість ще до відправлення, але остаточне рішення завжди за сервером.
Заблоковані розширення
Розширення зі списку files.blocked_extensions відхиляються завжди, навіть якщо їх перелічено в
types(). Список за замовчуванням охоплює:
- серверні скрипти: сімейство PHP (
php,phtml,phar,incтощо), CGI, Perl, Python, Ruby, shell, ASP/ASP.NET, JSP, ColdFusion, SSI (shtml); - виконувані файли й скриптові хости Windows:
exe,dll,msi,bat,cmd,scr,hta,vbs,ps1,reg,lnk,jarта інші; - розмітку й скрипти, які виконує браузер: HTML, XHTML, MHT, SVG, XML, XSL, JS (
js,mjs,cjs),swf; - конфігурацію сервера:
htaccess,htpasswd,ini.
Перевіряються:
- кожен сегмент імені з браузера після першої крапки, і в сирому, і в очищеному імені (без кінцевих
крапок і пробілів, які відкидає Windows):
photo.php.jpg,run.bat"і.htaccessвідхиляються. Сама основа імені не перевіряється, тожphp.pdfпроходить (і на диск потрапляє з випадковим іменем); - розширення, визначене за вмістом: PHP-код, перейменований на
.jpg, теж відхиляється.
Публічні диски віддає вебсервер, тому для них діє мінімальний список, який не можна вимкнути
конфігом: розмітка й скрипти для браузера (svg, svgz, html, htm, xhtml, xht, xhtm,
shtml, mht, mhtml, xml, xsl, xslt, js, mjs) і серверні (php, phtml, phar,
htaccess). Вони відхиляються, навіть якщо прибрати їх з files.blocked_extensions.
SVG може містити скрипти. Дозволити його можна лише на приватному диску, прибравши svg (і за
потреби svgz) з files.blocked_extensions, і лише в полі File: Image SVG не приймає. Такий
файл віддається як вкладення, не відкривається в браузері.
Ліміти PHP
PHP відкидає завеликі запити ще до Laravel. Перевірте php.ini:
| Директива | Що обмежує |
|---|---|
upload_max_filesize |
розмір одного файлу; за замовчуванням 2M |
post_max_size |
розмір усього запиту з усіма файлами; за замовчуванням 8M |
max_file_uploads |
кількість файлів в одному запиті; за замовчуванням 20 |
Поле повідомляє ці ліміти браузеру, тож форма показує менше з двох значень (maxSize і
upload_max_filesize) і не дає відправити завеликі файли. Файл більший за upload_max_filesize
сервер відхиляє як незавантажений. Запит більший за post_max_size Laravel зупиняє з відповіддю
413; форма показує повідомлення «Файли завеликі для одного збереження», а не сторінку помилки.
Ліміти вебсервера (client_max_body_size у nginx) теж мають бути не меншими.
Приватні файли й підписані посилання
Посилання на приватні файли на дисках local ведуть на маршрут панелі:
GET {path}/files/{entity}/{id}/{field}/{token}?expires=…&signature=…
Ім'я маршруту larsadmin.files.show. Шлях до файлу в посиланні не передається: token це перші 32
hex-символи HMAC від APP_KEY, диска й шляху. Підпис відносний (signed:relative), тож посилання
працює за проксі й з іншим хостом. Параметр download=1 (частина підпису) вмикає завантаження
замість перегляду.
Права перевіряються під час кожного запиту, у тому ж порядку, що й для сторінки запису:
- підпис і термін дії (інакше 403), вхід у панель і
viewLarsadmin, ліміт запитів; - Entity існує (інакше 404) і
viewAny(інакше 403); - запис знаходиться через
detailQuery(інакше 404); viewдля запису (інакше 403);- поле
File/Imageз цим атрибутом показується користувачу на detail чи index, або на формі редагування, якщо є правоupdate;canSee()повертаєtrue(інакше 404); - токен збігається з одним з поточних файлів запису (інакше 404), файл існує на диску (інакше 404).
Отже, посилання перестає працювати, щойно користувач втратив доступ або файл замінили, навіть якщо термін дії ще не минув.
Інші правила:
- Посилання видаються лише тоді, коли користувач може переглядати запис (
view). Інакше в списку видно тільки назви файлів. - Термін дії не менший за
files.url_ttl(хвилини, за замовчуванням 30). Якщо сторінка була відкрита довше, клік по посиланню спершу оновлює дані сторінки й відкриває свіже посилання. - Ліміт
files.throttleзавантажень на хвилину на користувача (на IP для гостей), лімітерlarsadmin-files.nullвимикає ліміт. Кожен запит віддає файл повністю (безRange), тож за 600 запитів на хвилину один користувач може завантажити кілька гігабайтів. Якщо трафік сховища платний, зменшіть ліміт. - Відповідь передається потоком, файл не читається в пам'ять повністю.
- Заголовки: растрові зображення (
jpg,jpeg,png,gif,webp,avif) віддаються inline зі своїм типом, усе інше якapplication/octet-streamзContent-Disposition: attachment. ЗавждиX-Content-Type-Options: nosniff,Content-Security-Policyзsandbox,Cross-Origin-Resource-Policy: same-originіReferrer-Policy: no-referrer. Ім'я файлу очищується й кодується за RFC 6266 з ASCII-варіантом.
Кешування
- Вкладення (усе, що завантажується, а не показується) віддаються з
Cache-Control: private, no-store: копія не лишається в браузері після виходу чи втрати доступу. - Растрові зображення, показані inline, віддаються з
Cache-Control: private, max-age=<url_ttl>. - Термін дії посилання округлюється вгору до чверті
url_ttl(не менше хвилини). Тому протягом цього інтервалу посилання на файл однакові, і браузер бере мініатюри з кешу. Фактичний термін дії відurl_ttlдоurl_ttlплюс чверть. - Тимчасові посилання S3 отримують ті самі значення через
ResponseCacheControl.
Видалення й очищення
Диск не транзакційний, тому панель веде журнал файлів одного збереження:
- нові файли записуються до транзакції бази даних;
- якщо заповнення,
save()чи колбек після збереження кидає виняток, файли, записані цим запитом, видаляються, а старі лишаються; - замінені й прибрані файли видаляються лише після коміту найзовнішньої транзакції. Якщо ваш код обгортає збереження власною транзакцією, видалення чекає її коміту, а при відкаті файли лишаються;
- помилки видалення з диска передаються в
report()(з диском, шляхом, Entity й ключем) і не ламають запит.
Очищення (pruning)
Очищення увімкнене за замовчуванням (files.prune). Воно видаляє:
- файл, замінений новим;
- файли, прибрані у формі;
- усі файли запису, видаленого через панель.
Вимкнути для поля:
File::make('Архів', 'archive')->prunable(false),
або для всієї панелі: 'prune' => false у config/larsadmin.php.
Очищення розраховане на правило «один файл, один запис» і не рахує посилань. Панель сама його
дотримується: кожне завантаження отримує нове випадкове ім'я. Але якщо ваш код записує той самий шлях
у кілька записів ($post->replicate(), спільний файл за замовчуванням на кшталт
avatars/default.png із сідера), заміна файлу чи видалення одного запису видалить файл і для решти.
Для таких колонок використовуйте prunable(false).
Одночасне редагування
Форма надсилає два списки токенів: файли, які вона показала (known), і файли, які користувач
залишив (keep). Прибраними вважаються лише файли з known, яких немає в keep. Тому:
- файли, які хтось інший додав, поки форма була відкрита, зберігаються;
- токени зі старим
APP_KEY(форма відкрита до зміни ключа) не збігаються ні з чим, і нічого не прибирається; - з
removable(false)сервер не прибирає жодного файлу, навіть якщо запит надіслано в обхід форми. Новий файл в одиночному полі все одно замінює старий.
Для запитів не з форми панелі (API-клієнти, тести): без known нічого не прибирається, нові файли
multiple() додаються до наявних, а файл, завантажений в одиночне поле, замінює старий.
Видалення записів
Кнопка видалення в панелі викликає Entity::deleteRecord(Request $request): delete() у
транзакції, а після коміту видалення файлів усіх полів File/Image Entity, навіть прихованих чи
недоступних поточному користувачу (запису вже немає). prunable(false) і files.prune діють і тут.
У власних діях видалення використовуйте той самий метод замість $model->delete(). handle() не
отримує Request, тож передайте request(). $this->entity містить клас Entity, до якої панель
прив'язала дію:
public function handle(ActionFields $fields, Collection $models): ?ActionResponse
{
// Через Entity, а не $model->delete(): файли полів File/Image видаляються після коміту.
$models->each(fn (Model $model) => $this->entity::make($model)->deleteRecord(request()));
return ActionResponse::message('Видалено записів: '.$models->count());
}
$this->entity дорівнює null, якщо дію створено поза панеллю (наприклад, у тесті через new). Тоді
вкажіть клас Entity явно: \App\Larsadmin\Post::make($model)->deleteRecord(request()).
М'яке видалення
Якщо модель використовує SoftDeletes, видалення з панелі лише встановлює deleted_at, а файли
лишаються, щоб запис можна було відновити.
Остаточне видалення поза панеллю (forceDelete(), $model->delete() у вашому коді, команди
очищення) файли не видаляє. Додайте слухача моделі:
// app/Models/Post.php
use Illuminate\Support\Facades\Storage;
protected static function booted(): void
{
static::forceDeleted(function (Post $post) {
$cover = $post->cover; // Image, диск public
$attachments = collect($post->attachments ?? [])->pluck('path')->filter()->all(); // multiple(), диск local
$post->getConnection()->afterCommit(function () use ($cover, $attachments) {
if ($cover) {
Storage::disk('public')->delete($cover);
}
Storage::disk('local')->delete($attachments);
});
});
}
afterCommit() чекає коміту транзакції, а без транзакції виконується одразу. Приклад розраховано
на cast array для attachments.
Власні поля з файлами
Поле розширення, що записує файли, реєструє їх у тому самому журналі збереження, яким користуються
File і Image: перевизначає Field::fillInto(Request $request, Model $record, FileJournal $journal) і
викликає $journal->created($disk, $path) для записаних файлів (видаляються, якщо збереження не вдалося)
та $journal->obsolete($disk, $path) для замінених (видаляються після коміту). Форма такого поля
надсилається як multipart/form-data, якщо поле має multipart(). Див.
Розширення: хуки збереження.
FileSafety для розширень
З 1.6 перевірки безпеки, якими користуються File і Image, доступні розширенням через стабільний фасад
Maxcxam\Larsadmin\Files\FileSafety. Розширення, що зберігає чи віддає файли (як доповнення медіатеки),
не тримає власної копії цих перевірок.
| Метод | Опис |
|---|---|
rule(bool $publicDisk = false): Closure |
правило валідації: відхиляє заблоковані розширення за кожним сегментом імені й за типом, визначеним за вмістом; з $publicDisk також розмітку й скрипти |
blockedExtension(UploadedFile $file, bool $publicDisk = false): ?string |
перше заблоковане розширення файлу або null |
storedExtension(UploadedFile $file, bool $publicDisk = false): string |
розширення для збереження: визначене за вмістом і дозволене, інакше bin; ніколи не розширення з браузера |
sanitizeName(string $name): string |
очищена назва з браузера: останній сегмент шляху, без керівних, bidi/zero-width і зарезервованих символів, до 200 символів; може бути '' |
asciiName(string $name): string |
ASCII-варіант назви для параметра filename у Content-Disposition |
isRasterImage(string $path, ?string $mime = null): bool |
чи можна показати файл inline: растрове розширення (jpg, jpeg, png, gif, webp, avif; не SVG) і, якщо передано, растровий MIME-тип |
response(FilesystemAdapter $disk, string $path, ?string $name = null, bool $download = false, ?int $ttlMinutes = null): StreamedResponse |
потокова відповідь із заголовками панелі (див. нижче) |
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Maxcxam\Larsadmin\Files\FileSafety;
// валідація завантаження
$request->validate(['upload' => ['required', 'file', 'max:10240', FileSafety::rule($public)]]);
// збереження під випадковим ім'ям з розширенням за вмістом
$file = $request->file('upload');
$path = $file->storeAs('acme', Str::random(40).'.'.FileSafety::storedExtension($file, $public), $disk);
$name = FileSafety::sanitizeName($file->getClientOriginalName());
// віддача (після авторизації запиту, наприклад Larsadmin::findRecord())
return FileSafety::response(Storage::disk($disk), $path, $name);
response() віддає inline лише растрові зображення (за розширенням шляху, якщо не $download), решту як
вкладення application/octet-stream; завжди з X-Content-Type-Options: nosniff,
Content-Security-Policy: sandbox, Cross-Origin-Resource-Policy: same-origin,
Referrer-Policy: no-referrer і приватним кешем (inline-зображення на $ttlMinutes, за замовчуванням
files.url_ttl; значення менше 1 кидає InvalidArgumentException; вкладення не кешуються). Назва за замовчуванням: базове ім'я шляху. Авторизацію робить
код розширення до виклику. FileTypes, FileResponse і StoredFile лишаються внутрішніми.
Безпека
- Шлях до файлу ніколи не приходить з браузера. Форма надсилає токени наявних файлів, і вони порівнюються лише з поточними файлами запису; невідомі токени ігноруються. Значення поля, що не є масивом (наприклад, підставлений шлях), дає помилку валідації, нічого не зберігається.
- Ім'я на диску випадкове, розширення визначається за вмістом. Каталог задається лише в коді.
- Приватні файли перевіряються на кожному запиті (див. вище) і віддаються з безпечними заголовками.
- Публічні диски віддає вебсервер, і панель не може задати там заголовки. Розмітку й скрипти
(HTML, SVG, XML, JS, PHP) на публічних дисках заблоковано без можливості вимкнути, але інші типи,
наприклад
pdfчиtxt, вебсервер віддає inline з домену застосунку. Рекомендації:- обмежуйте поля через
types(), а не лише через список заблокованих розширень; - додайте для
/storageу конфігурацію вебсервера заголовкиX-Content-Type-Options: nosniff,Content-Security-Policy: sandboxіContent-Disposition: attachmentдля всього, крім зображень; - файли, які не повинні бути публічними, зберігайте на приватному диску.
- обмежуйте поля через
- Видаляються лише шляхи, на які посилався запис, і лише після коміту.
- Поля
FileіImage, як і будь-яке поле зmultipart: true, не можна використовувати вfields()дій (LogicException): дія заповнює тимчасову модель, і файли лишилися б без власника. - Розширення, що працюють з файлами, користуються тими самими перевірками через
FileSafety.
Довідник API
Класи Maxcxam\Larsadmin\Fields\File і Maxcxam\Larsadmin\Fields\Image (Image extends File).
| Метод | Опис |
|---|---|
File::make(string $name, ?string $attribute = null) |
компонент file-field; Image::make() дає image-field |
multiple(bool $multiple = true) |
кілька файлів у JSON-колонці |
maxFiles(int $max) |
максимум файлів для multiple(); менше 1 кидає InvalidArgumentException |
disk(string $disk) |
диск; за замовчуванням files.disk |
public(), private() |
видимість; за замовчуванням з visibility диска. public() на диску local без url кидає InvalidArgumentException |
path(string|Closure $directory) |
каталог або fn (Request $request, Model $record): string |
types(array $types) |
дозволені розширення, наприклад ['pdf', 'docx']; без виклику (за замовчуванням у File) будь-який незаблокований тип; types([]) кидає InvalidArgumentException; Image::types() приймає лише растрові типи |
maxSize(int $kilobytes) |
максимальний розмір одного файлу в КБ; менше 1 кидає InvalidArgumentException |
storeOriginalName(string $column) |
колонка для очищеної оригінальної назви; лише для одного файлу |
prunable(bool $prunable = true) |
видаляти замінені, прибрані файли й файли видалених записів; за замовчуванням files.prune. false для шляхів, спільних для кількох записів |
removable(bool $removable = true) |
дозволити прибирати файли. З false форма не показує кнопку «Видалити», а сервер не прибирає файли навіть із запиту в обхід форми; новий файл в одиночному полі все одно замінює старий |
Image::thumbnail(Closure $callback) |
fn (StoredFile $file, Model $record): ?string; URL мініатюри або null для оригіналу |
filesOf(Model $record): array |
поточні файли запису як list<StoredFile> (path, name, size, mime, extension(), isRasterImage()) |
rules(), creationRules(), updateRules() |
required для поля, решта для кожного файлу (див. Валідація) |
fillUsing(callable) |
повністю замінює збереження: поле нічого не записує на диск і нічого не видаляє |
displayUsing(callable) |
fn (array $names, Model $record, string $attribute), отримує список назв файлів. Результат потрапляє в displayedAs; вбудовані компоненти показують список файлів, тож він потрібен лише власним компонентам |
resolveUsing(callable) |
для полів файлів ігнорується |
sortable() |
кидає LogicException: сортувати за файлами не можна |
nullable() |
приймається, нічого не змінює |
Інше:
| API | Опис |
|---|---|
Entity::deleteRecord(Request $request): void |
видалення запису в транзакції й очищення файлів після коміту (див. Видалення записів) |
маршрут larsadmin.files.show |
GET {path}/files/{entity}/{id}/{field}/{token}, підписаний |
лімітер larsadmin-files |
files.throttle запитів на хвилину |
Поведінка в списку: для одного файлу показується він сам, для multiple() до трьох мініатюр чи
назв і лічильник +N. Мініатюри це оригінали, зменшені CSS.
Конфігурація
Секція files у config/larsadmin.php. Конфіг, опублікований до 1.1, працює без неї: усі ключі
мають значення за замовчуванням.
| Ключ | За замовчуванням | Опис |
|---|---|---|
disk |
env('LARSADMIN_FILES_DISK', 'local') |
диск полів без disk() |
directory |
'larsadmin' |
базовий каталог; каталог поля {directory}/{uriKey}/{атрибут} |
max_size |
10240 |
максимальний розмір файлу, КБ |
max_files |
10 |
максимум файлів для multiple() без maxFiles() |
prune |
true |
видаляти замінені, прибрані файли й файли видалених записів |
url_ttl |
30 |
термін дії підписаних і тимчасових посилань, хвилини |
throttle |
600 |
завантажень приватних файлів на хвилину на користувача; null без ліміту |
blocked_extensions |
серверні скрипти, виконувані файли Windows, розмітка й скрипти для браузера, конфігурація сервера | розширення, які не приймаються ніколи (див. Заблоковані розширення); запобіжник, основний контроль це types(). На публічних дисках розмітка, скрипти й PHP заблоковані завжди |
Обмеження
-
Немає обробки зображень на сервері. Мініатюри це оригінали, зменшені CSS. Якщо у вас є власні мініатюри (CDN з resize, Glide тощо), поверніть їх URL з
->thumbnail(...):Image::make('Обкладинка', 'cover') ->thumbnail(fn (StoredFile $file, Model $record) => "https://img.example.com/{$file->path}?w=160"), -
Немає зміни порядку файлів перетягуванням: нові файли додаються в кінець.
-
Немає попереднього чи порційного завантаження. Файли надсилаються разом з формою, тож загальний розмір обмежує
post_max_size. -
Поля файлів не підтримуються в діях і не сортуються.
-
Файл може залишитися без власника, якщо процес завершився між записом файлу й комітом або якщо зовнішня транзакція вашого коду відкотилася вже після збереження. Команди для прибирання таких файлів у 1.1 немає.
-
Очищення не рахує посилань: шлях, спільний для кількох записів, потребує
prunable(false)(див. Очищення). -
Ці поля не використовують
spatie/laravel-medialibrary. Її колекції підтримує окреме доповненняmaxcxam/larsadmin-medialibrary(Media,MediaGallery), побудоване на публічному API розширень іFiles\FileSafety.
Далі: Глобальний пошук.