Frango is a focused, framework-agnostic middleware that integrates PHP into any Go HTTP server using FrankenPHP.
IT IS STILL WORK IN PROGRESS
Frango is built as a pure middleware with no server abstractions. This design:
- Maximizes compatibility - Works with any HTTP server or router
- Minimizes dependencies - No external dependencies beyond FrankenPHP
- Simplifies integration - Drop-in middleware pattern fits Go's standard idioms
- Enables flexibility - Stack PHP processing with other middleware in any order
- Pure middleware implementation - Implements http.Handler for maximum compatibility
- Framework adapters - Specialized adapters for popular frameworks (Chi, Gin, Echo)
- Development mode - Automatic PHP file change detection during development
- Path-based routing - Map URLs to PHP files with fine-grained control
- Directory mounting - Serve entire directories of PHP files with one call
- Embedded support - Embed PHP files directly in your Go binary
Before you begin, make sure you have all the prerequisites below. The library will not work without properly built PHP.
- Go 1.21 or later
- PHP 8.2 or later built with specific flags (see Building PHP section)
- Required PHP extensions:
- Redis extension (
pecl install redis
) - cURL extension
- Redis extension (
- GCC and other build tools for compiling FrankenPHP
FrankenPHP requires PHP to be built as a static library with ZTS (thread safety) enabled. The standard PHP installation from Homebrew won't work.
- Install required dependencies:
brew install libiconv bison brotli re2c pkg-config
echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc # Reload shell configuration
- Get PHP source and configure it:
# Get PHP source
cd ~ && mkdir -p php-build && cd php-build
curl -LO https://www.php.net/distributions/php-8.2.20.tar.gz
tar -xzf php-8.2.20.tar.gz
cd php-8.2.20
# Configure with the correct flags for macOS
# Note: We're explicitly configuring with minimal extensions to avoid dependency issues
./configure \
--enable-embed=static \
--enable-zts \
--disable-zend-signals \
--disable-opcache-jit \
--enable-static \
--enable-shared=no \
--with-iconv=/opt/homebrew/opt/libiconv/ \
--without-sqlite3 \
--without-pdo-sqlite \
--disable-dom \
--disable-xml \
--disable-simplexml \
--disable-xmlreader \
--disable-xmlwriter \
--disable-libxml
- Compile and install PHP:
make -j"$(getconf _NPROCESSORS_ONLN)"
sudo make install
- Verify the PHP build:
# Check that the static library was created
ls -la /usr/local/lib/libphp.a
# Check php-config output
php-config --includes
php-config --ldflags
php-config --libs
# The output should include paths to the PHP header files and libraries
If the above method doesn't work, try using the exact method from the FrankenPHP repository:
- Clone the FrankenPHP repository and build PHP from there:
# Clone the repository
git clone https://github.com/dunglas/frankenphp.git
cd frankenphp
# Build PHP using the provided script (this will handle everything for you)
./install.sh
# The script will download, configure and compile PHP with the correct flags
- Install Go dependencies:
go mod tidy
- Run the application with the correct CGO flags:
# Method 1: Using php-config
CGO_CFLAGS=$(php-config --includes) CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" go run -tags=nowatcher ./examples/basic
# Method 2: Explicitly setting the paths (try this if Method 1 fails)
CGO_CFLAGS="-I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/Zend -I/usr/local/include/php/TSRM -I/usr/local/include/php/ext" CGO_LDFLAGS="-L/usr/local/lib -lphp" go run -tags=nowatcher ./examples/basic
Before using this library, be aware of these FrankenPHP characteristics:
- It doesn't provide a built-in global memory store that persists across multiple PHP requests and workers.
- It runs PHP scripts using Caddy and a worker pool model, which means:
- Each request is processed independently.
- PHP memory resets after each request (just like traditional FPM).
- There is no global memory space shared between requests by default.
go get github.com/davidroman0O/frango
package main
import (
"log"
"net/http"
"github.com/davidroman0O/frango"
)
func main() {
// Create the PHP middleware
php, err := frango.New(
frango.WithSourceDir("./web"),
frango.WithDevelopmentMode(true),
)
if err != nil {
log.Fatalf("Error creating PHP middleware: %v", err)
}
defer php.Shutdown()
// Register PHP endpoints
php.HandlePHP("/api/users", "api/users.php")
php.HandleDir("/pages", "pages")
// Create standard HTTP mux
mux := http.NewServeMux()
// Add Go handlers
mux.HandleFunc("/api/time", timeHandler)
// Use PHP middleware for /api paths
mux.Handle("/api/", php.Wrap(http.NotFoundHandler()))
// Mount PHP directly for a dedicated path
mux.Handle("/php/", http.StripPrefix("/php", php))
// Start the server
log.Println("Server starting on :8080")
http.ListenAndServe(":8080", mux)
}
// Basic usage as middleware
mux := http.NewServeMux()
mux.Handle("/api/", php.Wrap(apiHandler))
// Direct mounting
mux.Handle("/php/", http.StripPrefix("/php", php))
http.ListenAndServe(":8080", mux)
For complex applications, you may want to clearly separate PHP and Go routes:
// Create a router for Go API endpoints
apiMux := http.NewServeMux()
apiMux.HandleFunc("GET /api/status", statusHandler)
// Create a router for PHP pages
phpMux := http.NewServeMux()
phpMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
php.ServeHTTP(w, r)
})
// Register PHP files
php.HandlePHP("/", "index.php")
php.HandlePHP("/users", "users.php")
// Create a parent router
rootMux := http.NewServeMux()
rootMux.Handle("/api/", apiMux)
rootMux.Handle("/", phpMux)
http.ListenAndServe(":8080", rootMux)
r := chi.NewRouter()
// Mount at a specific path
r.Mount("/php", php)
// As middleware in a group
r.Group(func(r chi.Router) {
r.Use(php.ForChi())
r.Get("/api/*", yourHandler)
})
http.ListenAndServe(":8080", r)
g := gin.New()
// Use middleware on specific routes
apiGroup := g.Group("/api")
apiGroup.Use(func(c *gin.Context) {
req := c.Request
if php.ShouldHandlePHP(req) {
php.ServeHTTP(c.Writer, req)
c.Abort()
return
}
c.Next()
})
g.Run(":8080")
e := echo.New()
// Use the built-in adapter
e.Use(php.ForEcho())
e.Start(":8080")
New(options...)
- Create a new middleware instanceHandlePHP(pattern, phpFile)
- Register a PHP file at a URL pathHandleDir(prefix, dirPath)
- Register all PHP files in a directoryAddFromEmbed(urlPath, fs, fsPath)
- Add a PHP file from embed.FSWrap(next http.Handler)
- Wrap another handler (middleware pattern)ServeHTTP(w, r)
- Implements http.Handler interfaceShouldHandlePHP(r *http.Request)
- Checks if a request should be handled by PHP
php, err := frango.New(
frango.WithSourceDir("./web"), // Source directory for PHP files
frango.WithDevelopmentMode(true), // Enable development mode
frango.WithLogger(customLogger), // Custom logger
)
You can inject Go variables into PHP templates:
// Register a PHP file with dynamic data
php.HandleRender("/dashboard", "dashboard.php", func(w http.ResponseWriter, r *http.Request) map[string]interface{} {
return map[string]interface{}{
"title": "Dashboard",
"user": map[string]interface{}{
"name": "John Doe",
"role": "Admin",
},
}
})
In PHP, access these variables:
<?php
$title = go_var('title', 'Default Title');
$user = go_var('user', []);
echo "<h1>$title</h1>";
echo "<p>Welcome, {$user['name']}!</p>";
?>
MIT
Contributions are welcome! Please feel free to submit a Pull Request.