From ba8e305c63ba3c2f95465e7a3badb72ea01f9ee5 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sun, 10 Aug 2025 22:15:45 +0200 Subject: [PATCH 01/18] Makes go arrays more consistent with PHP arrays. --- docs/extensions.md | 69 ++++++---- internal/extgen/gofile_test.go | 14 +- threadtasks.go | 91 +++++++++++++ types.go | 240 ++++++++++++++++++++------------- types_test.go | 128 +++++++++++++++++- typestest.go | 18 --- 6 files changed, 418 insertions(+), 142 deletions(-) create mode 100644 threadtasks.go delete mode 100644 typestest.go diff --git a/docs/extensions.md b/docs/extensions.md index 0efc34f6dc..dcfb4dcc78 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -112,45 +112,68 @@ func process_data(arr *C.zval) unsafe.Pointer { // Convert PHP array to Go goArray := frankenphp.GoArray(unsafe.Pointer(arr)) - result := &frankenphp.Array{} + result := NewArray( + KeyValuePair{"firstName", "John"}, + KeyValuePair{"lastName", "Doe"}, + ) - result.SetInt(0, "first") - result.SetInt(1, "second") - result.Append("third") // Automatically assigns next integer key + result.Set("age", 25") - 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) - } + // loop over the entries + for _, entry := range goArray.Entries() { + result.Set("processed_"+entry.key, entry.value) } - // Convert back to PHP array + // Convert back to PHP array (*zval) return frankenphp.PHPArray(result) } + +//export_php:function process_data(array $input): array +func example_packed_array(arr *C.zval) unsafe.Pointer { + packedArray := NewPackedArray( + "first", + "second", + "third", + ) + + for i, value := range packedArray.Values() { + // This will return a error if the array is not packed + _ := packedArray.SetAtIndex(i, value+"-processed") + } + + // Convert back to a PHP packed array (*zval) + return frankenphp.PHPArray(packedArray) +} ``` **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 +* **Optimized based on keys** - The array will automatically be associative or packed based on the keys used * **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 +**Packed and associative** + +* `Entries() []KeyValuePair` - Get all entries by key-value (recommended for associative array, but also works for packed array) +* `Values() []interface{}` - Get values (recommended for packed array, but also works for associative array) +* `IsPacked() bool` - Weather packed array (list) or associative array (hashmap) +* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convert to PHP array (*zval) + +**Associative array only** + +* `NewArray(entries ...KeyValuePair) *frankenphp.Array` - Create a new associative array with key-value pairs +* `Set(key string, value interface{})` - Set value with key (associative array), will convert a packed array to associative +* `Get(key string) interface{}` - Get value with O(1) (associative array) + +**Packed array only** + +* `NewPackedArray(values ...interface{}) *frankenphp.Array` - Create a new packed array with values +* `SetAtIndex(index int64, value interface{}) error` - Set value at index (packed array) +* `GetAtIndex(index int64) (value interface{} error)` - Get value at index (packed array) +* `Append(value interface{}) error` - Add value at the end (packed array) ### Declaring a Native PHP Class diff --git a/internal/extgen/gofile_test.go b/internal/extgen/gofile_test.go index 11e962f3d3..6cbb3afadd 100644 --- a/internal/extgen/gofile_test.go +++ b/internal/extgen/gofile_test.go @@ -516,9 +516,8 @@ type ArrayStruct struct { //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) + for i, entry := range items.Entries() { + result.Set(fmt.Sprintf("processed_%d", i), value) } return result } @@ -545,9 +544,8 @@ func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *franke }, 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) + for i, entry := range items.Entries() { + result.Set(fmt.Sprintf("processed_%d", i), value) } return result }`, @@ -617,7 +615,7 @@ func (nas *NullableArrayStruct) ProcessOptionalArray(items *frankenphp.Array, na if items == nil { return "No items: " + name } - return fmt.Sprintf("Processing %d items for %s", items.Len(), name) + return fmt.Sprintf("Processing %d items for %s", len(items.Entries()), name) }` sourceFile := filepath.Join(tmpDir, "test.go") @@ -638,7 +636,7 @@ func (nas *NullableArrayStruct) ProcessOptionalArray(items *frankenphp.Array, na if items == nil { return "No items: " + name } - return fmt.Sprintf("Processing %d items for %s", items.Len(), name) + return fmt.Sprintf("Processing %d items for %s", len(items.Entries()), name) }`, }, } diff --git a/threadtasks.go b/threadtasks.go new file mode 100644 index 0000000000..0a392633aa --- /dev/null +++ b/threadtasks.go @@ -0,0 +1,91 @@ +package frankenphp + +import ( + "sync" +) + +// EXPERIMENTAL (only used in tests) +// representation of a thread that handles tasks directly assigned by go +// implements the threadHandler interface +type taskThread struct { + thread *phpThread + execChan chan *task +} + +// task callbacks will be executed directly on the PHP thread +// therefore having full access to the PHP runtime +type task struct { + callback func() + done sync.Mutex +} + +func newTask(cb func()) *task { + t := &task{callback: cb} + t.done.Lock() + + return t +} + +func (t *task) waitForCompletion() { + t.done.Lock() +} + +func convertToTaskThread(thread *phpThread) *taskThread { + handler := &taskThread{ + thread: thread, + execChan: make(chan *task), + } + thread.setHandler(handler) + return handler +} + +func (handler *taskThread) beforeScriptExecution() string { + thread := handler.thread + + switch thread.state.get() { + case stateTransitionRequested: + return thread.transitionToNewHandler() + case stateBooting, stateTransitionComplete: + thread.state.set(stateReady) + handler.waitForTasks() + + return handler.beforeScriptExecution() + case stateReady: + handler.waitForTasks() + + return handler.beforeScriptExecution() + case stateShuttingDown: + // signal to stop + return "" + } + panic("unexpected state: " + thread.state.name()) +} + +func (handler *taskThread) afterScriptExecution(int) { + panic("task threads should not execute scripts") +} + +func (handler *taskThread) getRequestContext() *frankenPHPContext { + return nil +} + +func (handler *taskThread) name() string { + return "Task PHP Thread" +} + +func (handler *taskThread) waitForTasks() { + for { + select { + case task := <-handler.execChan: + task.callback() + task.done.Unlock() // unlock the task to signal completion + case <-handler.thread.drainChan: + // thread is shutting down, do not execute the function + return + } + } +} + +func (handler *taskThread) execute(t *task) { + handler.execChan <- t +} diff --git a/types.go b/types.go index 02044bc88e..b16d66a026 100644 --- a/types.go +++ b/types.go @@ -4,7 +4,11 @@ package frankenphp #include "types.h" */ import "C" -import "unsafe" +import ( + "fmt" + "strconv" + "unsafe" +) // EXPERIMENTAL: GoString copies a zend_string to a Go string. func GoString(s unsafe.Pointer) string { @@ -34,101 +38,174 @@ 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 +type KeyValuePair struct { + key string + value interface{} } // Array represents a PHP array with ordered key-value pairs type Array struct { - keys []PHPKey - values []interface{} + isPacked bool + pairs map[string]KeyValuePair + entries []KeyValuePair + packedValues []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) +// EXPERIMENTAL: NewArray creates a new associative array +func NewArray(entries ...KeyValuePair) Array { + arr := Array{ + pairs: make(map[string]KeyValuePair), + entries: entries, + } + for _, pair := range entries { + arr.pairs[pair.key] = pair + } + + return arr } -// 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) +// EXPERIMENTAL: NewPackedArray creates a new packed array +func NewPackedArray(values ...interface{}) Array { + arr := Array{ + isPacked: true, + packedValues: values, + } + + return arr } -// 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) +// Get value (for associative arrays) +func (arr *Array) Get(key string) (interface{}, bool) { + if pair, exists := arr.pairs[key]; exists { + return pair.value, true + } + return nil, false } -// 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 +// Set value (for associative arrays) +func (arr *Array) Set(key string, value interface{}) { + if pair, exists := arr.pairs[key]; exists { + pair.value = value + return + } + + // setting a key in a packed array will make it an associative array + if arr.isPacked { + arr.isPacked = false + for i, v := range arr.packedValues { + arr.Set(strconv.Itoa(i), v) } + arr.packedValues = nil + } + + newPair := KeyValuePair{key: key, value: value} + arr.entries = append(arr.entries, newPair) + if arr.pairs == nil { + arr.pairs = make(map[string]KeyValuePair) + } + arr.pairs[key] = newPair +} + +// Set value at index (for packed arrays) +func (arr *Array) SetAtIndex(index int, value interface{}) error { + if !arr.isPacked { + return fmt.Errorf("SetAtIndex is only supported for packed arrays, use Set instead") } - return maxKey + 1 + if index < 0 || index >= len(arr.packedValues) { + return fmt.Errorf("Index %d out of bounds for packed array with length %d", index, len(arr.packedValues)) + } + arr.packedValues[index] = value + + return nil } -// Len returns the number of elements in the array -func (arr *Array) Len() uint32 { - return uint32(len(arr.keys)) +// Get value at index (for packed arrays) +func (arr *Array) GetAtIndex(index int) (interface{}, error) { + if !arr.isPacked { + return nil, fmt.Errorf("GetAtIndex is only supported for packed arrays, use Get instead") + } + + if index < 0 || index >= len(arr.packedValues) { + return nil, fmt.Errorf("Index %d out of bounds for packed array with length %d", index, len(arr.packedValues)) + } + + return arr.packedValues[index], nil } -// 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 +// Append to the array (only for packed arrays) +func (arr *Array) Append(value interface{}) error { + if !arr.isPacked { + return fmt.Errorf("Append is not supported for associative arrays, use Set instead") } - return arr.keys[index], arr.values[index] + + arr.packedValues = append(arr.packedValues, value) + + return nil } -// 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), +func (arr *Array) IsPacked() bool { + return arr.isPacked +} + +// Entries returns all ordered key-value pairs, best performance for associative arrays +func (arr *Array) Entries() []KeyValuePair { + if !arr.isPacked { + return arr.entries + } + entries := make([]KeyValuePair, 0, len(arr.packedValues)) + for i, value := range arr.packedValues { + entries = append(entries, KeyValuePair{key: strconv.Itoa(i), value: value}) } + return entries +} + +// Values returns all values, best performance for packed arrays +func (arr *Array) Values() []interface{} { + if arr.isPacked { + return arr.packedValues + } + values := make([]interface{}, 0, len(arr.entries)) + for _, pair := range arr.entries { + values = append(values, pair.value) + } + + return values +} + +// EXPERIMENTAL: GoArray converts a zend_array to a Go Array +func GoArray(arr unsafe.Pointer) Array { if arr == nil { - return result + fmt.Println("GoArray received a nil pointer") + return Array{} } zval := (*C.zval)(arr) hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY)) if hashTable == nil { - return result + fmt.Println("GoArray received a nil pointer") + return Array{} } - used := hashTable.nNumUsed + nNumUsed := hashTable.nNumUsed + result := Array{} + if htIsPacked(hashTable) { - for i := C.uint32_t(0); i < used; i++ { + result.isPacked = true + result.packedValues = make([]interface{}, 0, nNumUsed) + for i := C.uint32_t(0); i < nNumUsed; 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) + result.packedValues = append(result.packedValues, convertZvalToGo(v)) } } return result } - for i := C.uint32_t(0); i < used; i++ { + for i := C.uint32_t(0); i < nNumUsed; i++ { bucket := C.get_ht_bucket_data(hashTable, i) if bucket == nil || C.zval_get_type(&bucket.val) == C.IS_UNDEF { continue @@ -138,58 +215,40 @@ func GoArray(arr unsafe.Pointer) *Array { if bucket.key != nil { keyStr := GoString(unsafe.Pointer(bucket.key)) - result.SetString(keyStr, v) + result.Set(keyStr, v) continue } - result.SetInt(int64(bucket.h), v) + // as fallback convert the bucket index to a string key + result.Set(strconv.Itoa(int(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)) - } +func PHPArray(arr Array) unsafe.Pointer { + zendArray := createNewArray((uint32)(len(arr.entries))) - 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 { + if arr.isPacked { + for _, v := range arr.packedValues { zval := convertGoToZval(v) C.zend_hash_next_index_insert(zendArray, zval) } + } else { + for _, pair := range arr.entries { + zval := convertGoToZval(pair.value) - return unsafe.Pointer(zendArray) - } - - zendArray = createNewArray(arr.Len()) - for i, k := range arr.keys { - zval := convertGoToZval(arr.values[i]) - - if k.Type == PHPStringKey { - keyStr := k.Str - keyData := (*C.char)(unsafe.Pointer(unsafe.StringData(keyStr))) - C.zend_hash_str_add(zendArray, keyData, C.size_t(len(keyStr)), zval) - - continue + keyStr := PHPString(pair.key, false) + C.zend_hash_update(zendArray, (*C.zend_string)(keyStr), zval) } - - C.zend_hash_index_update(zendArray, C.zend_ulong(k.Int), zval) } - return unsafe.Pointer(zendArray) + var zval C.zval + C.__zval_arr__(&zval, zendArray) + + return unsafe.Pointer(&zval) } // convertZvalToGo converts a PHP zval to a Go interface{} @@ -246,9 +305,8 @@ func convertGoToZval(value interface{}) *C.zval { case string: str := (*C.zend_string)(PHPString(v, false)) C.__zval_string__(&zval, str) - case *Array: - arr := (*C.zend_array)(PHPArray(v)) - C.__zval_arr__(&zval, arr) + case Array: + return (*C.zval)(PHPArray(v)) default: C.__zval_null__(&zval) } diff --git a/types_test.go b/types_test.go index be4559a4f6..a43d8ff035 100644 --- a/types_test.go +++ b/types_test.go @@ -1,7 +1,131 @@ package frankenphp -import "testing" +import ( + "io" + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" +) + +// execute the function on a PHP thread directly +// this is necessary if tests make use of PHP's internal allocation +func testOnDummyPHPThread(t *testing.T, test func()) { + t.Helper() + logger = slog.New(slog.NewTextHandler(io.Discard, nil)) + _, err := initPHPThreads(1, 1, nil) // boot 1 thread + assert.NoError(t, err) + handler := convertToTaskThread(phpThreads[0]) + + task := newTask(test) + handler.execute(task) + task.waitForCompletion() + + drainPHPThreads() +} func TestGoString(t *testing.T) { - testGoString(t) + testOnDummyPHPThread(t, func() { + originalString := "Hello, World!" + + convertedString := GoString(PHPString(originalString, false)) + + assert.Equal(t, originalString, convertedString, "string -> zend_string -> string should yield an equal string") + }) +} + +func TestPHPArray(t *testing.T) { + testOnDummyPHPThread(t, func() { + originalArray := NewArray( + KeyValuePair{"foo1", "bar1"}, + KeyValuePair{"foo2", "bar2"}, + ) + originalArray.Set("foo3", "bar3") + + convertedArray := GoArray(PHPArray(originalArray)) + + assert.Equal(t, originalArray, convertedArray, "associative array should be equal after conversion") + assert.Len(t, convertedArray.Entries(), 3) + assert.Len(t, convertedArray.Values(), 3) + foo1, exists := convertedArray.Get("foo1") + assert.True(t, exists, "key 'foo1' should exist in the converted array") + assert.Equal(t, "bar1", foo1, "value for key 'foo1' should be 'bar1'") + _, exists = convertedArray.Get("foo4") + assert.False(t, exists, "key 'foo4' should exist in the converted array") + }) +} + +func TestPHPPackedArray(t *testing.T) { + testOnDummyPHPThread(t, func() { + originalArray := NewPackedArray( + "bar1", + "bar2", + ) + originalArray.Append("bar3") + + goArray := GoArray(PHPArray(originalArray)) + + assert.Equal(t, originalArray, goArray, "packed array should be equal after conversion") + bar1, err := goArray.GetAtIndex(0) + assert.NoError(t, err, "GetAtIndex should not return an error for packed arrays") + assert.Equal(t, "bar1", bar1, "first element should be 'bar1'") + bar3, err := goArray.GetAtIndex(2) + assert.NoError(t, err, "GetAtIndex should not return an error for packed arrays") + assert.Equal(t, "bar3", bar3, "third element should be 'bar3'") + _, err = goArray.GetAtIndex(3) + assert.Error(t, err, "GetAtIndex should return an error for out-of-bounds index on packed arrays") + }) +} + +func TestNestedMixedArray(t *testing.T) { + testOnDummyPHPThread(t, func() { + originalArray := NewArray( + KeyValuePair{"array", NewArray( + KeyValuePair{"foo", "bar"}, + )}, + KeyValuePair{"packedArray", NewPackedArray( + float64(1.1), + int64(2), + "packed", + )}, + KeyValuePair{"integer", int64(1)}, + KeyValuePair{"float", float64(1.2)}, + KeyValuePair{"string", "bar"}, + KeyValuePair{"null", nil}, + KeyValuePair{"false", false}, + KeyValuePair{"true", true}, + ) + + zendArray := PHPArray(originalArray) + goArray := GoArray(zendArray) + + assert.Equal(t, originalArray, goArray, "nested mixed array should be equal after conversion") + }) +} + +func TestArrayShouldBeCorrectlyPacked(t *testing.T) { + originalArray := NewPackedArray("bar1", "bar2") + + assert.True(t, originalArray.IsPacked(), "Array should be packed") + + originalArray.Append("bar3") + + assert.Equal(t, 3, len(originalArray.packedValues), "Packed array should have 3 elements after appending") + assert.True(t, originalArray.IsPacked(), "Array should be packed after Append") + + err := originalArray.SetAtIndex(1, "newBar2") + + assert.NoError(t, err, "SetAtIndex should not return an error for packed arrays") + assert.Equal(t, "newBar2", originalArray.Values()[1], "Second element should be updated to 'newBar2'") + assert.True(t, originalArray.IsPacked(), "Array should be packed after SetAtIndex") + + originalArray.Set("hello", "world") + assert.False(t, originalArray.IsPacked(), "Array should not be packed anymore after setting a string offset") + + assert.Equal(t, NewArray( + KeyValuePair{"0", "bar1"}, + KeyValuePair{"1", "newBar2"}, + KeyValuePair{"2", "bar3"}, + KeyValuePair{"hello", "world"}, + ), originalArray, "Array should be associated after mixed operations") } diff --git a/typestest.go b/typestest.go deleted file mode 100644 index 178dae220b..0000000000 --- a/typestest.go +++ /dev/null @@ -1,18 +0,0 @@ -package frankenphp - -//#include -// -//zend_string *hello_string() { -// return zend_string_init("Hello", 5, 1); -//} -import "C" -import ( - "github.com/stretchr/testify/assert" - "testing" - "unsafe" -) - -func testGoString(t *testing.T) { - assert.Equal(t, "", GoString(nil)) - assert.Equal(t, "Hello", GoString(unsafe.Pointer(C.hello_string()))) -} From af7a4f4f246f291aeb99c00a46127bb39f77e5d8 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sun, 10 Aug 2025 22:30:13 +0200 Subject: [PATCH 02/18] NewAssociativeArray. --- docs/extensions.md | 4 ++-- types.go | 4 ++-- types_test.go | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index dcfb4dcc78..b41cb9dc58 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -112,7 +112,7 @@ func process_data(arr *C.zval) unsafe.Pointer { // Convert PHP array to Go goArray := frankenphp.GoArray(unsafe.Pointer(arr)) - result := NewArray( + result := NewAssociativeArray( KeyValuePair{"firstName", "John"}, KeyValuePair{"lastName", "Doe"}, ) @@ -164,7 +164,7 @@ func example_packed_array(arr *C.zval) unsafe.Pointer { **Associative array only** -* `NewArray(entries ...KeyValuePair) *frankenphp.Array` - Create a new associative array with key-value pairs +* `NewAssociativeArray(entries ...KeyValuePair) *frankenphp.Array` - Create a new associative array with key-value pairs * `Set(key string, value interface{})` - Set value with key (associative array), will convert a packed array to associative * `Get(key string) interface{}` - Get value with O(1) (associative array) diff --git a/types.go b/types.go index b16d66a026..0c35e14186 100644 --- a/types.go +++ b/types.go @@ -51,8 +51,8 @@ type Array struct { packedValues []interface{} } -// EXPERIMENTAL: NewArray creates a new associative array -func NewArray(entries ...KeyValuePair) Array { +// EXPERIMENTAL: NewAssociativeArray creates a new associative array +func NewAssociativeArray(entries ...KeyValuePair) Array { arr := Array{ pairs: make(map[string]KeyValuePair), entries: entries, diff --git a/types_test.go b/types_test.go index a43d8ff035..1545723ae4 100644 --- a/types_test.go +++ b/types_test.go @@ -36,7 +36,7 @@ func TestGoString(t *testing.T) { func TestPHPArray(t *testing.T) { testOnDummyPHPThread(t, func() { - originalArray := NewArray( + originalArray := NewAssociativeArray( KeyValuePair{"foo1", "bar1"}, KeyValuePair{"foo2", "bar2"}, ) @@ -79,8 +79,8 @@ func TestPHPPackedArray(t *testing.T) { func TestNestedMixedArray(t *testing.T) { testOnDummyPHPThread(t, func() { - originalArray := NewArray( - KeyValuePair{"array", NewArray( + originalArray := NewAssociativeArray( + KeyValuePair{"array", NewAssociativeArray( KeyValuePair{"foo", "bar"}, )}, KeyValuePair{"packedArray", NewPackedArray( @@ -122,7 +122,7 @@ func TestArrayShouldBeCorrectlyPacked(t *testing.T) { originalArray.Set("hello", "world") assert.False(t, originalArray.IsPacked(), "Array should not be packed anymore after setting a string offset") - assert.Equal(t, NewArray( + assert.Equal(t, NewAssociativeArray( KeyValuePair{"0", "bar1"}, KeyValuePair{"1", "newBar2"}, KeyValuePair{"2", "bar3"}, From 009ad61ee5fa1b84ab27a84636adc867c5a52845 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sun, 10 Aug 2025 22:41:29 +0200 Subject: [PATCH 03/18] linting --- docs/extensions.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index b41cb9dc58..869b71cd12 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -153,22 +153,20 @@ func example_packed_array(arr *C.zval) unsafe.Pointer { * **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:** - -**Packed and associative** +##### Available methods - packed and associative * `Entries() []KeyValuePair` - Get all entries by key-value (recommended for associative array, but also works for packed array) * `Values() []interface{}` - Get values (recommended for packed array, but also works for associative array) * `IsPacked() bool` - Weather packed array (list) or associative array (hashmap) * `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convert to PHP array (*zval) -**Associative array only** +##### Associative only * `NewAssociativeArray(entries ...KeyValuePair) *frankenphp.Array` - Create a new associative array with key-value pairs * `Set(key string, value interface{})` - Set value with key (associative array), will convert a packed array to associative * `Get(key string) interface{}` - Get value with O(1) (associative array) -**Packed array only** +##### Packed only * `NewPackedArray(values ...interface{}) *frankenphp.Array` - Create a new packed array with values * `SetAtIndex(index int64, value interface{}) error` - Set value at index (packed array) From 4cb93373d5e020a8ad6f10cc2bcf774f7f5aae15 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sun, 10 Aug 2025 22:45:29 +0200 Subject: [PATCH 04/18] go linting --- types.go | 8 ++++---- types_test.go | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/types.go b/types.go index 0c35e14186..8f1bfa2d78 100644 --- a/types.go +++ b/types.go @@ -109,11 +109,11 @@ func (arr *Array) Set(key string, value interface{}) { // Set value at index (for packed arrays) func (arr *Array) SetAtIndex(index int, value interface{}) error { if !arr.isPacked { - return fmt.Errorf("SetAtIndex is only supported for packed arrays, use Set instead") + return fmt.Errorf("method is only supported for packed arrays, use Set instead") } if index < 0 || index >= len(arr.packedValues) { - return fmt.Errorf("Index %d out of bounds for packed array with length %d", index, len(arr.packedValues)) + return fmt.Errorf("index %d out of bounds for packed array with length %d", index, len(arr.packedValues)) } arr.packedValues[index] = value @@ -123,11 +123,11 @@ func (arr *Array) SetAtIndex(index int, value interface{}) error { // Get value at index (for packed arrays) func (arr *Array) GetAtIndex(index int) (interface{}, error) { if !arr.isPacked { - return nil, fmt.Errorf("GetAtIndex is only supported for packed arrays, use Get instead") + return nil, fmt.Errorf("method is only supported for packed arrays, use Get instead") } if index < 0 || index >= len(arr.packedValues) { - return nil, fmt.Errorf("Index %d out of bounds for packed array with length %d", index, len(arr.packedValues)) + return nil, fmt.Errorf("index %d out of bounds for packed array with length %d", index, len(arr.packedValues)) } return arr.packedValues[index], nil diff --git a/types_test.go b/types_test.go index 1545723ae4..a1aab66ebd 100644 --- a/types_test.go +++ b/types_test.go @@ -61,10 +61,11 @@ func TestPHPPackedArray(t *testing.T) { "bar1", "bar2", ) - originalArray.Append("bar3") + err := originalArray.Append("bar3") goArray := GoArray(PHPArray(originalArray)) + assert.NoError(t, err) assert.Equal(t, originalArray, goArray, "packed array should be equal after conversion") bar1, err := goArray.GetAtIndex(0) assert.NoError(t, err, "GetAtIndex should not return an error for packed arrays") @@ -108,12 +109,13 @@ func TestArrayShouldBeCorrectlyPacked(t *testing.T) { assert.True(t, originalArray.IsPacked(), "Array should be packed") - originalArray.Append("bar3") + err := originalArray.Append("bar3") + assert.NoError(t, err) assert.Equal(t, 3, len(originalArray.packedValues), "Packed array should have 3 elements after appending") assert.True(t, originalArray.IsPacked(), "Array should be packed after Append") - err := originalArray.SetAtIndex(1, "newBar2") + err = originalArray.SetAtIndex(1, "newBar2") assert.NoError(t, err, "SetAtIndex should not return an error for packed arrays") assert.Equal(t, "newBar2", originalArray.Values()[1], "Second element should be updated to 'newBar2'") From 274d2ff8db304ca7f1caa2e3b993c3e149362ce0 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Wed, 13 Aug 2025 14:19:46 +0200 Subject: [PATCH 05/18] Exposes all primitive types. --- docs/extensions.md | 141 ++++++++++++------------ types.go | 261 +++++++++++++++++++-------------------------- types_test.go | 144 +++++++++++++------------ 3 files changed, 255 insertions(+), 291 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index 869b71cd12..ec32c567b0 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -81,17 +81,18 @@ 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` | `*frankenphp.Array` | ❌ | frankenphp.GoArray() | frankenphp.PHPArray() | ✅ | -| `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.AssociativeArray` | ❌ | frankenphp.GoAssociativeArray() | frankenphp.PHPAssociativeArray() | ✅ | +| `array (packed)` | `*frankenphp.PackedArray` | ❌ | frankenphp.GoPackedArray() | frankenphp.PHPPackedArray() | ✅ | +| `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. @@ -102,76 +103,84 @@ If you refer to the code snippet of the previous section, you can see that helpe #### 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. +FrankenPHP provides native support for PHP arrays through the `frankenphp.AssociativeArray` array amd `frankenphp.PackedArray` types. + +`AssociativeArray` represents an associative hashmap and is composed of a `Map: map[string]interface{}` and an optional order `[]string`. + +`PackedArray` is just an alias for a go slice `[]interface{}` **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 := NewAssociativeArray( - KeyValuePair{"firstName", "John"}, - KeyValuePair{"lastName", "Doe"}, - ) - - result.Set("age", 25") - - // loop over the entries - for _, entry := range goArray.Entries() { - result.Set("processed_"+entry.key, entry.value) - } - - // Convert back to PHP array (*zval) - return frankenphp.PHPArray(result) +// export_php:function process_data_ordered(array $input): array +func process_data_ordered(arr *C.zval) unsafe.Pointer { + // Convert PHP associative array to Go while keeping the order + associativeArray := frankenphp.GoAssociativeArray(unsafe.Pointer(arr), true) + + // loop over the entries in order + for _, key := range associativeArray.Order { + value, _ = associativeArray.Map[key] + // do something with key and value + } + + // return an ordered array + return frankenphp.PHPAssociativeArray(AssociativeArray{ + Map: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + Order: []string{"key1", "key2"}, + }) } -//export_php:function process_data(array $input): array -func example_packed_array(arr *C.zval) unsafe.Pointer { - packedArray := NewPackedArray( - "first", - "second", - "third", - ) - - for i, value := range packedArray.Values() { - // This will return a error if the array is not packed - _ := packedArray.SetAtIndex(i, value+"-processed") - } - - // Convert back to a PHP packed array (*zval) - return frankenphp.PHPArray(packedArray) +// export_php:function process_data_unordered(array $input): array +func process_data_unordered(arr *C.zval) unsafe.Pointer { + // Convert PHP associative array to Go without keeping the order + // ignoring the order will be more performant + associativeArray := frankenphp.GoAssociativeArray(unsafe.Pointer(arr), false) + + // loop over the entries in no specific order + for key, value := range associativeArray.Map { + value, _ = associativeArray.Map[key] + // do something with key and value + } + + // return an unordered array + return frankenphp.PHPAssociativeArray(AssociativeArray{Map: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }}) +} + +// export_php:function process_data_packed(array $input): array +func process_data_packed(arr *C.zval) unsafe.Pointer { + // Convert PHP packed array to Go + packedArray := frankenphp.GoPackedArray(unsafe.Pointer(arr), false) + + // loop over the slice in order + for index, value := range packedArray { + // do something with index and value + } + + // return a packed array + return frankenphp.PHPackedArray{"value1", "value2", "value3"} } ``` -**Key features of `frankenphp.Array`:** +**Key features of `frankenphp.PackedArray` and `frankenphp.AssociativeArray`:** -* **Ordered key-value pairs** - Maintains insertion order like PHP arrays -* **Optimized based on keys** - The array will automatically be associative or packed based on the keys used +* **Ordered key-value pairs** - Option to keep the order of the associative array +* **Optimized for multiple cases** - Option to ditch the order for better performance or convert straight to a slice * **Automatic list detection** - When converting to PHP, automatically detects if array should be a packed list or hashmap +* **Nested Arrays** - Arrays can be nested and will convert all support types automatically (int64,float64,string,bool,nil,AssociativeArray,PackedArray) * **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 - packed and associative -* `Entries() []KeyValuePair` - Get all entries by key-value (recommended for associative array, but also works for packed array) -* `Values() []interface{}` - Get values (recommended for packed array, but also works for associative array) -* `IsPacked() bool` - Weather packed array (list) or associative array (hashmap) -* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convert to PHP array (*zval) - -##### Associative only - -* `NewAssociativeArray(entries ...KeyValuePair) *frankenphp.Array` - Create a new associative array with key-value pairs -* `Set(key string, value interface{})` - Set value with key (associative array), will convert a packed array to associative -* `Get(key string) interface{}` - Get value with O(1) (associative array) - -##### Packed only - -* `NewPackedArray(values ...interface{}) *frankenphp.Array` - Create a new packed array with values -* `SetAtIndex(index int64, value interface{}) error` - Set value at index (packed array) -* `GetAtIndex(index int64) (value interface{} error)` - Get value at index (packed array) -* `Append(value interface{}) error` - Add value at the end (packed array) +* `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to a PHP array with key value pairs, optionally pass a specific order +* `frankenphp.PHPPackedArray(arr frankenphp.PackedArray) unsafe.Pointer` - Convert to a PHP packed array with indexed values only +* `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) (frankenphp.AssociativeArray, error)` - Convert a PHP array to a Go map and optionally keep the order +* `frankenphp.GoPackedArray(arr unsafe.Pointer) (frankenphp.PackedArray, error)` - Convert a PHP array to a go slice ### Declaring a Native PHP Class diff --git a/types.go b/types.go index 8f1bfa2d78..d81a809389 100644 --- a/types.go +++ b/types.go @@ -5,7 +5,7 @@ package frankenphp */ import "C" import ( - "fmt" + "errors" "strconv" "unsafe" ) @@ -38,209 +38,136 @@ func PHPString(s string, persistent bool) unsafe.Pointer { return unsafe.Pointer(zendStr) } -type KeyValuePair struct { - key string - value interface{} +// AssociativeArray represents a PHP array with ordered key-value pairs +type AssociativeArray struct { + Map map[string]interface{} + Order []string } -// Array represents a PHP array with ordered key-value pairs -type Array struct { - isPacked bool - pairs map[string]KeyValuePair - entries []KeyValuePair - packedValues []interface{} -} +// PackedArray represents a 'packed' PHP array as an indexed list +type PackedArray = []interface{} -// EXPERIMENTAL: NewAssociativeArray creates a new associative array -func NewAssociativeArray(entries ...KeyValuePair) Array { - arr := Array{ - pairs: make(map[string]KeyValuePair), - entries: entries, - } - for _, pair := range entries { - arr.pairs[pair.key] = pair - } - - return arr -} +type ZendArray = unsafe.Pointer -// EXPERIMENTAL: NewPackedArray creates a new packed array -func NewPackedArray(values ...interface{}) Array { - arr := Array{ - isPacked: true, - packedValues: values, +// EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray +func GoAssociativeArray(arr unsafe.Pointer, ordered bool) (AssociativeArray, error) { + if arr == nil { + return AssociativeArray{}, errors.New("GoAssociativeArray received a nil pointer") } - return arr -} + zval := (*C.zval)(arr) + hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY)) -// Get value (for associative arrays) -func (arr *Array) Get(key string) (interface{}, bool) { - if pair, exists := arr.pairs[key]; exists { - return pair.value, true + if hashTable == nil { + return AssociativeArray{}, errors.New("GoAssociativeArray received a zval that was not a HashTable") } - return nil, false -} -// Set value (for associative arrays) -func (arr *Array) Set(key string, value interface{}) { - if pair, exists := arr.pairs[key]; exists { - pair.value = value - return + nNumUsed := hashTable.nNumUsed + result := AssociativeArray{Map: make(map[string]interface{})} + if ordered { + result.Order = make([]string, 0, nNumUsed) } - // setting a key in a packed array will make it an associative array - if arr.isPacked { - arr.isPacked = false - for i, v := range arr.packedValues { - arr.Set(strconv.Itoa(i), v) + if htIsPacked(hashTable) { + // if the HashTable is packed, convert all integer keys to strings + for i := C.uint32_t(0); i < nNumUsed; i++ { + v := C.get_ht_packed_data(hashTable, i) + if v != nil && C.zval_get_type(v) != C.IS_UNDEF { + strIndex := strconv.Itoa(int(i)) + result.Map[strIndex] = convertZvalToGo(v) + if ordered { + result.Order = append(result.Order, strIndex) + } + } } - arr.packedValues = nil - } - newPair := KeyValuePair{key: key, value: value} - arr.entries = append(arr.entries, newPair) - if arr.pairs == nil { - arr.pairs = make(map[string]KeyValuePair) + return result, nil } - arr.pairs[key] = newPair -} -// Set value at index (for packed arrays) -func (arr *Array) SetAtIndex(index int, value interface{}) error { - if !arr.isPacked { - return fmt.Errorf("method is only supported for packed arrays, use Set instead") - } - - if index < 0 || index >= len(arr.packedValues) { - return fmt.Errorf("index %d out of bounds for packed array with length %d", index, len(arr.packedValues)) - } - arr.packedValues[index] = value - - return nil -} - -// Get value at index (for packed arrays) -func (arr *Array) GetAtIndex(index int) (interface{}, error) { - if !arr.isPacked { - return nil, fmt.Errorf("method is only supported for packed arrays, use Get instead") - } - - if index < 0 || index >= len(arr.packedValues) { - return nil, fmt.Errorf("index %d out of bounds for packed array with length %d", index, len(arr.packedValues)) - } - - return arr.packedValues[index], nil -} - -// Append to the array (only for packed arrays) -func (arr *Array) Append(value interface{}) error { - if !arr.isPacked { - return fmt.Errorf("Append is not supported for associative arrays, use Set instead") - } - - arr.packedValues = append(arr.packedValues, value) - - return nil -} + for i := C.uint32_t(0); i < nNumUsed; i++ { + bucket := C.get_ht_bucket_data(hashTable, i) + if bucket == nil || C.zval_get_type(&bucket.val) == C.IS_UNDEF { + continue + } -func (arr *Array) IsPacked() bool { - return arr.isPacked -} + v := convertZvalToGo(&bucket.val) -// Entries returns all ordered key-value pairs, best performance for associative arrays -func (arr *Array) Entries() []KeyValuePair { - if !arr.isPacked { - return arr.entries - } - entries := make([]KeyValuePair, 0, len(arr.packedValues)) - for i, value := range arr.packedValues { - entries = append(entries, KeyValuePair{key: strconv.Itoa(i), value: value}) - } + if bucket.key != nil { + keyStr := GoString(unsafe.Pointer(bucket.key)) + result.Map[keyStr] = v + if ordered { + result.Order = append(result.Order, keyStr) + } - return entries -} + continue + } -// Values returns all values, best performance for packed arrays -func (arr *Array) Values() []interface{} { - if arr.isPacked { - return arr.packedValues - } - values := make([]interface{}, 0, len(arr.entries)) - for _, pair := range arr.entries { - values = append(values, pair.value) + // as fallback convert the bucket index to a string key + strIndex := strconv.Itoa(int(bucket.h)) + result.Map[strIndex] = v + if ordered { + result.Order = append(result.Order, strIndex) + } } - return values + return result, nil } -// EXPERIMENTAL: GoArray converts a zend_array to a Go Array -func GoArray(arr unsafe.Pointer) Array { +// EXPERIMENTAL: GoPackedArray converts a zend_array to a Go slice +func GoPackedArray(arr unsafe.Pointer) (PackedArray, error) { if arr == nil { - fmt.Println("GoArray received a nil pointer") - return Array{} + return PackedArray{}, errors.New("GoPackedArray received a nil pointer") } zval := (*C.zval)(arr) hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY)) if hashTable == nil { - fmt.Println("GoArray received a nil pointer") - return Array{} + return PackedArray{}, errors.New("GoPackedArray received zval that wasn'T a HashTable") } nNumUsed := hashTable.nNumUsed - result := Array{} + result := make(PackedArray, 0, nNumUsed) if htIsPacked(hashTable) { - result.isPacked = true - result.packedValues = make([]interface{}, 0, nNumUsed) for i := C.uint32_t(0); i < nNumUsed; i++ { v := C.get_ht_packed_data(hashTable, i) if v != nil && C.zval_get_type(v) != C.IS_UNDEF { - result.packedValues = append(result.packedValues, convertZvalToGo(v)) + result = append(result, convertZvalToGo(v)) } } - return result + return result, nil } + // fallback if ht isn't packed - equivalent to array_values() for i := C.uint32_t(0); i < nNumUsed; i++ { bucket := C.get_ht_bucket_data(hashTable, i) - if bucket == nil || C.zval_get_type(&bucket.val) == C.IS_UNDEF { - continue + if bucket != nil && C.zval_get_type(&bucket.val) != C.IS_UNDEF { + result = append(result, convertZvalToGo(&bucket.val)) } - - v := convertZvalToGo(&bucket.val) - - if bucket.key != nil { - keyStr := GoString(unsafe.Pointer(bucket.key)) - result.Set(keyStr, v) - - continue - } - - // as fallback convert the bucket index to a string key - result.Set(strconv.Itoa(int(bucket.h)), v) } - return result + return result, nil } -// PHPArray converts a Go Array to a PHP zend_array. -func PHPArray(arr Array) unsafe.Pointer { - zendArray := createNewArray((uint32)(len(arr.entries))) +// PHPAssociativeArray converts a Go AssociativeArray to a PHP zend_array. +func PHPAssociativeArray(arr AssociativeArray) ZendArray { + var zendArray *C.HashTable - if arr.isPacked { - for _, v := range arr.packedValues { - zval := convertGoToZval(v) - C.zend_hash_next_index_insert(zendArray, zval) + if len(arr.Order) != 0 { + zendArray = createNewArray((uint32)(len(arr.Order))) + for _, key := range arr.Order { + val, _ := arr.Map[key] + zval := convertGoToZval(val) + keyStr := PHPString(key, false) + C.zend_hash_update(zendArray, (*C.zend_string)(keyStr), zval) } } else { - for _, pair := range arr.entries { - zval := convertGoToZval(pair.value) - - keyStr := PHPString(pair.key, false) + zendArray = createNewArray((uint32)(len(arr.Map))) + for key, val := range arr.Map { + zval := convertGoToZval(val) + keyStr := PHPString(key, false) C.zend_hash_update(zendArray, (*C.zend_string)(keyStr), zval) } } @@ -251,6 +178,20 @@ func PHPArray(arr Array) unsafe.Pointer { return unsafe.Pointer(&zval) } +// PHPPackedArray converts a Go PHPPackedArray to a PHP zend_array. +func PHPPackedArray(arr PackedArray) ZendArray { + zendArray := createNewArray((uint32)(len(arr))) + for _, val := range arr { + zval := convertGoToZval(val) + C.zend_hash_next_index_insert(zendArray, zval) + } + + var zval C.zval + C.__zval_arr__(&zval, zendArray) + + return unsafe.Pointer(&zval) +} + // convertZvalToGo converts a PHP zval to a Go interface{} func convertZvalToGo(zval *C.zval) interface{} { t := C.zval_get_type(zval) @@ -281,7 +222,15 @@ func convertZvalToGo(zval *C.zval) interface{} { return GoString(unsafe.Pointer(str)) case C.IS_ARRAY: - return GoArray(unsafe.Pointer(zval)) + hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY)) + if hashTable != nil && htIsPacked(hashTable) { + packedArray, _ := GoPackedArray(unsafe.Pointer(zval)) + + return packedArray + } + associativeArray, _ := GoAssociativeArray(unsafe.Pointer(zval), true) + + return associativeArray default: return nil } @@ -305,8 +254,12 @@ func convertGoToZval(value interface{}) *C.zval { case string: str := (*C.zend_string)(PHPString(v, false)) C.__zval_string__(&zval, str) - case Array: - return (*C.zval)(PHPArray(v)) + case PackedArray: + return (*C.zval)(PHPPackedArray(v)) + case AssociativeArray: + return (*C.zval)(PHPAssociativeArray(v)) + case map[string]interface{}: + return (*C.zval)(PHPAssociativeArray(AssociativeArray{Map: v})) default: C.__zval_null__(&zval) } diff --git a/types_test.go b/types_test.go index a1aab66ebd..f4ea6e31cf 100644 --- a/types_test.go +++ b/types_test.go @@ -34,100 +34,102 @@ func TestGoString(t *testing.T) { }) } -func TestPHPArray(t *testing.T) { +func TestPHPAssociativeArray(t *testing.T) { testOnDummyPHPThread(t, func() { - originalArray := NewAssociativeArray( - KeyValuePair{"foo1", "bar1"}, - KeyValuePair{"foo2", "bar2"}, - ) - originalArray.Set("foo3", "bar3") + originalArray := AssociativeArray{Map: map[string]interface{}{ + "foo1": "bar1", + "foo2": "bar2", + }} - convertedArray := GoArray(PHPArray(originalArray)) + convertedArray, err := GoAssociativeArray(PHPAssociativeArray(originalArray), false) + assert.NoError(t, err) assert.Equal(t, originalArray, convertedArray, "associative array should be equal after conversion") - assert.Len(t, convertedArray.Entries(), 3) - assert.Len(t, convertedArray.Values(), 3) - foo1, exists := convertedArray.Get("foo1") - assert.True(t, exists, "key 'foo1' should exist in the converted array") - assert.Equal(t, "bar1", foo1, "value for key 'foo1' should be 'bar1'") - _, exists = convertedArray.Get("foo4") - assert.False(t, exists, "key 'foo4' should exist in the converted array") }) } -func TestPHPPackedArray(t *testing.T) { +func TestOrderedPHPAssociativeArray(t *testing.T) { testOnDummyPHPThread(t, func() { - originalArray := NewPackedArray( - "bar1", - "bar2", - ) - err := originalArray.Append("bar3") + originalArray := AssociativeArray{ + Map: map[string]interface{}{ + "foo1": "bar1", + "foo2": "bar2", + }, + Order: []string{"foo2", "foo1"}, + } - goArray := GoArray(PHPArray(originalArray)) + convertedArray, err := GoAssociativeArray(PHPAssociativeArray(originalArray), true) assert.NoError(t, err) - assert.Equal(t, originalArray, goArray, "packed array should be equal after conversion") - bar1, err := goArray.GetAtIndex(0) - assert.NoError(t, err, "GetAtIndex should not return an error for packed arrays") - assert.Equal(t, "bar1", bar1, "first element should be 'bar1'") - bar3, err := goArray.GetAtIndex(2) - assert.NoError(t, err, "GetAtIndex should not return an error for packed arrays") - assert.Equal(t, "bar3", bar3, "third element should be 'bar3'") - _, err = goArray.GetAtIndex(3) - assert.Error(t, err, "GetAtIndex should return an error for out-of-bounds index on packed arrays") + assert.Equal(t, originalArray, convertedArray, "associative array should be equal after conversion") }) } -func TestNestedMixedArray(t *testing.T) { +func TestPHPPackedArray(t *testing.T) { testOnDummyPHPThread(t, func() { - originalArray := NewAssociativeArray( - KeyValuePair{"array", NewAssociativeArray( - KeyValuePair{"foo", "bar"}, - )}, - KeyValuePair{"packedArray", NewPackedArray( - float64(1.1), - int64(2), - "packed", - )}, - KeyValuePair{"integer", int64(1)}, - KeyValuePair{"float", float64(1.2)}, - KeyValuePair{"string", "bar"}, - KeyValuePair{"null", nil}, - KeyValuePair{"false", false}, - KeyValuePair{"true", true}, - ) - - zendArray := PHPArray(originalArray) - goArray := GoArray(zendArray) - - assert.Equal(t, originalArray, goArray, "nested mixed array should be equal after conversion") + originalArray := PackedArray{"bar1", "bar2"} + + convertedArray, err := GoPackedArray(PHPPackedArray(originalArray)) + + assert.NoError(t, err) + assert.Equal(t, originalArray, convertedArray, "packed array should be equal after conversion") }) } -func TestArrayShouldBeCorrectlyPacked(t *testing.T) { - originalArray := NewPackedArray("bar1", "bar2") +func TestPHPPackedArrayToAssociative(t *testing.T) { + testOnDummyPHPThread(t, func() { + originalArray := PackedArray{"bar1", "bar2"} + expectedArray := AssociativeArray{Map: map[string]interface{}{ + "0": "bar1", + "1": "bar2", + }} - assert.True(t, originalArray.IsPacked(), "Array should be packed") + convertedArray, err := GoAssociativeArray(PHPPackedArray(originalArray), false) - err := originalArray.Append("bar3") + assert.NoError(t, err) + assert.Equal(t, expectedArray, convertedArray, "convert a packed to an associative array") + }) +} - assert.NoError(t, err) - assert.Equal(t, 3, len(originalArray.packedValues), "Packed array should have 3 elements after appending") - assert.True(t, originalArray.IsPacked(), "Array should be packed after Append") +func TestPHPAssociativeArrayToPacked(t *testing.T) { + testOnDummyPHPThread(t, func() { + originalArray := AssociativeArray{ + Map: map[string]interface{}{ + "foo1": "bar1", + "foo2": "bar2", + }, + Order: []string{"foo1", "foo2"}, + } + expectedArray := PackedArray{"bar1", "bar2"} - err = originalArray.SetAtIndex(1, "newBar2") + convertedArray, err := GoPackedArray(PHPAssociativeArray(originalArray)) - assert.NoError(t, err, "SetAtIndex should not return an error for packed arrays") - assert.Equal(t, "newBar2", originalArray.Values()[1], "Second element should be updated to 'newBar2'") - assert.True(t, originalArray.IsPacked(), "Array should be packed after SetAtIndex") + assert.NoError(t, err) + assert.Equal(t, expectedArray, convertedArray, "convert an associative to a packed array") + }) +} - originalArray.Set("hello", "world") - assert.False(t, originalArray.IsPacked(), "Array should not be packed anymore after setting a string offset") +func TestNestedMixedArray(t *testing.T) { + testOnDummyPHPThread(t, func() { + originalArray := AssociativeArray{ + Map: map[string]interface{}{ + "foo": "bar", + "int": int64(123), + "float": float64(1.2), + "true": true, + "false": false, + "nil": nil, + "packedArray": PackedArray{"bar1", "bar2"}, + "associativeArray": AssociativeArray{ + Map: map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}, + Order: []string{"foo2", "foo1"}, + }, + }, + } + + convertedArray, err := GoAssociativeArray(PHPAssociativeArray(originalArray), false) - assert.Equal(t, NewAssociativeArray( - KeyValuePair{"0", "bar1"}, - KeyValuePair{"1", "newBar2"}, - KeyValuePair{"2", "bar3"}, - KeyValuePair{"hello", "world"}, - ), originalArray, "Array should be associated after mixed operations") + assert.NoError(t, err) + assert.Equal(t, originalArray, convertedArray, "nested mixed array should be equal after conversion") + }) } From 08ed97c1a2977983da5f56f07e3563fbcfea7176 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Wed, 13 Aug 2025 14:25:51 +0200 Subject: [PATCH 06/18] Removes pointer alias --- types.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/types.go b/types.go index d81a809389..48f5d63600 100644 --- a/types.go +++ b/types.go @@ -47,8 +47,6 @@ type AssociativeArray struct { // PackedArray represents a 'packed' PHP array as an indexed list type PackedArray = []interface{} -type ZendArray = unsafe.Pointer - // EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray func GoAssociativeArray(arr unsafe.Pointer, ordered bool) (AssociativeArray, error) { if arr == nil { @@ -152,7 +150,7 @@ func GoPackedArray(arr unsafe.Pointer) (PackedArray, error) { } // PHPAssociativeArray converts a Go AssociativeArray to a PHP zend_array. -func PHPAssociativeArray(arr AssociativeArray) ZendArray { +func PHPAssociativeArray(arr AssociativeArray) unsafe.Pointer { var zendArray *C.HashTable if len(arr.Order) != 0 { @@ -179,7 +177,7 @@ func PHPAssociativeArray(arr AssociativeArray) ZendArray { } // PHPPackedArray converts a Go PHPPackedArray to a PHP zend_array. -func PHPPackedArray(arr PackedArray) ZendArray { +func PHPPackedArray(arr PackedArray) unsafe.Pointer { zendArray := createNewArray((uint32)(len(arr))) for _, val := range arr { zval := convertGoToZval(val) From 0fff364c3bbdd1ca26aaddc9bac8b6a06f9996b0 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Wed, 13 Aug 2025 23:13:52 +0200 Subject: [PATCH 07/18] linting --- docs/extensions.md | 4 ++-- types.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index ec32c567b0..1e91505983 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -103,7 +103,7 @@ If you refer to the code snippet of the previous section, you can see that helpe #### Working with Arrays -FrankenPHP provides native support for PHP arrays through the `frankenphp.AssociativeArray` array amd `frankenphp.PackedArray` types. +FrankenPHP provides native support for PHP arrays through the `frankenphp.AssociativeArray` array amd `frankenphp.PackedArray` types. `AssociativeArray` represents an associative hashmap and is composed of a `Map: map[string]interface{}` and an optional order `[]string`. @@ -177,7 +177,7 @@ func process_data_packed(arr *C.zval) unsafe.Pointer { ##### Available methods - packed and associative -* `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to a PHP array with key value pairs, optionally pass a specific order +* `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to a PHP array with key-value pairs, optionally pass a specific order * `frankenphp.PHPPackedArray(arr frankenphp.PackedArray) unsafe.Pointer` - Convert to a PHP packed array with indexed values only * `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) (frankenphp.AssociativeArray, error)` - Convert a PHP array to a Go map and optionally keep the order * `frankenphp.GoPackedArray(arr unsafe.Pointer) (frankenphp.PackedArray, error)` - Convert a PHP array to a go slice diff --git a/types.go b/types.go index 48f5d63600..598ee69565 100644 --- a/types.go +++ b/types.go @@ -156,7 +156,7 @@ func PHPAssociativeArray(arr AssociativeArray) unsafe.Pointer { if len(arr.Order) != 0 { zendArray = createNewArray((uint32)(len(arr.Order))) for _, key := range arr.Order { - val, _ := arr.Map[key] + val := arr.Map[key] zval := convertGoToZval(val) keyStr := PHPString(key, false) C.zend_hash_update(zendArray, (*C.zend_string)(keyStr), zval) From d720e2fcc9c1a02e0f7edf99047539d92368e56f Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Wed, 13 Aug 2025 23:22:06 +0200 Subject: [PATCH 08/18] Optimizes hash update. --- types.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/types.go b/types.go index 598ee69565..4e091dd7eb 100644 --- a/types.go +++ b/types.go @@ -158,15 +158,13 @@ func PHPAssociativeArray(arr AssociativeArray) unsafe.Pointer { for _, key := range arr.Order { val := arr.Map[key] zval := convertGoToZval(val) - keyStr := PHPString(key, false) - C.zend_hash_update(zendArray, (*C.zend_string)(keyStr), zval) + C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval) } } else { zendArray = createNewArray((uint32)(len(arr.Map))) for key, val := range arr.Map { zval := convertGoToZval(val) - keyStr := PHPString(key, false) - C.zend_hash_update(zendArray, (*C.zend_string)(keyStr), zval) + C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval) } } From d79c7706b8ce157b2b7f0fb693fa17ae7e9e8828 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Wed, 13 Aug 2025 23:39:27 +0200 Subject: [PATCH 09/18] Fixes extgen tests. --- docs/extensions.md | 6 ++++-- internal/extgen/gofile_test.go | 38 ++++++++++++++-------------------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index 1e91505983..60bbc19306 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -103,9 +103,10 @@ If you refer to the code snippet of the previous section, you can see that helpe #### Working with Arrays -FrankenPHP provides native support for PHP arrays through the `frankenphp.AssociativeArray` array amd `frankenphp.PackedArray` types. +FrankenPHP provides native support for PHP arrays through `frankenphp.AssociativeArray` and `frankenphp.PackedArray`. -`AssociativeArray` represents an associative hashmap and is composed of a `Map: map[string]interface{}` and an optional order `[]string`. +`AssociativeArray` represents an associative hashmap and is composed of a `Map: map[string]interface{}` and an optional `Order: []string`. +For unordered arrays, `Order` will be left empty. `PackedArray` is just an alias for a go slice `[]interface{}` @@ -124,6 +125,7 @@ func process_data_ordered(arr *C.zval) unsafe.Pointer { } // return an ordered array + // if 'Order' is not empty, only the key-value paris in 'Order' will be respected return frankenphp.PHPAssociativeArray(AssociativeArray{ Map: map[string]interface{}{ "key1": "value1", diff --git a/internal/extgen/gofile_test.go b/internal/extgen/gofile_test.go index 6cbb3afadd..ce7fe2c52e 100644 --- a/internal/extgen/gofile_test.go +++ b/internal/extgen/gofile_test.go @@ -514,17 +514,17 @@ type ArrayStruct struct { } //export_php:method ArrayClass::processArray(array $items): array -func (as *ArrayStruct) ProcessArray(items *frankenphp.Array) *frankenphp.Array { - result := &frankenphp.Array{} - for i, entry := range items.Entries() { - result.Set(fmt.Sprintf("processed_%d", i), value) +func (as *ArrayStruct) ProcessArray(items frankenphp.AssociativeArray) frankenphp.AssociativeArray { + result := frankenphp.AssociativeArray{} + for key, value := range items.Map { + result.Set("processed_"+key, 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{} +func (as *ArrayStruct) FilterData(data frankenphp.AssociativeArray, filter string) frankenphp.AssociativeArray { + result := frankenphp.AssociativeArray{} // Filter logic here return result }` @@ -542,10 +542,10 @@ func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *franke Params: []phpParameter{ {Name: "items", PhpType: phpArray, IsNullable: false}, }, - GoFunction: `func (as *ArrayStruct) ProcessArray(items *frankenphp.Array) *frankenphp.Array { - result := &frankenphp.Array{} - for i, entry := range items.Entries() { - result.Set(fmt.Sprintf("processed_%d", i), value) + GoFunction: `func (as *ArrayStruct) ProcessArray(items frankenphp.AssociativeArray) frankenphp.AssociativeArray { + result := frankenphp.AssociativeArray{} + for key, value := range items.Entries() { + result.Set("processed_"+key, value) } return result }`, @@ -560,8 +560,8 @@ func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *franke {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{} + GoFunction: `func (as *ArrayStruct) FilterData(data frankenphp.AssociativeArray, filter string) frankenphp.AssociativeArray { + result := frankenphp.AssociativeArray{} return result }`, }, @@ -611,11 +611,8 @@ func TestGoFileGenerator_MethodWrapperWithNullableArrayParams(t *testing.T) { 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", len(items.Entries()), name) +func (nas *NullableArrayStruct) ProcessOptionalArray(items frankenphp.AssociativeArray, name string) string { + return fmt.Sprintf("Processing %d items for %s", len(items.Map), name) }` sourceFile := filepath.Join(tmpDir, "test.go") @@ -632,11 +629,8 @@ func (nas *NullableArrayStruct) ProcessOptionalArray(items *frankenphp.Array, na {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", len(items.Entries()), name) + GoFunction: `func (nas *NullableArrayStruct) ProcessOptionalArray(items frankenphp.AssociativeArray, name string) string { + return fmt.Sprintf("Processing %d items for %s", len(items.Map), name) }`, }, } From e1c0dc20cc6a2ac376f4f32c2f46b6814bc12337 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Thu, 14 Aug 2025 20:50:34 +0200 Subject: [PATCH 10/18] Moves file to tests. --- threadtasks.go => threadtasks_test.go | 1 - 1 file changed, 1 deletion(-) rename threadtasks.go => threadtasks_test.go (98%) diff --git a/threadtasks.go b/threadtasks_test.go similarity index 98% rename from threadtasks.go rename to threadtasks_test.go index 0a392633aa..d81c555350 100644 --- a/threadtasks.go +++ b/threadtasks_test.go @@ -4,7 +4,6 @@ import ( "sync" ) -// EXPERIMENTAL (only used in tests) // representation of a thread that handles tasks directly assigned by go // implements the threadHandler interface type taskThread struct { From 2288f490cfbf8b6bc689ddc597ec2e55ebfd9dfb Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Thu, 14 Aug 2025 20:50:54 +0200 Subject: [PATCH 11/18] Fixes suggested by @dunglas. --- types.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/types.go b/types.go index 4e091dd7eb..c16fccdc34 100644 --- a/types.go +++ b/types.go @@ -44,7 +44,7 @@ type AssociativeArray struct { Order []string } -// PackedArray represents a 'packed' PHP array as an indexed list +// PackedArray represents a 'packed' PHP array as a Go slice type PackedArray = []interface{} // EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray @@ -68,6 +68,8 @@ func GoAssociativeArray(arr unsafe.Pointer, ordered bool) (AssociativeArray, err if htIsPacked(hashTable) { // if the HashTable is packed, convert all integer keys to strings + // this is probably a bug by the dev using this function + // still, we'll (inefficiently) convert to an associative array for i := C.uint32_t(0); i < nNumUsed; i++ { v := C.get_ht_packed_data(hashTable, i) if v != nil && C.zval_get_type(v) != C.IS_UNDEF { From d4c264d44a40d0cae6c863697a980f2376fb733a Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Thu, 14 Aug 2025 20:52:48 +0200 Subject: [PATCH 12/18] Replaces 'interface{}' with 'any'. --- docs/extensions.md | 8 ++++---- types.go | 16 ++++++++-------- types_test.go | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index 60bbc19306..eefa4b4578 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -105,10 +105,10 @@ If you refer to the code snippet of the previous section, you can see that helpe FrankenPHP provides native support for PHP arrays through `frankenphp.AssociativeArray` and `frankenphp.PackedArray`. -`AssociativeArray` represents an associative hashmap and is composed of a `Map: map[string]interface{}` and an optional `Order: []string`. +`AssociativeArray` represents an associative hashmap and is composed of a `Map: map[string]any` and an optional `Order: []string`. For unordered arrays, `Order` will be left empty. -`PackedArray` is just an alias for a go slice `[]interface{}` +`PackedArray` is just an alias for a go slice `[]any` **Creating and manipulating arrays in Go:** @@ -127,7 +127,7 @@ func process_data_ordered(arr *C.zval) unsafe.Pointer { // return an ordered array // if 'Order' is not empty, only the key-value paris in 'Order' will be respected return frankenphp.PHPAssociativeArray(AssociativeArray{ - Map: map[string]interface{}{ + Map: map[string]any{ "key1": "value1", "key2": "value2", }, @@ -148,7 +148,7 @@ func process_data_unordered(arr *C.zval) unsafe.Pointer { } // return an unordered array - return frankenphp.PHPAssociativeArray(AssociativeArray{Map: map[string]interface{}{ + return frankenphp.PHPAssociativeArray(AssociativeArray{Map: map[string]any{ "key1": "value1", "key2": "value2", }}) diff --git a/types.go b/types.go index c16fccdc34..59778e9a7f 100644 --- a/types.go +++ b/types.go @@ -40,12 +40,12 @@ func PHPString(s string, persistent bool) unsafe.Pointer { // AssociativeArray represents a PHP array with ordered key-value pairs type AssociativeArray struct { - Map map[string]interface{} + Map map[string]any Order []string } // PackedArray represents a 'packed' PHP array as a Go slice -type PackedArray = []interface{} +type PackedArray = []any // EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray func GoAssociativeArray(arr unsafe.Pointer, ordered bool) (AssociativeArray, error) { @@ -61,7 +61,7 @@ func GoAssociativeArray(arr unsafe.Pointer, ordered bool) (AssociativeArray, err } nNumUsed := hashTable.nNumUsed - result := AssociativeArray{Map: make(map[string]interface{})} + result := AssociativeArray{Map: make(map[string]any)} if ordered { result.Order = make([]string, 0, nNumUsed) } @@ -190,8 +190,8 @@ func PHPPackedArray(arr PackedArray) unsafe.Pointer { return unsafe.Pointer(&zval) } -// convertZvalToGo converts a PHP zval to a Go interface{} -func convertZvalToGo(zval *C.zval) interface{} { +// convertZvalToGo converts a PHP zval to a Go any +func convertZvalToGo(zval *C.zval) any { t := C.zval_get_type(zval) switch t { case C.IS_NULL: @@ -234,8 +234,8 @@ func convertZvalToGo(zval *C.zval) interface{} { } } -// convertGoToZval converts a Go interface{} to a PHP zval -func convertGoToZval(value interface{}) *C.zval { +// convertGoToZval converts a Go any to a PHP zval +func convertGoToZval(value any) *C.zval { var zval C.zval switch v := value.(type) { @@ -256,7 +256,7 @@ func convertGoToZval(value interface{}) *C.zval { return (*C.zval)(PHPPackedArray(v)) case AssociativeArray: return (*C.zval)(PHPAssociativeArray(v)) - case map[string]interface{}: + case map[string]any: return (*C.zval)(PHPAssociativeArray(AssociativeArray{Map: v})) default: C.__zval_null__(&zval) diff --git a/types_test.go b/types_test.go index f4ea6e31cf..fb7968b421 100644 --- a/types_test.go +++ b/types_test.go @@ -36,7 +36,7 @@ func TestGoString(t *testing.T) { func TestPHPAssociativeArray(t *testing.T) { testOnDummyPHPThread(t, func() { - originalArray := AssociativeArray{Map: map[string]interface{}{ + originalArray := AssociativeArray{Map: map[string]any{ "foo1": "bar1", "foo2": "bar2", }} @@ -51,7 +51,7 @@ func TestPHPAssociativeArray(t *testing.T) { func TestOrderedPHPAssociativeArray(t *testing.T) { testOnDummyPHPThread(t, func() { originalArray := AssociativeArray{ - Map: map[string]interface{}{ + Map: map[string]any{ "foo1": "bar1", "foo2": "bar2", }, @@ -79,7 +79,7 @@ func TestPHPPackedArray(t *testing.T) { func TestPHPPackedArrayToAssociative(t *testing.T) { testOnDummyPHPThread(t, func() { originalArray := PackedArray{"bar1", "bar2"} - expectedArray := AssociativeArray{Map: map[string]interface{}{ + expectedArray := AssociativeArray{Map: map[string]any{ "0": "bar1", "1": "bar2", }} @@ -94,7 +94,7 @@ func TestPHPPackedArrayToAssociative(t *testing.T) { func TestPHPAssociativeArrayToPacked(t *testing.T) { testOnDummyPHPThread(t, func() { originalArray := AssociativeArray{ - Map: map[string]interface{}{ + Map: map[string]any{ "foo1": "bar1", "foo2": "bar2", }, @@ -112,7 +112,7 @@ func TestPHPAssociativeArrayToPacked(t *testing.T) { func TestNestedMixedArray(t *testing.T) { testOnDummyPHPThread(t, func() { originalArray := AssociativeArray{ - Map: map[string]interface{}{ + Map: map[string]any{ "foo": "bar", "int": int64(123), "float": float64(1.2), @@ -121,7 +121,7 @@ func TestNestedMixedArray(t *testing.T) { "nil": nil, "packedArray": PackedArray{"bar1", "bar2"}, "associativeArray": AssociativeArray{ - Map: map[string]interface{}{"foo1": "bar1", "foo2": "bar2"}, + Map: map[string]any{"foo1": "bar1", "foo2": "bar2"}, Order: []string{"foo2", "foo1"}, }, }, From 89f5a06c18642e37559659b8a1329a8d59a8f2d2 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Fri, 15 Aug 2025 22:36:58 +0200 Subject: [PATCH 13/18] Panics on wrong zval. --- docs/extensions.md | 4 ++-- types.go | 28 ++++++++++++---------------- types_test.go | 18 ++++++------------ 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index eefa4b4578..0cdc014965 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -181,8 +181,8 @@ func process_data_packed(arr *C.zval) unsafe.Pointer { * `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to a PHP array with key-value pairs, optionally pass a specific order * `frankenphp.PHPPackedArray(arr frankenphp.PackedArray) unsafe.Pointer` - Convert to a PHP packed array with indexed values only -* `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) (frankenphp.AssociativeArray, error)` - Convert a PHP array to a Go map and optionally keep the order -* `frankenphp.GoPackedArray(arr unsafe.Pointer) (frankenphp.PackedArray, error)` - Convert a PHP array to a go slice +* `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to a Go map and optionally keep the order +* `frankenphp.GoPackedArray(arr unsafe.Pointer) frankenphp.PackedArray` - Convert a PHP array to a go slice ### Declaring a Native PHP Class diff --git a/types.go b/types.go index 59778e9a7f..54b09f153d 100644 --- a/types.go +++ b/types.go @@ -5,7 +5,6 @@ package frankenphp */ import "C" import ( - "errors" "strconv" "unsafe" ) @@ -48,16 +47,16 @@ type AssociativeArray struct { type PackedArray = []any // EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray -func GoAssociativeArray(arr unsafe.Pointer, ordered bool) (AssociativeArray, error) { +func GoAssociativeArray(arr unsafe.Pointer, ordered bool) AssociativeArray { if arr == nil { - return AssociativeArray{}, errors.New("GoAssociativeArray received a nil pointer") + panic("GoAssociativeArray received a nil pointer") } zval := (*C.zval)(arr) hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY)) if hashTable == nil { - return AssociativeArray{}, errors.New("GoAssociativeArray received a zval that was not a HashTable") + panic("GoAssociativeArray received a *zval that wasn't a HashTable") } nNumUsed := hashTable.nNumUsed @@ -81,7 +80,7 @@ func GoAssociativeArray(arr unsafe.Pointer, ordered bool) (AssociativeArray, err } } - return result, nil + return result } for i := C.uint32_t(0); i < nNumUsed; i++ { @@ -110,20 +109,20 @@ func GoAssociativeArray(arr unsafe.Pointer, ordered bool) (AssociativeArray, err } } - return result, nil + return result } // EXPERIMENTAL: GoPackedArray converts a zend_array to a Go slice -func GoPackedArray(arr unsafe.Pointer) (PackedArray, error) { +func GoPackedArray(arr unsafe.Pointer) PackedArray { if arr == nil { - return PackedArray{}, errors.New("GoPackedArray received a nil pointer") + panic("GoPackedArray received a nil pointer") } zval := (*C.zval)(arr) hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY)) if hashTable == nil { - return PackedArray{}, errors.New("GoPackedArray received zval that wasn'T a HashTable") + panic("GoPackedArray received *zval that wasn't a HashTable") } nNumUsed := hashTable.nNumUsed @@ -137,7 +136,7 @@ func GoPackedArray(arr unsafe.Pointer) (PackedArray, error) { } } - return result, nil + return result } // fallback if ht isn't packed - equivalent to array_values() @@ -148,7 +147,7 @@ func GoPackedArray(arr unsafe.Pointer) (PackedArray, error) { } } - return result, nil + return result } // PHPAssociativeArray converts a Go AssociativeArray to a PHP zend_array. @@ -222,13 +221,10 @@ func convertZvalToGo(zval *C.zval) any { case C.IS_ARRAY: hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY)) if hashTable != nil && htIsPacked(hashTable) { - packedArray, _ := GoPackedArray(unsafe.Pointer(zval)) - - return packedArray + return GoPackedArray(unsafe.Pointer(zval)) } - associativeArray, _ := GoAssociativeArray(unsafe.Pointer(zval), true) - return associativeArray + return GoAssociativeArray(unsafe.Pointer(zval), true) default: return nil } diff --git a/types_test.go b/types_test.go index fb7968b421..08bc10810e 100644 --- a/types_test.go +++ b/types_test.go @@ -41,9 +41,8 @@ func TestPHPAssociativeArray(t *testing.T) { "foo2": "bar2", }} - convertedArray, err := GoAssociativeArray(PHPAssociativeArray(originalArray), false) + convertedArray := GoAssociativeArray(PHPAssociativeArray(originalArray), false) - assert.NoError(t, err) assert.Equal(t, originalArray, convertedArray, "associative array should be equal after conversion") }) } @@ -58,9 +57,8 @@ func TestOrderedPHPAssociativeArray(t *testing.T) { Order: []string{"foo2", "foo1"}, } - convertedArray, err := GoAssociativeArray(PHPAssociativeArray(originalArray), true) + convertedArray := GoAssociativeArray(PHPAssociativeArray(originalArray), true) - assert.NoError(t, err) assert.Equal(t, originalArray, convertedArray, "associative array should be equal after conversion") }) } @@ -69,9 +67,8 @@ func TestPHPPackedArray(t *testing.T) { testOnDummyPHPThread(t, func() { originalArray := PackedArray{"bar1", "bar2"} - convertedArray, err := GoPackedArray(PHPPackedArray(originalArray)) + convertedArray := GoPackedArray(PHPPackedArray(originalArray)) - assert.NoError(t, err) assert.Equal(t, originalArray, convertedArray, "packed array should be equal after conversion") }) } @@ -84,9 +81,8 @@ func TestPHPPackedArrayToAssociative(t *testing.T) { "1": "bar2", }} - convertedArray, err := GoAssociativeArray(PHPPackedArray(originalArray), false) + convertedArray := GoAssociativeArray(PHPPackedArray(originalArray), false) - assert.NoError(t, err) assert.Equal(t, expectedArray, convertedArray, "convert a packed to an associative array") }) } @@ -102,9 +98,8 @@ func TestPHPAssociativeArrayToPacked(t *testing.T) { } expectedArray := PackedArray{"bar1", "bar2"} - convertedArray, err := GoPackedArray(PHPAssociativeArray(originalArray)) + convertedArray := GoPackedArray(PHPAssociativeArray(originalArray)) - assert.NoError(t, err) assert.Equal(t, expectedArray, convertedArray, "convert an associative to a packed array") }) } @@ -127,9 +122,8 @@ func TestNestedMixedArray(t *testing.T) { }, } - convertedArray, err := GoAssociativeArray(PHPAssociativeArray(originalArray), false) + convertedArray := GoAssociativeArray(PHPAssociativeArray(originalArray), false) - assert.NoError(t, err) assert.Equal(t, originalArray, convertedArray, "nested mixed array should be equal after conversion") }) } From 5ac0f1786ef6281413a0181c1f2dda1b0383f6e8 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sun, 17 Aug 2025 22:46:47 +0200 Subject: [PATCH 14/18] interface improvements as suggested by @dunglas. --- docs/extensions.md | 24 ++++++++--------- types.go | 65 ++++++++++++++++++++++++++++++---------------- types_test.go | 44 +++++++++++++++---------------- 3 files changed, 76 insertions(+), 57 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index 0cdc014965..a1d387ed09 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -105,10 +105,9 @@ If you refer to the code snippet of the previous section, you can see that helpe FrankenPHP provides native support for PHP arrays through `frankenphp.AssociativeArray` and `frankenphp.PackedArray`. -`AssociativeArray` represents an associative hashmap and is composed of a `Map: map[string]any` and an optional `Order: []string`. -For unordered arrays, `Order` will be left empty. +`AssociativeArray` represents a [hash map](https://en.wikipedia.org/wiki/Hash_table) composed of a `Map: map[string]any`field and an optional `Order: []string` field (unlike PHP "associative arrays", Go maps aren't ordered). -`PackedArray` is just an alias for a go slice `[]any` +PackedArray` is an alias for a Go slice `[]any` **Creating and manipulating arrays in Go:** @@ -116,7 +115,7 @@ For unordered arrays, `Order` will be left empty. // export_php:function process_data_ordered(array $input): array func process_data_ordered(arr *C.zval) unsafe.Pointer { // Convert PHP associative array to Go while keeping the order - associativeArray := frankenphp.GoAssociativeArray(unsafe.Pointer(arr), true) + associativeArray := frankenphp.GoAssociativeArray(unsafe.Pointer(arr)) // loop over the entries in order for _, key := range associativeArray.Order { @@ -136,22 +135,21 @@ func process_data_ordered(arr *C.zval) unsafe.Pointer { } // export_php:function process_data_unordered(array $input): array -func process_data_unordered(arr *C.zval) unsafe.Pointer { - // Convert PHP associative array to Go without keeping the order +func process_data_unordered_map(arr *C.zval) unsafe.Pointer { + // Convert PHP associative array to a Go map without keeping the order // ignoring the order will be more performant - associativeArray := frankenphp.GoAssociativeArray(unsafe.Pointer(arr), false) + goMap := frankenphp.GoMap(unsafe.Pointer(arr)) // loop over the entries in no specific order - for key, value := range associativeArray.Map { - value, _ = associativeArray.Map[key] + for key, value := range goMap { // do something with key and value } // return an unordered array - return frankenphp.PHPAssociativeArray(AssociativeArray{Map: map[string]any{ + return frankenphp.PHPMap(map[string]any{ "key1": "value1", "key2": "value2", - }}) + }) } // export_php:function process_data_packed(array $input): array @@ -179,9 +177,11 @@ func process_data_packed(arr *C.zval) unsafe.Pointer { ##### Available methods - packed and associative -* `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to a PHP array with key-value pairs, optionally pass a specific order +* `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to an ordered PHP array with key-value pairs +* `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convert to an unordered PHP array with key-value pairs * `frankenphp.PHPPackedArray(arr frankenphp.PackedArray) unsafe.Pointer` - Convert to a PHP packed array with indexed values only * `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to a Go map and optionally keep the order +* `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered go map * `frankenphp.GoPackedArray(arr unsafe.Pointer) frankenphp.PackedArray` - Convert a PHP array to a go slice ### Declaring a Native PHP Class diff --git a/types.go b/types.go index 54b09f153d..4753ba7373 100644 --- a/types.go +++ b/types.go @@ -47,22 +47,34 @@ type AssociativeArray struct { type PackedArray = []any // EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray -func GoAssociativeArray(arr unsafe.Pointer, ordered bool) AssociativeArray { +func GoAssociativeArray(arr unsafe.Pointer) AssociativeArray { + entries, order := goArray(arr, true) + return AssociativeArray{entries, order} +} + +// EXPERIMENTAL: GoMap converts a zend_array to an unordered Go map +func GoMap(arr unsafe.Pointer) map[string]any { + entries, _ := goArray(arr, false) + return entries +} + +func goArray(arr unsafe.Pointer, ordered bool) (map[string]any, []string) { if arr == nil { - panic("GoAssociativeArray received a nil pointer") + panic("received a nil pointer on array conversion") } zval := (*C.zval)(arr) hashTable := (*C.HashTable)(castZval(zval, C.IS_ARRAY)) if hashTable == nil { - panic("GoAssociativeArray received a *zval that wasn't a HashTable") + panic("received a *zval that wasn't a HashTable on array conversion") } nNumUsed := hashTable.nNumUsed - result := AssociativeArray{Map: make(map[string]any)} + entries := make(map[string]any) + var order []string if ordered { - result.Order = make([]string, 0, nNumUsed) + order = make([]string, 0, nNumUsed) } if htIsPacked(hashTable) { @@ -73,14 +85,14 @@ func GoAssociativeArray(arr unsafe.Pointer, ordered bool) AssociativeArray { v := C.get_ht_packed_data(hashTable, i) if v != nil && C.zval_get_type(v) != C.IS_UNDEF { strIndex := strconv.Itoa(int(i)) - result.Map[strIndex] = convertZvalToGo(v) + entries[strIndex] = convertZvalToGo(v) if ordered { - result.Order = append(result.Order, strIndex) + order = append(order, strIndex) } } } - return result + return entries, order } for i := C.uint32_t(0); i < nNumUsed; i++ { @@ -93,9 +105,9 @@ func GoAssociativeArray(arr unsafe.Pointer, ordered bool) AssociativeArray { if bucket.key != nil { keyStr := GoString(unsafe.Pointer(bucket.key)) - result.Map[keyStr] = v + entries[keyStr] = v if ordered { - result.Order = append(result.Order, keyStr) + order = append(order, keyStr) } continue @@ -103,13 +115,13 @@ func GoAssociativeArray(arr unsafe.Pointer, ordered bool) AssociativeArray { // as fallback convert the bucket index to a string key strIndex := strconv.Itoa(int(bucket.h)) - result.Map[strIndex] = v + entries[strIndex] = v if ordered { - result.Order = append(result.Order, strIndex) + order = append(order, strIndex) } } - return result + return entries, order } // EXPERIMENTAL: GoPackedArray converts a zend_array to a Go slice @@ -150,20 +162,29 @@ func GoPackedArray(arr unsafe.Pointer) PackedArray { return result } -// PHPAssociativeArray converts a Go AssociativeArray to a PHP zend_array. +// EXPERIMENTAL: PHPMap converts an unordered Go map to a PHP zend_array. +func PHPMap(arr map[string]any) unsafe.Pointer { + return phpArray(arr, nil) +} + +// EXPERIMENTAL: PHPAssociativeArray converts a Go AssociativeArray to a PHP zend_array. func PHPAssociativeArray(arr AssociativeArray) unsafe.Pointer { + return phpArray(arr.Map, arr.Order) +} + +func phpArray(entries map[string]any, order []string) unsafe.Pointer { var zendArray *C.HashTable - if len(arr.Order) != 0 { - zendArray = createNewArray((uint32)(len(arr.Order))) - for _, key := range arr.Order { - val := arr.Map[key] + if len(order) != 0 { + zendArray = createNewArray((uint32)(len(order))) + for _, key := range order { + val := entries[key] zval := convertGoToZval(val) C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval) } } else { - zendArray = createNewArray((uint32)(len(arr.Map))) - for key, val := range arr.Map { + zendArray = createNewArray((uint32)(len(entries))) + for key, val := range entries { zval := convertGoToZval(val) C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval) } @@ -175,7 +196,7 @@ func PHPAssociativeArray(arr AssociativeArray) unsafe.Pointer { return unsafe.Pointer(&zval) } -// PHPPackedArray converts a Go PHPPackedArray to a PHP zend_array. +// EXPERIMENTAL: PHPPackedArray converts a Go slice to a PHP zend_array. func PHPPackedArray(arr PackedArray) unsafe.Pointer { zendArray := createNewArray((uint32)(len(arr))) for _, val := range arr { @@ -224,7 +245,7 @@ func convertZvalToGo(zval *C.zval) any { return GoPackedArray(unsafe.Pointer(zval)) } - return GoAssociativeArray(unsafe.Pointer(zval), true) + return GoAssociativeArray(unsafe.Pointer(zval)) default: return nil } diff --git a/types_test.go b/types_test.go index 08bc10810e..80e588ac24 100644 --- a/types_test.go +++ b/types_test.go @@ -34,14 +34,14 @@ func TestGoString(t *testing.T) { }) } -func TestPHPAssociativeArray(t *testing.T) { +func TestPHPMap(t *testing.T) { testOnDummyPHPThread(t, func() { - originalArray := AssociativeArray{Map: map[string]any{ + originalArray := map[string]any{ "foo1": "bar1", "foo2": "bar2", - }} + } - convertedArray := GoAssociativeArray(PHPAssociativeArray(originalArray), false) + convertedArray := GoMap(PHPMap(originalArray)) assert.Equal(t, originalArray, convertedArray, "associative array should be equal after conversion") }) @@ -57,7 +57,7 @@ func TestOrderedPHPAssociativeArray(t *testing.T) { Order: []string{"foo2", "foo1"}, } - convertedArray := GoAssociativeArray(PHPAssociativeArray(originalArray), true) + convertedArray := GoAssociativeArray(PHPAssociativeArray(originalArray)) assert.Equal(t, originalArray, convertedArray, "associative array should be equal after conversion") }) @@ -73,15 +73,15 @@ func TestPHPPackedArray(t *testing.T) { }) } -func TestPHPPackedArrayToAssociative(t *testing.T) { +func TestPHPPackedArrayToGoMap(t *testing.T) { testOnDummyPHPThread(t, func() { originalArray := PackedArray{"bar1", "bar2"} - expectedArray := AssociativeArray{Map: map[string]any{ + expectedArray := map[string]any{ "0": "bar1", "1": "bar2", - }} + } - convertedArray := GoAssociativeArray(PHPPackedArray(originalArray), false) + convertedArray := GoMap(PHPPackedArray(originalArray)) assert.Equal(t, expectedArray, convertedArray, "convert a packed to an associative array") }) @@ -106,23 +106,21 @@ func TestPHPAssociativeArrayToPacked(t *testing.T) { func TestNestedMixedArray(t *testing.T) { testOnDummyPHPThread(t, func() { - originalArray := AssociativeArray{ - Map: map[string]any{ - "foo": "bar", - "int": int64(123), - "float": float64(1.2), - "true": true, - "false": false, - "nil": nil, - "packedArray": PackedArray{"bar1", "bar2"}, - "associativeArray": AssociativeArray{ - Map: map[string]any{"foo1": "bar1", "foo2": "bar2"}, - Order: []string{"foo2", "foo1"}, - }, + originalArray := map[string]any{ + "string": "value", + "int": int64(123), + "float": float64(1.2), + "true": true, + "false": false, + "nil": nil, + "packedArray": PackedArray{"bar1", "bar2"}, + "associativeArray": AssociativeArray{ + Map: map[string]any{"foo1": "bar1", "foo2": "bar2"}, + Order: []string{"foo2", "foo1"}, }, } - convertedArray := GoAssociativeArray(PHPAssociativeArray(originalArray), false) + convertedArray := GoMap(PHPMap(originalArray)) assert.Equal(t, originalArray, convertedArray, "nested mixed array should be equal after conversion") }) From dc3fe3b79e0130364db8420fc8e1dab471586d8e Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sun, 17 Aug 2025 22:48:33 +0200 Subject: [PATCH 15/18] Adjusts docs. --- docs/extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensions.md b/docs/extensions.md index a1d387ed09..91eb8ee3db 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -180,7 +180,7 @@ func process_data_packed(arr *C.zval) unsafe.Pointer { * `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to an ordered PHP array with key-value pairs * `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convert to an unordered PHP array with key-value pairs * `frankenphp.PHPPackedArray(arr frankenphp.PackedArray) unsafe.Pointer` - Convert to a PHP packed array with indexed values only -* `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to a Go map and optionally keep the order +* `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to an ordered Go AssociativeArray (map with order) * `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered go map * `frankenphp.GoPackedArray(arr unsafe.Pointer) frankenphp.PackedArray` - Convert a PHP array to a go slice From a43e226999eac8a1c23721d4679718507b3cddeb Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sun, 17 Aug 2025 22:50:39 +0200 Subject: [PATCH 16/18] Adjusts docs. --- docs/extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensions.md b/docs/extensions.md index 91eb8ee3db..4b2fc26575 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -175,7 +175,7 @@ func process_data_packed(arr *C.zval) unsafe.Pointer { * **Nested Arrays** - Arrays can be nested and will convert all support types automatically (int64,float64,string,bool,nil,AssociativeArray,PackedArray) * **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 - packed and associative +##### Available methods: Packed and Associative * `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to an ordered PHP array with key-value pairs * `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convert to an unordered PHP array with key-value pairs From 7c297c684e37da5a5fc78a89bba99fa46166a773 Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Mon, 18 Aug 2025 21:11:37 +0200 Subject: [PATCH 17/18] Removes PackedArray alias and adjusts docs. --- docs/extensions.md | 23 +++++++++++------------ types.go | 17 +++++++---------- types_test.go | 28 ++++++++++++++-------------- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index 4b2fc26575..27f126f93f 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -91,7 +91,6 @@ While some variable types have the same memory representation between C/PHP and | `?bool` | `*bool` | ✅ | - | - | ✅ | | `string`/`?string` | `*C.zend_string` | ❌ | frankenphp.GoString() | frankenphp.PHPString() | ✅ | | `array` | `*frankenphp.AssociativeArray` | ❌ | frankenphp.GoAssociativeArray() | frankenphp.PHPAssociativeArray() | ✅ | -| `array (packed)` | `*frankenphp.PackedArray` | ❌ | frankenphp.GoPackedArray() | frankenphp.PHPPackedArray() | ✅ | | `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ | > [!NOTE] @@ -103,17 +102,17 @@ If you refer to the code snippet of the previous section, you can see that helpe #### Working with Arrays -FrankenPHP provides native support for PHP arrays through `frankenphp.AssociativeArray` and `frankenphp.PackedArray`. +FrankenPHP provides native support for PHP arrays through `frankenphp.AssociativeArray` or direct conversion to a map or slice. `AssociativeArray` represents a [hash map](https://en.wikipedia.org/wiki/Hash_table) composed of a `Map: map[string]any`field and an optional `Order: []string` field (unlike PHP "associative arrays", Go maps aren't ordered). -PackedArray` is an alias for a Go slice `[]any` +If order or association are not needed, it's also possible to directly convert to a slice `[]any` or unordered map `map[string]any`. **Creating and manipulating arrays in Go:** ```go // export_php:function process_data_ordered(array $input): array -func process_data_ordered(arr *C.zval) unsafe.Pointer { +func process_data_ordered_map(arr *C.zval) unsafe.Pointer { // Convert PHP associative array to Go while keeping the order associativeArray := frankenphp.GoAssociativeArray(unsafe.Pointer(arr)) @@ -155,34 +154,34 @@ func process_data_unordered_map(arr *C.zval) unsafe.Pointer { // export_php:function process_data_packed(array $input): array func process_data_packed(arr *C.zval) unsafe.Pointer { // Convert PHP packed array to Go - packedArray := frankenphp.GoPackedArray(unsafe.Pointer(arr), false) + goSlice := frankenphp.GoPackedArray(unsafe.Pointer(arr), false) // loop over the slice in order - for index, value := range packedArray { + for index, value := range goSlice { // do something with index and value } // return a packed array - return frankenphp.PHPackedArray{"value1", "value2", "value3"} + return frankenphp.PHPackedArray([]any{"value1", "value2", "value3"}) } ``` -**Key features of `frankenphp.PackedArray` and `frankenphp.AssociativeArray`:** +**Key features of array conversion:** * **Ordered key-value pairs** - Option to keep the order of the associative array * **Optimized for multiple cases** - Option to ditch the order for better performance or convert straight to a slice * **Automatic list detection** - When converting to PHP, automatically detects if array should be a packed list or hashmap -* **Nested Arrays** - Arrays can be nested and will convert all support types automatically (int64,float64,string,bool,nil,AssociativeArray,PackedArray) +* **Nested Arrays** - Arrays can be nested and will convert all support types automatically (`int64`,`float64`,`string`,`bool`,`nil`,`AssociativeArray`,`map[string]any`,`[]any`) * **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: Packed and Associative * `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to an ordered PHP array with key-value pairs -* `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convert to an unordered PHP array with key-value pairs -* `frankenphp.PHPPackedArray(arr frankenphp.PackedArray) unsafe.Pointer` - Convert to a PHP packed array with indexed values only +* `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convert a map to an unordered PHP array with key-value pairs +* `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convert a slice to a PHP packed array with indexed values only * `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to an ordered Go AssociativeArray (map with order) * `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered go map -* `frankenphp.GoPackedArray(arr unsafe.Pointer) frankenphp.PackedArray` - Convert a PHP array to a go slice +* `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a go slice ### Declaring a Native PHP Class diff --git a/types.go b/types.go index 4753ba7373..4fe2352a43 100644 --- a/types.go +++ b/types.go @@ -43,9 +43,6 @@ type AssociativeArray struct { Order []string } -// PackedArray represents a 'packed' PHP array as a Go slice -type PackedArray = []any - // EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray func GoAssociativeArray(arr unsafe.Pointer) AssociativeArray { entries, order := goArray(arr, true) @@ -125,7 +122,7 @@ func goArray(arr unsafe.Pointer, ordered bool) (map[string]any, []string) { } // EXPERIMENTAL: GoPackedArray converts a zend_array to a Go slice -func GoPackedArray(arr unsafe.Pointer) PackedArray { +func GoPackedArray(arr unsafe.Pointer) []any { if arr == nil { panic("GoPackedArray received a nil pointer") } @@ -138,7 +135,7 @@ func GoPackedArray(arr unsafe.Pointer) PackedArray { } nNumUsed := hashTable.nNumUsed - result := make(PackedArray, 0, nNumUsed) + result := make([]any, 0, nNumUsed) if htIsPacked(hashTable) { for i := C.uint32_t(0); i < nNumUsed; i++ { @@ -197,9 +194,9 @@ func phpArray(entries map[string]any, order []string) unsafe.Pointer { } // EXPERIMENTAL: PHPPackedArray converts a Go slice to a PHP zend_array. -func PHPPackedArray(arr PackedArray) unsafe.Pointer { - zendArray := createNewArray((uint32)(len(arr))) - for _, val := range arr { +func PHPPackedArray(slice []any) unsafe.Pointer { + zendArray := createNewArray((uint32)(len(slice))) + for _, val := range slice { zval := convertGoToZval(val) C.zend_hash_next_index_insert(zendArray, zval) } @@ -269,12 +266,12 @@ func convertGoToZval(value any) *C.zval { case string: str := (*C.zend_string)(PHPString(v, false)) C.__zval_string__(&zval, str) - case PackedArray: - return (*C.zval)(PHPPackedArray(v)) case AssociativeArray: return (*C.zval)(PHPAssociativeArray(v)) case map[string]any: return (*C.zval)(PHPAssociativeArray(AssociativeArray{Map: v})) + case []any: + return (*C.zval)(PHPPackedArray(v)) default: C.__zval_null__(&zval) } diff --git a/types_test.go b/types_test.go index 80e588ac24..9499301f6a 100644 --- a/types_test.go +++ b/types_test.go @@ -36,14 +36,14 @@ func TestGoString(t *testing.T) { func TestPHPMap(t *testing.T) { testOnDummyPHPThread(t, func() { - originalArray := map[string]any{ + originalMap := map[string]any{ "foo1": "bar1", "foo2": "bar2", } - convertedArray := GoMap(PHPMap(originalArray)) + convertedMap := GoMap(PHPMap(originalMap)) - assert.Equal(t, originalArray, convertedArray, "associative array should be equal after conversion") + assert.Equal(t, originalMap, convertedMap, "associative array should be equal after conversion") }) } @@ -65,25 +65,25 @@ func TestOrderedPHPAssociativeArray(t *testing.T) { func TestPHPPackedArray(t *testing.T) { testOnDummyPHPThread(t, func() { - originalArray := PackedArray{"bar1", "bar2"} + originalSlice := []any{"bar1", "bar2"} - convertedArray := GoPackedArray(PHPPackedArray(originalArray)) + convertedSlice := GoPackedArray(PHPPackedArray(originalSlice)) - assert.Equal(t, originalArray, convertedArray, "packed array should be equal after conversion") + assert.Equal(t, originalSlice, convertedSlice, "slice should be equal after conversion") }) } func TestPHPPackedArrayToGoMap(t *testing.T) { testOnDummyPHPThread(t, func() { - originalArray := PackedArray{"bar1", "bar2"} - expectedArray := map[string]any{ + originalSlice := []any{"bar1", "bar2"} + expectedMap := map[string]any{ "0": "bar1", "1": "bar2", } - convertedArray := GoMap(PHPPackedArray(originalArray)) + convertedMap := GoMap(PHPPackedArray(originalSlice)) - assert.Equal(t, expectedArray, convertedArray, "convert a packed to an associative array") + assert.Equal(t, expectedMap, convertedMap, "convert a packed to an associative array") }) } @@ -96,11 +96,11 @@ func TestPHPAssociativeArrayToPacked(t *testing.T) { }, Order: []string{"foo1", "foo2"}, } - expectedArray := PackedArray{"bar1", "bar2"} + expectedSlice := []any{"bar1", "bar2"} - convertedArray := GoPackedArray(PHPAssociativeArray(originalArray)) + convertedSlice := GoPackedArray(PHPAssociativeArray(originalArray)) - assert.Equal(t, expectedArray, convertedArray, "convert an associative to a packed array") + assert.Equal(t, expectedSlice, convertedSlice, "convert an associative array to a slice") }) } @@ -113,7 +113,7 @@ func TestNestedMixedArray(t *testing.T) { "true": true, "false": false, "nil": nil, - "packedArray": PackedArray{"bar1", "bar2"}, + "packedArray": []any{"bar1", "bar2"}, "associativeArray": AssociativeArray{ Map: map[string]any{"foo1": "bar1", "foo2": "bar2"}, Order: []string{"foo2", "foo1"}, From 86f563f1e34ea0c28888eac045dd3217be7c502f Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Mon, 18 Aug 2025 21:15:38 +0200 Subject: [PATCH 18/18] Updates docs. --- docs/extensions.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index 27f126f93f..c87b627b92 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -81,17 +81,19 @@ 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` | `*frankenphp.AssociativeArray` | ❌ | frankenphp.GoAssociativeArray() | frankenphp.PHPAssociativeArray() | ✅ | -| `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.AssociativeArray` | ❌ | frankenphp.GoAssociativeArray() | frankenphp.PHPAssociativeArray() | ✅ | +| `array` | `map[string]any` | ❌ | frankenphp.GoMap() | frankenphp.PHPMap() | ✅ | +| `array` | `[]any` | ❌ | frankenphp.GoPackedArray() | frankenphp.PHPPackedArray() | ✅ | +| `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.