Laravel でいつも最初から仕込むようなファイルたちはパッケージにしてしまおう

# はじめに
以前、[Laravel の Validation を正しく拡張する](https://qiita.com/moobay9/items/f1cdd3c8f995fdcf0963) という記事を書きましたが、こういったバリデーションの拡張などは最初から仕込みで動いていて欲しいわけです。  
なので `composer` でインストール後、コマンドを叩けばある程度の設定が最初から入ってしまう状態にしたい、という欲求が湧いてきます。    
これに答えてくれるのが[パッケージ化](https://readouble.com/laravel/5.8/ja/packages.html)です。  
FuelPHP を使ってた頃は作り方が簡単だったんですが、Laravel のパッケージは定型があまりないので自由度が高い分、ちとやり方がめんどくさいですが整理のためにも記録として残しておきます。  

Laravel におけるパッケージとは簡単に言えば vendor ディレクトリ以下にインストールされるファイル群たちの構成要素を自分で一つ増やす感じの配置になりますね。  

## 開発の準備

ディレクトリは任意です。
ただ動作確認的には自分自身の Laravel と組み合わせながら作るのが一番手っ取り早いとは思いますので、開発中は Laravel をインストール後に app たちと同一階層とかにパッケージ専用ディレクトリを作るのが自分としてはわかりやすかったです。

```shell
$ php artisan tinker
Psy Shell v0.9.12 (PHP 7.4.3 — cli) by Justin Hileman
>>> base_path();
=> "/FULL/PATH/Parent/PackageDir"
```

この `base_path()` と同じ所にいれるのがよかったです。このディレクトリで下記を実行。


```shell

$ mkdir packages
$ cd packages
$ composer init
```

いろいろ聞かれると思いますが、あとでどうせ編集するので適当でよいです。
この `composer.json` を最終的に下記のように調整しました。一部情報はダミーです。


```json:packages/composer.json
{
    "name": "funaffect/packages",
    "description": "Funaffect Laravel Packages",
    "authors": [
        {
            "name": "moobay9",
            "email": "dummy@mail.dummy"
        }
    ],
    "require": {
        "php": "^7.0",
        "laravel/framework": "5.8.*"
    },
    "autoload": {
        "psr-4": {
            "Funaffect\\LaravelPackages\\": "app/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Funaffect\\LaravelPackages\\Providers\\StartupServiceProvider"
            ]
        }
    }
}
```

ここでは

* 依存
* 名前空間とオートロード
* Laravel 本体から呼ばれる ServiceProvider 

を合わせて設定しています。

## 名前空間とオートロード

`composer.json` の autoload 部分ですが、今回は `app/` にしていますが、これは任意です。
ただディレクトリは具体的にどこだよ、って感じになりますが、

```php
base_path().'/packages/app/'
```

です。app と同じ階層に今回は config と resources を置きました。
ディレクトリ構造を Laravel がインストールされているものと合わせたかったからです。

名前空間は任意ですきなものをつかたら良いと思いますが、今後ついてまわるのでちゃんと考えて名前をつけましょう。
今回は psr-4 の下の `Funaffect\\LaravelPackages` の部分ですね。

## サービスプロバイダの準備
今回は `Funaffect\\LaravelPackages\\Providers\\StartupServiceProvider` という名前にしたサービスプロバイダーでこのパッケージのインストールから利用まで全てをカバーさせます。
設置パスは

```php
base_path().'/packages/app/Providers/StartupServiceProvider.php'
```

になりますので、ここにファイルを作りました。


## サービスプロバイダでできること
最初にファイルを展開することと、アプリケーションが動作中にもサービスプロバイダーを通過して起動するようになります。
このサービスプロバイダで自分がやりたかったのは、下記の数ありました。

  * バリデーションの拡張ファイルの設置
  * バリデーションルールの追加
  * config のファイルを設置
  * 言語ファイルの設置
  * middleware の追加

これは後ほど細部を説明します。


## 動作させてみる

親のディレクトリ（Laravel の Root）にある composer.json で、一個下のパッケージ開発中の composer.json を認識させる必要があります。


```json:composer.json
"repositories": [
    {
        "type": "path",
        "url": "/packages",
        "symlink": true
    }
],
```

このあと

```shell
composer require funaffect/packages 
```

でインストールできます。require は `packages/composer.json` の **name** と一致させてください。
場合によってはそのあとで

```shell
php artisan vendor:publish --tag=packages
```

が必要です。


## サービスプロバイダに設定していく

サービスプロバイダー内は基本的に `register()` と `boot()` で設定していきますが、register はドキュメントなどにも記載がありますが、まだロードが終わってなかったりインスタンス化されてないクラスがあったりしますので大抵は boot 側に記載していくことになると思います。

### configファイル を app/config へ設置する

```php:packages/app/Providers/StartupServiceProvider.php
    public function boot() {
        $this->publishes([
            __DIR__.'/../../config/packages.php' => config_path('packages.php'),
        ], 'packages');
    }
```

`publishes()` で設置するべきディレクトリに設置するべきファイルをおくだけですね。今回はパッケージ内にある packages/config/packages.php をいつも使う config のディレクトリの中へコピーできるようにしました。

### ミドルウェアを追加する
ミドルウェアは通常、Route から始まるクラスが出てくるあたりで設定しますので、一般的には `routes/web.php` の中でやっていることが多いと思います。
ですので、web というミドルウェアグループがありますんで、これに追加してあげる形にしました。


```php:packages/app/Providers/StartupServiceProvider.php
use Funaffect\LaravelPackages\Http\Middleware\ConvertDot;
// (略)

    public function boot() {
        $this->app['router']->pushMiddlewareToGroup('web', ConvertDot::class);
    }
```

### バリデーションを拡張する
バリデーションの拡張はクラスにまとめる場合と一個一個追加する場合があります。
例では大まかに **ExtensionValidator** でクラスごとバリデーションを拡張、 `max_length` と `min_length` を個別に追加しています。
それとは別にルールを一つ追加するために publishes で先ほどの config と同じようにファイルを設置しました。
（設置先はパスでわかると思います）


```php:packages/app/Providers/StartupServiceProvider.php
use Funaffect\LaravelPackages\Http\Validators\ExtensionValidator;
    public function boot() {
        // Validation Extension
        $this->app['validator']->resolver(function($translator, $data, $rules, $messages, $attribute) {
            return new ExtensionValidator($translator, $data, $rules, $messages, $attribute);
        });

        $this->app['validator']->replacer('max_length', function ($message, $attribute, $rule, $parameters) {
            return str_replace(':max', $parameters[0], $message);
        });

        $this->app['validator']->replacer('min_length', function ($message, $attribute, $rule, $parameters) {
            return str_replace(':min', $parameters[0], $message);
        });

        $this->publishes([
            __DIR__.'/../Rules/CurrentPassword.php' => app_path('Http/Rules/CurrentPassword.php'),
        ], 'packages');
    }
```

### 初期設定を上書きする
config 配下の app.config を毎度毎度書き直していますが、これもめんどくせえとなったときには最初から変えておく手段が欲しいなと思った時に、 `mergeConfigFrom()` という真逆の動きをする関数がサービスプロバイダ内部にあります。なのでこれの src と dst を入れ替えた `mergeConfigFor()` という関数を作り、register で実行するようにしました。

```php:packages/app/Providers/StartupServiceProvider.php

    public function register()
    {
        // Config
        $this->mergeConfigFor(__DIR__.'/../../config/app.php', 'app');
    }

    protected function mergeConfigFor($path, $key)
    {
        if (! ($this->app instanceof CachesConfiguration && $this->app->configurationIsCached())) {
            $this->app['config']->set($key, array_merge(
                $this->app['config']->get($key, []), require $path
            ));
        }
    }
```


ここまでで、大体やりたいことは初期構築時に組み込めるようになりました。


## 実際に運用する場合

[こちらで公開](https://github.com/moobay9/laravelpackages)してますので、README にも同じことを書いていますが、Laravel をインストール後、composer.json に以下を追加してください。

```json:composer.json
"repositories": [
    {
        "type": "vcs",
        "url": "https://github.com/moobay9/laravelpackages.git",
        "symlink": true
    }
],
"require": {
    "funaffect/packages": "dev-master"
},
```

`composer install` 実行後、`php artisan vendor:publish --tag=packages` をお忘れなく。

なお、packagist には登録していません。


## まとめ

駆け足で振り返ってきましたが、とりあえず備忘録の代わりなのでミスが見つかったら随時修正していきます。
ミスを発見した方はお手数ですがコメントでお知らせください。
git のほうは pull req していただいても構いません。