|
| 1 | +# Hot Reload |
| 2 | + |
| 3 | +FrankenPHP includes a built-in **hot reload** feature designed to vastly improve the developer experience. |
| 4 | + |
| 5 | + |
| 6 | + |
| 7 | +This feature provides a workflow similar to **Hot Module Replacement (HMR)** found in modern JavaScript tooling (like Vite or webpack). |
| 8 | +Instead of manually refreshing the browser after every file change (PHP code, templates, JavaScript and CSS files...), |
| 9 | +FrankenPHP updates the content in real-time. |
| 10 | + |
| 11 | +Hot Reload natively works with WordPress, Laravel, Symfony, and any other PHP application or framework. |
| 12 | + |
| 13 | +When enabled, FrankenPHP watches your current working directory for filesystem changes. |
| 14 | +When a file is modified, it pushes a [Mercure](mercure.md) update to the browser. |
| 15 | + |
| 16 | +Depending on your setup, the browser will either: |
| 17 | + |
| 18 | +- **Morph the DOM** (preserving scroll position and input state) if [Idiomorph](https://github.com/bigskysoftware/idiomorph) is loaded. |
| 19 | +- **Reload the page** (standard live reload) if Idiomorph is not present. |
| 20 | + |
| 21 | +## Configuration |
| 22 | + |
| 23 | +To enable hot reloading, enable Mercure, then add the `hot_reload` sub-directive to the `php_server` directive in your `Caddyfile`. |
| 24 | + |
| 25 | +> [!WARNING] |
| 26 | +> This feature is intended for **development environments only**. |
| 27 | +> Do not enable `hot_reload` in production, as watching the filesystem incurs performance overhead and exposes internal endpoints. |
| 28 | +
|
| 29 | +```caddyfile |
| 30 | +localhost |
| 31 | +
|
| 32 | +mercure { |
| 33 | + anonymous |
| 34 | +} |
| 35 | +
|
| 36 | +root public/ |
| 37 | +php_server { |
| 38 | + hot_reload |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +By default, FrankenPHP will watch all files in the current working directory matching this glob pattern: `./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}` |
| 43 | + |
| 44 | +It's possible to explicitly set the files to watch using the glob syntax: |
| 45 | + |
| 46 | +```caddyfile |
| 47 | +localhost |
| 48 | +
|
| 49 | +mercure { |
| 50 | + anonymous |
| 51 | +} |
| 52 | +
|
| 53 | +root public/ |
| 54 | +php_server { |
| 55 | + hot_reload src/**/*{.php,.js} config/**/*.yaml |
| 56 | +} |
| 57 | +``` |
| 58 | + |
| 59 | +Use the long form to specify the Mercure topic to use as well as which directories or files to watch by providing paths to the `hot_reload` option: |
| 60 | + |
| 61 | +```caddyfile |
| 62 | +localhost |
| 63 | +
|
| 64 | +mercure { |
| 65 | + anonymous |
| 66 | +} |
| 67 | +
|
| 68 | +root public/ |
| 69 | +php_server { |
| 70 | + hot_reload { |
| 71 | + topic hot-reload-topic |
| 72 | + watch src/**/*.php |
| 73 | + watch assets/**/*.{ts,json} |
| 74 | + watch templates/ |
| 75 | + watch public/css/ |
| 76 | + } |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +## Client-Side Integration |
| 81 | + |
| 82 | +While the server detects changes, the browser needs to subscribe to these events to update the page. |
| 83 | +FrankenPHP exposes the Mercure Hub URL to use for subscribing to file changes via the `$_SERVER['FRANKENPHP_HOT_RELOAD']` environment variable. |
| 84 | + |
| 85 | +A convenience JavaScript library, [frankenphp-hot-reload](https://www.npmjs.com/package/frankenphp-hot-reload), is also available to handle the client-side logic. |
| 86 | +To use it, add the following to your main layout: |
| 87 | + |
| 88 | +```php |
| 89 | +<!DOCTYPE html> |
| 90 | +<title>FrankenPHP Hot Reload</title> |
| 91 | +<?php if (isset($_SERVER['FRANKENPHP_HOT_RELOAD'])): ?> |
| 92 | +<meta name="frankenphp-hot-reload:url" content="<?=$_SERVER['FRANKENPHP_HOT_RELOAD']?>"> |
| 93 | +<script src="https://cdn.jsdelivr.net/npm/idiomorph"></script> |
| 94 | +<script src="https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm" type="module"></script> |
| 95 | +<?php endif ?> |
| 96 | +``` |
| 97 | + |
| 98 | +The library will automatically subscribe to the Mercure hub, fetch the current URL in the background when a file change is detected and morph the DOM. |
| 99 | +It is available as a [npm](https://www.npmjs.com/package/frankenphp-hot-reload) package and on [GitHub](https://github.com/dunglas/frankenphp-hot-reload). |
| 100 | + |
| 101 | +Alternatively, you can implement your own client-side logic by subscribing directly to the Mercure hub using the `EventSource` native JavaScript class. |
| 102 | + |
| 103 | +### Worker Mode |
| 104 | + |
| 105 | +If you are running your application in [Worker Mode](https://frankenphp.dev/docs/worker/), your application script remains in memory. |
| 106 | +This means changes to your PHP code will not be reflected immediately, even if the browser reloads. |
| 107 | + |
| 108 | +For the best developer experience, you should combine `hot_reload` with [the `watch` sub-directive in the `worker` directive](config.md#watching-for-file-changes). |
| 109 | + |
| 110 | +- `hot_reload`: refreshes the **browser** when files change |
| 111 | +- `worker.watch`: restarts the worker when files change |
| 112 | + |
| 113 | +```caddy |
| 114 | +localhost |
| 115 | +
|
| 116 | +mercure { |
| 117 | + anonymous |
| 118 | +} |
| 119 | +
|
| 120 | +root public/ |
| 121 | +php_server { |
| 122 | + hot_reload |
| 123 | + worker { |
| 124 | + file /path/to/my_worker.php |
| 125 | + watch |
| 126 | + } |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +### How it works |
| 131 | + |
| 132 | +1. **Watch**: FrankenPHP monitors the filesystem for modifications using [the `e-dant/watcher` library](https://github.com/e-dant/watcher) under the hood (we contributed the Go binding). |
| 133 | +2. **Restart (Worker Mode)**: if `watch` is enabled in the worker config, the PHP worker is restarted to load the new code. |
| 134 | +3. **Push**: a JSON payload containing the list of changed files is sent to the built-in [Mercure hub](https://mercure.rocks). |
| 135 | +4. **Receive**: The browser, listening via the JavaScript library, receives the Mercure event. |
| 136 | +5. **Update**: |
| 137 | + |
| 138 | +- If **Idiomorph** is detected, it fetches the updated content and morphs the current HTML to match the new state, applying changes instantly without losing state. |
| 139 | +- Otherwise, `window.location.reload()` is called to refresh the page. |
0 commit comments