Search code examples
phparraysreferencearrayaccess

ArrayAccess in PHP -- assigning to offset by reference


First, a quote from the ole' manual on ArrayAccess::offsetSet():

This function is not called in assignments by reference and otherwise indirect changes to array dimensions overloaded with ArrayAccess (indirect in the sense they are made not by changing the dimension directly, but by changing a sub-dimension or sub-property or assigning the array dimension by reference to another variable). Instead, ArrayAccess::offsetGet() is called. The operation will only be successful if that method returns by reference, which is only possible since PHP 5.3.4.

I'm a bit confused by this. It appears that this suggests that (as of 5.3.4) one can define offsetGet() to return by reference in an implementing class, thus handling assignments by reference.

So, now a test snippet:

(Disregard the absence of validation and isset() checking)

class Test implements ArrayAccess
{
    protected $data = array();

    public function &offsetGet($key)
    {
        return $this->data[$key];
    }

    public function offsetSet($key, $value)
    {
        $this->data[$key] = $value;
    }

    public function offsetExists($key) { /* ... */ }

    public function offsetUnset($key) { /* ... */ }

}

$test = new Test();

$test['foo'] = 'bar';
$test['foo'] = &$bar; // Fatal error: Cannot assign by reference to
                      // overloaded object in

var_dump($test, $bar);    

Ok, so that doesn't work. Then what does this manual note refer to?

Reason
I'd like to permit assignment by reference via the array operator to an object implementing ArrayAccess, as the example snippet shows. I've investigated this before, and I didn't think it was possible, but having come back to this due to uncertainty, I (re-)discovered this mention in the manual. Now I'm just confused.


Update: As I hit Post Your Question, I realized that this is likely just referring to assignment by reference to another variable, such as $bar = &$test['foo'];. If that's the case, then apologies; though knowing how, if it is at all possible, to assign by reference to the overloaded object would be great.


Further elaboration: What it all comes down to, is I would like to have the following method aliases:

isset($obj[$key]);       // $obj->has_data($key);

$value = $obj[$key];     // $obj->get_data($key);

$obj[$key] = $value;     // $obj->set_data($key, $value);

$obj[$key] = &$variable; // $obj->bind_data($key, $variable);

// also, flipping the operands is a syntactic alternative
$variable = &$obj[$key]; // $obj->bind_data($key, $variable);

unset($obj[$key]);       // $obj->remove_data($key);

As far as has, get, set, and remove go, they're no problem with the supported methods of ArrayAccess. The binding functionality is where I'm at a loss, and am beginning to accept that the limitations of ArrayAccess and PHP are simply prohibitive of this.


Solution

  • This does not work with ArrayAccess, you could add yourself a public function that allows you to set a reference to an offset (sure, this looks different to using array syntax, so it's not really a sufficient answer):

    class Test implements ArrayAccess{
    
        protected $_data = array();
    
        public function &offsetGet($key){
            return $this->_data[$key];
        }
    
        ... 
    
        public function offsetSetReference($key, &$value)
        {
            $this->_data[$key] = &$value;
        }
    }
    
    $test = new Test();
    $test['foo'] = $var = 'bar';
    $test->offsetSetReference('bar', $var);    
    $var = 'foo';    
    echo $test['bar']; # foo    
    $alias = &$test['bar'];    
    $alias = 'hello :)';    
    echo $var; # hello :)
    

    Probably such a function was forgotten when ArrayAccess was first implemented.

    Edit: Pass it as "a reference assignment":

    class ArrayAccessReferenceAssignment
    {
        private $reference;
        public function __construct(&$reference)
        {
            $this->reference = &$reference;
        }
        public function &getReference()
        {
            $reference = &$this->reference;
            return $reference;
        }
    }
    
    
    class Test implements ArrayAccess{
        ...
        public function offsetSet($key, $value){
            if ($value instanceof ArrayAccessReferenceAssignment)
            {
               $this->offsetSetReference($key, $value->getReference());
            }
            else
            {
               $this->_data[$key] = $value;
            }
        }
    

    Which then works flawlessly because you implemented it. That's probably more nicely interfacing than the more explicit offsetSetReference variant above:

    $test = new Test();
    $test['foo'] = $var = 'bar';
    $test['bar'] = new ArrayAccessReferenceAssignment($var);
    
    $var = 'foo';
    echo $test['bar']; # foo
    $alias = &$test['bar'];
    $alias = 'hello :)';
    echo $var; # hello :)