cli/cmd/core-app/handler.go
Snider e5c82dab5e feat(core-app): FrankenPHP + Wails v3 native desktop app
Single 53MB binary embedding PHP 8.4 ZTS runtime, Laravel 12,
Livewire 4, and Octane worker mode inside a Wails v3 native
desktop window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 22:50:18 +00:00

137 lines
3.7 KiB
Go

package main
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/dunglas/frankenphp"
)
// PHPHandler implements http.Handler by delegating to FrankenPHP.
// It resolves URLs to files (like Caddy's try_files) before passing
// requests to the PHP runtime.
type PHPHandler struct {
docRoot string
laravelRoot string
}
// NewPHPHandler extracts the embedded Laravel app, prepares the environment,
// initialises FrankenPHP with worker mode, and returns the handler.
func NewPHPHandler() (*PHPHandler, *AppEnvironment, func(), error) {
// Extract embedded Laravel to temp directory
laravelRoot, err := extractLaravel()
if err != nil {
return nil, nil, nil, fmt.Errorf("extract Laravel: %w", err)
}
// Prepare persistent environment
env, err := PrepareEnvironment(laravelRoot)
if err != nil {
os.RemoveAll(laravelRoot)
return nil, nil, nil, fmt.Errorf("prepare environment: %w", err)
}
docRoot := filepath.Join(laravelRoot, "public")
log.Printf("Laravel root: %s", laravelRoot)
log.Printf("Document root: %s", docRoot)
log.Printf("Data directory: %s", env.DataDir)
log.Printf("Database: %s", env.DatabasePath)
// 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(4),
frankenphp.WithWorkers("laravel", workerScript, 2, workerEnv, nil),
frankenphp.WithPhpIni(map[string]string{
"display_errors": "Off",
"opcache.enable": "1",
}),
); err != nil {
log.Printf("Worker mode init failed (%v), falling back to standard mode", err)
} else {
workerMode = true
}
}
if !workerMode {
if err := frankenphp.Init(
frankenphp.WithNumThreads(4),
frankenphp.WithPhpIni(map[string]string{
"display_errors": "Off",
"opcache.enable": "1",
}),
); err != nil {
os.RemoveAll(laravelRoot)
return nil, nil, nil, fmt.Errorf("init FrankenPHP: %w", err)
}
}
if workerMode {
log.Println("FrankenPHP initialised (Octane worker mode, 2 workers)")
} else {
log.Println("FrankenPHP initialised (standard mode, 4 threads)")
}
cleanup := func() {
frankenphp.Shutdown()
os.RemoveAll(laravelRoot)
}
handler := &PHPHandler{
docRoot: docRoot,
laravelRoot: laravelRoot,
}
return handler, env, cleanup, nil
}
func (h *PHPHandler) 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)
}
}