Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

php unset local reference affecting global scope

Tags:

php

i have come across some very strange php behaviour (5.3.2 on ubuntu 10.04). an unset which should occur within local scope is affecting the scope of the caller function. the following snippet is a simplification of my code which displays what i can only assume is a bug:

<?php
function should_not_alter($in)
{
    $in_ref =& $in['level1'];
    should_only_unset_locally($in);
    return $in;
}
function should_only_unset_locally($in)
{
    unset($in['level1']['level2_0']);
}
$data = array('level1' => array('level2_0' => 'first value', 'level2_1' => 'second value'));
$data = should_not_alter($data); //test 1
//should_only_unset_locally($data); //test 2
print_r($data);
?>

if you run the above you will see that the value 'first value' has been unset from the $data array in the global scope. however if you comment out test 1 and run test 2 this does not happen.

i can only assume that php does not like referencing an element of an array. in my code i need to alter $in_ref - hence the reason for the $in_ref =& $in['level1']; line in the above code. i realize that removing this line would fix the problem of 'first value' being unset in the global scope, but this is not an option.

can anyone confirm if this is intended behaviour of php?

i suspect it is a bug, rather than a feature, because this behaviour is inconsistent with the way that php handles scopes and references with normal (non-array) variables. for example, using a string rather than an array function should_only_unset_locally() has no effect on the global scope:

<?php

function should_not_alter($in)
{
    $in_ref =& $in;
    should_only_unset_locally($in);
    return $in;
}
function should_only_unset_locally($in)
{
    unset($in);
}
$data = 'original';
$data = should_not_alter($data); //test 1
//should_only_unset_locally($data); //test 2
print_r($data);

?>

both test1 or test2 output original as expected. actually, even if $data is an array but $in_ref is referenced to the entire array (ie $in_ref =& $in;) then the buggy behaviour goes away.

update

i have submitted a bug report

like image 973
mulllhausen Avatar asked Dec 28 '12 04:12

mulllhausen


People also ask

What's better at freeing memory with PHP unset () or $var Null?

You might get memory freed / shrunk faster, but it may steal CPU cycles from the code that truly needs them sooner, resulting in a longer overall execution time. It seems that $a = null is a bit faster than its unset() counterpart: updating a symbol table entry appears to be faster than removing it.

What does the unset () function do?

unset() destroys the specified variables. The behavior of unset() inside of a function can vary depending on what type of variable you are attempting to destroy. If a globalized variable is unset() inside of a function, only the local variable is destroyed.

Is unset variable used in PHP?

The unset() function is an inbuilt function in PHP which is used to unset a specified variable.

What does unset ($ foo do in PHP?

The function unset() destroys the specified variables.


2 Answers

Yup, looks like a bug.

As the name of the function implies, should_not_alter should not alter the array since it's passed by value. (I'm of course not basing that just off of the name -- it also should not alter anything based on its definition.)

The fact that commenting $in_ref =& $in['level1']; makes it leave $in alone seems to be further proof that it's a bug. That is quite an odd little quirk. No idea what could be happening internally to cause that.

I'd file a bug report on the PHP bug tracker. For what it's worth, it still exists in 5.4.6.

like image 76
Corbin Avatar answered Nov 20 '22 04:11

Corbin


$data = should_not_alter($data)

This line is overwriting the $data array with the return value of should_not_alter, which is $in. This is normal behavior.

Also, while you're creating a reference $in_ref =& $in['level1']; but you're not doing anything with it. It will have no effect on the program output.

Short answer:

Delete the reference variable via unset($in_ref) before calling the should_only_unset_locally() function.

Long answer:

When a reference to an array element is created, the array element is replaced with a reference. This behavior is weird but it isn't a bug - it's a feature of the language and is by design.

Consider the following PHP program:

<?php
$a = array(
    'key1' => 'value1',
    'key2' => 'value2',
);
$r = &$a['key1'];
$a['key1'] = 'value3';
var_dump($a['key1']);
var_dump($r);
var_dump($a['key1'] === $r);

Output:
string(6) "value3"
string(6) "value3"
bool(true)

Assigning a value to $a['key1'] changes the value of $r as they both reference the same value. Conversely updating $r will update the array element:

$r = 'value4';
var_dump($a['key1']);
var_dump($r);

Output:
string(6) "value4"
string(6) "value4"

The value doesn't live in $r or $a['key'] - those are just references. It's like they're both referencing some spooky, hidden value. Weird, huh?

For most use cases this is desired and useful behavior.

Now apply this to your program. The following line modifies the local $in array and replaces the 'level1' element with a reference:

$in_ref = &$in['level1'];

$in_ref is not a reference to $in['level1'] - instead they both reference the same spooky value. So when this line comes around:

unset($in['level1']['level2_0']);

PHP sees $in['level1'] as a reference to a spooky value and removes the 'level2_0' element. And since it's a reference the removal is also felt in the scope of the should_not_alter() function.

The solution to your particular problem is to destroy the reference variable which will automagically restore $in['level1'] back to normal behavior:

function should_not_alter($in) {
    $in_ref =& $in['level1'];
    // Do some stuff with $in_ref
    // After you're done with it delete the reference to restore $in['level1']
    unset($in_ref);
    should_only_unset_locally($in);
    return $in;
}
like image 43
leepowers Avatar answered Nov 20 '22 02:11

leepowers