mirror of
https://github.com/php/frankenphp.git
synced 2026-03-24 00:52:11 +01:00
feat(extgen): support for mixed type (#1913)
* feat(extgent): support for mixed type
* refactor: use unsafe.Pointer
* Revert "refactor: use unsafe.Pointer"
This reverts commit 8a0b9c1beb.
* fix docs
* fix docs
* cleanup template
* fix template
* fix tests
This commit is contained in:
@@ -2,12 +2,13 @@ package caddy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/dunglas/frankenphp/internal/extgen"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dunglas/frankenphp/internal/extgen"
|
||||
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -27,27 +28,21 @@ Initializes a PHP extension from a Go file. This command generates the necessary
|
||||
})
|
||||
}
|
||||
|
||||
func cmdInitExtension(fs caddycmd.Flags) (int, error) {
|
||||
func cmdInitExtension(_ caddycmd.Flags) (int, error) {
|
||||
if len(os.Args) < 3 {
|
||||
return 1, errors.New("the path to the Go source is required")
|
||||
}
|
||||
|
||||
sourceFile := os.Args[2]
|
||||
baseName := extgen.SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go"))
|
||||
|
||||
baseName := strings.TrimSuffix(filepath.Base(sourceFile), ".go")
|
||||
|
||||
baseName = extgen.SanitizePackageName(baseName)
|
||||
|
||||
sourceDir := filepath.Dir(sourceFile)
|
||||
buildDir := filepath.Join(sourceDir, "build")
|
||||
|
||||
generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: buildDir}
|
||||
generator := extgen.Generator{BaseName: baseName, SourceFile: sourceFile, BuildDir: filepath.Dir(sourceFile)}
|
||||
|
||||
if err := generator.Generate(); err != nil {
|
||||
return 1, err
|
||||
}
|
||||
|
||||
log.Printf("PHP extension %q initialized successfully in %q", baseName, generator.BuildDir)
|
||||
log.Printf("PHP extension %q initialized successfully in directory %q", baseName, generator.BuildDir)
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -146,11 +146,11 @@ func process_data(arr *C.zval) unsafe.Pointer {
|
||||
|
||||
**可用方法:**
|
||||
|
||||
- `SetInt(key int64, value interface{})` - 使用整数键设置值
|
||||
- `SetString(key string, value interface{})` - 使用字符串键设置值
|
||||
- `Append(value interface{})` - 使用下一个可用整数键添加值
|
||||
- `SetInt(key int64, value any)` - 使用整数键设置值
|
||||
- `SetString(key string, value any)` - 使用字符串键设置值
|
||||
- `Append(value any)` - 使用下一个可用整数键添加值
|
||||
- `Len() uint32` - 获取元素数量
|
||||
- `At(index uint32) (PHPKey, interface{})` - 获取索引处的键值对
|
||||
- `At(index uint32) (PHPKey, any)` - 获取索引处的键值对
|
||||
- `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - 转换为 PHP 数组
|
||||
|
||||
### 声明原生 PHP 类
|
||||
|
||||
@@ -33,7 +33,7 @@ As covered in the manual implementation section below as well, you need to [get
|
||||
The first step to writing a PHP extension in Go is to create a new Go module. You can use the following command for this:
|
||||
|
||||
```console
|
||||
go mod init github.com/my-account/my-module
|
||||
go mod init example.com/example
|
||||
```
|
||||
|
||||
The second step is to [get the PHP sources](https://www.php.net/downloads.php) for the next steps. Once you have them, decompress them into the directory of your choice, not inside your Go module:
|
||||
@@ -47,10 +47,14 @@ tar xf php-*
|
||||
Everything is now setup to write your native function in Go. Create a new file named `stringext.go`. Our first function will take a string as an argument, the number of times to repeat it, a boolean to indicate whether to reverse the string, and return the resulting string. This should look like this:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"C"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"strings"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:function repeat_this(string $str, int $count, bool $reverse): string
|
||||
@@ -98,6 +102,7 @@ This table summarizes what you need to know:
|
||||
| `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, primitive types and arrays are currently supported. Objects cannot be used as method parameters or return types yet.
|
||||
@@ -115,6 +120,16 @@ If order or association are not needed, it's also possible to directly convert t
|
||||
**Creating and manipulating arrays in Go:**
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
// export_php:function process_data_ordered(array $input): array
|
||||
func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
|
||||
// Convert PHP associative array to Go while keeping the order
|
||||
@@ -128,7 +143,7 @@ func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
|
||||
|
||||
// return an ordered array
|
||||
// if 'Order' is not empty, only the key-value pairs in 'Order' will be respected
|
||||
return frankenphp.PHPAssociativeArray(AssociativeArray{
|
||||
return frankenphp.PHPAssociativeArray(frankenphp.AssociativeArray{
|
||||
Map: map[string]any{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
@@ -192,6 +207,8 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {
|
||||
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:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
Name string
|
||||
@@ -216,6 +233,16 @@ This approach provides better encapsulation and prevents PHP code from accidenta
|
||||
Since properties are not directly accessible, you **must define methods** to interact with your opaque classes. Use the `//export_php:method` directive to define behavior:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:class User
|
||||
type UserStruct struct {
|
||||
Name string
|
||||
@@ -248,6 +275,16 @@ func (us *UserStruct) SetNamePrefix(prefix *C.zend_string) {
|
||||
The generator supports nullable parameters using the `?` prefix in PHP signatures. When a parameter is nullable, it becomes a pointer in your Go function, allowing you to check if the value was `null` in PHP:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void
|
||||
func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {
|
||||
// Check if name was provided (not null)
|
||||
@@ -275,6 +312,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. **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**:
|
||||
@@ -311,6 +349,8 @@ The generator supports exporting Go constants to PHP using two directives: `//ex
|
||||
Use the `//export_php:const` directive to create global PHP constants:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
//export_php:const
|
||||
const MAX_CONNECTIONS = 100
|
||||
|
||||
@@ -329,6 +369,8 @@ const STATUS_ERROR = iota
|
||||
Use the `//export_php:classconstant ClassName` directive to create constants that belong to a specific PHP class:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
//export_php:classconstant User
|
||||
const STATUS_ACTIVE = 1
|
||||
|
||||
@@ -368,10 +410,15 @@ The directive supports various value types including strings, integers, booleans
|
||||
You can use constants just like you are used to in the Go code. For example, let's take the `repeat_this()` function we declared earlier and change the last argument to an integer:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"C"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"strings"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:const
|
||||
@@ -388,37 +435,37 @@ const MODE_UPPERCASE = 2
|
||||
|
||||
//export_php:function repeat_this(string $str, int $count, int $mode): string
|
||||
func repeat_this(s *C.zend_string, count int64, mode int) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(s))
|
||||
str := frankenphp.GoString(unsafe.Pointer(s))
|
||||
|
||||
result := strings.Repeat(str, int(count))
|
||||
if mode == STR_REVERSE {
|
||||
// reverse the string
|
||||
}
|
||||
result := strings.Repeat(str, int(count))
|
||||
if mode == STR_REVERSE {
|
||||
// reverse the string
|
||||
}
|
||||
|
||||
if mode == STR_NORMAL {
|
||||
// no-op, just to showcase the constant
|
||||
}
|
||||
if mode == STR_NORMAL {
|
||||
// no-op, just to showcase the constant
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(result, false)
|
||||
return frankenphp.PHPString(result, false)
|
||||
}
|
||||
|
||||
//export_php:class StringProcessor
|
||||
type StringProcessorStruct struct {
|
||||
// internal fields
|
||||
// internal fields
|
||||
}
|
||||
|
||||
//export_php:method StringProcessor::process(string $input, int $mode): string
|
||||
func (sp *StringProcessorStruct) Process(input *C.zend_string, mode int64) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(input))
|
||||
str := frankenphp.GoString(unsafe.Pointer(input))
|
||||
|
||||
switch mode {
|
||||
case MODE_LOWERCASE:
|
||||
str = strings.ToLower(str)
|
||||
case MODE_UPPERCASE:
|
||||
str = strings.ToUpper(str)
|
||||
}
|
||||
switch mode {
|
||||
case MODE_LOWERCASE:
|
||||
str = strings.ToLower(str)
|
||||
case MODE_UPPERCASE:
|
||||
str = strings.ToUpper(str)
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(str, false)
|
||||
return frankenphp.PHPString(str, false)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -432,9 +479,13 @@ Use the `//export_php:namespace` directive at the top of your Go file to place a
|
||||
|
||||
```go
|
||||
//export_php:namespace My\Extension
|
||||
package main
|
||||
package example
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export_php:function hello(): string
|
||||
func hello() string {
|
||||
@@ -537,25 +588,26 @@ We'll see how to write a simple PHP extension in Go that defines a new native fu
|
||||
In your module, you need to define a new native function that will be called from PHP. To do this, create a file with the name you want, for example, `extension.go`, and add the following code:
|
||||
|
||||
```go
|
||||
package ext_go
|
||||
package example
|
||||
|
||||
//#include "extension.h"
|
||||
// #include "extension.h"
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"log/slog"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
|
||||
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
|
||||
}
|
||||
|
||||
//export go_print_something
|
||||
func go_print_something() {
|
||||
go func() {
|
||||
caddy.Log().Info("Hello from a goroutine!")
|
||||
}()
|
||||
go func() {
|
||||
slog.Info("Hello from a goroutine!")
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
@@ -731,7 +783,16 @@ There's only one thing left to do: implement the `go_upper` function in Go.
|
||||
Our Go function will take a `*C.zend_string` as a parameter, convert it to a Go string using FrankenPHP's helper function, process it, and return the result as a new `*C.zend_string`. The helper functions handle all the memory management and conversion complexity for us.
|
||||
|
||||
```go
|
||||
import "strings"
|
||||
package example
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
"strings"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
//export go_upper
|
||||
func go_upper(s *C.zend_string) *C.zend_string {
|
||||
@@ -743,9 +804,12 @@ func go_upper(s *C.zend_string) *C.zend_string {
|
||||
}
|
||||
```
|
||||
|
||||
This approach is much cleaner and safer than manual memory management. FrankenPHP's helper functions handle the conversion between PHP's `zend_string` format and Go strings automatically. The `false` parameter in `PHPString()` indicates that we want to create a new non-persistent string (freed at the end of the request).
|
||||
This approach is much cleaner and safer than manual memory management.
|
||||
FrankenPHP's helper functions handle the conversion between PHP's `zend_string` format and Go strings automatically.
|
||||
The `false` parameter in `PHPString()` indicates that we want to create a new non-persistent string (freed at the end of the request).
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> In this example, we don't perform any error handling, but you should always check that pointers are not `nil` and that the data is valid before using it in your Go functions.
|
||||
|
||||
### Integrating the Extension into FrankenPHP
|
||||
|
||||
@@ -146,11 +146,11 @@ func process_data(arr *C.zval) unsafe.Pointer {
|
||||
|
||||
**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
|
||||
- `SetInt(key int64, value any)` - Définir une valeur avec une clé entière
|
||||
- `SetString(key string, value any)` - Définir une valeur avec une clé chaîne
|
||||
- `Append(value any)` - 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
|
||||
- `At(index uint32) (PHPKey, any)` - 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
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNamespacedClassName(t *testing.T) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCFile_NamespacedPHPMethods(t *testing.T) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCFileGenerator_Generate(t *testing.T) {
|
||||
|
||||
@@ -177,22 +177,23 @@ func (cp *classParser) typeToString(expr ast.Expr) string {
|
||||
case *ast.MapType:
|
||||
return "map[" + cp.typeToString(t.Key) + "]" + cp.typeToString(t.Value)
|
||||
default:
|
||||
return "interface{}"
|
||||
return "any"
|
||||
}
|
||||
}
|
||||
|
||||
var goToPhpTypeMap = 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,
|
||||
"any": phpMixed,
|
||||
}
|
||||
|
||||
func (cp *classParser) goTypeToPHPType(goType string) phpType {
|
||||
goType = strings.TrimPrefix(goType, "*")
|
||||
|
||||
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 {
|
||||
if phpType, exists := goToPhpTypeMap[goType]; exists {
|
||||
return phpType
|
||||
}
|
||||
|
||||
@@ -244,7 +245,7 @@ func (cp *classParser) parseMethods(filename string) (methods []phpClassMethod,
|
||||
IsReturnNullable: method.isReturnNullable,
|
||||
}
|
||||
|
||||
if err := validator.validateScalarTypes(phpFunc); err != nil {
|
||||
if err := validator.validateTypes(phpFunc); err != nil {
|
||||
fmt.Printf("Warning: Method \"%s::%s\" uses unsupported types: %v\n", className, method.Name, err)
|
||||
|
||||
continue
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClassParser(t *testing.T) {
|
||||
@@ -282,7 +282,7 @@ func TestGoTypeToPHPType(t *testing.T) {
|
||||
{"[]string", phpArray},
|
||||
{"map[string]int", phpArray},
|
||||
{"*[]int", phpArray},
|
||||
{"interface{}", phpMixed},
|
||||
{"any", phpMixed},
|
||||
{"CustomType", phpMixed},
|
||||
}
|
||||
|
||||
@@ -335,7 +335,7 @@ type NullableStruct struct {
|
||||
type CollectionStruct struct {
|
||||
StringSlice []string
|
||||
IntMap map[string]int
|
||||
MixedSlice []interface{}
|
||||
MixedSlice []any
|
||||
}`,
|
||||
expected: []phpType{phpArray, phpArray, phpArray},
|
||||
},
|
||||
@@ -381,7 +381,7 @@ type TestClass struct {
|
||||
}
|
||||
|
||||
//export_php:method TestClass::arrayMethod(array $data): string
|
||||
func (tc *TestClass) arrayMethod(data interface{}) unsafe.Pointer {
|
||||
func (tc *TestClass) arrayMethod(data any) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
@@ -398,7 +398,7 @@ type TestClass struct {
|
||||
}
|
||||
|
||||
//export_php:method TestClass::objectMethod(object $obj): string
|
||||
func (tc *TestClass) objectMethod(obj interface{}) unsafe.Pointer {
|
||||
func (tc *TestClass) objectMethod(obj any) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
@@ -415,7 +415,7 @@ type TestClass struct {
|
||||
}
|
||||
|
||||
//export_php:method TestClass::mixedMethod(mixed $value): string
|
||||
func (tc *TestClass) mixedMethod(value interface{}) unsafe.Pointer {
|
||||
func (tc *TestClass) mixedMethod(value any) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
@@ -432,7 +432,7 @@ type TestClass struct {
|
||||
}
|
||||
|
||||
//export_php:method TestClass::arrayReturn(string $name): array
|
||||
func (tc *TestClass) arrayReturn(name *C.zend_string) interface{} {
|
||||
func (tc *TestClass) arrayReturn(name *C.zend_string) any {
|
||||
return []string{"result"}
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
@@ -449,8 +449,8 @@ type TestClass struct {
|
||||
}
|
||||
|
||||
//export_php:method TestClass::objectReturn(string $name): object
|
||||
func (tc *TestClass) objectReturn(name *C.zend_string) interface{} {
|
||||
return map[string]interface{}{"key": "value"}
|
||||
func (tc *TestClass) objectReturn(name *C.zend_string) any {
|
||||
return map[string]any{"key": "value"}
|
||||
}`,
|
||||
expectedClasses: 1,
|
||||
expectedMethods: 0,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConstantsIntegration(t *testing.T) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConstantParser(t *testing.T) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDocumentationGenerator_Generate(t *testing.T) {
|
||||
|
||||
@@ -50,7 +50,7 @@ func (fp *FuncParser) parse(filename string) (functions []phpFunction, err error
|
||||
continue
|
||||
}
|
||||
|
||||
if err := validator.validateScalarTypes(*phpFunc); err != nil {
|
||||
if err := validator.validateTypes(*phpFunc); err != nil {
|
||||
fmt.Printf("Warning: Function '%s' uses unsupported types: %v\n", phpFunc.Name, err)
|
||||
|
||||
continue
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFunctionParser(t *testing.T) {
|
||||
@@ -306,7 +306,7 @@ func TestFunctionParserUnsupportedTypes(t *testing.T) {
|
||||
input: `package main
|
||||
|
||||
//export_php:function arrayFunc(array $data): string
|
||||
func arrayFunc(data interface{}) unsafe.Pointer {
|
||||
func arrayFunc(data any) unsafe.Pointer {
|
||||
return String("processed")
|
||||
}`,
|
||||
expected: 0,
|
||||
@@ -317,7 +317,7 @@ func arrayFunc(data interface{}) unsafe.Pointer {
|
||||
input: `package main
|
||||
|
||||
//export_php:function objectFunc(object $obj): string
|
||||
func objectFunc(obj interface{}) unsafe.Pointer {
|
||||
func objectFunc(obj any) unsafe.Pointer {
|
||||
return String("processed")
|
||||
}`,
|
||||
expected: 0,
|
||||
@@ -328,7 +328,7 @@ func objectFunc(obj interface{}) unsafe.Pointer {
|
||||
input: `package main
|
||||
|
||||
//export_php:function mixedFunc(mixed $value): string
|
||||
func mixedFunc(value interface{}) unsafe.Pointer {
|
||||
func mixedFunc(value any) unsafe.Pointer {
|
||||
return String("processed")
|
||||
}`,
|
||||
expected: 0,
|
||||
@@ -339,7 +339,7 @@ func mixedFunc(value interface{}) unsafe.Pointer {
|
||||
input: `package main
|
||||
|
||||
//export_php:function arrayReturnFunc(string $name): array
|
||||
func arrayReturnFunc(name *C.zend_string) interface{} {
|
||||
func arrayReturnFunc(name *C.zend_string) any {
|
||||
return []string{"result"}
|
||||
}`,
|
||||
expected: 0,
|
||||
@@ -350,8 +350,8 @@ func arrayReturnFunc(name *C.zend_string) interface{} {
|
||||
input: `package main
|
||||
|
||||
//export_php:function objectReturnFunc(string $name): object
|
||||
func objectReturnFunc(name *C.zend_string) interface{} {
|
||||
return map[string]interface{}{"key": "value"}
|
||||
func objectReturnFunc(name *C.zend_string) any {
|
||||
return map[string]any{"key": "value"}
|
||||
}`,
|
||||
expected: 0,
|
||||
hasWarning: true,
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
const BuildDir = "build"
|
||||
|
||||
type Generator struct {
|
||||
BaseName string
|
||||
SourceFile string
|
||||
|
||||
@@ -47,7 +47,7 @@ func (gg *GoFileGenerator) buildContent() (string, error) {
|
||||
|
||||
filteredImports := make([]string, 0, len(imports))
|
||||
for _, imp := range imports {
|
||||
if imp != `"C"` {
|
||||
if imp != `"C"` && imp != `"unsafe"` && imp != `"github.com/dunglas/frankenphp"` {
|
||||
filteredImports = append(filteredImports, imp)
|
||||
}
|
||||
}
|
||||
@@ -104,20 +104,20 @@ type GoParameter struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
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: "",
|
||||
}
|
||||
var phpToGoTypeMap = map[phpType]string{
|
||||
phpString: "string",
|
||||
phpInt: "int64",
|
||||
phpFloat: "float64",
|
||||
phpBool: "bool",
|
||||
phpArray: "*frankenphp.Array",
|
||||
phpMixed: "any",
|
||||
phpVoid: "",
|
||||
}
|
||||
|
||||
if goType, exists := typeMap[phpT]; exists {
|
||||
func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string {
|
||||
if goType, exists := phpToGoTypeMap[phpT]; exists {
|
||||
return goType
|
||||
}
|
||||
|
||||
return "interface{}"
|
||||
return "any"
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGoFileGenerator_Generate(t *testing.T) {
|
||||
@@ -109,7 +109,7 @@ func test() {
|
||||
contains: []string{
|
||||
"package simple",
|
||||
`#include "simple.h"`,
|
||||
"import \"C\"",
|
||||
`import "C"`,
|
||||
"func init()",
|
||||
"frankenphp.RegisterExtension(",
|
||||
"//export test",
|
||||
@@ -143,11 +143,11 @@ func process(data *go_string) *go_value {
|
||||
},
|
||||
contains: []string{
|
||||
"package complex",
|
||||
`import "fmt"`,
|
||||
`import "strings"`,
|
||||
`import "encoding/json"`,
|
||||
`"fmt"`,
|
||||
`"strings"`,
|
||||
`"encoding/json"`,
|
||||
"//export process",
|
||||
`import "C"`,
|
||||
`"C"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -193,7 +193,7 @@ func internalFunc2(data string) {
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, expected := range tt.contains {
|
||||
assert.Contains(t, content, expected, "Generated Go content should contain '%s'", expected)
|
||||
assert.Contains(t, content, expected, "Generated Go content should contain %q", expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -305,9 +305,9 @@ func test() {}`
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedImports := []string{
|
||||
`import "fmt"`,
|
||||
`import "strings"`,
|
||||
`import "github.com/other/package"`,
|
||||
`"fmt"`,
|
||||
`"strings"`,
|
||||
`"github.com/other/package"`,
|
||||
}
|
||||
|
||||
for _, imp := range expectedImports {
|
||||
@@ -315,10 +315,10 @@ func test() {}`
|
||||
}
|
||||
|
||||
forbiddenImports := []string{
|
||||
`import "C"`,
|
||||
`"C"`,
|
||||
}
|
||||
|
||||
cImportCount := strings.Count(content, `import "C"`)
|
||||
cImportCount := strings.Count(content, `"C"`)
|
||||
assert.Equal(t, 1, cImportCount, "Expected exactly 1 occurrence of 'import \"C\"'")
|
||||
|
||||
for _, imp := range forbiddenImports[1:] {
|
||||
@@ -340,7 +340,7 @@ import (
|
||||
func processData(input *go_string, options *go_nullable) *go_value {
|
||||
data := CStringToGoString(input)
|
||||
processed := internalProcess(data)
|
||||
return types.Array([]interface{}{processed})
|
||||
return types.Array([]any{processed})
|
||||
}
|
||||
|
||||
//export_php: validateInput(data string): bool
|
||||
@@ -358,7 +358,7 @@ func validateFormat(input string) bool {
|
||||
return !strings.Contains(input, "invalid")
|
||||
}
|
||||
|
||||
func jsonHelper(data interface{}) ([]byte, error) {
|
||||
func jsonHelper(data any) ([]byte, error) {
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ func debugPrint(msg string) {
|
||||
GoFunction: `func processData(input *go_string, options *go_nullable) *go_value {
|
||||
data := CStringToGoString(input)
|
||||
processed := internalProcess(data)
|
||||
return Array([]interface{}{processed})
|
||||
return Array([]any{processed})
|
||||
}`,
|
||||
},
|
||||
{
|
||||
@@ -403,7 +403,7 @@ func debugPrint(msg string) {
|
||||
internalFuncs := []string{
|
||||
"func internalProcess(data string) string",
|
||||
"func validateFormat(input string) bool",
|
||||
"func jsonHelper(data interface{}) ([]byte, error)",
|
||||
"func jsonHelper(data any) ([]byte, error)",
|
||||
"func debugPrint(msg string)",
|
||||
}
|
||||
|
||||
@@ -510,7 +510,7 @@ import "fmt"
|
||||
|
||||
//export_php:class ArrayClass
|
||||
type ArrayStruct struct {
|
||||
data []interface{}
|
||||
data []any
|
||||
}
|
||||
|
||||
//export_php:method ArrayClass::processArray(array $items): array
|
||||
@@ -675,10 +675,8 @@ func createTempSourceFile(t *testing.T, content string) string {
|
||||
func testGoFileBasicStructure(t *testing.T, content, baseName string) {
|
||||
requiredElements := []string{
|
||||
"package " + SanitizePackageName(baseName),
|
||||
"/*",
|
||||
"#include <stdlib.h>",
|
||||
`#include "` + baseName + `.h"`,
|
||||
"*/",
|
||||
"// #include <stdlib.h>",
|
||||
`// #include "` + baseName + `.h"`,
|
||||
`import "C"`,
|
||||
"func init() {",
|
||||
"frankenphp.RegisterExtension(",
|
||||
@@ -691,7 +689,7 @@ func testGoFileBasicStructure(t *testing.T, content, baseName string) {
|
||||
}
|
||||
|
||||
func testGoFileImports(t *testing.T, content string) {
|
||||
cImportCount := strings.Count(content, `import "C"`)
|
||||
cImportCount := strings.Count(content, `"C"`)
|
||||
assert.Equal(t, 1, cImportCount, "Expected exactly 1 C import")
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHeaderGenerator_Generate(t *testing.T) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNamespaceParser(t *testing.T) {
|
||||
|
||||
@@ -68,7 +68,7 @@ func (pp *ParameterParser) generateSingleParamDeclaration(param phpParameter) []
|
||||
if param.IsNullable {
|
||||
decls = append(decls, fmt.Sprintf("zend_bool %s_is_null = 0;", param.Name))
|
||||
}
|
||||
case phpArray:
|
||||
case phpArray, phpMixed:
|
||||
decls = append(decls, fmt.Sprintf("zval *%s = NULL;", param.Name))
|
||||
}
|
||||
|
||||
@@ -119,6 +119,8 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
|
||||
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)
|
||||
case phpMixed:
|
||||
return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s)", param.Name)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -134,6 +136,8 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
|
||||
return fmt.Sprintf("\n Z_PARAM_BOOL(%s)", param.Name)
|
||||
case phpArray:
|
||||
return fmt.Sprintf("\n Z_PARAM_ARRAY(%s)", param.Name)
|
||||
case phpMixed:
|
||||
return fmt.Sprintf("\n Z_PARAM_ZVAL(%s)", param.Name)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -164,25 +168,19 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
|
||||
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
|
||||
case phpBool:
|
||||
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
|
||||
case phpArray:
|
||||
return param.Name
|
||||
default:
|
||||
return param.Name
|
||||
}
|
||||
} else {
|
||||
switch param.PhpType {
|
||||
case phpString:
|
||||
return param.Name
|
||||
case phpInt:
|
||||
return fmt.Sprintf("(long) %s", param.Name)
|
||||
case phpFloat:
|
||||
return fmt.Sprintf("(double) %s", param.Name)
|
||||
case phpBool:
|
||||
return fmt.Sprintf("(int) %s", param.Name)
|
||||
case phpArray:
|
||||
return param.Name
|
||||
default:
|
||||
return param.Name
|
||||
}
|
||||
}
|
||||
|
||||
switch param.PhpType {
|
||||
case phpInt:
|
||||
return fmt.Sprintf("(long) %s", param.Name)
|
||||
case phpFloat:
|
||||
return fmt.Sprintf("(double) %s", param.Name)
|
||||
case phpBool:
|
||||
return fmt.Sprintf("(int) %s", param.Name)
|
||||
default:
|
||||
return param.Name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,20 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
|
||||
},
|
||||
expected: " zend_string *name = NULL;\n zval *items = NULL;\n zend_long count = 5;",
|
||||
},
|
||||
{
|
||||
name: "mixed parameter",
|
||||
params: []phpParameter{
|
||||
{Name: "m", PhpType: phpMixed, HasDefault: false},
|
||||
},
|
||||
expected: " zval *m = NULL;",
|
||||
},
|
||||
{
|
||||
name: "nullable mixed parameter",
|
||||
params: []phpParameter{
|
||||
{Name: "m", PhpType: phpMixed, HasDefault: false, IsNullable: true},
|
||||
},
|
||||
expected: " zval *m = NULL;",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -346,6 +360,16 @@ func TestParameterParser_GenerateParamParsingMacro(t *testing.T) {
|
||||
param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true},
|
||||
expected: "\n Z_PARAM_ARRAY_OR_NULL(items)",
|
||||
},
|
||||
{
|
||||
name: "mixed parameter",
|
||||
param: phpParameter{Name: "m", PhpType: phpMixed},
|
||||
expected: "\n Z_PARAM_ZVAL(m)",
|
||||
},
|
||||
{
|
||||
name: "nullable mixed parameter",
|
||||
param: phpParameter{Name: "m", PhpType: phpMixed, IsNullable: true},
|
||||
expected: "\n Z_PARAM_ZVAL_OR_NULL(m)",
|
||||
},
|
||||
{
|
||||
name: "unknown type",
|
||||
param: phpParameter{Name: "unknown", PhpType: phpType("unknown")},
|
||||
|
||||
@@ -50,21 +50,21 @@ func (pfg *PHPFuncGenerator) generateGoCall(fn phpFunction) string {
|
||||
return fmt.Sprintf(" zend_array *result = %s(%s);", fn.Name, callParams)
|
||||
}
|
||||
|
||||
if fn.ReturnType == phpMixed {
|
||||
return fmt.Sprintf(" zval *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 phpType) string {
|
||||
switch returnType {
|
||||
case phpString:
|
||||
return "zend_string*"
|
||||
case phpInt:
|
||||
return "long"
|
||||
case phpFloat:
|
||||
return "double"
|
||||
case phpBool:
|
||||
return "int"
|
||||
case phpArray:
|
||||
return "zend_array*"
|
||||
default:
|
||||
return "void"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPHPFuncGenerator_NamespacedFunctions(t *testing.T) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ void register_all_classes() {
|
||||
|
||||
PHP_MINIT_FUNCTION({{.BaseName}}) {
|
||||
{{ if .Classes}}register_all_classes();{{end}}
|
||||
|
||||
|
||||
{{- range .Constants}}
|
||||
{{- if eq .ClassName ""}}
|
||||
{{if .IsIota}}REGISTER_LONG_CONSTANT("{{.Name}}", {{.Name}}, CONST_CS | CONST_PERSISTENT);
|
||||
@@ -180,4 +180,3 @@ zend_module_entry {{.BaseName}}_module_entry = {STANDARD_MODULE_HEADER,
|
||||
NULL, /* MINFO */
|
||||
"1.0.0", /* Version */
|
||||
STANDARD_MODULE_PROPERTIES};
|
||||
|
||||
|
||||
@@ -1,52 +1,55 @@
|
||||
package {{.PackageName}}
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include "{{.BaseName}}.h"
|
||||
*/
|
||||
// #include <stdlib.h>
|
||||
// #include "{{.BaseName}}.h"
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
{{- range .Imports}}
|
||||
import {{.}}
|
||||
{{.}}
|
||||
{{- end}}
|
||||
)
|
||||
|
||||
func init() {
|
||||
frankenphp.RegisterExtension(unsafe.Pointer(&C.{{.BaseName}}_module_entry))
|
||||
}
|
||||
{{- range .Constants}}
|
||||
|
||||
{{ range .Constants}}
|
||||
const {{.Name}} = {{.Value}}
|
||||
{{- end}}
|
||||
|
||||
{{ range .Variables}}
|
||||
{{- end}}
|
||||
{{- range .Variables}}
|
||||
|
||||
{{.}}
|
||||
{{- end}}
|
||||
|
||||
{{range .InternalFunctions}}
|
||||
{{- range .InternalFunctions}}
|
||||
{{.}}
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
||||
{{- range .Functions}}
|
||||
//export {{.Name}}
|
||||
{{.GoFunction}}
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
||||
{{- range .Classes}}
|
||||
type {{.GoStruct}} struct {
|
||||
{{- range .Properties}}
|
||||
{{.Name}} {{.GoType}}
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
{{- end}}
|
||||
|
||||
{{- if .Classes}}
|
||||
|
||||
//export registerGoObject
|
||||
func registerGoObject(obj interface{}) C.uintptr_t {
|
||||
func registerGoObject(obj any) C.uintptr_t {
|
||||
handle := cgo.NewHandle(obj)
|
||||
return C.uintptr_t(handle)
|
||||
}
|
||||
|
||||
//export getGoObject
|
||||
func getGoObject(handle C.uintptr_t) interface{} {
|
||||
func getGoObject(handle C.uintptr_t) any {
|
||||
h := cgo.Handle(handle)
|
||||
return h.Value()
|
||||
}
|
||||
@@ -58,7 +61,6 @@ func removeGoObject(handle C.uintptr_t) {
|
||||
}
|
||||
|
||||
{{- end}}
|
||||
|
||||
{{- range $class := .Classes}}
|
||||
//export create_{{.GoStruct}}_object
|
||||
func create_{{.GoStruct}}_object() C.uintptr_t {
|
||||
@@ -70,8 +72,8 @@ func create_{{.GoStruct}}_object() C.uintptr_t {
|
||||
{{- if .GoFunction}}
|
||||
{{.GoFunction}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
||||
{{- range .Methods}}
|
||||
//export {{.Name}}_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}} {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNamespacedName(t *testing.T) {
|
||||
|
||||
@@ -10,26 +10,17 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func scalarTypes() []phpType {
|
||||
return []phpType{phpString, phpInt, phpFloat, phpBool, phpArray}
|
||||
}
|
||||
var (
|
||||
paramTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed}
|
||||
returnTypes = []phpType{phpVoid, phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed, phpNull, phpTrue, phpFalse}
|
||||
propTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed}
|
||||
supportedTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpMixed}
|
||||
|
||||
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_]*$`)
|
||||
var propNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
functionNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
parameterNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
classNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
propNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
)
|
||||
|
||||
type Validator struct{}
|
||||
|
||||
@@ -64,8 +55,7 @@ func (v *Validator) validateParameter(param phpParameter) error {
|
||||
return fmt.Errorf("invalid parameter name: %s", param.Name)
|
||||
}
|
||||
|
||||
validTypes := paramTypes()
|
||||
if !v.isValidPHPType(param.PhpType, validTypes) {
|
||||
if !slices.Contains(paramTypes, param.PhpType) {
|
||||
return fmt.Errorf("invalid parameter type: %s", param.PhpType)
|
||||
}
|
||||
|
||||
@@ -73,8 +63,7 @@ func (v *Validator) validateParameter(param phpParameter) error {
|
||||
}
|
||||
|
||||
func (v *Validator) validateReturnType(returnType phpType) error {
|
||||
validReturnTypes := returnTypes()
|
||||
if !v.isValidPHPType(returnType, validReturnTypes) {
|
||||
if !slices.Contains(returnTypes, returnType) {
|
||||
return fmt.Errorf("invalid return type: %s", returnType)
|
||||
}
|
||||
return nil
|
||||
@@ -107,43 +96,32 @@ func (v *Validator) validateClassProperty(prop phpClassProperty) error {
|
||||
return fmt.Errorf("invalid property name: %s", prop.Name)
|
||||
}
|
||||
|
||||
validTypes := propTypes()
|
||||
if !v.isValidPHPType(prop.PhpType, validTypes) {
|
||||
if !slices.Contains(propTypes, prop.PhpType) {
|
||||
return fmt.Errorf("invalid property type: %s", prop.PhpType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Validator) isValidPHPType(phpType phpType, validTypes []phpType) bool {
|
||||
return slices.Contains(validTypes, phpType)
|
||||
}
|
||||
|
||||
// validateScalarTypes checks if PHP signature contains only supported scalar types
|
||||
func (v *Validator) validateScalarTypes(fn phpFunction) error {
|
||||
supportedTypes := scalarTypes()
|
||||
|
||||
// validateTypes checks if PHP signature contains only supported types
|
||||
func (v *Validator) validateTypes(fn phpFunction) error {
|
||||
for i, param := range fn.Params {
|
||||
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 !slices.Contains(supportedTypes, param.PhpType) {
|
||||
return fmt.Errorf("parameter %d %q has unsupported type %q, supported typed: string, int, float, bool, array and mixed, can be nullable", i+1, param.Name, param.PhpType)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if fn.ReturnType != phpVoid && !slices.Contains(supportedTypes, fn.ReturnType) {
|
||||
return fmt.Errorf("return type %q is not supported, supported typed: string, int, float, bool, array and mixed, can be nullable", fn.ReturnType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Validator) isScalarPHPType(phpType phpType, supportedTypes []phpType) bool {
|
||||
return slices.Contains(supportedTypes, phpType)
|
||||
}
|
||||
|
||||
// validateGoFunctionSignatureWithOptions validates with option for method vs function
|
||||
func (v *Validator) validateGoFunctionSignatureWithOptions(phpFunc phpFunction, isMethod bool) error {
|
||||
if phpFunc.GoFunction == "" {
|
||||
return fmt.Errorf("no Go function found for PHP function '%s'", phpFunc.Name)
|
||||
return fmt.Errorf("no Go function found for PHP function %q", phpFunc.Name)
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
@@ -199,7 +177,7 @@ func (v *Validator) validateGoFunctionSignatureWithOptions(phpFunc phpFunction,
|
||||
actualGoType := v.goTypeToString(goParam.Type)
|
||||
|
||||
if !v.isCompatibleGoType(expectedGoType, actualGoType) {
|
||||
return fmt.Errorf("parameter %d type mismatch: PHP '%s' requires Go type '%s' but found '%s'", i+1, phpParam.PhpType, expectedGoType, actualGoType)
|
||||
return fmt.Errorf("parameter %d type mismatch: PHP %q requires Go type %q but found %q", i+1, phpParam.PhpType, expectedGoType, actualGoType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,7 +186,7 @@ func (v *Validator) validateGoFunctionSignatureWithOptions(phpFunc phpFunction,
|
||||
actualGoReturnType := v.goReturnTypeToString(goFunc.Type.Results)
|
||||
|
||||
if !v.isCompatibleGoType(expectedGoReturnType, actualGoReturnType) {
|
||||
return fmt.Errorf("return type mismatch: PHP '%s' requires Go return type '%s' but found '%s'", phpFunc.ReturnType, expectedGoReturnType, actualGoReturnType)
|
||||
return fmt.Errorf("return type mismatch: PHP %q requires Go return type %q but found %q", phpFunc.ReturnType, expectedGoReturnType, actualGoReturnType)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -225,10 +203,10 @@ func (v *Validator) phpTypeToGoType(t phpType, isNullable bool) string {
|
||||
baseType = "float64"
|
||||
case phpBool:
|
||||
baseType = "bool"
|
||||
case phpArray:
|
||||
case phpArray, phpMixed:
|
||||
baseType = "*C.zval"
|
||||
default:
|
||||
baseType = "interface{}"
|
||||
baseType = "any"
|
||||
}
|
||||
|
||||
if isNullable && t != phpString && t != phpArray {
|
||||
@@ -271,7 +249,7 @@ func (v *Validator) phpReturnTypeToGoType(phpReturnType phpType) string {
|
||||
case phpArray:
|
||||
return "unsafe.Pointer"
|
||||
default:
|
||||
return "interface{}"
|
||||
return "any"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -417,7 +417,7 @@ func TestValidateClass(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateScalarTypes(t *testing.T) {
|
||||
func TestValidateTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
function phpFunction
|
||||
@@ -494,19 +494,7 @@ func TestValidateScalarTypes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "parameter 1 (objectParam) has unsupported type 'object'",
|
||||
},
|
||||
{
|
||||
name: "invalid mixed parameter",
|
||||
function: phpFunction{
|
||||
Name: "mixedFunction",
|
||||
ReturnType: phpString,
|
||||
Params: []phpParameter{
|
||||
{Name: "mixedParam", PhpType: phpMixed},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "parameter 1 (mixedParam) has unsupported type 'mixed'",
|
||||
errorMsg: `parameter 1 "objectParam" has unsupported type "object"`,
|
||||
},
|
||||
{
|
||||
name: "invalid object return type",
|
||||
@@ -518,7 +506,7 @@ func TestValidateScalarTypes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "return type 'object' is not supported",
|
||||
errorMsg: `return type "object" is not supported`,
|
||||
},
|
||||
{
|
||||
name: "mixed scalar and invalid parameters",
|
||||
@@ -532,20 +520,20 @@ func TestValidateScalarTypes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "parameter 2 (invalidParam) has unsupported type 'object'",
|
||||
errorMsg: `parameter 2 "invalidParam" has unsupported type "object"`,
|
||||
},
|
||||
}
|
||||
|
||||
validator := Validator{}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validator.validateScalarTypes(tt.function)
|
||||
err := validator.validateTypes(tt.function)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err, "validateScalarTypes() should return an error for function %s", tt.function.Name)
|
||||
assert.Error(t, err, "validateTypes() should return an error for function %s", tt.function.Name)
|
||||
assert.Contains(t, err.Error(), tt.errorMsg, "Error message should contain expected text")
|
||||
} else {
|
||||
assert.NoError(t, err, "validateScalarTypes() should not return an error for function %s", tt.function.Name)
|
||||
assert.NoError(t, err, "validateTypes() should not return an error for function %s", tt.function.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -628,7 +616,7 @@ func TestValidateGoFunctionSignature(t *testing.T) {
|
||||
}`,
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "parameter 2 type mismatch: PHP 'int' requires Go type 'int64' but found 'string'",
|
||||
errorMsg: `parameter 2 type mismatch: PHP "int" requires Go type "int64" but found "string"`,
|
||||
},
|
||||
{
|
||||
name: "return type mismatch",
|
||||
@@ -643,7 +631,7 @@ func TestValidateGoFunctionSignature(t *testing.T) {
|
||||
}`,
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "return type mismatch: PHP 'int' requires Go return type 'int64' but found 'string'",
|
||||
errorMsg: `return type mismatch: PHP "int" requires Go return type "int64" but found "string"`,
|
||||
},
|
||||
{
|
||||
name: "valid bool parameter and return",
|
||||
@@ -751,7 +739,7 @@ func TestPhpTypeToGoType(t *testing.T) {
|
||||
{"bool", true, "*bool"},
|
||||
{"array", false, "*C.zval"},
|
||||
{"array", true, "*C.zval"},
|
||||
{"unknown", false, "interface{}"},
|
||||
{"unknown", false, "any"},
|
||||
}
|
||||
|
||||
validator := Validator{}
|
||||
@@ -780,7 +768,7 @@ func TestPhpReturnTypeToGoType(t *testing.T) {
|
||||
{"bool", "bool"},
|
||||
{"array", "unsafe.Pointer"},
|
||||
{"array", "unsafe.Pointer"},
|
||||
{"unknown", "interface{}"},
|
||||
{"unknown", "any"},
|
||||
}
|
||||
|
||||
validator := Validator{}
|
||||
|
||||
10
types.h
10
types.h
@@ -1,11 +1,11 @@
|
||||
#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>
|
||||
#include <Zend/zend.h>
|
||||
#include <Zend/zend_API.h>
|
||||
#include <Zend/zend_alloc.h>
|
||||
#include <Zend/zend_hash.h>
|
||||
#include <Zend/zend_types.h>
|
||||
|
||||
zval *get_ht_packed_data(HashTable *, uint32_t index);
|
||||
Bucket *get_ht_bucket_data(HashTable *, uint32_t index);
|
||||
|
||||
Reference in New Issue
Block a user