Skip to content

ArrayAccess (offsetSet) is not triggered on object properties #2465

@zither

Description

@zither

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

  1. Save the files as shown above.
  2. Compile the extension: zephir build
  3. 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"
  }
}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions