19

I can see why

$a = new ArrayObject();
$a['ID'] = 42;
$b = &$a['ID'];
$c = $a;
$c['ID'] = 37;
echo $a['ID']."\n";
echo $b."\n";
echo $c['ID']."\n";

outputs 37, 42, 37

while

$a = new ArrayObject();
$a['ID'] = 42;
$b = &$a['ID'];
$c = $a;
$b = 37;
echo $a['ID']."\n";
echo $b."\n";
echo $c['ID']."\n";

outputs 37, 37, 37

In both cases $b is a reference to $a['ID'] while $c is a pointer to the same object as $a.

When $b changes $a['ID'] and $c['ID'] change because assigning $b changes the value referenced by $a['ID'].

When $c['ID'] changes, a new int is assigned to $a['ID'], $b doesn't reference $a['ID'] anymore.

But this itches me

$a = new ArrayObject();
$a['ID'] = 42;
$b = &$a['ID'];
$c = $a;
$c['ID'] &= 0;
$c['ID'] |= 37;
echo $a['ID']."\n";
echo $b."\n";
echo $c['ID']."\n";

(outputs 37, 37, 37)

Is this defined behaviour? I didn't see anything about that in the documentation...

Mmmh mmh
  • 5,334
  • 3
  • 21
  • 29
  • 3
    Yeah, I don't think it's documented. The manual only says that ++ and -- perform indirect modification and thus trigger `ArrayAccess::offsetGet()` and not `ArrayAccess::offsetSet()`. For consistency sake, I guess +=, -=, and cousins were implemented in the same manner. – cleong May 21 '13 at 00:12
  • @cleong I can't seem to find it in the docs. Could you point out where? – webmaster777 May 22 '13 at 09:15

2 Answers2

3

Let's take this code as a basis: ( refcounting documentation )

$a = new ArrayObject();
$a['ID'] = 42;
$b = &$a['ID'];
$c = $a;

xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');

This gives:

a:
(refcount=2, is_ref=0),
object(ArrayObject)[1]
  public 'ID' => (refcount=2, is_ref=1),int 42
b:
(refcount=2, is_ref=1),int 42
c:
(refcount=2, is_ref=0),
object(ArrayObject)[1]
  public 'ID' => (refcount=2, is_ref=1),int 42

As you say: $a is an object, $b is a reference of $a['ID'] ( $a['ID'] and $b : refcount=2, is_ref=1) and $c is copy as a reference (since PHP5), so $c is a reference of $a ( it's now the same object : refcount=2, is_ref=0)


If we do: $c['ID'] = 37;

We get:

a:
(refcount=2, is_ref=0),
object(ArrayObject)[1]
  public 'ID' => (refcount=1, is_ref=0),int 37
b:
(refcount=1, is_ref=0),int 42
c:
(refcount=2, is_ref=0),
object(ArrayObject)[1]
  public 'ID' => (refcount=1, is_ref=0),int 37

$c['ID'] is assigned a new int so =>

$b becomes independent (refcount=1 and is_ref=0), as well as $a['ID'] and $c['ID']

BUT as $c and $a are dependent, $a['ID'] and $c['ID'] take the same value 37.


Now, let's take the base code and we do: $c['ID'] &= 0;

UPDATE: Unexpectedly, we get:

a:
(refcount=2, is_ref=0),
object(ArrayObject)[1]
  public 'ID' => (refcount=2, is_ref=1),int 0
b:
(refcount=2, is_ref=1),int 0
c:
(refcount=2, is_ref=0),
object(ArrayObject)[1]
  public 'ID' => (refcount=2, is_ref=1),int 0

instead of: ( if: $c['ID'] = $c['ID'] & 0;)

a:
(refcount=2, is_ref=0),
object(ArrayObject)[1]
  public 'ID' => (refcount=1, is_ref=0),int 0
b:
(refcount=1, is_ref=0),int 42
c:
(refcount=2, is_ref=0),
object(ArrayObject)[1]
  public 'ID' => (refcount=1, is_ref=0),int 0

ArrayObject implements ArrayAccess so:

As said in the comment and documented here:

A direct modification is one that replaces completely the value of the array dimension, as in $obj[6] = 7. An indirect modification, on the other hand, only changes part of the dimension, or attempts to assign the dimension by reference to another variable, as in $obj[6][7] = 7 or $var =& $obj[6]. Increments with ++ and decrements with -- are also implemented in a way that requires indirect modification.

A possible answer:

"Combined Operator (+=, -=, &=, |=) could be worked as the same manner (indirect modification.)":

refcount and is_ref are not impacted therefore (in our case) the values of all related variables are modified. ($c['ID'] => $a['ID'] => $b)

antoox
  • 1,309
  • 11
  • 9
2

It's more or less defined (but sometimes undocumented) behaviour; mainly because $a is not an array but an ArrayObject.

Let's take a look at your third code fragment first:

$a = new ArrayObject();
$a['ID'] = 42;
$b = &$a['ID'];
$c = $a;
$c['ID'] &= 0;

The last assignment translates to:

$tmp = &$c->offsetGet('ID');
$tmp &= 0; // or: $tmp = $tmp & 0;

The take-away point here is only offsetGet() is called and it returns a reference to $c['ID'], as noted in this comment. Because offsetSet() is not called, the value of $b changes as well.

Btw, the increment (++) and decrement operator (--) work in a similar fashion, no offsetSet() is called.

Differences

This is different from your first example:

$a = new ArrayObject();
$a['ID'] = 42;
$b = &$a['ID'];
$c = $a;
$c['ID'] = 37;

The last statement has the following equivalent:

$c->offsetSet('ID', 37);

Before a new value is assigned to $c['ID'], the previous value is effectively unset(); this is why $b is the only variable still holding on to 42.

Proof of this behaviour can be seen when you use objects instead of numbers:

class MyLoggerObj
{
        public function __destruct()
        {
                echo "Destruct of " . __CLASS__ . "\n";
        }
}

$a = new ArrayObject();
$a['ID'] = new MyLoggerObj();
$a['ID'] = 37;

echo $a['ID']."\n";

Output:

Destruct of MyLoggerObj
37

As you can see, the destructor is called on MyLoggerObj; that's because in this case there's no variable holding on to the value anymore.

Bonus

If you try to find out when offsetGet() and offsetSet() are called by extending ArrayObject you will be disappointed to find out that you can't properly mimic mixed &offsetGet($key);. In fact, doing so changes the behaviour of ArrayObject in such a way that it's impossible to prove the behaviour of this class.

Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
  • 1
    Still not convinced :P, How come that b doesn't get updated in example 1? According to your _expected_ behaviour of offsetSet, it changes the reference (indirect modification), but then b should be 37 aswell. – webmaster777 May 22 '13 at 11:44
  • I can only guess that other semantics are at work here, but once I get to a computer I'll give that aspect some attention. – Ja͢ck May 22 '13 at 11:56
  • 1
    That would make Ex.3 result in 37,42,37 (since the reference $c['id'] is unset by your standards, and after that set to 0). – webmaster777 May 22 '13 at 13:24