Les tableaux
Un tableau PHP est une table de hachage ordonnĂ©e, et câest la seule collection native. Il joue Ă la fois le rĂŽle de la liste et du dictionnaire de Python, du tableau et de lâobjet de JavaScript, de lâArrayList et de la LinkedHashMap de Java. Les clĂ©s sont des entiers ou des chaĂźnes, les valeurs sont nâimporte quoi, et lâordre dâinsertion est toujours conservĂ©.
<?php
declare(strict_types=1);
$list = ['apple', 'pear']; // keys 0, 1
$map = ['name' => 'Ada', 'born' => 1815]; // string keys
$mixed = [5 => 'five', 'six', 'x' => 'ex']; // keys 5, 6, 'x'
$list[] = 'plum'; // append, key 2
$map['died'] = 1852; // insert, at the end
var_dump(array_is_list($list)); // true
var_dump(array_is_list($map)); // false
Il nây a pas de type liste Ă part. Une « liste » est un tableau dont les clĂ©s se trouvent ĂȘtre 0, 1, 2 et ainsi de suite, dans cet ordre, et array_is_list() (PHP 8.1) vous dit si câest le cas. La distinction compte quand le tableau quitte PHP : json_encode() produit [...] pour une liste et {...} pour tout le reste.
Les clés sont normalisées
Une clĂ© est soit un int, soit une string, et PHP convertit tout le reste Ă lâentrĂ©e. Une chaĂźne numĂ©rique devient lâentier quâelle nomme, un flottant perd ses dĂ©cimales, un boolĂ©en devient 0 ou 1, et null devient la chaĂźne vide :
<?php
declare(strict_types=1);
$a = [];
$a['1'] = 'a'; // key 1, not '1'
$a[1.7] = 'b'; // key 1, overwrites (and a deprecation notice since 8.1)
$a[true] = 'c'; // key 1, overwrites again
$a[null] = 'd'; // key ''
var_dump($a); // [1 => 'c', '' => 'd']
Ces trois Ă©critures visent la mĂȘme clĂ©. La rĂšgle rend service quand une base de donnĂ©es renvoie des identifiants sous forme de chaĂźnes, et elle devient un piĂšge quand vous comptiez sur '1' et 1 pour former deux entrĂ©es distinctes, ce qui nâarrive jamais.
Lire une clé absente
Lire une clĂ© absente Ă©met un avertissement et renvoie null. Deux fonctions vous disent si une clĂ© existe, et elles ne sont pas dâaccord sur null :
<?php
declare(strict_types=1);
$user = ['name' => 'Ada', 'email' => null];
var_dump(isset($user['email'])); // false: the value is null
var_dump(array_key_exists('email', $user)); // true: the key is there
var_dump(isset($user['phone'])); // false, no warning
$phone = $user['phone'] ?? 'unknown'; // no warning, default applied
isset() rĂ©pond « y a-t-il ici une valeur non nulle ? », array_key_exists() rĂ©pond « la clĂ© est-elle prĂ©sente ? ». Dans presque tous les cas, câest ?? que vous voulez : il lit la clĂ© si elle existe et nâest pas null, et se rabat sur la valeur par dĂ©faut sinon, en silence.
Les tableaux sont des valeurs
Si vous ne retenez quâune chose de ce chapitre, retenez celle-ci. Affecter un tableau, le passer Ă une fonction ou le renvoyer produit Ă chaque fois une copie, et lâoriginal ne voit jamais ce qui arrive Ă cette copie.
<?php
declare(strict_types=1);
function addItem(array $cart, string $item): array
{
$cart[] = $item;
return $cart;
}
$cart = ['book'];
$bigger = addItem($cart, 'pen');
var_dump(count($cart)); // 1
var_dump(count($bigger)); // 2
En JavaScript, en Python ou en Java, cart contiendrait maintenant deux éléments, parce que ces langages font circuler une référence vers une structure partagée. En PHP, la fonction a reçu son propre tableau, et pour vous donner le résultat elle doit le renvoyer.
Le coĂ»t est plus faible quâil nây paraĂźt, parce que PHP partage la mĂ©moire sous le capot et ne duplique les donnĂ©es quâĂ la premiĂšre Ă©criture, un mĂ©canisme appelĂ© copie Ă lâĂ©criture. Passer un tableau de dix mille Ă©lĂ©ments Ă une fonction qui se contente de le lire ne coĂ»te donc rien.
Vous pouvez y renoncer avec une référence, &, sur le paramÚtre :
function addItemInPlace(array &$cart, string $item): void
{
$cart[] = $item;
}
RĂ©servez cette Ă©criture Ă la rare boucle critique oĂč la copie se mesure. Une fonction qui renvoie un nouveau tableau se lit, se teste et se type plus facilement, et si la famille sort() modifie sur place par rĂ©fĂ©rence, câest une exception hĂ©ritĂ©e de lâhistoire du langage plutĂŽt quâun modĂšle Ă suivre.
Les objets se comportent Ă lâinverse : une variable objet est un identifiant, et les copies de cet identifiant dĂ©signent le mĂȘme objet, comme dans tous les langages que vous connaissez. Quand il vous faut une sĂ©mantique de rĂ©fĂ©rence pour une collection, enveloppez-la dans une classe, ce que le chapitre Les classes dĂ©taille.
foreach, par valeur et par référence
foreach itĂšre sur une copie, donc modifier $item dans la boucle ne change rien :
<?php
declare(strict_types=1);
$prices = [10, 20, 30];
foreach ($prices as $price) {
$price *= 2; // local copy, the array is untouched
}
foreach ($prices as &$price) {
$price *= 2; // writes through
}
unset($price); // break the reference
var_dump($prices); // [20, 40, 60]
Le unset() qui suit la boucle par rĂ©fĂ©rence a une vraie fonction : sans lui, $price pointe toujours vers le dernier Ă©lĂ©ment, et un $price = 0; innocent plus loin dans le fichier Ă©crase $prices[2]. La plupart des dĂ©veloppeurs PHP se sont fait prendre une fois. Un foreach avec $key => $value et une Ă©criture dans $prices[$key] Ă©vite la question, et array_map() lâĂ©vite tout autant.
Démonter et remonter des tableaux
La déstructuration fonctionne sur les listes et sur les dictionnaires :
<?php
declare(strict_types=1);
[$x, $y] = [3, 4];
['id' => $id, 'name' => $name] = ['id' => 7, 'name' => 'Ada'];
[, $second] = ['skip', 'keep']; // holes are allowed
$defaults = ['color' => 'blue', 'size' => 'M'];
$order = [...$defaults, 'size' => 'L']; // string keys spread since 8.1
var_dump($order); // ['color' => 'blue', 'size' => 'L']
Le dépliage avec des clés chaßnes se comporte comme {...defaults, size: 'L'} en JavaScript : les derniÚres entrées gagnent. Avec des clés entiÚres, le dépliage renumérote, donc [...[1, 2], ...[3]] vaut [1, 2, 3], et non un dictionnaire avec des clés en double.
Le trio fonctionnel et ses cousins
array_map(), array_filter() et array_reduce() font ce que leur nom dit, avec chacun sa subtilité :
<?php
declare(strict_types=1);
$orders = [
['id' => 1, 'total' => 40, 'paid' => true],
['id' => 2, 'total' => 15, 'paid' => false],
['id' => 3, 'total' => 90, 'paid' => true],
];
$totals = array_map(fn(array $o) => $o['total'], $orders); // [40, 15, 90]
$paid = array_filter($orders, fn(array $o) => $o['paid']); // keys 0 and 2
$sum = array_reduce($totals, fn(int $carry, int $t) => $carry + $t, 0); // 145
echo json_encode($paid); // {"0":{...},"2":{...}} an object!
echo json_encode(array_values($paid)); // [{...},{...}] a list
array_filter() conserve les clĂ©s dâorigine. AprĂšs le filtrage dâune liste, les clĂ©s ont des trous, array_is_list() rĂ©pond false et json_encode() produit un objet ; array_values() renumĂ©rote et remet tout en ordre. Notez aussi lâordre des arguments, qui place le tableau en premier pour array_filter() et array_reduce() mais la fonction de rappel en premier pour array_map(). Cette incohĂ©rence a trente ans, et la complĂ©tion de votre Ă©diteur reste le meilleur remĂšde.
PHP 8.4 a ajouté les recherches que vous réécriviez à la main : array_find() renvoie le premier élément qui correspond, array_find_key() sa clé, array_any() et array_all() renvoient des booléens. PHP 8.5 a ajouté array_first() et array_last(), qui renvoient la premiÚre et la derniÚre valeur quelles que soient les clés, à cÎté des plus anciennes array_key_first() et array_key_last().
// PHP 8.4
$firstBig = array_find($orders, fn(array $o) => $o['total'] > 50);
$allPaid = array_all($orders, fn(array $o) => $o['paid']); // false
Le tri modifie sur place et, depuis PHP 8.0, est stable. usort() avec lâopĂ©rateur vaisseau spatial est lâidiome :
usort($orders, fn(array $a, array $b) => $b['total'] <=> $a['total']);
sort() et usort() renumĂ©rotent les clĂ©s ; asort() et uasort() les conservent ; ksort() trie par clĂ©. array_column($orders, 'total', 'id') extrait un champ dâune liste de lignes et, avec le troisiĂšme argument, indexe le rĂ©sultat par un autre. array_combine(), array_flip(), array_unique(), array_slice() et array_splice() sont lĂ aussi, avec count() pour la longueur.
Vous croiserez aussi compact() et extract(), qui transforment des variables locales en tableau et inversement. Il suffit de savoir les reconnaĂźtre, parce quâelles mettent en Ă©chec lâanalyse statique comme votre Ă©diteur et quâaucun code rĂ©cent ne les Ă©crit.
ItĂ©rer sur nâimporte quoi
foreach ne se limite pas aux tableaux. Tout ce qui est iterable fonctionne : tableaux, gĂ©nĂ©rateurs, et objets implĂ©mentant Iterator ou IteratorAggregate. Une fonction qui accepte iterable peut recevoir un gĂ©nĂ©rateur dâun million de lignes sans charger le million de lignes, ce que Fonctions et closures reprend.
<?php
declare(strict_types=1);
function total(iterable $amounts): int
{
$sum = 0;
foreach ($amounts as $amount) {
$sum += $amount;
}
return $sum;
}
echo total([1, 2, 3]); // 6
Quand un tableau ne suffit pas
Un tableau ne peut pas dire ce quâil contient : array $orders nâapprend rien au lecteur, et le langage nâa pas dâarray<Order>. La rĂ©ponse lĂ©gĂšre est un docblock, @param list<Order> $orders, que PHPStan et Psalm imposent comme un vrai type et que votre Ă©diteur utilise pour la complĂ©tion. La rĂ©ponse plus lourde est une petite classe, une final class Orders qui dĂ©tient un tableau privĂ©, expose exactement les opĂ©rations dont vous avez besoin, et implĂ©mente les interfaces qui lui permettent de se comporter comme un tableau lĂ oĂč câest utile : Countable pour count(), ArrayAccess pour $orders[0] et IteratorAggregate pour foreach.
Pour les cas quâun tableau ne couvre pas, la bibliothĂšque standard fournit SplObjectStorage, qui associe des objets Ă des donnĂ©es en utilisant lâobjet lui-mĂȘme comme clĂ©, et WeakMap (PHP 8.0), qui fait de mĂȘme sans garder lâobjet en vie, ce qui permet aux caches indexĂ©s par entitĂ© de ne pas fuir.
Les deux habitudes Ă perdre ici sont dâattendre dâune fonction quâelle modifie le tableau que vous lui passez, et dâoublier quâ
array_filter()laisse des trous. Renvoyez le nouveau tableau, et passez pararray_values()avant dâencoder.
Les tableaux sont ce que la plupart du code PHP fait circuler, et les fonctions sont ce à quoi il les confie. Comme les closures de PHP capturent leur contexte autrement que celles que vous connaissez, elles méritent leur propre chapitre, Fonctions et closures.