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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 81 additions & 48 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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` | `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.
Expand All @@ -102,55 +104,86 @@ 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 `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).

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(array $input): array
func process_data(arr *C.zval) unsafe.Pointer {
// Convert PHP array to Go
goArray := frankenphp.GoArray(unsafe.Pointer(arr))

result := &frankenphp.Array{}

result.SetInt(0, "first")
result.SetInt(1, "second")
result.Append("third") // Automatically assigns next integer key

result.SetString("name", "John")
result.SetString("age", int64(30))

for i := uint32(0); i < goArray.Len(); i++ {
key, value := goArray.At(i)
if key.Type == frankenphp.PHPStringKey {
result.SetString("processed_"+key.Str, value)
} else {
result.SetInt(key.Int+100, value)
}
}

// Convert back to PHP array
return frankenphp.PHPArray(result)
// export_php:function process_data_ordered(array $input): array
func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
// Convert PHP associative array to Go while keeping the order
associativeArray := frankenphp.GoAssociativeArray(unsafe.Pointer(arr))

// 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
// if 'Order' is not empty, only the key-value paris in 'Order' will be respected
return frankenphp.PHPAssociativeArray(AssociativeArray{
Map: map[string]any{
"key1": "value1",
"key2": "value2",
},
Order: []string{"key1", "key2"},
})
}

// export_php:function process_data_unordered(array $input): array
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
goMap := frankenphp.GoMap(unsafe.Pointer(arr))

// loop over the entries in no specific order
for key, value := range goMap {
// do something with key and value
}

// return an unordered array
return frankenphp.PHPMap(map[string]any{
"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
goSlice := frankenphp.GoPackedArray(unsafe.Pointer(arr), false)

// loop over the slice in order
for index, value := range goSlice {
// do something with index and value
}

// return a packed array
return frankenphp.PHPackedArray([]any{"value1", "value2", "value3"})
}
```

**Key features of `frankenphp.Array`:**
**Key features of array conversion:**

* **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
* **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`,`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:**
##### Available methods: Packed and Associative

* `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
* `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 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) []any` - Convert a PHP array to a go slice

### Declaring a Native PHP Class

Expand Down
40 changes: 16 additions & 24 deletions internal/extgen/gofile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,18 +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 := uint32(0); i < items.Len(); i++ {
key, value := items.At(i)
result.SetString(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
}`
Expand All @@ -543,11 +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 := uint32(0); i < items.Len(); i++ {
key, value := items.At(i)
result.SetString(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
}`,
Expand All @@ -562,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
}`,
},
Expand Down Expand Up @@ -613,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", items.Len(), 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")
Expand All @@ -634,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", items.Len(), name)
GoFunction: `func (nas *NullableArrayStruct) ProcessOptionalArray(items frankenphp.AssociativeArray, name string) string {
return fmt.Sprintf("Processing %d items for %s", len(items.Map), name)
}`,
},
}
Expand Down
90 changes: 90 additions & 0 deletions threadtasks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package frankenphp

import (
"sync"
)

// 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
}
Loading
Loading