json_encode et array

Si vous avez jamais eu à travailler avec une API PHP, qu’il s’agisse de la produire ou de la consommer, vous avez sans doute déjà rencontré des cas où le logiciel client se met à agonir d’injures son fournisseur sous prétexte qu’il a demandé un array et qu’il se retrouve avec un object.

Pourtant, côté serveur, le code se contente d’appliquer un json_encode à un tableau tout bête supposément non-associatif.

Quelque chose comme :

$someArray = ["a", "b", "c", "d"];

var_dump(json_encode($someArray));
string(17) "["a","b","c","d"]"

Pas de problème.

Tiens, et si j’enlevais le premier élément du tableau :

unset($someArray[0]);

var_dump(json_encode($someArray));
string(25) "{"1":"b","2":"c","3":"d"}"

Mais, que, quoi ?

On la refait, mais en changeant un peu la syntaxe.

var_dump(json_encode([0 => "a", 1 => "b", 2 => "c", 3 => "d"]));
var_dump(json_encode([1 => "b", 2 => "c", 3 => "d"]));
string(17) "["a","b","c","d"]"
string(25) "{"1":"b","2":"c","3":"d"}"

Vous commencez à voir le truc ?

En PHP, tout « tableau » est fondamentalement un dictionnaire. Un dictionnaire sur lequel a été greffé une relation d’ordre certes (le langage conserve les éléments dans l’ordre dans lequel ils ont été insérés dans la structure), mais qui se base avant tout sur cette idée d’associer chaque valeur qu’il transporte à une clé.

Et si ces clés ne sont pas explicitement précisées, le langage va par défaut utiliser des entiers, en commençant par celui suivant la plus grande clé actuellement dans le tableau.

Un exemple, parce que c’est franchement tordu :

$myArray = [12 => "b", "toto" => "c", 1 => "a"];
$myArray []= "a";
print_r($myArray);
Array
(
    [12] => b
    [toto] => c
    [1] => a
    [13] => a
)

Bon, soit, à la rigueur.

Sauf qu’en JSON, les choses ne fonctionnent pas ainsi. Il y a d’un côté les array, une structure ordonnée, et de l’autre les object, qui sont des associations clé-valeur.

Et lorsque json_encode doit convertir un array (PHP) vers l’une de ces deux structures, il tente de faire au mieux selon un algorithme simple :

  • Si l’array PHP ne comporte que des clés entières (assertion avec un ==) consécutives et commençant à 0, alors celui-ci deviendra un array JSON
  • Sinon, il sera représenté sous la forme d’un type object

Autrement dit :

[] -> array
["a"] -> array
[0.0 => "a"] -> array
["key" => "a"] -> object
[1 => "a"] -> object

Ouch.

Cela explique notamment des blagues comme :

var_dump(json_encode(array_filter(["a", "b", null])));
var_dump(json_encode(array_filter(["a", null, "b"])));
string(9) "["a","b"]"
string(17) "{"0":"a","2":"b"}"

En effet, array_filter, comme la plupart des fonctions PHP, conserve les clés d’origine du tableau qu’il manipule. Autrement dit :

print_r(array_filter(["a", "b", null]));
print_r(array_filter(["a", null, "b"]));
Array
(
    [0] => a
    [1] => b
)
Array
(
    [0] => a
    [2] => b
)

Dans le premier cas, par hasard, les clés continuent de se suivre, le typage est donc bon, dans le second, ce n’est plus le cas, et les problèmes surviennent.

Dans la pratique, au moindre doute sur les clés d’un tableau PHP, ce qui sera le cas la plupart des temps, une astuce pour sécuriser le résultat consiste à forcer le typage JSON via l’utilisation de (object) et de array_values.

var_dump(json_encode(array_values([0 => "a", 2 => "c"])));
var_dump(json_encode((object) [0 => "a"]));
string(9) "["a","c"]"
string(9) "{"0":"a"}"

Un peu lourd, mais efficace. Au pire, rien n’empêche de cacher ces roueries un peu bas niveau derrière des classes, via l’implémentation de JsonSerializable.

class JsonArray implements \JsonSerializable
{
  private $array;

  public function __construct(array $array)
  {
    $this->array = $array;
  }

  public function jsonSerialize()
  {
    return array_values($this->array);
  }
}

class JsonObject implements \JsonSerializable
{
  private $array;

  public function __construct(array $array)
  {
    $this->array = $array;
  }

  public function jsonSerialize()
  {
    return (object) $this->array;
  }
}

var_dump(json_encode(new JsonArray([0 => "a", 2 => "c"])));
var_dump(json_encode(new JsonObject([0 => "a"])));
string(9) "["a","c"]"
string(9) "{"0":"a"}"

Mais on rentre dans des détails d’implémentation hors-sujet.

L’important restant en effet, quelle que soit la solution choisie, que le client reçoive bien ce que le serveur s’est engagé à lui envoyer, et pas autre chose.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion /  Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion /  Changer )

w

Connexion à %s