Skip to content

Commit bb1c367

Browse files
feat(extgen): add support for callable in parameters (#1731)
1 parent 58a6370 commit bb1c367

17 files changed

Lines changed: 743 additions & 77 deletions

docs/extensions.md

Lines changed: 50 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,42 @@ func process_data_packed(arr *C.zend_array) 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.zend_array, callback *C.zval) unsafe.Pointer {
225+
goSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
226+
if err != nil {
227+
panic(err)
228+
}
229+
230+
result := make([]any, len(goSlice))
231+
232+
for index, value := range goSlice {
233+
result[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
234+
}
235+
236+
return frankenphp.PHPPackedArray(result)
237+
}
238+
```
239+
240+
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:
241+
242+
```php
243+
<?php
244+
245+
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
246+
// $result will be [2, 4, 6]
247+
248+
$result = my_array_map(['hello', 'world'], 'strtoupper');
249+
// $result will be ['HELLO', 'WORLD']
250+
```
251+
215252
### Declaring a Native PHP Class
216253

217254
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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,42 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {
210210
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convertir un tableau PHP vers une map Go non ordonnée
211211
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convertir un tableau PHP vers un slice Go
212212

213+
### Travailler avec des Callables
214+
215+
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.
216+
217+
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 :
218+
219+
```go
220+
// export_php:function my_array_map(array $data, callable $callback): array
221+
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
222+
goSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
223+
if err != nil {
224+
panic(err)
225+
}
226+
227+
result := make([]any, len(goSlice))
228+
229+
for index, value := range goSlice {
230+
result[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
231+
}
232+
233+
return frankenphp.PHPPackedArray(result)
234+
}
235+
```
236+
237+
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_ :
238+
239+
```php
240+
<?php
241+
242+
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
243+
// $result vaudra [2, 4, 6]
244+
245+
$result = my_array_map(['hello', 'world'], 'strtoupper');
246+
// $result vaudra ['HELLO', 'WORLD']
247+
```
248+
213249
### Déclarer une Classe PHP Native
214250

215251
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
@@ -128,14 +128,15 @@ type GoParameter struct {
128128
Type string
129129
}
130130

131-
var phpToGoTypeMap = map[phpType]string{
132-
phpString: "string",
133-
phpInt: "int64",
134-
phpFloat: "float64",
135-
phpBool: "bool",
136-
phpArray: "*frankenphp.Array",
137-
phpMixed: "any",
138-
phpVoid: "",
131+
var phpToGoTypeMap= map[phpType]string{
132+
phpString: "string",
133+
phpInt: "int64",
134+
phpFloat: "float64",
135+
phpBool: "bool",
136+
phpArray: "*frankenphp.Array",
137+
phpMixed: "any",
138+
phpVoid: "",
139+
phpCallable: "*C.zval",
139140
}
140141

141142
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",

0 commit comments

Comments
 (0)