37
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Laravelコーディングよくやる集2022

Last updated at Posted at 2022-12-23

この記事は「【マイスター・ギルド】本物のAdvent Calendar 2022」18日目の記事です。

LaravelでWebアプリケーションを開発しているんですが、これは毎回書きそうだなーというのがいくつかあったので、それをまとめてみました。開発はまだまだ途中なので、これから変化していく可能性もありますが。

グローバルスコープ

グローバルスコ―プを使ってDBのデータ + 何らかの条件付きを表現します。それを使えば疑似的1に条件付きのモデルが定義できます。

例えばUserモデルがあり、利用者が閲覧可能かどうかをis_visibleで表したデータがあるとき、

class VisibleUser extends User
{
    protected $table = 'users';

    protected $hidden = [
        'is_visible',  // 持つ必要が無い
    ];

    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(function (Builder $builder) {
            // 公開中のもののみという条件を付与
            $builder->where('is_visible', true);
        });
    }
}

とする事で「利用者が閲覧可能なデータのみに絞られたVisibleUser」を定義できます。

利用者に閲覧可能なユーザーの情報のみが表示できる/public/users/{user}というルーティングがあった時に、モデルバインディングで

Route::get('/public/users/{user}', function (VisibleUser $user) {
    return $user;
});

としてしまえば、is_visible=falaseなUserを表示しようとしても404エラーに出来ます。

カスタムクエリビルダ

ユースケースから呼び出す検索条件は、Illuminate\Database\Eloquent\Builderを拡張したカスタムクエリビルダに書くようにします。

use Illuminate\Database\Eloquent\Builder;

class UserQueryBuilder extends Builder
{
    public function nameIs(string $val): static
    {
        return $this->where('name', '=', $val);
    }
}

というカスタムクエリビルダを定義し、モデルで利用するクエリに差し替えます。

class User extends Model
{
    ...

    public static function query(): UserQueryBuilder
    {
        return parent::query();
    }

    public function newEloquentBuilder($query): UserQueryBuilder
    {
        return new UserQueryBuilder($query);
    }
}

こうするとユースケースで

User::query()->nameIs('山田')->orderBy('id');

と呼ぶことが出来ます。

トランザクション

コントローラーでトランザクション制御を行います。

public function create(CreateRequest $request): JsonResponse
{
    $input = $request->validated();
    DB::transaction(function () use ($input) {
        $this->usecase->create($input);
    });
    return response()->json();
}

DB::transactionでユースケースの処理を囲っておき、成功時はコミット、失敗(エラー発生)時はロールバックを自動でされるようにしておきます。

要トランザクション トランザクション不要
登録、更新、削除 取得

処理の種類とトランザクションの要・不要は上の通りで、ユースケースに「要」の処理が含まれるならトランザクションで囲うようにします。

エラーログ

Log::error(...);

で Laravel のログ出力先に吐き出します。実際にエラーが起きた際のログは、基本的には何もしなくてもLaravelが勝手に出してくれるんですが、try-catchしてエラーを投げなおさない場合には、

try{
    ...
} catch (Exception $e) {
    Log::error($e);
    ...
}

として手動でエラーを出力する必要が出てきます。

メッセージ

多言語対応・メッセージ管理

画面へ表示されるメッセージを書く場合、多言語対応を見据えて__ヘルパを使っておきます。
https://readouble.com/laravel/9.x/ja/localization.html#retrieving-translation-strings

__('登録に成功しました。')

__ヘルパを使っておくとlang/ディレクトリ下で

'登録に成功しました。' => 'registration successful',

を定義すれば、configの言語設定に従って別言語化されます。

あるいは__ヘルパには定数を渡すようにして、lang/ja/下で定数と表示するメッセージの対応を書くようにすれば、langフォルダ内に表示メッセージを集めて管理ができます。

__('message.welcome')

(定数を.で区切ることで階層化が可能です)

lang/ja/message.php
`welcome` => 'ようこそ'

画面へのフラッシュ表示(1度だけ表示)

laracast/flashを利用します。
成功メッセージは

Flash::success(...);

で出力し、

エラーメッセージは

Flash::error(...);

で出力します。表示させる側の blade では

@include('flash::message')

を読ませるだけです。成功メッセージに対してはalert-success、失敗メッセージにはalert-dangerのクラスが付いた DOM が生成されるので、文字色を変えるなどのスタイルを当てます。

通知

Notification を使います。

イベント発火から通知までの流れは以下のようにします。

イベント発火

ユースケースでイベントを発火します(Registeredがイベントです)。この時リスナー処理で必要になる情報をイベントに渡しておきます(DB 登録したモデルとか)。

event(new Registered($user));

イベントとリスナーの対応付け

Providers/EventServiceProviderに定義しておいたイベントとリスナーの対応づけから、リスナー(SendRegisteredEmail)が呼び出されます。

protected $listen = [
    Registered::class => [
        SendRegisteredEmail::class,
    ],
];

リスナー処理

リスナーでイベント発火時に渡したモデル(DB 登録したモデル)を参照し、そのモデルに定義されている通知用のメソッド(sendRegisteredNotification)を呼び出します。リスナ―にはShouldQueueインターフェースを使うようにし、後々非同期処理されるようにします。

class SendRegisteredEmail implements ShouldQueue
{
    public function handle(Registered $event)
    {
        $event->user->sendRegisteredNotification();
    }
}

イベント通知

通知用のメソッド内にてnotifyメソッドを呼び、引数に通知クラス(ここのRegisteredは通知クラス)を渡します。このモデルにはNotifiableトレイトが使用されている事が前提です。

class User extends Model
{
    use Notifiable;

    public function sendRegisteredNotification()
    {
        $this->notify(new Registered());
    }
}

通知処理

通知クラスでは、通知処理(送信する情報の構築)を書きます。

class Registered extends Notification
{
    ...

    public function via($notifiable)
    {
        return ['mail'];
    }

    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject(__('登録が完了しました'))
            ->markdown('mails.registered');
    }
}

上記はメール通知処理ですが、SMS通知やSlack通知などもここに実装出来ます。

テスト

テスト用DB

  • phpunit の設定で、testingという開発とは別の DB を使用する設定にします。
  • RefreshDatabaseトレイトを使い、DB はテストケース毎にリフレッシュされるようにします。
  • あらかじめデータが入った DB が必要な場合は、テストコードの前半で必要なデータを作成します。

テストケース

@dataProviderアノテーションを使い、TDT(テーブル駆動テスト)っぽくなるようにします。

    /**
     * @dataProvider registDataProvider
     */
    public function test_regist($input, $seed, $expected, $expectedException)
    {
        ...
    }

    public function registDataProvider()
    {
        return [
            '正常系' => [
                [
                    'name' => 'Test User',
                    'email' => 'test@m-gild.com',
                    'password' => 'password',
                ], // 引数の$inputにあたります
                RegistTestSeeder::class, // 引数の$seedにあたります
                'test@m-gild.com', // 引数の$expectedにあたります
                null, // 引数の$expectedExceptionにあたります
            ],
            '異常系' => [
                [], // 引数の$inputにあたります
                RegistTestSeeder::class, // 引数の$seedにあたります
                '', // 引数の$expectedにあたります
                Exception::class, // 引数の$expectedExceptionにあたります
            ]
        ];
    }

上記のようにすると、正常系異常系がテストケースとなり、test_regist が2度回ります。その際に test_regist に渡される引数は、配列として書いた順に渡されます。

モック

Usecase をテストしたい場合、それに依存している Model や Service はモック化し、テストに影響を与えないようにします。
UT のsetUp関数でテスト対象の初期化処理を書くときにモックを渡し、準備をします。

    protected function setUp(): void
    {
        parent::setUp();
        $this->mock = Mockery::mock('App\Models\User'); // モック化したいクラスのパスを渡します
        // UserUsecaseがテスト対象です
        $this->usecase = new UserUsecase(
            $this->mock,
        );
    }

テストコードの前半でモックのメソッドをダミーなもので定義します。定義していないメソッドが呼び出されるとエラーになります。

Userモデルのcreateというメソッドが使われる Usecase をテストする場合は、

        $receive = $this->mock->shouldReceive('create')
            ->once() // 一度だけシミュレーションするという宣言
            ->with($input); // 引数
        if (is_null($mockException)) {
            // エラーを期待しない場合
            $receive->andReturn($mockUser); // 応答をダミーに置き換えます
        } else {
            // エラーを期待する場合
            $receive->andThrow($mockException); // 異常時のエラーの発生をダミーに置き換えます
        }

        $this->usecase->regist($input);  // テスト対象。この中でUser::createが呼ばれています

となります。

  1. グローバルスコープは DB から取得時のSQLに'is_visible' = trueを付与するだけ。DB から取得したデータのis_visibletrueに上書きすることは可能なので、モデルが常にis_visible=trueであることを保証するわけではないことに注意。

37
37
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?