php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Doc Bug #40610 Array changes when missing array element is ref'd (w/ no assignment to array!)
Submitted: 2007-02-23 17:59 UTC Modified: 2007-02-24 11:35 UTC
Votes:1
Avg. Score:4.0 ± 0.0
Reproduced:1 of 1 (100.0%)
Same Version:0 (0.0%)
Same OS:0 (0.0%)
From: Webbed dot Pete at gmail dot com Assigned: colder (profile)
Status: Closed Package: Documentation problem
PHP Version: 5.2.1 OS: Windows, Linux
Private report: No CVE-ID: None
 [2007-02-23 17:59 UTC] Webbed dot Pete at gmail dot com
Description:
------------
Pass an unset array element by reference, without doing anything at all.

The passed array will gain an element.

(Note: We use isset() in our real function, which is what pointed us to the bug, but it is not necessary for demoing the defect.)

Reproduce code:
---------------
<?php
// NOP function with pass-by-reference (PBR) parameter
function refTest( &$param ) { }
// Do test with empty and non-empty arrays
$aEmpty = array();        $aOne = array( 'corn' );

// Initial state is fine
echo "<pre>BEFORE\n";
echo "aEmpty contains ".count($aEmpty)." element(s)\n",print_r( $aEmpty, TRUE ); echo "aOne contains ".count($aOne)." element(s)\n",print_r( $aOne, TRUE );

// Pass by reference modifies the arrays. (we use with 'isset()' and saw this; I've reduced to basic issue.)

$aEmpty = array();        $aOne = array( 'corn' );
refTest( $aEmpty['wheat'] ); refTest( $aOne['wheat'] );

echo "\nAFTER PASS BY REFERENCE\n";
echo "aEmpty contains ".count($aEmpty)." element(s)\n",print_r( $aEmpty, TRUE ); echo "aOne contains ".count($aOne)." element(s)\n",print_r( $aOne, TRUE );

?>


Expected result:
----------------
BEFORE
aEmpty contains 0 element(s)
Array
(
)
aOne contains 1 element(s)
Array
(
    [0] => corn
)


[Same if you directly reference, or use isset() outside of a function, etc etc]
[If you don't pass by reference you get notice error so that is not a solution]

Actual result:
--------------
AFTER PASS BY REFERENCE
aEmpty contains 1 element(s)
Array
(
    [wheat] => 
)
aOne contains 2 element(s)
Array
(
    [0] => corn
    [wheat] => 
)

Patches

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2007-02-23 19:01 UTC] Webbed dot Pete at gmail dot com
Even simpler:
 $x=&$aEmpty['wheat'];

Adds element 'wheat' to the array.

This creates a nasty side effect for a couple of valuable and common functions, which basically extend isset() for default values and so forth:

function varset(&$val,$default='') {
	if (isset($val)) return $val;
	return $default;
}
function varsettrue(&$val,$default='') {
	if (isset($val) && $val) return $val;
	return $default;
}

$myVal = varset($pref['maxsize'],1000); // set myVal to pref or default

NOTE: all of the following leave $aEmpty alone. I understand why this might be the case, yet it still is wrong to break references IMHO, not least because of losing ability to create functions like those above.
  $aEmpty['wheat']; // simple reference
  isset($aEmpty['wheat']); // built-in function
  myFunc($aEmpty['wheat']); // pass-by-value to user func
 [2007-02-23 20:13 UTC] tony2001@php.net
Thank you for taking the time to write to us, but this is not
a bug. Please double-check the documentation available at
http://www.php.net/manual/ and the instructions on how to report
a bug at http://bugs.php.net/how-to-report.php


 [2007-02-23 22:47 UTC] Webbed dot Pete at gmail dot com
I have double and triple checked the documentation...
http://us3.php.net/manual/en/language.references.whatdo.php
...tells us that
<<<PHP references allow you to make two variables to refer to the same content. Meaning, when you do:
<?php
$a =& $b;
?>
it means that $a and $b point to the same content. Note: $a and $b are completely equal here, that's not $a is pointing to $b or vice versa, that's $a and $b pointing to the same place.>>>

This works as far as it goes. What's left unsaid is that if $b is a missing array key, the unset key will be created and the $b array will grow. 

Neither 'array' nor 'references' documentation specify this behavior. They don't say whether or not array elements are created willy-nilly when an array reference exists.

I find nothing in the reference manual that either specifies nor suggests this behavior is intentional. I do find several user comments about this bug. It's just never been reported as a bug before.

The impact is that we cannot trust the keys of an array.

If this is declared as missing documentation, we will have to live with the bug.

I'd prefer to see it eventually fixed; php will be better for it.
 [2007-02-23 23:38 UTC] tony2001@php.net
Reclassified a docu problem.
When you pass a non-existent variable by reference of course it HAS to be created, or what do you think should be referenced?
 [2007-02-24 00:38 UTC] Webbed dot Pete at gmail dot com
<<<When you pass a non-existent variable by reference of course it HAS to be created, or what do you think should be referenced?>>>

Funny thing is, passing a reference to a non-existent normal variable works fine. Only array elements require something to be created.

If we accept this as a requirement, I believe we arrive at the following set of conclusions:

What we're declarng here is that
a) isset() is not identical to "is created".
b) A php app has no way to discover if a variable is created.
c) Array elements must be treated distinctly from other variables whenever the number of created keys is important.
d) For array elements, key references must be carried separately from value references.

Thus, to accomplish the equivalent of "isset() with default," without causing side effects, can still be accomplished with some pain. 

It requires separate functions for variables, constants and array elements. I believe the following patterns would be correct:

function varset(&$val,$default='') {
	if (isset($val)) return $val;
	return $default;
}

function defset($str,$default='') {
	if (defined($str)) return constant($str);
	return $default;
}

function arrset(&$arr,$key,$default='') {
	if ( isset($arr) && is_array($arr) && array_key_exists($key,$arr) && isset($arr[$key]) ) return $val;
	return $default;
}

Corrections welcome.

Thanks!
 [2007-02-24 03:20 UTC] Webbed dot Pete at gmail dot com
Typo on function arrset(). Instead of
    ... return $val;
it should be
    ... return $arr[$key];
 [2007-02-24 09:16 UTC] colder@php.net
function foo(&$a) {}
foo($a); // will "create" $a
$b = new stdclass;
foo($b->c); // will "create" the public property "c" 
$d = array();
foo($d['index']); // Will "create" the array index

In each case, it will be assigned to null. Calling isset will return false because isset() returns false on variables assigned to NULL. However, there are other functions like array_key_exist() or property_exist() that are able to detect the NULL value.

Conclusion: there is simply no solution to effectively emulate the isset() construct with default value using an user land function.


 [2007-02-24 09:54 UTC] Webbed dot Pete at gmail dot com
If we require a single function, to *fully* emulate 'isset with default' (with only two parameters), colder@php.net is correct.

A compromise is currently required involving separate functions or at least separate parameters that isolate potential array keys or object property names.

The above emulations are effective userland emulations.

Further emulations are also helpful, such as emulation of '(isset AND true) with default', which was noted in an earlier comment. This too can be emulated through a set of functions. 

It gets messy to do these workarounds, but the resulting application code is much cleaner.

Instead of coding

$x = (isset($var) && $var) ? $var : $default;
$y = (isset($pref['key']) && $pref['key']) ? $pref['key'] : $default;

one can code

$x = varsettrue($var, $default);
$y = arrsettrue($pref,'key',$default);

by creating function varsettrue() as noted above, and now

function arrsettrue($arr,$key,$default='') {
	if ( isset($arr) && is_array($arr) && array_key_exists($key,$arr) &&
isset($arr[$key]) && $arr[$key] ) return $arr[$key];
	return $default;
}

The 'mess' is encapsulated in a set of userland functions. This frees coders to write well-protected code that never generates notices.

Perhaps someday there can be built-in functions that accomplish all this, but for now at least we know there are effective (if slow/messy) workarounds. I now believe a single userland function may be possible; will think on that.
 [2007-02-24 11:00 UTC] Webbed dot Pete at gmail dot com
Yes, a single userland function can take care of all but constants. Here is a robust workaround (if messy and/or slow) for the undocumented effect described in this bug report (i.e. that PHP creates array elements and object properties when pass-by-reference is used).

// For PHP4 compatibility...
if (!function_exists('property_exists')) {
  function property_exists($obj, $property) {
   return array_key_exists($property, get_object_vars($obj));
  }
}

// Pass potentially undefined array keys and object property names separately else they get auto-created
function varset(&$var,$param,$default='') {
	if (!isset($var))    return $default;
	if (is_scalar($var)) return $var;
	if (is_array($var))  return (array_key_exists($param,$var) && isset($var[$param])) ? $var[$param] : $default;
	if (is_object($var)) return (property_exists($var,$param) && isset($var->$param)) ? $var->$param : $default;
	die('varset used on NULL or resource');
}

Notes:
1) I'll leave the argument to others over whether it is better to have a single function when scalars have no need for the second parameter. :)

2) varsettrue() is not replicated here; it simply adds "&& $var", "&& $var[$param]" or "&& $var->$param" to the end of each appropriate test.

3) This example highlights a small inconsistency between array_key_exists() and property_exists().
 [2007-02-24 11:35 UTC] colder@php.net
This bug has been fixed in the documentation's XML sources. Since the
online and downloadable versions of the documentation need some time
to get updated, we would like to ask you to be a bit patient.

Thank you for the report, and for helping us make our documentation better.

isset() is a construct that doesn't affect in any way the variable passed. So no, you can't fully emulate it.

Undefined variables will get created, which will hide potentially important "Undefined *" notices.
 [2020-02-07 06:10 UTC] phpdocbot@php.net
Automatic comment on behalf of colder
Revision: http://git.php.net/?p=doc/en.git;a=commit;h=04091afcbe97046b667b182073f5f5a2ff236f10
Log: Fix #40610 (Undefined variables get defined when used with references)
 
PHP Copyright © 2001-2025 The PHP Group
All rights reserved.
Last updated: Fri Jul 04 13:01:35 2025 UTC