Skip to content

Commit 71440c2

Browse files
feat(extgen): add support for callable in parameters
1 parent 724c0b1 commit 71440c2

File tree

14 files changed

+545
-46
lines changed

14 files changed

+545
-46
lines changed

docs/extensions.md

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,20 @@ While some variable types have the same memory representation between C/PHP and
8888
This table summarizes what you need to know:
8989

9090
| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support |
91-
| ------------------ | ----------------------------- | ----------------- | --------------------------------- | ---------------------------------- | --------------------- |
92-
| `int` | `int64` || - | - ||
93-
| `?int` | `*int64` || - | - ||
94-
| `float` | `float64` || - | - ||
95-
| `?float` | `*float64` || - | - ||
96-
| `bool` | `bool` || - | - ||
97-
| `?bool` | `*bool` || - | - ||
98-
| `string`/`?string` | `*C.zend_string` || `frankenphp.GoString()` | `frankenphp.PHPString()` ||
99-
| `array` | `frankenphp.AssociativeArray` || `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` ||
100-
| `array` | `map[string]any` || `frankenphp.GoMap()` | `frankenphp.PHPMap()` ||
101-
| `array` | `[]any` || `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` ||
102-
| `mixed` | `any` || `GoValue()` | `PHPValue()` ||
103-
| `object` | `struct` || _Not yet implemented_ | _Not yet implemented_ ||
91+
|--------------------|-------------------------------|-------------------|-----------------------------------|------------------------------------|-----------------------|
92+
| `int` | `int64` || - | - ||
93+
| `?int` | `*int64` || - | - ||
94+
| `float` | `float64` || - | - ||
95+
| `?float` | `*float64` || - | - ||
96+
| `bool` | `bool` || - | - ||
97+
| `?bool` | `*bool` || - | - ||
98+
| `string`/`?string` | `*C.zend_string` || `frankenphp.GoString()` | `frankenphp.PHPString()` ||
99+
| `array` | `frankenphp.AssociativeArray` || `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` ||
100+
| `array` | `map[string]any` || `frankenphp.GoMap()` | `frankenphp.PHPMap()` ||
101+
| `array` | `[]any` || `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` ||
102+
| `mixed` | `any` || `GoValue()` | `PHPValue()` ||
103+
| `callable` | `*C.zval` || - | frankenphp.CallPHPCallable() ||
104+
| `object` | `struct` || _Not yet implemented_ | _Not yet implemented_ ||
104105

105106
> [!NOTE]
106107
>
@@ -212,6 +213,54 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {
212213
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered Go map
213214
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a Go slice
214215

216+
### Working with Callables
217+
218+
FrankenPHP provides a way to work with PHP callables using the `frankenphp.CallPHPCallable` helper. This allows you to call PHP functions or methods from Go code.
219+
220+
To showcase this, let's create our own `array_map()` function that takes a callable and an array, applies the callable to each element of the array, and returns a new array with the results:
221+
222+
```go
223+
// export_php:function my_array_map(array $data, callable $callback): array
224+
func my_array_map(arr *C.zval, callback *C.zval) unsafe.Pointer {
225+
goArr := frankenphp.GoArray(unsafe.Pointer(arr))
226+
result := &frankenphp.Array{}
227+
228+
for i := uint32(0); i < goArr.Len(); i++ {
229+
key, value := goArr.At(i)
230+
231+
callbackResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
232+
233+
if key.Type == frankenphp.PHPIntKey {
234+
result.SetInt(key.Int, callbackResult)
235+
} else {
236+
result.SetString(key.Str, callbackResult)
237+
}
238+
}
239+
240+
return frankenphp.PHPArray(result)
241+
}
242+
```
243+
244+
Notice how we use `frankenphp.CallPHPCallable()` to call the PHP callable passed as a parameter. This function takes a pointer to the callable and an array of arguments, and it returns the result of the callable execution. You can use the callable syntax you're used to:
245+
246+
```php
247+
<?php
248+
249+
$strArray = ['a' => 'hello', 'b' => 'world', 'c' => 'php'];
250+
$result = my_array_map($strArray, 'strtoupper'); // $result will be ['a' => 'HELLO', 'b' => 'WORLD', 'c' => 'PHP']
251+
252+
$arr = [1, 2, 3, 4, [5, 6]];
253+
$result = my_array_map($arr, function($item) {
254+
if (\is_array($item)) {
255+
return my_array_map($item, function($subItem) {
256+
return $subItem * 2;
257+
});
258+
}
259+
260+
return $item * 3;
261+
}); // $result will be [3, 6, 9, 12, [10, 12]]
262+
```
263+
215264
### Declaring a Native PHP Class
216265

217266
The generator supports declaring **opaque classes** as Go structs, which can be used to create PHP objects. You can use the `//export_php:class` directive comment to define a PHP class. For example:

docs/fr/extensions.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,54 @@ func process_data(arr *C.zval) unsafe.Pointer {
153153
- `At(index uint32) (PHPKey, any)` - Obtenir la paire clé-valeur à l'index
154154
- `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convertir vers un tableau PHP
155155

156+
### Travailler avec des Callables
157+
158+
FrankenPHP propose un moyen de travailler avec les _callables_ PHP grâce au helper `frankenphp.CallPHPCallable()`. Cela permet d’appeler des fonctions ou des méthodes PHP depuis du code Go.
159+
160+
Pour illustrer cela, créons notre propre fonction `array_map()` qui prend un _callable_ et un tableau, applique le _callable_ à chaque élément du tableau, et retourne un nouveau tableau avec les résultats :
161+
162+
```go
163+
// export_php:function my_array_map(array $data, callable $callback): array
164+
func my_array_map(arr *C.zval, callback *C.zval) unsafe.Pointer {
165+
goArr := frankenphp.GoArray(unsafe.Pointer(arr))
166+
result := &frankenphp.Array{}
167+
168+
for i := uint32(0); i < goArr.Len(); i++ {
169+
key, value := goArr.At(i)
170+
171+
callbackResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
172+
173+
if key.Type == frankenphp.PHPIntKey {
174+
result.SetInt(key.Int, callbackResult)
175+
} else {
176+
result.SetString(key.Str, callbackResult)
177+
}
178+
}
179+
180+
return frankenphp.PHPArray(result)
181+
}
182+
```
183+
184+
Remarquez comment nous utilisons `frankenphp.CallPHPCallable()` pour appeler le _callable_ PHP passé en paramètre. Cette fonction prend un pointeur vers le _callable_ et un tableau d’arguments, et elle retourne le résultat de l’exécution du _callable_. Vous pouvez utiliser la syntaxe habituelle des _callables_ :
185+
186+
```php
187+
<?php
188+
189+
$strArray = ['a' => 'hello', 'b' => 'world', 'c' => 'php'];
190+
$result = my_array_map($strArray, 'strtoupper'); // $result vaudra ['a' => 'HELLO', 'b' => 'WORLD', 'c' => 'PHP']
191+
192+
$arr = [1, 2, 3, 4, [5, 6]];
193+
$result = my_array_map($arr, function($item) {
194+
if (\is_array($item)) {
195+
return my_array_map($item, function($subItem) {
196+
return $subItem * 2;
197+
});
198+
}
199+
200+
return $item * 3;
201+
}); // $result vaudra [3, 6, 9, 12, [10, 12]]
202+
```
203+
156204
### Déclarer une Classe PHP Native
157205

158206
Le générateur prend en charge la déclaration de **classes opaques** comme structures Go, qui peuvent être utilisées pour créer des objets PHP. Vous pouvez utiliser la directive `//export_php:class` pour définir une classe PHP. Par exemple :

internal/extgen/gofile.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,15 @@ type GoParameter struct {
104104
Type string
105105
}
106106

107-
var phpToGoTypeMap = map[phpType]string{
108-
phpString: "string",
109-
phpInt: "int64",
110-
phpFloat: "float64",
111-
phpBool: "bool",
112-
phpArray: "*frankenphp.Array",
113-
phpMixed: "any",
114-
phpVoid: "",
107+
var phpToGoTypeMap= map[phpType]string{
108+
phpString: "string",
109+
phpInt: "int64",
110+
phpFloat: "float64",
111+
phpBool: "bool",
112+
phpArray: "*frankenphp.Array",
113+
phpMixed: "any",
114+
phpVoid: "",
115+
phpCallable: "*C.zval",
115116
}
116117

117118
func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string {

internal/extgen/gofile_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,125 @@ func testGoFileExportedFunctions(t *testing.T, content string, functions []phpFu
703703
}
704704
}
705705

706+
func TestGoFileGenerator_MethodWrapperWithCallableParams(t *testing.T) {
707+
tmpDir := t.TempDir()
708+
709+
sourceContent := `package main
710+
711+
import "C"
712+
713+
//export_php:class CallableClass
714+
type CallableStruct struct{}
715+
716+
//export_php:method CallableClass::processCallback(callable $callback): string
717+
func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {
718+
return "processed"
719+
}
720+
721+
//export_php:method CallableClass::processOptionalCallback(?callable $callback): string
722+
func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
723+
return "processed_optional"
724+
}`
725+
726+
sourceFile := filepath.Join(tmpDir, "test.go")
727+
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
728+
729+
methods := []phpClassMethod{
730+
{
731+
Name: "ProcessCallback",
732+
PhpName: "processCallback",
733+
ClassName: "CallableClass",
734+
Signature: "processCallback(callable $callback): string",
735+
ReturnType: phpString,
736+
Params: []phpParameter{
737+
{Name: "callback", PhpType: phpCallable, IsNullable: false},
738+
},
739+
GoFunction: `func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {
740+
return "processed"
741+
}`,
742+
},
743+
{
744+
Name: "ProcessOptionalCallback",
745+
PhpName: "processOptionalCallback",
746+
ClassName: "CallableClass",
747+
Signature: "processOptionalCallback(?callable $callback): string",
748+
ReturnType: phpString,
749+
Params: []phpParameter{
750+
{Name: "callback", PhpType: phpCallable, IsNullable: true},
751+
},
752+
GoFunction: `func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
753+
return "processed_optional"
754+
}`,
755+
},
756+
}
757+
758+
classes := []phpClass{
759+
{
760+
Name: "CallableClass",
761+
GoStruct: "CallableStruct",
762+
Methods: methods,
763+
},
764+
}
765+
766+
generator := &Generator{
767+
BaseName: "callable_test",
768+
SourceFile: sourceFile,
769+
Classes: classes,
770+
BuildDir: tmpDir,
771+
}
772+
773+
goGen := GoFileGenerator{generator}
774+
content, err := goGen.buildContent()
775+
require.NoError(t, err)
776+
777+
expectedCallableWrapperSignature := "func ProcessCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer"
778+
assert.Contains(t, content, expectedCallableWrapperSignature, "Generated content should contain callable wrapper signature: %s", expectedCallableWrapperSignature)
779+
780+
expectedOptionalCallableWrapperSignature := "func ProcessOptionalCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer"
781+
assert.Contains(t, content, expectedOptionalCallableWrapperSignature, "Generated content should contain optional callable wrapper signature: %s", expectedOptionalCallableWrapperSignature)
782+
783+
expectedCallableCall := "structObj.ProcessCallback(callback)"
784+
assert.Contains(t, content, expectedCallableCall, "Generated content should contain callable method call: %s", expectedCallableCall)
785+
786+
expectedOptionalCallableCall := "structObj.ProcessOptionalCallback(callback)"
787+
assert.Contains(t, content, expectedOptionalCallableCall, "Generated content should contain optional callable method call: %s", expectedOptionalCallableCall)
788+
789+
assert.Contains(t, content, "//export ProcessCallback_wrapper", "Generated content should contain ProcessCallback export directive")
790+
assert.Contains(t, content, "//export ProcessOptionalCallback_wrapper", "Generated content should contain ProcessOptionalCallback export directive")
791+
}
792+
793+
func TestGoFileGenerator_phpTypeToGoType(t *testing.T) {
794+
generator := &Generator{}
795+
goGen := GoFileGenerator{generator}
796+
797+
tests := []struct {
798+
phpType phpType
799+
expected string
800+
}{
801+
{phpString, "string"},
802+
{phpInt, "int64"},
803+
{phpFloat, "float64"},
804+
{phpBool, "bool"},
805+
{phpArray, "*frankenphp.Array"},
806+
{phpMixed, "any"},
807+
{phpVoid, ""},
808+
{phpCallable, "*C.zval"},
809+
}
810+
811+
for _, tt := range tests {
812+
t.Run(string(tt.phpType), func(t *testing.T) {
813+
result := goGen.phpTypeToGoType(tt.phpType)
814+
assert.Equal(t, tt.expected, result, "phpTypeToGoType(%s) should return %s", tt.phpType, tt.expected)
815+
})
816+
}
817+
818+
t.Run("unknown_type", func(t *testing.T) {
819+
unknownType := phpType("unknown")
820+
result := goGen.phpTypeToGoType(unknownType)
821+
assert.Equal(t, "any", result, "phpTypeToGoType should fallback to interface{} for unknown types")
822+
})
823+
}
824+
706825
func testGoFileInternalFunctions(t *testing.T, content string) {
707826
internalIndicators := []string{
708827
"func internalHelper",

internal/extgen/nodes.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ import (
99
type phpType string
1010

1111
const (
12-
phpString phpType = "string"
13-
phpInt phpType = "int"
14-
phpFloat phpType = "float"
15-
phpBool phpType = "bool"
16-
phpArray phpType = "array"
17-
phpObject phpType = "object"
18-
phpMixed phpType = "mixed"
19-
phpVoid phpType = "void"
20-
phpNull phpType = "null"
21-
phpTrue phpType = "true"
22-
phpFalse phpType = "false"
12+
phpString phpType = "string"
13+
phpInt phpType = "int"
14+
phpFloat phpType = "float"
15+
phpBool phpType = "bool"
16+
phpArray phpType = "array"
17+
phpObject phpType = "object"
18+
phpMixed phpType = "mixed"
19+
phpVoid phpType = "void"
20+
phpNull phpType = "null"
21+
phpTrue phpType = "true"
22+
phpFalse phpType = "false"
23+
phpCallable phpType = "callable"
2324
)
2425

2526
type phpFunction struct {

internal/extgen/paramparser.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ func (pp *ParameterParser) generateSingleParamDeclaration(param phpParameter) []
7070
}
7171
case phpArray, phpMixed:
7272
decls = append(decls, fmt.Sprintf("zval *%s = NULL;", param.Name))
73+
case "callable":
74+
decls = append(decls, fmt.Sprintf("zval *%s_callback;", param.Name))
7375
}
7476

7577
return decls
@@ -121,6 +123,8 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
121123
return fmt.Sprintf("\n Z_PARAM_ARRAY_OR_NULL(%s)", param.Name)
122124
case phpMixed:
123125
return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s)", param.Name)
126+
case phpCallable:
127+
return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s_callback)", param.Name)
124128
default:
125129
return ""
126130
}
@@ -138,6 +142,8 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
138142
return fmt.Sprintf("\n Z_PARAM_ARRAY(%s)", param.Name)
139143
case phpMixed:
140144
return fmt.Sprintf("\n Z_PARAM_ZVAL(%s)", param.Name)
145+
case phpCallable:
146+
return fmt.Sprintf("\n Z_PARAM_ZVAL(%s_callback)", param.Name)
141147
default:
142148
return ""
143149
}
@@ -168,6 +174,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
168174
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
169175
case phpBool:
170176
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
177+
case phpCallable:
178+
return fmt.Sprintf("%s_callback", param.Name)
171179
default:
172180
return param.Name
173181
}

0 commit comments

Comments
 (0)