本記事は、サムザップ Advent Calendar 2021の12/12の記事です。
はじめに
Laravel Octane(Swoole版)でアプリケーションをつくるときにはまったポイントを紹介していきます
ハマりポイントをすぐ知りたい方は、ローカルの開発環境構築はすっとばしてください(サンプルアプリケーションの構築から読んでください)
ローカルの開発環境構築
基本は公式ドキュメントにしたがって構築していきます
今回は、Laravel Sailを使う方法を選択しています
Laravel アプリケーションの作成
まずはLaravelのアプリケーションを作成します
$ composer create-project laravel/laravel sample
Laravel Octaneのインストール
つづいてLaravel Octaneのインストール
$ cd sample
$ composer require laravel/octane
Laravel Sailのインストール
つづいてLaravel Sailのインストール
$ composer require laravel/sail --dev
$ php artisan sail:install
使用するミドルウェアの選択肢がでてきます
今回は、MySQLだけを使用するので、0を選択します
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します
$ ./vendor/bin/sail build --no-cache
supervisor.confファイルを調整するためにPublishします
$ ./vendor/bin/sail up
$ ./vendor/bin/sail artisan sail:publish
※ コンテナ名を変更している場合は、環境変数 APP_SERVICE
に変更したコンテナ名を渡してください
変更しないと、 sail:publish
で使用するコンテナ名の解決に失敗するため
カレントディレクトリ配下に docker
ディレクトリが作成されます
sailがつかう docker-compose.yml
にて
context: ./docker/8.0
になっていることを確認します(PHP8.0を使います)
supervisord.confを編集して、Swooleを使うようにします
$ vi docker/8.0/supervisord.conf
commandの行を書き換えます
変更前
command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80
user=sail
変更後
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
に変更しています
もう一度コンテナイメージを作り直します
$ ./vendor/bin/sail build --no-cache
Laravel OctaneのSwooleモードでは、Port 8000番をListenするように変更したので、それにあわせて、 docker-compose.yml
も修正します
ports:
- '${APP_PORT:-80}:80'
- 8000:8000
8000番を追加します
Laravel Octaneの起動
Laravel OctaneをSwooleモードで起動します
$ ./vendor/bin/sail up
起動時のメッセージでLarave Octaneが8000番をListenしているのが確認できます
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ファイルを生成します
$ ./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)コントローラー
<?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)サービス
<?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)ルーティング
<?php
use Illuminate\Support\Facades\Route;
Route::get('/sample', 'App\Http\Controllers\SampleController@index');
動作確認
実際に 「 http://127.0.0.1:8000/sample 」にブラウザでアクセスしてみると
次のように表示されます(初回表示)
ハマりポイント
ここでブラウザをリロードしてみましょう
プログラムの挙動として、乱数で生成された数字を表示しようとするため、前回と違う数字が表示されることが期待されます
が、実際には、次のように表示されます(2回目以降の表示)
かわっていない!
Laravel Octaneの挙動
通常のPHPはリクエストごとに(リクエストが終わるごとに)状態が破棄されますが、Laravel Octaneは、自身がWebサーバーとなり常時PHPのプログラムがうごいたままになっています
つまり、サンプルアプリケーションの記述の仕方では、コントローラーのインスタンスの生成が1回しかされず、そのまま使いまわされます(2回目以降のリクエストは、生成済みのコントローラーのインスタンスがレスポンスを返している)
はまりました・・・
解決案(1)
Workerに注目する
実は、Laravel OctaneのSwoole版には起動オプションがいくつかあります
その一つが max-requests
のオプションです
Laravel Octaneは厳密に言うとWorkerがリクエストの処理を担います
1つのWorkerが何回もリクエストを捌くことになるのですが、Worker自身をリロードする契機としてこの max-reqeust
があります
ここに指定した回数のリクエストを処理すると、Workerはリロードします
max-requests=1 にする
では、起動オプションで max-requests
を1にしてみましょう
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
でコンテナイメージは作り直します)
では実際の挙動はどうなるでしょうか?
ブラウザでアクセスしてみます
かわった!
けど、おそい!
max-request
を1に変更する前は、1回目のリクエスト以外は、50ms以内にレスポンスがかえってきました
しかし、max-request
を1に変更したあとのレスポンスは、概ね1000msです
(手元のPCでの計測です)
これは、Laravel Octaneをつかっている意味がなさそうです
解決案(2)
Closureに注目
Laravelのルーターは、第二引数にClosureをとることができます
これを利用します
つまり、ルーターを次のようにします
Route::get('/sample', 'App\Http\Controllers\SampleController@index');
Route::get('/sample2', fn () => app()->make(\App\Http\Controllers\SampleController::class)->index());
では、実際に sample2
にアクセスしてみましょう
(画像は省略します)
リロードするたびに値が変わるのが確認できます!
しかもレスポンスも、1回目のリクエスト以外は、50ms以内にレスポンスがかえってきます
解決案(3)(未解決)
Octaneの作法
上記以外にも、Laravel Octaneで用意している仕組みがあります
config/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でアプリケーションを作った際に、最初に、コントローラーのコンストラクタが1回しか呼ばれない現象になやまされました
よくよく考えれば、わかることなのですが、その回避策をいくつか検討してみました
現時点では、Closureを使う方法で回避することができました
しかし、よりOctaneらしい記述があれば、そちらを踏襲していければと思います
明日は@norimatsu_yusukeの記事です!