Skip to content

Commit d0b847a

Browse files
committed
feat: mercure_publish() PHP function to dispatch Mercure updates
# Conflicts: # types.go # types_test.go # Conflicts: # caddy/go.mod # caddy/go.sum # docs/mercure.md # go.mod # go.sum
1 parent b49aed1 commit d0b847a

10 files changed

Lines changed: 199 additions & 31 deletions

.clang-format-ignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
frankenphp_arginfo.h

.gitleaksignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
/github/workspace/docs/mercure.md:jwt:65
1+
/github/workspace/docs/mercure.md:jwt:88

caddy/module.go

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
2020
"github.com/dunglas/frankenphp"
2121
"github.com/dunglas/frankenphp/internal/fastabs"
22+
mercureCaddy "github.com/dunglas/mercure/caddy"
2223
)
2324

2425
// FrankenPHPModule represents the "php_server" and "php" directives in the Caddyfile
@@ -45,6 +46,7 @@ type FrankenPHPModule struct {
4546
preparedEnv frankenphp.PreparedEnv
4647
preparedEnvNeedsReplacement bool
4748
logger *slog.Logger
49+
mercureHubRequestOption *frankenphp.RequestOption
4850
}
4951

5052
// CaddyModule returns the Caddy module information.
@@ -142,6 +144,8 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
142144
}
143145
}
144146

147+
f.assignMercureHubRequestOption(ctx)
148+
145149
return nil
146150
}
147151

@@ -184,14 +188,34 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c
184188
}
185189
}
186190

187-
fr, err := frankenphp.NewRequestWithContext(
188-
r,
189-
documentRootOption,
190-
frankenphp.WithRequestSplitPath(f.SplitPath),
191-
frankenphp.WithRequestPreparedEnv(env),
192-
frankenphp.WithOriginalRequest(&origReq),
193-
frankenphp.WithWorkerName(workerName),
191+
var (
192+
err error
193+
fr *http.Request
194194
)
195+
if f.mercureHubRequestOption == nil {
196+
fr, err = frankenphp.NewRequestWithContext(
197+
r,
198+
documentRootOption,
199+
frankenphp.WithRequestSplitPath(f.SplitPath),
200+
frankenphp.WithRequestPreparedEnv(env),
201+
frankenphp.WithOriginalRequest(&origReq),
202+
frankenphp.WithWorkerName(workerName),
203+
)
204+
} else {
205+
fr, err = frankenphp.NewRequestWithContext(
206+
r,
207+
documentRootOption,
208+
frankenphp.WithRequestSplitPath(f.SplitPath),
209+
frankenphp.WithRequestPreparedEnv(env),
210+
frankenphp.WithOriginalRequest(&origReq),
211+
frankenphp.WithWorkerName(workerName),
212+
*f.mercureHubRequestOption,
213+
)
214+
}
215+
216+
if err != nil {
217+
return caddyhttp.Error(http.StatusInternalServerError, err)
218+
}
195219

196220
if err = frankenphp.ServeHTTP(w, fr); err != nil {
197221
return caddyhttp.Error(http.StatusInternalServerError, err)
@@ -272,6 +296,13 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
272296
return nil
273297
}
274298

299+
func (f *FrankenPHPModule) assignMercureHubRequestOption(ctx caddy.Context) {
300+
if hub := mercureCaddy.FindHub(ctx.Modules()); hub != nil {
301+
opt := frankenphp.WithMercureHub(hub)
302+
f.mercureHubRequestOption = &opt
303+
}
304+
}
305+
275306
// parseCaddyfile unmarshals tokens from h into a new Middleware.
276307
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
277308
m := &FrankenPHPModule{}

context.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"strconv"
99
"strings"
1010
"time"
11+
12+
"github.com/dunglas/mercure"
1113
)
1214

1315
// frankenPHPContext provides contextual information about the Request to handle.
@@ -34,6 +36,8 @@ type frankenPHPContext struct {
3436

3537
done chan any
3638
startedAt time.Time
39+
40+
mercureHub *mercure.Hub
3741
}
3842

3943
// fromContext extracts the frankenPHPContext from a context.

docs/mercure.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,29 @@ To subscribe to updates, use the native [`EventSource`](https://developer.mozill
5454

5555
## Publishing Updates
5656

57+
### Using `mercure_publish()`
58+
59+
FrankenPHP provides a convenient `mercure_publish()` function to publish updates to the built-in Mercure hub:
60+
61+
```php
62+
<?php
63+
// public/publish.php
64+
65+
$updateID = mercure_publish('my-topic', json_encode(['key' => 'value']));
66+
67+
// Write to FrankenPHP's logs
68+
error_log("update $updateID published", 4);
69+
```
70+
71+
The full function signature is:
72+
73+
```php
74+
/**
75+
* @param string|string[] $topics
76+
*/
77+
function mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}
78+
```
79+
5780
### Using `file_get_contents()`
5881

5982
To dispatch an update to connected subscribers, send an authenticated POST request to the Mercure hub with the `topic` and `data` parameters:

frankenphp.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,47 @@ PHP_FUNCTION(headers_send) {
509509
RETURN_LONG(sapi_send_headers());
510510
}
511511

512+
PHP_FUNCTION(mercure_publish) {
513+
zval *topics;
514+
zend_string *data = NULL, *id = NULL, *type = NULL;
515+
zend_bool private = 0;
516+
zend_long retry = 0;
517+
518+
ZEND_PARSE_PARAMETERS_START(1, 6)
519+
Z_PARAM_ZVAL(topics)
520+
Z_PARAM_OPTIONAL
521+
Z_PARAM_STR_OR_NULL(data)
522+
Z_PARAM_BOOL(private)
523+
Z_PARAM_STR_OR_NULL(id)
524+
Z_PARAM_STR_OR_NULL(type)
525+
Z_PARAM_LONG(retry)
526+
ZEND_PARSE_PARAMETERS_END();
527+
528+
if (Z_TYPE_P(topics) != IS_ARRAY && Z_TYPE_P(topics) != IS_STRING) {
529+
zend_argument_type_error(1, "must be of type array|string");
530+
RETURN_THROWS();
531+
}
532+
533+
struct go_mercure_publish_return result =
534+
go_mercure_publish(thread_index, topics, data, private, id, type, retry);
535+
536+
switch (result.r1) {
537+
case 0:
538+
RETURN_STR(result.r0);
539+
case 1:
540+
zend_throw_exception(spl_ce_RuntimeException, "No Mercure hub configured",
541+
0);
542+
RETURN_THROWS();
543+
case 2:
544+
zend_throw_exception(spl_ce_RuntimeException, "Publish failed", 1);
545+
RETURN_THROWS();
546+
}
547+
548+
zend_throw_exception(spl_ce_RuntimeException,
549+
"FrankenPHP not built with Mercure support", 1);
550+
RETURN_THROWS();
551+
}
552+
512553
PHP_MINIT_FUNCTION(frankenphp) {
513554
zend_function *func;
514555

frankenphp.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import (
3737
"unsafe"
3838
// debug on Linux
3939
//_ "github.com/ianlancetaylor/cgosymbolizer"
40+
41+
"github.com/dunglas/mercure"
4042
)
4143

4244
type contextKeyStruct struct{}
@@ -591,6 +593,53 @@ func go_is_context_done(threadIndex C.uintptr_t) C.bool {
591593
return C.bool(phpThreads[threadIndex].getRequestContext().isDone)
592594
}
593595

596+
//export go_mercure_publish
597+
func go_mercure_publish(threadIndex C.uintptr_t, topics *C.struct__zval_struct, data unsafe.Pointer, private bool, id, typ unsafe.Pointer, retry uint64) (generatedID *C.zend_string, error C.short) {
598+
fc := phpThreads[threadIndex].getRequestContext()
599+
600+
if fc.mercureHub == nil {
601+
logger.Error("No Mercure hub configured")
602+
603+
return nil, 1
604+
}
605+
606+
u := &mercure.Update{
607+
Event: mercure.Event{
608+
Data: GoString(data),
609+
ID: GoString(id),
610+
Retry: retry,
611+
Type: GoString(typ),
612+
},
613+
Private: private,
614+
}
615+
616+
zvalType := C.zval_get_type(topics)
617+
switch zvalType {
618+
case C.IS_STRING:
619+
u.Topics = []string{GoString(unsafe.Pointer(topics))}
620+
case C.IS_ARRAY:
621+
ts, err := GoPackedArray[string](unsafe.Pointer(topics))
622+
if err != nil {
623+
logger.Error("invalid topics type", slog.String("error", err.Error()))
624+
625+
return nil, 1
626+
}
627+
628+
u.Topics = ts
629+
default:
630+
// Never happens as the function is called from C with proper types
631+
panic("invalid topics type")
632+
}
633+
634+
if err := fc.mercureHub.Publish(u); err != nil {
635+
logger.Error("Unable to publish Mercure update", slog.String("error", err.Error()))
636+
637+
return nil, 2
638+
}
639+
640+
return (*C.zend_string)(PHPString(u.ID, false)), 0
641+
}
642+
594643
// ExecuteScriptCLI executes the PHP script passed as parameter.
595644
// It returns the exit status code of the script.
596645
func ExecuteScriptCLI(script string, args []string) int {

frankenphp.stub.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ function frankenphp_response_headers(): array|bool {}
3232
*/
3333
function apache_response_headers(): array|bool {}
3434

35+
/**
36+
* @param string|string[] $topics
37+
*/
38+
function mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}

frankenphp_arginfo.h

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,57 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: 05ebde17137c559e891362fba6524fad1e0a2dfe */
2+
* Stub hash: fcc86e17663887b089b79144e10ab3ca50ce3faa */
33

4-
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1,
5-
_IS_BOOL, 0)
6-
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
4+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1, _IS_BOOL, 0)
5+
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
76
ZEND_END_ARG_INFO()
87

98
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_headers_send, 0, 0, IS_LONG, 0)
10-
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200")
9+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, status, IS_LONG, 0, "200")
1110
ZEND_END_ARG_INFO()
1211

13-
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0,
14-
_IS_BOOL, 0)
12+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_finish_request, 0, 0, _IS_BOOL, 0)
1513
ZEND_END_ARG_INFO()
1614

1715
#define arginfo_fastcgi_finish_request arginfo_frankenphp_finish_request
1816

19-
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_request_headers, 0,
20-
0, IS_ARRAY, 0)
17+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_request_headers, 0, 0, IS_ARRAY, 0)
2118
ZEND_END_ARG_INFO()
2219

2320
#define arginfo_apache_request_headers arginfo_frankenphp_request_headers
2421

2522
#define arginfo_getallheaders arginfo_frankenphp_request_headers
2623

27-
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_frankenphp_response_headers, 0,
28-
0, MAY_BE_ARRAY | MAY_BE_BOOL)
24+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_frankenphp_response_headers, 0, 0, MAY_BE_ARRAY|MAY_BE_BOOL)
2925
ZEND_END_ARG_INFO()
3026

3127
#define arginfo_apache_response_headers arginfo_frankenphp_response_headers
3228

29+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mercure_publish, 0, 1, IS_STRING, 0)
30+
ZEND_ARG_INFO(0, topics)
31+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_STRING, 0, "\'\'")
32+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, private, _IS_BOOL, 0, "false")
33+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, id, IS_STRING, 1, "null")
34+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, type, IS_STRING, 1, "null")
35+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, retry, IS_LONG, 1, "null")
36+
ZEND_END_ARG_INFO()
37+
3338
ZEND_FUNCTION(frankenphp_handle_request);
3439
ZEND_FUNCTION(headers_send);
3540
ZEND_FUNCTION(frankenphp_finish_request);
3641
ZEND_FUNCTION(frankenphp_request_headers);
3742
ZEND_FUNCTION(frankenphp_response_headers);
43+
ZEND_FUNCTION(mercure_publish);
3844

39-
// clang-format off
4045
static const zend_function_entry ext_functions[] = {
41-
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
42-
ZEND_FE(headers_send, arginfo_headers_send)
43-
ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)
44-
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request)
45-
ZEND_FE(frankenphp_request_headers, arginfo_frankenphp_request_headers)
46-
ZEND_FALIAS(apache_request_headers, frankenphp_request_headers, arginfo_apache_request_headers)
47-
ZEND_FALIAS(getallheaders, frankenphp_request_headers, arginfo_getallheaders)
48-
ZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers)
49-
ZEND_FALIAS(apache_response_headers, frankenphp_response_headers, arginfo_apache_response_headers)
50-
ZEND_FE_END
46+
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
47+
ZEND_FE(headers_send, arginfo_headers_send)
48+
ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)
49+
ZEND_RAW_FENTRY("fastcgi_finish_request", zif_frankenphp_finish_request, arginfo_fastcgi_finish_request, 0, NULL, NULL)
50+
ZEND_FE(frankenphp_request_headers, arginfo_frankenphp_request_headers)
51+
ZEND_RAW_FENTRY("apache_request_headers", zif_frankenphp_request_headers, arginfo_apache_request_headers, 0, NULL, NULL)
52+
ZEND_RAW_FENTRY("getallheaders", zif_frankenphp_request_headers, arginfo_getallheaders, 0, NULL, NULL)
53+
ZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers)
54+
ZEND_RAW_FENTRY("apache_response_headers", zif_frankenphp_response_headers, arginfo_apache_response_headers, 0, NULL, NULL)
55+
ZEND_FE(mercure_publish, arginfo_mercure_publish)
56+
ZEND_FE_END
5157
};
52-
// clang-format on

request_options.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"sync/atomic"
99

1010
"github.com/dunglas/frankenphp/internal/fastabs"
11+
"github.com/dunglas/mercure"
1112
)
1213

1314
// RequestOption instances allow to configure a FrankenPHP Request.
@@ -134,3 +135,12 @@ func WithWorkerName(name string) RequestOption {
134135
return nil
135136
}
136137
}
138+
139+
// WithMercureHub sets the mercure.Hub to use to publish updates
140+
func WithMercureHub(hub *mercure.Hub) RequestOption {
141+
return func(o *frankenPHPContext) error {
142+
o.mercureHub = hub
143+
144+
return nil
145+
}
146+
}

0 commit comments

Comments
 (0)