弁護士ドットコム株式会社 Creators’ blog

弁護士ドットコムがエンジニア・デザイナーのサービス開発事例やデザイン活動を発信する公式ブログです。

Bref の PHP function ランタイムによる PHP サーバーレス

Bref の PHP function ランタイムによる PHP サーバーレス

この記事は弁護士ドットコム Advent Calendar 2025の 14 日目の記事です。

はじめに

リーガルブレイン基盤チームのtsuchiyaです。

イベント駆動の処理を PHP で実装するにあたり、BrefPHP functions runtime を採用しました。 本記事では、Bref の function ランタイムの基本的な使い方に加え、DI コンテナとの統合や Terraform + lambroll によるデプロイ構成を紹介します。

Bref とは

Bref は Composer で配布されている OSS のパッケージで、主に AWS Lambda 上で PHP を実行するための実行環境を提供しています。

Bref では以下の 3 つの実行環境を提供しています。

  • ウェブアプリケーション向けの PHP-FPM ランタイム
  • イベント駆動アプリケーション向けの function ランタイム
  • CLI コマンド向けの console ランタイム

上記に加え、Laravel や Symfony のためのインテグレーションを提供しており、フレームワークユーザーも簡単にアプリケーションを Lambda にデプロイできます。

function ランタイム

function ランタイムは、AWS サービスからのイベントやデータを処理する際に用いるランタイムで、SQS による非同期タスクの例S3 のファイル処理の例が紹介されています。

Bref の function runtime にある例を以下に引用します。

<?php

require __DIR__ . '/vendor/autoload.php';

return function ($event) {
    return 'Hello ' . ($event['name'] ?? 'world');
};

Dependency Injection コンテナとの統合

引用したコードは単純な例です。実際のアプリケーションにおいては、いくつかのクラスを組み合わせて実装したいニーズがあります。 PSR-11 の ContainerInterface を実装したクラスを利用することで、依存解決済みのクラスをハンドラーとして使えます。

以下のような、Bref::setContainer()PSR-11 ContainerInterface を返す callable を渡す PHP ファイルを作成します。

<?php // init.php

use Bref\Bref;
use App;

Bref::setContainer(static function () {
    return ContainerFactory::newInstance(); // PSR-11 を実装したクラスのインスタンスを生成し return する。
});

composer.json の autoload.files に、上記のファイルを設定します。 以下は、Bref の DI 統合の例 から引用した composer.json の設定例です。

{
    "autoload": {
        "psr-4": {
            // ...
        },
        "files": [
            "init.php"
        ]
    },
}

ここまでで、任意のクラスをハンドラーとして指定して実行できるようになります。

Lambda のハンドラーとして、完全修飾名で対象となるクラスを指定します。 以下は、Bref の DI 統合の例 から引用した、serverless.yml の設定例です。 ここでは、MyApp\Handler という完全修飾名のクラスが指定されています。

functions:
    hello:
        handler: MyApp\Handler

型付きイベントハンドラ

Bref は汎用的な Handler インタフェースに加えて、AWS サービスごとの型付きハンドラを提供しています。これらを使うことでイベントのパース処理が不要になり、型安全にイベントを扱えます。 例えば SQS イベントを処理する場合は SqsHandler を継承します。

<?php

namespace MyApp;

use Bref\Context\Context;
use Bref\Event\Sqs\SqsEvent;
use Bref\Event\Sqs\SqsHandler;
use MyApp\Service\MessageProcessor;

class MySqsHandler extends SqsHandler
{
    public function __construct(
        private MessageProcessor $processor
    ) {}

    public function handleSqs(SqsEvent $event, Context $context): void
    {
        foreach ($event->getRecords() as $record) {
            $body = $record->getBody();
            $this->processor->process($body);
        }
    }
}

SqsEventSqsRecord といった型付きオブジェクトを通じてメッセージにアクセスでき、生の配列を扱う必要がありません。 同様に S3 イベント用の S3Handler や EventBridge 用の EventBridgeHandler なども用意されています。

実際の構成

ここからは、実際に使用している構成を紹介します。

インフラ管理とデプロイ

インフラは Terraform で管理し、関数のデプロイには lambroll を使用しています。

Terraform では Lambda 関数のリソースを作成しつつ、lambroll の function.json で管理するパラメータは ignore_changes で無視します。

resource "aws_lambda_function" "example_function" {
    lifecycle {
        ignore_changes = [
            architectures,
            environment,
            ephemeral_storage,
            handler,
            layers,
            logging_config[0].log_format,
            memory_size,
            runtime,
            snap_start,
            timeout,
            tracing_config,
            filename,
            source_code_hash,
            vpc_config,
        ]
    }

    function_name = "${var.prefix}-example-function"
    role          = aws_iam_role.example-function.arn
    handler       = "dummy"
    runtime       = "provided.al2"
    filename      = data.archive_file.dummy.output_path
}

lambroll の function.json では Terraform の output を参照します。

{
  "FunctionName": "{{ tfstate `output.lambda_example_function_name` }}",
  "Role": "{{ tfstate `output.lambda_example_function_role_arn` }}",
  "Runtime": "provided.al2",
  "Handler": "MyApp\\Handler",
  "MemorySize": 256,
  "Timeout": 5
}

この構成により、インフラの変更は Terraform で、関数コードや実行パラメータの調整は lambroll で、それぞれ独立してデプロイできます。

Ray.Di による依存解決

DI には Ray.Di を利用しています。 モジュールという単位で束縛ルールを管理・インストール可能で、弊社では複数のサービスで利用しています。

CompiledInjector

Ray.Di の通常の Injector は、実行時に依存グラフを解析しますが、Lambda 環境ではコールドスタートのオーバーヘッドを抑えたいところです。 CompiledInjector を使うことで、ビルド時に依存解決のコードを事前生成できます。 ビルド時にコード生成を済ませておくことで、実行時はリフレクションによる解析が不要になり、高速に起動できます。

以下は、コードを事前生成するためのスクリプトの例です。

<?php

use Ray\Compiler\Compiler;
use MyApp\AppModule;

$appDir = dirname(__DIR__);

require $appDir . '/vendor/autoload.php';

$module = new AppModule();
$compiler = new Compiler();

try {
    $compiler->compile($module, $appDir . '/var/tmp');
} catch (Throwable $e) {
    echo $e . PHP_EOL;

    exit(1);
}

echo 'Compiled successfully.' . PHP_EOL;
exit(0);

Bref::setContainer を呼び出すファイルは、以下のようになります。

<?php

declare(strict_types=1);

use Bref\Bref;
use MyApp\PsrContainer;
use Ray\Compiler\CompiledInjector;

Bref::setContainer(static function () {
    $tmpPath = __DIR__ . '/var/tmp'; // コンパイル済みのスクリプトのパスを指定
    $injector = new CompiledInjector($tmpPath);

    return new PsrContainer($injector); // PSR-11 のラッパーでラップする
});

まとめ

Bref の function ランタイムと PSR-11 コンテナの統合と実際に使用している Terraform + lambroll によるデプロイ構成や Ray.Di の活用例を紹介しました。 イベント駆動の処理を PHP で実装する際の参考になれば幸いです。