-
-
Notifications
You must be signed in to change notification settings - Fork 462
Description
Environment
- Zephir Version:
0.19.0 - PHP Version:
8.3.6 - Operating System: Ubuntu 24.04
Description
When a class property is an object that implements the ArrayAccess interface, attempting to set a value using array syntax (e.g., let this->property[key] = value;) within a Zephir method does not trigger the offsetSet method of the property object.
In native PHP, the same operation correctly calls the offsetSet method. This discrepancy leads to inconsistent and unexpected behavior between Zephir-compiled extensions and native PHP code, breaking a fundamental object-oriented pattern in PHP.
It appears that instead of invoking offsetSet, Zephir reads the property's internal data into a new array, modifies that array, and then overwrites the original object property with this new array. This effectively breaks the object reference and prevents the ArrayAccess interface from functioning as intended.
How To Reproduce
Here is a minimal reproducible example, including a native PHP class for direct comparison.
1. lib/collection.zep
namespace Dust\Lib;
use ArrayAccess;
use Countable;
use IteratorAggregate;
use ArrayIterator;
class Collection implements ArrayAccess, Countable, IteratorAggregate
{
protected data = [];
public function __construct()
{
let this->data = [];
}
public function has(key) -> boolean
{
return isset this->data[key];
}
public function get(key, defaultValue = null)
{
return this->has(key) ? this->data[key] : defaultValue;
}
public function set(string key, var value)
{
let this->data[key] = value;
}
public function remove(key)
{
unset(this->data[key]);
}
public function offsetExists(key) -> boolean
{
return this->has(key);
}
public function offsetGet(key)
{
return this->get(key);
}
public function offsetSet(key, value)
{
this->set(key, value);
}
public function offsetUnset(key)
{
this->remove(key);
}
public function count() -> int
{
return count(this->data);
}
public function getIterator() -> <ArrayIterator>
{
return new ArrayIterator(this->data);
}
}2. lib/test.zep
namespace Dust\Lib;
class Test
{
public data;
public function __construct(<Collection> data)
{
let this->data = data;
}
public function set(k, v)
{
// This is where the bug occurs. It does not call offsetSet.
let this->data[k] = v;
}
}3. test.php (The test script)
<?php
use Dust\Lib\Collection;
use Dust\Lib\Test;
// Native PHP class for comparison
class A
{
public $data;
public function __construct(Collection $data)
{
$this->data = $data;
}
public function set($k, $v)
{
// This works as expected, calling offsetSet on the Collection object.
$this->data[$k] = $v;
}
}
// --- Test Case ---
$collection = new Collection();
$collection->set('123', '123');
// 1. Test with the Zephir-compiled class
$a = new Test($collection);
$a->set('k', 'v');
// This incorrectly returns false
var_dump($collection->has('k'));
// 2. Test with the native PHP class
$b = new A($collection);
$b->set('k', 'v');
// This correctly returns true
var_dump($collection->has('k'));
// 3. Inspect the final state of the properties
var_dump($a->data, $b->data);4. Steps
- Save the files as shown above.
- Compile the extension:
zephir build - Run the test script:
php -d extension=dust.so test.php
Actual Behavior
The script outputs the following:
bool(false)
bool(true)
array(2) {
["*data"]=>
array(1) {
[123]=>
string(3) "123"
}
["k"]=>
string(1) "v"
}
object(Dust\Lib\Collection)#1 (1) {
["data":protected]=>
array(2) {
[123]=>
string(3) "123"
["k"]=>
string(1) "v"
}
}
Notice that $collection->has('k') is false after being called from the Test (Zephir) object. Also, var_dump($a->data) reveals that the property is no longer a Collection object but has been converted into a plain array.
Expected Behavior
The behavior should be identical to the native PHP class. The offsetSet method of the Collection object should be triggered in both cases.
bool(true)
bool(true)
object(Dust\Lib\Collection)#1 (1) {
["data":protected]=>
array(2) {
[123]=>
string(3) "123"
["k"]=>
string(1) "v"
}
}
object(Dust\Lib\Collection)#1 (1) {
["data":protected]=>
array(2) {
[123]=>
string(3) "123"
["k"]=>
string(1) "v"
}
}