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/cmd_serve_frankenphp.go
Snider 84ce94b713
Some checks failed
Security Scan / security (push) Successful in 9s
Test / test (push) Failing after 25s
feat: add core-php binary, fix imports to use core/cli
- Create cmd/core-php/main.go standalone binary entry point
- Add FrankenPHP serve:embedded and exec commands (CGO build-tagged)
- Replace all cobra imports with core/cli re-exports
- Replace core/go/pkg/cli with core/cli/pkg/cli
- Replace core/go/pkg/i18n with core/go-i18n/pkg/i18n
- Add core/go and core/go-i18n as go.mod dependencies

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-04 17:22:10 +00:00

141 lines
3.6 KiB
Go

//go:build cgo
package php
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"forge.lthn.ai/core/cli/pkg/cli"
)
var (
serveFPPort int
serveFPPath string
serveFPWorkers int
serveFPThreads int
)
func init() {
cli.RegisterCommands(addFrankenPHPCommands)
}
func addFrankenPHPCommands(root *cli.Command) {
// Find the php parent command, or create one
phpCmd := findOrCreatePHPCmd(root)
serveCmd := &cli.Command{
Use: "serve:embedded",
Short: "Serve Laravel via embedded FrankenPHP runtime",
Long: "Start an HTTP server using the embedded FrankenPHP runtime with Octane worker mode support.",
RunE: runFrankenPHPServe,
}
serveCmd.Flags().IntVar(&serveFPPort, "port", 8000, "HTTP listen port")
serveCmd.Flags().StringVar(&serveFPPath, "path", ".", "Laravel application root")
serveCmd.Flags().IntVar(&serveFPWorkers, "workers", 2, "Octane worker count")
serveCmd.Flags().IntVar(&serveFPThreads, "threads", 4, "PHP thread count")
phpCmd.AddCommand(serveCmd)
execCmd := &cli.Command{
Use: "exec [command...]",
Short: "Execute a PHP artisan command via FrankenPHP",
Long: "Boot FrankenPHP, run an artisan command, then exit. Stdin/stdout pass-through.",
Args: cli.MinimumNArgs(1),
RunE: runFrankenPHPExec,
}
execCmd.Flags().StringVar(&serveFPPath, "path", ".", "Laravel application root")
phpCmd.AddCommand(execCmd)
}
func runFrankenPHPServe(cmd *cli.Command, args []string) error {
handler, cleanup, err := NewHandler(serveFPPath, HandlerConfig{
NumThreads: serveFPThreads,
NumWorkers: serveFPWorkers,
})
if err != nil {
return fmt.Errorf("init FrankenPHP: %w", err)
}
defer cleanup()
addr := fmt.Sprintf(":%d", serveFPPort)
srv := &http.Server{
Addr: addr,
Handler: handler,
}
// Graceful shutdown
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
log.Printf("core-php: serving on http://localhost%s (doc root: %s)", addr, handler.DocRoot())
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("core-php: server error: %v", err)
}
}()
<-ctx.Done()
log.Println("core-php: shutting down...")
return srv.Shutdown(context.Background())
}
func runFrankenPHPExec(cmd *cli.Command, args []string) error {
handler, cleanup, err := NewHandler(serveFPPath, HandlerConfig{
NumThreads: 1,
NumWorkers: 0,
})
if err != nil {
return fmt.Errorf("init FrankenPHP: %w", err)
}
defer cleanup()
// Build an artisan request
artisanArgs := "artisan"
for _, a := range args {
artisanArgs += " " + a
}
log.Printf("core-php: exec %s (root: %s)", artisanArgs, handler.LaravelRoot())
// Execute via internal HTTP request to FrankenPHP
// This routes through the PHP runtime as if it were a CLI call
req, err := http.NewRequest("GET", "/artisan-exec?cmd="+artisanArgs, nil)
if err != nil {
return err
}
// For now, use the handler directly
w := &execResponseWriter{os.Stdout}
handler.ServeHTTP(w, req)
return nil
}
// findOrCreatePHPCmd finds an existing "php" command or creates one.
func findOrCreatePHPCmd(root *cli.Command) *cli.Command {
for _, c := range root.Commands() {
if c.Use == "php" {
return c
}
}
phpCmd := &cli.Command{
Use: "php",
Short: "PHP/Laravel development tools",
}
root.AddCommand(phpCmd)
return phpCmd
}
// execResponseWriter writes HTTP response body directly to stdout.
type execResponseWriter struct {
out *os.File
}
func (w *execResponseWriter) Header() http.Header { return http.Header{} }
func (w *execResponseWriter) WriteHeader(statusCode int) {}
func (w *execResponseWriter) Write(b []byte) (int, error) { return w.out.Write(b) }