ixbox / phambda
Requires
- php: ^8.2
- php-http/discovery: ^1.17
- psr/container: ^2.0
- psr/http-client: ^1.0
- psr/http-client-implementation: ^1.0
- psr/http-factory: ^1.0
- psr/http-factory-implementation: ^1.0
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/log: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95.28
- laravel/pint: ^1.32.1
- mockery/mockery: ^1.6.15
- nyholm/psr7: ^1.8.2
- pestphp/pest: ^4.7.8 || ^5.3.5
- php-http/curl-client: ^2.4
- phpstan/phpstan: ^2.3.5
- rector/rector: ^2.8.4
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
概要
Phambda は、PHP で AWS Lambda を簡単に利用するためのランタイムを提供します。このパッケージを使用することで、AWS Lambda 上で PHP コードを実行するための設定や実装が簡素化されます。
主な特徴
- PSR-7 および PSR-15 に準拠したリクエスト/レスポンス処理
- 標準的な PHP インターフェースを使用することで、既存の PHP アプリケーションとの互換性を確保
- ミドルウェアパターンによる柔軟な処理の追加が可能
- AWS Lambda 環境に最適化されたランタイム
- コールドスタートの最小化
- メモリ使用の効率化
- エラーハンドリングの強化
- static-php-cli 対応
- コンテナ化不要: 静的ビルドされた PHP バイナリを使用して直接デプロイ
- 軽量なデプロイパッケージで高速なコールドスタートを実現
- Docker 環境が不要なため、CI/CD パイプラインが簡素化
- シンプルなインターフェースでの実装
- 最小限のコードで Lambda 関数を作成可能
- 豊富なログ機能により、デバッグとモニタリングが容易
ランタイム方式の比較
AWS Lambda で PHP を動かす主な方法と、Phambda との違いです。
| 方法 | 仕組み | 扱えるイベント | 向いている用途 |
|---|---|---|---|
| Phambda(本ライブラリ) | カスタムランタイム(provided.al2023)上の PHP が Runtime API と直接やり取りする。HTTP のイベントは PSR-7 のリクエストに変換し、PSR-15 のハンドラに渡す |
HTTP(API Gateway、関数 URL、ALB)と HTTP 以外(SQS など) | Web サーバーを置かずに、PSR-15 のハンドラやイベント処理を軽い構成で動かしたい |
| Bref | PHP のランタイムを Layer で提供する。PHP-FPM ランタイムを使えば、既存のアプリをそのまま動かせる | HTTP と HTTP 以外 | Laravel や Symfony のアプリを、デプロイ用のツールも含めて揃った環境で運用したい |
| Lambda Web Adapter(LWA) | Lambda の拡張機能がイベントを HTTP リクエストに変換し、関数の中で動く Web サーバー(nginx + PHP-FPM、FrankenPHP など)に転送する | HTTP(API Gateway、関数 URL、ALB)。HTTP 以外のイベントは、指定したパスへの POST として届く | 既存の Web アプリを、Lambda 向けに書き換えずに動かしたい |
| Lambda Web Functions | 2026 年 10 月に追加された関数の形態。アプリが listen するポートに、Lambda が HTTP リクエストを直接渡す。1 つの実行環境が複数のリクエストを同時に処理し、HTTPS のエンドポイントも Lambda が用意する | HTTP | 同時リクエストの多い Web アプリや API。ただし公式に対応しているランタイムは、現時点で Node.js だけ |
FrankenPHP や nginx + PHP-FPM のような Web サーバーは、Runtime API を扱わないので、それだけでは Lambda で動きません。LWA と組み合わせて動かします。 LWA の README でも、Node.js 以外のランタイムでは、Web Functions が対応するまで LWA を使い続けるよう案内されています。
PHP は Web Functions に公式には対応していません。
Node.js ランタイムから FrankenPHP に execve で置き換えて動かす PoC(ixbox/lambda-web-function-php)がありますが、ランタイム内部の非公開 API に依存しているため、ランタイムの更新で動かなくなる可能性があります。
Phambda は Lambda Event Functions(Web Functions の登場で、従来の Lambda 関数はこう呼ばれるようになりました)として動きます。 HTTP 以外のイベントも同じ仕組みで扱えることと、Web サーバーやアダプターを挟まず、PHP の CLI とアプリのコードだけで動くことが特徴です。
インストール方法
以下のコマンドを実行してインストールします:
composer require ixbox/phambda
必要な環境
- PHP 8.2 以上
- Composer
- AWS Lambda の実行権限を持つ IAM ロール
- 基本的な Lambda 実行権限
- CloudWatch Logs へのアクセス権限
AWS Lambda へのデプロイ
Phambda は Lambda のカスタムランタイム(provided.al2023)で動きます。static-php-cli の静的ビルドを使えば、コンテナイメージを作らずに zip でデプロイできます。
関数に必要なものは次の 3 つです。
- PHP バイナリ: static-php-cli の静的ビルド(例:
https://dl.static-php.dev/static-php-cli/common/php-8.5.8-cli-linux-aarch64.tar.gz)。関数のアーキテクチャ(arm64 ならaarch64、x86_64 ならx86_64)に合わせます。Layer に置くと、関数のコードと分けて更新できます。 - アプリケーション: エントリーポイントの PHP ファイルと
vendor/。composer install --no-dev --optimize-autoloaderで作ります。Phambda は PSR-7 / PSR-17 / PSR-18 の実装を同梱しないので、使う実装(例:nyholm/psr7とphp-http/curl-client)をアプリケーションのrequireに追加してください。使う実装に必要な拡張(php-http/curl-clientならcurl)がバイナリに含まれていることも確認してください。アプリケーションで php-http/discovery の Composer プラグインを許可している場合(composer config allow-plugins.php-http/discovery true)は、実装が入っていなければ自動でインストールされます。 bootstrap: Lambda が起動する実行可能ファイルです(chmod +xが必要)。
PHP バイナリを Layer に置いた場合の構成は次のとおりです。Layer は /opt、関数のコードは /var/task に展開されます。
/opt ← Layer
└── bin/php
/var/task ← 関数
├── bootstrap
├── main.php
└── vendor/
#!/bin/sh exec /opt/bin/php /var/task/main.php
関数の設定:
- ランタイム:
provided.al2023 - ハンドラー: カスタムランタイムでは使われないので、任意の値(例:
bootstrap) - 環境変数:
AWS_LAMBDA_RUNTIME_APIなどは Lambda が設定します。必要に応じてLOG_TIMEZONE(ログのタイムゾーン、デフォルト: UTC)を設定してください
使用方法
基本的な Hello World の例
以下は、AWS Lambda 上で動作する簡単な Hello World の例です:
#!/usr/bin/env php <?php require __DIR__ . '/vendor/autoload.php'; use Nyholm\Psr7\Response; use Phambda\Http\Runtime; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; $handler = new class implements RequestHandlerInterface { public function handle(ServerRequestInterface $request): ResponseInterface { return new Response(200, [], "Hello World!"); } }; $runtime = new Runtime($handler); $runtime->run();
ロギング機能を使用した例
以下は、ロギング機能を活用した実装例です:
#!/usr/bin/env php <?php require __DIR__ . '/vendor/autoload.php'; use Nyholm\Psr7\Response; use Phambda\Context; use Phambda\Http\Runtime; use Phambda\Logger\JsonLogger; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerInterface; class MyHandler implements RequestHandlerInterface { public function __construct( private readonly LoggerInterface $logger ) {} public function handle(ServerRequestInterface $request): ResponseInterface { // リクエストIDの取得 $requestId = $request->getAttribute('awsRequestId'); /** @var \Phambda\Context $context */ $context = $request->getAttribute('lambda-context'); $this->logger->info('リクエスト処理開始', [ 'request_id' => $requestId, 'function' => [ 'name' => $context->functionName, 'version' => $context->functionVersion, 'memory' => $context->memoryLimitInMb ] ]); // 処理ロジック $data = [ 'message' => 'Hello from Phambda!', 'timestamp' => time(), 'request_id' => $requestId, ]; $this->logger->info('レスポンス送信', ['data' => $data]); return new Response( status: 200, headers: [ 'Content-Type' => 'application/json', 'X-Request-ID' => $requestId ], body: json_encode($data) ); } } // ロガーの作成(JSON 形式で標準エラー出力に書き、CloudWatch Logs に送られる。info 以上を出力) $logger = new JsonLogger('info'); // ハンドラーの作成とランタイムの実行(ランタイム自身のログも同じロガーに出す) $handler = new MyHandler($logger); $runtime = new Runtime($handler, $logger); $runtime->run();
ミドルウェアでヘッダーを付ける例
Http\Runtime は RequestHandlerInterface を 1 つ受け取るだけなので、PSR-15 のミドルウェアを重ねたハンドラーをそのまま渡せます。
すべての応答に付けたいヘッダーは、ハンドラーごとに書かずにミドルウェアで付けてください。
MiddlewareInterface を使うには、アプリケーションで psr/http-server-middleware を追加します(relay/relay などのディスパッチャーを使ってもかまいません)。
use Phambda\Http\Runtime; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; final class SecurityHeaders implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { return $handler->handle($request) ->withHeader('X-Content-Type-Options', 'nosniff') ->withHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains') ->withHeader('Cache-Control', 'no-store'); } } // ミドルウェアとハンドラーを、1 つのハンドラーにまとめる final class MiddlewareHandler implements RequestHandlerInterface { public function __construct( private readonly MiddlewareInterface $middleware, private readonly RequestHandlerInterface $next, ) { } public function handle(ServerRequestInterface $request): ResponseInterface { return $this->middleware->process($request, $this->next); } } Runtime::execute(new MiddlewareHandler(new SecurityHeaders(), new MyHandler()));
ハンドラーが例外を投げたときに Phambda が返す 500 応答は、ミドルウェアを通らないので、これらのヘッダーは付きません。 500 応答にも付けたい場合は、ミドルウェアの中で例外を受け止めて応答を返すか、CloudFront の応答ヘッダーポリシーで付けてください。
エラーハンドリングの例
public function handle(ServerRequestInterface $request): ResponseInterface { try { // 処理ロジック $result = $this->processRequest($request); return new Response( status: 200, headers: ['Content-Type' => 'application/json'], body: json_encode(['data' => $result]) ); } catch (\InvalidArgumentException $e) { $this->logger->warning('不正なリクエスト', [ 'error' => $e->getMessage(), 'request_id' => $request->getAttribute('awsRequestId') ]); return new Response( status: 400, headers: ['Content-Type' => 'application/json'], body: json_encode([ 'error' => 'Bad Request', 'message' => $e->getMessage() ]) ); } catch (\Exception $e) { $this->logger->error('内部エラー', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'request_id' => $request->getAttribute('awsRequestId') ]); return new Response( status: 500, headers: ['Content-Type' => 'application/json'], body: json_encode([ 'error' => 'Internal Server Error', 'request_id' => $request->getAttribute('awsRequestId') ]) ); } }
SQS、EventBridge、S3 のイベントを扱う
HTTP 以外のイベントは、Phambda\Runtime に Phambda\HandlerInterface を実装したハンドラーを渡して処理します。
SQS、EventBridge、S3 には、イベントを型付きで受け取れるハンドラーを用意しています。
use Phambda\Context; use Phambda\Runtime; use Phambda\Sqs\SqsHandler; use Phambda\Sqs\SqsRecord; $handler = new class (reportBatchItemFailures: true) extends SqsHandler { protected function handleRecord(SqsRecord $record, Context $context): void { $order = $record->json(); // 本文を JSON としてデコードする // 例外を投げると、そのメッセージが失敗になる } }; Runtime::execute($handler);
SqsHandler は、バッチのメッセージを 1 件ずつ handleRecord() に渡します。
reportBatchItemFailures: trueを指定すると、失敗したメッセージだけを再配信させる部分的なバッチ応答を返します。イベントソースマッピングでReportBatchItemFailuresを有効にしているときだけ指定してください。有効にしていないと、Lambda は応答を無視してバッチ全体を成功とみなし、失敗したメッセージも削除します。- 指定しない場合は、1 件でも失敗するとバッチ全体が失敗になり、すべてのメッセージが再配信されます。
- FIFO キューで部分的なバッチ応答を使う場合は、順序を保つため、最初に失敗したメッセージ以降を処理せずに失敗として返します。
EventBridge のイベント(スケジュール実行を含む)は、EventBridgeHandler で受け取れます。
use Phambda\EventBridge\EventBridgeEvent; use Phambda\EventBridge\EventBridgeHandler; $handler = new class extends EventBridgeHandler { protected function handleEvent(EventBridgeEvent $event, Context $context): void { // $event->detailType, $event->source, $event->detail, $event->time など } };
S3 のイベント通知は、S3Handler で受け取れます。通知に入っているオブジェクトキーは URL エンコードされている(red flower.jpg は red+flower.jpg になる)ので、$record->key ではデコードしたキーを返します。通知を設定したときに S3 が送るテストメッセージ(s3:TestEvent)は無視します。
use Phambda\S3\S3Handler; use Phambda\S3\S3Record; $handler = new class extends S3Handler { protected function handleRecord(S3Record $record, Context $context): void { // $record->bucketName, $record->key, $record->size, $record->isObjectCreated() など } };
S3 の通知を SQS 経由で受け取る場合は、SqsHandler の中で S3Event::fromJson() にメッセージの本文を渡します。
protected function handleRecord(SqsRecord $record, Context $context): void { foreach (S3Event::fromJson($record->body)->records as $s3) { // テストメッセージの場合、records は空になる } }
設定オプション
Phambda は以下の設定オプションをサポートしています:
環境変数
| 環境変数 | 説明 | デフォルト値 |
|---|---|---|
AWS_LAMBDA_RUNTIME_API |
Lambda ランタイム API のエンドポイント | 127.0.0.1:9001 |
AWS_LAMBDA_FUNCTION_NAME |
Lambda 関数名 | - |
AWS_LAMBDA_FUNCTION_VERSION |
Lambda 関数のバージョン | - |
AWS_LAMBDA_FUNCTION_MEMORY_SIZE |
Lambda 関数のメモリサイズ | - |
AWS_LAMBDA_LOG_GROUP_NAME |
CloudWatch ログのグループ名 | - |
AWS_LAMBDA_LOG_STREAM_NAME |
CloudWatch ログのストリーム名 | - |
LOG_TIMEZONE |
ログのタイムゾーン | UTC |
PHAMBDA_DEBUG |
1 のとき、HTTP のハンドラーで例外が出た際の 500 応答に、例外メッセージとスタックトレースを含める。指定しなければ {"message":"Internal Server Error"} だけを返し、詳細はログに出す |
- |
ランタイム設定
Runtime のコンストラクタが受け取るのは、ハンドラーとロガー、それにランタイム API とやり取りする Worker です。
Runtime::execute($handler, $logger) は、new Runtime($handler, $logger) で作って run() するのと同じです。
PSR-7 / PSR-17 / PSR-18 の実装は、何も指定しなければ php-http/discovery がインストール済みのものから選びます。 使う実装を明示したい場合は、Worker を自分で組み立てて渡します。
use Http\Client\Curl\Client; use Nyholm\Psr7\Factory\Psr17Factory; use Phambda\Factory\HttpWorkerFactory; use Phambda\Factory\WorkerFactory; use Phambda\Http\Runtime; $psr17 = new Psr17Factory(); $client = new Client($psr17, $psr17); $worker = HttpWorkerFactory::create( logger: $logger, worker: WorkerFactory::create($logger, $client, $psr17, $psr17), serverRequestFactory: $psr17, streamFactory: $psr17, responseFactory: $psr17, ); $runtime = new Runtime($handler, $logger, $worker); $runtime->run();
AWS Lambda 特有の考慮事項
初期化を 1 回だけにする
Phambda は、実行環境ごとに PHP のプロセスを 1 つだけ起動し、そのプロセスで呼び出しを順番に処理します。 そのため、ハンドラーのプロパティに入れたものは、次の呼び出しでも使えます。
初期化処理は、ハンドラーに Phambda\BootableInterface を実装し、boot() に書いてください。
Runtime と Http\Runtime は、最初の呼び出しを受け取る前(Lambda の初期化フェーズ)に boot() を 1 回だけ呼びます。
use Phambda\BootableInterface; $handler = new class implements RequestHandlerInterface, BootableInterface { private Database $db; public function boot(): void { $this->db = new Database(); // ほかの初期化処理もここにまとめる } public function handle(ServerRequestInterface $request): ResponseInterface { // ハンドラーロジック } };
boot() が例外を投げると、Phambda はその内容を Lambda に初期化エラーとして通知し、プロセスを終了します。次の呼び出しでは、実行環境が作り直されます。
Runtime::execute() より前(ハンドラーのコンストラクタの引数など)で初期化すると、失敗しても Phambda はエラーを通知できません。
DB の一時的な障害などで失敗したときに、実行環境を作り直さず同じプロセスのままやり直したい場合は、最初の呼び出しのときに handle() の中で初期化します。
private ?bool $initialized = null; public function handle(ServerRequestInterface $request): ResponseInterface { // 失敗した場合は、その呼び出しのエラーになり、次の呼び出しでやり直す $this->initialized ??= $this->initialize(); // ハンドラーロジック }
どちらの場合も、static プロパティやシングルトンに入れたリクエストごとのデータは、次の呼び出しに残ります。呼び出しの合間に DB の接続が切れていることもあるので、再接続できるようにしておいてください。
タイムアウト設定
タイムアウトは Lambda 関数の設定で決まります。デフォルトは 3 秒、最大は 900 秒です。Phambda はタイムアウトを独自に設定しません。 API Gateway を前に置く場合は、統合のタイムアウト(HTTP API は最大 30 秒、REST API はデフォルト 29 秒)にも注意してください。関数のタイムアウトを長くしても、先に API Gateway がエラーを返します。
呼び出しの期限は $context->deadlineMs(UNIX 時間のミリ秒)で取れます。HTTP のハンドラーでは、$request->getAttribute('lambda-context') で Context を取り出せます。
長い処理は、複数の関数に分けるか、Step Functions などを使うことを検討してください。
ALB で同じ名前のヘッダーを複数返す
ALB のターゲットグループで複数値ヘッダーを有効にしていない場合、ALB は同じ名前のヘッダーを 1 つしか受け付けません。
このとき Phambda は最後の値だけを返し、落とした値があることを warning のログに記録します。
複数の Set-Cookie を返したい場合などは、ターゲットグループの属性で複数値ヘッダー(lambda.multi_value_headers.enabled)を有効にしてください。
デバッグとトラブルシューティング
カスタムランタイムで起きやすいエラー
| エラー | 考えられる原因 | 確認すること |
|---|---|---|
Runtime.InvalidEntrypoint |
bootstrap が無い、または実行権限が無い |
bootstrap が zip の直下にあり、chmod +x されているか確認 |
Runtime.ExitError |
PHP のプロセスが終了した(PHP バイナリのパス違い、致命的エラーなど) | bootstrap のパスと CloudWatch Logs を確認 |
Phambda がどのエラーを Lambda にどう通知するかは、docs/error-handling.md にまとめています。
ライセンス
このプロジェクトは MIT ライセンスの下で提供されています。
コントリビューション
コントリビューションは歓迎します!バグ報告や機能提案は、GitHub の Issue を通じて行ってください。プルリクエストを送信する前に、以下のことを確認してください:
- テストが追加されていること
- すべてのテストが通ること(
composer test) - 静的解析が通ること(
composer analyze) - コードスタイルが守られていること(
vendor/bin/pint --test)