Laravel11でServiceを手作業で作るのがちょっと面倒くさいなと感じたのでコマンドにしてみた

## はじめに

Laravelを使用する際にService層とRepository層を使用することがあるかと思います。自分が今後Laravelを使用する上でかなり使いそうだったのでコマンドで作成できるようにしてみました。

## 環境

PHP:8.3.8
Laravel:11.11.1
composer:2.7.6
OS:Ubuntu 22.04.4 LTS

## Laravelってコマンド作れるの？

はい、コマンドが作れます。詳細については以下の記事によくまとまっておりますので気になる方は読んでみてください。今回は「コマンドが作れる」という部分だけ押さえておけば問題ないです。


https://skrum.co.jp/blog/create-service-class-with-artisan-command/

https://qiita.com/ntm718/items/14751e6d52b4bebde810

すぐに使いたい方は以下の部分を確認ください。

<details><summary>完成したコード</summary>

```php:app/Console/Commands/CreateServiceCommand
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\File;

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

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create a new service class';

    /**
     * Directory path for services.
     *
     * @const string
     */
    private const SERVICES_PATH = 'app/Services/';

    /**
     * The class name of the service to create.
     *
     * @var string
     */
    private string $className;

    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        $this->className = Str::studly($this->argument('className'));

        if ($this->className === '') {
            $this->error('The service class name is invalid. Please provide a valid class name.');
            return;
        }

        $this->createServiceClass();
    }

    /**
     * Create the service class file.
     */
    private function createServiceClass(): void
    {
        $directoryPath = $this->getServiceDirectoryPath();

        // Create the directory if it doesn't exist
        if (!File::exists($directoryPath)) {
            File::makeDirectory($directoryPath, 0755, true);
        }

        $filePath = $this->getServiceFilePath();
        $fileContent = $this->getServiceClassTemplate();

        $this->writeFile($filePath, $fileContent);
    }

    /**
     * Get the full file path of the service class.
     *
     * @return string
     */
    private function getServiceFilePath(): string
    {
        return $this->getServiceDirectoryPath() . $this->className . '.php';
    }

    /**
     * Get the full directory path for service classes.
     *
     * @return string
     */
    private function getServiceDirectoryPath(): string
    {
        return base_path(self::SERVICES_PATH);
    }

    /**
     * Get the content of the service class.
     *
     * @return string
     */
    private function getServiceClassTemplate(): string
    {
        return "<?php\n\nnamespace App\\Services\\{$this->className};\n\nclass {$this->className}\n{\n    // Implement your service methods here\n}\n";
    }

    /**
     * Write content to a file, handling existence and errors.
     *
     * @param string $filePath
     * @param string $fileContent
     */
    private function writeFile(string $filePath, string $fileContent): void
    {
        if (File::exists($filePath)) {
            $this->warn("File {$filePath} already exists.");
        } else {
            try {
                File::put($filePath, $fileContent);
                $this->info("Service {$this->className} created successfully.");
            } catch (\Exception $e) {
                $this->error("Failed to create file: {$filePath}. Error: " . $e->getMessage());
            }
        }
    }
}
```

</details>

## やりかた

以下コマンドを実行します。

```bash:bash
$ php artisan make:command CreateServiceCommand
```

上記のコードを実行すると`app/Console/Commands/CreateServiceCommand`が作成されます。

```php:app/Console/Commands/CreateServiceCommand
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

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

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

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

## コンソールコマンドの名前や引数、オプションを定義

```php:変更前
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:create-service-command';
```

```php:変更後
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'make:service {className : The name of the service class}';
```

今回作成するコマンドはServiceを作成するコマンドであり、ほかのコマンドと見た目をそろえたかったので`app:create-service-command`から`make:service {className : The name of the service class}`に変更しました。

`className`は必須の引数、`The name of the service class`は`className`引数の説明です。

<details><summary>現在のソースコード</summary>

```php:app/Console/Commands/CreateServiceCommand
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

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

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

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

</details>


## コンソールコマンドの説明文を設定

```php:変更前
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';
```

```php:変更後
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create a new service class';
```

こちらも他の`make:`コマンドを参考にして設定しました。

```bash:例
make:controller           Create a new controller class
make:model                Create a new Eloquent model class
make:view                 Create a new view
```

<details><summary>現在のソースコード</summary>

```php:app/Console/Commands/CreateServiceCommand
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

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

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create a new service class';

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

</details>


## サービスクラスを格納するディレクトリのパスを定義

```php:追加
    /**
     * Directory path for services.
     *
     * @const string
     */
    private const SERVICES_PATH = 'app/Services/';
```

共通しているのでパスは固定していいと思います。

<details><summary>現在のソースコード</summary>

```php:app/Console/Commands/CreateServiceCommand
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

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

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create a new service class';

    /**
     * Directory path for services.
     *
     * @const string
     */
    private const SERVICES_PATH = 'app/Services/';

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

</details>

## handle処理を追加する

こちらは一気に追加します。

```php:変更前
    /**
     * Execute the console command.
     */
    public function handle()
    {
        //
    }
```

```php:変更後
    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        $this->className = Str::studly($this->argument('className'));

        if ($this->className === '') {
            $this->error('The service class name is invalid. Please provide a valid class name.');
            return;
        }

        $this->createServiceClass();
    }

    /**
     * Create the service class file.
     */
    private function createServiceClass(): void
    {
        $directoryPath = $this->getServiceDirectoryPath();

        // Create the directory if it doesn't exist
        if (!File::exists($directoryPath)) {
            File::makeDirectory($directoryPath, 0755, true);
        }

        $filePath = $this->getServiceFilePath();
        $fileContent = $this->getServiceClassTemplate();

        $this->writeFile($filePath, $fileContent);
    }

    /**
     * Get the full file path of the service class.
     *
     * @return string
     */
    private function getServiceFilePath(): string
    {
        return $this->getServiceDirectoryPath() . $this->className . '.php';
    }

    /**
     * Get the full directory path for service classes.
     *
     * @return string
     */
    private function getServiceDirectoryPath(): string
    {
        return base_path(self::SERVICES_PATH);
    }

    /**
     * Get the content of the service class.
     *
     * @return string
     */
    private function getServiceClassTemplate(): string
    {
        return "<?php\n\nnamespace App\\Services\\{$this->className};\n\nclass {$this->className}\n{\n    // Implement your service methods here\n}\n";
    }

    /**
     * Write content to a file, handling existence and errors.
     *
     * @param string $filePath
     * @param string $fileContent
     */
    private function writeFile(string $filePath, string $fileContent): void
    {
        if (File::exists($filePath)) {
            $this->warn("File {$filePath} already exists.");
        } else {
            try {
                File::put($filePath, $fileContent);
                $this->info("Service {$this->className} created successfully.");
            } catch (\Exception $e) {
                $this->error("Failed to create file: {$filePath}. Error: " . $e->getMessage());
            }
        }
    }
```

上記のコードについて説明していきます。

### handle()

```php:app/Console/Commands/CreateServiceCommand.php@handle()
    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        $this->className = Str::studly($this->argument('className'));

        if ($this->className === '') {
            $this->error('The service class name is invalid. Please provide a valid class name.');
            return;
        }

        $this->createServiceClass();
    }
```

`handle()`で行っていることは

- サービスクラスネームの取得
- クラスネームの存在チェック
- サービスクラスの作成

となります。

### createServiceClass()

```php:app/Console/Commands/CreateServiceCommand.php@createServiceClass()
    /**
     * Create the service class file.
     */
    private function createServiceClass(): void
    {
        $directoryPath = $this->getServiceDirectoryPath();

        // Create the directory if it doesn't exist
        if (!File::exists($directoryPath)) {
            File::makeDirectory($directoryPath, 0755, true);
        }

        $filePath = $this->getServiceFilePath();
        $fileContent = $this->getServiceClassTemplate();

        $this->writeFile($filePath, $fileContent);
    }
```

`createServiceClass()`で行っていることは

- Serviceディレクトリパスを取得する
- Serviceディレクトが存在しないとき、Serviceディレクトを作成する
- Serviceファイルを作成するパスを取得する
- Serviceクラスのテンプレートを取得する
- Serviceファイルを作成する

といった流れとなります。

### getServiceFilePath()

```php:app/Console/Commands/CreateServiceCommand.php@getServiceFilePath()
    /**
     * Get the full file path of the service class.
     *
     * @return string
     */
    private function getServiceFilePath(): string
    {
        return $this->getServiceDirectoryPath() . $this->className . '.php';
    }
```

`getServiceFilePath()`は以下の通りです。

- サービスファイルを作成するパスを返却する

### getServiceDirectoryPath()

```php:app/Console/Commands/CreateServiceCommand.php@getServiceDirectoryPath()
    /**
     * Get the full directory path for service classes.
     *
     * @return string
     */
    private function getServiceDirectoryPath(): string
    {
        return base_path(self::SERVICES_PATH);
    }
```

`getServiceDirectoryPath()`は以下の通りです。

- サービスディレクトリのパスを返却する

### getServiceClassTemplate()

```php:app/Console/Commands/CreateServiceCommand.php@getServiceClassTemplate()
    /**
     * Get the content of the service class.
     *
     * @return string
     */
    private function getServiceClassTemplate(): string
    {
        return "<?php\n\nnamespace App\\Services\\{$this->className};\n\nclass {$this->className}\n{\n    // Implement your service methods here\n}\n";
    }
```

`getServiceClassTemplate()`は以下の通りです。

- サービスファイルに記述するサービスファイルのテンプレートを返却する

### writeFile()

```php:app/Console/Commands/CreateServiceCommand.php@writeFile()
    /**
     * Write content to a file, handling existence and errors.
     *
     * @param string $filePath
     * @param string $fileContent
     */
    private function writeFile(string $filePath, string $fileContent): void
    {
        if (File::exists($filePath)) {
            $this->warn("File {$filePath} already exists.");
        } else {
            try {
                File::put($filePath, $fileContent);
                $this->info("Service {$this->className} created successfully.");
            } catch (\Exception $e) {
                $this->error("Failed to create file: {$filePath}. Error: " . $e->getMessage());
            }
        }
    }
```

`writeFile()`は以下の通りです。

- サービスファイルが既に存在していた場合はwarnを出力する
- それ以外の場合はファイルを作成する

## 実行してみる


<details><summary>完成したコード</summary>

```php:app/Console/Commands/CreateServiceCommand
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\File;

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

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create a new service class';

    /**
     * Directory path for services.
     *
     * @const string
     */
    private const SERVICES_PATH = 'app/Services/';

    /**
     * The class name of the service to create.
     *
     * @var string
     */
    private string $className;

    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        $this->className = Str::studly($this->argument('className'));

        if ($this->className === '') {
            $this->error('The service class name is invalid. Please provide a valid class name.');
            return;
        }

        $this->createServiceClass();
    }

    /**
     * Create the service class file.
     */
    private function createServiceClass(): void
    {
        $directoryPath = $this->getServiceDirectoryPath();

        // Create the directory if it doesn't exist
        if (!File::exists($directoryPath)) {
            File::makeDirectory($directoryPath, 0755, true);
        }

        $filePath = $this->getServiceFilePath();
        $fileContent = $this->getServiceClassTemplate();

        $this->writeFile($filePath, $fileContent);
    }

    /**
     * Get the full file path of the service class.
     *
     * @return string
     */
    private function getServiceFilePath(): string
    {
        return $this->getServiceDirectoryPath() . $this->className . '.php';
    }

    /**
     * Get the full directory path for service classes.
     *
     * @return string
     */
    private function getServiceDirectoryPath(): string
    {
        return base_path(self::SERVICES_PATH);
    }

    /**
     * Get the content of the service class.
     *
     * @return string
     */
    private function getServiceClassTemplate(): string
    {
        return "<?php\n\nnamespace App\\Services\\{$this->className};\n\nclass {$this->className}\n{\n    // Implement your service methods here\n}\n";
    }

    /**
     * Write content to a file, handling existence and errors.
     *
     * @param string $filePath
     * @param string $fileContent
     */
    private function writeFile(string $filePath, string $fileContent): void
    {
        if (File::exists($filePath)) {
            $this->warn("File {$filePath} already exists.");
        } else {
            try {
                File::put($filePath, $fileContent);
                $this->info("Service {$this->className} created successfully.");
            } catch (\Exception $e) {
                $this->error("Failed to create file: {$filePath}. Error: " . $e->getMessage());
            }
        }
    }
}
```

</details>

上記のコードができましたでしょうか？できましたら以下コマンドを実行してみてください。

```bash:bash
php artisan list
```

```bash:出力内容
~中略~
  make:seeder               Create a new seeder class
  make:service              Create a new service class
  make:session-table        [session:table] Create a migration for the session database table
~中略~
```

上記のようになっていればコマンドが作られています。コマンドを実行してみましょう。

```bash:bash
php artisan make:service SampleService
```

```bash:出力内容
Service SampleService created successfully.
```

上記のように出力されたら正常終了してますので確認してみてください。

```php:app/Services/SampleService.php
<?php

namespace App\Services\SampleService;

class SampleService
{
    // Implement your service methods here
}

```

## さいごに

今回はServiceクラスを作成するコマンドを実装しました。もっと言えばServiceInterfaceを一緒に作成するコマンドを作れば効率的かと思いますが「Interfaceをどこに配置するか」などが人によって異なると思いますので一旦Serviceクラスを作成するコマンドにしました（Serviceクラスの配置場所についての議論もちらほら見たことがありますがInterfaceよりかは少ないかなぁと思います）。

Repositoryクラスに関してはパスなどを変えてあげればすぐに作成できるのでそちらの記事を書くことはないと思います。