Relayer
Docsルーティングnative-routes

ネイティブルート(Go)#

route.go は Go で書く API ルートです。URL のマッピングは route.php と同じで、ハンドラだけがネイティブバイナリにコンパイルされます。標準ライブラリの http.HandlerFunc の形で func GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS を宣言するだけで、 main() もフレームワークの import も要りません。CPU 負荷の高いエンドポイント向けです。

// src/Pages/api/hello/route.go
package main

import (
    "encoding/json"
    "net/http"
)

func GET(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"message": "Hello from Go!"})
}
vendor/bin/relayer native:build     # route.go を変更するたびに 1 回
curl -i http://localhost:8080/api/hello

ビルド#

relayer native:build は src/Pages 配下の全 route.go とルート直下の middleware.go を var/cache/native/bin/<slug> にコンパイルします。<slug> は URL パターンの / を __ に置き換えたものです(/api/users/[id] → api__users__[id])。

  • PATH 上の go(または GO_BIN=/path/to/go)が必要で、そのため relayer compile とは別コマンドです。
  • route.go の隣に go.mod / go.sum があればそれを使うので、サードパーティのモジュールも使えます。コピーされるのは route.go と go.mod / go.sum だけなので、隣の helper.go はコンパイルされません。共有コードはモジュールとして import します。
  • 同じ slug になる 2 つのルート、ハンドラのない route.go、package main でない route.go はビルドエラーです。
  • リクエスト中に再ビルドはしません。未ビルドのルートは native:build を促す JSON 500 を返し、 dev ではバイナリが route.go より古いときも同じ応答になります。

雛形の Dockerfile には Go がなく、native:build も実行しません。そのままの本番イメージでは route.go がすべて未ビルドの 500 になるので、Go を入れたビルドステージで native:build を実行し、var/cache/native/bin を最終イメージへコピーしてください。

リクエストの流れ#

常駐する Go サーバはありません。PHP がリクエストごとにバイナリを CGI の子プロセスとして起動します(proc_open)。1 呼び出しのオーバーヘッドは数ミリ秒です。ハンドラからは次のものが見えます。

  • r.Method、r.URL、r.Header、r.Host、r.RemoteAddr、r.Body。クライアントの Proxy: ヘッダは転送しません。r.RemoteAddr のポートは 0 です。
  • 動的セグメントは RELAYER_ROUTE_PARAMS に JSON で、パターンは RELAYER_ROUTE_PATTERN に入ります。
  var params map[string]string
  json.Unmarshal([]byte(os.Getenv("RELAYER_ROUTE_PARAMS")), &params)
  id := params["id"]
  • PHP プロセスの実際の環境変数も見えますが、.env の値は 含まれません。Go で必要な設定はシェルやコンテナで実際の環境変数として渡してください。
  • multipart/form-data の本文は PHP が解析した内容から組み立て直すので、r.FormFile が使えます。 PHP が拒否したアップロードは JSON 500 になります。

メソッドと失敗#

メソッドの扱いは route.php と同じです。未宣言の OPTIONS は 204 + Allow、GET があって HEAD が未宣言なら GET を実行して本文を落とし、それ以外の未宣言メソッドは 405 + Allow です。

非 0 終了(panic を含む)、CGI 応答でない出力、タイムアウト超過は JSON 500 になります。stderr は PSR-3 ロガーに記録されます(成功した実行の stderr は warning)。タイムアウトの既定は 30 秒で、 NATIVE_ROUTE_TIMEOUT で変更できます。disable_functions で proc_open を無効にした PHP では動きません。

効かないもの#

route.go にはオートワイヤも PageContext も Request も #[Auth] / requireAuth() もありません。保護はルートミドルウェアで行います。_private フォルダは route.go も除外し、 1 ディレクトリにハンドラは 1 つです(route.go と route.php やページの同居はスキャンエラー)。

middleware.go#

ルートミドルウェアも Go で書けます。src/Pages/middleware.go に func Proxy を 1 つ書くと、 native:build で var/cache/native/bin/middleware にビルドされ、ルートマッチの前にリクエストごとに起動します。

// src/Pages/middleware.go
package main

import "net/http"

func Proxy(w http.ResponseWriter, r *http.Request) {
    // 短絡: Relayer-Next が無ければ、これがそのまま最終応答
    if r.Header.Get("X-Api-Key") == "" && len(r.URL.Path) >= 4 && r.URL.Path[:4] == "/api" {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(401)
        w.Write([]byte(`{"error":"missing api key"}`))
        return
    }

    // 続行: Relayer-Next があれば、マッチしたページ / ルートが実行される
    w.Header().Set("Relayer-Next", "1")
    w.Header().Set("Relayer-Request-Header-X-User", "42") // ルートから見えるリクエストヘッダ
    w.Header().Set("X-Proxied-By", "go")                  // 最終応答にマージされるレスポンスヘッダ
}
  • Relayer-Next: 1 がある → 続行。本文は無視します。Relayer-Rewrite-Path: /other で別のパスにルーティングし、Relayer-Request-Header-<Name>: value でリクエストヘッダを追加/上書きします。それ以外のレスポンスヘッダは最終応答にマージされます(Status:・Content-Type・ Content-Length を除く)。
  • Relayer-Next がない → 短絡。Status:(既定 200)/ ヘッダ / 本文をそのまま送ります。
  • Relayer-* ヘッダはクライアントに届きません。
  • RELAYER_MATCHED_PATTERN に、そのパスがマッチするパターン(404 なら空)が入ります。
  • middleware.php と middleware.go が両方あると RuntimeException になります。
  • 失敗はページへのリクエストでも JSON 500 です。
  • すべての ページとルートに数ミリ秒が乗ります。/_defer/* と /_profiler はこの外で動くので、それらも覆う検査は guard に置きます。
最終更新 — 2026-11-01
改訂履歴 (1)
  • v0.31.0: 新規ページ