69

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?

alex
  • 479,566
  • 201
  • 878
  • 984
  • Had the same problem. I got keys like "0", "1" in the input data (from http request) and wanted to filter it by using `array_intersect_key`... looks like I'll use another approach. – kirilloid Sep 27 '12 at 09:43
  • @Flimm That question was asked after mine, so it's a duplicate of this one. – alex Apr 19 '16 at 12:31
  • If you want to force the keys to strings, you can, but you can't access them ;) https://nikic.github.io/2012/03/28/Understanding-PHPs-internal-array-implementation.html#the-symtable – Brian Leishman Oct 12 '17 at 18:44
  • 1
    Future readers, please don't try this, it will only cause you pain. There have always been ways to workaround this oddity of PHP by using functions & operators that preserve keys, or just iterating. Cast your keys back to strings when you read from arrays, if you need to. – Walf Feb 24 '21 at 12:53

8 Answers8

32

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

alexkorn
  • 315
  • 2
  • 6
TarranJones
  • 4,084
  • 2
  • 38
  • 55
  • 3
    Still valid in 2021 with PHP 8. It's a real bad behavior on PHP side. Your solution might work, it's more abusing an error in PHPs inconsistency but the performance impact is likely significant. – John Apr 05 '21 at 23:25
  • @John Why is it bad behaviour? The bad behaviour is trying to store an integer as string in the key of an array. – Daniel W. Dec 21 '21 at 14:51
  • 4
    @DanielW. "208" is a string, not an integer! 208 is an integer There is nothing "bad" about using a number as an associative array key. This only becomes bad if the high level language converts that numeric string into an integer internally and now accesses the wrong element. It is inconsistent and shows lack of professionalism from the time when php fundamentals were implemented. Due to how PHP aged it likely became difficult to change that behaviour without side effects. The worst kind of security vulnerabilities are born from such implementations. – John Dec 24 '21 at 02:34
  • 1
    2023 and php still does those insane conversions, thanks for your answer – John Feb 11 '23 at 04:30
12

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.

Tim Aagaard
  • 518
  • 5
  • 10
11

Use an object instead of an array $object = (object)$array;

Paul-Emile MINY
  • 918
  • 1
  • 8
  • 6
  • @ÁlvaroGonzález you can iterate any object just like an array, no? It will go through the properties available in the calling scope. – nawfal Nov 22 '15 at 10:49
  • @nawfal - You are right, you get all public properties automatically. I wasn't aware of that feature. I'll remove my comment to avoid spreading confusion. Thank you for the pointer. – Álvaro González Nov 23 '15 at 08:43
9

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
Community
  • 1
  • 1
prodigitalson
  • 60,050
  • 10
  • 100
  • 114
  • `array_map` arguments are around the wrong way. Trying it out now, thanks. – alex Aug 10 '10 at 04:27
  • Sorry, I didn't mention it, but this is on a PHP4 project so no `array_combine()`. I'll just iterate through and build it. – alex Aug 10 '10 at 04:28
  • 1
    Bummer, didn't work. They seem to get cast back to integer. I even tried using the cast operator `(string)`. It is not until I add a non integer that they turn to string. – alex Aug 10 '10 at 04:32
  • Sorry about the argument order faux pas. – prodigitalson Aug 10 '10 at 05:00
  • Thanks for the update, I didn't realise I could reorder a numeric keyed array. – alex Aug 10 '10 at 06:07
  • No problem... I wish i would have mentioned it sooner.. i might have saved you some time :-) – prodigitalson Aug 10 '10 at 06:26
  • 2
    I know it's old, but very annoying issue. Looks that PHP would convert "numeric strings" (?integer strings i suppose) into integers always when creating a key for an array. Here's the bug report: https://bugs.php.net/bug.php?id=45348&edit=3 – Dimitry K Mar 20 '15 at 09:26
  • 1
    This literally does nothing; PHP casts your numeric strings straight back to integer keys. – Walf Feb 16 '18 at 11:10
  • 1
    It's issues like this that make me want to drop PHP as a tool :/ – ChristoKiwi Mar 25 '18 at 21:50
  • Consider deleting this answer. [Proof that it does nothing](https://3v4l.org/YkoA0), output has always been same as input. At least you'll earn a "Discipline" badge. – Walf Feb 25 '21 at 04:31
3

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.

NeuroXc
  • 652
  • 6
  • 22
  • If your keys are not floats then this is a much worse solution than adding a "\0". If your keys are float then adding .0 on 0 precision values is a solution – John Apr 05 '21 at 23:28
1

Edit: This should work

foreach($array as $key => $value) { 
    $newkey = sprintf('%s',$key);
    $newArray["'$newkey'"] = $value; 
} 
user387302
  • 395
  • 1
  • 4
  • 13
  • 1
    Interesting, but this just adds more complexity I believe. – alex Aug 10 '10 at 04:46
  • Actually now that I think about this, even doing this might work: $newArray["'$key'"] =$value – user387302 Aug 10 '10 at 04:56
  • 5
    It works if you want array indexes wrapped in single quotes, which I don't. I guess I might be asking the impossible. Thanks for your answer anyway. – alex Aug 10 '10 at 05:12
  • 2
    Maybe worth mentioning that the `sprintf()` is probably redundant. – alex Apr 19 '16 at 12:33
-1

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;
}
miile7
  • 2,547
  • 3
  • 23
  • 38
  • This method is mentioned in the question already where the OP sais "If I add an `a` to every index...". This is the exact same as you are doing, adding a string so the key gets converted. – miile7 May 12 '20 at 07:02
-3

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"}
mickmackusa
  • 43,625
  • 12
  • 83
  • 136
Walf
  • 8,535
  • 2
  • 44
  • 59