Laravel 7 コマンドの実行結果に実行時間、最大使用メモリ、処理件数を出力する

どのコマンドがいつ実行されて、どのくらい実行時間がかかったのか。
実行時の処理件数や最大使用メモリを知りたかったので、試しにやってみました。

## 環境

- PHP: 7.4.x
- Laravel: 7.x

## 作成するファイルについて

- app/Console/Command.php
- app/Console/CommandResult.php
- app/Console/CommandTimer.php
- app/Console/Commands/HelloWorldCommand.php

上記の4つのファイルを作成します。

## app/Console/Command.php

[Illuminate\Console\Command](https://laravel.com/api/7.x/Illuminate/Console/Command.html) クラスを継承して `App\Console\Command` クラスを作成します。

やってることはコマンド実行前にコマンドの計測を開始して、コマンド実行後に計測結果を出力するようにしてます。

```php:app/Console/Command.php
<?php declare(strict_types=1);

namespace App\Console;

use Exception;
use Illuminate\Console\Command as BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

abstract class Command extends BaseCommand
{
    protected CommandTimer $timer;
    protected int $statusCode = 0;

    public function __construct()
    {
        parent::__construct();
        $this->timer = new CommandTimer();
    }

    /**
     * Execute the console command.
     *
     * @param  InputInterface  $input
     * @param  OutputInterface  $output
     * @return mixed
     * @throws
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $this->beforeHandle();

        try {
            $result = $this->laravel->call([$this, 'handle']);

            if ($result instanceof CommandResult) {
                $addMessage = $result->getMessage();
                $this->statusCode = $result->getStatusCode();
            }

            $this->afterHandle($addMessage ?? '');
        } catch (Exception $e) {
            $this->errorHandle();
            throw $e;
        }

        return $this->statusCode;
    }

    /**
     * コマンド実行前の処理
     * @throws Exception
     */
    protected function beforeHandle(): void
    {
        $this->timer->start();
        $message = sprintf('[Start] Execute command %s is started', $this->name);
        $this->comment($message);
        info($message);
    }

    /**
     * コマンド実行後の処理
     * @param string $addMessage
     * @throws Exception
     */
    protected function afterHandle(string $addMessage): void
    {
        $this->timer->stop();
        $message = sprintf('[Successful] Execute command %s is finished, %s seconds, max memory: %s MB.', $this->name, $this->timer->getTotalSeconds(), $this->getMaxMemory());
        $this->comment($message . ' ' . $addMessage);
        info($message . ' ' . $addMessage);
    }

    /**
     * コマンド実行エラーの処理
     * @throws Exception
     */
    protected function errorHandle(): void
    {
        $this->timer->stop();
        $message = sprintf('[Failed] Execute command %s is finished, %s seconds, max memory: %s MB.', $this->name, $this->timer->getTotalSeconds(), $this->getMaxMemory());
        $this->error($message);
        logger()->critical($message);
    }

    /**
     * コマンド実行時に割り当てられたメモリの最大値をMB単位で返す
     * @return float
     */
    private function getMaxMemory(): float
    {
        return memory_get_peak_usage(true) / (1024 * 1024);
    }
}
```

## app/Console/CommandTimer.php

コマンドの実行時間を計測するクラスを作成します。

```php:app/Console/CommandTimer.php
<?php declare(strict_types=1);

namespace App\Console;

use Carbon\Carbon;
use DateTime;
use Exception;

/**
 * コマンドの実行時間を計測
 */
class CommandTimer
{
    private const DATE_FORMAT = 'Y-m-d H:i:s.u';
    private Carbon $startedTime;
    private Carbon $stoppedTime;

    /**
     * @return DateTime
     * @throws Exception
     */
    public function start(): DateTime
    {
        return $this->startedTime = new Carbon();
    }

    /**
     * @return DateTime
     * @throws Exception
     */
    public function stop(): DateTime
    {
        return $this->stoppedTime = new Carbon();
    }

    /**
     * @return float
     * @throws Exception
     */
    public function getTotalSeconds(): float
    {
        if (empty($this->startedTime)) {
            throw new Exception('The timer has not started.');
        }

        if (empty($this->stoppedTime)) {
            throw new Exception('The timer has not stopped');
        }

        return $this->startedTime->floatDiffInSeconds($this->stoppedTime);
    }

    /**
     * @return string
     */
    public function getStartedTime(): string
    {
        return $this->startedTime->format(self::DATE_FORMAT);
    }

    /**
     * @return string
     */
    public function getStoppedTime(): string
    {
        return $this->stoppedTime->format(self::DATE_FORMAT);
    }
}
```

## app/Console/CommandResult.php

```php:app/Console/CommandResult.php
<?php declare(strict_types=1);

namespace App\Console;

/**
 * コマンドの実行結果
 */
class CommandResult
{
    private int $statusCode = 0;
    private int $successCount = 0;
    private int $failedCount = 0;

    /**
     * @param int $statusCode
     */
    public function setStatusCode(int $statusCode): void
    {
        $this->statusCode = $statusCode;
    }

    /**
     * @return int
     */
    public function getStatusCode(): int
    {
        return $this->statusCode;
    }

    public function addSuccess(): void
    {
        $this->successCount++;
    }

    public function addFail(): void
    {
        $this->failedCount++;
    }

    /**
     * @param int $count
     */
    public function setSuccess(int $count): void
    {
        $this->successCount = $count;
    }

    /**
     * @param int $count
     */
    public function setFail(int $count): void
    {
        $this->failedCount = $count;
    }

    /**
     * @return int
     */
    public function getSuccessCount(): int
    {
        return $this->successCount;
    }

    /**
     * @return int
     */
    public function getFailedCount(): int
    {
        return $this->failedCount;
    }

    /**
     * @return int
     */
    public function getTotalCount(): int
    {
        return $this->successCount + $this->failedCount;
    }

    /**
     * @return string
     */
    public function getMessage(): string
    {
        return sprintf('Total: %d Success: %d Failed: %d', $this->getTotalCount(), $this->getSuccessCount(), $this->getFailedCount());
    }
}
```

## 試しにコマンドを作成する

### app/Console/Commands/HelloWorldCommand.php

```php:app/Console/Commands/HelloWorldCommand.php
<?php declare(strict_types=1);

namespace App\Console\Commands;

use App\Console\Command;
use App\Console\CommandResult;

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 を出力するコマンド';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $commandResult = new CommandResult();

        $this->comment('Hello World');
        $commandResult->addSuccess();

        return $commandResult;
    }
}
```

## コマンドを実行

```shell-session
$ php artisan hello:world
[Start] Execute command hello:world is started
Hello World
[Successful] Execute command hello:world is finished, 0.05698 seconds, max memory: 20 MB. Total: 1 Success: 1 Failed: 0
```

このようにコマンドの実行結果が出力されます。

- 実行時間: 0.05698 秒
- 最大使用メモリ: 20 MB
- 実行件数: 1件
- 成功件数: 1件
- 失敗件数: 0件

ログファイルも確認してみます。

```text:storage/logs/laravel.log
[2020-06-16 09:30:00] local.INFO: [Start] Execute command hello:world is started  
[2020-06-16 09:30:00] local.INFO: [Successful] Execute command hello:world is finished, 0.05698 seconds, max memory: 20 MB. Total: 1 Success: 1 Failed: 0  
```

ログにも同様の内容が出力されます。

## 追記: スタブの差し替え

`stubs/console.stub` ファイルを差し替えておくと、
`php artisan make:command` した時に変更したテンプレートを使用できます。

```php:stubs/console.stub
<?php declare(strict_types=1);

namespace {{ namespace }};

use App\Console\Command;

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

    /**
     * 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()
    {
        //
    }
}
```