Laravel で vendor 配下のファイル操作ライブラリ Filesystem（Flysystem） を拡張する方法

こんにちは、しょいみんです。
Laravel フレームワーク内の処理で詰まったところがあったので知見を共有していきますm(_ _)m

# 概要
Laravel フレームワークはフレームワーク自体に様々なオープンソースのライブラリが含まれているため、それほどプラスα でライブラリをインストールすることなく簡単に開発できるようになっています。

しかし、開発を進めていくと**「既存のライブラリだと少し物足りないなぁ」**、**「既存のライブラリを少しだけ拡張したいなぁ」**といった要望が出てくると思います。


今回は、Laravel で標準インストールされている **Flysystem のライブラリを拡張する方法**を紹介いたします。この方法を利用すれば Flysystem に限らず Laravel 内の**他のライブラリも同様に拡張**することができます。

# 結論
**サービスプロバイダー**を**既存のクラス**から**自分で定義したクラス**に**切り替える**ことでどんなライブラリでも自分で定義したクラスに差し替えて呼び出すことができます！！！！！

拡張したい関数だけ修正して他の処理は元のライブラリの処理のままコピペすることで既存の処理を壊さずに拡張したい関数だけ修正することができます。

# 環境
Laravel 5.8
php7.4.5
※検証してませんが Laravel 6, 7 でもサービスプロバイダーの考え方は一緒なので同様の方法で実現できると思います。

# 目標（拡張したい処理の内容）
画像一覧を取得する関数がデフォルトでファイル名ソートしかできないので**任意のメタ情報で昇順・降順ソートできるように関数を拡張します**

## 拡張前の元ファイルと関数
下記ライブラリの関数を修正します。
見ての通りオプションもなく**ファイルパスでしかソートできない**ですね (´・ω・｀)
`vendor/league/flysystem/src/Util/ContentListingFormatter.php`

```php:ContentListingFormatter.php
class ContentListingFormatter
{
・・・・・省略・・・・・・

    /**
     * @param array $listing
     *
     * @return array
     */
    private function sortListing(array $listing)
    {
        usort($listing, function ($a, $b) {
            return strcasecmp($a['path'], $b['path']);
        });

        return $listing;
    }
}

```

## 拡張後のファイルと関数
拡張後のファイルと関数の内容です。
ファイルパスでしかソートできなかった `sortListing()` 関数を**任意のメタ情報**と**昇順・降順フラグ**を引数にとって**並び替え**できるように拡張します。
 
`app/Extensions/Flysystem/Util/ContentListingFormatter.php`

```php:ContentListingFormatter.php
class ContentListingFormatter
{
・・・・・省略・・・・・・

    /**
     * @param array $listing
     * @param string $sortType
     * @param bool $isAsc
     *
     * @return array
     */
    private function sortListing(array $listing, string $key = 'path', $isAsc = true)
    {
        usort($listing, function ($a, $b) use ($key, $isAsc) {
            $result = $isAsc ? strcasecmp($a[$key], $b[$key]) : strcasecmp($b[$key], $a[$key]);
            return $result;
        });

        return $listing;
    }
}
```


# 修正手順
修正手順です。この３つの手順に沿って修正手順を解説していきます。

1. 拡張したい関数が記述されてるクラスとそのクラスを呼び出しているクラスをコピペして新しく作成
2. 関数の修正やネームスペース、呼び出しクラスの参照パスを修正
3. サービスプロバイダー（`FilesystemServiceProvider::class`）の切り替え

##新しく作成するファイル一覧
下記ファイルパスの vendor 配下のライブラリのコードをコピー元として新規作成します。
修正箇所は後述します。

```text:元ファイルと新規作成ファイル
# league/flysystem ライブラリ
コピー元：  vendor/league/flysystem/src/Filesystem.php
新規作成： app/Extensions/Flysystem/Filesystem.php

コピー元：  vendor/league/flysystem/src/Util/ContentListingFormatter.php
新規作成： app/Extensions/Flysystem/Util/ContentListingFormatter.php

# Laravel 標準フレームワーク内の ファイルシステム 
コピー元：  vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemManager.php
新規作成： app/Extensions/Flysystem/FilesystemManager.php

# Laravel 標準フレームワーク内の サービスプロバイダー
コピー元：  vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemServiceProvider.php
新規作成： app/Providers/FilesystemServiceProvider.php
```
※ 新規作成するファイルの保存ディレクトリはどこでもいいですが Extensions に格納することとします。
※ サービスプロバイダーは Providers フォルダに格納することとします。

## 1. 拡張したい関数が記述されてるクラスとそのクラスを呼び出しているクラスをコピペして新しく作成
`ContentListingFormatter.php` を自身で定義したクラスに差し替えるために**必要なファイルをすべてコピペ**します。
新規作成するファイルは上記の **「元ファイルと新規作成ファイル」** で記載されている 4 ファイルです。
Laravel のファイルシステム実行処理の関係で修正が必要なクラスファイルが増えています。
下記に画像一覧を表示させるための処理の流れを記述しておきます。

---------------------------------------

1. `config/app.php` に記載されている `FilesystemServiceProvider.php` が呼び出される
2. `FilesystemServiceProvider.php` の `registerManager()` がエイリアス `'filesystem'` で `FilesystemManager`インスタンスを作成する。
3. `FilesystemManager.php` の `createFlysystem()` 関数で `Flysystem` のインスタンスを返す。
4. `Flysystem.php` の `listContents()` 関数で `ContentListingFormatter`のインスタンスの`formatListing()` 関数を実行する。
5. `ContentListingFormatter.php` の `formatListing()` 関数内でソートの関数である `$this->sortListing()` が実行される。　← **今回はここの関数の処理を改造します。**

---------------------------------------


流れを確認したことで、5番で利用されている `ContentListingFormatter.php` を**自身で定義したクラスに差し替えるため**に 1 ~ 4 で呼び出されているクラスファイルの**参照パスを修正する必要がある**ことがわかります。

## 2. 関数の修正とネームスペース、呼び出しクラスの参照パスを修正
次にコピペして新規作成したファイルを一つずつ修正していきます。

### ContentListingFormatter.php
```php:app/Extensions/Flysystem/Util/ContentListingFormatter.php

<?php

namespace app\Extensions\Flysystem\Util;
# namespace League\Flysystem\Util;

use League\Flysystem\Util;

/**
 * @internal
 */
class ContentListingFormatter
{

--------- 途中 vendor と同じなので省略 ---------

    /**
     * @param array $listing
     * @param string $sortType
     * @param bool $isAsc
     *
     * @return array
     */
#    private function sortListing(array $listing)
    private function sortListing(array $listing, string $key = 'path', $isAsc = true)
    {
        usort($listing, function ($a, $b) use ($key, $isAsc) {
            $result = $isAsc ? strcasecmp($a[$key], $b[$key]) : strcasecmp($b[$key], $a[$key]);
            return $result;
        });

#        usort($listing, function ($a, $b) {
#            return strcasecmp($a['path'], $b['path']);
#        });
        return $listing;
    }
}
```
`ContentListingFormatter.php` では拡張したい関数を修正し `namespace` を app フォルダ配下のパスに修正します。

それ以外のコードは元ファイルのコピペをそのまま利用します。

### Filesystem.php
```php:app/Extensions/Flysystem/Filesystem.php
<?php

# namespace League\Flysystem;
namespace app\Extensions\Flysystem;

use InvalidArgumentException;
use League\Flysystem\Adapter\CanOverwriteFiles;
use League\Flysystem\Plugin\PluggableTrait;
# use League\Flysystem\Util\ContentListingFormatter;
use League\Flysystem\FilesystemInterface; # 追記
use League\Flysystem\ConfigAwareTrait; # 追記
use League\Flysystem\AdapterInterface; # 追記
use League\Flysystem\File; # 追記
use League\Flysystem\Directory; # 追記
use League\Flysystem\Handler; # 追記
use League\Flysystem\Util; # 追記
use League\Flysystem\Config; # 追記
use League\Flysystem\FileNotFoundException; # 追記
use League\Flysystem\FileExistsException; # 追記
use League\Flysystem\RootViolationException; # 追記
use App\Extensions\Flysystem\Util\ContentListingFormatter; # 追記
--------- 以下 vendor と同じなので省略 ---------
```
`Filesystem.php` では `namespace` と `Filesystem.php` 内で利用してるクラスを use 文で参照させます。

もともと vendor 配下の `Filesystem.php` では同ディレクトリに他のクラスファイルの php が配置されていたので直で参照されていました。今回は app フォルダ配下に `Filesystem.php` を新規作成したので vendor 配下のクラスファイルをしっかりと use 文で参照させています。

また、
`use League\Flysystem\Util\ContentListingFormatter;` は vendor 配下のクラスファイルを参照してるのでコメントアウトし、今回自分で作成したファイル
`use App\Extensions\Flysystem\Util\ContentListingFormatter;` 
に参照させ直します。

### FilesystemManager.php
```php:app/Extensions/Flysystem/FilesystemManager.php
<?php

# namespace Illuminate\Filesystem;
namespace app\Extensions\Flysystem;

use Closure;
use Aws\S3\S3Client;
use OpenCloud\Rackspace;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use League\Flysystem\AdapterInterface;
use League\Flysystem\Sftp\SftpAdapter;
use League\Flysystem\FilesystemInterface;
use League\Flysystem\Cached\CachedAdapter;
# use League\Flysystem\Filesystem as Flysystem;
use App\Extensions\Flysystem\Filesystem as Flysystem; # 追記
use League\Flysystem\Adapter\Ftp as FtpAdapter;
use League\Flysystem\Rackspace\RackspaceAdapter;
use League\Flysystem\Adapter\Local as LocalAdapter;
use League\Flysystem\AwsS3v3\AwsS3Adapter as S3Adapter;
use League\Flysystem\Cached\Storage\Memory as MemoryStore;
use Illuminate\Contracts\Filesystem\Factory as FactoryContract;
use Illuminate\Filesystem\FilesystemAdapter; # 追記
--------- 以下 vendor と同じなので省略 ---------
```
こちらも同様 namespace と参照している vendor 配下の `Filesystem.php` の参照先を自分で作成した app 配下の `Filesystem.php` に参照させ直しています。

### FilesystemServiceProvider.php
```php:app/Providers/FilesystemServiceProvider.php

<?php

# namespace Illuminate\Filesystem;
namespace App\Providers; # 追記

use Illuminate\Support\ServiceProvider;
use Illuminate\Filesystem\Filesystem; # 追記
use App\Extensions\Flysystem\FilesystemManager; # 追記
--------- 以下 vendor と同じなので省略 ---------
```
Filesystem のサービスプロバイダーです。 `config/app.php` から呼び出されるやつですね。
こちらも同様、namespace と `FilesystemManager.php` の参照パスを修正し、参照が切れてしまっているクラスファイルを参照させ直しましょう。


以上で、新規作成したファイルの修正が完了となります。
最後にサービスプロバイダーの切り替えを行いましょう。

## 3. サービスプロバイダー（`FilesystemServiceProvider::class`）の切り替え
既存のファイルシステムサービスプロバイダーを自身で作成したサービスプロバイダーに切り替えて変更を反映させます。

### app.php
```php:config/app.php
<?php

return [
    /*
    |--------------------------------------------------------------------------
    | Autoloaded Service Providers
    |--------------------------------------------------------------------------
    |
    | The service providers listed here will be automatically loaded on the
    | request to your application. Feel free to add your own services to
    | this array to grant expanded functionality to your applications.
    |
    */

    'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        Illuminate\Cache\CacheServiceProvider::class,
        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
        Illuminate\Cookie\CookieServiceProvider::class,
        Illuminate\Database\DatabaseServiceProvider::class,
        Illuminate\Encryption\EncryptionServiceProvider::class,
        // 既存の Filesystem ではなくソートのコードを拡張した Filesystem を利用するため
        // Illuminate\Filesystem\FilesystemServiceProvider::class,
        App\Providers\FilesystemServiceProvider::class, # 追記

----------- 以下略 -------------
    ],
];
```
`config/app.php` ファイルを開けて、`'providers'` 配列を見てみましょう。
既存のファイルシステムサービスプロバイダーである`Illuminate\Filesystem\FilesystemServiceProvider::class` 
クラスが記述されています。

ここを自身で作成した、
`App\Providers\FilesystemServiceProvider::class`
に変更します。

以上ですべての修正が完了です。あとはオートローダのリロードを行うと反映されます。

```zsh
composer dump-autoload
```

# まとめ
Laravel ではサービスプロバイダーの切り替えで vendor 配下のライブラリですら自身で定義したクラスファイルに切り替えできることがわかりました。

切り替えの手順は ３ ステップ

1. 拡張したい関数が記述されてるクラスとそのクラスを呼び出しているクラスをコピペして新しく作成
2. 関数の修正やネームスペース、呼び出しクラスの参照パスを修正
3. サービスプロバイダー（`FilesystemServiceProvider::class`）の切り替え

これで vendor 配下を汚さずに拡張できるのでもし要件があったら参考にしてみてくださいm(_ _)m。

ただし、自身で拡張したクラスファイル（ここの場合は `Filesystem.php`、`ContentListingFormatter.php`、`FilesystemManager.php`、`FilesystemServiceProvider.php` の4ファイル） は composer でのライブラリアップデートには追随しないことに注意してください。