Создание своего драйвера Filesystem в Laravel

07 Февраля 2023 16:40

Это может потребоваться в тех случаях, когда в базе хранятся разные пути, например, к изображениям, но для взаимодействия с ними используется один диск. Например, один изображения имеют путь в storage/images/, а другие storage/upload и мы хотим сами формировать url файла с помощью url() метода.
В документации рассмотрен один вариант - на примере с драйвером Dropbox. Но это не совсем то. Нас интересует работа именно с локальными файлами.

Создаем адаптер

Создаём файл laravel/app/Filesystem/PublicFilesystemAdapter.php.

Помещаем в него следующее содержимое:

<?php

namespace App\Filesystem;

use League\Flysystem\Adapter\Local;

class PublicFilesystemAdapter extends Local {

    public function __construct(private $config)
    {
        parent::__construct($config['root']);
    }

    public function getUrl($path): string
    {
        $url = array_key_exists('url', $this->config) ? $this->config['url'] : '/storage';

        if (!\Str::startsWith($path, '/upload')) {
            return $this->concatPathToUrl($url, $path);
        }

        return $path;
    }

    /**
     * Concatenate a path to a URL.
     *
     * @param string $url
     * @param string $path
     * @return string
     */
    protected function concatPathToUrl(string $url, string $path): string
    {
        return rtrim($url, '/') . '/' . ltrim($path, '/');
    }
}

Здесь создается адаптер и наследуется от стандартного ларавелевского Local адаптера. Он имеет все методы Local.

Инициализируем подключение кастомного драйвера в сервис провайдере.

Идем в файл laravel/app/Providers/AppServiceProvider.php.

Подключаем классы:

use Illuminate\Support\Facades\Storage;
use League\Flysystem\Filesystem;
use App\Filesystem\PublicFilesystemAdapter;

Добавляем туда код в метод boot():

public function boot()
{
    Storage::extend('public', function ($app, $config) {
        return new Filesystem(new PublicFilesystemAdapter($config));
    });
}

Здесь мы регистрируем свой драйвер, который будет потом в конфиге использоваться.

Обязательно должен быть возвращен Filesystem, а он параметром обязательно принимает адаптер, а адаптеру передаем конфиг (адаптер Local по умолчанию принимает не конфиг, а путь root).

Редактируем конфиг filesystems.php.

Идем в файл laravel/config/filesystems.php.

Добавляем новый диск:

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

Благолдаря регистрации в AppServiceProvider, система уже знает о новом драйвере, поэтому сюда добавляем его название которое регистрировали - public.

Теперь мы можем использоваться всеми стандартными функциями обычного диска, но метод url() у нас будет работать по скорректированной логике.

Пример:

\Storage::disk('public')->url('path/to/file.jpg');

Немного о принципах работы Filesystem в laravel.

В рамках этой системы используется паттерн "Адаптер". Его задача предоставить единый интерфейс взаимодействия с разными сущностями, выполняющими одну задачу. В данном случае, задача - манипулирование файлами, разные сущности - это локальное файловое хранилище, облачное хранилище Dropbox, облачное хранилище Google и т.д.

В качестве основного пакета для файловой системы в laravel используется League/Flysystem. Экземпляр его класса и должен возвращаться в сервис провайдере при объявлении кастомного драйвера.

Стоит обратить внимание на метод url() файла laravel/vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php.

    public function url($path)
    {
        $adapter = $this->driver->getAdapter();

        if ($adapter instanceof CachedAdapter) {
            $adapter = $adapter->getAdapter();
        }

        if (method_exists($adapter, 'getUrl')) {
            return $adapter->getUrl($path);
        } elseif (method_exists($this->driver, 'getUrl')) {
            return $this->driver->getUrl($path);
        } elseif ($adapter instanceof AwsS3Adapter) {
            return $this->getAwsUrl($adapter, $path);
        } elseif ($adapter instanceof Ftp || $adapter instanceof Sftp) {
            return $this->getFtpUrl($path);
        } elseif ($adapter instanceof LocalAdapter) {
            return $this->getLocalUrl($path);
        } else {
            throw new RuntimeException('This driver does not support retrieving URLs.');
        }
    }

Именно он работает когда мы хотим получить url файла через \Storage::disk('public')->url('path/to/file.jpg');. Он ищет функцию getUrl() в адаптере или драйверах и выполняет первый попавшийся вариант. Этот метод мы и определили в своем кастомном адаптере.

Материалы по этой теме на просторах интернета

  1. Документация - https://laravel.su/docs/8.x/filesystem#custom-filesystems. Пример с пакетом Dropbox.
  2. Старая статья 7милетней давности - https://laracasts.com/discuss/channels/general-discussion/l5-use-flysystem-replicate-adapter-with-filesystem. Здесь предлагают наследовать FilesystemManager без особых подробностей где и для чего это делать.
  3. Возможно, более простой способ переопределить стандартный драйвер файловой системы - https://spatie.be/docs/laravel-medialibrary/v10/advanced-usage/overriding-the-default-filesystem-behaviour
  4. Вопрос на Stackoverflow - https://stackoverflow.com/questions/42744293/how-to-extend-laravel-storage-facade. Тоже пример с FilesystemManager.
  5. Статья разработчика пакета Flysystem - https://benjamincrozat.com/laravel-dropbox-driver#how-to-create-a-custom-storage-driver-in-laravel. В последнем абзаце "Как сделать кастомный драйвер". Пробовал сделать как в статье - не сработало.