This repository has been archived on 2026-03-06. You can view files and clone it, but cannot push or open issues or pull requests.
go-php/handler.go
Snider e894e4b5bc
Some checks failed
Security Scan / security (push) Successful in 8s
Test / test (push) Failing after 1m30s
fix: add cgo build tags to FrankenPHP files, add .gitignore
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-04 17:27:43 +00:00

155 lines
4.4 KiB
Go

//go:build cgo
// Package php provides FrankenPHP embedding for Go applications.
// Serves a Laravel application via the FrankenPHP runtime, with support
// for Octane worker mode (in-memory, sub-ms responses) and standard mode
// fallback. Designed for use with Wails v3's AssetOptions.Handler.
package php
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/dunglas/frankenphp"
)
// Handler implements http.Handler by delegating to FrankenPHP.
// It resolves URLs to files (Caddy try_files pattern) before passing
// requests to the PHP runtime.
type Handler struct {
docRoot string
laravelRoot string
}
// HandlerConfig configures the FrankenPHP handler.
type HandlerConfig struct {
// NumThreads is the number of PHP threads (default: 4).
NumThreads int
// NumWorkers is the number of Octane workers (default: 2).
NumWorkers int
// PHPIni provides php.ini overrides.
PHPIni map[string]string
}
// NewHandler extracts the Laravel app from the given filesystem, prepares the
// environment, initialises FrankenPHP with worker mode, and returns the handler.
// The cleanup function must be called on shutdown to release resources and remove
// the extracted files.
func NewHandler(laravelRoot string, cfg HandlerConfig) (*Handler, func(), error) {
if cfg.NumThreads == 0 {
cfg.NumThreads = 4
}
if cfg.NumWorkers == 0 {
cfg.NumWorkers = 2
}
if cfg.PHPIni == nil {
cfg.PHPIni = map[string]string{
"display_errors": "Off",
"opcache.enable": "1",
}
}
docRoot := filepath.Join(laravelRoot, "public")
log.Printf("go-php: Laravel root: %s", laravelRoot)
log.Printf("go-php: Document root: %s", docRoot)
// Try Octane worker mode first, fall back to standard mode.
// Worker mode keeps Laravel booted in memory — sub-ms response times.
workerScript := filepath.Join(laravelRoot, "vendor", "laravel", "octane", "bin", "frankenphp-worker.php")
workerEnv := map[string]string{
"APP_BASE_PATH": laravelRoot,
"FRANKENPHP_WORKER": "1",
}
workerMode := false
if _, err := os.Stat(workerScript); err == nil {
if err := frankenphp.Init(
frankenphp.WithNumThreads(cfg.NumThreads),
frankenphp.WithWorkers("laravel", workerScript, cfg.NumWorkers, workerEnv, nil),
frankenphp.WithPhpIni(cfg.PHPIni),
); err != nil {
log.Printf("go-php: worker mode init failed (%v), falling back to standard mode", err)
} else {
workerMode = true
}
}
if !workerMode {
if err := frankenphp.Init(
frankenphp.WithNumThreads(cfg.NumThreads),
frankenphp.WithPhpIni(cfg.PHPIni),
); err != nil {
return nil, nil, fmt.Errorf("init FrankenPHP: %w", err)
}
}
if workerMode {
log.Printf("go-php: FrankenPHP initialised (Octane worker mode, %d workers)", cfg.NumWorkers)
} else {
log.Printf("go-php: FrankenPHP initialised (standard mode, %d threads)", cfg.NumThreads)
}
cleanup := func() {
frankenphp.Shutdown()
}
handler := &Handler{
docRoot: docRoot,
laravelRoot: laravelRoot,
}
return handler, cleanup, nil
}
// LaravelRoot returns the path to the extracted Laravel application.
func (h *Handler) LaravelRoot() string {
return h.laravelRoot
}
// DocRoot returns the path to the document root (public/).
func (h *Handler) DocRoot() string {
return h.docRoot
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
urlPath := r.URL.Path
filePath := filepath.Join(h.docRoot, filepath.Clean(urlPath))
info, err := os.Stat(filePath)
if err == nil && info.IsDir() {
// Directory → try index.php inside it
urlPath = strings.TrimRight(urlPath, "/") + "/index.php"
} else if err != nil && !strings.HasSuffix(urlPath, ".php") {
// File not found and not a .php request → front controller
urlPath = "/index.php"
}
// Serve static assets directly (CSS, JS, images)
if !strings.HasSuffix(urlPath, ".php") {
staticPath := filepath.Join(h.docRoot, filepath.Clean(urlPath))
if info, err := os.Stat(staticPath); err == nil && !info.IsDir() {
http.ServeFile(w, r, staticPath)
return
}
}
// Route to FrankenPHP
r.URL.Path = urlPath
req, err := frankenphp.NewRequestWithContext(r,
frankenphp.WithRequestDocumentRoot(h.docRoot, false),
)
if err != nil {
http.Error(w, fmt.Sprintf("FrankenPHP request error: %v", err), http.StatusInternalServerError)
return
}
if err := frankenphp.ServeHTTP(w, req); err != nil {
http.Error(w, fmt.Sprintf("FrankenPHP serve error: %v", err), http.StatusInternalServerError)
}
}