2022-10-04 14:36:03 +02:00
|
|
|
package frankenphp
|
|
|
|
|
|
2022-10-04 17:42:45 +02:00
|
|
|
import (
|
2025-11-17 16:32:23 +01:00
|
|
|
"context"
|
2025-06-30 09:38:18 +02:00
|
|
|
"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
|
|
|
)
|
2022-10-04 14:36:03 +02:00
|
|
|
|
2025-06-30 09:38:18 +02:00
|
|
|
// defaultMaxConsecutiveFailures is the default maximum number of consecutive failures before panicking
|
|
|
|
|
const defaultMaxConsecutiveFailures = 6
|
|
|
|
|
|
2022-10-15 11:54:42 +02:00
|
|
|
// Option instances allow to configure FrankenPHP.
|
2022-10-04 14:36:03 +02:00
|
|
|
type Option func(h *opt) error
|
|
|
|
|
|
2025-06-30 09:38:18 +02:00
|
|
|
// WorkerOption instances allow configuring FrankenPHP worker.
|
|
|
|
|
type WorkerOption func(*workerOpt) error
|
|
|
|
|
|
2022-10-04 14:36:03 +02:00
|
|
|
// 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
|
|
|
|
|
|
2025-11-17 16:32:23 +01:00
|
|
|
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
|
2022-10-04 14:36:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2025-06-30 09:38:18 +02:00
|
|
|
name string
|
|
|
|
|
fileName string
|
|
|
|
|
num int
|
2025-11-18 11:55:29 +01:00
|
|
|
maxThreads int
|
2025-06-30 09:38:18 +02:00
|
|
|
env PreparedEnv
|
2025-11-23 23:14:23 +01:00
|
|
|
requestOptions []RequestOption
|
2025-06-30 09:38:18 +02:00
|
|
|
watch []string
|
|
|
|
|
maxConsecutiveFailures int
|
2025-11-10 14:12:14 +01:00
|
|
|
extensionWorkers *extensionWorkers
|
2025-10-28 20:37:20 +01:00
|
|
|
onThreadReady func(int)
|
|
|
|
|
onThreadShutdown func(int)
|
|
|
|
|
onServerStartup func()
|
|
|
|
|
onServerShutdown func()
|
2022-10-04 14:36:03 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 16:32:23 +01:00
|
|
|
// WithContext sets the main context to use.
|
|
|
|
|
func WithContext(ctx context.Context) Option {
|
|
|
|
|
return func(h *opt) error {
|
|
|
|
|
h.ctx = ctx
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-15 11:54:42 +02:00
|
|
|
// WithNumThreads configures the number of PHP threads to start.
|
2022-10-04 14:36:03 +02:00
|
|
|
func WithNumThreads(numThreads int) Option {
|
|
|
|
|
return func(o *opt) error {
|
2022-10-12 09:38:45 +02:00
|
|
|
o.numThreads = numThreads
|
2022-10-04 14:36:03 +02:00
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-19 20:39:33 +01:00
|
|
|
func WithMaxThreads(maxThreads int) Option {
|
|
|
|
|
return func(o *opt) error {
|
|
|
|
|
o.maxThreads = maxThreads
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-26 09:53:37 +02:00
|
|
|
func WithMetrics(m Metrics) Option {
|
|
|
|
|
return func(o *opt) error {
|
|
|
|
|
o.metrics = m
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-05 21:14:19 +07:00
|
|
|
// WithWorkers configures the PHP workers to start
|
2025-11-10 14:12:14 +01:00
|
|
|
func WithWorkers(name, fileName string, num int, options ...WorkerOption) Option {
|
2022-10-04 14:36:03 +02:00
|
|
|
return func(o *opt) error {
|
2025-06-30 09:38:18 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-10 14:12:14 +01:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-30 09:38:18 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 23:14:23 +01:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 11:55:29 +01:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-30 09:38:18 +02:00
|
|
|
// 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
|
2022-10-04 14:36:03 +02:00
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-10-04 17:42:45 +02:00
|
|
|
|
2025-10-28 20:37:20 +01: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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-10 14:12:14 +01:00
|
|
|
func withExtensionWorkers(w *extensionWorkers) WorkerOption {
|
|
|
|
|
return func(wo *workerOpt) error {
|
|
|
|
|
wo.extensionWorkers = w
|
2025-03-19 13:21:37 +01:00
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|