Illuminating a Dark Alley

15 10 2014

Flickr: by I.Gouss

 

“PHP has its share of dark alleys that you really don’t want to find yourself inside.   Object properties with names that are numbers is one of them…”

Jon,  StackOverflow

 

Object properties with integers for names are illegal in PHP.  Yet, if one is unaware of the potential repercussions of casting an array to an object, you could end up with such an odd object.   Consider an array with a key whose name is a numeric string that becomes cast to an object, as in the following example:

<?php
$obj = (object) array('123' => 456);
print_r($obj);
echo $obj->{'123'};

/** Result:
stdClass Object (
 [123] => 456
)
Notice: Undefined property: stdClass::$123 ...
**/

Casting the array to an object leads to the miserable result of the inaccessibility of its sole property, hidden for all PHP-related intents and purposes, exclusive of var_dump() and print_r(). Depending on one’s humor, the issue may infuriate or amuse. How does this sort of situation even arise? Generally, if a developer scrupulously heeds all the rules, PHP reciprocates by protecting the developer from falling into traps like this one.

A slightly different example provides more disturbing information:

<?php
$obj = (object) array('123' => 456);
if (!property_exists($obj,'123')) {
 echo 'property \'123\' does not exist',"\n";
}
if ( !isset( $obj->{'123'} ) ) {
 echo 'property \'123\' is null',"\n";</pre>
<pre>}

/** Result:
property '123' does not exist
property '123' is null
**/
?>

While investigating this matter, I recalled that when an array’s key holds a numeric string, PHP automatically converts it to an integer. Nikita Popov kindly provides the details in Understanding PHP’s Internal Array Implementation. Apparently, the conversion occurs when zend_symtable_update()  stores the numeric key ‘123’ as an integer in a special hashtable called a Symtable. Nikita notes that using integers for array keys makes sense because they “… are smaller and faster than strings.”

Generally, the conversion goes without a hitch. After all, in the first example, print_r() reports that the object has a numeric property 123 set to 456. However, therein lies a conundrum. Function property_exists() only conceives of property names as  strings, so it searches Symtable in vain for a string bearing the name “123”.  After failing to locate it, the function reports  the property’s non-existence which of course confers upon it a NULL status as well.

The only way to access the value attached to the hidden, numbered property is to cast the object back to an array, as follows:

<?php
$obj = (object)array('123' => 456);
$arr = (array) $obj;
echo $arr[123],"\n"; // 456
?>

 

There is a bright spot involving  arrays containing numeric keys with one or more alphabetical characters.  These keys  avoid turning up as hidden properties when cast to objects because they fail to qualify as legitimate numbers.  PHP stores the array keys  in the aforementioned hashtable as strings. So, the following code yields the expected result:

$obj = (object)array('123a' => '456');
var_dump($obj);
if ( isset($obj->{'123a'})) {
 echo $obj->{'123a'},"\n";
}

/** Result:
object(stdClass)#1 (1) {
 ["123a"]=>
 string(3) "456"
}
456
**/
?>

As an aside, if you wished for some unfathomable reason  to intentionally hide key  ‘123a’,  you need only supply some numeric context,  as follows:

$obj = (object)array( ('123a' + 0) => '456');
var_dump($obj);

/** Result:
object(stdClass)#1 (1) {
  [123]=>
  string(3) "456"
}
**/

 

PHP also allows for hidden array elements to occur as shown in the following snippet:

<?php
$obj = new stdClass;
$obj->{'123'} = "foo";
$arr = (array) $obj;
print_r($arr);
echo $arr['123']; // PHP4 only: "foo"

/** Result:
Array
(
 [123] => foo
)

Notice: Undefined offset: 123
**/

Interestingly, in PHP4 $arr[“123”] is a valid, visible element whereas in PHP5 and successive versions the element is hidden; an error message complains about an undefined index. The only way to access value “foo” is to recast the array back to an object.

This matter especially becomes an issue in connection to PHP and JSON, as evidenced by the following bug reports: #51635, #46758 and #67640.  The reports all point to the problem of hidden object properties after an array cast and yet reviewers of the first two bug reports  dismiss them as bogus, while the last bug report is still “Open.”  The next example vividly illustrates the problem:

<?php
$json = '{ "0" : "test1" }';
$obj = json_decode($json);
var_dump($obj);
$array = (array) $obj;
var_dump($array);
echo $array['0'], ' ', $array[0];

/** Result:
object(stdClass)#1 (1) {
 ["0"]=>
 string(5) "test1"
}
array(1) {
 ["0"]=>
 string(5) "test1"
}

Notice: Undefined offset: 0 ...

Notice: Undefined offset: 0 ...
**/

Var_dump gives the misleading impression that the numeric string zero exists as a viable key in $array.  The user, of course, can rectify the matter by recasting $array back to an object or better yet set the json_decode ASSOC parameter to true.  In the latter case, the array’s element becomes easily accessible; see http://3v4l.org/N4W3n.

One member of the PHP Internals List suggested that PHP needs some kind of remedy to avoid  hidden properties and elements. But, other members were less than enthusiastic, concerned that a solution involving type checks would necessarily impair performance. One comment in particular sums up the prevailing attitude:

“Let’s say the behavior is here ‘by design’ ;-)”

The quip deftly glosses over an issue whose impact on the majority of PHP developers is largely unknown.  The prolific Drupal contributor “chx” documented the problem back in 2009 at this website.  Surprisingly, Eevee in his exhaustive critique bashing PHP  in 2012 neglects to mention this particular failing of PHP.   An oversight perhaps, since the aforementioned three bug reports plus  bug report #45346  as well as  two StackOverflow discussions (see “Recommended Reading” below)  point to some level of Userland dissatisfaction with the official PHP position that merely acknowledges the problem without resolving it:

If an object is converted to an array, the result is an array whose elements are the object‘s properties. The keys are the member variable names, with a few notable exceptions: integer properties are unaccessible…

                                                                                                                                                                        The Manual

Some argue that accepting the trade-off of living with the status quo in this case in exchange for optimal performance  makes sense, especially since those of us who dwell in Userland have recourse to ameliorating the untoward outcome of a hidden property or element by recasting it back to its original form.  In all fairness, offloading this internal PHP problem to Userland for handling seems a little harsh.  If the issue remains without a satisfactory resolution, then minimally, the official PHP documentation needs to inform users about this matter and advise them how to manage it, displaying the information prominently.

If this is an issue that matters to you, please respond to the following poll:

Recommended Reading

StackOverflow: how-to-access-object-properties-with-names-like-integers

StackOverflow: php-cast-to-array-and-return-type-array-is-not-the-same

Nikita Popov: Understanding-PHPs-internal-array-implementation

 

This work is licensed under a Creative Commons License


Actions

Information

One response

21 02 2015

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.