Laravel5.7で非同期処理がしたい

この記事は[ニフティグループ Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nifty)の11日目の記事です。

以降では、Laravel(5.7)で非同期処理を実装してHorizonで動かしてみる、ということをしてみます。
（Laravelが動く環境はすでにあることを想定しています。[Laradoc](https://laradock.io)などで作成しておいてください。）

# 目次
1. Laravel(5.7)で非同期処理を実装する
    1. 非同期に実行したい処理を実装する
    2. コントローラーから呼び出す
    3. 試しに同期的に実行してみる
1. Redisキューを使って動かしてみる
1. Laravel Horizonで動かしてみる
1. さいごに

## 1. Laravel(5.7)で非同期処理を実装する

LaravelにはQueueサービスというものがあり、Beanstalkd、Amazon SQS、Redisなどをキューとして用いた非同期処理が簡単に実装できます。

詳しくは[公式のドキュメント](https://laravel.com/docs/5.7/queues#class-structure)を御覧ください。

### 1.1 非同期に実行したい処理を実装する

以下のコマンドを実行すると、`app/Jobs`配下に`ProcessSample`というクラスが生成されます。このクラスに非同期処理（ジョブ）を実装していきます。

```
php artisan make:job ProcessSample
```

作成したクラスの `handle` メソッドが、実際にジョブが実行されるときに呼び出される関数です。
試しに、適当なテキストを受け取って、実行時にログを出力するジョブを作ってみました。

```php:app/Jobs/ProcessSample.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class ProcessSample implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    private $text;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($text)
    {
        $this->text = $text;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        logger()->info("It's work! | ".$this->text);
    }
}
```

### 1.2 コントローラーから呼び出す

`dispatch` という関数でジョブをキューに追加します。
引数は `ProcessSample` のコンストラクタに渡されます。

```php:routes/web.php
use App\Jobs\ProcessSample;

Route::get('/test/{text}', function ($text) {
    ProcessSample::dispatch($text);
    return 'Queued!';
});
```

## 1.3 試しに同期的に実行してみる

`.env`ファイルの`QUEUE_CONNECTION`が実際にどのキューを使うかの設定です。
Amazon SQSやRedisなどを指定することができます。

ここではまず動くか確かめたいので、同期的に実行するよう`sync`と設定します。

```shell:.env
QUEUE_CONNECTION=sync
```

さきほど`/test/{text}`というURLを設定したので`/test/sampletext`にアクセスしてみます。

```:storage/logs/laravel-2018-12-11.log
[2018-12-11 **:**:**] local.INFO: It's work! | sampletext
```

このようなログが出力されていたら成功です🎉

# 2. Redisキューを使って動かしてみる

## Redisを準備する

Redis本体は各自ご用意ください。
LaradocであればRedisは含まれているので以下で起動できます。

```
docker-compose up -d redis
```

## パッケージの追加と設定

LaravelでRedisをキューとして使うにはPredisが必要なので、プロジェクトに追加します。

```
composer require predis/predis
```

Redisをキューとして使うように設定します。

```shell:.env
QUEUE_CONNECTION=redis

# 以下は環境に合わせて設定してください
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
```

Redisの設定は以上です！

## 動かしてみる

次は実際にキューを監視させます。

```
php artisan queue:work
```

この状態で`/test/sampletext2`にアクセスしてみます。

```:storage/logs/laravel-2018-12-11.log
[2018-12-11 **:**:**] local.INFO: It's work! | sampletext2
```

このようなログが出力されていたら成功です🎉
非同期処理の実装、完璧です✨

# 3. Laravel Horizonで動かしてみる

HorizonはLaravelのRedisキューの監視ツールです。
非同期処理をする分には上記までの手順で問題ないのですが、Horizonにはダッシュボードがあり、処理のスループットや失敗などを確認することができて楽しいので使ってみます。

詳しくは[公式のドキュメント](https://laravel.com/docs/5.7/horizon)を御覧ください。

<img width="1422" alt="Screen Shot 2018-12-11 at 2.41.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/190918/45189cf1-2994-878d-d05c-6b281aab68b3.png">

## パッケージの追加

Horizonをプロジェクトに追加します。

```
composer require laravel/horizon
```

Horizonはpcntlが必須です。ないと以下のようなエラーが表示されます。

```bash
$ composer require laravel/horizon
Using version ^2.0 for laravel/horizon
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - laravel/horizon v2.0.0 requires ext-pcntl * -> the requested PHP extension pcntl is missing from your system.
    - laravel/horizon 2.0.x-dev requires ext-pcntl * -> the requested PHP extension pcntl is missing from your system.
    - Installation request for laravel/horizon ^2.0 -> satisfiable by laravel/horizon[2.0.x-dev, v2.0.0].


Installation failed, reverting ./composer.json to its original content.
```

Dockerを使っている方はDockerファイルに以下を追加してからビルド＆再起動で動く可能性があります。
どうにかしてpcntlをインストールしてください。

```:Dockerfile
(略)

RUN docker-php-ext-install pcntl
```

```
docker-compose up -d --build
```

## セットアップ

Horizonをインストールします。

```
php artisan horizon:install
```

失敗したキューのためのテーブルを作成します。

```
php artisan queue:failed-table

php artisan migrate
```

Horizonの準備は以上です！

## 動かしてみる

Horizonを起動します。

```
php artisan horizon
```

`/horizon` にアクセスすると、ダッシュボードが表示されるはずです。

この状態で`/test/sampletext3`にアクセスしてみます。

```:storage/logs/laravel-2018-12-11.log
[2018-12-11 **:**:**] local.INFO: It's work! | sampletext3
```

ログが出力されていたら成功です。
おつかれさまでした👏

あとはもっとカスタマイズしたり、`exit(1)`で異常終了させたりして遊べます。
失敗したジョブ一覧から失敗したジョブをもう一度動かしてみることも可能です。

<img width="1422" alt="Screen Shot 2018-12-11 at 2.47.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/190918/e5cbe2be-1309-44e0-86e0-0d9c8b55667f.png">

# 4. さいごに

非同期処理はLaravelがきちんとお世話してくれるので、簡単に実装できることがわかりました。
あとはSupervisorなどを用いてよしなにやればきちんと動くはずです。

動かしてみるだけでしたが、Horizonについてもご紹介しました。
Horizonにはその他認証、待ち状態が長いときの通知など機能がもりもりなので、ぜひ[公式のドキュメント](https://laravel.com/docs/5.7/horizon)をチェックしてみてください。