Skip to content

Commit 7e7a22b

Browse files
feat(extgen): add support for callable in parameters
1 parent 50b438f commit 7e7a22b

File tree

11 files changed

+443
-56
lines changed

11 files changed

+443
-56
lines changed

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/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
}

internal/extgen/paramparser_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,29 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
163163
},
164164
expected: " zend_string *name = NULL;\n zval *items = NULL;\n zend_long count = 5;",
165165
},
166+
{
167+
name: "callable parameter",
168+
params: []phpParameter{
169+
{Name: "callback", PhpType: "callable", HasDefault: false},
170+
},
171+
expected: " zval *callback_callback;",
172+
},
173+
{
174+
name: "nullable callable parameter",
175+
params: []phpParameter{
176+
{Name: "callback", PhpType: "callable", HasDefault: false, IsNullable: true},
177+
},
178+
expected: " zval *callback_callback;",
179+
},
180+
{
181+
name: "mixed types with callable",
182+
params: []phpParameter{
183+
{Name: "data", PhpType: "array", HasDefault: false},
184+
{Name: "callback", PhpType: "callable", HasDefault: false},
185+
{Name: "options", PhpType: "int", HasDefault: true, DefaultValue: "0"},
186+
},
187+
expected: " zval *data = NULL;\n zval *callback_callback;\n zend_long options = 0;",
188+
},
166189
}
167190

168191
for _, tt := range tests {
@@ -278,6 +301,29 @@ func TestParameterParser_GenerateGoCallParams(t *testing.T) {
278301
},
279302
expected: "name, items, (long) count",
280303
},
304+
{
305+
name: "callable parameter",
306+
params: []phpParameter{
307+
{Name: "callback", PhpType: "callable"},
308+
},
309+
expected: "callback_callback",
310+
},
311+
{
312+
name: "nullable callable parameter",
313+
params: []phpParameter{
314+
{Name: "callback", PhpType: "callable", IsNullable: true},
315+
},
316+
expected: "callback_callback",
317+
},
318+
{
319+
name: "mixed parameters with callable",
320+
params: []phpParameter{
321+
{Name: "data", PhpType: "array"},
322+
{Name: "callback", PhpType: "callable"},
323+
{Name: "limit", PhpType: "int"},
324+
},
325+
expected: "data, callback_callback, (long) limit",
326+
},
281327
}
282328

283329
for _, tt := range tests {
@@ -346,6 +392,16 @@ func TestParameterParser_GenerateParamParsingMacro(t *testing.T) {
346392
param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true},
347393
expected: "\n Z_PARAM_ARRAY_OR_NULL(items)",
348394
},
395+
{
396+
name: "callable parameter",
397+
param: phpParameter{Name: "callback", PhpType: "callable"},
398+
expected: "\n Z_PARAM_ZVAL(callback_callback)",
399+
},
400+
{
401+
name: "nullable callable parameter",
402+
param: phpParameter{Name: "callback", PhpType: "callable", IsNullable: true},
403+
expected: "\n Z_PARAM_ZVAL_OR_NULL(callback_callback)",
404+
},
349405
{
350406
name: "unknown type",
351407
param: phpParameter{Name: "unknown", PhpType: phpType("unknown")},
@@ -456,6 +512,16 @@ func TestParameterParser_GenerateSingleGoCallParam(t *testing.T) {
456512
param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true},
457513
expected: "items",
458514
},
515+
{
516+
name: "callable parameter",
517+
param: phpParameter{Name: "callback", PhpType: "callable"},
518+
expected: "callback_callback",
519+
},
520+
{
521+
name: "nullable callable parameter",
522+
param: phpParameter{Name: "callback", PhpType: "callable", IsNullable: true},
523+
expected: "callback_callback",
524+
},
459525
{
460526
name: "unknown type",
461527
param: phpParameter{Name: "unknown", PhpType: phpType("unknown")},
@@ -534,6 +600,16 @@ func TestParameterParser_GenerateSingleParamDeclaration(t *testing.T) {
534600
param: phpParameter{Name: "items", PhpType: phpArray, HasDefault: false, IsNullable: true},
535601
expected: []string{"zval *items = NULL;"},
536602
},
603+
{
604+
name: "callable parameter",
605+
param: phpParameter{Name: "callback", PhpType: "callable", HasDefault: false},
606+
expected: []string{"zval *callback_callback;"},
607+
},
608+
{
609+
name: "nullable callable parameter",
610+
param: phpParameter{Name: "callback", PhpType: "callable", HasDefault: false, IsNullable: true},
611+
expected: []string{"zval *callback_callback;"},
612+
},
537613
}
538614

539615
for _, tt := range tests {

0 commit comments

Comments
 (0)