4

I am missing something obvious. Here's a trivial piece of PHP, littered with debug echos:

function echo_rows(&$res) {
    $rows= array();
    while ($row= $res->fetch()) {
           echo $row['ccorID'] . "\r\n";
        $rows[]= $row;
           echo $rows[0]['ccorID'] . "\r\n";
    }

    echo "---.---\r\n";
    echo count($rows) . "\r\n";
    foreach($rows as $row) {
        echo $row['ccorID'] . "\r\n";
    }
    echo json_encode($rows);
}

Here's what I see in the response:

0
0
3
3
13
13
182
182
---.---
4
182
182
182
182

It seems perfectly clear to me that :-

Anyone got any idea what I need to do to get a copy of $row (which is an associative array)?

Thanks.

EDIT: Since many of you are so insistent to know what $res is, here is the class. I genuinely believe that this is more likely to confuse than enlighten (hence the omission from my OP).

class mysqlie_results {
    private $stmt;
    private $paramArray= array(); 
    private $assocArray= array(); 

    public function __construct(&$stmt) {
        $this->stmt= $stmt;
        $meta= $stmt->result_metadata(); 

        while ($colData= $meta->fetch_field()) {
            $this->paramArray[]= &$this->assocArray[$colData->name]; 
        }

        call_user_func_array(array($stmt,'bind_result'),$this->paramArray); 
        $meta->close(); 
    } 

    public function __destruct() {
        $this->stmt->free_result();
    } 

    public function fetch() { 
        return $this->stmt->fetch()? $this->assocArray : false;
    } 
} 
16
  • 2
    You're right. It does not perform a value copy. It performs a variable copy. So if the variable is a reference, the resulting array element will be a reference as well... If it's not a reference, it will perform the value copy (just like any other variable copy)... And the value copy bit you cite is when you copy the entire element, not inserting records... Commented Aug 25, 2010 at 17:16
  • 11
    Please rename your title! Commented Aug 25, 2010 at 17:16
  • For some reason the first element of $rows is being overwritten here. Commented Aug 25, 2010 at 17:17
  • What is the desired output of this code? Also, should you be using fetch_assoc()? Commented Aug 25, 2010 at 17:18
  • 7
    Interesting, I get dumb when using php. Commented Aug 25, 2010 at 17:25

5 Answers 5

3

edit: Using object was only a guess. It turns out that the problem is caused by references as others have guessed before.

Since we don't know (yet) what $res is and what $res->fetch() actually returns let me try to replicate the behaviour.

<?php
$res = new Foo;
echo_rows($res);

function echo_rows(&$res) {
  // as provided in the question
}

class Foo extends ArrayObject {  
  protected $bar, $counter;

  public function fetch() {
    if ( ++$this->counter > 4 ) {
      return false;
    }
    else {
      $this->bar['ccorID'] = $this->counter;
      return $this->bar;
    }
  }

  public function __construct() {
    $this->bar = new ArrayObject;
    $this->counter = 0;
  }
}

prints

1
1
2
2
3
3
4
4
---.---
4
4
4
4
4
[{"ccorID":4},{"ccorID":4},{"ccorID":4},{"ccorID":4}]

The reason is, that all elements in $rows point to the same underlying object (see http://docs.php.net/language.oop5.references) since my class Foo always returns the same object and just modifies the state of this single object.


update: "what might the manual mean when it says "Use the reference operator to copy an array by reference"?"

Let's start with this script

<?php
$arr = array();
$s = '';

$b = array();
$b['foo'] = $arr;
$b['bar'] = $s;

$b['foo']['bar'] = 1;
$b['bar'] = 'xyz';

var_dump($arr, $s);

the output is

array(0) {
}
string(0) ""

i.e. $b['foo']['bar'] = 1; and $b['bar'] = 'xyz'; didn't change $arr and $s since the array contains copies of the values.

Now change the two assignments to use references

$b['foo'] = &$arr;
$b['bar'] = &$s;

and the output changes to

array(1) {
  ["bar"]=>
  int(1)
}
string(3) "xyz"

i.e. the elements are not copies.

Sign up to request clarification or add additional context in comments.

13 Comments

+1 since objects are passed by reference by default (well, the instance is anyway)...
Not exactly references, but close, see docs.php.net/language.oop5.references
I completely agree with you guys. That's clearly what's happening. I am merely put out because the PHP manual states: "Array assignment always involves value copying. Use the reference operator to copy an array by reference." It is clearly not so.
@Ollie2893: The "value" of a variable that points to an object is the object-id that is used to point to the object (php5). A copy of that object-id still points to the same object. What you've expected is a clone of the object but that doesn't happen implicitly (again php5), see docs.php.net/language.oop5.references and docs.php.net/clone
Yes, I am familiar with this concept, Volker. I don't quite agree with you, though, that one can read the PHP manual entry in this manner. Following your logic, what might the manual mean when it says "Use the reference operator to copy an array by reference"?
|
1

PHP core developer Johannes Schlüter says Do not use PHP references. References in PHP are mostly a holdover from PHP 4. In PHP 5, references are not needed in most cases where you would be tempted to use them.

The PHP manual on Returning References says (emphasis is theirs):

Do not use return-by-reference to increase performance. The engine will automatically optimize this on its own. Only return references when you have a valid technical reason to do so.

If you do need to clone an array reference, then you can copy it by value this way:

$row = unserialize(serialize($row));

2 Comments

Ok, so this is vaguely frightening because what you seem to be suggesting is that, owing to some performance considerations, the actual behaviour of the code becomes unpredictable. At no point does my code use references in returning the content to $row and yet it is quite obvious that a reference is what results. I can also add (I did not want earlier - thought my OP was stupid enough) that the results from this function varied, depending on where it was called from. Optmization may well account for that.
Bill, I just followed your link to Schlüter. This is a very interesting article. Strangely, though, if you follow his logic to the end, then I should still get the result I want. When I do my 2nd fetch, which will refill the memory area that is now shared with $rows[0], it follows that PHP should perform a copy-on-write. It does not. So perhaps it's a bug?
0

Let's take a closer look at

    while ($colData= $meta->fetch_field()) {
        $this->paramArray[]= &$this->assocArray[$colData->name]; 
    }

    call_user_func_array(array($stmt,'bind_result'),$this->paramArray);

There you have your reference(s) and that's causing the trouble. If you copy an array that contains a reference the result is still an array containing a reference.

Try

while ($row= $res->fetch()) {
  var_dump($row);

and you will see that $row also contains references. All those corresponding elements in paramArray, assocArray and $row actually reference the same value. And thus when you invoke return $this->stmt->fetch() it doesn't only affect paramArray but all those references, all the way down to $rows since $rows[]= $row; still only copies the array but does not "de-reference" the elements.

3 Comments

That's not my reading of the situation. The fancy bit in this code is paramArray, which is indeed an array of references. But it is merely used to trick mysqli::fetch (via bind_result) into depositing its values into assocArray, the vanilla structure. When fetch returns assocArray, it is returning an array of values, I think.
@Ollie2893: "When fetch returns assocArray, it is returning an array of values, I think." - no, it does not. Stop assuming/speculating, test! Especially when it's as easy as putting one line of code in there as suggested.
You are quite right - I should have tried. var_dump reveals indeed that all elements are references, as you write. The logic why this happens defeats me - it does not match my expectations (based on C etc.). Turns out, though, that Eli was right all along. When I morphed my code from mysql to mysqli, I was somehow steered down the road of ::prepare and ::execute, which landed me in the pickle of having to write my own fetch_assoc (which went so wrong). Using ::query instead, I end up with mysqli_result::fetch_all, and that's SOOOO much easier. Thanks for your help.
0

I'll bet that $res->fetch() is returning a reference for some reason... Try doing this to break the reference:

while ($row= $res->fetch()) {
    echo $row['ccorID'] . "\r\n";
    $rows[]= $row;
    echo $rows[0]['ccorID'] . "\r\n";
    unset($row);
}

(Noticed the added unset at the end)...

1 Comment

Appreciate the idea. Tried it. No difference.
0

i would try array_merge(), but its not clear what the intention of this function is, quick fix just add an indexer because what you are doing is just copying the reference and on every row you just update where the reference is pointing to

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.