ArrayAccess in PHP — assigning to offset by refere

2019-04-04 03:57发布

问题:

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.

回答1:

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 :)


回答2:

What the manual is referring to are so called "indirect modifications". Consider the following script:

$array = new ArrayObject;
$array['foo'] = array();
$array['foo']['bar'] = 'foobar';

In the above script $array['foo'] = array(); will trigger a offsetSet('foo', array()). $array['foo']['bar'] = 'foobar'; on the other hand will trigger a offsetGet('foo'). Why so? The last line will be evaluated roughly like this under the hood:

$tmp =& $array['foo'];
$tmp['bar'] = 'foobar';

So $array['foo'] is first fetched by ref and then modified. If your offsetGet returns by ref this will succeed. If not you'll get some indirect modification error.


What you want on the other hand is the exact opposite: Not fetch a value by reference, but assign it. This would theoretically require a signature of offsetSet($key, &$value), but practically this is just not possible.

By the way, references are hard to grasp. You'll get lots of non-obvious behavior and this is especially true for array item references (those have some special rules). I'd recommend you to just avoid them altogether.