Laravel Octaneの開発時のハマりポイントと回避方法について

本記事は、[サムザップ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/sumzap)の12/12の記事です。

# はじめに
Laravel Octane（Swoole版）でアプリケーションをつくるときにはまったポイントを紹介していきます

ハマりポイントをすぐ知りたい方は、ローカルの開発環境構築はすっとばしてください（サンプルアプリケーションの構築から読んでください）

# ローカルの開発環境構築
基本は[公式ドキュメント](https://laravel.com/docs/8.x/octane)にしたがって構築していきます
今回は、[Laravel Sail](https://laravel.com/docs/8.x/sail)を使う方法を選択しています

### Laravel アプリケーションの作成
まずはLaravelのアプリケーションを作成します

```shell-session
$ composer create-project laravel/laravel sample
```

## Laravel Octaneのインストール
つづいてLaravel Octaneのインストール

```shell-session
$ cd sample
$ composer require laravel/octane
```

## Laravel Sailのインストール
つづいてLaravel Sailのインストール

```shell-session
$ composer require laravel/sail --dev
$ php artisan sail:install
```

使用するミドルウェアの選択肢がでてきます
今回は、MySQLだけを使用するので、0を選択します

```shell-session
 Which services would you like to install? [mysql]:
  [0] mysql
  [1] pgsql
  [2] mariadb
  [3] redis
  [4] memcached
  [5] meilisearch
  [6] minio
  [7] mailhog
  [8] selenium
>0
```

ドキュメントに従い一度、buildします

```shell-session
$ ./vendor/bin/sail build --no-cache
```

supervisor.confファイルを調整するためにPublishします

```shell-session
$ ./vendor/bin/sail up
$ ./vendor/bin/sail artisan sail:publish
```
※ コンテナ名を変更している場合は、環境変数 ```APP_SERVICE``` に変更したコンテナ名を渡してください
変更しないと、 ```sail:publish``` で使用するコンテナ名の解決に失敗するため

カレントディレクトリ配下に ```docker``` ディレクトリが作成されます

sailがつかう ```docker-compose.yml``` にて 

```docker-compose.yml
context: ./docker/8.0
```

になっていることを確認します（PHP8.0を使います）

supervisord.confを編集して、Swooleを使うようにします

```shell-session
$ vi docker/8.0/supervisord.conf
```

commandの行を書き換えます

変更前

```supervisord.conf(変更前)
command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80
user=sail
```

変更後

```supervisord.conf（変更後)
command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=swoole --host=0.0.0.0 --port=8000
```

```serve``` を ```octane:start --server=swoole``` に変更しています

もう一度コンテナイメージを作り直します

```shell-session
$ ./vendor/bin/sail build --no-cache
```

Laravel OctaneのSwooleモードでは、Port 8000番をListenするように変更したので、それにあわせて、 ```docker-compose.yml``` も修正します

```docker-compose.yml
        ports:
            - '${APP_PORT:-80}:80'
            - 8000:8000
```

8000番を追加します

## Laravel Octaneの起動
Laravel OctaneをSwooleモードで起動します

```shell-session
$ ./vendor/bin/sail up
```

起動時のメッセージでLarave Octaneが8000番をListenしているのが確認できます

```console
sample-laravel.sample-1  | 2021-11-20 09:28:47,887 INFO Set uid to user 0 succeeded
sample-laravel.sample-1  | 2021-11-20 09:28:47,891 INFO supervisord started with pid 16
sample-laravel.sample-1  | 2021-11-20 09:28:48,897 INFO spawned: 'php' with pid 17
sample-laravel.sample-1  | 2021-11-20 09:28:49,899 INFO success: php entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
sample-laravel.sample-1  | 
sample-laravel.sample-1  |    INFO  Server running…
sample-laravel.sample-1  | 
sample-laravel.sample-1  |   Local: http://0.0.0.0:8000 
sample-laravel.sample-1  | 
sample-laravel.sample-1  |   Press Ctrl+C to stop the server
sample-laravel.sample-1  | 
```

最後にoctaneのconfigファイルを生成します

```shell-session
$ ./vendor/bin/sail artisan octane:install
```

```
 Which application server you would like to use?:
  [0] roadrunner
  [1] swoole
 >1
```

今回はSwooleなので1を選択します
これにより、```config/octane.php``` が生成されます


# サンプルアプリケーションの構築

次のディレクトリ構成でファイルを作成します

```
Sample
├── app
│   ├── Console
│   ├── Exceptions
│   ├── Http
│   │   ├── Controllers
│   │   │   ├── Controller.php
│   │   │   └── SampleController.php 　　新規作成
│   │   ├── Kernel.php
│   │   └── Middleware
│   ├── Models
│   ├── Providers
│   └── Service
│       └── RandomNumberService.php　新規作成
├── artisan
├── bootstrap
├── composer.json
├── config
├── database
├── docker-compose.yml
├── package.json
├── phpunit.xml
├── public
├── resources
├── routes
│   ├── api.php
│   ├── channels.php
│   ├── console.php
│   └── web.php   変更
├── server.php
├── storage
├── tests
└── vendor
```

それぞれのファイルは次のように記述しています

(1)コントローラー

```SampleController.php
<?php

namespace App\Http\Controllers;

use App\Service\RandomNumberService;
use Illuminate\Http\Response;

class SampleController extends Controller
{
    public function __construct(
        private RandomNumberService $service
    ){}

    public function index(): Response
    {
        return response('Number:' . $this->service->getNumber(), Response::HTTP_OK);
    }
}
```

(2)サービス

```RandomNumberService.php
<?php
namespace App\Service;

class RandomNumberService
{
    protected $number;

    public function __construct()
    {
        $this->number = mt_rand(1, 10000);
    }

    public function getNumber() :int
    {
        return $this->number;
    }    
}
```

(3)ルーティング

```web.php
<?php

use Illuminate\Support\Facades\Route;

Route::get('/sample', 'App\Http\Controllers\SampleController@index');
```

## 動作確認
実際に 「 http://127.0.0.1:8000/sample 」にブラウザでアクセスしてみると
次のように表示されます（初回表示）

![ブラウザでの表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67626/dea5eca9-36b7-d362-ebfa-df1eca0259df.png)

# ハマりポイント
ここでブラウザをリロードしてみましょう

プログラムの挙動として、乱数で生成された数字を表示しようとするため、前回と違う数字が表示されることが期待されます

が、実際には、次のように表示されます（２回目以降の表示）

![ブラウザでの表示2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67626/dea5eca9-36b7-d362-ebfa-df1eca0259df.png)
### **かわっていない！**

## Laravel Octaneの挙動
通常のPHPはリクエストごとに（リクエストが終わるごとに）状態が破棄されますが、Laravel Octaneは、自身がWebサーバーとなり常時PHPのプログラムがうごいたままになっています
つまり、サンプルアプリケーションの記述の仕方では、コントローラーのインスタンスの生成が１回しかされず、そのまま使いまわされます（２回目以降のリクエストは、生成済みのコントローラーのインスタンスがレスポンスを返している）

はまりました・・・

# 解決案（１）
## Workerに注目する
実は、Laravel OctaneのSwoole版には起動オプションがいくつかあります
その一つが ```max-requests``` のオプションです
Laravel Octaneは厳密に言うとWorkerがリクエストの処理を担います
1つのWorkerが何回もリクエストを捌くことになるのですが、Worker自身をリロードする契機としてこの ```max-reqeust``` があります
ここに指定した回数のリクエストを処理すると、Workerはリロードします

### max-requests=1 にする
では、起動オプションで ```max-requests``` を1にしてみましょう

supervisord.confを編集します

```supervisord.conf
command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=swoole --host=0.0.0.0 --port=8000 --max-requests=1
```
（```sail build``` でコンテナイメージは作り直します）
では実際の挙動はどうなるでしょうか？

ブラウザでアクセスしてみます

![Worker1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67626/e6a40139-2abf-303c-7041-d412c68b517b.png)

かわった！
**けど、おそい!**
```max-request``` を１に変更する前は、１回目のリクエスト以外は、50ms以内にレスポンスがかえってきました
しかし、```max-request``` を１に変更したあとのレスポンスは、概ね1000msです
(手元のPCでの計測です)
これは、Laravel Octaneをつかっている意味がなさそうです

# 解決案（2）
## Closureに注目
Laravelのルーターは、第二引数にClosureをとることができます
これを利用します

つまり、ルーターを次のようにします

```web.php(追記)
Route::get('/sample', 'App\Http\Controllers\SampleController@index');
Route::get('/sample2', fn () => app()->make(\App\Http\Controllers\SampleController::class)->index());
```

では、実際に ```sample2``` にアクセスしてみましょう
(画像は省略します)

リロードするたびに値が変わるのが確認できます！
しかもレスポンスも、1回目のリクエスト以外は、50ms以内にレスポンスがかえってきます

# 解決案（３）（未解決）
## Octaneの作法
上記以外にも、Laravel Octaneで用意している仕組みがあります

```config/octane.php``` に次のセクションがあります

```octane.php
    /*
    |--------------------------------------------------------------------------
    | Octane Listeners
    |--------------------------------------------------------------------------
    |
    | All of the event listeners for Octane's events are defined below. These
    | listeners are responsible for resetting your application's state for
    | the next request. You may even add your own listeners to the list.
    |
    */

    'listeners' => [
        WorkerStarting::class => [
            EnsureUploadedFilesAreValid::class,
            EnsureUploadedFilesCanBeMoved::class,
        ],

        RequestReceived::class => [
            ...Octane::prepareApplicationForNextOperation(),
            ...Octane::prepareApplicationForNextRequest(),
            //
        ],
```

```RequestReceived::class``` にて、リクエストを受け付けた時に処理をはしらせるhookの記述があります
ここのhookがつかえそうです

が、コントローラーをうまく初期化する方法まで調べることができませんでした
もしご存知の方がいれば、情報下さい！

# まとめ
Laravel Octaneでアプリケーションを作った際に、最初に、コントローラーのコンストラクタが１回しか呼ばれない現象になやまされました
よくよく考えれば、わかることなのですが、その回避策をいくつか検討してみました
現時点では、Closureを使う方法で回避することができました

しかし、よりOctaneらしい記述があれば、そちらを踏襲していければと思います

明日は@norimatsu_yusukeの記事です！