Блог на Laravel 7, часть 13. Загрузка и ресайз изображений для категорий и постов блога

26.01.2021

Теги: ComposerLaravelMySQLPHPWeb-разработкаБазаДанныхБлогИзображениеПрактикаФормаФреймворк

Загрузка и ресайз изображений

У нас предусмотрена возможность загрузки изображения для категории и поста блога. Имя файла изображения сохраняется в базе данных — это поле image в таблицах categories и posts. Давайте для начала просто загрузим изображение для категории и сохраним имя файла в БД — а потом напишем отдельный класс ImageSaver, чтобы использовать его в контроллерах CategoryController и PostController.

Первым делом выполняем в консоли artisan-команду, которая создаст символическую ссылку:

> php artisan storage:link

Сохраняем изображение для категории на диск и записываем имя файла изображения в БД:

namespace App\Http\Controllers\Admin;

use App\Category;
use App\Http\Controllers\Controller;
use App\Http\Requests\CategoryRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class CategoryController extends Controller {

    public function __construct() {
        $this->middleware('perm:manage-categories')->only('index');
        $this->middleware('perm:create-category')->only(['create', 'store']);
        $this->middleware('perm:edit-category')->only(['edit', 'update']);
        $this->middleware('perm:delete-category')->only('destroy');
    }

    /**
     * Показывает список всех категорий
     */
    public function index() {
        $items = Category::all();
        return view('admin.category.index', compact('items'));
    }

    /**
     * Показывает форму для создания категории
     */
    public function create() {
        return view('admin.category.create');
    }

    /**
     * Сохраняет новую категорию в базу данных
     */
    public function store(CategoryRequest $request) {
        $image = $request->file('image');
        if ($image) { // был загружен файл изображения
            $path = $image->store('category/source', 'public');
            $base = basename($path);
        }
        $data = $request->all();
        $data['image'] = $base ?? null;
        Category::create($data);
        return redirect()
            ->route('admin.category.index')
            ->with('success', 'Новая категория успешно создана');
    }

    /**
     * Показывает форму для редактирования категории
     */
    public function edit(Category $category) {
        return view('admin.category.edit', compact('category'));
    }

    /**
     * Обновляет категорию блога в базе данных
     */
    public function update(CategoryRequest $request, Category $category) {
        if ($request->remove) { // если надо удалить изображение
            $old = $category->image;
            if ($old) {
                Storage::disk('public')->delete('category/source/' . $old);
            }
        }
        $file = $request->file('image');
        if ($file) { // был загружен файл изображения
            $path = $file->store('category/source', 'public');
            $base = basename($path);
            // удаляем старый файл изображения
            $old = $category->image;
            if ($old) {
                Storage::disk('public')->delete('category/source/' . $old);
            }
        }
        $data = $request->all();
        $data['image'] = $base ?? null;
        $category->update($data);
        return redirect()
            ->route('admin.category.index')
            ->with('success', 'Категория была успешно исправлена');
    }

    /**
     * Удаляет категорию блога
     */
    public function destroy(Category $category) {
        if ($category->children->count()) {
            $errors[] = 'Нельзя удалить категорию с дочерними категориями';
        }
        if ($category->posts->count()) {
            $errors[] = 'Нельзя удалить категорию, которая содержит посты';
        }
        if (!empty($errors)) {
            return back()->withErrors($errors);
        }
        // удаляем файл изображения
        $image = $category->image;
        if ($image) {
            Storage::disk('public')->delete('category/source/' . $image);
        }
        // удаляем категорию блога
        $category->delete();
        return redirect()
            ->route('admin.category.index')
            ->with('success', 'Категория блога успешно удалена');
    }
}

Для постов блога все будет аналогично, так что нет смысла повторять. Теперь создадим отдельный класс ImageSaver в директории app/Helpers — который будет отвечать за загрузку изображений. Но кроме того, будет еще изменять размер загружаемого изображения — чтобы его размер был 1000x300 px — у обычного пользователя может и не быть подходящего инструмента, чтобы изменить размер найденной в интернете картинки.

Устанавливаем пакет для изменения размера изображения:

> composer require intervention/image

При установке пакета получил предупреждение «You are using an outdated version of Composer. Composer 2.0 is now available and you should upgrade». Обновлять composer до 2-ой версии пока не стал, но поискал информацию на этот счет. В принципе, обновляться достаточно безопасно:

  • Composer 2.0 по-прежнему поддерживает PHP 5.3 и выше, так же как Composer 1.x
  • Файлы composer.lock совместимы, можно обновиться до 2.0 и откатиться обратно
  • Большинство команд и аргументов остаются неизменными

Если запустить composer self-update на 1.x, он предупредит, что доступна новая версия Composer, и можно использовать composer self-update --2 для перехода. Если возникнут проблемы с новой версией, можно откатиться обратно с помощью composer self-update --1. При автоматической установке composer из скрипта можно передать аргумент --1, чтобы скрипт не устанавливал Composer 2.0 по умолчанию.

Открываем файл config/app.php и добавляем следующие строки:

return [
    /* ... */
    'providers' => [
        /* ... */
        Intervention\Image\ImageServiceProvider::class,
    ],
    
    'aliases' => [
        /* ... */
        'Image' => Intervention\Image\Facades\Image::class,
    ]
    /* ... */
];

Класс ImageSaver для загрузки и ресайза изображений:

namespace App\Helpers;

use App\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;

class ImageSaver {
    /**
     * Сохраняет изображение при создании или редактировании категории,
     * или поста блога + создает уменьшеное изображение 1000x300 px.
     *
     * @param App\Item $item — модель категории блога или поста блога
     * @return string|null — имя файла изображения для сохранения в БД
     */
    public function upload($item = null) {
        $dir = 'post';
        if ($item instanceof Category) {
            $dir = 'category';
        }
        $name = $item->image;
        if ($name && request()->remove) { // если надо удалить изображение
            $this->remove($item);
            $name = null;
        }
        $source = request()->file('image');
        if ($source) { // если было загружено изображение
            // перед загрузкой нового изображения удаляем старое
            if ($item->image) {
                $this->remove($item);
                $name = null;
            }
            // сохраняем загруженное изображение на диск; $src будет
            // содержать путь относительно хранилища вместе с именем
            $src = $source->store($dir . '/source', 'public');
            $name = basename($src); // имя загруженного файла
            // создаем уменьшенное изображение 1000x300px, качество 100%
            $dst = str_replace('source', 'image', $src);
            $this->resize($src, $dst, 1000, 300);
        }
        return $name;
    }

    /**
     * Создает уменьшенную копию изображения
     *
     * @param string $src — путь к исходному изображению
     * @param string $dst — путь к уменьшенному изображению
     * @param integer $width — ширина в пикселях
     * @param integer $height — высота в пикселях
     */
    private function resize($src, $dst, $width, $height) {
        // абсолютный путь к исходному изображению
        $path = Storage::disk('public')->path($src);
        $image = Image::make($path)
            ->heighten($height)
            ->resizeCanvas($width, $height, 'center', false, 'eeeeee')
            ->encode(pathinfo($path, PATHINFO_EXTENSION), 100);
        Storage::disk('public')->put($dst, $image);
        $image->destroy();
    }

    /**
     * Удаляет изображение при удалении категории или поста блога
     *
     * @param App\Item $item — модель категории или поста блога
     */
    public function remove($item) {
        $dir = 'post';
        if ($item instanceof Category) {
            $dir = 'category';
        }
        $image = $item->image;
        if ($image) {
            Storage::disk('public')->delete($dir . '/source/' . $image);
            Storage::disk('public')->delete($dir . '/image/' . $image);
        }
    }
}

Внедряем экземпляр класса ImageSaver в контроллер CategoryController (dependency injection):

namespace App\Http\Controllers\Admin;

use App\Category;
use App\Helpers\ImageSaver;
use App\Http\Controllers\Controller;
use App\Http\Requests\CategoryRequest;
use Illuminate\Http\Request;

class CategoryController extends Controller {

    private $imageSaver;

    public function __construct(ImageSaver $imageSaver) {
        $this->imageSaver = $imageSaver;
        $this->middleware('perm:manage-categories')->only('index');
        $this->middleware('perm:create-category')->only(['create', 'store']);
        $this->middleware('perm:edit-category')->only(['edit', 'update']);
        $this->middleware('perm:delete-category')->only('destroy');
    }

    /**
     * Показывает список всех категорий
     */
    public function index() {
        $items = Category::all();
        return view('admin.category.index', compact('items'));
    }

    /**
     * Показывает форму для создания категории
     */
    public function create() {
        return view('admin.category.create');
    }

    /**
     * Сохраняет новую категорию в базу данных
     */
    public function store(CategoryRequest $request) {
        $category = new Category();
        $category->fill($request->except('image'));
        $category->image = $this->imageSaver->upload($category);
        $category->save();
        return redirect()
            ->route('admin.category.index')
            ->with('success', 'Новая категория успешно создана');
    }

    /**
     * Показывает форму для редактирования категории
     */
    public function edit(Category $category) {
        return view('admin.category.edit', compact('category'));
    }

    /**
     * Обновляет категорию блога в базе данных
     */
    public function update(CategoryRequest $request, Category $category) {
        $data = $request->except('image');
        $data['image'] = $this->imageSaver->upload($category);
        $category->update($data);
        return redirect()
            ->route('admin.category.index')
            ->with('success', 'Категория была успешно исправлена');
    }

    /**
     * Удаляет категорию блога
     */
    public function destroy(Category $category) {
        if ($category->children->count()) {
            $errors[] = 'Нельзя удалить категорию с дочерними категориями';
        }
        if ($category->posts->count()) {
            $errors[] = 'Нельзя удалить категорию, которая содержит посты';
        }
        if (!empty($errors)) {
            return back()->withErrors($errors);
        }
        // удаляем файл изображения
        $this->imageSaver->remove($category);
        // удаляем категорию блога
        $category->delete();
        return redirect()
            ->route('admin.category.index')
            ->with('success', 'Категория блога успешно удалена');
    }
}

С загрузкой изображения для поста немного сложнее. Потому что пост может создать и отредактировать обычный пользователь в личном кабинете. Кроме того, пост может отредактировать администратор из панели управления. Так что мы должны внедрить экземпляр класса ImageSaver в классы Admin\PostController и User\PostController.

namespace App\Http\Controllers\Admin;

use App\Category;
use App\Helpers\ImageSaver;
use App\Http\Controllers\Controller;
use App\Http\Requests\PostRequest;
use App\Post;
use Illuminate\Http\Request;

class PostController extends Controller {

    private $imageSaver;

    public function __construct(ImageSaver $imageSaver) {
        $this->imageSaver = $imageSaver;
        /* ... */
    }
    /* ... */
    public function update(PostRequest $request, Post $post) {
        $data = $request->except(['image', 'tags']);
        $data['image'] = $this->imageSaver->upload($post);
        $post->update($data);
        $post->tags()->sync($request->tags);
        // кнопка редактирования может быть нажата в режиме пред.просмотра
        // или в панели управления блогом, так что и редирект будет разный
        $route = 'admin.post.index';
        $param = [];
        if (session('preview')) {
            $route = 'admin.post.show';
            $param = ['post' => $post->id];
        }
        return redirect()
            ->route($route, $param)
            ->with('success', 'Пост был успешно обновлен');
    }
    /* ... */
    public function destroy(Post $post) {
        // удаляем изображение поста
        $this->imageSaver->remove($post);
        // удаляем пост из базы данных
        $post->delete();
        // пост может быть удален в режиме пред.просмотра или из панели
        // управления, так что и редирект после удаления будет разным
        $route = 'admin.post.index';
        if (session('preview')) {
            $route = 'blog.index';
        }
        return redirect()
            ->route($route)
            ->with('success', 'Пост блога успешно удален');
    }
}
namespace App\Http\Controllers\User;

use App\Helpers\ImageSaver;
use App\Http\Controllers\Controller;
use App\Http\Requests\PostRequest;
use App\Post;
use Illuminate\Http\Request;

class PostController extends Controller {

    private $imageSaver;

    public function __construct(ImageSaver $imageSaver) {
        $this->imageSaver = $imageSaver;
        $this->middleware('perm:create-post')->only(['create', 'store']);
    }
    /* ... */
    public function store(PostRequest $request) {
        // уникальный идентификатор автора поста
        $request->merge(['user_id' => auth()->user()->id]);
        // сохраняем новый пост в базе данных
        $post = new Post();
        $post->fill($request->except(['image', 'tags']));
        $post->image = $this->imageSaver->upload($post);
        $post->save();
        // привязываем теги к новому посту
        $post->tags()->attach($request->tags);
        // все готово, выполняем редирект
        return redirect()
            ->route('user.post.show', ['post' => $post->id])
            ->with('success', 'Новый пост успешно создан');
    }
    /* ... */
    public function update(PostRequest $request, Post $post) {
        // проверяем права пользователя на это действие
        if ( ! $this->can($post)) {
            abort(404);
        }
        // обновляем сам пост и привязку тегов к посту
        $data = $request->except(['image', 'tags']);
        $data['image'] = $this->imageSaver->upload($post);
        $post->update($data);
        $post->tags()->sync($request->tags);
        // кнопка редактирования может быть нажата в режиме пред.просмотра
        // или в личном кабинете пользователя, поэтому и редирект разный
        $route = 'user.post.index';
        $param = [];
        if (session('preview')) {
            $route = 'user.post.show';
            $param = ['post' => $post->id];
        }
        return redirect()
            ->route($route, $param)
            ->with('success', 'Пост был успешно обновлен');
    }
    /* ... */
    public function destroy(Post $post) {
        // проверяем права пользователя на это действие
        if ( ! $this->can($post)) {
            abort(404);
        }
        // удаляем изображение поста
        $this->imageSaver->remove($post);
        // удаляем пост из базы данных
        $post->delete();
        return redirect()
            ->route('user.post.index')
            ->with('success', 'Пост блога успешно удален');
    }
}

Laravel mix и сборка фронтенда

Чтобы не возникало путаницы, давайте договоримся, какие js и css файлы мы будем подключать в шаблонах layout.site (публичная часть), layout.user (личный кабинет) и layout.admin (панель управления).

  • файл resources/js/app.js будет подключаться во всех трех layout-шаблонах, при сборке будет сохраняться как public/js/app.js
  • файл resources/js/site.js будет подключаться в шаблоне layout.site, при сборке будет сохраняться как public/js/site.js
  • файл resources/js/back.js будет подключаться в шаблонах layout.user и layout.admin, при сборке — это public/js/back.js
  • файл resources/sass/app.css будет подключаться во всех трех layout-шаблонах, при сборке будет сохраняться как public/css/app.css
  • файл resources/css/site.css будет подключаться в шаблоне layout.site, при сборке будет сохраняться как public/css/site.css
  • файл resources/css/back.css будет подключаться в шаблонах layout.user и layout.admin, при сборке — это public/css/back.js

В файле конфигурации mix webpack.mix.js зададим правила сборки фронтенда:

const mix = require('laravel-mix');

mix.js('resources/js/app.js', 'public/js/app.js')
    .scripts('resources/js/site.js', 'public/js/site.js')
    .scripts('resources/js/back.js', 'public/js/back.js');

mix.sass('resources/sass/app.scss', 'public/css')
    .styles('resources/css/site.css', 'public/css/site.css')
    .styles('resources/css/back.css', 'public/css/back.css');

И подключим js и css-файлы в шаблонах layout.site, layout.user и layout.admin.

Wysiwyg-редактор для постов

Будем использовать summernote — простой, легкий и есть возможность вставлять видео и изображения. Но самое главное — можно навесить свои обработчики события добавления и удаления изображений. А это означает, что можно организовать систему автозагрузки и автоудаления изображений на сервере.

Устанавливаем пакет summernote с помощью npm:

> npm install summernote --save-prod

Установить все пакеты из секций dependencies и devDependencies файла package.json:

> npm install

Установить все пакеты из секции dependencies файла package.json (для production сервера)

> npm install --production

Установить пакет package-name, но не добавлять его в файл package.json:

> npm install package-name --no-save

Установить пакет package-name и добавить в секцию devDependencies файла package.json:

> npm install package-name --save-dev
> npm install -D package-name

Установить пакет package-name и добавить в секцию dependencies файла package.json:

> npm install package-name --save-prod
> npm install -P package-name
> npm install package-name

Удалить пакет package-name, но не удалять его из файла package.json:

> npm uninstall package-name

Удалить пакет package-name и удалить его из секции devDependencies файла package.json:

> npm uninstall package-name --save-dev
> npm uninstall -D package-name

Удалить пакет package-name и удалить его из секции dependencies файла package.json:

> npm uninstall package-name --save
> npm uninstall -S package-name

Теперь редактируем файл resources/js/bootstrap.js (в котором происходит подключение css-фреймворка Bootstrap и js-фреймворка jQuery), чтобы подключить редактор.

window._ = require('lodash');

try {
    window.Popper = require('popper.js').default;
    window.$ = window.jQuery = require('jquery');

    require('bootstrap');

    require('summernote');
    require('summernote/dist/summernote-bs4.css');
    require('summernote/dist/summernote-bs4.js');
    require('summernote/dist/lang/summernote-ru-RU.js');
} catch (e) {}

window.axios = require('axios');

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

В шаблоне с формой редактирования поста и страницы задаем идентификатор для textarea:

<div class="form-group">
    <textarea class="form-control" name="content" id="editor" placeholder="Текст поста"
              rows="4">{{ old('content') ?? $post->content ?? '' }}</textarea>
</div>
<div class="form-group">
    <textarea class="form-control" name="content" id="editor" placeholder="Контент (html)"
              required rows="10">{{ old('content') ?? $page->content ?? '' }}</textarea>
</div>

И добавляем код для редактора в js-файле resources/js/back.js:

$(document).ready(function () {
    $('#editor').summernote({
        lang: 'ru-RU',
        height: 300
    });
});

Осталось только заново собрать фронтенд:

> npm run dev

Загрузка изображений редактора

По умолчанию редактор summernote сохраняет изображение прямо в атрибуте src тега <img>:

<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi assumenda blanditiis consequatur cum cupiditate ea
facere, facilis fuga fugit ipsum itaque laboriosam, laudantium nemo, nulla odio placeat quas recusandae repellat
repudiandae sint unde ut vitae voluptas voluptate voluptatem. Amet assumenda dolorum enim iusto odit quis similique.
</p>
<p>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5
ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4
..........
ciI/Pv3k9zcAAACiSURBVHja7NAxAQAACAMgtX/nWcHPByLQKa5GgSxZsmTJkqVAlixZsmTJUiBLlixZsmQpkCVLlixZshTIkiVLlixZCmTJkiVLliwFsmTJ
kiVLlgJZsmTJkiVLgSxZsmTJkqVAlixZsmTJUiBLlixZsmQpkCVLlixZshTIkiVLlixZCmTJkiVLliwFsmTJkiVLlgJZsmTJkiVLgSxZ31YAAQYAil4Bx7aJ
z7QAAAAASUVORK5CYII=" data-filename="test.png" style="width: 100px;">
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi assumenda blanditiis consequatur cum cupiditate ea
facere, facilis fuga fugit ipsum itaque laboriosam, laudantium nemo, nulla odio placeat quas recusandae repellat
repudiandae sint unde ut vitae voluptas voluptate voluptatem. Amet assumenda dolorum enim iusto odit quis similique.
</p>

Но во-первых, в этом случае неудобно работать в редакторе, если переключиться в режим редактирования кода. А во-вторых, не хотелось бы хранить изображения в базе данных, раздувая ее без необходимости. Так что будем сохранять изображения на сервере, и заменять значение атрибута src тега <img>.

Редактор summernote предоставляет возможность навесить свои обработчики на события вставки и удаления изображений:

$(document).ready(function () {
    /*
     * Подключение wysiwyg-редактора + загрузка и удаление изображений
     */
    (function () {
        // что сейчас редактируется — пост или страница?
        var entity = getEntity();
        if (entity === '') return;
        /*
         * Подключение wysiwyg-редактора для формы поста или страницы
         */
        $('#editor').summernote({
            lang: 'ru-RU',
            height: 300,
            callbacks: {
                /*
                 * При вставке изображения загружаем его на сервер
                 */
                onImageUpload: function(images) {
                    for (var i = 0; i < images.length; i++) {
                        uploadImage(images[i], this);
                    }
                },
                /*
                 * При удалении изображения удаляем его на сервере
                 */
                onMediaDelete: function(target) {
                    removeImage(target[0].src);
                }
            }
        });
        /*
         * Загружает на сервер вставленное в редакторе изображение
         */
        function uploadImage(image, textarea) {
            var data = new FormData();
            data.append('image', image);
            data.append('entity', entity);
            $.ajax({
                data: data,
                type: 'POST',
                url: '/user/upload/' + entity + '/image',
                cache: false,
                contentType: false,
                processData: false,
                success: function(url) {
                    // $(textarea).summernote('insertImage', url);
                    $(textarea).summernote('insertImage', url, function ($img) {
                        $img.css('max-width', '100%');
                    });
                }
            });
        }
        /*
         * Удаляет на сервере удаленное в редакторе изображение
         */
        function removeImage(src) {
            $.ajax({
                data: {'remove': src, '_method': 'DELETE', 'entity': entity},
                type: 'POST',
                url: '/user/remove/' + entity + '/image',
                cache: false,
                success: function(msg) {
                    // console.log(msg);
                }
            });
        }
        /*
         * Определяет, что сейчас редактируется — пост или страница
         */
        function getEntity() {
            var entity = '';
            if (window.location.pathname.indexOf('page') !== -1) {
                entity = 'page';
            }
            if (window.location.pathname.indexOf('post') !== -1) {
                entity = 'post';
            }
            return entity;
        }
    })();
});

При вставке или удалении изображения в wysiwyg-редакторе — отправляем post-запрос на /user/upload/{post|page}/image или /user/remove/{post|page}/image. Добавим четыре маршрута и создадим контроллеры, которые будут обрабатывать эти post-запросы.

/*
 * Личный кабинет пользователя
 */
Route::group([
    'as' => 'user.', // имя маршрута, например user.index
    'prefix' => 'user', // префикс маршрута, например user/index
    'namespace' => 'User', // пространство имен контроллеров
    'middleware' => ['auth'] // один или несколько посредников
], function () {
    /* ... */
    // загрузка изображения поста блога из wysiwyg-редактора
    Route::post('upload/post/image', 'PostImageController@upload')
        ->name('upload.post.image');
    // удаление изображения поста блога в wysiwyg-редакторе
    Route::delete('remove/post/image', 'PostImageController@remove')
        ->name('remove.post.image');
    // загрузка изображения страницы из wysiwyg-редактора
    Route::post('upload/page/image', 'PageImageController@upload')
        ->name('upload.page.image');
    // удаление изображения страницы в wysiwyg-редакторе
    Route::delete('remove/page/image', 'PageImageController@remove')
        ->name('remove.page.image');
});
> php artisan make:controller User\ImageController
namespace App\Http\Controllers\User;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;

class ImageController extends Controller {

    // TODO: нужна валидация загружаемого изображения

    /**
     * Загружает изображение, которое было добавлено в wysiwyg-редакторе и
     * возвращает ссылку на него, чтобы в редакторе вставить <img src="…"/>
     */
    public function upload(Request $request) {
        $path = $request->file('image')->store('upload', 'public');
        return Storage::disk('public')->url($path);
    }

    /**
     * Удаляет изображение, которое было удалено в wysiwyg-редакторе
     */
    public function remove(Request $request) {
        $path = parse_url($request->remove, PHP_URL_PATH);
        $path = str_replace('/storage/', '', $path);
        Storage::disk('public')->delete($path);
    }
}
> php artisan make:controller User\PageImageController
namespace App\Http\Controllers\User;

class PageImageController extends ImageController {
    public function __construct() {
        // TODO: нужна проверка прав на редактирование страницы
    }
}
> php artisan make:controller User\PostImageController
namespace App\Http\Controllers\User;

class PostImageController extends ImageController {
    public function __construct() {
        // TODO: нужна проверка прав на редактирование поста
    }
}

Вроде все готово, можно проверять. Но вылезла ошибка «CSRF token mismatch» — при отправке формы нужно отправлять csrf-токен.

Чтобы решить эту проблему раз и навсегда, добавим в back.js отправку токена при всех ajax-запросах — и пересоберем фронтенд. А токен наш js-код будет брать из мета-тега, который нужно добавить в шаблоны layout.user и layout.admin.

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>.....</title>
    .....
</head>
$(document).ready(function () {
    /*
     * Общие настройки ajax-запросов, отправка на сервер csrf-токена
     */
    $.ajaxSetup({
        headers: {
            'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
        }
    });
    /* ... */
});
> npm run dev

Не совсем очевидно, как удалить изображение, чтобы вызвать событие удаления. Если кликнуть по изображению — появится всплывающая менюшка. И уже в этой менюшке надо кликнуть по иконке корзины.

Поиск: Composer • Laravel • MySQL • PHP • Web-разработка • База данных • Блог • Изображение • Практика • Форма • Фреймворк

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.