Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Un tableau confié à une fonction : la fonction reçoit une photocopie de la feuille pendant que l'original reste intact sur le bureau de l'appelant ; ce n'est que lorsque la fonction écrit sur sa copie que les deux feuilles diffÚrent vraiment

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.

À gauche, une caisse ouverte Ă©tiquetĂ©e array oĂč l'on a jetĂ© n'importe quoi ; Ă  droite, une boĂźte Ă©tiquetĂ©e avec une fente typĂ©e sur le dessus qui n'accepte que des piĂšces en forme d'Order, avec un petit compteur et une poignĂ©e sur le cĂŽtĂ©

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 par array_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.