Indirect modification of overloaded element has no effect

Récemment je me suis trouvé devant cette notice PHP qui m’a laissé perplexe. La ligne fautive était

$app['data_collector.templates']['twig'] = '@WebProfiler/Collector/twig.html.twig';

Rien de bien sorcier, on se contente d’ajouter un couple clé/valeur au tableau contenu dans $app['data_collector.templates']. Pourtant si nous affichons ensuite le contenu de ce tableau, la clé twig ne s’y trouvera pas.

La faute à $app qui n’est pas un simple array mais un objet implémentant ArrayAccess. Par conséquent la ligne de code précédente est équivalente à

$app->offsetGet('data_collector.templates')['twig'] = '@WebProfiler/Collector/twig.html.twig';

Il apparaît maintenant clairement qu’on ne travaille pas sur le tableau original mais sur celui retourné par offsetGet. Or par défaut le retour d’une fonction est passé par copie, ce qui veut dire qu’on va en réalité modifier une copie du tableau.

Pour visualiser le problème, assignons le retour de offsetGet à une variable :

$dataCollectorTemplates = $app->offsetGet('data_collector.templates');
$dataCollectorTemplates['twig'] = '@WebProfiler/Collector/twig.html.twig';

On voit que $dataCollectorTemplates ne sert à rien, et surtout, $app n’est pas modifié.

Si la valeur de retour n’est pas utilisée, il est donc extrêmement probable que votre code ne fasse pas ce que vous attendiez, et PHP vous le signale.

Notez que cette confusion est possible avec autre chose qu’ArrayAccess ; la notice mentionne « overloaded element », c’est à dire des propriétés dynamiques accessibles via la surcharge magique. Pour simplifier, vous vous exposez au problème chaque fois que l’accès à un élément passe par une fonction, comme c’est le cas avec __get() par exemple :

<?php

class Test  
{
    public function __get($name)
    {
        return array(); // pour l’exemple
    }
}

$test = new Test();
$test->dynamic[] = 1;

// Indirect modification of overloaded property Test::$dynamic has no effect

La solution la plus générique consiste à modifier la copie retournée par la fonction, puis à assigner la copie modifiée en place de la valeur originale. Au lieu du code du premier exemple on aurait donc pu écrire

$dataCollectorTemplates = $app['data_collector.templates'];
$dataCollectorTemplates['twig'] = '@WebProfiler/Collector/twig.html.twig';
$app['data_collector.templates'] = $dataCollectorTemplates;

ce qui n’est pas du tout intuitif mais a le mérite de marcher.

Bien sûr selon le contexte il est possible qu’il existe des solutions plus performantes, l’essentiel étant que vous compreniez le problème ! Pour exemple, vous pouvez jeter un coup d’œil à une correction sur GitHub.