Help us understand the problem. What is going on with this article?

Laravelでウェブアプリケーションをつくるときのベストプラクティスを探る (8) コマンド編

More than 3 years have passed since last update.

はじめに

このエントリーについて

この記事は「Laravelでウェブアプリケーションをつくるときのベストプラクティスを探る」シリーズの一編です。
他の記事は目次からアクセスしてください。

今回は php artisan {command} という形で実行できるカスタムコマンドを追加するときのベストプラクティスです。

環境

  • PHP 5.6
  • Laravel 5.3

公式リファレンス

Console Commands - Laravel - The PHP Framework For Web Artisans

詳細

よくあるシナリオ

  1. コンソールから手動実行されるコマンド
  2. cron から自動実行されるコマンド

スケジューラーについては今回はパスしますが、2 のケースは特別な事情がない限りスケジューラーを使うのが Laravel 流みたいです。

共通のガイドライン

  • コマンド名の命名規則は {app:名詞:動詞} にしましょう
  • Command クラスの仕事は極力減らしましょう
  • 実際の処理を行うオブジェクトは DI しましょう

手動実行されるコマンドのガイドライン

人間がコマンドを打つので、help で表示されるコメントは充実させておきましょう

  • 引数には説明をつけましょう
  • 成功の場合も失敗の場合も標準出力にメッセージを出しましょう

自動実行されるコマンドのガイドライン

  • 失敗の場合にログにメッセージを出力しましょう

シナリオ 1 のサンプルコード

1ユーザーに対してメールを送るコマンドです。

アプリケーション固有のコマンドには app と先頭につけておくと、どんなコマンドがあるか php artisan list app で一覧表示できるので便利です。

引数があるときは、 $signature に {パラメータ名 : 説明} のようにコロンで区切ってプレースホルダーを書くと引数についての説明を付記できます。

SendEmail.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Mailer;
use App\User;

class SendEmail extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:email:send {user : User ID}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Send an email to a user';

    /**
    * @var App\Mailer
    */
    protected $mailer;

    /**
     * Create a new command instance.
     *
     * @param App\Mailer $mailer
     * @return void
     */
    public function __construct(Mailer $mailer)
    {
        parent::__construct();

        $this->mailer = $mailer;
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $userId = $this->argument('user');
        try {
            $user = User::findOrFail($userId);
            $this->mailer->send($user);
        } catch (ModelNotFoundException $e) {
            $this->error('Error: No user with ID(' . $userId . ') found - Exception: ' . $e->getMessage());
            return;
        } catch (Exception $e) {
            $this->error('Error:' . $e->getMessage());
            return;
        }

        $this->info('Success: Email sent to ' . $user->email);
    }
}

App\Mailer をコンストラクタインジェクションして、実際の処理はこのクラスに担当させます。基本的には、何らかのビジネスロジックはモデルやサービス側に閉じ込めて、Command クラスの責務は

  • 入力パラメーターの処理
  • 実行結果の処理

の2点に限定した方がいいでしょう。

ちなみに、php artisan help app:email:send と打つとコマンドの詳細が確認できます。

» php artisan help app:email:send
Usage:
  app:email:send <user>

Arguments:
  user                  User ID

Options:
(略)

Help:
  Send an email to a user

シナリオ 2 のサンプルコード

複数のユーザーに対してメールを送るコマンドで、一日に一回 cron から実行されます。

SendEmails.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Mailer;
use App\User;

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

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Send an email to subscribers';

    /**
    * @var App\Mailer
    */
    protected $mailer;

    /**
     * Create a new command instance.
     *
     * @param App\Mailer $mailer
     * @return void
     */
    public function __construct(Mailer $mailer)
    {
        parent::__construct();

        $this->mailer = $mailer;
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $subscribers = User::newsletterSubscribers()->get();
        $nSent = 0;
        foreach ($subscribers as $user) {
            try {
                $this->mailer->sendNewsletter($user);
                ++$nSent;
            } catch (\Exception $e) {
                $this->error('Error: failed to send to user with ID(' . $user->id . ') - Exception: ' . $e->getMessage());
            }
        }

        $this->info('Success: Email sent to ' . $nSent . ' of ' . $subscribers->count() . ' subscribers.');
    }
}

ポイントは、バッチの場合何らかのコレクションに対してループを回しますが、ループブロック内の行数は極力抑えるようにした方が、モジュラリティが高まるので、処理が長くなるようなら別クラスか別メソッドに移しましょう、ということと、途中で例外が起きても処理を続行する必要があるケースがあるので、例外は必ず捕捉して、メッセージを出しましょう、ということです (後述しますが、Command の出力は Kernel クラスにてログに出すようにしましょう)。

出力については、バッチの場合処理対象の件数が多いとログが大きくなってしまうので、成功の場合のメッセージはなくていいと思います。

ちなみに、スケジューラー (Kernel クラスの schedule メソッド) の記述は以下のようになります。

app/Console/Kernel.php
<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        Commands\SendEmails::class,
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('app:email:send')
                 ->dailyAt('20:00')->appendOutputTo(storage_path('logs/email_send.log'));
    }

    /**
     * Register the Closure based commands for the application.
     *
     * @return void
     */
    protected function commands()
    {
        require base_path('routes/console.php');
    }
}

appendOutputTo はログ保存ポリシーによっては sendOutputTo でもいいでしょうし、emailOutputTo と組み合せるとよりいいんじゃないでしょうか。

詳しくは公式リファレンスを。
Task Scheduling - Laravel - The PHP Framework For Web Artisans

まとめ

  • コマンド名の命名規則は {app:名詞:動詞} にしましょう
  • Command クラスの仕事は極力減らしましょう
  • 実際の処理を行うオブジェクトは DI しましょう
  • 手動実行されるコマンドは、引数には説明をつけましょう
  • 手動実行されるコマンドは、成功の場合も失敗の場合も標準出力にメッセージを出しましょう
  • 自動実行されるコマンドは、失敗の場合にログにメッセージを出力しましょう

他にもこんなベストプラクティスがあるよ、などコメントや編集リクエストいただけると助かります :bow:

nunulk
PHP, Laravel, オブジェクト指向プログラミング, デザインパターン, リファクタリング, 関数プログラミング, etc.
http://nunulk.hatenablog.com
phper-oop
ペチオブはオブジェクト指向ワーキンググループです。様々なエンジニアの方に参加頂いております。
https://phper-oop.connpass.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away