Verification: a143cc29221c9be0

Php artisan storage link не работает

Php artisan storage link не работает

Введение

Laravel обеспечивает мощную абстракцию файловой системы благодаря замечательному пакету Flysystem PHP от Фрэнка де Йонга. Интеграция Laravel с Flysystem содержит простые драйверы для работы с локальными файловыми системами, SFTP и Amazon S3. Более того, удивительно просто переключаться между этими вариантами хранения: как локального, так и производственного серверов – поскольку API остается одинаковым для каждой системы.

Конфигурирование

Файл конфигурации файловой системы Laravel находится в config/filesystems.php. В этом файле вы можете настроить все «диски» файловой системы. Каждый диск представляет собой определенный драйвер хранилища и место хранения. Примеры конфигураций для каждого поддерживаемого драйвера включены в конфигурационный файл, так что вы можете изменить конфигурацию, отражающую ваши предпочтения хранения и учетные данные.

Драйвер local взаимодействует с файлами, хранящимися локально на сервере, на котором запущено приложение Laravel, в то время как драйвер s3 используется для записи в службу облачного хранилища Amazon S3.

Вы можете настроить столько дисков, сколько захотите, и даже иметь несколько дисков, использующих один и тот же драйвер.

Локальный драйвер

При использовании драйвера local все операции с файлами выполняются относительно корневого каталога, определенного в файле конфигурации filesystems. По умолчанию это значение задано каталогом storage/app. Следовательно, следующий метод запишет файл в storage/app/example.txt:

use Illuminate\Support\Facades\Storage;

Storage::disk('local')->put('example.txt', 'Contents');

Публичный диск

Диск public, определенный в файле конфигурации filesystems вашего приложения, предназначен для файлов, которые будут общедоступными. По умолчанию публичный диск использует драйвер local и хранит свои файлы в storage/app/public.

Чтобы сделать эти файлы доступными из интернета, вы должны создать символическую ссылку на storage/app/public в public/storage. Использование этого соглашения о папках позволит хранить ваши публичные файлы в одном каталоге, который может быть легко доступен между развертываниями при использовании систем развертывания с нулевым временем простоя, таких как Envoyer.

Чтобы создать символическую ссылку, вы можете использовать команду storage:link Artisan:

php artisan storage:link

После того, как была создана символическая ссылка, вы можете создавать URL-адреса для сохраненных файлов, используя помощник asset:

echo asset('storage/file.txt');

Вы можете настроить дополнительные символические ссылки в файле конфигурации filesystems. Каждая из настроенных ссылок будет создана, когда вы запустите команду storage:link:

'links' => [
    public_path('storage') => storage_path('app/public'),
    public_path('images') => storage_path('app/images'),
],

Предварительная подготовка драйверов

Пакеты Composer

Перед использованием драйверов S3 или SFTP вам необходимо установить соответствующий пакет с помощью менеджера пакетов Composer:

  • Amazon S3: composer require league/flysystem-aws-s3-v3 "~1.0"
  • SFTP: composer require league/flysystem-sftp "~1.0"

Кроме того, вы можете установить декоратор CachedAdapter для повышения производительности:

  • CachedAdapter: composer require league/flysystem-cached-adapter "~1.0"

Конфигурирование драйвера S3

Информация о конфигурации драйвера S3 находится в вашем файле конфигурации config/filesystems.php. Этот файл содержит пример массива конфигурации для драйвера S3. Вы можете изменить этот массив своей собственной конфигурацией S3 и учетными данными. Для удобства эти переменные среды соответствуют соглашению об именах, используемому в интерфейсе командной строки AWS.

Конфигурирование драйвера FTP

Интеграция Laravel с Flysystem отлично работает с FTP; однако, пример конфигурации по умолчанию не включен в конфигурационный файл config/filesystems.php фреймворка. Если вам нужно настроить файловую систему FTP, вы можете использовать пример конфигурации ниже:

'ftp' => [
    'driver' => 'ftp',
    'host' => 'ftp.example.com',
    'username' => 'your-username',
    'password' => 'your-password',

    
    
    
    
    
    
],

Конфигурирование драйвера SFTP

Интеграция Laravel с Flysystem отлично работает с SFTP; однако, пример конфигурации по умолчанию не включен в конфигурационный файл config/filesystems.php фреймворка. Если вам нужно настроить файловую систему SFTP, вы можете использовать пример конфигурации ниже:

'sftp' => [
    'driver' => 'sftp',
    'host' => 'example.com',
    'username' => 'your-username',
    'password' => 'your-password',

    
    'privateKey' => '/path/to/privateKey',
    'password' => 'encryption-password',

    
    
    
    
],

Кеширование

Чтобы включить кеширование для конкретного диска, вы можете добавить директиву cache в параметры конфигурации этого диска. Параметр cache должен быть массивом параметров кеширования, содержащим имя disk, время expire в секундах и prefix кеша:

's3' => [
    'driver' => 's3',

    

    'cache' => [
        'store' => 'memcached',
        'expire' => 600,
        'prefix' => 'cache-prefix',
    ],
],

Доступ к экземплярам дисков

Фасад Storage используется для взаимодействия с любым из ваших сконфигурированных дисков. Например, вы можете использовать метод put фасада, чтобы сохранить аватар на диске по умолчанию. Если вы вызываете методы фасада Storage без предварительного вызова метода disk, то метод будет проксирован на диск по умолчанию:

use Illuminate\Support\Facades\Storage;

Storage::put('avatars/1', $content);

Если ваше приложение взаимодействует с несколькими дисками, то вы можете использовать метод disk фасада Storage для работы с файлами на указанном диске:

Storage::disk('s3')->put('avatars/1', $content);

Получение файлов

Метод get используется для получения содержимого файла. Необработанное строковое содержимое файла будет возвращено методом. Помните, что все пути к файлам должны быть указаны относительно «корня» диска:

$contents = Storage::get('file.jpg');

Метод exists используется для определения, существует ли файл на диске:

if (Storage::disk('s3')->exists('file.jpg')) {
    
}

Метод missing используется, чтобы определить, отсутствует ли файл на диске:

if (Storage::disk('s3')->missing('file.jpg')) {
    
}

Скачивание файлов

Метод download используется для генерации ответа, который заставляет браузер пользователя загружать файл по указанному пути. Метод download принимает имя файла в качестве второго аргумента метода, определяющий имя файла, которое видит пользователь, скачивающий этот файл. Наконец, вы можете передать массив заголовков HTTP в качестве третьего аргумента метода:

return Storage::download('file.jpg');

return Storage::download('file.jpg', $name, $headers);

URL-адреса файлов

Вы можете использовать метод url, чтобы получить URL для указанного файла. Если вы используете драйвер local, он обычно просто добавляет /storage к указанному пути и возвращает относительный URL-адрес файла. Если вы используете драйвер s3, будет возвращен абсолютный внешний URL-адрес:

use Illuminate\Support\Facades\Storage;

$url = Storage::url('file.jpg');

При использовании драйвера local все файлы, которые должны быть общедоступными, должны быть помещены в каталог storage/app/public. Кроме того, вы должны создать символическую ссылку в public/storage, которая указывает на каталог storage/app/public.

При использовании драйвера local возвращаемое значение url не является URL-кодированным. По этой причине мы рекомендуем всегда хранить ваши файлы, используя имена, которые будут создавать допустимые URL-адреса.

Временные URL

Используя метод temporaryUrl, вы можете создавать временные URL-адреса для файлов, хранящихся с помощью драйвера s3. Этот метод принимает путь и экземпляр DateTime, указывающий, когда должен истечь доступ к файлу по URL:

use Illuminate\Support\Facades\Storage;

$url = Storage::temporaryUrl(
    'file.jpg', now()->addMinutes(5)
);

Если вам нужно указать дополнительные параметры запроса S3, то вы можете передать массив параметров запроса в качестве третьего аргумент методу temporaryUrl:

$url = Storage::temporaryUrl(
    'file.jpg',
    now()->addMinutes(5),
    [
        'ResponseContentType' => 'application/octet-stream',
        'ResponseContentDisposition' => 'attachment; filename=file2.jpg',
    ]
);

Настройка хоста URL

Если вы хотите заранее определить хост для URL-адресов, сгенерированных с помощью фасада Storage, то вы можете добавить параметр url в массив конфигурации диска:

'public' => [
    'driver' => 'local',
    'root' => storage_path('app/public'),
    'url' => env('APP_URL').'/storage',
    'visibility' => 'public',
],

Метаданные файла

Помимо чтения и записи файлов, Laravel также может предоставлять информацию о самих файлах. Например, метод size используется для получения размера файла в байтах:

use Illuminate\Support\Facades\Storage;

$size = Storage::size('file.jpg');

Метод lastModified возвращает временную метку UNIX последнего изменения файла:

$time = Storage::lastModified('file.jpg');

Пути к файлам

Вы можете использовать метод path, чтобы получить путь к указанному файлу. Если вы используете драйвер local, он вернет абсолютный путь к файлу. Если вы используете драйвер s3, этот метод вернет относительный путь к файлу в корзине S3:

use Illuminate\Support\Facades\Storage;

$path = Storage::path('file.jpg');

Хранение файлов

Метод put используется для сохранения содержимого файла на диске. Вы также можете передать resource PHP методу put, который будет использовать поддержку базового потока Flysystem. Помните, что все пути к файлам должны быть указаны относительно «корневого» расположения, настроенного для диска:

use Illuminate\Support\Facades\Storage;

Storage::put('file.jpg', $contents);

Storage::put('file.jpg', $resource);

Автоматическая потоковая передача

Потоковая передача файлов в хранилище позволяет значительно сократить использование памяти. Если вы хотите, чтобы Laravel автоматически управлял потоковой передачей переданного файла в ваше хранилище, вы можете использовать методы putFile или putFileAs. Эти методы принимают экземпляр Illuminate\Http\File или Illuminate\Http\UploadedFile и автоматически передают файл в нужное место:

use Illuminate\Http\File;
use Illuminate\Support\Facades\Storage;


$path = Storage::putFile('photos', new File('/path/to/photo'));


$path = Storage::putFileAs('photos', new File('/path/to/photo'), 'photo.jpg');

Следует отметить несколько важных моментов, касающихся метода putFile. Обратите внимание, что мы указали только имя каталога, а не имя файла. По умолчанию метод putFile генерирует уникальный идентификатор, который будет служить именем файла. Расширение файла будет определено путем проверки MIME-типа файла. Путь к файлу будет возвращен методом putFile, так что вы можете сохранить путь, включая сгенерированное имя файла, в вашей базе данных.

Методы putFile и putFileAs также принимают аргумент для определения «видимости» сохраненного файла. Это особенно полезно, если вы храните файл на облачном диске, таком как Amazon S3, и хотите, чтобы файл был общедоступным через сгенерированные URL:

Storage::putFile('photos', new File('/path/to/photo'), 'public');

Добавление информации к файлам

Методы prepend и append позволяют записывать в начало или конец файла, соответственно:

Storage::prepend('file.log', 'Prepended Text');

Storage::append('file.log', 'Appended Text');

Копирование и перемещение файлов

Метод copy используется для копирования существующего файла в новое место на диске, а метод move используется для переименования или перемещения существующего файла в новое место:

Storage::copy('old/file.jpg', 'new/file.jpg');

Storage::move('old/file.jpg', 'new/file.jpg');

Загрузка файлов

В веб-приложениях одним из наиболее распространенных вариантов хранения файлов является хранение загруженных пользователем файлов, таких как фотографии и документы. Laravel упрощает хранение загруженных файлов с помощью метода store экземпляра загружаемого файла. Вызовите метод store, указав путь, по которому вы хотите сохранить загруженный файл:

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class UserAvatarController extends Controller
{
    
    public function update(Request $request)
    {
        $path = $request->file('avatar')->store('avatars');

        return $path;
    }
}

В этом примере следует отметить несколько важных моментов. Обратите внимание, что мы указали только имя каталога, а не имя файла. По умолчанию метод store генерирует уникальный идентификатор, который будет служить именем файла. Расширение файла будет определено путем проверки MIME-типа файла. Путь к файлу будет возвращен методом store, поэтому вы можете сохранить путь, включая сгенерированное имя файла, в своей базе данных.

Вы также можете вызвать метод putFile фасада Storage, чтобы выполнить ту же операцию сохранения файлов, что и в примере выше:

$path = Storage::putFile('avatars', $request->file('avatar'));

Указание имени файла

Если вы не хотите, чтобы имя файла автоматически присваивалось вашему сохраненному файлу, вы можете использовать метод storeAs, который получает путь, имя файла и (необязательный) диск в качестве аргументов:

$path = $request->file('avatar')->storeAs(
    'avatars', $request->user()->id
);

Вы также можете использовать метод putFileAs фасада Storage, который будет выполнять ту же операцию сохранения файлов, что и в примере выше:

$path = Storage::putFileAs(
    'avatars', $request->file('avatar'), $request->user()->id
);
Непечатаемые и недопустимые символы Unicode будут автоматически удалены из путей к файлам. По этой причине, вы по желанию можете очистить пути к файлам перед их передачей в методы хранения файлов Laravel. Пути к файлам нормализуются с помощью метода League\Flysystem\Util::normalizePath.

Указание диска

По умолчанию метод store загружаемого файла будет использовать ваш диск по умолчанию. Если вы хотите указать другой диск, передайте имя диска в качестве второго аргумента методу store:

$path = $request->file('avatar')->store(
    'avatars/'.$request->user()->id, 's3'
);

Если вы используете метод storeAs, вы можете передать имя диска в качестве третьего аргумента метода:

$path = $request->file('avatar')->storeAs(
    'avatars',
    $request->user()->id,
    's3'
);

Другая информация о загружаемом файле

Если вы хотите получить оригинальное имя загружаемого файла, вы можете сделать это с помощью метода getClientOriginalName:

$name = $request->file('avatar')->getClientOriginalName();

Метод extension используется для получения расширения загружаемого файла:

$extension = $request->file('avatar')->extension();

Видимость файла

В интеграции Laravel Flysystem «видимость» – это абстракция прав доступа к файлам на нескольких платформах. Файлы могут быть объявлены public или private. Когда файл объявляется public, вы указываете, что файл обычно должен быть доступен для других. Например, при использовании драйвера s3 вы можете получить URL-адреса для public файлов.

Вы можете задать видимость при записи файла с помощью метода put:

use Illuminate\Support\Facades\Storage;

Storage::put('file.jpg', $contents, 'public');

Если файл уже был сохранен, его видимость может быть получена и задана с помощью методов getVisibility и setVisibility, соответственно:

$visibility = Storage::getVisibility('file.jpg');

Storage::setVisibility('file.jpg', 'public');

При взаимодействии с загружаемыми файлами, вы можете использовать методы storePublicly и storePubliclyAs для сохранения загружаемого файла с видимостью public:

$path = $request->file('avatar')->storePublicly('avatars', 's3');

$path = $request->file('avatar')->storePubliclyAs(
    'avatars',
    $request->user()->id,
    's3'
);

Локальные файлы и видимость

При использовании драйвера local, видимость public интерпретируется в право доступа 0755 для каталогов и право доступа 0644 для файлов. Вы можете изменить сопоставление прав доступа в файле конфигурации filesystems вашего приложения:

'local' => [
    'driver' => 'local',
    'root' => storage_path('app'),
    'permissions' => [
        'file' => [
            'public' => 0664,
            'private' => 0600,
        ],
        'dir' => [
            'public' => 0775,
            'private' => 0700,
        ],
    ],
],

Удаление файлов

Метод delete принимает имя одного файла или массив имен файлов для удаления:

use Illuminate\Support\Facades\Storage;

Storage::delete('file.jpg');

Storage::delete(['file.jpg', 'file2.jpg']);

При необходимости вы можете указать диск, с которого следует удалить файл:

use Illuminate\Support\Facades\Storage;

Storage::disk('s3')->delete('path/file.jpg');

Каталоги

Получение всех файлов каталога

Метод files возвращает массив всех файлов указанного каталога. Если вы хотите получить список всех файлов каталога, включая все подкаталоги, вы можете использовать метод allFiles:

use Illuminate\Support\Facades\Storage;

$files = Storage::files($directory);

$files = Storage::allFiles($directory);

Получение всех каталогов из каталога

Метод directories возвращает массив всех каталогов указанного каталога. Кроме того, вы можете использовать метод allDirectories, чтобы получить список всех каталогов внутри указанного каталога и всех его подкаталогов:

$directories = Storage::directories($directory);

$directories = Storage::allDirectories($directory);

Создание каталога

Метод makeDirectory создаст указанный каталог, включая все необходимые подкаталоги:

Storage::makeDirectory($directory);

Удаление каталога

Наконец, для удаления каталога и всех его файлов можно использовать метод deleteDirectory:

Storage::deleteDirectory($directory);

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

У нас предусмотрена возможность загрузки изображения для категории и поста блога. Имя файла изображения сохраняется в базе данных — это поле 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:

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

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

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

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

> npm run dev

Введение

Структура приложения Laravel по умолчанию предназначена для обеспечения отличной отправной точки как для больших, так и небольших приложений. Но вы можете организовать свое приложение так, как вам нравится. Laravel почти не налагает ограничений на расположение любого конкретного класса, до тех пор, пока Composer может автоматически загружать класс.

Корневой каталог

Каталог app

Каталог app содержит основной код вашего приложения. Вскоре мы рассмотрим этот каталог более подробно; однако почти все классы в вашем приложении будут в этом каталоге.

Каталог bootstrap

Каталог bootstrap содержит файл app.php, который загружает фреймворк. В этом каталоге также находится каталог cache, содержащий файлы, сгенерированные фреймворком для оптимизации производительности, например, файлы кеша маршрутов и служб. Обычно вам не нужно изменять какие-либо файлы в этом каталоге.

Каталог config

Каталог config, как следует из названия, содержит все файлы конфигурации вашего приложения. Отличная идея прочитать все эти файлы и ознакомиться со всеми доступными вам параметрами.

Каталог database

Каталог database содержит миграции ваших баз данных, фабрики моделей и наполнители. При желании вы также можете использовать этот каталог для хранения SQLite БД.

Каталог public

Каталог public содержит файл index.php, который является точкой входа для всех запросов, поступающих в ваше приложение, и конфигурирует автозагрузку. В этом каталоге также находятся ваши ресурсы, например, изображения, JavaScript и CSS.

Каталог resources

Каталог resources содержит ваши шаблоны, а также ваши необработанные, нескомпилированные ресурсы, например, JavaScript или CSS. В этом каталоге также находятся все ваши языковые файлы.

Каталог routes

Каталог routes содержит все определения маршрутов для вашего приложения. По умолчанию в Laravel входит несколько файлов маршрутов: web.php, api.php, console.php и channels.php.

Файл web.php содержит маршруты, которые RouteServiceProvider размещает в группе посредников web, обеспечивающие состояние сессии, защиту CSRF и шифрование файлов куки. Если в вашем приложении не предполагается сохранение состояния и RESTful API, то, скорее всего, все ваши маршруты будут определены в файле web.php.

Файл api.php содержит маршруты, которые RouteServiceProvider размещает в группе посредников api. Эти маршруты не предполагают сохранения состояния, поэтому запросы, поступающие в приложение через эти маршруты, предназначены для аутентификации с помощью токенов и не имеют доступа к состоянию сессии.

В файле console.php вы можете определить все ваши анонимные консольные команды. Каждое замыкание привязано к экземпляру команды, что обеспечивает простой подход к взаимодействию с методами ввода-вывода каждой команды. Несмотря на то, что этот файл не определяет маршруты HTTP, он определяет точки входа (маршруты) в ваше консольное приложение.

В файле channels.php вы можете зарегистрировать все каналы трансляции событий, которые поддерживает ваше приложение.

Каталог storage

Каталог storage содержит ваши журналы (логи), скомпилированные шаблоны Blade, файлы сессий, кеша и другие файлы, созданные фреймворком. Этот каталог разделен на каталоги app, framework, и logs. Каталог app может использоваться для хранения любых файлов, созданных вашим приложением. Каталог framework используется для хранения файлов и кешей, сгенерированных фреймворком. Наконец, каталог logs содержит файлы журнала вашего приложения.

Каталог storage/app/public может использоваться для хранения файлов, созданных пользователями, таких как аватары профиля, которые должны быть общедоступными. Вы должны создать символическую ссылку (ярлык) в public/storage, которая указывает на этот каталог. Вы можете создать ссылку, используя команду php artisan storage:link Artisan.

Каталог tests

Каталог tests содержит ваши автоматизированные тесты. Примеры модульного PHPUnit и функционального тестов предоставляются из коробки. Каждый тестовый класс должен иметь суффикс Test. Вы можете запускать свои тесты с помощью команд phpunit или php vendor/bin/phpunit. Или, если вы хотите более подробное и красивое отображение результатов ваших тестов, вы можете запускать свои тесты с помощью команды php artisan test Artisan.

Каталог vendor

Каталог vendor содержит ваши Composer-зависимости.