Laravel自作make:〇〇コマンド

## まえがき
Laravelで開発をしている人であれば

```
php artisan make:controller ExampleController
```
のように「make:〇〇」コマンドを使い新しいクラスを作ったことがあると思います

`make:controller`や`make:model`など、はじめから多くの「make:〇〇」コマンドが用意されており、クラスの雛形をコマンド一つで作れて便利ですが

アプリケーション特有のクラスは、いちいち似たようなファイルをコピーしてクラス名を変えて...とやってはいませんか？

すでにやっている方も多いと思いますが、今回は備忘録を兼ねて「make:〇〇」コマンドの作り方をまとめました

[Laravel で Service 層を取り入れるときに検討したいこと](https://qiita.com/nunulk/items/6b7a7bbda17192f6b2f5)
こちらの記事のようにService層の導入を例にし「make:service」コマンドを作ってみたいと思います

## 1.コマンドの雛形を作る
コマンドを作るartisanコマンドを使い「make:service」コマンドの雛形を作ります

```
php artisan make:command ServiceMakeCommand
```

コマンドを実行すると`app/Console/Commands/`の中に`ServiceMakeCommand.php`が作られます

```php:app/Console/Commands/ServiceMakeCommand.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

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

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

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        //
    }
}
```

## 2.GeneratorCommandを継承する
[ドキュメント](https://github.com/laravel/docs/blob/6.x/artisan.md)や[翻訳](https://readouble.com/laravel/6.0/ja/artisan.html)などを読めばコマンドの作り方は載っていますが「make:〇〇」系のコマンドを作る場合は、手っ取り早く作るための`GeneratorCommand`があるのでそちらを継承して作っていきます

```diff
-use Illuminate\Console\Command;
+use Illuminate\Console\GeneratorCommand as Command;
```

## 3.コマンド名を設定する
```diff
-    protected $signature = 'command:name';
+    protected $name = 'make:service';  // make:〇〇にする
```
`$signature`を消す理由は[後述](#9引数やオプションを整備するやる必要がある人のみ)

## 説明を設定する

```php
    protected $description = 'Create a new Service class';  // $descriptionプロパティを書き換える
```

## 4.クラスの種類を設定する
今回はServiceクラスを作りたいのでServiceとする

```php
    protected $type = 'Service';
```

`$type`を設定しておくとコマンドを実行して成功した場合などに

```
Service created successfully.
```
と表示してくれる

## 5.コンストラクタを消す
`GeneratorCommand`のコンストラクタでは`Illuminate\Filesystem\Filesystem`を受け取っているため`make:command`で生成された状態だと動かないです
必要でない限りコンストラクタは消しておいたほうが良さげ

```diff
-    /**
-     * Create a new command instance.
-     *
-     * @return void
-     */
-    public function __construct()
-    {
-        parent::__construct();
-    }
```

どうしても必要なら`Filesystem`クラスをuseして

```diff
+use Illuminate\Filesystem\Filesystem;
```

`GeneratorCommand`のコンストラクタに渡してあげる

```php
    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct(Filesystem $files)
    {
        parent::__construct($files);

        //

    }
```

## 6.handle()メソッドを消す
`handle`メソッドは`GeneratorCommand`でいい感じに実装されているので、特別必要でなければ消す

```diff
-    /**
-     * Execute the console command.
-     *
-     * @return mixed
-     */
-    public function handle()
-    {
-        //
-    }
```

## 7.getStub()メソッドを実装する
`getStub()`メソッドは、クラスを生成する際に元となるファイルのパスを返す抽象メソッドです
今回は`app/Console/Commands/stubs/service.stub`というServiceクラスを生成する際の元となるファイルを用意する予定なので以下のように実装

```php
    /**
     * 生成に使うスタブファイルを取得する
     *
     * @return string
     */
    protected function getStub()
    {
        return __DIR__.'/stubs/service.stub';
    }
```

## 8.getDefaultNamespace($rootNamespace)メソッドをオーバーライドする
生成するクラスのデフォルトの名前空間を指定する
`getDefaultNamespace($rootNamespace)`メソッドをオーバーライドしないと`App`直下に作られてしまうのでやっておくが吉

今回は`App\Services`名前空間に作りたいので以下のようにオーバーライド

```php
    /**
     * クラスのデフォルトの名前空間を取得する
     *
     * @param  string  $rootNamespace
     * @return string
     */
    protected function getDefaultNamespace($rootNamespace)
    {
        return $rootNamespace . '\Services';
    }
```

## 9.引数やオプションを整備する(やる必要がある人のみ)
[翻訳](https://readouble.com/laravel/6.0/ja/artisan.html)とかでは`$signature`プロパティを使う、引数やオプションの定義の仕方が書いてありますが
`$name`プロパティと`getArguments()`メソッド`getOptions()`メソッドを使う、もう一つのやり方があります

`GeneratorCommand`は後者のやり方で、クラス名を受け取る引数`name`が、既に定義してありますので、そちらに合わせて実装します

### 前準備
 1. [コマンド名を設定する](#3コマンド名を設定する)

 2. 以下のクラスをuseしておきます

```diff
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
```

#### 引数の定義

```php
    /**
     * InputArgumentのコンストラクタへ渡す引数の配列のリストを返します
     * 
     * @return array
     */
    protected function getArguments()
    {
        // GeneratorCommandでクラス名を受け取る引数(name)を定義しているのでマージする
        return array_merge(
            parent::getArguments(),
            [
                // InputArgumentのコンストラクタへ渡す引数の配列を追加していく
                // ここに引数を表現する配列を追加していく
                // 左から
                // @param string               $name        引数名
                // @param int|null             $mode        引数のモード(self::REQUIREDとself::OPTIONALはどちらか一つ)
                // @param string               $description 引数の説明
                // @param string|string[]|null $default     引数の初期値(引数のモードにself::OPTIONALを指定している場合のみ)
                ['argName', InputArgument::REQUIRED, 'the argument description']

            ]
        );
    }
```

引数のモードは以下のいずれか、または組み合わせを指定します

|  | 説明 |
|---|---|
| InputArgument::REQUIRED | 必須の引数 |
| InputArgument::OPTIONAL | 任意の引数 |
| InputArgument::IS_ARRAY | 配列を受け取る引数 |

> 組み合わせるときはビット和演算子(`|`)で
`InputArgument::REQUIRED|InputArgument::IS_ARRAY`

#### オプションの定義

```php
    /**
     * InputOptionのコンストラクタへ渡す引数の配列のリストを返します
     *
     * @return array
     */
    protected function getOptions()
    {
        return [
            // InputOptionのコンストラクタへ渡す引数の配列を追加していく
            // 左から
            // @param string                        $name        オプション名
            // @param string|array|null             $shortcut    オプションショートカット
            // @param int|null                      $mode        オプションのモード(self::VALUE_NONEとself::VALUE_REQUIREDとself::VALUE_OPTIONALはどれか一つ)
            // @param string                        $description オプションの説明
            // @param string|string[]|int|bool|null $default     オプションの初期値(オプションのモードにself::VALUE_NONE以外を指定している場合のみ)
            ['optName', 'o', InputOption::VALUE_OPTIONAL, 'the option description'],
        ];
    }
```

オプションのモードは以下のいずれか、または組み合わせを指定します

|  | 説明 |
|---|---|
| InputOption::VALUE_NONE     | 値を受け取らないオプション(オプションを指定されるとtrue、指定なしだとfalseになるタイプのオプション) |
| InputOption::VALUE_REQUIRED | そのオプションを指定した場合、値の入力は必須 |
| InputOption::VALUE_OPTIONAL | そのオプションを指定した場合でも、値の入力は任意 |
| InputOption::VALUE_IS_ARRAY | 配列を受け取るオプション |

> 組み合わせるときはビット和演算子(`|`)で
`InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY`

### 確認

コマンドが追加されたか確認(`list`コマンド)

```
php artisan list
```

引数やオプションの確認(`-h`オプション)

```
php artisan make:service -h
```


## 10.stubファイルを作る
`getStub()`メソッドのところで言っていたクラスの元となるファイルを作ります

※`App\Services\Service`スーパークラスを継承した〇〇クラスを作る想定

```php:app/Console/Commands/stubs/service.stub
<?php

namespace DummyNamespace;

use DummyRootNamespaceServices\Service;

class DummyClass extends Service
{
    //
}

```

ここででてきた`DummyClass`のような文字列はコマンド実行時に置換されます

| 変換前 | 変換後 |
|---|---|
| `DummyClass` | コマンドの引数`name`で受け取ったクラス名 |
| `DummyNamespace` | `getDefaultNamespace()`とコマンドの引数`name`で受け取った名前空間の部分を繋げたもの |
| `DummyRootNamespace` | Laravelアプリケーションのルート名前空間(App\\) <br>※`\`がついている|
| `NamespacedDummyUserModel` | 認証に使うUserモデルの完全修飾クラス名 |

## 11.他にも置換したい場合
置換は`buildClass($name)`メソッドで行われているのでオーバーライドする

```php
    /**
     * 指定された名前でクラスを構築する
     *
     * @param  string  $name
     * @return string
     */
    protected function buildClass($name)
    {
        // 引数やオプションなどを使い'置換前'=>'置換後'のような配列を作る
        $replace = [
            'DummyAuthor' => $this->option('author')
        ];

        return str_replace(
            array_keys($replace),
            array_values($replace),
            parent::buildClass($name)
        );
    }
```

## 使う

```
php artisan make:service ExampleService
```

`app/Services/ExampleService.php`ができる!!

## あとがき
コマンドさえ作ってしまえばストレスフリーになれるので初めのうちに作っておくとかなり便利!!
今回はクラスだったけどviewファイルを作るコマンドとかいいんじゃね