Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can you explicitly copy a PHP array containing "referenced" elements by value?

Background on references to PHP array elements

Let's say you create a complex data structure using nested PHP arrays like this:

$a1 = array(
    'b' => array('foo' => 1),
    'c' => array('bar' => 1)
);

Imagine the array to be more deeply nested, with more elements and with longer, meaningful names.

If one needs to access a substructure of $a1 a lot, for reading and writing, one might be tempted to create an "alias" like this:

$b = &$a1['b'];

However, this leads to massive confusion down the line as the "assignment" actually changes $a1.

I think many inexperienced PHP developers (like me) would assume that $b is a reference to $a1['b'] after the assignment. What really happens is that both $b AND $a1['b'] turns into references to the element array('foo' => 1), with unexpected consequences. I find this very unintuitive.

Let's say you need to preserve $a1 as it is, but you also need a copy of $a1, let's call the copy $a2, and change some of the elements of $a2:

$a2 = $a1;           // Copy $a1 to $a2
$a2['b']['foo'] = 2; // GOTCHA! This will change $a1['b']['foo'] as well!
$a2['c']['bar'] = 2; // This will not change $a1['c']['bar'] however

Because we previously created a reference to $a1['b'], that element will be assigned by reference while the rest of $a1 is copied by value. This took me hours to figure out.

var_dump($a1);
var_dump($a2);

Will output

array (size=2)
  'b' => &
    array (size=1)
      'foo' => int 2
  'c' => 
    array (size=1)
      'bar' => int 1

array (size=2)
  'b' => &
    array (size=1)
      'foo' => int 2
  'c' => 
    array (size=1)
      'bar' => int 2

Please note the & after the 'b' element, indicating it is a reference. And that the value of $a1['b']['foo'] was changed from 1 to 2.

My question

Is there a fool-proof way to copy an array by value when references might have been made to elements in the source array?

Or in other words: Is there any way to make sure that while copying $a1 to $a2 that $a1['b'] is copied by value so that changing $a2['b'] does not destroy $a1['b']?

I've found that you can do this:

unset($b);      // Remove reference to $a1['b']
$a2 = $a1;      // Now all of $a1 will be copied to $a2 by value
$b = &$a1['b']; // Recreate reference

Of course, if there are more references to $a1['b'] or other elements of $a1 you need to unset them all. I was hoping for a smarter way, such as an "inverse" to the =& (assignment by reference) operator or a function to recursively copy a deeply nested associative array by value. Something that would always work even if there are references created to the array elements that you aren't aware of.

This is the closest to a solution I've found so far. It seems to work, but it's not "pretty":

$a2 = json_decode(json_encode($a1), true);

Research done

My question is related to several others here on SO. However, I have yet to find an answer to my specific question. Or even an answer that provides an intuitive understanding of the mechanism behind this strange and unexpected behavior. For example, the answer to Array reference confusion in PHP quotes the PHP manual which says:

Note, however, that references inside arrays are potentially dangerous. Doing a normal (not by reference) assignment with a reference on the right side does not turn the left side into a reference, but references inside arrays are preserved in these normal assignments.

I've read the section of the manual and I've found it unhelpful. It provides no deeper understanding, nor does it explain how to safely work with references to array elements. One comment on the PHP manual page What References Do mentions the strange behavior, but no suggestions are made on how to deal with it.

Is this just one of those poorly documented "quirks" of PHP that you just have to learn to live with? Or would some deeper understanding of the subject help, and where would I in such case most likely find enlightenment?

like image 512
Tomas Eklund Avatar asked Jan 08 '15 21:01

Tomas Eklund


1 Answers

You did great research.

Just can't understand what is exact question?

I think I found another one solution very close to your json encode decode variant:

Is there any way to make sure that while copying $a1 to $a2 that $a1['b'] is copied by value so that changing $a2['b'] does not destroy $a1['b']?

Check please:

$a1 = array(
    'b' => array('foo' => 1),
    'c' => array('bar' => 1)
);

$b = &$a1['b'];

$a2 = unserialize(serialize($a1));           // Copy $a1 to $a2
$a2['b']['foo'] = 2; // GOTCHA! 
$a2['c']['bar'] = 2; // 

var_dump($a1);
var_dump($a2);

that outputs for me:

array(2) {
  ["b"]=>
  &array(1) {
    ["foo"]=>
    int(1)
  }
  ["c"]=>
  array(1) {
    ["bar"]=>
    int(1)
  }
}
array(2) {
  ["b"]=>
  array(1) {
    ["foo"]=>
    int(2)
  }
  ["c"]=>
  array(1) {
    ["bar"]=>
    int(2)
  }
}

is it what you asked for?

and by the way, here a link to compare json_encode vs serialize it could be helpful

Preferred method to store PHP arrays (json_encode vs serialize)

like image 192
Alex Avatar answered Sep 28 '22 05:09

Alex