Een van de core datatypes van PHP is de array. Deze bestaat al min of meer in de zelfde vorm sinds het vroege begin van de taal. De naam "array" is nogal onhandig gekozen, en de implementatie ook, want eigenlijk is het niet echt een array.
Eigenlijk is het een soort van Frankenstein combinatie van een list en een dictionary zoals we die in andere talen kennen. Dat is op zijn zachtst gezegd verwarrend, en kan zorgen voor onverwachte en soms nare effecten. Soms gaan dingen er zelfs door kapot. Zoals ik deze week meemaakte. Hierover later meer.
Maar om te beginnen, wat is het verschil nou tussen lists en dictionaries?
Lists
Een list, of array zoals ie ook wel genoemd wordt, is, zoals de naam al zegt, een lijst van elementen. Deze elementen hebben een vaste volgorde, en elk element heeft een opeenvolgende numerieke index, beginnend met 0.
Voorbeeld in Javascript:
let myList = [2, 1, 'foo', new Date()];
let myElem = myList[2]; // myElem contains 'foo'
Dictionaries
In Python heet het een dictionary, Perl en Ruby noemen het een hash, en in Javascript en in JSON heet het een object . Wat ook een nogal verwarrende naam is, maar dat is weer een ander verhaal.
Hoe ie ook heet, een dictionary is in de kern een verzameling van key/value pairs. Deze key/value pairs hebben niet per se een vaste volgorde. De keys zijn in de regel strings. En elke key is uniek.
Een voorbeeld in Python:
myDict = {'foo': 'bar', 'boo': 'baz'}
myElem = myDict['foo'] # myElem contains 'bar'
myDict['boo'] = 'bla'
print(myDict) # output: {'foo': 'bar', 'boo': 'bla'}
De "Frankenstein" array van PHP
Ooit, lang geleden, dachten de makers van PHP dat het een Goed Idee zou zijn om lists en dictionaries samen te voegen tot 1 datatype, dat ze ook nog om de verwarring compleet te maken, de array hebben genoemd. Dat had de volgende gevolgen:
- elementen in een PHP array hebben altijd een vaste volgorde
- elementen in een PHP array kunnen een string based key hebben, of een numerieke index
- deze numerieke indexes kunnen opeenvolgend zijn (spoiler alert: dit is een essentieel punt!)
Dit zijn best veel variabelen, zou dat tot problemen kunnen leiden? Let's see...
Lists in PHP
$myArray = [
'element 1',
'element 2',
'element 3',
];
print_r($myArray);
print_r($myArray[1]);
geeft als output:
Array
(
[0] => element 1
[1] => element 2
[2] => element 3
)
element 2
Dat ziet er op zich best intuitief uit, en dit werkt eigenlijk net als in andere talen. De elementen krijgen automatisch een numerieke, opeenvolgende index toegewezen, en de elementen zijn benaderbaar met deze index.
Dictionaries in PHP
Met key/value pairs werkt het op zich ook zoals je zou verwachten:
$myArray = [
'foo' => 'bar',
'boo' => 'baz',
];
print_r($myArray);
print_r($myArray['boo']);
Output:
Array
(
[foo] => bar
[boo] => baz
)
baz
De specifiek gedefinieerde keys vervangen hier dus de automatisch toegewezen numerieke keys.
List of dictionary?
Iets verwarrender wordt het als we lists en dictionaries gaan combineren, wat volkomen okay is in PHP:
$myArray = [
'foo' => 'bar',
'blarp',
4 => 'elem with numeric index',
2 => 'another elem with lower numeric index',
'boo' => 'baz',
];
// add an element to the end of the array
$myArray[] = 'zonk';
print_r($myArray);
$myArray[3] = 'three';
print_r($myArray);
Dit geeft de volgende output:
Array
(
[foo] => bar
[0] => blarp
[4] => elem with numeric index
[2] => another elem with lower numeric index
[boo] => baz
[5] => zonk
)
Array
(
[foo] => bar
[0] => blarp
[4] => elem with numeric index
[2] => another elem with lower numeric index
[boo] => baz
[5] => zonk
[3] => three
)
Wat gebeurt hier?
- als er niet expliciet een key wordt gespecificeerd, wordt er door PHP automatisch een numerieke key toegewezen
- deze key is de hoogste aanwezige numerieke key + 1, of 0 als er geen zijn
- de positie van elk element is niet afhankelijk van de numerieke key
Itereren over arrays
Aangezien een PHP array niet altijd opeenvolgende indexes heeft, kun je er dus ook niet altijd op de volgende manier over itereren:
for ($i = 0; $i < count($myArray); $i++) {
$elem = $myArray[$i];
// ...
}
Met bovenstaande array zou dat onverwachte resultaten geven, en Undefined array key
warnings:
blarp
PHP Warning: Undefined array key 1 in /home/lennart/Development/php/arrays.php on line 47
another elem with lower numeric index
three
elem with numeric index
zonk
PHP Warning: Undefined array key 6 in /home/lennart/Development/php/arrays.php on line 47
In plaats van bovenstaande for
loop gebruik je beter een foreach
loop:
foreach ($myArray as $key => $elem) {
// ...
}
Want dat werkt wel altijd zoals je verwacht.
Dus, niks aan de hand toch?
Nee. De wereld is echter veel groter dan alleen PHP. Het gebeurt heel vaak dat je data uit moet wisselen met andere talen of applicaties. En dan kan het wel eens wel problematisch zijn.
JSON conversie
JSON is tegenwoordig de de facto standaard om data uit te wisselen tussen verschillende applicaties en talen. Elke taal heeft functies om een JSON string te decoderen naar een interne datastructuur, en omgekeerd die structuren naar een JSON string te encoden.
Bij deze conversies is het wel belangrijk dat deze volledig symmetrisch is. Met andere woorden: als je data naar json omzet, en weer terug, dan mag die data niet veranderd zijn.
Met een taal waar een array zowel een list als een dictionary kan zijn, geeft dat wel eens problemen.
Dictionaries die in lists veranderen
$json = '{"0": "No", "1": "Yes"}';
$array = json_decode($json, true);
print json_encode($array);
Je zou de oorspronkelijke JSON string verwachten, maar dit geeft in werkelijkheid een heel ander resultaat:
["No","Yes"]
Een dictionary wordt opeens een list! De conversie is in dit geval dus niet symmetrisch!
Andersom gebeurt het ook:
Arrays die in dictionaries veranderen
$array = [
'first',
'second',
'third',
];
print json_encode($array) . PHP_EOL;
// remove the second element
unset($array[1]);
print json_encode($array) . PHP_EOL;
Hier wordt een array opeens een dictionary!
["first","second","third"]
{"0":"first","2":"third"}
Hoe komt dit nou?
Opeenvolgende numerieke keys
Een array in PHP is een list als het opeenvolgende, numerieke keys heeft, beginnend met 0. Een dergelijke array wordt na JSON conversie ook een list.
In alle andere gevallen is het dus eigenlijk een dictionary en wordt het na JSON conversie een object.
PHP8.1 to the rescue?
Tot voor kort bestond er geen aparte functie om het "array type" te checken. Maar, met de komst van PHP8.1 is er eindelijk de functie array_is_list. Beter laat dan nooit, zullen we maar zeggen.
Mocht je nou nog niet met 8.1 werken, dan kun je deze simpele polyfill gebruiken:
if (!function_exists('array_is_list')) {
function array_is_list(array $a): bool
{
if ($a === []) {
return true;
}
return array_keys($a) === range(0, count($a) - 1);
}
}
Deze functie is op zich soms wel handig, maar is zeker geen fix voor alles. Het had me niet behoed voor de valkuil hierboven. Want het voorbeeld (en eigenlijk deze hele blogpost) is geinspireerd op een praktijk case.
Hoe ik in de valkuil viel
$json = '{"0": "No", "1": "Yes"}';
$array = json_decode($json, true);
print json_encode($array);
Bovenstaande stukje JSON was deel van een veel groter JSON document, dat ergens in een database stond. Ik moest het omzetten naar PHP zodat ik er een aantal transformaties op kon uitvoeren, het daarna weer terug naar JSON converteren en in de DB updaten.
Let op het tweede argument voor json_decode. De true
zorgt ervoor dat de JSON omgezet wordt in arrays, in plaats van stdClass
objecten. Ik doe dit zelf eigenlijk bijna altijd zo, omdat een array over het algemeen veel makkelijker is om mee te werken dan stdClass
objecten.
Er bestaat in PHP voor arrays een hele verzameling aan array_*
functies, maar bijna niks voor objecten. Als je bijvoorbeeld twee objecten wil mergen, is het handig om deze om te zetten naar arrays, deze in array_merge
te gooien, en dan eventueel weer naar een object te casten. Dus waarom dan uberhaupt stdClass
objecten gebruiken?
Nou, inmiddels weet ik dat wel! De JSON dictionary {"0": "No", "1": "Yes"}
heeft opeenvolgende numerieke keys. Ja, het zijn weliswaar strings, maar hey, we hebben het over PHP, die converteert dat gewoon stilletjes naar integers! Dus de dictionary veranderde in PHP en daarna weer in JSON in een list. Met als gevolg dat onverwacht ergens een formulier kapotging. Gelukkig had ik de backups nog.
Samenvattend
-
Arrays kunnen verraderlijk zijn in PHP.
-
Ze kunnen "onder water" een list of een dictionary zijn. Daar merk je meestal pas wat van als je ze van of naar een formaat als JSON converteert.
-
Als je JSON decodeert die je ook weer terug naar JSON moet encoderen, decodeer dan liever naar een object, niet naar een array.
-
Als je een PHP array geforceerd naar een JSON list wil laten encoden:
json_encode(array_values($array));
- En als je een PHP array geforceerd naar een JSON object wil laten encoden:
json_encode((object)$array);