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');

 

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