Laravelパッケージ開発ハンズオン〜環境編〜

Laravelのパッケージ開発を行う流れやディレクトリ構成について考えました。

## 流れ

- 作業ディレクトリを作成
- 開発パッケージを作成
  - 例として artisan に `hello:world` コマンドを追加する
- Docker環境の構築
- 開発パッケージのインストール
- 動作確認

## 最終的なディレクトリ構成

```shell-session
.
└── work # 作業ディレクトリ
    ├── core # Laravelインストール場所
    ├── docker # docker環境
    └── packages # パッケージのディレクトリ
        └── laravel-hello-world # myパッケージ
```

- 複数のパッケージを開発する場合を考慮して `packages` と一つディレクトリを挟む
- coreにLaravelをインストールする。
- Laravelのバージョン切り替えは考慮してない。
  - もし切り替えたい場合は `core` の中に 各バージョンのディレクトリを切るか🤔

## 作業ディレクトリを作成

```shell-session
$ mkdir -p work/packages/laravel-hello-world
```

## Docker環境の構築

以前の記事で紹介した構成をカスタマイズして使います。

```shell-session
$ cd work
$ git clone git@github.com:ucan-lab/docker-laravel5.git docker
$ cd docker
```

`work` ディレクトリでgit cloneを実行します。

### work/docker/.env 修正(変更箇所のみ)

```
COMPOSE_PROJECT_NAME=laravel-hello-world
PROJECT_PATH=../core
PACKAGES_PATH=../packages
```

### work/docker/docker-compose.yml 修正(変更箇所のみ)

```yml
services:
  app:
    volumes:
      - ${PACKAGES_PATH}:/packages
```

`- ${PACKAGES_PATH}:/packages` の行を追記します。

### Dockerビルド＆Laravelインストール

```
$ docker-compose up -d --build
$ docker-compose exec app composer create-project --prefer-dist "laravel/laravel=5.8.*" .
```

`core` ディレクトリ内にLaravelがインストールされます。

## 開発パッケージを作成

今回は例として `laravel-hello-world` パッケージという artisan コマンドに `hello:world` を追加するだけのサンプルパッケージを作ることにします。

### 作業ディレクトリへ移動

```shell-session
$ docker-compose exec app ash
$ cd /packages/laravel-hello-world
```

### composer init

`composer init` コマンドを実行して、 `composer.json` ファイルを作成します。

```shell-console
$ composer init

  Welcome to the Composer config generator  
                                            
This command will guide you through creating your composer.json config.

Package name (<vendor>/<name>) [root/laravel-hello-world]: ucan-lab/laravel-hello-world
Description []: laravel-hello-world
Author [, n to skip]: n
Minimum Stability []: 空Enter
Package Type (e.g. library, project, metapackage, composer-plugin) []: library
License []: MIT

Define your dependencies.

Would you like to define your dependencies (require) interactively [yes]? n
Would you like to define your dev dependencies (require-dev) interactively [yes]? n

{
    "name": "ucan-lab/laravel-hello-world",
    "description": "laravel-hello-world",
    "type": "library",
    "license": "MIT",
    "require": {}
}

Do you confirm generation [yes]? 空Enter
```

`"require": {}` から以下の設定を追記します。

```json
    "require": {
        "php": "^7.1",
        "laravel/framework": "^5.5.0"
    },
    "autoload": {
        "psr-4": {
            "UcanLab\\LaravelHelloWorld\\": "src/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "UcanLab\\LaravelHelloWorld\\Providers\\ConsoleServiceProvider"
            ]
        }
    }
```

- 依存ライブラリ
- 名前空間とオートロード
- Laravel用のサービスプロバイダ

### composer バリデーション

バリデーションコマンドを実行して `composer.json` の記述に誤りがなければokです。

```
$ composer validate
./composer.json is valid
```

### 空のディレクトリと空のファイルを作成

```shell-session
$ mkdir -p src/{Console,Providers}
$ touch src/Console/HelloWorldCommand.php
$ touch src/Providers/ConsoleServiceProvider.php
```

### src/Console/HelloWorldCommand.php 作成

```php
<?php

namespace UcanLab\LaravelHelloWorld\Console;

use Illuminate\Console\Command;

class HelloWorldCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'hello:world';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Hello World.';

    /**
     * @return void
     */
    public function handle(): void
    {
        $this->info('Hello World.');
    }
}
```

### src/Providers/ConsoleServiceProvider.php

```php
<?php

namespace UcanLab\LaravelHelloWorld\Providers;

use Illuminate\Support\ServiceProvider;
use UcanLab\LaravelHelloWorld\Console\HelloWorldCommand;

class ConsoleServiceProvider extends ServiceProvider
{
    protected $defer = true;

    public function boot()
    {
        $this->registerCommands();
    }

    public function register()
    {
        // register bindings
    }

    protected function registerCommands()
    {
        $this->app->singleton('command.ucan-lab.hello.world', function () {
            return new HelloWorldCommand();
        });

        $this->commands([
            'command.ucan-lab.hello.world',
        ]);
    }

    public function provides()
    {
        return [
            'command.ucan-lab.hello.world',
        ];
    }
}
```

## 開発パッケージのインストール

### work/core/composer.json

下記のリポジトリ設定を追記します。 どこでもいいんですけど、 `require` の手前あたりが綺麗な気がします。

```json
    "repositories": [
        {
            "type": "path",
            "url": "/packages/laravel-hello-world",
            "symlink": true
        }
    ],
```

`"symlink": true` をしておくと `core/vender/laravel-hello-world` シンボリックリンクを貼ってくれるので、パッケージのソースコードを変更しても `composer update` 不要でリアルタイムで反映されます。
`url` は dockerから見たファイルパスを設定します。

```shell-session
$ docker-compose exec app composer require ucan-lab/laravel-hello-world:dev-master
```

## 動作確認

```shell-session
$ docker-compose exec app php artisan hello:world
Hello World.
```

`Hello World.` と表示される。

<img width="428" alt="スクリーンショット 2019-08-28 20.30.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52879/622f9f7e-17ac-3168-0723-a567dda9e2be.png">

## さいごに

テストコードやpackagist登録は別記事にしようかなと思います。

## 参考

- [Laravel 5.8 パッケージ開発
](https://readouble.com/laravel/5.8/ja/packages.html)
- [
Laravel 5.8 Artisanコンソール
](https://readouble.com/laravel/5.8/ja/artisan.html)
- [Laravelの開発環境をDockerを使って構築する](https://qiita.com/ucan-lab/items/17c806973e69792ada99)
- [Laravel Aspect](https://github.com/ytake/Laravel-Aspect/tree/master/tests)
- [Laravel Dacapo](https://github.com/ucan-lab/laravel-dacapo)