SIGN IN SIGN UP
package frankenphp
2022-10-04 17:42:45 +02:00
import (
"context"
"fmt"
2025-04-26 17:04:46 +08:00
"log/slog"
2025-03-19 13:21:37 +01:00
"time"
2022-10-04 17:42:45 +02:00
)
// defaultMaxConsecutiveFailures is the default maximum number of consecutive failures before panicking
const defaultMaxConsecutiveFailures = 6
// Option instances allow to configure FrankenPHP.
type Option func(h *opt) error
// WorkerOption instances allow configuring FrankenPHP worker.
type WorkerOption func(*workerOpt) error
// opt contains the available options.
//
// If you change this, also update the Caddy module and the documentation.
type opt struct {
feat: hot reload (#2031) This patch brings hot reloading capabilities to PHP apps: in development, the browser will automatically refresh the page when any source file changes! It's similar to HMR in JavaScript. It is built on top of [the watcher mechanism](https://frankenphp.dev/docs/config/#watching-for-file-changes) and of the [Mercure](https://frankenphp.dev/docs/mercure/) integration. Each time a watched file is modified, a Mercure update is sent, giving the ability to the client to reload the page, or part of the page (assets, images...). Here is an example implementation: ```caddyfile root ./public mercure { subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} anonymous } php_server { hot_reload } ``` ```php <?php header('Content-Type: text/html'); ?> <!DOCTYPE html> <html lang="en"> <head> <title>Test</title> <script> const es = new EventSource('<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>'); es.onmessage = () => location.reload(); </script> </head> <body> Hello ``` I plan to create a helper JS library to handle more advanced cases (reloading CSS, JS, etc), similar to [HotWire Spark](https://github.com/hotwired/spark). Be sure to attend my SymfonyCon to learn more! There is still room for improvement: - Provide an option to only trigger the update without reloading the worker for some files (ex, images, JS, CSS...) - Support classic mode (currently, only the worker mode is supported) - Don't reload all workers when only the files used by one change However, this PR is working as-is and can be merged as a first step. This patch heavily refactors the watcher module. Maybe it will be possible to extract it as a standalone library at some point (would be useful to add a similar feature but not tight to PHP as a Caddy module). --------- Signed-off-by: Kévin Dunglas <kevin@dunglas.fr> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-12 14:29:18 +01:00
hotReloadOpt
ctx context.Context
2025-03-19 13:21:37 +01:00
numThreads int
maxThreads int
workers []workerOpt
2025-04-26 17:04:46 +08:00
logger *slog.Logger
2025-03-19 13:21:37 +01:00
metrics Metrics
phpIni map[string]string
maxWaitTime time.Duration
}
type workerOpt struct {
feat: hot reload (#2031) This patch brings hot reloading capabilities to PHP apps: in development, the browser will automatically refresh the page when any source file changes! It's similar to HMR in JavaScript. It is built on top of [the watcher mechanism](https://frankenphp.dev/docs/config/#watching-for-file-changes) and of the [Mercure](https://frankenphp.dev/docs/mercure/) integration. Each time a watched file is modified, a Mercure update is sent, giving the ability to the client to reload the page, or part of the page (assets, images...). Here is an example implementation: ```caddyfile root ./public mercure { subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} anonymous } php_server { hot_reload } ``` ```php <?php header('Content-Type: text/html'); ?> <!DOCTYPE html> <html lang="en"> <head> <title>Test</title> <script> const es = new EventSource('<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>'); es.onmessage = () => location.reload(); </script> </head> <body> Hello ``` I plan to create a helper JS library to handle more advanced cases (reloading CSS, JS, etc), similar to [HotWire Spark](https://github.com/hotwired/spark). Be sure to attend my SymfonyCon to learn more! There is still room for improvement: - Provide an option to only trigger the update without reloading the worker for some files (ex, images, JS, CSS...) - Support classic mode (currently, only the worker mode is supported) - Don't reload all workers when only the files used by one change However, this PR is working as-is and can be merged as a first step. This patch heavily refactors the watcher module. Maybe it will be possible to extract it as a standalone library at some point (would be useful to add a similar feature but not tight to PHP as a Caddy module). --------- Signed-off-by: Kévin Dunglas <kevin@dunglas.fr> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-12 14:29:18 +01:00
mercureContext
name string
fileName string
num int
maxThreads int
env PreparedEnv
requestOptions []RequestOption
watch []string
maxConsecutiveFailures int
extensionWorkers *extensionWorkers
onThreadReady func(int)
onThreadShutdown func(int)
onServerStartup func()
onServerShutdown func()
}
// WithContext sets the main context to use.
func WithContext(ctx context.Context) Option {
return func(h *opt) error {
h.ctx = ctx
return nil
}
}
// WithNumThreads configures the number of PHP threads to start.
func WithNumThreads(numThreads int) Option {
return func(o *opt) error {
2022-10-12 09:38:45 +02:00
o.numThreads = numThreads
return nil
}
}
func WithMaxThreads(maxThreads int) Option {
return func(o *opt) error {
o.maxThreads = maxThreads
return nil
}
}
func WithMetrics(m Metrics) Option {
return func(o *opt) error {
o.metrics = m
return nil
}
}
feat: define domain specific workers in `php_server` and `php` blocks (#1509) * add module (php_server directive) based workers * refactor moduleID to uintptr for faster comparisons * let workers inherit environment variables and root from php_server * caddy can shift FrankenPHPModules in memory for some godforsaken reason, can't rely on them staying the same * remove debugging statement * fix tests * refactor moduleID to uint64 for faster comparisons * actually allow multiple workers per script filename * remove logging * utility function * reuse existing worker with same filename and environment when calling newWorker with a filepath that already has a suitable worker, simply add number of threads * no cleanup happens between tests, so restore old global worker overwriting logic * add test, use getWorker(ForContext) function in frankenphp.go as well * bring error on second global worker with the same filename again * refactor to using name instead of moduleID * nicer name * nicer name * add more tests * remove test case already covered by previous test * revert back to single variable, moduleIDs no longer relevant * update comment * figure out the worker to use in FrankenPHPModule::ServeHTTP * add caddy/config_tests, add --retry 5 to download * add caddy/config_tests * sum up logic a bit, put worker thread addition into moduleWorkers parsing, before workers are actually created * implement suggestions as far as possible * fixup * remove tags * feat: download the mostly static binary when possible (#1467) * feat: download the mostly static binary when possible * cs * docs: remove wildcard matcher from root directive (#1513) * docs: update README with additional documentation links Add link to classic mode, efficiently serving large static files and monitoring FrankenPHP Signed-off-by: Romain Bastide <romain.bastide@orange.com> * ci: combine dependabot updates for one group to 1 pull-request * feat: compatibility with libphp.dylib on macOS * feat: upgrade to Caddy 2.10 * feat: upgrade to Caddy 2.10 * chore: run prettier * fix: build-static.sh consecutive builds (#1496) * fix consecutive builds * use minor version in PHP_VERSION * install jq in centos container * fix "arm64" download arch for spc binary * jq is not available as a rpm download * linter * specify php 8.4 default specify 8.4 so we manually switch to 8.5 when we make sure it works allows to run without jq installed * Apply suggestions from code review Co-authored-by: Kévin Dunglas <kevin@dunglas.fr> --------- Co-authored-by: Kévin Dunglas <kevin@dunglas.fr> * chore: update Go and toolchain version (#1526) * apply suggestions one be one - scriptpath only * generate unique worker names by filename and number * support worker config from embedded apps * rename back to make sure we don't accidentally add FrankenPHPApp workers to the slice * fix test after changing error message * use 🧩 for module workers * use 🌍 for global workers :) * revert 1c414cebbc4380b26c4ac46a8662f88bd807aa09 * revert 4cc8893cedc8a2c9e2195ca0e83e8e9cc359e136 * apply suggestions * add dynamic config loading test of module worker * fix test * minor changes --------- Signed-off-by: Romain Bastide <romain.bastide@orange.com> Co-authored-by: Kévin Dunglas <kevin@dunglas.fr> Co-authored-by: Indra Gunawan <hello@indra.my.id> Co-authored-by: Romain Bastide <romain.bastide@orange.com>
2025-05-05 21:14:19 +07:00
// WithWorkers configures the PHP workers to start
func WithWorkers(name, fileName string, num int, options ...WorkerOption) Option {
return func(o *opt) error {
worker := workerOpt{
name: name,
fileName: fileName,
num: num,
env: PrepareEnv(nil),
watch: []string{},
maxConsecutiveFailures: defaultMaxConsecutiveFailures,
}
for _, option := range options {
if err := option(&worker); err != nil {
return err
}
}
o.workers = append(o.workers, worker)
return nil
}
}
// EXPERIMENTAL: WithExtensionWorkers allow extensions to create workers.
//
// A worker script with the provided name, fileName and thread count will be registered, along with additional
// configuration through WorkerOptions.
//
// Workers are designed to run indefinitely and will be gracefully shut down when FrankenPHP shuts down.
//
// Extension workers receive the lowest priority when determining thread allocations. If the requested number of threads
// cannot be allocated, then FrankenPHP will panic and provide this information to the user (who will need to allocate
// more total threads). Don't be greedy.
func WithExtensionWorkers(name, fileName string, numThreads int, options ...WorkerOption) (Workers, Option) {
w := &extensionWorkers{
name: name,
fileName: fileName,
num: numThreads,
}
w.options = append(options, withExtensionWorkers(w))
return w, WithWorkers(w.name, w.fileName, w.num, w.options...)
}
// WithLogger configures the global logger to use.
func WithLogger(l *slog.Logger) Option {
return func(o *opt) error {
o.logger = l
return nil
}
}
// WithPhpIni configures user defined PHP ini settings.
func WithPhpIni(overrides map[string]string) Option {
return func(o *opt) error {
o.phpIni = overrides
return nil
}
}
// WithMaxWaitTime configures the max time a request may be stalled waiting for a thread.
func WithMaxWaitTime(maxWaitTime time.Duration) Option {
return func(o *opt) error {
o.maxWaitTime = maxWaitTime
return nil
}
}
// WithWorkerEnv sets environment variables for the worker
func WithWorkerEnv(env map[string]string) WorkerOption {
return func(w *workerOpt) error {
w.env = PrepareEnv(env)
return nil
}
}
// WithWorkerRequestOptions sets options for the main dummy request created for the worker
func WithWorkerRequestOptions(options ...RequestOption) WorkerOption {
return func(w *workerOpt) error {
w.requestOptions = append(w.requestOptions, options...)
return nil
}
}
// WithWorkerMaxThreads sets the max number of threads for this specific worker
func WithWorkerMaxThreads(num int) WorkerOption {
return func(w *workerOpt) error {
w.maxThreads = num
return nil
}
}
// WithWorkerWatchMode sets directories to watch for file changes
func WithWorkerWatchMode(watch []string) WorkerOption {
return func(w *workerOpt) error {
w.watch = watch
return nil
}
}
// WithWorkerMaxFailures sets the maximum number of consecutive failures before panicking
func WithWorkerMaxFailures(maxFailures int) WorkerOption {
return func(w *workerOpt) error {
if maxFailures < -1 {
return fmt.Errorf("max consecutive failures must be >= -1, got %d", maxFailures)
}
w.maxConsecutiveFailures = maxFailures
return nil
}
}
2022-10-04 17:42:45 +02:00
func WithWorkerOnReady(f func(int)) WorkerOption {
return func(w *workerOpt) error {
w.onThreadReady = f
return nil
}
}
func WithWorkerOnShutdown(f func(int)) WorkerOption {
return func(w *workerOpt) error {
w.onThreadShutdown = f
return nil
}
}
// WithWorkerOnServerStartup adds a function to be called right after server startup. Useful for extensions.
func WithWorkerOnServerStartup(f func()) WorkerOption {
return func(w *workerOpt) error {
w.onServerStartup = f
return nil
}
}
// WithWorkerOnServerShutdown adds a function to be called right before server shutdown. Useful for extensions.
func WithWorkerOnServerShutdown(f func()) WorkerOption {
return func(w *workerOpt) error {
w.onServerShutdown = f
return nil
}
}
func withExtensionWorkers(w *extensionWorkers) WorkerOption {
return func(wo *workerOpt) error {
wo.extensionWorkers = w
2025-03-19 13:21:37 +01:00
return nil
}
}