From 8d71f9ba27c987f4190dbf58f80d2e69d547b584 Mon Sep 17 00:00:00 2001 From: Louis C <1530720+Starfox64@users.noreply.github.com> Date: Sun, 28 Sep 2025 04:17:54 +0200 Subject: [PATCH 1/5] feat: Add Caddy placeholders --- caddy/interceptor.go | 39 +++++++++++++++++++++++++++++++++++++++ caddy/module.go | 4 +++- docs/config.md | 18 ++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 caddy/interceptor.go diff --git a/caddy/interceptor.go b/caddy/interceptor.go new file mode 100644 index 0000000000..df9b716797 --- /dev/null +++ b/caddy/interceptor.go @@ -0,0 +1,39 @@ +package caddy + +import ( + "net/http" + "strings" + + "github.com/caddyserver/caddy/v2" +) + +type responseWriterInterceptor struct { + http.ResponseWriter + replacer *caddy.Replacer + wroteHeader bool +} + +func (i *responseWriterInterceptor) WriteHeader(statusCode int) { + if !i.wroteHeader { + if i.replacer != nil { + i.replacer.Set("http.frankenphp.status_code", statusCode) + i.replacer.Set("http.frankenphp.status_text", http.StatusText(statusCode)) + + for key, values := range i.Header() { + i.replacer.Set("http.frankenphp.header."+key, strings.Join(values, ",")) + } + } + + i.wroteHeader = true + } + + i.ResponseWriter.WriteHeader(statusCode) +} + +func (i *responseWriterInterceptor) Write(b []byte) (int, error) { + if !i.wroteHeader { + i.WriteHeader(http.StatusOK) + } + + return i.ResponseWriter.Write(b) +} diff --git a/caddy/module.go b/caddy/module.go index eba65bc461..16e5552693 100644 --- a/caddy/module.go +++ b/caddy/module.go @@ -194,7 +194,9 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c frankenphp.WithWorkerName(workerName), ) - if err = frankenphp.ServeHTTP(w, fr); err != nil { + interceptor := &responseWriterInterceptor{ResponseWriter: w, replacer: repl} + + if err = frankenphp.ServeHTTP(interceptor, fr); err != nil { return caddyhttp.Error(http.StatusInternalServerError, err) } diff --git a/docs/config.md b/docs/config.md index 64654a18fb..755219f5f4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -206,6 +206,24 @@ where the FrankenPHP process was started. You can instead also specify one or mo The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher). +### Placeholders + +FrankenPHP exposes some placeholders that can be used in Caddy directives like `log_append`. +- `{http.frankenphp.status_code}`: the HTTP status code of the response +- `{http.frankenphp.status_text}`: the HTTP status text of the response (e.g. "OK", "Not Found", etc.) +- `{http.frankenphp.header.*}`: any HTTP header returned by the worker, e.g. `{http.frankenphp.header.X-User-Id}` + +```caddyfile +http:// { + log + route { + # ... + + log_append user {http.frankenphp.header.X-User-Id} + } +} +``` + ## Matching the worker to a path In traditional PHP applications, scripts are always placed in the public directory. From b54b7712e5aaad6d6c9c90cc3a456453c8ac6de1 Mon Sep 17 00:00:00 2001 From: Louis C <1530720+Starfox64@users.noreply.github.com> Date: Sun, 28 Sep 2025 17:26:21 +0200 Subject: [PATCH 2/5] fix(docs): Add blank line before placeholders list --- docs/config.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/config.md b/docs/config.md index 755219f5f4..9d2260d587 100644 --- a/docs/config.md +++ b/docs/config.md @@ -209,6 +209,7 @@ The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher) ### Placeholders FrankenPHP exposes some placeholders that can be used in Caddy directives like `log_append`. + - `{http.frankenphp.status_code}`: the HTTP status code of the response - `{http.frankenphp.status_text}`: the HTTP status text of the response (e.g. "OK", "Not Found", etc.) - `{http.frankenphp.header.*}`: any HTTP header returned by the worker, e.g. `{http.frankenphp.header.X-User-Id}` From 651a1d5ca593c840d67eae56a615fa48fa16055d Mon Sep 17 00:00:00 2001 From: Louis C <1530720+Starfox64@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:13:05 +0200 Subject: [PATCH 3/5] refactor: Expose a PHP function instead of using HTTP headers --- caddy/interceptor.go | 39 --------------------------------------- caddy/module.go | 10 +++++++--- context.go | 8 +++++++- docs/config.md | 12 +++++++----- frankenphp.c | 12 ++++++++++++ frankenphp.go | 15 +++++++++++++++ frankenphp.h | 3 +++ frankenphp.stub.php | 1 + frankenphp_arginfo.h | 9 ++++++++- 9 files changed, 60 insertions(+), 49 deletions(-) delete mode 100644 caddy/interceptor.go diff --git a/caddy/interceptor.go b/caddy/interceptor.go deleted file mode 100644 index df9b716797..0000000000 --- a/caddy/interceptor.go +++ /dev/null @@ -1,39 +0,0 @@ -package caddy - -import ( - "net/http" - "strings" - - "github.com/caddyserver/caddy/v2" -) - -type responseWriterInterceptor struct { - http.ResponseWriter - replacer *caddy.Replacer - wroteHeader bool -} - -func (i *responseWriterInterceptor) WriteHeader(statusCode int) { - if !i.wroteHeader { - if i.replacer != nil { - i.replacer.Set("http.frankenphp.status_code", statusCode) - i.replacer.Set("http.frankenphp.status_text", http.StatusText(statusCode)) - - for key, values := range i.Header() { - i.replacer.Set("http.frankenphp.header."+key, strings.Join(values, ",")) - } - } - - i.wroteHeader = true - } - - i.ResponseWriter.WriteHeader(statusCode) -} - -func (i *responseWriterInterceptor) Write(b []byte) (int, error) { - if !i.wroteHeader { - i.WriteHeader(http.StatusOK) - } - - return i.ResponseWriter.Write(b) -} diff --git a/caddy/module.go b/caddy/module.go index 16e5552693..87094bc6a6 100644 --- a/caddy/module.go +++ b/caddy/module.go @@ -194,12 +194,16 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c frankenphp.WithWorkerName(workerName), ) - interceptor := &responseWriterInterceptor{ResponseWriter: w, replacer: repl} - - if err = frankenphp.ServeHTTP(interceptor, fr); err != nil { + if err = frankenphp.ServeHTTP(w, fr); err != nil { return caddyhttp.Error(http.StatusInternalServerError, err) } + placeholders := fr.Context().Value(frankenphp.PlaceholdersContextKey).(map[string]string) + + for k, v := range placeholders { + repl.Set(k, v) + } + return nil } diff --git a/context.go b/context.go index 2e897cd5c1..1a592bf605 100644 --- a/context.go +++ b/context.go @@ -36,6 +36,9 @@ type frankenPHPContext struct { startedAt time.Time } +type placeholdersContextKeyStruct struct{} +var PlaceholdersContextKey = placeholdersContextKeyStruct{} + // fromContext extracts the frankenPHPContext from a context. func fromContext(ctx context.Context) (fctx *frankenPHPContext, ok bool) { fctx, ok = ctx.Value(contextKey).(*frankenPHPContext) @@ -80,8 +83,11 @@ func NewRequestWithContext(r *http.Request, opts ...RequestOption) (*http.Reques } c := context.WithValue(r.Context(), contextKey, fc) + c = context.WithValue(c, PlaceholdersContextKey, make(map[string]string)) + r = r.WithContext(c) + fc.request = r - return r.WithContext(c), nil + return r, nil } // newDummyContext creates a fake context from a request path diff --git a/docs/config.md b/docs/config.md index 9d2260d587..412fba7161 100644 --- a/docs/config.md +++ b/docs/config.md @@ -208,11 +208,13 @@ The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher) ### Placeholders -FrankenPHP exposes some placeholders that can be used in Caddy directives like `log_append`. +You can set Caddy placeholders from your PHP code using the `frankenphp_set_caddy_placeholder(string $key, string $value)` function. +These placeholders can then be used in Caddy directives like `log_append`. -- `{http.frankenphp.status_code}`: the HTTP status code of the response -- `{http.frankenphp.status_text}`: the HTTP status text of the response (e.g. "OK", "Not Found", etc.) -- `{http.frankenphp.header.*}`: any HTTP header returned by the worker, e.g. `{http.frankenphp.header.X-User-Id}` +Example usage: +```php +frankenphp_set_caddy_placeholder('frankenphp.custom_placeholder', 'Look at my placeholder!'); +``` ```caddyfile http:// { @@ -220,7 +222,7 @@ http:// { route { # ... - log_append user {http.frankenphp.header.X-User-Id} + log_append my_placeholder {frankenphp.custom_placeholder} } } ``` diff --git a/frankenphp.c b/frankenphp.c index 6b84380430..135672863c 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -342,6 +342,18 @@ PHP_FUNCTION(frankenphp_getenv) { } } /* }}} */ +/* {{{ Set a placeholder on the current Caddy request */ +PHP_FUNCTION(frankenphp_set_caddy_placeholder) { + zend_string *key, *value; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(key) + Z_PARAM_STR(value) + ZEND_PARSE_PARAMETERS_END(); + + go_set_caddy_placeholder(thread_index, ZSTR_VAL(key), ZSTR_VAL(value)); +} /* }}} */ + /* {{{ Fetch all HTTP request headers */ PHP_FUNCTION(frankenphp_request_headers) { ZEND_PARSE_PARAMETERS_NONE(); diff --git a/frankenphp.go b/frankenphp.go index 78d25308c3..868ec87519 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -565,6 +565,21 @@ func go_log(message *C.char, level C.int) { } } +//export go_set_caddy_placeholder +func go_set_caddy_placeholder(threadIndex C.uintptr_t, key *C.char, value *C.char) { + fc := phpThreads[threadIndex].getRequestContext() + + placeholders, _ := fc.request.Context().Value(PlaceholdersContextKey).(map[string]string) + + if placeholders == nil { + logger.LogAttrs(context.Background(), slog.LevelDebug, "frankenphp_set_caddy_placeholder() called in non-HTTP context", slog.String("worker", fc.scriptFilename)) + + return + } + + placeholders[C.GoString(key)] = C.GoString(value) +} + //export go_is_context_done func go_is_context_done(threadIndex C.uintptr_t) C.bool { return C.bool(phpThreads[threadIndex].getRequestContext().isDone) diff --git a/frankenphp.h b/frankenphp.h index c17df6061a..d72e2aa991 100644 --- a/frankenphp.h +++ b/frankenphp.h @@ -56,6 +56,9 @@ int frankenphp_execute_script(char *file_name); int frankenphp_execute_script_cli(char *script, int argc, char **argv, bool eval); +void frankenphp_set_caddy_placeholder(uintptr_t thread_index, zend_string *key, + zend_string *value); + void frankenphp_register_variables_from_request_info( zval *track_vars_array, zend_string *content_type, zend_string *path_translated, zend_string *query_string, diff --git a/frankenphp.stub.php b/frankenphp.stub.php index 6c5a71cb5c..b0f109d575 100644 --- a/frankenphp.stub.php +++ b/frankenphp.stub.php @@ -32,3 +32,4 @@ function frankenphp_response_headers(): array|bool {} */ function apache_response_headers(): array|bool {} +function frankenphp_set_caddy_placeholder(string $key, string $value): void {} diff --git a/frankenphp_arginfo.h b/frankenphp_arginfo.h index c1bd7b550a..9da8e63c12 100644 --- a/frankenphp_arginfo.h +++ b/frankenphp_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 05ebde17137c559e891362fba6524fad1e0a2dfe */ + * Stub hash: c5318079b1c5629258a1f4b682c7aaf327588b71 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1, _IS_BOOL, 0) @@ -30,11 +30,17 @@ ZEND_END_ARG_INFO() #define arginfo_apache_response_headers arginfo_frankenphp_response_headers +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_set_caddy_placeholder, 0, 2, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, value, IS_STRING, 0) +ZEND_END_ARG_INFO() + ZEND_FUNCTION(frankenphp_handle_request); ZEND_FUNCTION(headers_send); ZEND_FUNCTION(frankenphp_finish_request); ZEND_FUNCTION(frankenphp_request_headers); ZEND_FUNCTION(frankenphp_response_headers); +ZEND_FUNCTION(frankenphp_set_caddy_placeholder); // clang-format off static const zend_function_entry ext_functions[] = { @@ -47,6 +53,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FALIAS(getallheaders, frankenphp_request_headers, arginfo_getallheaders) ZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers) ZEND_FALIAS(apache_response_headers, frankenphp_response_headers, arginfo_apache_response_headers) + ZEND_FE(frankenphp_set_caddy_placeholder, arginfo_frankenphp_set_caddy_placeholder) ZEND_FE_END }; // clang-format on From 5812bcd83e8e641bd4f4e2031bca878603c055bf Mon Sep 17 00:00:00 2001 From: Louis C <1530720+Starfox64@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:29:55 +0200 Subject: [PATCH 4/5] chore: Fix linter errors --- docs/config.md | 1 + frankenphp.c | 12 ++++++------ frankenphp_arginfo.h | 7 ++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/config.md b/docs/config.md index 412fba7161..8ade842244 100644 --- a/docs/config.md +++ b/docs/config.md @@ -212,6 +212,7 @@ You can set Caddy placeholders from your PHP code using the `frankenphp_set_cadd These placeholders can then be used in Caddy directives like `log_append`. Example usage: + ```php frankenphp_set_caddy_placeholder('frankenphp.custom_placeholder', 'Look at my placeholder!'); ``` diff --git a/frankenphp.c b/frankenphp.c index 135672863c..4b1f795c0f 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -344,14 +344,14 @@ PHP_FUNCTION(frankenphp_getenv) { /* {{{ Set a placeholder on the current Caddy request */ PHP_FUNCTION(frankenphp_set_caddy_placeholder) { - zend_string *key, *value; + zend_string *key, *value; - ZEND_PARSE_PARAMETERS_START(2, 2) - Z_PARAM_STR(key) - Z_PARAM_STR(value) - ZEND_PARSE_PARAMETERS_END(); + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(key) + Z_PARAM_STR(value) + ZEND_PARSE_PARAMETERS_END(); - go_set_caddy_placeholder(thread_index, ZSTR_VAL(key), ZSTR_VAL(value)); + go_set_caddy_placeholder(thread_index, ZSTR_VAL(key), ZSTR_VAL(value)); } /* }}} */ /* {{{ Fetch all HTTP request headers */ diff --git a/frankenphp_arginfo.h b/frankenphp_arginfo.h index 9da8e63c12..8126b36abb 100644 --- a/frankenphp_arginfo.h +++ b/frankenphp_arginfo.h @@ -30,9 +30,10 @@ ZEND_END_ARG_INFO() #define arginfo_apache_response_headers arginfo_frankenphp_response_headers -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_set_caddy_placeholder, 0, 2, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, value, IS_STRING, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX( + arginfo_frankenphp_set_caddy_placeholder, 0, 2, IS_VOID, 0) +ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) +ZEND_ARG_TYPE_INFO(0, value, IS_STRING, 0) ZEND_END_ARG_INFO() ZEND_FUNCTION(frankenphp_handle_request); From 5da88ecface71caa8aa4fa0a13ca170af86cf29f Mon Sep 17 00:00:00 2001 From: Louis C <1530720+Starfox64@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:49:41 +0200 Subject: [PATCH 5/5] fix: Correct indentation in frankenphp_set_caddy_placeholder function --- frankenphp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frankenphp.c b/frankenphp.c index 4b1f795c0f..f4c05e49ab 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -347,8 +347,8 @@ PHP_FUNCTION(frankenphp_set_caddy_placeholder) { zend_string *key, *value; ZEND_PARSE_PARAMETERS_START(2, 2) - Z_PARAM_STR(key) - Z_PARAM_STR(value) + Z_PARAM_STR(key) + Z_PARAM_STR(value) ZEND_PARSE_PARAMETERS_END(); go_set_caddy_placeholder(thread_index, ZSTR_VAL(key), ZSTR_VAL(value));