-
Notifications
You must be signed in to change notification settings - Fork 425
feat: hot reloading #2031
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: hot reloading #2031
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces hot reloading capabilities for PHP applications in FrankenPHP, enabling automatic browser refresh when source files change during development. The implementation is built on top of the existing watcher mechanism and Mercure integration, sending Mercure updates when watched files are modified so clients can reload the page or specific assets.
Key changes include:
- Refactored watcher module to support per-pattern callbacks and event metadata
- Added Mercure integration to publish file change events as hot reloading updates
- Introduced new
Eventtype system with structured event types (EffectType, PathType) and JSON marshaling
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| worker.go | Integrates watcher patterns with worker-specific hot reloading callbacks and configures Mercure hub |
| state.go | Code formatting improvements and initialization cleanup |
| options.go | Adds mercureContext field to workerOpt struct |
| mercure.go | Implements hot reloading update publishing via Mercure hub and environment variable configuration |
| mercure-skip.go | Provides stub implementations for nomercure build tag |
| internal/watcher/watcher.h | Updates C function signatures with renamed parameters |
| internal/watcher/watcher.go | Major refactor: introduces PatternGroup concept, per-pattern callbacks, and event-driven architecture |
| internal/watcher/watcher.c | Updates C event handler to pass full event struct |
| internal/watcher/watcher-skip.go | Provides stub implementation for nowatcher build tag |
| internal/watcher/watch_pattern_test.go | Updates tests to use new Event-based API |
| internal/watcher/watch_pattern.go | Refactors pattern matching to work with Event objects and adds CGO integration for watcher events |
| internal/watcher/types.go | Defines Event type system with EffectType and PathType enums and JSON marshaling |
| caddy/workerconfig.go | Adds mercureContext field to workerConfig |
| caddy/module.go | Updates to use new assignMercureHub method name |
| caddy/mercure.go | Implements Mercure hub assignment for workers and adds appendMercureHubOption helper |
| caddy/mercure-skip.go | Provides stub implementations for nomercure build tag with corrected function name |
| caddy/app.go | Simplifies worker options initialization using appendMercureHubOption |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
a46e160 to
d840b63
Compare
|
I'll add a review later, but very cool! I currently have this running through PhpStorm (forces a F5), but we could even go further with mercure + turbo and send a morph update (configurable by users, of course). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 9 comments.
Comments suppressed due to low confidence (1)
internal/watcher/watcher.go:77
- If
activeWatcher.startWatching()returns an error (line 73), the function returns immediately without cleaning up. ThewatcherIsActiveatomic bool is set totrue(line 60), but it's not reset tofalseon error. This could prevent subsequent attempts to initialize the watcher. Consider adding cleanup logic:
if err := activeWatcher.startWatching(); err != nil {
watcherIsActive.Store(false)
activeWatcher = nil
return err
} if err := activeWatcher.startWatching(); err != nil {
return err
}
return nil
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
internal/watcher/watcher.go
Outdated
| for i, w := range g.watchers { | ||
| w.events = g.events | ||
| if err := w.startSession(); err != nil { | ||
| for j := 0; j < i; j++ { | ||
| g.watchers[j].stop() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would the mercure events start a separate instance of the watcher here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I think all this would be much easier if we just stick to 1 callback, something like this:
func(events) {
if watchingWorkers {
RestartWorkers()
}
if watchingWithMercure{
broadcastHotReloadEvents(events)
}
}()Or if the callback of the pattern group already contains both worker restarting and hot reload broadcasting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if it is worth it, because patterns can be different for hot reloading and for Mercure.
For instance, you don't want to restart the workers when a JS file changes, but you want to notify through Mercure.
|
The "eternal grace" issue is something that I should fix Mercure-side and is unrelated to this specific feature. |
withinboredom
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually really cool. I'm pretty excited to use it on my hugo generator. Developing with it is a pita because its mostly generated html (other than the API + interactive blog posts), and this would work perfectly for that.
933ada1 to
14ebc1d
Compare
6a14220 to
cabb101
Compare
Fixes #2114 This early [Shutdown](https://github.com/php/frankenphp/blob/11160fb7b31171d706cf9933abb9102dbb1cdb3c/frankenphp.go#L281) introduced in #2031 segfaults instead of returning an error since threads have not started yet.

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 and of the 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:
I plan to create a helper JS library to handle more advanced cases (reloading CSS, JS, etc), similar to HotWire Spark. Be sure to attend my SymfonyCon to learn more!
There is still room for improvement:
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).