Skip to content

Commit 99e63c2

Browse files
feat(extgen): add support for callable in parameters
1 parent 8175ae7 commit 99e63c2

14 files changed

Lines changed: 571 additions & 64 deletions

docs/extensions.md

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,18 @@ While the first point speaks for itself, the second may be harder to apprehend.
8181

8282
While some variable types have the same memory representation between C/PHP and Go, some types require more logic to be directly used. This is maybe the hardest part when it comes to writing extensions because it requires understanding internals of the Zend Engine and how variables are stored internally in PHP. This table summarizes what you need to know:
8383

84-
| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support |
85-
|--------------------|---------------------|-------------------|-----------------------|------------------------|-----------------------|
86-
| `int` | `int64` || - | - ||
87-
| `?int` | `*int64` || - | - ||
88-
| `float` | `float64` || - | - ||
89-
| `?float` | `*float64` || - | - ||
90-
| `bool` | `bool` || - | - ||
91-
| `?bool` | `*bool` || - | - ||
92-
| `string`/`?string` | `*C.zend_string` || frankenphp.GoString() | frankenphp.PHPString() ||
93-
| `array` | `*frankenphp.Array` || frankenphp.GoArray() | frankenphp.PHPArray() ||
94-
| `object` | `struct` || _Not yet implemented_ | _Not yet implemented_ ||
84+
| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support |
85+
|--------------------|---------------------|-------------------|-----------------------|------------------------------|-----------------------|
86+
| `int` | `int64` || - | - ||
87+
| `?int` | `*int64` || - | - ||
88+
| `float` | `float64` || - | - ||
89+
| `?float` | `*float64` || - | - ||
90+
| `bool` | `bool` || - | - ||
91+
| `?bool` | `*bool` || - | - ||
92+
| `string`/`?string` | `*C.zend_string` || frankenphp.GoString() | frankenphp.PHPString() ||
93+
| `array` | `*frankenphp.Array` || frankenphp.GoArray() | frankenphp.PHPArray() ||
94+
| `callable` | `*C.zval` || - | frankenphp.CallPHPCallable() ||
95+
| `object` | - || _Not yet implemented_ | _Not yet implemented_ ||
9596

9697
> [!NOTE]
9798
> This table is not exhaustive yet and will be completed as the FrankenPHP types API gets more complete.
@@ -152,6 +153,54 @@ func process_data(arr *C.zval) unsafe.Pointer {
152153
* `At(index uint32) (PHPKey, interface{})` - Get key-value pair at index
153154
* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convert to PHP array
154155

156+
### Working with Callables
157+
158+
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.
159+
160+
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:
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+
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:
185+
186+
```php
187+
<?php
188+
189+
$strArray = ['a' => 'hello', 'b' => 'world', 'c' => 'php'];
190+
$result = my_array_map($strArray, 'strtoupper'); // $result will be ['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 will be [3, 6, 9, 12, [10, 12]]
202+
```
203+
155204
### Declaring a Native PHP Class
156205

157206
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
@@ -152,6 +152,54 @@ func process_data(arr *C.zval) unsafe.Pointer {
152152
* `At(index uint32) (PHPKey, interface{})` - Obtenir la paire clé-valeur à l'index
153153
* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convertir vers un tableau PHP
154154

155+
### Travailler avec des Callables
156+
157+
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.
158+
159+
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 :
160+
161+
```go
162+
// export_php:function my_array_map(array $data, callable $callback): array
163+
func my_array_map(arr *C.zval, callback *C.zval) unsafe.Pointer {
164+
goArr := frankenphp.GoArray(unsafe.Pointer(arr))
165+
result := &frankenphp.Array{}
166+
167+
for i := uint32(0); i < goArr.Len(); i++ {
168+
key, value := goArr.At(i)
169+
170+
callbackResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
171+
172+
if key.Type == frankenphp.PHPIntKey {
173+
result.SetInt(key.Int, callbackResult)
174+
} else {
175+
result.SetString(key.Str, callbackResult)
176+
}
177+
}
178+
179+
return frankenphp.PHPArray(result)
180+
}
181+
```
182+
183+
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_ :
184+
185+
```php
186+
<?php
187+
188+
$strArray = ['a' => 'hello', 'b' => 'world', 'c' => 'php'];
189+
$result = my_array_map($strArray, 'strtoupper'); // $result vaudra ['a' => 'HELLO', 'b' => 'WORLD', 'c' => 'PHP']
190+
191+
$arr = [1, 2, 3, 4, [5, 6]];
192+
$result = my_array_map($arr, function($item) {
193+
if (\is_array($item)) {
194+
return my_array_map($item, function($subItem) {
195+
return $subItem * 2;
196+
});
197+
}
198+
199+
return $item * 3;
200+
}); // $result vaudra [3, 6, 9, 12, [10, 12]]
201+
```
202+
155203
### Déclarer une Classe PHP Native
156204

157205
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: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,14 @@ type GoParameter struct {
104104

105105
func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string {
106106
typeMap := map[phpType]string{
107-
phpString: "string",
108-
phpInt: "int64",
109-
phpFloat: "float64",
110-
phpBool: "bool",
111-
phpArray: "*frankenphp.Array",
112-
phpMixed: "interface{}",
113-
phpVoid: "",
107+
phpString: "string",
108+
phpInt: "int64",
109+
phpFloat: "float64",
110+
phpBool: "bool",
111+
phpArray: "*frankenphp.Array",
112+
phpMixed: "interface{}",
113+
phpVoid: "",
114+
phpCallable: "*C.zval",
114115
}
115116

116117
if goType, exists := typeMap[phpT]; exists {

internal/extgen/gofile_test.go

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

716+
func TestGoFileGenerator_MethodWrapperWithCallableParams(t *testing.T) {
717+
tmpDir := t.TempDir()
718+
719+
sourceContent := `package main
720+
721+
import "C"
722+
723+
//export_php:class CallableClass
724+
type CallableStruct struct{}
725+
726+
//export_php:method CallableClass::processCallback(callable $callback): string
727+
func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {
728+
return "processed"
729+
}
730+
731+
//export_php:method CallableClass::processOptionalCallback(?callable $callback): string
732+
func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
733+
return "processed_optional"
734+
}`
735+
736+
sourceFile := filepath.Join(tmpDir, "test.go")
737+
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
738+
739+
methods := []phpClassMethod{
740+
{
741+
Name: "ProcessCallback",
742+
PhpName: "processCallback",
743+
ClassName: "CallableClass",
744+
Signature: "processCallback(callable $callback): string",
745+
ReturnType: phpString,
746+
Params: []phpParameter{
747+
{Name: "callback", PhpType: phpCallable, IsNullable: false},
748+
},
749+
GoFunction: `func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {
750+
return "processed"
751+
}`,
752+
},
753+
{
754+
Name: "ProcessOptionalCallback",
755+
PhpName: "processOptionalCallback",
756+
ClassName: "CallableClass",
757+
Signature: "processOptionalCallback(?callable $callback): string",
758+
ReturnType: phpString,
759+
Params: []phpParameter{
760+
{Name: "callback", PhpType: phpCallable, IsNullable: true},
761+
},
762+
GoFunction: `func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
763+
return "processed_optional"
764+
}`,
765+
},
766+
}
767+
768+
classes := []phpClass{
769+
{
770+
Name: "CallableClass",
771+
GoStruct: "CallableStruct",
772+
Methods: methods,
773+
},
774+
}
775+
776+
generator := &Generator{
777+
BaseName: "callable_test",
778+
SourceFile: sourceFile,
779+
Classes: classes,
780+
BuildDir: tmpDir,
781+
}
782+
783+
goGen := GoFileGenerator{generator}
784+
content, err := goGen.buildContent()
785+
require.NoError(t, err)
786+
787+
expectedCallableWrapperSignature := "func ProcessCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer"
788+
assert.Contains(t, content, expectedCallableWrapperSignature, "Generated content should contain callable wrapper signature: %s", expectedCallableWrapperSignature)
789+
790+
expectedOptionalCallableWrapperSignature := "func ProcessOptionalCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer"
791+
assert.Contains(t, content, expectedOptionalCallableWrapperSignature, "Generated content should contain optional callable wrapper signature: %s", expectedOptionalCallableWrapperSignature)
792+
793+
expectedCallableCall := "structObj.ProcessCallback(callback)"
794+
assert.Contains(t, content, expectedCallableCall, "Generated content should contain callable method call: %s", expectedCallableCall)
795+
796+
expectedOptionalCallableCall := "structObj.ProcessOptionalCallback(callback)"
797+
assert.Contains(t, content, expectedOptionalCallableCall, "Generated content should contain optional callable method call: %s", expectedOptionalCallableCall)
798+
799+
assert.Contains(t, content, "//export ProcessCallback_wrapper", "Generated content should contain ProcessCallback export directive")
800+
assert.Contains(t, content, "//export ProcessOptionalCallback_wrapper", "Generated content should contain ProcessOptionalCallback export directive")
801+
}
802+
803+
func TestGoFileGenerator_phpTypeToGoType(t *testing.T) {
804+
generator := &Generator{}
805+
goGen := GoFileGenerator{generator}
806+
807+
tests := []struct {
808+
phpType phpType
809+
expected string
810+
}{
811+
{phpString, "string"},
812+
{phpInt, "int64"},
813+
{phpFloat, "float64"},
814+
{phpBool, "bool"},
815+
{phpArray, "*frankenphp.Array"},
816+
{phpMixed, "interface{}"},
817+
{phpVoid, ""},
818+
{phpCallable, "*C.zval"},
819+
}
820+
821+
for _, tt := range tests {
822+
t.Run(string(tt.phpType), func(t *testing.T) {
823+
result := goGen.phpTypeToGoType(tt.phpType)
824+
assert.Equal(t, tt.expected, result, "phpTypeToGoType(%s) should return %s", tt.phpType, tt.expected)
825+
})
826+
}
827+
828+
t.Run("unknown_type", func(t *testing.T) {
829+
unknownType := phpType("unknown")
830+
result := goGen.phpTypeToGoType(unknownType)
831+
assert.Equal(t, "interface{}", result, "phpTypeToGoType should fallback to interface{} for unknown types")
832+
})
833+
}
834+
716835
func testGoFileInternalFunctions(t *testing.T, content string) {
717836
internalIndicators := []string{
718837
"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: 10 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:
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
@@ -119,6 +121,8 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
119121
return fmt.Sprintf("\n Z_PARAM_BOOL_OR_NULL(%s, %s_is_null)", param.Name, param.Name)
120122
case phpArray:
121123
return fmt.Sprintf("\n Z_PARAM_ARRAY_OR_NULL(%s)", param.Name)
124+
case "callable":
125+
return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s_callback)", param.Name)
122126
default:
123127
return ""
124128
}
@@ -134,6 +138,8 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
134138
return fmt.Sprintf("\n Z_PARAM_BOOL(%s)", param.Name)
135139
case phpArray:
136140
return fmt.Sprintf("\n Z_PARAM_ARRAY(%s)", param.Name)
141+
case "callable":
142+
return fmt.Sprintf("\n Z_PARAM_ZVAL(%s_callback)", param.Name)
137143
default:
138144
return ""
139145
}
@@ -166,6 +172,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
166172
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
167173
case phpArray:
168174
return param.Name
175+
case "callable":
176+
return fmt.Sprintf("%s_callback", param.Name)
169177
default:
170178
return param.Name
171179
}
@@ -181,6 +189,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
181189
return fmt.Sprintf("(int) %s", param.Name)
182190
case phpArray:
183191
return param.Name
192+
case "callable":
193+
return fmt.Sprintf("%s_callback", param.Name)
184194
default:
185195
return param.Name
186196
}

0 commit comments

Comments
 (0)