Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 62 additions & 13 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,20 @@ While some variable types have the same memory representation between C/PHP and
This table summarizes what you need to know:

| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support |
| ------------------ | ----------------------------- | ----------------- | --------------------------------- | ---------------------------------- | --------------------- |
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
|--------------------|-------------------------------|-------------------|-----------------------------------|------------------------------------|-----------------------|
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
| `callable` | `*C.zval` | ❌ | - | frankenphp.CallPHPCallable() | ❌ |
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |

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

### Working with Callables

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.

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:

```go
// export_php:function my_array_map(array $data, callable $callback): array
func my_array_map(arr *C.zval, callback *C.zval) unsafe.Pointer {
goArr := frankenphp.GoArray(unsafe.Pointer(arr))
result := &frankenphp.Array{}

for i := uint32(0); i < goArr.Len(); i++ {
key, value := goArr.At(i)

callbackResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})

if key.Type == frankenphp.PHPIntKey {
result.SetInt(key.Int, callbackResult)
} else {
result.SetString(key.Str, callbackResult)
}
}

return frankenphp.PHPArray(result)
}
```

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:

```php
<?php

$strArray = ['a' => 'hello', 'b' => 'world', 'c' => 'php'];
$result = my_array_map($strArray, 'strtoupper'); // $result will be ['a' => 'HELLO', 'b' => 'WORLD', 'c' => 'PHP']

$arr = [1, 2, 3, 4, [5, 6]];
$result = my_array_map($arr, function($item) {
if (\is_array($item)) {
return my_array_map($item, function($subItem) {
return $subItem * 2;
});
}

return $item * 3;
}); // $result will be [3, 6, 9, 12, [10, 12]]
```

### Declaring a Native PHP Class

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:
Expand Down
48 changes: 48 additions & 0 deletions docs/fr/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,54 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convertir un tableau PHP vers une map Go non ordonnée
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convertir un tableau PHP vers un slice Go

### Travailler avec des Callables

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.

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 :

```go
// export_php:function my_array_map(array $data, callable $callback): array
func my_array_map(arr *C.zval, callback *C.zval) unsafe.Pointer {
goArr := frankenphp.GoArray(unsafe.Pointer(arr))
result := &frankenphp.Array{}

for i := uint32(0); i < goArr.Len(); i++ {
key, value := goArr.At(i)

callbackResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})

if key.Type == frankenphp.PHPIntKey {
result.SetInt(key.Int, callbackResult)
} else {
result.SetString(key.Str, callbackResult)
}
}

return frankenphp.PHPArray(result)
}
```

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_ :

```php
<?php

$strArray = ['a' => 'hello', 'b' => 'world', 'c' => 'php'];
$result = my_array_map($strArray, 'strtoupper'); // $result vaudra ['a' => 'HELLO', 'b' => 'WORLD', 'c' => 'PHP']

$arr = [1, 2, 3, 4, [5, 6]];
$result = my_array_map($arr, function($item) {
if (\is_array($item)) {
return my_array_map($item, function($subItem) {
return $subItem * 2;
});
}

return $item * 3;
}); // $result vaudra [3, 6, 9, 12, [10, 12]]
```

### Déclarer une Classe PHP Native

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 :
Expand Down
17 changes: 9 additions & 8 deletions internal/extgen/gofile.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,15 @@ type GoParameter struct {
Type string
}

var phpToGoTypeMap = map[phpType]string{
phpString: "string",
phpInt: "int64",
phpFloat: "float64",
phpBool: "bool",
phpArray: "*frankenphp.Array",
phpMixed: "any",
phpVoid: "",
var phpToGoTypeMap= map[phpType]string{
phpString: "string",
phpInt: "int64",
phpFloat: "float64",
phpBool: "bool",
phpArray: "*frankenphp.Array",
phpMixed: "any",
phpVoid: "",
phpCallable: "*C.zval",
}

func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string {
Expand Down
119 changes: 119 additions & 0 deletions internal/extgen/gofile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,125 @@ func testGoFileExportedFunctions(t *testing.T, content string, functions []phpFu
}
}

func TestGoFileGenerator_MethodWrapperWithCallableParams(t *testing.T) {
tmpDir := t.TempDir()

sourceContent := `package main

import "C"

//export_php:class CallableClass
type CallableStruct struct{}

//export_php:method CallableClass::processCallback(callable $callback): string
func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {
return "processed"
}

//export_php:method CallableClass::processOptionalCallback(?callable $callback): string
func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
return "processed_optional"
}`

sourceFile := filepath.Join(tmpDir, "test.go")
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))

methods := []phpClassMethod{
{
Name: "ProcessCallback",
PhpName: "processCallback",
ClassName: "CallableClass",
Signature: "processCallback(callable $callback): string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "callback", PhpType: phpCallable, IsNullable: false},
},
GoFunction: `func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {
return "processed"
}`,
},
{
Name: "ProcessOptionalCallback",
PhpName: "processOptionalCallback",
ClassName: "CallableClass",
Signature: "processOptionalCallback(?callable $callback): string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "callback", PhpType: phpCallable, IsNullable: true},
},
GoFunction: `func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
return "processed_optional"
}`,
},
}

classes := []phpClass{
{
Name: "CallableClass",
GoStruct: "CallableStruct",
Methods: methods,
},
}

generator := &Generator{
BaseName: "callable_test",
SourceFile: sourceFile,
Classes: classes,
BuildDir: tmpDir,
}

goGen := GoFileGenerator{generator}
content, err := goGen.buildContent()
require.NoError(t, err)

expectedCallableWrapperSignature := "func ProcessCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer"
assert.Contains(t, content, expectedCallableWrapperSignature, "Generated content should contain callable wrapper signature: %s", expectedCallableWrapperSignature)

expectedOptionalCallableWrapperSignature := "func ProcessOptionalCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer"
assert.Contains(t, content, expectedOptionalCallableWrapperSignature, "Generated content should contain optional callable wrapper signature: %s", expectedOptionalCallableWrapperSignature)

expectedCallableCall := "structObj.ProcessCallback(callback)"
assert.Contains(t, content, expectedCallableCall, "Generated content should contain callable method call: %s", expectedCallableCall)

expectedOptionalCallableCall := "structObj.ProcessOptionalCallback(callback)"
assert.Contains(t, content, expectedOptionalCallableCall, "Generated content should contain optional callable method call: %s", expectedOptionalCallableCall)

assert.Contains(t, content, "//export ProcessCallback_wrapper", "Generated content should contain ProcessCallback export directive")
assert.Contains(t, content, "//export ProcessOptionalCallback_wrapper", "Generated content should contain ProcessOptionalCallback export directive")
}

func TestGoFileGenerator_phpTypeToGoType(t *testing.T) {
generator := &Generator{}
goGen := GoFileGenerator{generator}

tests := []struct {
phpType phpType
expected string
}{
{phpString, "string"},
{phpInt, "int64"},
{phpFloat, "float64"},
{phpBool, "bool"},
{phpArray, "*frankenphp.Array"},
{phpMixed, "any"},
{phpVoid, ""},
{phpCallable, "*C.zval"},
}

for _, tt := range tests {
t.Run(string(tt.phpType), func(t *testing.T) {
result := goGen.phpTypeToGoType(tt.phpType)
assert.Equal(t, tt.expected, result, "phpTypeToGoType(%s) should return %s", tt.phpType, tt.expected)
})
}

t.Run("unknown_type", func(t *testing.T) {
unknownType := phpType("unknown")
result := goGen.phpTypeToGoType(unknownType)
assert.Equal(t, "any", result, "phpTypeToGoType should fallback to interface{} for unknown types")
})
}

func testGoFileInternalFunctions(t *testing.T, content string) {
internalIndicators := []string{
"func internalHelper",
Expand Down
23 changes: 12 additions & 11 deletions internal/extgen/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@ import (
type phpType string

const (
phpString phpType = "string"
phpInt phpType = "int"
phpFloat phpType = "float"
phpBool phpType = "bool"
phpArray phpType = "array"
phpObject phpType = "object"
phpMixed phpType = "mixed"
phpVoid phpType = "void"
phpNull phpType = "null"
phpTrue phpType = "true"
phpFalse phpType = "false"
phpString phpType = "string"
phpInt phpType = "int"
phpFloat phpType = "float"
phpBool phpType = "bool"
phpArray phpType = "array"
phpObject phpType = "object"
phpMixed phpType = "mixed"
phpVoid phpType = "void"
phpNull phpType = "null"
phpTrue phpType = "true"
phpFalse phpType = "false"
phpCallable phpType = "callable"
)

type phpFunction struct {
Expand Down
10 changes: 10 additions & 0 deletions internal/extgen/paramparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ func (pp *ParameterParser) generateSingleParamDeclaration(param phpParameter) []
}
case phpArray, phpMixed:
decls = append(decls, fmt.Sprintf("zval *%s = NULL;", param.Name))
case "callable":
decls = append(decls, fmt.Sprintf("zval *%s_callback;", param.Name))
}

return decls
Expand Down Expand Up @@ -121,6 +123,8 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
return fmt.Sprintf("\n Z_PARAM_ARRAY_OR_NULL(%s)", param.Name)
case phpMixed:
return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s)", param.Name)
case phpCallable:
return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s_callback)", param.Name)
default:
return ""
}
Expand All @@ -138,6 +142,8 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
return fmt.Sprintf("\n Z_PARAM_ARRAY(%s)", param.Name)
case phpMixed:
return fmt.Sprintf("\n Z_PARAM_ZVAL(%s)", param.Name)
case phpCallable:
return fmt.Sprintf("\n Z_PARAM_ZVAL(%s_callback)", param.Name)
default:
return ""
}
Expand Down Expand Up @@ -168,6 +174,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
case phpBool:
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
case phpCallable:
return fmt.Sprintf("%s_callback", param.Name)
default:
return param.Name
}
Expand All @@ -180,6 +188,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
return fmt.Sprintf("(double) %s", param.Name)
case phpBool:
return fmt.Sprintf("(int) %s", param.Name)
case phpCallable:
return fmt.Sprintf("%s_callback", param.Name)
default:
return param.Name
}
Expand Down
Loading
Loading