feat(extgen): add support for arrays as parameters and return types (#1724)

* feat(extgen): add support for arrays as parameters and return types

* cs

---------

Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
This commit is contained in:
Alexandre Daubois
2025-07-16 12:05:29 +02:00
committed by GitHub
parent 1804e36b93
commit 8df41236d9
26 changed files with 1421 additions and 565 deletions

View File

@@ -81,25 +81,77 @@ While the first point speaks for itself, the second may be harder to apprehend.
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:
| 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` | `slice`/`map` | ❌ | _Not yet implemented_ | _Not yet implemented_ | |
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
| 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.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | |
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
> [!NOTE]
> This table is not exhaustive yet and will be completed as the FrankenPHP types API gets more complete.
>
> For class methods specifically, only primitive types are currently supported. Arrays and objects cannot be used as method parameters or return types yet.
> For class methods specifically, primitive types and arrays are currently supported. Objects cannot be used as method parameters or return types yet.
If you refer to the code snippet of the previous section, you can see that helpers are used to convert the first parameter and the return value. The second and third parameter of our `repeat_this()` function don't need to be converted as memory representation of the underlying types are the same for both C and Go.
#### Working with Arrays
FrankenPHP provides native support for PHP arrays through the `frankenphp.Array` type. This type represents both PHP indexed arrays (lists) and associative arrays (hashmaps) with ordered key-value pairs.
**Creating and manipulating arrays in Go:**
```go
//export_php:function process_data(array $input): array
func process_data(arr *C.zval) unsafe.Pointer {
// Convert PHP array to Go
goArray := frankenphp.GoArray(unsafe.Pointer(arr))
result := &frankenphp.Array{}
result.SetInt(0, "first")
result.SetInt(1, "second")
result.Append("third") // Automatically assigns next integer key
result.SetString("name", "John")
result.SetString("age", int64(30))
for i := uint32(0); i < goArray.Len(); i++ {
key, value := goArray.At(i)
if key.Type == frankenphp.PHPStringKey {
result.SetString("processed_"+key.Str, value)
} else {
result.SetInt(key.Int+100, value)
}
}
// Convert back to PHP array
return frankenphp.PHPArray(result)
}
```
**Key features of `frankenphp.Array`:**
* **Ordered key-value pairs** - Maintains insertion order like PHP arrays
* **Mixed key types** - Supports both integer and string keys in the same array
* **Type safety** - The `PHPKey` type ensures proper key handling
* **Automatic list detection** - When converting to PHP, automatically detects if array should be a packed list or hashmap
* **Objects are not supported** - Currently, only scalar types and arrays can be used as values. Providing an object will result in a `null` value in the PHP array.
**Available methods:**
* `SetInt(key int64, value interface{})` - Set value with integer key
* `SetString(key string, value interface{})` - Set value with string key
* `Append(value interface{})` - Add value with next available integer key
* `Len() uint32` - Get number of elements
* `At(index uint32) (PHPKey, interface{})` - Get key-value pair at index
* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convert to PHP array
### 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:
@@ -188,7 +240,7 @@ func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool)
* **PHP `null` becomes Go `nil`** - when PHP passes `null`, your Go function receives a `nil` pointer
> [!WARNING]
> Currently, class methods have the following limitations. **Arrays and objects are not supported** as parameter types or return types. Only scalar types are supported: `string`, `int`, `float`, `bool` and `void` (for return type). **Nullable parameter types are fully supported** for all scalar types (`?string`, `?int`, `?float`, `?bool`).
> Currently, class methods have the following limitations. **Objects are not supported** as parameter types or return types. **Arrays are fully supported** for both parameters and return types. Supported types: `string`, `int`, `float`, `bool`, `array`, and `void` (for return type). **Nullable parameter types are fully supported** for all scalar types (`?string`, `?int`, `?float`, `?bool`).
After generating the extension, you will be allowed to use the class and its methods in PHP. Note that you **cannot access properties directly**:

View File

@@ -81,25 +81,77 @@ Alors que le premier point parle de lui-même, le second peut être plus diffici
Bien que certains types de variables aient la même représentation mémoire entre C/PHP et Go, certains types nécessitent plus de logique pour être directement utilisés. C'est peut-être la partie la plus difficile quand il s'agit d'écrire des extensions car cela nécessite de comprendre les fonctionnements internes du moteur Zend et comment les variables sont stockées dans le moteur de PHP. Ce tableau résume ce que vous devez savoir :
| Type PHP | Type Go | Conversion directe | Assistant C vers Go | Assistant Go vers C | Support des Méthodes de Classe |
|--------------------|------------------|--------------------|-------------------------|-------------------------|--------------------------------|
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
| `array` | `slice`/`map` | ❌ | _Pas encore implémenté_ | _Pas encore implémenté_ | |
| `object` | `struct` | ❌ | _Pas encore implémenté_ | _Pas encore implémenté_ | ❌ |
| Type PHP | Type Go | Conversion directe | Assistant C vers Go | Assistant Go vers C | Support des Méthodes de Classe |
|--------------------|---------------------|--------------------|-------------------------|-------------------------|--------------------------------|
| `int` | `int64` | ✅ | - | - | ✅ |
| `?int` | `*int64` | ✅ | - | - | ✅ |
| `float` | `float64` | ✅ | - | - | ✅ |
| `?float` | `*float64` | ✅ | - | - | ✅ |
| `bool` | `bool` | ✅ | - | - | ✅ |
| `?bool` | `*bool` | ✅ | - | - | ✅ |
| `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ |
| `array` | `*frankenphp.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | |
| `object` | `struct` | ❌ | _Pas encore implémenté_ | _Pas encore implémenté_ | ❌ |
> [!NOTE]
> Ce tableau n'est pas encore exhaustif et sera complété au fur et à mesure que l'API de types FrankenPHP deviendra plus complète.
>
> Pour les méthodes de classe spécifiquement, seuls les types primitifs sont actuellement supportés. Les tableaux et objets ne peuvent pas encore être utilisés comme paramètres de méthode ou types de retour.
> Pour les méthodes de classe spécifiquement, les types primitifs et les tableaux sont supportés. Les objets ne peuvent pas encore être utilisés comme paramètres de méthode ou types de retour.
Si vous vous référez à l'extrait de code de la section précédente, vous pouvez voir que des assistants sont utilisés pour convertir le premier paramètre et la valeur de retour. Les deuxième et troisième paramètres de notre fonction `repeat_this()` n'ont pas besoin d'être convertis car la représentation mémoire des types sous-jacents est la même pour C et Go.
#### Travailler avec les Tableaux
FrankenPHP fournit un support natif pour les tableaux PHP à travers le type `frankenphp.Array`. Ce type représente à la fois les tableaux indexés PHP (listes) et les tableaux associatifs (hashmaps) avec des paires clé-valeur ordonnées.
**Créer et manipuler des tableaux en Go :**
```go
//export_php:function process_data(array $input): array
func process_data(arr *C.zval) unsafe.Pointer {
// Convertir le tableau PHP vers Go
goArray := frankenphp.GoArray(unsafe.Pointer(arr))
result := &frankenphp.Array{}
result.SetInt(0, "first")
result.SetInt(1, "second")
result.Append("third") // Assigne automatiquement la prochaine clé entière
result.SetString("name", "John")
result.SetString("age", int64(30))
for i := uint32(0); i < goArray.Len(); i++ {
key, value := goArray.At(i)
if key.Type == frankenphp.PHPStringKey {
result.SetString("processed_"+key.Str, value)
} else {
result.SetInt(key.Int+100, value)
}
}
// Reconvertir vers un tableau PHP
return frankenphp.PHPArray(result)
}
```
**Fonctionnalités clés de `frankenphp.Array` :**
* **Paires clé-valeur ordonnées** - Maintient l'ordre d'insertion comme les tableaux PHP
* **Types de clés mixtes** - Supporte les clés entières et chaînes dans le même tableau
* **Sécurité de type** - Le type `PHPKey` assure une gestion appropriée des clés
* **Détection automatique de liste** - Lors de la conversion vers PHP, détecte automatiquement si le tableau doit être une liste compacte ou un hashmap
* **Les objets ne sont pas supportés** - Actuellement, seuls les types scalaires et les tableaux sont supportés. Passer un objet en tant qu'élément du tableau résultera d'une valeur `null` dans le tableau PHP.
**Méthodes disponibles :**
* `SetInt(key int64, value interface{})` - Définir une valeur avec une clé entière
* `SetString(key string, value interface{})` - Définir une valeur avec une clé chaîne
* `Append(value interface{})` - Ajouter une valeur avec la prochaine clé entière disponible
* `Len() uint32` - Obtenir le nombre d'éléments
* `At(index uint32) (PHPKey, interface{})` - Obtenir la paire clé-valeur à l'index
* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convertir vers un tableau PHP
### 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 :
@@ -188,7 +240,7 @@ func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool)
* **PHP `null` devient Go `nil`** - quand PHP passe `null`, votre fonction Go reçoit un pointeur `nil`
> [!WARNING]
> Actuellement, les méthodes de classe ont les limitations suivantes. **Les tableaux et objets ne sont pas supportés** comme types de paramètres ou types de retour. Seuls les types scalaires sont supportés : `string`, `int`, `float`, `bool` et `void` (pour le type de retour). **Les types de paramètres nullables sont entièrement supportés** pour tous les types scalaires (`?string`, `?int`, `?float`, `?bool`).
> Actuellement, les méthodes de classe ont les limitations suivantes. **Les objets ne sont pas supportés** comme types de paramètres ou types de retour. **Les tableaux sont entièrement supportés** pour les paramètres et types de retour. Types supportés : `string`, `int`, `float`, `bool`, `array`, et `void` (pour le type de retour). **Les types de paramètres nullables sont entièrement supportés** pour tous les types scalaires (`?string`, `?int`, `?float`, `?bool`).
Après avoir généré l'extension, vous serez autorisé à utiliser la classe et ses méthodes en PHP. Notez que vous **ne pouvez pas accéder aux propriétés directement** :

View File

@@ -18,18 +18,18 @@ func TestCFileGenerator_Generate(t *testing.T) {
Functions: []phpFunction{
{
Name: "simpleFunction",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "input", PhpType: "string"},
{Name: "input", PhpType: phpString},
},
},
{
Name: "complexFunction",
ReturnType: "array",
ReturnType: phpArray,
Params: []phpParameter{
{Name: "data", PhpType: "string"},
{Name: "count", PhpType: "int", IsNullable: true},
{Name: "options", PhpType: "array", HasDefault: true, DefaultValue: "[]"},
{Name: "data", PhpType: phpString},
{Name: "count", PhpType: phpInt, IsNullable: true},
{Name: "options", PhpType: phpArray, HasDefault: true, DefaultValue: "[]"},
},
},
},
@@ -38,8 +38,8 @@ func TestCFileGenerator_Generate(t *testing.T) {
Name: "TestClass",
GoStruct: "TestStruct",
Properties: []phpClassProperty{
{Name: "id", PhpType: "int"},
{Name: "name", PhpType: "string"},
{Name: "id", PhpType: phpInt},
{Name: "name", PhpType: phpString},
},
},
},
@@ -84,7 +84,7 @@ func TestCFileGenerator_BuildContent(t *testing.T) {
name: "extension with functions only",
baseName: "func_only",
functions: []phpFunction{
{Name: "testFunc", ReturnType: "string"},
{Name: "testFunc", ReturnType: phpString},
},
contains: []string{
"PHP_FUNCTION(testFunc)",
@@ -110,7 +110,7 @@ func TestCFileGenerator_BuildContent(t *testing.T) {
name: "extension with functions and classes",
baseName: "full",
functions: []phpFunction{
{Name: "doSomething", ReturnType: "void"},
{Name: "doSomething", ReturnType: phpVoid},
},
classes: []phpClass{
{Name: "FullClass", GoStruct: "FullStruct"},
@@ -209,20 +209,20 @@ func TestCFileIntegrationWithGenerators(t *testing.T) {
functions := []phpFunction{
{
Name: "processData",
ReturnType: "array",
ReturnType: phpArray,
IsReturnNullable: true,
Params: []phpParameter{
{Name: "input", PhpType: "string"},
{Name: "options", PhpType: "array", HasDefault: true, DefaultValue: "[]"},
{Name: "callback", PhpType: "object", IsNullable: true},
{Name: "input", PhpType: phpString},
{Name: "options", PhpType: phpArray, HasDefault: true, DefaultValue: "[]"},
{Name: "callback", PhpType: phpObject, IsNullable: true},
},
},
{
Name: "validateInput",
ReturnType: "bool",
ReturnType: phpBool,
Params: []phpParameter{
{Name: "data", PhpType: "string", IsNullable: true},
{Name: "strict", PhpType: "bool", HasDefault: true, DefaultValue: "false"},
{Name: "data", PhpType: phpString, IsNullable: true},
{Name: "strict", PhpType: phpBool, HasDefault: true, DefaultValue: "false"},
},
},
}
@@ -232,18 +232,18 @@ func TestCFileIntegrationWithGenerators(t *testing.T) {
Name: "DataProcessor",
GoStruct: "DataProcessorStruct",
Properties: []phpClassProperty{
{Name: "mode", PhpType: "string"},
{Name: "timeout", PhpType: "int", IsNullable: true},
{Name: "options", PhpType: "array"},
{Name: "mode", PhpType: phpString},
{Name: "timeout", PhpType: phpInt, IsNullable: true},
{Name: "options", PhpType: phpArray},
},
},
{
Name: "Result",
GoStruct: "ResultStruct",
Properties: []phpClassProperty{
{Name: "success", PhpType: "bool"},
{Name: "data", PhpType: "mixed", IsNullable: true},
{Name: "errors", PhpType: "array"},
{Name: "success", PhpType: phpBool},
{Name: "data", PhpType: phpMixed, IsNullable: true},
{Name: "errors", PhpType: phpArray},
},
},
}
@@ -281,7 +281,7 @@ func TestCFileErrorHandling(t *testing.T) {
BaseName: "test",
BuildDir: "/invalid/readonly/path",
Functions: []phpFunction{
{Name: "test", ReturnType: "void"},
{Name: "test", ReturnType: phpVoid},
},
}
@@ -305,7 +305,7 @@ func TestCFileSpecialCharacters(t *testing.T) {
generator := &Generator{
BaseName: tt.baseName,
Functions: []phpFunction{
{Name: "test", ReturnType: "void"},
{Name: "test", ReturnType: phpVoid},
},
}
@@ -367,9 +367,9 @@ func TestCFileContentValidation(t *testing.T) {
Functions: []phpFunction{
{
Name: "testFunction",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "param", PhpType: "string"},
{Name: "param", PhpType: phpString},
},
},
},
@@ -415,12 +415,12 @@ func TestCFileConstants(t *testing.T) {
{
Name: "GLOBAL_INT",
Value: "42",
PhpType: "int",
PhpType: phpInt,
},
{
Name: "GLOBAL_STRING",
Value: `"test"`,
PhpType: "string",
PhpType: phpString,
},
},
contains: []string{

View File

@@ -181,15 +181,15 @@ func (cp *classParser) typeToString(expr ast.Expr) string {
}
}
func (cp *classParser) goTypeToPHPType(goType string) string {
func (cp *classParser) goTypeToPHPType(goType string) phpType {
goType = strings.TrimPrefix(goType, "*")
typeMap := map[string]string{
"string": "string",
"int": "int", "int64": "int", "int32": "int", "int16": "int", "int8": "int",
"uint": "int", "uint64": "int", "uint32": "int", "uint16": "int", "uint8": "int",
"float64": "float", "float32": "float",
"bool": "bool",
typeMap := map[string]phpType{
"string": phpString,
"int": phpInt, "int64": phpInt, "int32": phpInt, "int16": phpInt, "int8": phpInt,
"uint": phpInt, "uint64": phpInt, "uint32": phpInt, "uint16": phpInt, "uint8": phpInt,
"float64": phpFloat, "float32": phpFloat,
"bool": phpBool,
}
if phpType, exists := typeMap[goType]; exists {
@@ -197,10 +197,10 @@ func (cp *classParser) goTypeToPHPType(goType string) string {
}
if strings.HasPrefix(goType, "[]") || strings.HasPrefix(goType, "map[") {
return "array"
return phpArray
}
return "mixed"
return phpMixed
}
func (cp *classParser) parseMethods(filename string) (methods []phpClassMethod, err error) {
@@ -323,7 +323,7 @@ func (cp *classParser) parseMethodSignature(className, signature string) (*phpCl
ClassName: className,
Signature: signature,
Params: params,
ReturnType: returnType,
ReturnType: phpType(returnType),
isReturnNullable: isReturnNullable,
}, nil
}
@@ -347,7 +347,7 @@ func (cp *classParser) parseMethodParameter(paramStr string) (phpParameter, erro
typeStr := strings.TrimSpace(matches[1])
param.Name = strings.TrimSpace(matches[2])
param.IsNullable = strings.HasPrefix(typeStr, "?")
param.PhpType = strings.TrimPrefix(typeStr, "?")
param.PhpType = phpType(strings.TrimPrefix(typeStr, "?"))
return param, nil
}

View File

@@ -157,29 +157,29 @@ func GetUserInfo(u UserStruct, prefix *C.zend_string) unsafe.Pointer {
getName := class.Methods[0]
assert.Equal(t, "getName", getName.Name, "Expected method name 'getName'")
assert.Equal(t, "string", getName.ReturnType, "Expected return type 'string'")
assert.Equal(t, phpString, getName.ReturnType, "Expected return type 'string'")
assert.Empty(t, getName.Params, "Expected 0 params")
assert.Equal(t, "User", getName.ClassName, "Expected class name 'User'")
setAge := class.Methods[1]
assert.Equal(t, "setAge", setAge.Name, "Expected method name 'setAge'")
assert.Equal(t, "void", setAge.ReturnType, "Expected return type 'void'")
assert.Equal(t, phpVoid, setAge.ReturnType, "Expected return type 'void'")
require.Len(t, setAge.Params, 1, "Expected 1 param")
param := setAge.Params[0]
assert.Equal(t, "age", param.Name, "Expected param name 'age'")
assert.Equal(t, "int", param.PhpType, "Expected param type 'int'")
assert.Equal(t, phpInt, param.PhpType, "Expected param type 'int'")
assert.False(t, param.IsNullable, "Expected param to not be nullable")
assert.False(t, param.HasDefault, "Expected param to not have default value")
getInfo := class.Methods[2]
assert.Equal(t, "getInfo", getInfo.Name, "Expected method name 'getInfo'")
assert.Equal(t, "string", getInfo.ReturnType, "Expected return type 'string'")
assert.Equal(t, phpString, getInfo.ReturnType, "Expected return type 'string'")
require.Len(t, getInfo.Params, 1, "Expected 1 param")
param = getInfo.Params[0]
assert.Equal(t, "prefix", param.Name, "Expected param name 'prefix'")
assert.Equal(t, "string", param.PhpType, "Expected param type 'string'")
assert.Equal(t, phpString, param.PhpType, "Expected param type 'string'")
assert.True(t, param.HasDefault, "Expected param to have default value")
assert.Equal(t, "User", param.DefaultValue, "Expected default value 'User'")
}
@@ -196,7 +196,7 @@ func TestMethodParameterParsing(t *testing.T) {
paramStr: "int $age",
expectedParam: phpParameter{
Name: "age",
PhpType: "int",
PhpType: phpInt,
IsNullable: false,
HasDefault: false,
},
@@ -207,7 +207,7 @@ func TestMethodParameterParsing(t *testing.T) {
paramStr: "?string $name",
expectedParam: phpParameter{
Name: "name",
PhpType: "string",
PhpType: phpString,
IsNullable: true,
HasDefault: false,
},
@@ -218,7 +218,7 @@ func TestMethodParameterParsing(t *testing.T) {
paramStr: `string $prefix = "default"`,
expectedParam: phpParameter{
Name: "prefix",
PhpType: "string",
PhpType: phpString,
IsNullable: false,
HasDefault: true,
DefaultValue: "default",
@@ -230,7 +230,7 @@ func TestMethodParameterParsing(t *testing.T) {
paramStr: "?int $count = null",
expectedParam: phpParameter{
Name: "count",
PhpType: "int",
PhpType: phpInt,
IsNullable: true,
HasDefault: true,
DefaultValue: "null",
@@ -268,22 +268,22 @@ func TestMethodParameterParsing(t *testing.T) {
func TestGoTypeToPHPType(t *testing.T) {
tests := []struct {
goType string
expected string
expected phpType
}{
{"string", "string"},
{"*string", "string"},
{"int", "int"},
{"int64", "int"},
{"*int", "int"},
{"float64", "float"},
{"*float32", "float"},
{"bool", "bool"},
{"*bool", "bool"},
{"[]string", "array"},
{"map[string]int", "array"},
{"*[]int", "array"},
{"interface{}", "mixed"},
{"CustomType", "mixed"},
{"string", phpString},
{"*string", phpString},
{"int", phpInt},
{"int64", phpInt},
{"*int", phpInt},
{"float64", phpFloat},
{"*float32", phpFloat},
{"bool", phpBool},
{"*bool", phpBool},
{"[]string", phpArray},
{"map[string]int", phpArray},
{"*[]int", phpArray},
{"interface{}", phpMixed},
{"CustomType", phpMixed},
}
parser := classParser{}
@@ -299,7 +299,7 @@ func TestTypeToString(t *testing.T) {
tests := []struct {
name string
input string
expected []string
expected []phpType
}{
{
name: "basic types",
@@ -312,7 +312,7 @@ type TestStruct struct {
FloatField float64
BoolField bool
}`,
expected: []string{"string", "int", "float", "bool"},
expected: []phpType{phpString, phpInt, phpFloat, phpBool},
},
{
name: "pointer types",
@@ -325,7 +325,7 @@ type NullableStruct struct {
NullableFloat *float64
NullableBool *bool
}`,
expected: []string{"string", "int", "float", "bool"},
expected: []phpType{phpString, phpInt, phpFloat, phpBool},
},
{
name: "collection types",
@@ -337,7 +337,7 @@ type CollectionStruct struct {
IntMap map[string]int
MixedSlice []interface{}
}`,
expected: []string{"array", "array", "array"},
expected: []phpType{phpArray, phpArray, phpArray},
},
}

View File

@@ -86,7 +86,7 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e
if constant.IsIota {
// affect a default value because user didn't give one
constant.Value = fmt.Sprintf("%d", currentConstantValue)
constant.PhpType = "int"
constant.PhpType = phpInt
currentConstantValue++
}
@@ -108,26 +108,26 @@ func (cp *ConstantParser) parse(filename string) (constants []phpConstant, err e
}
// determineConstantType analyzes the value and determines its type
func determineConstantType(value string) string {
func determineConstantType(value string) phpType {
value = strings.TrimSpace(value)
if (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) ||
(strings.HasPrefix(value, "`") && strings.HasSuffix(value, "`")) {
return "string"
return phpString
}
if value == "true" || value == "false" {
return "bool"
return phpBool
}
// check for integer literals, including hex, octal, binary
if _, err := strconv.ParseInt(value, 0, 64); err == nil {
return "int"
return phpInt
}
if _, err := strconv.ParseFloat(value, 64); err == nil {
return "float"
return phpFloat
}
return "int"
return phpInt
}

View File

@@ -144,7 +144,7 @@ const FalseConstant = false`,
c := constants[0]
assert.Equal(t, "MyConstant", c.Name, "Expected constant name 'MyConstant'")
assert.Equal(t, `"test_value"`, c.Value, `Expected constant value '"test_value"'`)
assert.Equal(t, "string", c.PhpType, "Expected constant type 'string'")
assert.Equal(t, phpString, c.PhpType, "Expected constant type 'string'")
assert.False(t, c.IsIota, "Expected isIota to be false for string constant")
}
@@ -158,7 +158,7 @@ const FalseConstant = false`,
if tt.name == "multiple constants" && len(constants) == 3 {
expectedNames := []string{"FirstConstant", "SecondConstant", "ThirdConstant"}
expectedValues := []string{`"first"`, "42", "true"}
expectedTypes := []string{"string", "int", "bool"}
expectedTypes := []phpType{phpString, phpInt, phpBool}
for i, c := range constants {
assert.Equal(t, expectedNames[i], c.Name, "Expected constant name '%s'", expectedNames[i])
@@ -248,22 +248,22 @@ func TestConstantParserTypeDetection(t *testing.T) {
tests := []struct {
name string
value string
expectedType string
expectedType phpType
}{
{"string with double quotes", "\"hello world\"", "string"},
{"string with backticks", "`hello world`", "string"},
{"boolean true", "true", "bool"},
{"boolean false", "false", "bool"},
{"integer", "42", "int"},
{"negative integer", "-42", "int"},
{"hex integer", "0xFF", "int"},
{"octal integer", "0755", "int"},
{"go octal integer", "0o755", "int"},
{"binary integer", "0b1010", "int"},
{"float", "3.14", "float"},
{"negative float", "-3.14", "float"},
{"scientific notation", "1e10", "float"},
{"unknown type", "someFunction()", "int"},
{"string with double quotes", "\"hello world\"", phpString},
{"string with backticks", "`hello world`", phpString},
{"boolean true", "true", phpBool},
{"boolean false", "false", phpBool},
{"integer", "42", phpInt},
{"negative integer", "-42", phpInt},
{"hex integer", "0xFF", phpInt},
{"octal integer", "0755", phpInt},
{"go octal integer", "0o755", phpInt},
{"binary integer", "0b1010", phpInt},
{"float", "3.14", phpFloat},
{"negative float", "-3.14", phpFloat},
{"scientific notation", "1e10", phpFloat},
{"unknown type", "someFunction()", phpInt},
}
for _, tt := range tests {
@@ -354,7 +354,7 @@ const INVALID = "missing class name"`,
assert.Equal(t, "STATUS_ACTIVE", c.Name, "Expected constant name 'STATUS_ACTIVE'")
assert.Equal(t, "MyClass", c.ClassName, "Expected class name 'MyClass'")
assert.Equal(t, "1", c.Value, "Expected constant value '1'")
assert.Equal(t, "int", c.PhpType, "Expected constant type 'int'")
assert.Equal(t, phpInt, c.PhpType, "Expected constant type 'int'")
}
if tt.name == "multiple class constants" && len(constants) == 3 {
@@ -489,7 +489,7 @@ func TestPHPConstantCValue(t *testing.T) {
constant: phpConstant{
Name: "OctalConst",
Value: "0o35",
PhpType: "int",
PhpType: phpInt,
},
expected: "29", // 0o35 = 29 in decimal
},
@@ -498,7 +498,7 @@ func TestPHPConstantCValue(t *testing.T) {
constant: phpConstant{
Name: "OctalPerm",
Value: "0o755",
PhpType: "int",
PhpType: phpInt,
},
expected: "493", // 0o755 = 493 in decimal
},
@@ -507,7 +507,7 @@ func TestPHPConstantCValue(t *testing.T) {
constant: phpConstant{
Name: "RegularInt",
Value: "42",
PhpType: "int",
PhpType: phpInt,
},
expected: "42",
},
@@ -516,7 +516,7 @@ func TestPHPConstantCValue(t *testing.T) {
constant: phpConstant{
Name: "HexInt",
Value: "0xFF",
PhpType: "int",
PhpType: phpInt,
},
expected: "0xFF", // hex should remain unchanged
},
@@ -525,7 +525,7 @@ func TestPHPConstantCValue(t *testing.T) {
constant: phpConstant{
Name: "StringConst",
Value: "\"hello\"",
PhpType: "string",
PhpType: phpString,
},
expected: "\"hello\"", // strings should remain unchanged
},
@@ -534,7 +534,7 @@ func TestPHPConstantCValue(t *testing.T) {
constant: phpConstant{
Name: "BoolConst",
Value: "true",
PhpType: "bool",
PhpType: phpBool,
},
expected: "true", // booleans should remain unchanged
},
@@ -543,7 +543,7 @@ func TestPHPConstantCValue(t *testing.T) {
constant: phpConstant{
Name: "FloatConst",
Value: "3.14",
PhpType: "float",
PhpType: phpFloat,
},
expected: "3.14", // floats should remain unchanged
},

View File

@@ -23,9 +23,9 @@ func TestDocumentationGenerator_Generate(t *testing.T) {
Functions: []phpFunction{
{
Name: "greet",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "name", PhpType: "string"},
{Name: "name", PhpType: phpString},
},
Signature: "greet(string $name): string",
},
@@ -44,8 +44,8 @@ func TestDocumentationGenerator_Generate(t *testing.T) {
{
Name: "TestClass",
Properties: []phpClassProperty{
{Name: "name", PhpType: "string"},
{Name: "count", PhpType: "int", IsNullable: true},
{Name: "name", PhpType: phpString},
{Name: "count", PhpType: phpInt, IsNullable: true},
},
},
},
@@ -60,11 +60,11 @@ func TestDocumentationGenerator_Generate(t *testing.T) {
Functions: []phpFunction{
{
Name: "calculate",
ReturnType: "int",
ReturnType: phpInt,
IsReturnNullable: true,
Params: []phpParameter{
{Name: "base", PhpType: "int"},
{Name: "multiplier", PhpType: "int", HasDefault: true, DefaultValue: "2", IsNullable: true},
{Name: "base", PhpType: phpInt},
{Name: "multiplier", PhpType: phpInt, HasDefault: true, DefaultValue: "2", IsNullable: true},
},
Signature: "calculate(int $base, ?int $multiplier = 2): ?int",
},
@@ -73,7 +73,7 @@ func TestDocumentationGenerator_Generate(t *testing.T) {
{
Name: "Calculator",
Properties: []phpClassProperty{
{Name: "precision", PhpType: "int"},
{Name: "precision", PhpType: phpInt},
},
},
},
@@ -155,11 +155,11 @@ func TestDocumentationGenerator_GenerateMarkdown(t *testing.T) {
Functions: []phpFunction{
{
Name: "processData",
ReturnType: "array",
ReturnType: phpArray,
Params: []phpParameter{
{Name: "data", PhpType: "string"},
{Name: "options", PhpType: "array", IsNullable: true},
{Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "10"},
{Name: "data", PhpType: phpString},
{Name: "options", PhpType: phpArray, IsNullable: true},
{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "10"},
},
Signature: "processData(string $data, ?array $options, int $count = 10): array",
},
@@ -184,7 +184,7 @@ func TestDocumentationGenerator_GenerateMarkdown(t *testing.T) {
Functions: []phpFunction{
{
Name: "maybeGetValue",
ReturnType: "string",
ReturnType: phpString,
IsReturnNullable: true,
Params: []phpParameter{},
Signature: "maybeGetValue(): ?string",
@@ -205,9 +205,9 @@ func TestDocumentationGenerator_GenerateMarkdown(t *testing.T) {
{
Name: "DataProcessor",
Properties: []phpClassProperty{
{Name: "name", PhpType: "string"},
{Name: "config", PhpType: "array", IsNullable: true},
{Name: "enabled", PhpType: "bool"},
{Name: "name", PhpType: phpString},
{Name: "config", PhpType: phpArray, IsNullable: true},
{Name: "enabled", PhpType: phpBool},
},
},
},
@@ -244,7 +244,7 @@ func TestDocumentationGenerator_GenerateMarkdown(t *testing.T) {
Functions: []phpFunction{
{
Name: "getCurrentTime",
ReturnType: "int",
ReturnType: phpInt,
Params: []phpParameter{},
Signature: "getCurrentTime(): int",
},
@@ -324,7 +324,7 @@ func TestDocumentationGenerator_TemplateError(t *testing.T) {
Functions: []phpFunction{
{
Name: "test",
ReturnType: "string",
ReturnType: phpString,
Signature: "test(): string",
},
},
@@ -346,19 +346,19 @@ func BenchmarkDocumentationGenerator_GenerateMarkdown(b *testing.B) {
Functions: []phpFunction{
{
Name: "function1",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "param1", PhpType: "string"},
{Name: "param2", PhpType: "int", HasDefault: true, DefaultValue: "0"},
{Name: "param1", PhpType: phpString},
{Name: "param2", PhpType: phpInt, HasDefault: true, DefaultValue: "0"},
},
Signature: "function1(string $param1, int $param2 = 0): string",
},
{
Name: "function2",
ReturnType: "array",
ReturnType: phpArray,
IsReturnNullable: true,
Params: []phpParameter{
{Name: "data", PhpType: "array", IsNullable: true},
{Name: "data", PhpType: phpArray, IsNullable: true},
},
Signature: "function2(?array $data): ?array",
},
@@ -367,8 +367,8 @@ func BenchmarkDocumentationGenerator_GenerateMarkdown(b *testing.B) {
{
Name: "TestClass",
Properties: []phpClassProperty{
{Name: "prop1", PhpType: "string"},
{Name: "prop2", PhpType: "int", IsNullable: true},
{Name: "prop1", PhpType: phpString},
{Name: "prop2", PhpType: phpInt, IsNullable: true},
},
},
},

View File

@@ -150,7 +150,7 @@ func (fp *FuncParser) parseSignature(signature string) (*phpFunction, error) {
Name: name,
Signature: signature,
Params: params,
ReturnType: returnType,
ReturnType: phpType(returnType),
IsReturnNullable: isReturnNullable,
}, nil
}
@@ -174,7 +174,7 @@ func (fp *FuncParser) parseParameter(paramStr string) (phpParameter, error) {
typeStr := strings.TrimSpace(matches[1])
param.Name = strings.TrimSpace(matches[2])
param.IsNullable = strings.HasPrefix(typeStr, "?")
param.PhpType = strings.TrimPrefix(typeStr, "?")
param.PhpType = phpType(strings.TrimPrefix(typeStr, "?"))
return param, nil
}

View File

@@ -109,7 +109,7 @@ func someOtherGoName(num int64) int64 {
if tt.name == "single function" && len(functions) > 0 {
fn := functions[0]
assert.Equal(t, "testFunc", fn.Name, "Expected function name 'testFunc'")
assert.Equal(t, "string", fn.ReturnType, "Expected return type 'string'")
assert.Equal(t, phpString, fn.ReturnType, "Expected return type 'string'")
assert.Len(t, fn.Params, 1, "Expected 1 parameter")
if len(fn.Params) > 0 {
assert.Equal(t, "name", fn.Params[0].Name, "Expected parameter name 'name'")
@@ -133,7 +133,7 @@ func TestSignatureParsing(t *testing.T) {
expectError bool
funcName string
paramCount int
returnType string
returnType phpType
nullable bool
}{
{
@@ -141,7 +141,7 @@ func TestSignatureParsing(t *testing.T) {
signature: "test(name string): string",
funcName: "test",
paramCount: 1,
returnType: "string",
returnType: phpString,
nullable: false,
},
{
@@ -149,7 +149,7 @@ func TestSignatureParsing(t *testing.T) {
signature: "test(id int): ?string",
funcName: "test",
paramCount: 1,
returnType: "string",
returnType: phpString,
nullable: true,
},
{
@@ -157,7 +157,7 @@ func TestSignatureParsing(t *testing.T) {
signature: "calculate(a int, b float, name string): float",
funcName: "calculate",
paramCount: 3,
returnType: "float",
returnType: phpFloat,
nullable: false,
},
{
@@ -165,7 +165,7 @@ func TestSignatureParsing(t *testing.T) {
signature: "getValue(): int",
funcName: "getValue",
paramCount: 0,
returnType: "int",
returnType: phpInt,
nullable: false,
},
{
@@ -173,7 +173,7 @@ func TestSignatureParsing(t *testing.T) {
signature: "process(?string data, ?int count): bool",
funcName: "process",
paramCount: 2,
returnType: "bool",
returnType: phpBool,
nullable: false,
},
{
@@ -219,7 +219,7 @@ func TestParameterParsing(t *testing.T) {
name string
paramStr string
expectedName string
expectedType string
expectedType phpType
expectedNullable bool
expectedDefault string
hasDefault bool
@@ -229,20 +229,20 @@ func TestParameterParsing(t *testing.T) {
name: "simple string param",
paramStr: "string name",
expectedName: "name",
expectedType: "string",
expectedType: phpString,
},
{
name: "nullable int param",
paramStr: "?int count",
expectedName: "count",
expectedType: "int",
expectedType: phpInt,
expectedNullable: true,
},
{
name: "param with default",
paramStr: "string message = 'hello'",
expectedName: "message",
expectedType: "string",
expectedType: phpString,
expectedDefault: "hello",
hasDefault: true,
},
@@ -250,7 +250,7 @@ func TestParameterParsing(t *testing.T) {
name: "int with default",
paramStr: "int limit = 10",
expectedName: "limit",
expectedType: "int",
expectedType: phpInt,
expectedDefault: "10",
hasDefault: true,
},
@@ -258,7 +258,7 @@ func TestParameterParsing(t *testing.T) {
name: "nullable with default",
paramStr: "?string data = null",
expectedName: "data",
expectedType: "string",
expectedType: phpString,
expectedNullable: true,
expectedDefault: "null",
hasDefault: true,

View File

@@ -5,7 +5,6 @@ import (
_ "embed"
"fmt"
"path/filepath"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
@@ -54,11 +53,6 @@ func (gg *GoFileGenerator) buildContent() (string, error) {
classes := make([]phpClass, len(gg.generator.Classes))
copy(classes, gg.generator.Classes)
for i, class := range classes {
for j, method := range class.Methods {
classes[i].Methods[j].Wrapper = gg.generateMethodWrapper(method, class)
}
}
templateContent, err := gg.getTemplateContent(goTemplateData{
PackageName: SanitizePackageName(gg.generator.BaseName),
@@ -78,7 +72,16 @@ func (gg *GoFileGenerator) buildContent() (string, error) {
}
func (gg *GoFileGenerator) getTemplateContent(data goTemplateData) (string, error) {
tmpl := template.Must(template.New("gofile").Funcs(sprig.FuncMap()).Parse(goFileContent))
funcMap := sprig.FuncMap()
funcMap["phpTypeToGoType"] = gg.phpTypeToGoType
funcMap["isStringOrArray"] = func(t phpType) bool {
return t == phpString || t == phpArray
}
funcMap["isVoid"] = func(t phpType) bool {
return t == phpVoid
}
tmpl := template.Must(template.New("gofile").Funcs(funcMap).Parse(goFileContent))
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
@@ -88,72 +91,6 @@ func (gg *GoFileGenerator) getTemplateContent(data goTemplateData) (string, erro
return buf.String(), nil
}
func (gg *GoFileGenerator) generateMethodWrapper(method phpClassMethod, class phpClass) string {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("func %s_wrapper(handle C.uintptr_t", method.Name))
for _, param := range method.Params {
if param.PhpType == "string" {
builder.WriteString(fmt.Sprintf(", %s *C.zend_string", param.Name))
continue
}
goType := gg.phpTypeToGoType(param.PhpType)
if param.IsNullable {
goType = "*" + goType
}
builder.WriteString(fmt.Sprintf(", %s %s", param.Name, goType))
}
if method.ReturnType != "void" {
if method.ReturnType == "string" {
builder.WriteString(") unsafe.Pointer {\n")
} else {
goReturnType := gg.phpTypeToGoType(method.ReturnType)
builder.WriteString(fmt.Sprintf(") %s {\n", goReturnType))
}
} else {
builder.WriteString(") {\n")
}
builder.WriteString(" obj := getGoObject(handle)\n")
builder.WriteString(" if obj == nil {\n")
if method.ReturnType != "void" {
if method.ReturnType == "string" {
builder.WriteString(" return nil\n")
} else {
builder.WriteString(fmt.Sprintf(" var zero %s\n", gg.phpTypeToGoType(method.ReturnType)))
builder.WriteString(" return zero\n")
}
} else {
builder.WriteString(" return\n")
}
builder.WriteString(" }\n")
builder.WriteString(fmt.Sprintf(" structObj := obj.(*%s)\n", class.GoStruct))
builder.WriteString(" ")
if method.ReturnType != "void" {
builder.WriteString("return ")
}
builder.WriteString(fmt.Sprintf("structObj.%s(", gg.goMethodName(method.Name)))
for i, param := range method.Params {
if i > 0 {
builder.WriteString(", ")
}
builder.WriteString(param.Name)
}
builder.WriteString(")\n")
builder.WriteString("}")
return builder.String()
}
type GoMethodSignature struct {
MethodName string
Params []GoParameter
@@ -165,28 +102,20 @@ type GoParameter struct {
Type string
}
func (gg *GoFileGenerator) phpTypeToGoType(phpType string) string {
typeMap := map[string]string{
"string": "string",
"int": "int64",
"float": "float64",
"bool": "bool",
"array": "[]interface{}",
"mixed": "interface{}",
"void": "",
func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string {
typeMap := map[phpType]string{
phpString: "string",
phpInt: "int64",
phpFloat: "float64",
phpBool: "bool",
phpArray: "*frankenphp.Array",
phpMixed: "interface{}",
phpVoid: "",
}
if goType, exists := typeMap[phpType]; exists {
if goType, exists := typeMap[phpT]; exists {
return goType
}
return "interface{}"
}
func (gg *GoFileGenerator) goMethodName(phpMethodName string) string {
if len(phpMethodName) == 0 {
return phpMethodName
}
return strings.ToUpper(phpMethodName[:1]) + phpMethodName[1:]
}

View File

@@ -50,14 +50,14 @@ func anotherHelper() {
Functions: []phpFunction{
{
Name: "greet",
ReturnType: "string",
ReturnType: phpString,
GoFunction: `func greet(name *go_string) *go_value {
return types.String("Hello " + CStringToGoString(name))
}`,
},
{
Name: "calculate",
ReturnType: "int",
ReturnType: phpInt,
GoFunction: `func calculate(a long, b long) *go_value {
result := a + b
return types.Int(result)
@@ -102,7 +102,7 @@ func test() {
functions: []phpFunction{
{
Name: "test",
ReturnType: "void",
ReturnType: phpVoid,
GoFunction: "func test() {\n\t// simple function\n}",
},
},
@@ -135,7 +135,7 @@ func process(data *go_string) *go_value {
functions: []phpFunction{
{
Name: "process",
ReturnType: "string",
ReturnType: phpString,
GoFunction: `func process(data *go_string) *go_value {
return String(fmt.Sprintf("processed: %s", CStringToGoString(data)))
}`,
@@ -168,7 +168,7 @@ func internalFunc2(data string) {
functions: []phpFunction{
{
Name: "publicFunc",
ReturnType: "void",
ReturnType: phpVoid,
GoFunction: "func publicFunc() {}",
},
},
@@ -219,7 +219,7 @@ func TestGoFileGenerator_PackageNameSanitization(t *testing.T) {
BaseName: tt.baseName,
SourceFile: sourceFile,
Functions: []phpFunction{
{Name: "test", ReturnType: "void", GoFunction: "func test() {}"},
{Name: "test", ReturnType: phpVoid, GoFunction: "func test() {}"},
},
}
@@ -296,7 +296,7 @@ func test() {}`
BaseName: "importtest",
SourceFile: sourceFile,
Functions: []phpFunction{
{Name: "test", ReturnType: "void", GoFunction: "func test() {}"},
{Name: "test", ReturnType: phpVoid, GoFunction: "func test() {}"},
},
}
@@ -371,7 +371,7 @@ func debugPrint(msg string) {
functions := []phpFunction{
{
Name: "processData",
ReturnType: "array",
ReturnType: phpArray,
GoFunction: `func processData(input *go_string, options *go_nullable) *go_value {
data := CStringToGoString(input)
processed := internalProcess(data)
@@ -380,7 +380,7 @@ func debugPrint(msg string) {
},
{
Name: "validateInput",
ReturnType: "bool",
ReturnType: phpBool,
GoFunction: `func validateInput(data *go_string) *go_value {
input := CStringToGoString(data)
isValid := len(input) > 0 && validateFormat(input)
@@ -453,11 +453,11 @@ func (ts *TestStruct) ProcessData(name string, count *int64, enabled *bool) stri
PhpName: "processData",
ClassName: "TestClass",
Signature: "processData(string $name, ?int $count, ?bool $enabled): string",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "name", PhpType: "string", IsNullable: false},
{Name: "count", PhpType: "int", IsNullable: true},
{Name: "enabled", PhpType: "bool", IsNullable: true},
{Name: "name", PhpType: phpString, IsNullable: false},
{Name: "count", PhpType: phpInt, IsNullable: true},
{Name: "enabled", PhpType: phpBool, IsNullable: true},
},
GoFunction: `func (ts *TestStruct) ProcessData(name string, count *int64, enabled *bool) string {
result := fmt.Sprintf("name=%s", name)
@@ -501,6 +501,176 @@ func (ts *TestStruct) ProcessData(name string, count *int64, enabled *bool) stri
assert.Contains(t, content, exportDirective, "Generated content should contain export directive: %s", exportDirective)
}
func TestGoFileGenerator_MethodWrapperWithArrayParams(t *testing.T) {
tmpDir := t.TempDir()
sourceContent := `package main
import "fmt"
//export_php:class ArrayClass
type ArrayStruct struct {
data []interface{}
}
//export_php:method ArrayClass::processArray(array $items): array
func (as *ArrayStruct) ProcessArray(items *frankenphp.Array) *frankenphp.Array {
result := &frankenphp.Array{}
for i := uint32(0); i < items.Len(); i++ {
key, value := items.At(i)
result.SetString(fmt.Sprintf("processed_%d", i), value)
}
return result
}
//export_php:method ArrayClass::filterData(array $data, string $filter): array
func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *frankenphp.Array {
result := &frankenphp.Array{}
// Filter logic here
return result
}`
sourceFile := filepath.Join(tmpDir, "test.go")
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
methods := []phpClassMethod{
{
Name: "ProcessArray",
PhpName: "processArray",
ClassName: "ArrayClass",
Signature: "processArray(array $items): array",
ReturnType: phpArray,
Params: []phpParameter{
{Name: "items", PhpType: phpArray, IsNullable: false},
},
GoFunction: `func (as *ArrayStruct) ProcessArray(items *frankenphp.Array) *frankenphp.Array {
result := &frankenphp.Array{}
for i := uint32(0); i < items.Len(); i++ {
key, value := items.At(i)
result.SetString(fmt.Sprintf("processed_%d", i), value)
}
return result
}`,
},
{
Name: "FilterData",
PhpName: "filterData",
ClassName: "ArrayClass",
Signature: "filterData(array $data, string $filter): array",
ReturnType: phpArray,
Params: []phpParameter{
{Name: "data", PhpType: phpArray, IsNullable: false},
{Name: "filter", PhpType: phpString, IsNullable: false},
},
GoFunction: `func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *frankenphp.Array {
result := &frankenphp.Array{}
return result
}`,
},
}
classes := []phpClass{
{
Name: "ArrayClass",
GoStruct: "ArrayStruct",
Methods: methods,
},
}
generator := &Generator{
BaseName: "array_test",
SourceFile: sourceFile,
Classes: classes,
BuildDir: tmpDir,
}
goGen := GoFileGenerator{generator}
content, err := goGen.buildContent()
require.NoError(t, err)
expectedArrayWrapperSignature := "func ProcessArray_wrapper(handle C.uintptr_t, items *C.zval) unsafe.Pointer"
assert.Contains(t, content, expectedArrayWrapperSignature, "Generated content should contain array wrapper signature: %s", expectedArrayWrapperSignature)
expectedMixedWrapperSignature := "func FilterData_wrapper(handle C.uintptr_t, data *C.zval, filter *C.zend_string) unsafe.Pointer"
assert.Contains(t, content, expectedMixedWrapperSignature, "Generated content should contain mixed wrapper signature: %s", expectedMixedWrapperSignature)
expectedArrayCall := "structObj.ProcessArray(items)"
assert.Contains(t, content, expectedArrayCall, "Generated content should contain array method call: %s", expectedArrayCall)
expectedMixedCall := "structObj.FilterData(data, filter)"
assert.Contains(t, content, expectedMixedCall, "Generated content should contain mixed method call: %s", expectedMixedCall)
assert.Contains(t, content, "//export ProcessArray_wrapper", "Generated content should contain ProcessArray export directive")
assert.Contains(t, content, "//export FilterData_wrapper", "Generated content should contain FilterData export directive")
}
func TestGoFileGenerator_MethodWrapperWithNullableArrayParams(t *testing.T) {
tmpDir := t.TempDir()
sourceContent := `package main
//export_php:class NullableArrayClass
type NullableArrayStruct struct{}
//export_php:method NullableArrayClass::processOptionalArray(?array $items, string $name): string
func (nas *NullableArrayStruct) ProcessOptionalArray(items *frankenphp.Array, name string) string {
if items == nil {
return "No items: " + name
}
return fmt.Sprintf("Processing %d items for %s", items.Len(), name)
}`
sourceFile := filepath.Join(tmpDir, "test.go")
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
methods := []phpClassMethod{
{
Name: "ProcessOptionalArray",
PhpName: "processOptionalArray",
ClassName: "NullableArrayClass",
Signature: "processOptionalArray(?array $items, string $name): string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "items", PhpType: phpArray, IsNullable: true},
{Name: "name", PhpType: phpString, IsNullable: false},
},
GoFunction: `func (nas *NullableArrayStruct) ProcessOptionalArray(items *frankenphp.Array, name string) string {
if items == nil {
return "No items: " + name
}
return fmt.Sprintf("Processing %d items for %s", items.Len(), name)
}`,
},
}
classes := []phpClass{
{
Name: "NullableArrayClass",
GoStruct: "NullableArrayStruct",
Methods: methods,
},
}
generator := &Generator{
BaseName: "nullable_array_test",
SourceFile: sourceFile,
Classes: classes,
BuildDir: tmpDir,
}
goGen := GoFileGenerator{generator}
content, err := goGen.buildContent()
require.NoError(t, err)
expectedWrapperSignature := "func ProcessOptionalArray_wrapper(handle C.uintptr_t, items *C.zval, name *C.zend_string) unsafe.Pointer"
assert.Contains(t, content, expectedWrapperSignature, "Generated content should contain nullable array wrapper signature: %s", expectedWrapperSignature)
expectedCall := "structObj.ProcessOptionalArray(items, name)"
assert.Contains(t, content, expectedCall, "Generated content should contain method call: %s", expectedCall)
assert.Contains(t, content, "//export ProcessOptionalArray_wrapper", "Generated content should contain export directive")
}
func createTempSourceFile(t *testing.T, content string) string {
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "source.go")

View File

@@ -5,19 +5,36 @@ import (
"strings"
)
// phpType represents a PHP type
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"
)
type phpFunction struct {
Name string
Signature string
GoFunction string
Params []phpParameter
ReturnType string
ReturnType phpType
IsReturnNullable bool
lineNumber int
}
type phpParameter struct {
Name string
PhpType string
PhpType phpType
IsNullable bool
DefaultValue string
HasDefault bool
@@ -37,7 +54,7 @@ type phpClassMethod struct {
GoFunction string
Wrapper string
Params []phpParameter
ReturnType string
ReturnType phpType
isReturnNullable bool
lineNumber int
ClassName string // used by the "//export_php:method" directive
@@ -45,7 +62,7 @@ type phpClassMethod struct {
type phpClassProperty struct {
Name string
PhpType string
PhpType phpType
GoType string
IsNullable bool
}
@@ -53,7 +70,7 @@ type phpClassProperty struct {
type phpConstant struct {
Name string
Value string
PhpType string // "int", "string", "bool", "float"
PhpType phpType
IsIota bool
lineNumber int
ClassName string // empty for global constants, set for class constants
@@ -61,7 +78,7 @@ type phpConstant struct {
// CValue returns the constant value in C-compatible format
func (c phpConstant) CValue() string {
if c.PhpType != "int" {
if c.PhpType != phpInt {
return c.Value
}

View File

@@ -42,24 +42,24 @@ func (pp *ParameterParser) generateSingleParamDeclaration(param phpParameter) []
var decls []string
switch param.PhpType {
case "string":
case phpString:
decls = append(decls, fmt.Sprintf("zend_string *%s = NULL;", param.Name))
if param.IsNullable {
decls = append(decls, fmt.Sprintf("zend_bool %s_is_null = 0;", param.Name))
}
case "int":
case phpInt:
defaultVal := pp.getDefaultValue(param, "0")
decls = append(decls, fmt.Sprintf("zend_long %s = %s;", param.Name, defaultVal))
if param.IsNullable {
decls = append(decls, fmt.Sprintf("zend_bool %s_is_null = 0;", param.Name))
}
case "float":
case phpFloat:
defaultVal := pp.getDefaultValue(param, "0.0")
decls = append(decls, fmt.Sprintf("double %s = %s;", param.Name, defaultVal))
if param.IsNullable {
decls = append(decls, fmt.Sprintf("zend_bool %s_is_null = 0;", param.Name))
}
case "bool":
case phpBool:
defaultVal := pp.getDefaultValue(param, "0")
if param.HasDefault && param.DefaultValue == "true" {
defaultVal = "1"
@@ -68,6 +68,8 @@ func (pp *ParameterParser) generateSingleParamDeclaration(param phpParameter) []
if param.IsNullable {
decls = append(decls, fmt.Sprintf("zend_bool %s_is_null = 0;", param.Name))
}
case phpArray:
decls = append(decls, fmt.Sprintf("zval *%s = NULL;", param.Name))
}
return decls
@@ -107,27 +109,31 @@ func (pp *ParameterParser) generateParamParsing(params []phpParameter, requiredC
func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string {
if param.IsNullable {
switch param.PhpType {
case "string":
case phpString:
return fmt.Sprintf("\n Z_PARAM_STR_OR_NULL(%s, %s_is_null)", param.Name, param.Name)
case "int":
case phpInt:
return fmt.Sprintf("\n Z_PARAM_LONG_OR_NULL(%s, %s_is_null)", param.Name, param.Name)
case "float":
case phpFloat:
return fmt.Sprintf("\n Z_PARAM_DOUBLE_OR_NULL(%s, %s_is_null)", param.Name, param.Name)
case "bool":
case phpBool:
return fmt.Sprintf("\n Z_PARAM_BOOL_OR_NULL(%s, %s_is_null)", param.Name, param.Name)
case phpArray:
return fmt.Sprintf("\n Z_PARAM_ARRAY_OR_NULL(%s)", param.Name)
default:
return ""
}
} else {
switch param.PhpType {
case "string":
case phpString:
return fmt.Sprintf("\n Z_PARAM_STR(%s)", param.Name)
case "int":
case phpInt:
return fmt.Sprintf("\n Z_PARAM_LONG(%s)", param.Name)
case "float":
case phpFloat:
return fmt.Sprintf("\n Z_PARAM_DOUBLE(%s)", param.Name)
case "bool":
case phpBool:
return fmt.Sprintf("\n Z_PARAM_BOOL(%s)", param.Name)
case phpArray:
return fmt.Sprintf("\n Z_PARAM_ARRAY(%s)", param.Name)
default:
return ""
}
@@ -150,27 +156,31 @@ func (pp *ParameterParser) generateGoCallParams(params []phpParameter) string {
func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string {
if param.IsNullable {
switch param.PhpType {
case "string":
case phpString:
return fmt.Sprintf("%s_is_null ? NULL : %s", param.Name, param.Name)
case "int":
case phpInt:
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
case "float":
case phpFloat:
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
case "bool":
case phpBool:
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
case phpArray:
return param.Name
default:
return param.Name
}
} else {
switch param.PhpType {
case "string":
case phpString:
return param.Name
case "int":
case phpInt:
return fmt.Sprintf("(long) %s", param.Name)
case "float":
case phpFloat:
return fmt.Sprintf("(double) %s", param.Name)
case "bool":
case phpBool:
return fmt.Sprintf("(int) %s", param.Name)
case phpArray:
return param.Name
default:
return param.Name
}

View File

@@ -25,8 +25,8 @@ func TestParameterParser_AnalyzeParameters(t *testing.T) {
{
name: "all required parameters",
params: []phpParameter{
{Name: "name", PhpType: "string", HasDefault: false},
{Name: "count", PhpType: "int", HasDefault: false},
{Name: "name", PhpType: phpString, HasDefault: false},
{Name: "count", PhpType: phpInt, HasDefault: false},
},
expected: ParameterInfo{
RequiredCount: 2,
@@ -36,9 +36,9 @@ func TestParameterParser_AnalyzeParameters(t *testing.T) {
{
name: "mixed required and optional parameters",
params: []phpParameter{
{Name: "name", PhpType: "string", HasDefault: false},
{Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "10"},
{Name: "enabled", PhpType: "bool", HasDefault: true, DefaultValue: "true"},
{Name: "name", PhpType: phpString, HasDefault: false},
{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "10"},
{Name: "enabled", PhpType: phpBool, HasDefault: true, DefaultValue: "true"},
},
expected: ParameterInfo{
RequiredCount: 1,
@@ -71,75 +71,98 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
{
name: "string parameter",
params: []phpParameter{
{Name: "message", PhpType: "string", HasDefault: false},
{Name: "message", PhpType: phpString, HasDefault: false},
},
expected: " zend_string *message = NULL;",
},
{
name: "nullable string parameter",
params: []phpParameter{
{Name: "message", PhpType: "string", HasDefault: false, IsNullable: true},
{Name: "message", PhpType: phpString, HasDefault: false, IsNullable: true},
},
expected: " zend_string *message = NULL;\n zend_bool message_is_null = 0;",
},
{
name: "int parameter with default",
params: []phpParameter{
{Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "42"},
{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "42"},
},
expected: " zend_long count = 42;",
},
{
name: "nullable int parameter",
params: []phpParameter{
{Name: "count", PhpType: "int", HasDefault: false, IsNullable: true},
{Name: "count", PhpType: phpInt, HasDefault: false, IsNullable: true},
},
expected: " zend_long count = 0;\n zend_bool count_is_null = 0;",
},
{
name: "bool parameter with true default",
params: []phpParameter{
{Name: "enabled", PhpType: "bool", HasDefault: true, DefaultValue: "true"},
{Name: "enabled", PhpType: phpBool, HasDefault: true, DefaultValue: "true"},
},
expected: " zend_bool enabled = 1;",
},
{
name: "nullable bool parameter",
params: []phpParameter{
{Name: "enabled", PhpType: "bool", HasDefault: false, IsNullable: true},
{Name: "enabled", PhpType: phpBool, HasDefault: false, IsNullable: true},
},
expected: " zend_bool enabled = 0;\n zend_bool enabled_is_null = 0;",
},
{
name: "float parameter",
params: []phpParameter{
{Name: "ratio", PhpType: "float", HasDefault: false},
{Name: "ratio", PhpType: phpFloat, HasDefault: false},
},
expected: " double ratio = 0.0;",
},
{
name: "nullable float parameter",
params: []phpParameter{
{Name: "ratio", PhpType: "float", HasDefault: false, IsNullable: true},
{Name: "ratio", PhpType: phpFloat, HasDefault: false, IsNullable: true},
},
expected: " double ratio = 0.0;\n zend_bool ratio_is_null = 0;",
},
{
name: "multiple parameters",
params: []phpParameter{
{Name: "name", PhpType: "string", HasDefault: false},
{Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "10"},
{Name: "name", PhpType: phpString, HasDefault: false},
{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "10"},
},
expected: " zend_string *name = NULL;\n zend_long count = 10;",
},
{
name: "mixed nullable and non-nullable parameters",
params: []phpParameter{
{Name: "name", PhpType: "string", HasDefault: false, IsNullable: false},
{Name: "count", PhpType: "int", HasDefault: false, IsNullable: true},
{Name: "name", PhpType: phpString, HasDefault: false, IsNullable: false},
{Name: "count", PhpType: phpInt, HasDefault: false, IsNullable: true},
},
expected: " zend_string *name = NULL;\n zend_long count = 0;\n zend_bool count_is_null = 0;",
},
{
name: "array parameter",
params: []phpParameter{
{Name: "items", PhpType: phpArray, HasDefault: false},
},
expected: " zval *items = NULL;",
},
{
name: "nullable array parameter",
params: []phpParameter{
{Name: "items", PhpType: phpArray, HasDefault: false, IsNullable: true},
},
expected: " zval *items = NULL;",
},
{
name: "mixed types with array",
params: []phpParameter{
{Name: "name", PhpType: phpString, HasDefault: false},
{Name: "items", PhpType: phpArray, HasDefault: false},
{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "5"},
},
expected: " zend_string *name = NULL;\n zval *items = NULL;\n zend_long count = 5;",
},
}
for _, tt := range tests {
@@ -170,7 +193,7 @@ func TestParameterParser_GenerateParamParsing(t *testing.T) {
{
name: "single required string parameter",
params: []phpParameter{
{Name: "message", PhpType: "string", HasDefault: false},
{Name: "message", PhpType: phpString, HasDefault: false},
},
requiredCount: 1,
expected: ` ZEND_PARSE_PARAMETERS_START(1, 1)
@@ -180,9 +203,9 @@ func TestParameterParser_GenerateParamParsing(t *testing.T) {
{
name: "mixed required and optional parameters",
params: []phpParameter{
{Name: "name", PhpType: "string", HasDefault: false},
{Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "10"},
{Name: "enabled", PhpType: "bool", HasDefault: true, DefaultValue: "true"},
{Name: "name", PhpType: phpString, HasDefault: false},
{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "10"},
{Name: "enabled", PhpType: phpBool, HasDefault: true, DefaultValue: "true"},
},
requiredCount: 1,
expected: ` ZEND_PARSE_PARAMETERS_START(1, 3)
@@ -218,20 +241,43 @@ func TestParameterParser_GenerateGoCallParams(t *testing.T) {
{
name: "single string parameter",
params: []phpParameter{
{Name: "message", PhpType: "string"},
{Name: "message", PhpType: phpString},
},
expected: "message",
},
{
name: "multiple parameters of different types",
params: []phpParameter{
{Name: "name", PhpType: "string"},
{Name: "count", PhpType: "int"},
{Name: "ratio", PhpType: "float"},
{Name: "enabled", PhpType: "bool"},
{Name: "name", PhpType: phpString},
{Name: "count", PhpType: phpInt},
{Name: "ratio", PhpType: phpFloat},
{Name: "enabled", PhpType: phpBool},
},
expected: "name, (long) count, (double) ratio, (int) enabled",
},
{
name: "array parameter",
params: []phpParameter{
{Name: "items", PhpType: phpArray},
},
expected: "items",
},
{
name: "nullable array parameter",
params: []phpParameter{
{Name: "items", PhpType: phpArray, IsNullable: true},
},
expected: "items",
},
{
name: "mixed parameters with array",
params: []phpParameter{
{Name: "name", PhpType: phpString},
{Name: "items", PhpType: phpArray},
{Name: "count", PhpType: phpInt},
},
expected: "name, items, (long) count",
},
}
for _, tt := range tests {
@@ -252,47 +298,57 @@ func TestParameterParser_GenerateParamParsingMacro(t *testing.T) {
}{
{
name: "string parameter",
param: phpParameter{Name: "message", PhpType: "string"},
param: phpParameter{Name: "message", PhpType: phpString},
expected: "\n Z_PARAM_STR(message)",
},
{
name: "nullable string parameter",
param: phpParameter{Name: "message", PhpType: "string", IsNullable: true},
param: phpParameter{Name: "message", PhpType: phpString, IsNullable: true},
expected: "\n Z_PARAM_STR_OR_NULL(message, message_is_null)",
},
{
name: "int parameter",
param: phpParameter{Name: "count", PhpType: "int"},
param: phpParameter{Name: "count", PhpType: phpInt},
expected: "\n Z_PARAM_LONG(count)",
},
{
name: "nullable int parameter",
param: phpParameter{Name: "count", PhpType: "int", IsNullable: true},
param: phpParameter{Name: "count", PhpType: phpInt, IsNullable: true},
expected: "\n Z_PARAM_LONG_OR_NULL(count, count_is_null)",
},
{
name: "float parameter",
param: phpParameter{Name: "ratio", PhpType: "float"},
param: phpParameter{Name: "ratio", PhpType: phpFloat},
expected: "\n Z_PARAM_DOUBLE(ratio)",
},
{
name: "nullable float parameter",
param: phpParameter{Name: "ratio", PhpType: "float", IsNullable: true},
param: phpParameter{Name: "ratio", PhpType: phpFloat, IsNullable: true},
expected: "\n Z_PARAM_DOUBLE_OR_NULL(ratio, ratio_is_null)",
},
{
name: "bool parameter",
param: phpParameter{Name: "enabled", PhpType: "bool"},
param: phpParameter{Name: "enabled", PhpType: phpBool},
expected: "\n Z_PARAM_BOOL(enabled)",
},
{
name: "nullable bool parameter",
param: phpParameter{Name: "enabled", PhpType: "bool", IsNullable: true},
param: phpParameter{Name: "enabled", PhpType: phpBool, IsNullable: true},
expected: "\n Z_PARAM_BOOL_OR_NULL(enabled, enabled_is_null)",
},
{
name: "array parameter",
param: phpParameter{Name: "items", PhpType: phpArray},
expected: "\n Z_PARAM_ARRAY(items)",
},
{
name: "nullable array parameter",
param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true},
expected: "\n Z_PARAM_ARRAY_OR_NULL(items)",
},
{
name: "unknown type",
param: phpParameter{Name: "unknown", PhpType: "unknown"},
param: phpParameter{Name: "unknown", PhpType: phpType("unknown")},
expected: "",
},
}
@@ -316,19 +372,19 @@ func TestParameterParser_GetDefaultValue(t *testing.T) {
}{
{
name: "parameter without default",
param: phpParameter{Name: "count", PhpType: "int", HasDefault: false},
param: phpParameter{Name: "count", PhpType: phpInt, HasDefault: false},
fallback: "0",
expected: "0",
},
{
name: "parameter with default value",
param: phpParameter{Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "42"},
param: phpParameter{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "42"},
fallback: "0",
expected: "42",
},
{
name: "parameter with empty default value",
param: phpParameter{Name: "count", PhpType: "int", HasDefault: true, DefaultValue: ""},
param: phpParameter{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: ""},
fallback: "0",
expected: "0",
},
@@ -352,47 +408,57 @@ func TestParameterParser_GenerateSingleGoCallParam(t *testing.T) {
}{
{
name: "string parameter",
param: phpParameter{Name: "message", PhpType: "string"},
param: phpParameter{Name: "message", PhpType: phpString},
expected: "message",
},
{
name: "nullable string parameter",
param: phpParameter{Name: "message", PhpType: "string", IsNullable: true},
param: phpParameter{Name: "message", PhpType: phpString, IsNullable: true},
expected: "message_is_null ? NULL : message",
},
{
name: "int parameter",
param: phpParameter{Name: "count", PhpType: "int"},
param: phpParameter{Name: "count", PhpType: phpInt},
expected: "(long) count",
},
{
name: "nullable int parameter",
param: phpParameter{Name: "count", PhpType: "int", IsNullable: true},
param: phpParameter{Name: "count", PhpType: phpInt, IsNullable: true},
expected: "count_is_null ? NULL : &count",
},
{
name: "float parameter",
param: phpParameter{Name: "ratio", PhpType: "float"},
param: phpParameter{Name: "ratio", PhpType: phpFloat},
expected: "(double) ratio",
},
{
name: "nullable float parameter",
param: phpParameter{Name: "ratio", PhpType: "float", IsNullable: true},
param: phpParameter{Name: "ratio", PhpType: phpFloat, IsNullable: true},
expected: "ratio_is_null ? NULL : &ratio",
},
{
name: "bool parameter",
param: phpParameter{Name: "enabled", PhpType: "bool"},
param: phpParameter{Name: "enabled", PhpType: phpBool},
expected: "(int) enabled",
},
{
name: "nullable bool parameter",
param: phpParameter{Name: "enabled", PhpType: "bool", IsNullable: true},
param: phpParameter{Name: "enabled", PhpType: phpBool, IsNullable: true},
expected: "enabled_is_null ? NULL : &enabled",
},
{
name: "array parameter",
param: phpParameter{Name: "items", PhpType: phpArray},
expected: "items",
},
{
name: "nullable array parameter",
param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true},
expected: "items",
},
{
name: "unknown type",
param: phpParameter{Name: "unknown", PhpType: "unknown"},
param: phpParameter{Name: "unknown", PhpType: phpType("unknown")},
expected: "unknown",
},
}
@@ -415,49 +481,59 @@ func TestParameterParser_GenerateSingleParamDeclaration(t *testing.T) {
}{
{
name: "string parameter",
param: phpParameter{Name: "message", PhpType: "string", HasDefault: false},
param: phpParameter{Name: "message", PhpType: phpString, HasDefault: false},
expected: []string{"zend_string *message = NULL;"},
},
{
name: "nullable string parameter",
param: phpParameter{Name: "message", PhpType: "string", HasDefault: false, IsNullable: true},
param: phpParameter{Name: "message", PhpType: phpString, HasDefault: false, IsNullable: true},
expected: []string{"zend_string *message = NULL;", "zend_bool message_is_null = 0;"},
},
{
name: "int parameter with default",
param: phpParameter{Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "42"},
param: phpParameter{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "42"},
expected: []string{"zend_long count = 42;"},
},
{
name: "nullable int parameter",
param: phpParameter{Name: "count", PhpType: "int", HasDefault: false, IsNullable: true},
param: phpParameter{Name: "count", PhpType: phpInt, HasDefault: false, IsNullable: true},
expected: []string{"zend_long count = 0;", "zend_bool count_is_null = 0;"},
},
{
name: "bool parameter with true default",
param: phpParameter{Name: "enabled", PhpType: "bool", HasDefault: true, DefaultValue: "true"},
param: phpParameter{Name: "enabled", PhpType: phpBool, HasDefault: true, DefaultValue: "true"},
expected: []string{"zend_bool enabled = 1;"},
},
{
name: "nullable bool parameter",
param: phpParameter{Name: "enabled", PhpType: "bool", HasDefault: false, IsNullable: true},
param: phpParameter{Name: "enabled", PhpType: phpBool, HasDefault: false, IsNullable: true},
expected: []string{"zend_bool enabled = 0;", "zend_bool enabled_is_null = 0;"},
},
{
name: "bool parameter with false default",
param: phpParameter{Name: "disabled", PhpType: "bool", HasDefault: true, DefaultValue: "false"},
param: phpParameter{Name: "disabled", PhpType: phpBool, HasDefault: true, DefaultValue: "false"},
expected: []string{"zend_bool disabled = false;"},
},
{
name: "float parameter",
param: phpParameter{Name: "ratio", PhpType: "float", HasDefault: false},
param: phpParameter{Name: "ratio", PhpType: phpFloat, HasDefault: false},
expected: []string{"double ratio = 0.0;"},
},
{
name: "nullable float parameter",
param: phpParameter{Name: "ratio", PhpType: "float", HasDefault: false, IsNullable: true},
param: phpParameter{Name: "ratio", PhpType: phpFloat, HasDefault: false, IsNullable: true},
expected: []string{"double ratio = 0.0;", "zend_bool ratio_is_null = 0;"},
},
{
name: "array parameter",
param: phpParameter{Name: "items", PhpType: phpArray, HasDefault: false},
expected: []string{"zval *items = NULL;"},
},
{
name: "nullable array parameter",
param: phpParameter{Name: "items", PhpType: phpArray, HasDefault: false, IsNullable: true},
expected: []string{"zval *items = NULL;"},
},
}
for _, tt := range tests {
@@ -472,9 +548,9 @@ func TestParameterParser_Integration(t *testing.T) {
pp := &ParameterParser{}
params := []phpParameter{
{Name: "name", PhpType: "string", HasDefault: false},
{Name: "count", PhpType: "int", HasDefault: true, DefaultValue: "10"},
{Name: "enabled", PhpType: "bool", HasDefault: true, DefaultValue: "true"},
{Name: "name", PhpType: phpString, HasDefault: false},
{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "10"},
{Name: "enabled", PhpType: phpBool, HasDefault: true, DefaultValue: "true"},
}
info := pp.analyzeParameters(params)

View File

@@ -38,46 +38,58 @@ func (pfg *PHPFuncGenerator) generate(fn phpFunction) string {
func (pfg *PHPFuncGenerator) generateGoCall(fn phpFunction) string {
callParams := pfg.paramParser.generateGoCallParams(fn.Params)
if fn.ReturnType == "void" {
if fn.ReturnType == phpVoid {
return fmt.Sprintf(" %s(%s);", fn.Name, callParams)
}
if fn.ReturnType == "string" {
if fn.ReturnType == phpString {
return fmt.Sprintf(" zend_string *result = %s(%s);", fn.Name, callParams)
}
if fn.ReturnType == phpArray {
return fmt.Sprintf(" zend_array *result = %s(%s);", fn.Name, callParams)
}
return fmt.Sprintf(" %s result = %s(%s);", pfg.getCReturnType(fn.ReturnType), fn.Name, callParams)
}
func (pfg *PHPFuncGenerator) getCReturnType(returnType string) string {
func (pfg *PHPFuncGenerator) getCReturnType(returnType phpType) string {
switch returnType {
case "string":
case phpString:
return "zend_string*"
case "int":
case phpInt:
return "long"
case "float":
case phpFloat:
return "double"
case "bool":
case phpBool:
return "int"
case phpArray:
return "zend_array*"
default:
return "void"
}
}
func (pfg *PHPFuncGenerator) generateReturnCode(returnType string) string {
func (pfg *PHPFuncGenerator) generateReturnCode(returnType phpType) string {
switch returnType {
case "string":
case phpString:
return ` if (result) {
RETURN_STR(result);
} else {
RETURN_EMPTY_STRING();
}`
case "int":
}
RETURN_EMPTY_STRING();`
case phpInt:
return ` RETURN_LONG(result);`
case "float":
case phpFloat:
return ` RETURN_DOUBLE(result);`
case "bool":
case phpBool:
return ` RETURN_BOOL(result);`
case phpArray:
return ` if (result) {
RETURN_ARR(result);
}
RETURN_EMPTY_ARRAY();`
default:
return ""
}

View File

@@ -17,9 +17,9 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
name: "simple string function",
function: phpFunction{
Name: "greet",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "name", PhpType: "string"},
{Name: "name", PhpType: phpString},
},
},
contains: []string{
@@ -34,10 +34,10 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
name: "function with default parameter",
function: phpFunction{
Name: "calculate",
ReturnType: "int",
ReturnType: phpInt,
Params: []phpParameter{
{Name: "base", PhpType: "int"},
{Name: "multiplier", PhpType: "int", HasDefault: true, DefaultValue: "2"},
{Name: "base", PhpType: phpInt},
{Name: "multiplier", PhpType: phpInt, HasDefault: true, DefaultValue: "2"},
},
},
contains: []string{
@@ -54,9 +54,9 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
name: "void function",
function: phpFunction{
Name: "doSomething",
ReturnType: "void",
ReturnType: phpVoid,
Params: []phpParameter{
{Name: "action", PhpType: "string"},
{Name: "action", PhpType: phpString},
},
},
contains: []string{
@@ -68,9 +68,9 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
name: "bool function with default",
function: phpFunction{
Name: "isEnabled",
ReturnType: "bool",
ReturnType: phpBool,
Params: []phpParameter{
{Name: "flag", PhpType: "bool", HasDefault: true, DefaultValue: "true"},
{Name: "flag", PhpType: phpBool, HasDefault: true, DefaultValue: "true"},
},
},
contains: []string{
@@ -84,9 +84,9 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
name: "float function",
function: phpFunction{
Name: "calculate",
ReturnType: "float",
ReturnType: phpFloat,
Params: []phpParameter{
{Name: "value", PhpType: "float"},
{Name: "value", PhpType: phpFloat},
},
},
contains: []string{
@@ -96,6 +96,46 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
"RETURN_DOUBLE(result)",
},
},
{
name: "array function with array parameter",
function: phpFunction{
Name: "process_array",
ReturnType: phpArray,
Params: []phpParameter{
{Name: "input", PhpType: phpArray},
},
},
contains: []string{
"PHP_FUNCTION(process_array)",
"zval *input = NULL;",
"Z_PARAM_ARRAY(input)",
"zend_array *result = process_array(input);",
"RETURN_ARR(result)",
},
},
{
name: "array function with mixed parameters",
function: phpFunction{
Name: "filter_array",
ReturnType: phpArray,
Params: []phpParameter{
{Name: "data", PhpType: phpArray},
{Name: "key", PhpType: phpString},
{Name: "limit", PhpType: phpInt, HasDefault: true, DefaultValue: "10"},
},
},
contains: []string{
"PHP_FUNCTION(filter_array)",
"zval *data = NULL;",
"zend_string *key = NULL;",
"zend_long limit = 10;",
"Z_PARAM_ARRAY(data)",
"Z_PARAM_STR(key)",
"Z_PARAM_LONG(limit)",
"ZEND_PARSE_PARAMETERS_START(2, 3)",
"Z_PARAM_OPTIONAL",
},
},
}
generator := PHPFuncGenerator{}
@@ -122,7 +162,7 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) {
{
name: "string parameter",
params: []phpParameter{
{Name: "message", PhpType: "string"},
{Name: "message", PhpType: phpString},
},
contains: []string{
"zend_string *message = NULL;",
@@ -131,7 +171,7 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) {
{
name: "int parameter",
params: []phpParameter{
{Name: "count", PhpType: "int"},
{Name: "count", PhpType: phpInt},
},
contains: []string{
"zend_long count = 0;",
@@ -140,7 +180,7 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) {
{
name: "bool with default",
params: []phpParameter{
{Name: "enabled", PhpType: "bool", HasDefault: true, DefaultValue: "true"},
{Name: "enabled", PhpType: phpBool, HasDefault: true, DefaultValue: "true"},
},
contains: []string{
"zend_bool enabled = 1;",
@@ -149,12 +189,34 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) {
{
name: "float parameter with default",
params: []phpParameter{
{Name: "rate", PhpType: "float", HasDefault: true, DefaultValue: "1.5"},
{Name: "rate", PhpType: phpFloat, HasDefault: true, DefaultValue: "1.5"},
},
contains: []string{
"double rate = 1.5;",
},
},
{
name: "array parameter",
params: []phpParameter{
{Name: "items", PhpType: phpArray},
},
contains: []string{
"zval *items = NULL;",
},
},
{
name: "mixed types with array",
params: []phpParameter{
{Name: "name", PhpType: phpString},
{Name: "data", PhpType: phpArray},
{Name: "count", PhpType: phpInt},
},
contains: []string{
"zend_string *name = NULL;",
"zval *data = NULL;",
"zend_long count = 0;",
},
},
}
parser := ParameterParser{}
@@ -172,12 +234,12 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) {
func TestPHPFunctionGenerator_GenerateReturnCode(t *testing.T) {
tests := []struct {
name string
returnType string
returnType phpType
contains []string
}{
{
name: "string return",
returnType: "string",
returnType: phpString,
contains: []string{
"RETURN_STR(result)",
"RETURN_EMPTY_STRING()",
@@ -185,28 +247,36 @@ func TestPHPFunctionGenerator_GenerateReturnCode(t *testing.T) {
},
{
name: "int return",
returnType: "int",
returnType: phpInt,
contains: []string{
"RETURN_LONG(result)",
},
},
{
name: "bool return",
returnType: "bool",
returnType: phpBool,
contains: []string{
"RETURN_BOOL(result)",
},
},
{
name: "float return",
returnType: "float",
returnType: phpFloat,
contains: []string{
"RETURN_DOUBLE(result)",
},
},
{
name: "array return",
returnType: phpArray,
contains: []string{
"RETURN_ARR(result)",
"RETURN_EMPTY_ARRAY()",
},
},
{
name: "void return",
returnType: "void",
returnType: phpVoid,
contains: []string{},
},
}
@@ -214,7 +284,7 @@ func TestPHPFunctionGenerator_GenerateReturnCode(t *testing.T) {
generator := PHPFuncGenerator{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := generator.generateReturnCode(tt.returnType)
result := generator.generateReturnCode(phpType(tt.returnType))
if len(tt.contains) == 0 {
assert.Empty(t, result, "Return code should be empty for void")
@@ -242,33 +312,49 @@ func TestPHPFunctionGenerator_GenerateGoCallParams(t *testing.T) {
{
name: "simple string parameter",
params: []phpParameter{
{Name: "message", PhpType: "string"},
{Name: "message", PhpType: phpString},
},
expected: "message",
},
{
name: "int parameter",
params: []phpParameter{
{Name: "count", PhpType: "int"},
{Name: "count", PhpType: phpInt},
},
expected: "(long) count",
},
{
name: "multiple parameters",
params: []phpParameter{
{Name: "name", PhpType: "string"},
{Name: "age", PhpType: "int"},
{Name: "name", PhpType: phpString},
{Name: "age", PhpType: phpInt},
},
expected: "name, (long) age",
},
{
name: "bool and float parameters",
params: []phpParameter{
{Name: "enabled", PhpType: "bool"},
{Name: "rate", PhpType: "float"},
{Name: "enabled", PhpType: phpBool},
{Name: "rate", PhpType: phpFloat},
},
expected: "(int) enabled, (double) rate",
},
{
name: "array parameter",
params: []phpParameter{
{Name: "data", PhpType: phpArray},
},
expected: "data",
},
{
name: "mixed parameters with array",
params: []phpParameter{
{Name: "name", PhpType: phpString},
{Name: "items", PhpType: phpArray},
{Name: "count", PhpType: phpInt},
},
expected: "name, items, (long) count",
},
}
parser := ParameterParser{}
@@ -297,8 +383,8 @@ func TestPHPFunctionGenerator_AnalyzeParameters(t *testing.T) {
{
name: "all required",
params: []phpParameter{
{Name: "a", PhpType: "string"},
{Name: "b", PhpType: "int"},
{Name: "a", PhpType: phpString},
{Name: "b", PhpType: phpInt},
},
expectedReq: 2,
expectedTotal: 2,
@@ -306,8 +392,8 @@ func TestPHPFunctionGenerator_AnalyzeParameters(t *testing.T) {
{
name: "mixed required and optional",
params: []phpParameter{
{Name: "required", PhpType: "string"},
{Name: "optional", PhpType: "int", HasDefault: true, DefaultValue: "10"},
{Name: "required", PhpType: phpString},
{Name: "optional", PhpType: phpInt, HasDefault: true, DefaultValue: "10"},
},
expectedReq: 1,
expectedTotal: 2,
@@ -315,8 +401,8 @@ func TestPHPFunctionGenerator_AnalyzeParameters(t *testing.T) {
{
name: "all optional",
params: []phpParameter{
{Name: "opt1", PhpType: "string", HasDefault: true, DefaultValue: "hello"},
{Name: "opt2", PhpType: "int", HasDefault: true, DefaultValue: "0"},
{Name: "opt1", PhpType: phpString, HasDefault: true, DefaultValue: "hello"},
{Name: "opt2", PhpType: phpInt, HasDefault: true, DefaultValue: "0"},
},
expectedReq: 0,
expectedTotal: 2,

View File

@@ -40,11 +40,19 @@ func (sg *StubGenerator) buildContent() (string, error) {
return buf.String(), nil
}
// getPhpTypeAnnotation converts Go constant type to PHP type annotation
func getPhpTypeAnnotation(goType string) string {
switch goType {
case "string", "bool", "float", "int":
return goType
// getPhpTypeAnnotation converts phpType to PHP type annotation
func getPhpTypeAnnotation(t phpType) string {
switch t {
case phpString:
return "string"
case phpBool:
return "bool"
case phpFloat:
return "float"
case phpInt:
return "int"
case phpArray:
return "array"
default:
return "int"
}

View File

@@ -19,18 +19,18 @@ func TestStubGenerator_Generate(t *testing.T) {
Name: "greet",
Signature: "greet(string $name): string",
Params: []phpParameter{
{Name: "name", PhpType: "string"},
{Name: "name", PhpType: phpString},
},
ReturnType: "string",
ReturnType: phpString,
},
{
Name: "calculate",
Signature: "calculate(int $a, int $b): int",
Params: []phpParameter{
{Name: "a", PhpType: "int"},
{Name: "b", PhpType: "int"},
{Name: "a", PhpType: phpInt},
{Name: "b", PhpType: phpInt},
},
ReturnType: "int",
ReturnType: phpInt,
},
},
Classes: []phpClass{
@@ -43,12 +43,12 @@ func TestStubGenerator_Generate(t *testing.T) {
{
Name: "GLOBAL_CONST",
Value: "42",
PhpType: "int",
PhpType: phpInt,
},
{
Name: "USER_STATUS_ACTIVE",
Value: "1",
PhpType: "int",
PhpType: phpInt,
ClassName: "User",
},
},
@@ -128,7 +128,7 @@ func TestStubGenerator_BuildContent(t *testing.T) {
{
Name: "GLOBAL_CONST",
Value: `"test"`,
PhpType: "string",
PhpType: phpString,
},
},
contains: []string{
@@ -430,13 +430,13 @@ func TestStubGenerator_ClassConstants(t *testing.T) {
{
Name: "STATUS_ACTIVE",
Value: "1",
PhpType: "int",
PhpType: phpInt,
ClassName: "MyClass",
},
{
Name: "STATUS_INACTIVE",
Value: "0",
PhpType: "int",
PhpType: phpInt,
ClassName: "MyClass",
},
},
@@ -456,14 +456,14 @@ func TestStubGenerator_ClassConstants(t *testing.T) {
{
Name: "FIRST",
Value: "0",
PhpType: "int",
PhpType: phpInt,
IsIota: true,
ClassName: "StatusClass",
},
{
Name: "SECOND",
Value: "1",
PhpType: "int",
PhpType: phpInt,
IsIota: true,
ClassName: "StatusClass",
},
@@ -485,12 +485,12 @@ func TestStubGenerator_ClassConstants(t *testing.T) {
{
Name: "GLOBAL_CONST",
Value: `"global"`,
PhpType: "string",
PhpType: phpString,
},
{
Name: "CLASS_CONST",
Value: "42",
PhpType: "int",
PhpType: phpInt,
ClassName: "TestClass",
},
},

View File

@@ -1,5 +1,7 @@
#include <php.h>
#include <Zend/zend_API.h>
#include <Zend/zend_hash.h>
#include <Zend/zend_types.h>
#include <stddef.h>
#include "{{.BaseName}}.h"
@@ -92,6 +94,8 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
{{- else if eq $param.PhpType "bool"}}
zend_bool {{$param.Name}} = {{if $param.HasDefault}}{{if eq $param.DefaultValue "true"}}1{{else}}0{{end}}{{else}}0{{end}};{{if $param.IsNullable}}
zend_bool {{$param.Name}}_is_null = 0;{{end}}
{{- else if eq $param.PhpType "array"}}
zval *{{$param.Name}} = NULL;
{{- end}}
{{- end}}
@@ -100,7 +104,7 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
{{$optionalStarted := false}}{{range .Params}}{{if .HasDefault}}{{if not $optionalStarted -}}
Z_PARAM_OPTIONAL
{{$optionalStarted = true}}{{end}}{{end -}}
{{if .IsNullable}}{{if eq .PhpType "string"}}Z_PARAM_STR_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "int"}}Z_PARAM_LONG_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "float"}}Z_PARAM_DOUBLE_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "bool"}}Z_PARAM_BOOL_OR_NULL({{.Name}}, {{.Name}}_is_null){{end}}{{else}}{{if eq .PhpType "string"}}Z_PARAM_STR({{.Name}}){{else if eq .PhpType "int"}}Z_PARAM_LONG({{.Name}}){{else if eq .PhpType "float"}}Z_PARAM_DOUBLE({{.Name}}){{else if eq .PhpType "bool"}}Z_PARAM_BOOL({{.Name}}){{end}}{{end}}
{{if .IsNullable}}{{if eq .PhpType "string"}}Z_PARAM_STR_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "int"}}Z_PARAM_LONG_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "float"}}Z_PARAM_DOUBLE_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "bool"}}Z_PARAM_BOOL_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "array"}}Z_PARAM_ARRAY_OR_NULL({{.Name}}){{end}}{{else}}{{if eq .PhpType "string"}}Z_PARAM_STR({{.Name}}){{else if eq .PhpType "int"}}Z_PARAM_LONG({{.Name}}){{else if eq .PhpType "float"}}Z_PARAM_DOUBLE({{.Name}}){{else if eq .PhpType "bool"}}Z_PARAM_BOOL({{.Name}}){{else if eq .PhpType "array"}}Z_PARAM_ARRAY({{.Name}}){{end}}{{end}}
{{end -}}
ZEND_PARSE_PARAMETERS_END();
{{else}}
@@ -109,20 +113,28 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
{{- if ne .ReturnType "void"}}
{{- if eq .ReturnType "string"}}
zend_string* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{end}}{{else}}{{.Name}}{{end}}{{end}}{{end}});
zend_string* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{.Name}}{{end}}{{end}}{{end}});
RETURN_STR(result);
{{- else if eq .ReturnType "int"}}
zend_long result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{end}}{{else}}(long){{.Name}}{{end}}{{end}}{{end}});
zend_long result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else}}(long){{.Name}}{{end}}{{end}}{{end}}{{end}});
RETURN_LONG(result);
{{- else if eq .ReturnType "float"}}
double result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{end}}{{else}}(double){{.Name}}{{end}}{{end}}{{end}});
double result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else}}(double){{.Name}}{{end}}{{end}}{{end}}{{end}});
RETURN_DOUBLE(result);
{{- else if eq .ReturnType "bool"}}
int result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{end}}{{else}}(int){{.Name}}{{end}}{{end}}{{end}});
int result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else}}(int){{.Name}}{{end}}{{end}}{{end}}{{end}});
RETURN_BOOL(result);
{{- else if eq .ReturnType "array"}}
void* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{.Name}}{{end}}{{end}}{{end}});
if (result != NULL) {
HashTable *ht = (HashTable*)result;
RETURN_ARR(ht);
} else {
RETURN_NULL();
}
{{- end}}
{{- else}}
{{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{end}}{{else}}{{if eq .PhpType "string"}}{{.Name}}{{else if eq .PhpType "int"}}(long){{.Name}}{{else if eq .PhpType "float"}}(double){{.Name}}{{else if eq .PhpType "bool"}}(int){{.Name}}{{end}}{{end}}{{end}}{{end}});
{{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{if eq .PhpType "string"}}{{.Name}}{{else if eq .PhpType "int"}}(long){{.Name}}{{else if eq .PhpType "float"}}(double){{.Name}}{{else if eq .PhpType "bool"}}(int){{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{end}}{{end}}{{end}});
{{- end}}
}
{{end}}{{end}}

View File

@@ -55,7 +55,7 @@ func removeGoObject(handle C.uintptr_t) {
{{- end}}
{{- range .Classes}}
{{- range $class := .Classes}}
//export create_{{.GoStruct}}_object
func create_{{.GoStruct}}_object() C.uintptr_t {
obj := &{{.GoStruct}}{}
@@ -70,6 +70,22 @@ func create_{{.GoStruct}}_object() C.uintptr_t {
{{- range .Methods}}
//export {{.Name}}_wrapper
{{.Wrapper}}
func {{.Name}}_wrapper(handle C.uintptr_t{{range .Params}}{{if eq .PhpType "string"}}, {{.Name}} *C.zend_string{{else if eq .PhpType "array"}}, {{.Name}} *C.zval{{else}}, {{.Name}} {{if .IsNullable}}*{{end}}{{phpTypeToGoType .PhpType}}{{end}}{{end}}){{if not (isVoid .ReturnType)}}{{if isStringOrArray .ReturnType}} unsafe.Pointer{{else}} {{phpTypeToGoType .ReturnType}}{{end}}{{end}} {
obj := getGoObject(handle)
if obj == nil {
{{- if not (isVoid .ReturnType)}}
{{- if isStringOrArray .ReturnType}}
return nil
{{- else}}
var zero {{phpTypeToGoType .ReturnType}}
return zero
{{- end}}
{{- else}}
return
{{- end}}
}
structObj := obj.(*{{$class.GoStruct}})
{{if not (isVoid .ReturnType)}}return {{end}}structObj.{{.Name | title}}({{range $i, $param := .Params}}{{if $i}}, {{end}}{{$param.Name}}{{end}})
}
{{end}}
{{- end}}

View File

@@ -9,6 +9,22 @@ import (
"strings"
)
func scalarTypes() []phpType {
return []phpType{phpString, phpInt, phpFloat, phpBool, phpArray}
}
func paramTypes() []phpType {
return []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed}
}
func returnTypes() []phpType {
return []phpType{phpVoid, phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed, phpNull, phpTrue, phpFalse}
}
func propTypes() []phpType {
return []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed}
}
var functionNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
var parameterNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
var classNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
@@ -47,17 +63,17 @@ func (v *Validator) validateParameter(param phpParameter) error {
return fmt.Errorf("invalid parameter name: %s", param.Name)
}
validTypes := []string{"string", "int", "float", "bool", "array", "object", "mixed"}
if !v.isValidType(param.PhpType, validTypes) {
validTypes := paramTypes()
if !v.isValidPHPType(param.PhpType, validTypes) {
return fmt.Errorf("invalid parameter type: %s", param.PhpType)
}
return nil
}
func (v *Validator) validateReturnType(returnType string) error {
validReturnTypes := []string{"void", "string", "int", "float", "bool", "array", "object", "mixed", "null", "true", "false"}
if !v.isValidType(returnType, validReturnTypes) {
func (v *Validator) validateReturnType(returnType phpType) error {
validReturnTypes := returnTypes()
if !v.isValidPHPType(returnType, validReturnTypes) {
return fmt.Errorf("invalid return type: %s", returnType)
}
return nil
@@ -90,17 +106,17 @@ func (v *Validator) validateClassProperty(prop phpClassProperty) error {
return fmt.Errorf("invalid property name: %s", prop.Name)
}
validTypes := []string{"string", "int", "float", "bool", "array", "object", "mixed"}
if !v.isValidType(prop.PhpType, validTypes) {
validTypes := propTypes()
if !v.isValidPHPType(prop.PhpType, validTypes) {
return fmt.Errorf("invalid property type: %s", prop.PhpType)
}
return nil
}
func (v *Validator) isValidType(typeStr string, validTypes []string) bool {
func (v *Validator) isValidPHPType(phpType phpType, validTypes []phpType) bool {
for _, valid := range validTypes {
if typeStr == valid {
if phpType == valid {
return true
}
}
@@ -109,22 +125,22 @@ func (v *Validator) isValidType(typeStr string, validTypes []string) bool {
// validateScalarTypes checks if PHP signature contains only supported scalar types
func (v *Validator) validateScalarTypes(fn phpFunction) error {
supportedTypes := []string{"string", "int", "float", "bool"}
supportedTypes := scalarTypes()
for i, param := range fn.Params {
if !v.isScalarType(param.PhpType, supportedTypes) {
return fmt.Errorf("parameter %d (%s) has unsupported type '%s'. Only scalar types (string, int, float, bool) and their nullable variants are supported", i+1, param.Name, param.PhpType)
if !v.isScalarPHPType(param.PhpType, supportedTypes) {
return fmt.Errorf("parameter %d (%s) has unsupported type '%s'. Only scalar types (string, int, float, bool, array) and their nullable variants are supported", i+1, param.Name, param.PhpType)
}
}
if fn.ReturnType != "void" && !v.isScalarType(fn.ReturnType, supportedTypes) {
return fmt.Errorf("return type '%s' is not supported. Only scalar types (string, int, float, bool), void, and their nullable variants are supported", fn.ReturnType)
if fn.ReturnType != phpVoid && !v.isScalarPHPType(fn.ReturnType, supportedTypes) {
return fmt.Errorf("return type '%s' is not supported. Only scalar types (string, int, float, bool, array), void, and their nullable variants are supported", fn.ReturnType)
}
return nil
}
func (v *Validator) isScalarType(phpType string, supportedTypes []string) bool {
func (v *Validator) isScalarPHPType(phpType phpType, supportedTypes []phpType) bool {
for _, supported := range supportedTypes {
if phpType == supported {
return true
@@ -197,7 +213,7 @@ func (v *Validator) validateGoFunctionSignatureWithOptions(phpFunc phpFunction,
}
}
expectedGoReturnType := v.phpReturnTypeToGoType(phpFunc.ReturnType, phpFunc.IsReturnNullable)
expectedGoReturnType := v.phpReturnTypeToGoType(phpFunc.ReturnType)
actualGoReturnType := v.goReturnTypeToString(goFunc.Type.Results)
if !v.isCompatibleGoType(expectedGoReturnType, actualGoReturnType) {
@@ -207,22 +223,24 @@ func (v *Validator) validateGoFunctionSignatureWithOptions(phpFunc phpFunction,
return nil
}
func (v *Validator) phpTypeToGoType(phpType string, isNullable bool) string {
func (v *Validator) phpTypeToGoType(t phpType, isNullable bool) string {
var baseType string
switch phpType {
case "string":
switch t {
case phpString:
baseType = "*C.zend_string"
case "int":
case phpInt:
baseType = "int64"
case "float":
case phpFloat:
baseType = "float64"
case "bool":
case phpBool:
baseType = "bool"
case phpArray:
baseType = "*C.zval"
default:
baseType = "interface{}"
}
if isNullable && phpType != "string" {
if isNullable && t != phpString && t != phpArray {
return "*" + baseType
}
@@ -247,18 +265,20 @@ func (v *Validator) isCompatibleGoType(expectedType, actualType string) bool {
return false
}
func (v *Validator) phpReturnTypeToGoType(phpReturnType string, isNullable bool) string {
func (v *Validator) phpReturnTypeToGoType(phpReturnType phpType) string {
switch phpReturnType {
case "void":
case phpVoid:
return ""
case "string":
case phpString:
return "unsafe.Pointer"
case "int":
case phpInt:
return "int64"
case "float":
case phpFloat:
return "float64"
case "bool":
case phpBool:
return "bool"
case phpArray:
return "unsafe.Pointer"
default:
return "interface{}"
}

View File

@@ -16,10 +16,10 @@ func TestValidateFunction(t *testing.T) {
name: "valid function",
function: phpFunction{
Name: "validFunction",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "param1", PhpType: "string"},
{Name: "param2", PhpType: "int"},
{Name: "param1", PhpType: phpString},
{Name: "param2", PhpType: phpInt},
},
},
expectError: false,
@@ -28,10 +28,34 @@ func TestValidateFunction(t *testing.T) {
name: "valid function with nullable return",
function: phpFunction{
Name: "nullableReturn",
ReturnType: "string",
ReturnType: phpString,
IsReturnNullable: true,
Params: []phpParameter{
{Name: "data", PhpType: "array"},
{Name: "data", PhpType: phpArray},
},
},
expectError: false,
},
{
name: "valid function with array parameter",
function: phpFunction{
Name: "arrayFunction",
ReturnType: phpArray,
Params: []phpParameter{
{Name: "items", PhpType: phpArray},
{Name: "filter", PhpType: phpString},
},
},
expectError: false,
},
{
name: "valid function with nullable array parameter",
function: phpFunction{
Name: "nullableArrayFunction",
ReturnType: phpString,
Params: []phpParameter{
{Name: "items", PhpType: phpArray, IsNullable: true},
{Name: "name", PhpType: phpString},
},
},
expectError: false,
@@ -40,7 +64,7 @@ func TestValidateFunction(t *testing.T) {
name: "empty function name",
function: phpFunction{
Name: "",
ReturnType: "string",
ReturnType: phpString,
},
expectError: true,
},
@@ -48,7 +72,7 @@ func TestValidateFunction(t *testing.T) {
name: "invalid function name - starts with number",
function: phpFunction{
Name: "123invalid",
ReturnType: "string",
ReturnType: phpString,
},
expectError: true,
},
@@ -56,7 +80,7 @@ func TestValidateFunction(t *testing.T) {
name: "invalid function name - contains special chars",
function: phpFunction{
Name: "invalid-name",
ReturnType: "string",
ReturnType: phpString,
},
expectError: true,
},
@@ -64,9 +88,9 @@ func TestValidateFunction(t *testing.T) {
name: "invalid parameter name",
function: phpFunction{
Name: "validName",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "123invalid", PhpType: "string"},
{Name: "123invalid", PhpType: phpString},
},
},
expectError: true,
@@ -75,9 +99,9 @@ func TestValidateFunction(t *testing.T) {
name: "empty parameter name",
function: phpFunction{
Name: "validName",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "", PhpType: "string"},
{Name: "", PhpType: phpString},
},
},
expectError: true,
@@ -154,7 +178,7 @@ func TestValidateReturnType(t *testing.T) {
validator := Validator{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validator.validateReturnType(tt.returnType)
err := validator.validateReturnType(phpType(tt.returnType))
if tt.expectError {
assert.Error(t, err, "validateReturnType(%s) should return an error", tt.returnType)
@@ -175,7 +199,7 @@ func TestValidateClassProperty(t *testing.T) {
name: "valid property",
prop: phpClassProperty{
Name: "validProperty",
PhpType: "string",
PhpType: phpString,
GoType: "string",
},
expectError: false,
@@ -184,7 +208,7 @@ func TestValidateClassProperty(t *testing.T) {
name: "valid nullable property",
prop: phpClassProperty{
Name: "nullableProperty",
PhpType: "int",
PhpType: phpInt,
GoType: "*int",
IsNullable: true,
},
@@ -194,7 +218,7 @@ func TestValidateClassProperty(t *testing.T) {
name: "empty property name",
prop: phpClassProperty{
Name: "",
PhpType: "string",
PhpType: phpString,
},
expectError: true,
},
@@ -202,7 +226,7 @@ func TestValidateClassProperty(t *testing.T) {
name: "invalid property name",
prop: phpClassProperty{
Name: "123invalid",
PhpType: "string",
PhpType: phpString,
},
expectError: true,
},
@@ -210,7 +234,7 @@ func TestValidateClassProperty(t *testing.T) {
name: "invalid property type",
prop: phpClassProperty{
Name: "validName",
PhpType: "invalidType",
PhpType: phpType("invalidType"),
},
expectError: true,
},
@@ -240,7 +264,7 @@ func TestValidateParameter(t *testing.T) {
name: "valid string parameter",
param: phpParameter{
Name: "validParam",
PhpType: "string",
PhpType: phpString,
},
expectError: false,
},
@@ -248,7 +272,7 @@ func TestValidateParameter(t *testing.T) {
name: "valid nullable parameter",
param: phpParameter{
Name: "nullableParam",
PhpType: "int",
PhpType: phpInt,
IsNullable: true,
},
expectError: false,
@@ -257,17 +281,34 @@ func TestValidateParameter(t *testing.T) {
name: "valid parameter with default",
param: phpParameter{
Name: "defaultParam",
PhpType: "string",
PhpType: phpString,
HasDefault: true,
DefaultValue: "hello",
},
expectError: false,
},
{
name: "valid array parameter",
param: phpParameter{
Name: "arrayParam",
PhpType: phpArray,
},
expectError: false,
},
{
name: "valid nullable array parameter",
param: phpParameter{
Name: "nullableArrayParam",
PhpType: phpArray,
IsNullable: true,
},
expectError: false,
},
{
name: "empty parameter name",
param: phpParameter{
Name: "",
PhpType: "string",
PhpType: phpString,
},
expectError: true,
},
@@ -275,7 +316,7 @@ func TestValidateParameter(t *testing.T) {
name: "invalid parameter name",
param: phpParameter{
Name: "123invalid",
PhpType: "string",
PhpType: phpString,
},
expectError: true,
},
@@ -283,7 +324,7 @@ func TestValidateParameter(t *testing.T) {
name: "invalid parameter type",
param: phpParameter{
Name: "validName",
PhpType: "invalidType",
PhpType: phpType("invalidType"),
},
expectError: true,
},
@@ -315,8 +356,8 @@ func TestValidateClass(t *testing.T) {
Name: "ValidClass",
GoStruct: "ValidStruct",
Properties: []phpClassProperty{
{Name: "name", PhpType: "string"},
{Name: "age", PhpType: "int"},
{Name: "name", PhpType: phpString},
{Name: "age", PhpType: phpInt},
},
},
expectError: false,
@@ -327,8 +368,8 @@ func TestValidateClass(t *testing.T) {
Name: "NullableClass",
GoStruct: "NullableStruct",
Properties: []phpClassProperty{
{Name: "required", PhpType: "string", IsNullable: false},
{Name: "optional", PhpType: "string", IsNullable: true},
{Name: "required", PhpType: phpString, IsNullable: false},
{Name: "optional", PhpType: phpString, IsNullable: true},
},
},
expectError: false,
@@ -355,7 +396,7 @@ func TestValidateClass(t *testing.T) {
Name: "ValidClass",
GoStruct: "ValidStruct",
Properties: []phpClassProperty{
{Name: "123invalid", PhpType: "string"},
{Name: "123invalid", PhpType: phpString},
},
},
expectError: true,
@@ -387,12 +428,12 @@ func TestValidateScalarTypes(t *testing.T) {
name: "valid scalar parameters only",
function: phpFunction{
Name: "validFunction",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "stringParam", PhpType: "string"},
{Name: "intParam", PhpType: "int"},
{Name: "floatParam", PhpType: "float"},
{Name: "boolParam", PhpType: "bool"},
{Name: "stringParam", PhpType: phpString},
{Name: "intParam", PhpType: phpInt},
{Name: "floatParam", PhpType: phpFloat},
{Name: "boolParam", PhpType: phpBool},
},
},
expectError: false,
@@ -401,10 +442,10 @@ func TestValidateScalarTypes(t *testing.T) {
name: "valid nullable scalar parameters",
function: phpFunction{
Name: "nullableFunction",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "stringParam", PhpType: "string", IsNullable: true},
{Name: "intParam", PhpType: "int", IsNullable: true},
{Name: "stringParam", PhpType: phpString, IsNullable: true},
{Name: "intParam", PhpType: phpInt, IsNullable: true},
},
},
expectError: false,
@@ -413,32 +454,43 @@ func TestValidateScalarTypes(t *testing.T) {
name: "valid void return type",
function: phpFunction{
Name: "voidFunction",
ReturnType: "void",
ReturnType: phpVoid,
Params: []phpParameter{
{Name: "stringParam", PhpType: "string"},
{Name: "stringParam", PhpType: phpString},
},
},
expectError: false,
},
{
name: "invalid array parameter",
name: "valid array parameter and return",
function: phpFunction{
Name: "arrayFunction",
ReturnType: "string",
ReturnType: phpArray,
Params: []phpParameter{
{Name: "arrayParam", PhpType: "array"},
{Name: "arrayParam", PhpType: phpArray},
{Name: "stringParam", PhpType: phpString},
},
},
expectError: true,
errorMsg: "parameter 1 (arrayParam) has unsupported type 'array'",
expectError: false,
},
{
name: "valid nullable array parameter",
function: phpFunction{
Name: "nullableArrayFunction",
ReturnType: phpString,
Params: []phpParameter{
{Name: "arrayParam", PhpType: phpArray, IsNullable: true},
},
},
expectError: false,
},
{
name: "invalid object parameter",
function: phpFunction{
Name: "objectFunction",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "objectParam", PhpType: "object"},
{Name: "objectParam", PhpType: phpObject},
},
},
expectError: true,
@@ -448,33 +500,21 @@ func TestValidateScalarTypes(t *testing.T) {
name: "invalid mixed parameter",
function: phpFunction{
Name: "mixedFunction",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "mixedParam", PhpType: "mixed"},
{Name: "mixedParam", PhpType: phpMixed},
},
},
expectError: true,
errorMsg: "parameter 1 (mixedParam) has unsupported type 'mixed'",
},
{
name: "invalid array return type",
function: phpFunction{
Name: "arrayReturnFunction",
ReturnType: "array",
Params: []phpParameter{
{Name: "stringParam", PhpType: "string"},
},
},
expectError: true,
errorMsg: "return type 'array' is not supported",
},
{
name: "invalid object return type",
function: phpFunction{
Name: "objectReturnFunction",
ReturnType: "object",
ReturnType: phpObject,
Params: []phpParameter{
{Name: "stringParam", PhpType: "string"},
{Name: "stringParam", PhpType: phpString},
},
},
expectError: true,
@@ -484,15 +524,15 @@ func TestValidateScalarTypes(t *testing.T) {
name: "mixed scalar and invalid parameters",
function: phpFunction{
Name: "mixedFunction",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "validParam", PhpType: "string"},
{Name: "invalidParam", PhpType: "array"},
{Name: "anotherValidParam", PhpType: "int"},
{Name: "validParam", PhpType: phpString},
{Name: "invalidParam", PhpType: phpObject},
{Name: "anotherValidParam", PhpType: phpInt},
},
},
expectError: true,
errorMsg: "parameter 2 (invalidParam) has unsupported type 'array'",
errorMsg: "parameter 2 (invalidParam) has unsupported type 'object'",
},
}
@@ -522,10 +562,10 @@ func TestValidateGoFunctionSignature(t *testing.T) {
name: "valid Go function signature",
phpFunc: phpFunction{
Name: "testFunc",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "name", PhpType: "string"},
{Name: "count", PhpType: "int"},
{Name: "name", PhpType: phpString},
{Name: "count", PhpType: phpInt},
},
GoFunction: `func testFunc(name *C.zend_string, count int64) unsafe.Pointer {
return nil
@@ -537,9 +577,9 @@ func TestValidateGoFunctionSignature(t *testing.T) {
name: "valid void return type",
phpFunc: phpFunction{
Name: "voidFunc",
ReturnType: "void",
ReturnType: phpVoid,
Params: []phpParameter{
{Name: "message", PhpType: "string"},
{Name: "message", PhpType: phpString},
},
GoFunction: `func voidFunc(message *C.zend_string) {
// Do something
@@ -551,7 +591,7 @@ func TestValidateGoFunctionSignature(t *testing.T) {
name: "no Go function provided",
phpFunc: phpFunction{
Name: "noGoFunc",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{},
GoFunction: "",
},
@@ -562,10 +602,10 @@ func TestValidateGoFunctionSignature(t *testing.T) {
name: "parameter count mismatch",
phpFunc: phpFunction{
Name: "countMismatch",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "param1", PhpType: "string"},
{Name: "param2", PhpType: "int"},
{Name: "param1", PhpType: phpString},
{Name: "param2", PhpType: phpInt},
},
GoFunction: `func countMismatch(param1 *C.zend_string) unsafe.Pointer {
return nil
@@ -578,10 +618,10 @@ func TestValidateGoFunctionSignature(t *testing.T) {
name: "parameter type mismatch",
phpFunc: phpFunction{
Name: "typeMismatch",
ReturnType: "string",
ReturnType: phpString,
Params: []phpParameter{
{Name: "name", PhpType: "string"},
{Name: "count", PhpType: "int"},
{Name: "name", PhpType: phpString},
{Name: "count", PhpType: phpInt},
},
GoFunction: `func typeMismatch(name *C.zend_string, count string) unsafe.Pointer {
return nil
@@ -594,9 +634,9 @@ func TestValidateGoFunctionSignature(t *testing.T) {
name: "return type mismatch",
phpFunc: phpFunction{
Name: "returnMismatch",
ReturnType: "int",
ReturnType: phpInt,
Params: []phpParameter{
{Name: "value", PhpType: "string"},
{Name: "value", PhpType: phpString},
},
GoFunction: `func returnMismatch(value *C.zend_string) string {
return ""
@@ -609,9 +649,9 @@ func TestValidateGoFunctionSignature(t *testing.T) {
name: "valid bool parameter and return",
phpFunc: phpFunction{
Name: "boolFunc",
ReturnType: "bool",
ReturnType: phpBool,
Params: []phpParameter{
{Name: "flag", PhpType: "bool"},
{Name: "flag", PhpType: phpBool},
},
GoFunction: `func boolFunc(flag bool) bool {
return flag
@@ -623,12 +663,57 @@ func TestValidateGoFunctionSignature(t *testing.T) {
name: "valid float parameter and return",
phpFunc: phpFunction{
Name: "floatFunc",
ReturnType: "float",
ReturnType: phpFloat,
Params: []phpParameter{
{Name: "value", PhpType: "float"},
{Name: "value", PhpType: phpFloat},
},
GoFunction: `func floatFunc(value float64) float64 {
return value * 2.0
}`,
},
expectError: false,
},
{
name: "valid array parameter and return",
phpFunc: phpFunction{
Name: "arrayFunc",
ReturnType: phpArray,
Params: []phpParameter{
{Name: "items", PhpType: phpArray},
},
GoFunction: `func arrayFunc(items *C.zval) unsafe.Pointer {
return nil
}`,
},
expectError: false,
},
{
name: "valid nullable array parameter",
phpFunc: phpFunction{
Name: "nullableArrayFunc",
ReturnType: phpString,
Params: []phpParameter{
{Name: "items", PhpType: phpArray, IsNullable: true},
{Name: "name", PhpType: phpString},
},
GoFunction: `func nullableArrayFunc(items *C.zval, name *C.zend_string) unsafe.Pointer {
return nil
}`,
},
expectError: false,
},
{
name: "mixed array and scalar parameters",
phpFunc: phpFunction{
Name: "mixedFunc",
ReturnType: phpArray,
Params: []phpParameter{
{Name: "data", PhpType: phpArray},
{Name: "filter", PhpType: phpString},
{Name: "limit", PhpType: phpInt},
},
GoFunction: `func mixedFunc(data *C.zval, filter *C.zend_string, limit int64) unsafe.Pointer {
return nil
}`,
},
expectError: false,
@@ -657,20 +742,22 @@ func TestPhpTypeToGoType(t *testing.T) {
expected string
}{
{"string", false, "*C.zend_string"},
{"string", true, "*C.zend_string"}, // String is already a pointer, no change for nullable
{"string", true, "*C.zend_string"},
{"int", false, "int64"},
{"int", true, "*int64"}, // Nullable int becomes pointer to int64
{"int", true, "*int64"},
{"float", false, "float64"},
{"float", true, "*float64"}, // Nullable float becomes pointer to float64
{"float", true, "*float64"},
{"bool", false, "bool"},
{"bool", true, "*bool"}, // Nullable bool becomes pointer to bool
{"bool", true, "*bool"},
{"array", false, "*C.zval"},
{"array", true, "*C.zval"},
{"unknown", false, "interface{}"},
}
validator := Validator{}
for _, tt := range tests {
t.Run(tt.phpType, func(t *testing.T) {
result := validator.phpTypeToGoType(tt.phpType, tt.isNullable)
result := validator.phpTypeToGoType(phpType(tt.phpType), tt.isNullable)
assert.Equal(t, tt.expected, result, "phpTypeToGoType(%s, %v) should return %s", tt.phpType, tt.isNullable, tt.expected)
})
}
@@ -679,27 +766,28 @@ func TestPhpTypeToGoType(t *testing.T) {
func TestPhpReturnTypeToGoType(t *testing.T) {
tests := []struct {
phpReturnType string
isNullable bool
expected string
}{
{"void", false, ""},
{"void", true, ""},
{"string", false, "unsafe.Pointer"},
{"string", true, "unsafe.Pointer"},
{"int", false, "int64"},
{"int", true, "int64"},
{"float", false, "float64"},
{"float", true, "float64"},
{"bool", false, "bool"},
{"bool", true, "bool"},
{"unknown", false, "interface{}"},
{"void", ""},
{"void", ""},
{"string", "unsafe.Pointer"},
{"string", "unsafe.Pointer"},
{"int", "int64"},
{"int", "int64"},
{"float", "float64"},
{"float", "float64"},
{"bool", "bool"},
{"bool", "bool"},
{"array", "unsafe.Pointer"},
{"array", "unsafe.Pointer"},
{"unknown", "interface{}"},
}
validator := Validator{}
for _, tt := range tests {
t.Run(tt.phpReturnType, func(t *testing.T) {
result := validator.phpReturnTypeToGoType(tt.phpReturnType, tt.isNullable)
assert.Equal(t, tt.expected, result, "phpReturnTypeToGoType(%s, %v) should return %s", tt.phpReturnType, tt.isNullable, tt.expected)
result := validator.phpReturnTypeToGoType(phpType(tt.phpReturnType))
assert.Equal(t, tt.expected, result, "phpReturnTypeToGoType(%s) should return %s", tt.phpReturnType, tt.expected)
})
}
}

22
types.c Normal file
View File

@@ -0,0 +1,22 @@
#include "types.h"
zval *get_ht_packed_data(HashTable *ht, uint32_t index) {
if (ht->u.flags & HASH_FLAG_PACKED) {
return &ht->arPacked[index];
}
return NULL;
}
Bucket *get_ht_bucket_data(HashTable *ht, uint32_t index) {
if (!(ht->u.flags & HASH_FLAG_PACKED)) {
return &ht->arData[index];
}
return NULL;
}
void *__emalloc__(size_t size) { return emalloc(size); }
void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor,
bool persistent) {
zend_hash_init(ht, nSize, null, pDestructor, persistent);
}

271
types.go
View File

@@ -1,6 +1,8 @@
package frankenphp
//#include <zend.h>
/*
#include "types.h"
*/
import "C"
import "unsafe"
@@ -31,3 +33,270 @@ func PHPString(s string, persistent bool) unsafe.Pointer {
return unsafe.Pointer(zendStr)
}
// PHPKeyType represents the type of PHP hashmap key
type PHPKeyType int
const (
PHPIntKey PHPKeyType = iota
PHPStringKey
)
type PHPKey struct {
Type PHPKeyType
Str string
Int int64
}
// Array represents a PHP array with ordered key-value pairs
type Array struct {
keys []PHPKey
values []interface{}
}
// SetInt sets a value with an integer key
func (arr *Array) SetInt(key int64, value interface{}) {
arr.keys = append(arr.keys, PHPKey{Type: PHPIntKey, Int: key})
arr.values = append(arr.values, value)
}
// SetString sets a value with a string key
func (arr *Array) SetString(key string, value interface{}) {
arr.keys = append(arr.keys, PHPKey{Type: PHPStringKey, Str: key})
arr.values = append(arr.values, value)
}
// Append adds a value to the end of the array with the next available integer key
func (arr *Array) Append(value interface{}) {
nextKey := arr.getNextIntKey()
arr.SetInt(nextKey, value)
}
// getNextIntKey finds the next available integer key
func (arr *Array) getNextIntKey() int64 {
maxKey := int64(-1)
for _, key := range arr.keys {
if key.Type == PHPIntKey && key.Int > maxKey {
maxKey = key.Int
}
}
return maxKey + 1
}
// Len returns the number of elements in the array
func (arr *Array) Len() uint32 {
return uint32(len(arr.keys))
}
// At returns the key and value at the given index
func (arr *Array) At(index uint32) (PHPKey, interface{}) {
if index >= uint32(len(arr.keys)) {
return PHPKey{}, nil
}
return arr.keys[index], arr.values[index]
}
// EXPERIMENTAL: GoArray converts a zend_array to a Go Array
func GoArray(arr unsafe.Pointer) *Array {
result := &Array{
keys: make([]PHPKey, 0),
values: make([]interface{}, 0),
}
if arr == nil {
return result
}
zval := (*C.zval)(arr)
hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY))
if hashTable == nil {
return result
}
used := hashTable.nNumUsed
if htIsPacked(hashTable) {
for i := C.uint32_t(0); i < used; i++ {
v := C.get_ht_packed_data(hashTable, i)
if v != nil && C.zval_get_type(v) != C.IS_UNDEF {
value := convertZvalToGo(v)
result.SetInt(int64(i), value)
}
}
return result
}
for i := C.uint32_t(0); i < used; i++ {
bucket := C.get_ht_bucket_data(hashTable, i)
if bucket == nil || C.zval_get_type(&bucket.val) == C.IS_UNDEF {
continue
}
v := convertZvalToGo(&bucket.val)
if bucket.key != nil {
keyStr := GoString(unsafe.Pointer(bucket.key))
result.SetString(keyStr, v)
continue
}
result.SetInt(int64(bucket.h), v)
}
return result
}
// PHPArray converts a Go Array to a PHP zend_array.
func PHPArray(arr *Array) unsafe.Pointer {
if arr == nil || arr.Len() == 0 {
return unsafe.Pointer(createNewArray(0))
}
isList := true
for i, k := range arr.keys {
if k.Type != PHPIntKey || k.Int != int64(i) {
isList = false
break
}
}
var zendArray *C.HashTable
if isList {
zendArray = createNewArray(arr.Len())
for _, v := range arr.values {
zval := convertGoToZval(v)
C.zend_hash_next_index_insert(zendArray, zval)
}
return unsafe.Pointer(zendArray)
}
zendArray = createNewArray(arr.Len())
for i, k := range arr.keys {
zval := convertGoToZval(arr.values[i])
if k.Type == PHPStringKey {
keyStr := PHPString(k.Str, false)
C.zend_hash_update(zendArray, (*C.zend_string)(keyStr), zval)
continue
}
C.zend_hash_index_update(zendArray, C.zend_ulong(k.Int), zval)
}
return unsafe.Pointer(zendArray)
}
// convertZvalToGo converts a PHP zval to a Go interface{}
func convertZvalToGo(zval *C.zval) interface{} {
t := C.zval_get_type(zval)
switch t {
case C.IS_NULL:
return nil
case C.IS_FALSE:
return false
case C.IS_TRUE:
return true
case C.IS_LONG:
longPtr := (*C.zend_long)(castZval(zval, C.IS_LONG))
if longPtr != nil {
return int64(*longPtr)
}
return int64(0)
case C.IS_DOUBLE:
doublePtr := (*C.double)(castZval(zval, C.IS_DOUBLE))
if doublePtr != nil {
return float64(*doublePtr)
}
return float64(0)
case C.IS_STRING:
str := (*C.zend_string)(castZval(zval, C.IS_STRING))
if str == nil {
return ""
}
return GoString(unsafe.Pointer(str))
case C.IS_ARRAY:
return GoArray(unsafe.Pointer(zval))
default:
return nil
}
}
// convertGoToZval converts a Go interface{} to a PHP zval
func convertGoToZval(value interface{}) *C.zval {
zval := (*C.zval)(C.__emalloc__(C.size_t(unsafe.Sizeof(C.zval{}))))
u1 := (*C.uint8_t)(unsafe.Pointer(&zval.u1[0]))
v0 := unsafe.Pointer(&zval.value[0])
switch v := value.(type) {
case nil:
*u1 = C.IS_NULL
case bool:
if v {
*u1 = C.IS_TRUE
} else {
*u1 = C.IS_FALSE
}
case int:
*u1 = C.IS_LONG
*(*C.zend_long)(v0) = C.zend_long(v)
case int64:
*u1 = C.IS_LONG
*(*C.zend_long)(v0) = C.zend_long(v)
case float64:
*u1 = C.IS_DOUBLE
*(*C.double)(v0) = C.double(v)
case string:
*u1 = C.IS_STRING
*(**C.zend_string)(v0) = (*C.zend_string)(PHPString(v, false))
case *Array:
*u1 = C.IS_ARRAY
*(**C.zend_array)(v0) = (*C.zend_array)(PHPArray(v))
default:
*u1 = C.IS_NULL
}
return zval
}
// createNewArray creates a new zend_array with the specified size.
func createNewArray(size uint32) *C.HashTable {
ht := C.__emalloc__(C.size_t(unsafe.Sizeof(C.HashTable{})))
C.__zend_hash_init__((*C.struct__zend_array)(ht), C.uint32_t(size), nil, C._Bool(false))
return (*C.HashTable)(ht)
}
// htIsPacked checks if a HashTable is a list (packed) or hashmap (not packed).
func htIsPacked(ht *C.HashTable) bool {
flags := *(*C.uint32_t)(unsafe.Pointer(&ht.u[0]))
return (flags & C.HASH_FLAG_PACKED) != 0
}
// castZval casts a zval to the expected type and returns a pointer to the value
func castZval(zval *C.zval, expectedType C.uint8_t) unsafe.Pointer {
if zval == nil || C.zval_get_type(zval) != expectedType {
return nil
}
v := unsafe.Pointer(&zval.value[0])
switch expectedType {
case C.IS_LONG:
return v
case C.IS_DOUBLE:
return v
case C.IS_STRING:
return unsafe.Pointer(*(**C.zend_string)(v))
case C.IS_ARRAY:
return unsafe.Pointer(*(**C.zend_array)(v))
default:
return nil
}
}

17
types.h Normal file
View File

@@ -0,0 +1,17 @@
#ifndef TYPES_H
#define TYPES_H
#include <zend.h>
#include <zend_API.h>
#include <zend_alloc.h>
#include <zend_hash.h>
#include <zend_types.h>
zval *get_ht_packed_data(HashTable *, uint32_t index);
Bucket *get_ht_bucket_data(HashTable *, uint32_t index);
void *__emalloc__(size_t size);
void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor,
bool persistent);
#endif