Access array element indexed by numerical string - php

I have encountered something odd.
I have a php array, indexed with numerical keys.
However it appears impossible to access any of the elements because php automatically treats numerical strings as integers, causing an illegal offset notice.
Under normal circumstances its imposable to create a php array with numerical string indexes, but it can happen with type casting.
To reproduce:
$object = new stdClass();
$object->{'1'} = 'one';
$array = (array) $object;
var_dump($array);
/* produces
array(1) {
["1"]=>
string(3) "one"
}
*/
//none of the following will work
$key = '1';
echo $array[1], $array['1'], $array["1"], $array[(string)1], $array[$key];
Is this just an edge case bug? I only encountered the problem when attempting to improve my answer for another SO question
Live code example: http://codepad.viper-7.com/dFSlH1

Unbelievable but this is normal behavior in php, it was considered as a bug (link) in the year 2008.
But they just pointed out to the manual for the cast with (array):
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;
You can use get_object_vars() instead:
$object = new stdClass();
$object->{'1'} = 'one';
$array = get_object_vars( $object );
$key = '1';
echo $array[1]."<br>";
echo $array['1']."<br>";
echo $array["1"]."<br>";
echo $array[(string)1]."<br>";
echo $array[$key]."<br>";
Doesn't explain why this happens, but is a solution to avoid the cast problem.
Off topic but I thought maybe it is interesting. Found this in the manual.
To avoid these kind of problems, always use an integer OR a string as index, don't mix it up and don't use integers in a string.
Example of mixed array:
$array = array(
1 => "a",
"1" => "b",//overrides 1
1.5 => "c",//overrides "1"
true => "d",//overrides 1.5
);
var_dump($array);

You can use
$vars = get_object_vars($object);
echo $vars[1];

String keys containing valid integer values would be cast to integer keys automatically in “normal” array creation – but it seems casting from object to array doesn’t apply the same logic.
It can be fixed however, by using
$array = array_combine(array_keys($array), array_values($array));
after your line that creates the array from the object. http://codepad.viper-7.com/v5rGJa
Although, same as Dave already said in his comment, using get_object_vars looks like a “cleaner” solution to me as well.

foreach ($array as $key => $value){
var_dump($key);
var_dump($value);
}
shows
string(1) "1"
string(3) "one"
But echo $array['"1"']; gives
E_NOTICE : type 8 -- Undefined index: "1" -- at line 8
That's strange!

Related

foreach loop scope with multidimensional arrays

Can someone explain the output of this code?
Why is it "fb" instead of "100100"?
$items = array();
$items[] = "foo";
$items[] = "bar";
foreach($items as $item) {
$item['points'] = 100;
}
foreach($items as $item) {
echo $item['points']; //output: "fb"
}
You loop though the $items array, which has two elements.
First: foo and second: bar. E.g.
Array (
[0] => foo
[1] => bar
)
Now you access them like this:
echo $item['points'];
PHP will convert points which is a string into an integer, as you can see from the manual warning:
Warning: [...] Non-integer types are converted to integer. [...]
Which in your case will be 0.
And so you access the two values (strings) as array:
string: f o o
index: 0 1 2 //$index["points"] -> $index[0]
string: b a r
index: 0 1 2 //$index["points"] -> $index[0]
So you print the first character of both strings (e.g. foo and bar), which are:
fb
EDIT:
Also worth to note here is, that PHP only silently converts it with PHP <5.4 from newer version you will get a warning, as from the manual:
As of PHP 5.4 string offsets have to either be integers or integer-like strings, otherwise a warning will be thrown. Previously an offset like "foo" was silently cast to 0.
Which in your case with PHP >=5.4 you would get:
Warning: Illegal string offset 'points' ...
I found this question intriguing.
I had my own walk-through and here is the result.
$items is defined as follows.
$items = [
0 => "foo",
1 => "bar"
];
Then, goes into the foreach loop.
foreach($items as $item) {
$item['points'] = 100;
}
At the beginning, $item contains a string "foo". The [] syntax is dominantly used for associative arrays, so it tricks us that $item might be an array, which is not the case. A less well-known usage of the [] is to get/set a single character in a string via [int] or {int} expression, as #Rizier123 has noted in his answer. For example, a "string"[0] gives "s". So, the following code
$item['points'] = 100;
is virtually similar to
"foo"['points'] = 100;
Now, a non-integer value given as a character position of a string, raises a PHP warning, and the position (here 'points') will be force-converted to an integer.
// Converting a string to integer:
echo intval('points'); // gives 0
As a result, the "foo"['points']" statement becomes "foo"[0], so
"foo"[0] = 100;
Now, the assignment part. The [] syntax operates on a single character. The numeric 100 is first converted to a string "100" and then only the first character is taken out for the assignment operation(=). The expression is now similar to
"foo"[0] = "1"; // result: "1oo"
To make things a bit twisted, the modified value of $item( which is "1oo") is not preserved. It's because the $item is not a reference. See https://stackoverflow.com/a/9920684/760211 for more information.
So, all the previous operations are negligible in terms of the end result. The $items are intact in the original state.
Now, in the last loop, we can see that the $item['point'] statement tries to read a character out of a string, in an erroneous way.
foreach($items as $item) {
echo $item['points']; //output: "fb"
}
echo "foo"[0]; // "f"
echo "boo"[0]; // "b"
You're not actually modifying the array by doing $items as $item. $item is its own variable, so it would make sense that you get the correct output when printing within that loop.

Undesired reordering of keys in PHP array

I have a quick question. When building an associative array key casting rules mean that strings containing valid integers will be cast to the integer type. E.g. the key "8" will actually be stored under 8. (On the other hand "08" will not be cast, as it isn't a valid decimal integer.) See for example: http://php.net/manual/en/language.types.array.php
The problem I have is that my keys are mixed integer and string .. meaning that when the associative array is built, all keys are reordered with numerical keys appear first before string. This is a sample of what I get in my console log:
...
2032: "9371.84"
2033: "9351.60"
2034: "9331.36"
2035: "9311.12"
ID: "1"
Misc1: "Russian Federation - Conventional"
Misc2: "RUS.Con1"
Misc3: "4"
Misc4: ""
... etc.
How can I avoid this issue, so that the associative array does not re-order my keys?
As an FYI, this is how I generate my array in PHP:
while ($array = mysqli_fetch_assoc($result)) {
$experiment[] = $array;
};
Thank you for your time,
G.
Adding an index to an array in PHP like this:
$array[] = ['another array'];
Will increment the indexes.
You can however specify a string for the key, or cast the integers to strings.
The workaround for the issue I have found is to avoid the associative array structure. This is what my loop looks like now:
while ($array = mysqli_fetch_assoc($result)) {
$experiment[0] = array_keys($array);
$experiment[] = array_values($array);
};
The annoying thing is that $experiment[0] = array_keys($array); gets looped uneccessarily... but at least I get the result that I am looking for and the keys are not cast and re-ordered by the associative array.
If anybody knows how to avoid the unecessary looping for $experiment[0], then please let me know :-)

Determine whether an array is associative (hash) or not [duplicate]

This question already has answers here:
How to check if PHP array is associative or sequential?
(60 answers)
Closed last year.
I'd like to be able to pass an array to a function and have the function behave differently depending on whether it's a "list" style array or a "hash" style array. E.g.:
myfunc(array("One", "Two", "Three")); // works
myfunc(array(1=>"One", 2=>"Two", 3=>"Three")); also works, but understands it's a hash
Might output something like:
One, Two, Three
1=One, 2=Two, 3=Three
ie: the function does something differently when it "detects" it's being passed a hash rather than an array. Can you tell I'm coming from a Perl background where %hashes are different references from #arrays?
I believe my example is significant because we can't just test to see whether the key is numeric, because you could very well be using numeric keys in your hash.
I'm specifically looking to avoid having to use the messier construct of myfunc(array(array(1=>"One"), array(2=>"Two"), array(3=>"Three")))
Pulled right out of the kohana framework.
public static function is_assoc(array $array)
{
// Keys of the array
$keys = array_keys($array);
// If the array keys of the keys match the keys, then the array must
// not be associative (e.g. the keys array looked like {0:0, 1:1...}).
return array_keys($keys) !== $keys;
}
This benchmark gives 3 methods.
Here's a summary, sorted from fastest to slowest. For more informations, read the complete benchmark here.
1. Using array_values()
function($array) {
return (array_values($array) !== $array);
}
2. Using array_keys()
function($array){
$array = array_keys($array); return ($array !== array_keys($array));
}
3. Using array_filter()
function($array){
return count(array_filter(array_keys($array), 'is_string')) > 0;
}
PHP treats all arrays as hashes, technically, so there is not an exact way to do this. Your best bet would be the following I believe:
if (array_keys($array) === range(0, count($array) - 1)) {
//it is a hash
}
No, PHP does not differentiate arrays where the keys are numeric strings from the arrays where the keys are integers in cases like the following:
$a = array("0"=>'a', "1"=>'b', "2"=>'c');
$b = array(0=>'a', 1=>'b', 2=>'c');
var_dump(array_keys($a), array_keys($b));
It outputs:
array(3) {
[0]=> int(0) [1]=> int(1) [2]=> int(2)
}
array(3) {
[0]=> int(0) [1]=> int(1) [2]=> int(2)
}
(above formatted for readability)
My solution is to get keys of an array like below and check that if the key is not integer:
private function is_hash($array) {
foreach($array as $key => $value) {
return ! is_int($key);
}
return false;
}
It is wrong to get array_keys of a hash array like below:
array_keys(array(
"abc" => "gfb",
"bdc" => "dbc"
)
);
will output:
array(
0 => "abc",
1 => "bdc"
)
So, it is not a good idea to compare it with a range of numbers as mentioned in top rated answer. It will always say that it is a hash array if you try to compare keys with a range.
Being a little frustrated, trying to write a function to address all combinations, an idea clicked in my mind: parse json_encode result.
When a json string contains a curly brace, then it must contain an object!
Of course, after reading the solutions here, mine is a bit funny...
Anyway, I want to share it with the community, just to present an attempt to solve the problem from another prospective (more "visual").
function isAssociative(array $arr): bool
{
// consider empty, and [0, 1, 2, ...] sequential
if(empty($arr) || array_is_list($arr)) {
return false;
}
// first scenario:
// [ 1 => [*any*] ]
// [ 'a' => [*any*] ]
foreach ($arr as $key => $value) {
if(is_array($value)) {
return true;
}
}
// second scenario: read the json string
$jsonNest = json_encode($arr, JSON_THROW_ON_ERROR);
return str_contains($jsonNest, '{'); // {} assoc, [] sequential
}
NOTES
php#8.1 is required, check out the gist on github containing the unit test of this method + Polyfills (php>=7.3).
I've tested also Hussard's posted solutions, A & B are passing all tests, C fails to recognize: {"1":0,"2":1}.
BENCHMARKS
Here json parsing is ~200 ms behind B, but still 1.7 seconds faster than solution C!
What do you think about this version? Improvements are welcome!

How can I force PHP to use strings for array keys? [duplicate]

This question already has answers here:
A numeric string as array key in PHP
(11 answers)
Closed 2 years ago.
The community reviewed whether to reopen this question 1 year ago and left it closed:
Original close reason(s) were not resolved
I've come across an old app that uses an id to name type array, for example...
array(1) {
[280]=>
string(3) "abc"
}
Now I need to reorder these, and a var_dump() would make it appear that that isn't going to happen while the keys are integers.
If I add an a to every index, var_dump() will show double quotes around the key, my guess to show it is now a string...
array(1) {
["280a"]=>
string(3) "abc"
}
This would let me easily reorder them, without having to touch more code.
This does not work.
$newArray = array();
foreach($array as $key => $value) {
$newArray[(string) $key] = $value;
}
A var_dump() still shows them as integer array indexes.
Is there a way to force the keys to be strings, so I can reorder them without ruining the array?
YOU CAN'T!!
Strings containing valid integers will be cast to the integer type. E.g. the key "8" will actually be stored under 8. On the other hand "08" will not be cast, as it isn't a valid decimal integer.
Edit:
ACTUALLY YOU CAN!!
Cast sequential array to associative array
$obj = new stdClass;
foreach($array as $key => $value){
$obj->{$key} = $value;
}
$array = (array) $obj;
In most cases, the following quote is true:
Strings containing valid integers will be cast to the integer type. E.g. the key "8" will actually be stored under 8. On the other hand "08" will not be cast, as it isn't a valid decimal integer.
This examples from the PHP Docs
<?php
$array = array(
1 => "a",
"1" => "b",
1.5 => "c",
true => "d",
);
var_dump($array);
?>
The above example will output:
array(1) {
[1]=> string(1) "d"
}
So even if you were to create an array with numbered keys they would just get casted back to integers.
Unfortunately for me I was not aware of this until recently but I thought I would share my failed attempts.
Failed attempts
$arr = array_​change_​key_​case($arr); // worth a try.
Returns an array with all keys from array lowercased or uppercased. Numbered indices are left as is.
My next attempts was to create a new array by array_combineing the old values the new (string)keys.
I tried several ways of making the $keys array contain numeric values of type string.
range("A", "Z" ) works for the alphabet so I though I would try it with a numeric string.
$keys = range("0", (string) count($arr) ); // integers
This resulted in an array full of keys but were all of int type.
Here's a couple of successful attempts of creating an array with the values of type string.
$keys = explode(',', implode(",", array_keys($arr))); // values strings
$keys = array_map('strval', array_keys($arr)); // values strings
Now just to combine the two.
$arr = array_combine( $keys, $arr);
This is when I discovered numeric strings are casted to integers.
$arr = array_combine( $keys, $arr); // int strings
//assert($arr === array_values($arr)) // true.
The only way to change the keys to strings and maintain their literal values would be to prefix the key with a suffix it with a decimal point "00","01","02" or "0.","1.","2.".
You can achieve this like so.
$keys = explode(',', implode(".,", array_keys($arr)) . '.'); // added decimal point
$arr = array_combine($keys, $arr);
Of course this is less than ideal as you will need to target array elements like this.
$arr["280."]
I've created a little function which will target the correct array element even if you only enter the integer and not the new string.
function array_value($array, $key){
if(array_key_exists($key, $array)){
return $array[ $key ];
}
if(is_numeric($key) && array_key_exists('.' . $key, $array)){
return $array[ '.' . $key ];
}
return null;
}
Usage
echo array_value($array, "208"); // "abc"
Edit:
ACTUALLY YOU CAN!!
Cast sequential array to associative array
All that for nothing
You can append the null character "\0" to the end of the array key. This makes it so PHP can't interpret the string as an integer. All of the array functions (like array_merge()) work on it. Also not even var_dump() will show anything extra after the string of integers.
Example:
$numbers1 = array();
$numbers2 = array();
$numbers = array();
$pool1 = array(111, 222, 333, 444);
$pool2 = array(555, 666, 777, 888);
foreach($pool1 as $p1)
{
$numbers1[$p1 . "\0"] = $p1;
}
foreach($pool2 as $p2)
{
$numbers2[$p2 . "\0"] = $p2;
}
$numbers = array_merge($numbers1, $numbers2);
var_dump($numbers);
The resulting output will be:
array(8) {
["111"] => string(3) "111"
["222"] => string(3) "222"
["333"] => string(3) "333"
["444"] => string(3) "444"
["555"] => string(3) "555"
["666"] => string(3) "666"
["777"] => string(3) "777"
["888"] => string(3) "888"
}
Without the . "\0" part the resulting array would be:
array(8) {
[0] => string(3) "111"
[1] => string(3) "222"
[2] => string(3) "333"
[3] => string(3) "444"
[4] => string(3) "555"
[5] => string(3) "666"
[6] => string(3) "777"
[7] => string(3) "888"
}
Also ksort() will also ignore the null character meaning $numbers[111] and $numbers["111\0"] will both have the same weight in the sorting algorithm.
The only downside to this method is that to access, for example $numbers["444"], you would actually have to access it via $numbers["444\0"] and since not even var_dump() will show you there's a null character at the end, there's no clue as to why you get "Undefined offset". So only use this hack if iterating via a foreach() or whoever ends up maintaining your code will hate you.
Use an object instead of an array $object = (object)$array;
EDIT:
I assumed that if they are integers, I
can't reorder them without changing
the key (which is significant in this
example). However, if they were
strings, I can reorder them how they
like as the index shouldn't be
interpreted to have any special
meaning. Anyway, see my question
update for how I did it (I went down a
different route).
Actually they dont have to be in numeric order...
array(208=>'a', 0=> 'b', 99=>'c');
Is perfectly valid if youre assigning them manually. Though i agree the integer keys might be misinterpreted as having a sequential meaning by someone although you would think if they were in a non-numeric order it would be evident they werent. That said i think since you had the leeway to change the code as you updated that is the better approach.
Probably not the most efficient way but easy as pie:
$keys = array_keys($data);
$values = array_values($data);
$stringKeys = array_map('strval', $keys);
$data = array_combine($stringKeys, $values);
//sort your data
I was able to get this to work by adding '.0' onto the end of each key, as such:
$options = [];
for ($i = 1; $i <= 4; $i++) {
$options[$i.'.0'] = $i;
}
Will return:
array("1.0" => 1, "2.0" => 2, "3.0" => 3, "4.0" => 4)
It may not be completely optimal but it does allow you to sort the array and extract (an equivalent of) the original key without having to truncate anything.
Edit:
This should work
foreach($array as $key => $value) {
$newkey = sprintf('%s',$key);
$newArray["'$newkey'"] = $value;
}
Hi we can make the index of the array a string using the following way. If we convert an array to xml then indexes like [0] may create issue so convert to string like [sample_0]
$newArray = array();
foreach($array as $key => $value) {
$newArray["sample_".$key] = $value;
}
All other answers thus far are hacks that either use fragile workarounds that could break between major PHP versions, create unnecessary gotchas by deliberately corrupting keys, or just slow down your code for no benefit. The various functions to sort arrays yet maintain the crucial key associations have existed since PHP 4.
It is pointless stop PHP from using integer keys, it only does so when the integer representation is exactly the same as the string, thus casting an integer key back to string when reading from the array is guaranteed to return the original data. PHP's internal representation of your data is completely irrelevant as long as you avoid the functions that rewrite integer keys. The docs clearly state which array functions will do that.
An example of sorting, without any hacks, that demonstrates how data remains uncorrupted:
<?php
# use string keys to define as populating from a db, etc. would,
# even though PHP will convert the keys to integers
$in = array(
'347' => 'ghi',
'176' => 'def',
'280' => 'abc',
);
# sort by key
ksort($in);
echo "K:\n";
$i = 1;
foreach ($in as $k => $v) {
echo $i++, "\n";
$k = (string) $k; # convert back to original
var_dump($k, $v);
}
# sort by value
asort($in, SORT_STRING);
echo "\nV:\n";
$i = 1;
foreach ($in as $k => $v) {
echo $i++, "\n";
$k = (string) $k;
var_dump($k, $v);
}
# unnecessary to cast as object unless keys could be sequential, gapless, and start with 0
if (function_exists('json_encode')) {
echo "\nJSON:\n", json_encode($in);
}
The output it produces hasn't changed since v5.2 (with only the JSON missing prior to that):
K:
1
string(3) "176"
string(3) "def"
2
string(3) "280"
string(3) "abc"
3
string(3) "347"
string(3) "ghi"
V:
1
string(3) "280"
string(3) "abc"
2
string(3) "176"
string(3) "def"
3
string(3) "347"
string(3) "ghi"
JSON:
{"280":"abc","176":"def","347":"ghi"}

Decode sparse json object to php array

I can create a sparse php array (or map) using the command:
$myarray = array(10=>'hi','test20'=>'howdy');
I want to serialize/deserialize this as JSON. I can serialize it using the command:
$json = json_encode($myarray);
which results in the string {"10":"hi","test20":"howdy"}. However, when I deserialize this and cast it to an array using the command:
$mynewarray = (array)json_decode($json);
I seem to lose any mappings with keys which were not valid php identifiers. That is, mynewarray has mapping 'test20'=>'howdy', but not 10=>'hi' nor '10'=>'hi'.
Is there a way to preserve the numerical keys in a php map when converting to and back from json using the standard json_encode / json_decode functions?
(I am using PHP Version 5.2.10-2ubuntu6.4.)
json_decode returns an object of type stdClass by default. You access members as properties (i.e., $result->test20). 10 isn't a valid name for a property, which is why you're losing it.
Instead of casting to an array, you can pass true as a second argument to json_decode to make it return an associative array itself:
$mynewarray = json_decode($json, true);
If you do that, $mynewarray[10] will work fine.
What version of PHP? On 5.2 the following program/script
$myarray = array(10=>'hi','test20'=>'howdy');
$json = json_encode($myarray);
$mynewarray = (array) json_decode($json);
var_dump($mynewarray);
Outputs
array(2) {
["10"]=>
string(2) "hi"
["test20"]=>
string(5) "howdy"
}
Which doesn't display the behavior you're describing.
That said, if your version of PHP is miscasting the JSON, try using get_object_vars on the stdClass object that json_decode returns
get_object_vars(json_decode($json))
That might return better results.
The problem is in the conversion from object to array.
$a = (array)json_decode('{"10":"hi","test20":"howdy"}');
var_dump($a);
//outputs
array(2) {
["10"]=>
string(2) "hi"
["test20"]=>
string(5) "howdy"
}
See how this array have index "10"? But in PHP, everything that looks like a number gets converted into a number, especially in array indexes. You can't just get a["10"] because it converts "10" into a number and this array does not have such an index.
However, foreach works.
foreach ($a as $key => $value) {
var_dump($key);
var_dump($value);
}
//outputs
string(2) "10"
string(2) "hi"
string(6) "test20"
string(5) "howdy"
You can also treat result of json_decode as an object. While you won't be able to do $a->10 or $a->"10",
$a = json_decode('{"10":"hi","test20":"howdy"}');
$b = 10;
var_dump($a->$b);
//outputs
string(2) "hi"
works.
But most likely, as Chris said, you just want to pass true as a second argument.
$a = json_decode('{"10":"hi","test20":"howdy"}', true);
var_dump($a[10]);
//outputs
string(2) "hi"

Categories