Skip to content

Commit 93a4bac

Browse files
AlliBalliBabahenderkes
authored andcommitted
feat:(extgen) make Go arrays more consistent with PHP arrays (php#1800)
* Makes go arrays more consistent with PHP arrays. * NewAssociativeArray. * linting * go linting * Exposes all primitive types. * Removes pointer alias * linting * Optimizes hash update. * Fixes extgen tests. * Moves file to tests. * Fixes suggested by @dunglas. * Replaces 'interface{}' with 'any'. * Panics on wrong zval. * interface improvements as suggested by @dunglas. * Adjusts docs. * Adjusts docs. * Removes PackedArray alias and adjusts docs. * Updates docs.
1 parent f4a87fa commit 93a4bac

File tree

6 files changed

+443
-203
lines changed

6 files changed

+443
-203
lines changed

docs/extensions.md

Lines changed: 81 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,19 @@ While the first point speaks for itself, the second may be harder to apprehend.
8181

8282
While some variable types have the same memory representation between C/PHP and Go, some types require more logic to be directly used. This is maybe the hardest part when it comes to writing extensions because it requires understanding internals of the Zend Engine and how variables are stored internally in PHP. This table summarizes what you need to know:
8383

84-
| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support |
85-
|--------------------|---------------------|-------------------|-----------------------|------------------------|-----------------------|
86-
| `int` | `int64` || - | - ||
87-
| `?int` | `*int64` || - | - ||
88-
| `float` | `float64` || - | - ||
89-
| `?float` | `*float64` || - | - ||
90-
| `bool` | `bool` || - | - ||
91-
| `?bool` | `*bool` || - | - ||
92-
| `string`/`?string` | `*C.zend_string` || frankenphp.GoString() | frankenphp.PHPString() ||
93-
| `array` | `*frankenphp.Array` || frankenphp.GoArray() | frankenphp.PHPArray() ||
94-
| `object` | `struct` || _Not yet implemented_ | _Not yet implemented_ ||
84+
| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support |
85+
|--------------------|-------------------------------|-------------------|---------------------------------|----------------------------------|-----------------------|
86+
| `int` | `int64` || - | - ||
87+
| `?int` | `*int64` || - | - ||
88+
| `float` | `float64` || - | - ||
89+
| `?float` | `*float64` || - | - ||
90+
| `bool` | `bool` || - | - ||
91+
| `?bool` | `*bool` || - | - ||
92+
| `string`/`?string` | `*C.zend_string` || frankenphp.GoString() | frankenphp.PHPString() ||
93+
| `array` | `frankenphp.AssociativeArray` || frankenphp.GoAssociativeArray() | frankenphp.PHPAssociativeArray() ||
94+
| `array` | `map[string]any` || frankenphp.GoMap() | frankenphp.PHPMap() ||
95+
| `array` | `[]any` || frankenphp.GoPackedArray() | frankenphp.PHPPackedArray() ||
96+
| `object` | `struct` || _Not yet implemented_ | _Not yet implemented_ ||
9597

9698
> [!NOTE]
9799
> This table is not exhaustive yet and will be completed as the FrankenPHP types API gets more complete.
@@ -102,55 +104,86 @@ If you refer to the code snippet of the previous section, you can see that helpe
102104

103105
#### Working with Arrays
104106

105-
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.
107+
FrankenPHP provides native support for PHP arrays through `frankenphp.AssociativeArray` or direct conversion to a map or slice.
108+
109+
`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).
110+
111+
If order or association are not needed, it's also possible to directly convert to a slice `[]any` or unordered map `map[string]any`.
106112

107113
**Creating and manipulating arrays in Go:**
108114

109115
```go
110-
//export_php:function process_data(array $input): array
111-
func process_data(arr *C.zval) unsafe.Pointer {
112-
// Convert PHP array to Go
113-
goArray := frankenphp.GoArray(unsafe.Pointer(arr))
114-
115-
result := &frankenphp.Array{}
116-
117-
result.SetInt(0, "first")
118-
result.SetInt(1, "second")
119-
result.Append("third") // Automatically assigns next integer key
120-
121-
result.SetString("name", "John")
122-
result.SetString("age", int64(30))
123-
124-
for i := uint32(0); i < goArray.Len(); i++ {
125-
key, value := goArray.At(i)
126-
if key.Type == frankenphp.PHPStringKey {
127-
result.SetString("processed_"+key.Str, value)
128-
} else {
129-
result.SetInt(key.Int+100, value)
130-
}
131-
}
132-
133-
// Convert back to PHP array
134-
return frankenphp.PHPArray(result)
116+
// export_php:function process_data_ordered(array $input): array
117+
func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
118+
// Convert PHP associative array to Go while keeping the order
119+
associativeArray := frankenphp.GoAssociativeArray(unsafe.Pointer(arr))
120+
121+
// loop over the entries in order
122+
for _, key := range associativeArray.Order {
123+
value, _ = associativeArray.Map[key]
124+
// do something with key and value
125+
}
126+
127+
// return an ordered array
128+
// if 'Order' is not empty, only the key-value paris in 'Order' will be respected
129+
return frankenphp.PHPAssociativeArray(AssociativeArray{
130+
Map: map[string]any{
131+
"key1": "value1",
132+
"key2": "value2",
133+
},
134+
Order: []string{"key1", "key2"},
135+
})
136+
}
137+
138+
// export_php:function process_data_unordered(array $input): array
139+
func process_data_unordered_map(arr *C.zval) unsafe.Pointer {
140+
// Convert PHP associative array to a Go map without keeping the order
141+
// ignoring the order will be more performant
142+
goMap := frankenphp.GoMap(unsafe.Pointer(arr))
143+
144+
// loop over the entries in no specific order
145+
for key, value := range goMap {
146+
// do something with key and value
147+
}
148+
149+
// return an unordered array
150+
return frankenphp.PHPMap(map[string]any{
151+
"key1": "value1",
152+
"key2": "value2",
153+
})
154+
}
155+
156+
// export_php:function process_data_packed(array $input): array
157+
func process_data_packed(arr *C.zval) unsafe.Pointer {
158+
// Convert PHP packed array to Go
159+
goSlice := frankenphp.GoPackedArray(unsafe.Pointer(arr), false)
160+
161+
// loop over the slice in order
162+
for index, value := range goSlice {
163+
// do something with index and value
164+
}
165+
166+
// return a packed array
167+
return frankenphp.PHPackedArray([]any{"value1", "value2", "value3"})
135168
}
136169
```
137170

138-
**Key features of `frankenphp.Array`:**
171+
**Key features of array conversion:**
139172

140-
* **Ordered key-value pairs** - Maintains insertion order like PHP arrays
141-
* **Mixed key types** - Supports both integer and string keys in the same array
142-
* **Type safety** - The `PHPKey` type ensures proper key handling
173+
* **Ordered key-value pairs** - Option to keep the order of the associative array
174+
* **Optimized for multiple cases** - Option to ditch the order for better performance or convert straight to a slice
143175
* **Automatic list detection** - When converting to PHP, automatically detects if array should be a packed list or hashmap
176+
* **Nested Arrays** - Arrays can be nested and will convert all support types automatically (`int64`,`float64`,`string`,`bool`,`nil`,`AssociativeArray`,`map[string]any`,`[]any`)
144177
* **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.
145178

146-
**Available methods:**
179+
##### Available methods: Packed and Associative
147180

148-
* `SetInt(key int64, value interface{})` - Set value with integer key
149-
* `SetString(key string, value interface{})` - Set value with string key
150-
* `Append(value interface{})` - Add value with next available integer key
151-
* `Len() uint32` - Get number of elements
152-
* `At(index uint32) (PHPKey, interface{})` - Get key-value pair at index
153-
* `frankenphp.PHPArray(arr *frankenphp.Array) unsafe.Pointer` - Convert to PHP array
181+
* `frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray) unsafe.Pointer` - Convert to an ordered PHP array with key-value pairs
182+
* `frankenphp.PHPMap(arr map[string]any) unsafe.Pointer` - Convert a map to an unordered PHP array with key-value pairs
183+
* `frankenphp.PHPPackedArray(slice []any) unsafe.Pointer` - Convert a slice to a PHP packed array with indexed values only
184+
* `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to an ordered Go AssociativeArray (map with order)
185+
* `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered go map
186+
* `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a go slice
154187

155188
### Declaring a Native PHP Class
156189

internal/extgen/gofile_test.go

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -514,18 +514,17 @@ type ArrayStruct struct {
514514
}
515515
516516
//export_php:method ArrayClass::processArray(array $items): array
517-
func (as *ArrayStruct) ProcessArray(items *frankenphp.Array) *frankenphp.Array {
518-
result := &frankenphp.Array{}
519-
for i := uint32(0); i < items.Len(); i++ {
520-
key, value := items.At(i)
521-
result.SetString(fmt.Sprintf("processed_%d", i), value)
517+
func (as *ArrayStruct) ProcessArray(items frankenphp.AssociativeArray) frankenphp.AssociativeArray {
518+
result := frankenphp.AssociativeArray{}
519+
for key, value := range items.Map {
520+
result.Set("processed_"+key, value)
522521
}
523522
return result
524523
}
525524
526525
//export_php:method ArrayClass::filterData(array $data, string $filter): array
527-
func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *frankenphp.Array {
528-
result := &frankenphp.Array{}
526+
func (as *ArrayStruct) FilterData(data frankenphp.AssociativeArray, filter string) frankenphp.AssociativeArray {
527+
result := frankenphp.AssociativeArray{}
529528
// Filter logic here
530529
return result
531530
}`
@@ -543,11 +542,10 @@ func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *franke
543542
Params: []phpParameter{
544543
{Name: "items", PhpType: phpArray, IsNullable: false},
545544
},
546-
GoFunction: `func (as *ArrayStruct) ProcessArray(items *frankenphp.Array) *frankenphp.Array {
547-
result := &frankenphp.Array{}
548-
for i := uint32(0); i < items.Len(); i++ {
549-
key, value := items.At(i)
550-
result.SetString(fmt.Sprintf("processed_%d", i), value)
545+
GoFunction: `func (as *ArrayStruct) ProcessArray(items frankenphp.AssociativeArray) frankenphp.AssociativeArray {
546+
result := frankenphp.AssociativeArray{}
547+
for key, value := range items.Entries() {
548+
result.Set("processed_"+key, value)
551549
}
552550
return result
553551
}`,
@@ -562,8 +560,8 @@ func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *franke
562560
{Name: "data", PhpType: phpArray, IsNullable: false},
563561
{Name: "filter", PhpType: phpString, IsNullable: false},
564562
},
565-
GoFunction: `func (as *ArrayStruct) FilterData(data *frankenphp.Array, filter string) *frankenphp.Array {
566-
result := &frankenphp.Array{}
563+
GoFunction: `func (as *ArrayStruct) FilterData(data frankenphp.AssociativeArray, filter string) frankenphp.AssociativeArray {
564+
result := frankenphp.AssociativeArray{}
567565
return result
568566
}`,
569567
},
@@ -613,11 +611,8 @@ func TestGoFileGenerator_MethodWrapperWithNullableArrayParams(t *testing.T) {
613611
type NullableArrayStruct struct{}
614612
615613
//export_php:method NullableArrayClass::processOptionalArray(?array $items, string $name): string
616-
func (nas *NullableArrayStruct) ProcessOptionalArray(items *frankenphp.Array, name string) string {
617-
if items == nil {
618-
return "No items: " + name
619-
}
620-
return fmt.Sprintf("Processing %d items for %s", items.Len(), name)
614+
func (nas *NullableArrayStruct) ProcessOptionalArray(items frankenphp.AssociativeArray, name string) string {
615+
return fmt.Sprintf("Processing %d items for %s", len(items.Map), name)
621616
}`
622617

623618
sourceFile := filepath.Join(tmpDir, "test.go")
@@ -634,11 +629,8 @@ func (nas *NullableArrayStruct) ProcessOptionalArray(items *frankenphp.Array, na
634629
{Name: "items", PhpType: phpArray, IsNullable: true},
635630
{Name: "name", PhpType: phpString, IsNullable: false},
636631
},
637-
GoFunction: `func (nas *NullableArrayStruct) ProcessOptionalArray(items *frankenphp.Array, name string) string {
638-
if items == nil {
639-
return "No items: " + name
640-
}
641-
return fmt.Sprintf("Processing %d items for %s", items.Len(), name)
632+
GoFunction: `func (nas *NullableArrayStruct) ProcessOptionalArray(items frankenphp.AssociativeArray, name string) string {
633+
return fmt.Sprintf("Processing %d items for %s", len(items.Map), name)
642634
}`,
643635
},
644636
}

threadtasks_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package frankenphp
2+
3+
import (
4+
"sync"
5+
)
6+
7+
// representation of a thread that handles tasks directly assigned by go
8+
// implements the threadHandler interface
9+
type taskThread struct {
10+
thread *phpThread
11+
execChan chan *task
12+
}
13+
14+
// task callbacks will be executed directly on the PHP thread
15+
// therefore having full access to the PHP runtime
16+
type task struct {
17+
callback func()
18+
done sync.Mutex
19+
}
20+
21+
func newTask(cb func()) *task {
22+
t := &task{callback: cb}
23+
t.done.Lock()
24+
25+
return t
26+
}
27+
28+
func (t *task) waitForCompletion() {
29+
t.done.Lock()
30+
}
31+
32+
func convertToTaskThread(thread *phpThread) *taskThread {
33+
handler := &taskThread{
34+
thread: thread,
35+
execChan: make(chan *task),
36+
}
37+
thread.setHandler(handler)
38+
return handler
39+
}
40+
41+
func (handler *taskThread) beforeScriptExecution() string {
42+
thread := handler.thread
43+
44+
switch thread.state.get() {
45+
case stateTransitionRequested:
46+
return thread.transitionToNewHandler()
47+
case stateBooting, stateTransitionComplete:
48+
thread.state.set(stateReady)
49+
handler.waitForTasks()
50+
51+
return handler.beforeScriptExecution()
52+
case stateReady:
53+
handler.waitForTasks()
54+
55+
return handler.beforeScriptExecution()
56+
case stateShuttingDown:
57+
// signal to stop
58+
return ""
59+
}
60+
panic("unexpected state: " + thread.state.name())
61+
}
62+
63+
func (handler *taskThread) afterScriptExecution(int) {
64+
panic("task threads should not execute scripts")
65+
}
66+
67+
func (handler *taskThread) getRequestContext() *frankenPHPContext {
68+
return nil
69+
}
70+
71+
func (handler *taskThread) name() string {
72+
return "Task PHP Thread"
73+
}
74+
75+
func (handler *taskThread) waitForTasks() {
76+
for {
77+
select {
78+
case task := <-handler.execChan:
79+
task.callback()
80+
task.done.Unlock() // unlock the task to signal completion
81+
case <-handler.thread.drainChan:
82+
// thread is shutting down, do not execute the function
83+
return
84+
}
85+
}
86+
}
87+
88+
func (handler *taskThread) execute(t *task) {
89+
handler.execChan <- t
90+
}

0 commit comments

Comments
 (0)