Laravel + Vue: завантаження файлів

Laravel + Vue: завантаження файлів

Коротко розглянемо, як завантажувати файли в Laravel та Vuejs.

Створюємо новий інстанс Laravel:

laravel new larexp

 

 Переходимо в директорію проекту і відразу встановлюємо залежності:

cd larexp && npm install

 

Vue

Оскільки це всього лише рецепт, не будемо особливо морочитися і просто перепишемо приклад компонента. Зайдемо в resources/js/components та змінимо назву ExampleComponent.vue на AttachmentForm.vue. Не забудемо внести зміни в реєстрацію компонента у файлі resources/js/app.js:

Vue.component('attachment-form', require('./components/AttachmentForm.vue').default);

 

Відкриваємо файл компонента і для початку пропишемо javascript. В даних у нас буде (що відображається) назва і безпосередньо сам файл (attachment). Також знадобляться методи onAttachmentChange(), який при зміні файлу буде присвоювати його властивості attachment, та метод submit() для відправки даних на сервер за допомогою axios. Оскільки ми передаємо файл, будемо використовувати об'єкт FormData, який на виході використовує такий же формат, як і при відправці звичайної форми з параметром encoding, значення якого встановлено в multipart/form-data. Крім цього, обов'язково треба і в опціях передати дане значення. Знову ж таки для спрощення, після отримання відповіді просто виведемо в консоль повідомлення або відловимо помилку, якщо така буде. Код метода submit():

submit () {
    const config = { 'content-type': 'multipart/form-data' }
    const formData = new FormData()
    formData.append('name', this.name)
    formData.append('attachment', this.attachment)

    axios.post('/', formData, config)
        .then(response => console.log(response.data.message))
        .catch(error => console.log(error))
}

 

Швиденько накидаємо форму за допомогою класів Bootstrap і в підсумку файл компонента буде виглядати так:

<template>
    <form @submit.prevent="submit">
        <div class="form-group">
            <input type="text" class="form-control" placeholder="Name" v-model="name">
        </div>
        <div class="form-group">
            <div class="custom-file">
                <input type="file"
                    class="custom-file-input"
                    id="customFile"
                    @change="onAttachmentChange"
                >
                <label class="custom-file-label" for="customFile">Choose file</label>
            </div>
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-primary">Submit</button>
        </div>
    </form>
</template>

<script>
    export default {
        data () {
            return {
                name: null,
                attachment: null
            }
        },
        methods: {
            submit () {
                const config = { 'content-type': 'multipart/form-data' }
                const formData = new FormData()
                formData.append('name', this.name)
                formData.append('attachment', this.attachment)

                axios.post('/', formData, config)
                    .then(response => console.log(response.data.message))
                    .catch(error => console.log(error))
            },
            onAttachmentChange (e) {
                this.attachment = e.target.files[0]
            }
        }
    }
</script>

 

Не заради авторизації, а для отримання готового шаблону Blade виконаємо команду:

php artisan make:auth

 

Відткриваємо resources/views/home.blade.php і вбудовуємо компонент:

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">File Uploads</div>

                <div class="card-body">
                    <attachment-form></attachment-form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

 

Laravel

Створюємо модель і міграцію для файлів-вкладень:

php artisan make:model -m Attachment

 

Додаємо потрібні поля:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateAttachmentsTable extends Migration
{
    public function up()
    {
        Schema::create('attachments', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('path')->unique();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('attachments');
    }
}

 

та запускаємо:

php artisan migrate

 

Хоча це і простий приклад, подумаємо про проект трохи більше і про те, що можливо доведеться завантажувати аватар в профіль користувача, зображення і / або вкладення постів або товарів і т.д. Іншими словами, замість того, щоб прописати метод завантаження прямо в моделі, створимо трейт, що дозволить горизонтально розширювати будь-які класи, де необхідне завантаження будь-яких файлів. Заодно і передбачимо ситуацію з використанням різних сховищ і каталогів в цих сховищах. Файл трейта буде розташовуватися в app/Traits/Eloquent (каталоги створюємо ручками) і буде називатися Uploadable.php. Ось його код:

<?php

namespace App\Traits\Eloquent;

use Illuminate\Support\Facades\Storage;

trait Uploadable
{
    public function upload($file, $storage = 'public', $folder = 'uploads')
    {
        $filename = uniqid() . '_' . str_replace(' ', '_', $file->getClientOriginalName());
        $path = Storage::disk($storage)->putFileAs($folder, $file, $filename);

        if (Storage::disk($storage)->exists($path)) {
            return $path;
        }

        return null;
    }
}

 

Можна було і не створювати своє ім'я файлу, а покластися на Laravel, але давайте таки додамо до згенерованого унікального ідентифікатору оригінальну назву файлу, попутно замінивши пробіли на нижні підкреслення. 

Включаємо трейт в модель Attachment:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use App\Traits\Eloquent\Uploadable;

class Attachment extends Model
{
    use Uploadable;

    protected $fillable = ['name', 'path'];
}

 

Також створимо клас StoreAttachmentRequest, в який винесемо валідацію (розширення вказані перші які прийшли в голову + максмальний розмір файлу 1Mb):

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreAttachmentRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'name' => 'required|string|max:255',
            'attachment' => 'required|max:1024|mimes:pdf,png,jpeg,gif,txt'
        ];
    }
}

 

Використаємо HomeController, в якому напишемо метод store() для збереженя файлів. Після збереження віддамо повідомлення про те, що файл успішно завантажений (те саме повідомлення, вивід якого ми раніше прописували у Vue):

<?php

namespace App\Http\Controllers;

use App\Attachment;
use App\Http\Requests\StoreAttachmentRequest;

class HomeController extends Controller
{
    public function index()
    {
        return view('home');
    }

    public function store(StoreAttachmentRequest $request)
    {
        $attachment = new Attachment;
        $attachment->name = $request->name;
        $attachment->path = $attachment->upload($request->attachment);

        $attachment->save();

        return response()->json([
            'message' => 'Attachment has been successfully uploaded.',
        ]);
    }
}

 

Останній штрих - роути. Відткриваємо routes/web.php та використаємо те, що є, додавши маршрут для збереження/завантаження даних:

Route::get('/', 'HomeController@index')->name('home');
Route::post('/', 'HomeController@store');

 

Готово, можна перевіряти. Успіхів!