PHP facilite la construction d'un système Web, ce qui explique en grande partie sa popularité. Mais malgré sa facilité d'utilisation, PHP est devenu un langage assez sophistiqué avec de nombreux cadres, nuances et subtilités qui peuvent mordre les développeurs, conduisant à des heures de débogage époustouflant. Cet article met en évidence dix des erreurs les plus courantes Développeurs PHP faut se méfier de.
foreach
bouclesVous ne savez pas comment utiliser les boucles foreach en PHP? Utilisation de références dans foreach
Les boucles peuvent être utiles si vous souhaitez opérer sur chaque élément du tableau sur lequel vous effectuez une itération. Par exemple:
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6, 8)
Le problème est que, si vous ne faites pas attention, cela peut également avoir des effets secondaires et des conséquences indésirables. Plus précisément, dans l'exemple ci-dessus, une fois le code exécuté, $value
restera dans la portée et contiendra une référence au dernier élément du tableau. Opérations ultérieures impliquant $value
pourrait donc involontairement finir par modifier le dernier élément du tableau.
La principale chose à retenir est que foreach
ne crée pas de portée. Ainsi, $value
dans l'exemple ci-dessus est un référence dans le cadre supérieur du script. À chaque itération foreach
définit la référence pour pointer vers l'élément suivant de $array
. Une fois la boucle terminée, par conséquent, $value
pointe toujours vers le dernier élément de $array
et reste dans la portée.
Voici un exemple du type de bogues évasifs et déroutants que cela peut entraîner:
$array = [1, 2, 3]; echo implode(',', $array), '
'; foreach ($array as &$value) {} // by reference echo implode(',', $array), '
'; foreach ($array as $value) {} // by value (i.e., copy) echo implode(',', $array), '
';
Le code ci-dessus affichera les éléments suivants:
1,2,3 1,2,3 1,2,2
Non, ce n’est pas une faute de frappe. La dernière valeur de la dernière ligne est en effet un 2, pas un 3.
Pourquoi?
Après avoir parcouru le premier foreach
boucle, $array
reste inchangé mais, comme expliqué ci-dessus, $value
est laissé comme une référence pendante au dernier élément de $array
(depuis que foreach
boucle accédée $value
par référence ).
En conséquence, lorsque nous passons par le second foreach
boucle, «des trucs bizarres» semblent se produire. Plus précisément, puisque $value
est maintenant accédé par valeur (c'est-à-dire par copie ), foreach
copies chaque séquentiel $array
élément dans $value
à chaque étape de la boucle. En conséquence, voici ce qui se passe à chaque étape de la seconde foreach
boucle:
$array[0]
(c'est-à-dire «1») dans $value
(qui est une référence à $array[2]
), donc $array[2]
est maintenant égal à 1. Donc $array
contient maintenant [1, 2, 1].$array[1]
(c'est-à-dire «2») dans $value
(qui est une référence à $array[2]
), donc $array[2]
est maintenant égal à 2. Donc $array
contient maintenant [1, 2, 2].$array[2]
(qui équivaut maintenant à «2») en $value
(qui est une référence à $array[2]
), donc $array[2]
est toujours égal à 2. Donc $array
contient maintenant [1, 2, 2].Pour continuer à profiter des références dans foreach
boucles sans courir le risque de ce genre de problèmes, appelez unset()
sur la variable, immédiatement après le foreach
boucle, pour supprimer la référence; par exemple.:
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value no longer references $arr[3]
isset()
comportementMalgré son nom, isset()
non seulement renvoie false si un élément n'existe pas, mais renvoie également false
pour null
valeurs .
Ce comportement est plus problématique qu'il n'y paraît au premier abord et constitue une source courante de problèmes.
Considérer ce qui suit:
$data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // do something here if 'keyShouldBeSet' is not set }
L'auteur de ce code voulait vraisemblablement vérifier si keyShouldBeSet
a été défini dans $data
. Mais, comme discuté, isset($data['keyShouldBeSet'])
volonté également renvoie false si $data['keyShouldBeSet']
était défini, mais a été défini sur null
. La logique ci-dessus est donc erronée.
Voici un autre exemple:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; }
Le code ci-dessus suppose que si $_POST['active']
renvoie true
, puis postData
sera forcément défini, et donc isset($postData)
retournera true
. Donc, à l'inverse, le code ci-dessus suppose que le seulement façon que isset($postData)
retournera false
est si $_POST['active']
renvoyé false
ainsi que.
Ne pas.
Comme expliqué, isset($postData)
retournera également false
si $postData
a été défini sur null
. Il donc est possible pour isset($postData)
retourner false
même si $_POST['active']
renvoyé true
. Encore une fois, la logique ci-dessus est erronée.
Et au fait, en passant, si l'intention du code ci-dessus était vraiment de vérifier à nouveau si $_POST['active']
renvoyé vrai, en s'appuyant sur isset()
car c'était en tout cas une mauvaise décision de codage. Au lieu de cela, il aurait été préférable de revérifier simplement $_POST['active']
; c'est à dire.:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; }
Pour les cas, cependant, où il est important pour vérifier si une variable était vraiment définie (c'est-à-dire pour faire la distinction entre une variable non définie et une variable définie sur null
), le array_key_exists()
est une solution beaucoup plus robuste.
Par exemple, nous pourrions réécrire le premier des deux exemples ci-dessus comme suit:
$data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // do this if 'keyShouldBeSet' isn't set }
De plus, en combinant array_key_exists()
avec get_defined_vars()
, nous pouvons vérifier de manière fiable si une variable dans la portée actuelle a été définie ou non:
if (array_key_exists('varShouldBeSet', get_defined_vars())) { // variable $varShouldBeSet exists in current scope }
Considérez cet extrait de code:
class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
Si vous exécutez le code ci-dessus, vous obtiendrez les éléments suivants:
PHP Notice: Undefined index: test in /path/to/my/script.php on line 21
Qu'est-ce qui ne va pas?
Le problème est que le code ci-dessus confond le renvoi de tableaux par référence avec le renvoi de tableaux par valeur. Sauf si vous dites explicitement à PHP de renvoyer un tableau par référence (c'est-à-dire en utilisant &
), PHP renverra par défaut le tableau «par valeur». Cela signifie qu'un copie du tableau sera retourné et donc la fonction appelée et l'appelant n'accéderont pas à la même instance du tableau.
Donc l'appel ci-dessus à getValues()
renvoie un copie du $values
tableau plutôt qu’une référence à celui-ci. Dans cet esprit, revenons aux deux lignes clés de l'exemple ci-dessus:
// getValues() returns a COPY of the $values array, so this adds a 'test' element // to a COPY of the $values array, but not to the $values array itself. $config->getValues()['test'] = 'test'; // getValues() again returns ANOTHER COPY of the $values array, and THIS copy doesn't // contain a 'test' element (which is why we get the 'undefined index' message). echo $config->getValues()['test'];
Une solution possible serait d'enregistrer la première copie de $values
tableau retourné par getValues()
puis opérer sur cette copie par la suite; par exemple.:
$vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test'];
Ce code fonctionnera correctement (c'est-à-dire qu'il affichera test
sans générer de message 'd'index non défini'), mais en fonction de ce que vous essayez d'accomplir, cette approche peut être adéquate ou non. En particulier, le code ci-dessus ne modifiera pas l'original $values
tableau. Alors si tu faire souhaitez que vos modifications (telles que l'ajout d'un élément «test») affectent le tableau d'origine, vous devrez à la place modifier le getValues()
fonction pour renvoyer une référence au $values
tableau lui-même. Cela se fait en ajoutant un &
avant le nom de la fonction, indiquant ainsi qu'elle doit renvoyer une référence; c'est à dire.:
class Config { private $values = []; // return a REFERENCE to the actual $values array public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
La sortie de ceci sera test
, comme prévu.
Mais pour rendre les choses plus confuses, considérez plutôt l'extrait de code suivant:
class Config { private $values; // using ArrayObject rather than array public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
Si vous devinez que cela entraînerait la même erreur «d'index non défini» que notre précédente array
exemple, vous aviez tort. En réalité, cette le code fonctionnera très bien. La raison en est que, contrairement aux tableaux, PHP passe toujours les objets par référence . (ArrayObject
est un objet SPL, qui imite entièrement l'utilisation des tableaux, mais fonctionne comme un objet.)
Comme le montrent ces exemples, il n'est pas toujours tout à fait évident en PHP que vous ayez affaire à une copie ou à une référence. Il est donc essentiel de comprendre ces comportements par défaut (c'est-à-dire que les variables et les tableaux sont passés par valeur; les objets sont passés par référence) et également de vérifier attentivement la documentation de l'API de la fonction que vous appelez pour voir si elle renvoie une valeur, un copie d'un tableau, une référence à un tableau ou une référence à un objet.
Cela dit, il est important de noter que la pratique consistant à renvoyer une référence à un tableau ou à un ArrayObject
est généralement quelque chose qui doit être évité, car il donne à l'appelant la possibilité de modifier les données privées de l'instance. Cela «va à l'encontre» de l'encapsulation. Au lieu de cela, il est préférable d'utiliser des 'getters' et des 'setters' à l'ancienne, par exemple:
class Config { private $values = []; public function setValue($key, $value) { $this->values[$key] = $value; } public function getValue($key) { return $this->values[$key]; } } $config = new Config(); $config->setValue('testKey', 'testValue'); echo $config->getValue('testKey'); // echos 'testValue'
Cette approche donne à l'appelant la possibilité de définir ou d'obtenir n'importe quelle valeur dans le tableau sans fournir un accès public à l'élément autrement privé $values
tableau lui-même.
Il n'est pas rare de rencontrer quelque chose comme ça si votre PHP ne fonctionne pas:
$models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); }
Bien qu'il n'y ait absolument rien de mal ici, mais si vous suivez la logique du code, vous constaterez peut-être que l'innocent appel ci-dessus à $valueRepository->findByValue()
aboutit finalement à une requête d'une sorte, telle que:
$result = $connection->query('SELECT `x`,`y` FROM `values` WHERE `value`=' . $inputValue);
En conséquence, chaque itération de la boucle ci-dessus entraînerait une requête distincte dans la base de données. Ainsi, si, par exemple, vous fournissez un tableau de 1 000 valeurs à la boucle, cela génère 1 000 requêtes distinctes vers la ressource! Si un tel script est appelé dans plusieurs threads, il pourrait potentiellement arrêter le système.
Il est donc crucial de reconnaître quand les requêtes sont effectuées par votre code et, dans la mesure du possible, de collecter les valeurs, puis d'exécuter une requête pour récupérer tous les résultats.
Un exemple d'endroit assez courant où les requêtes sont effectuées de manière inefficace (c'est-à-dire dans une boucle) est lorsqu'un formulaire est publié avec une liste de valeurs (ID, par exemple). Ensuite, pour récupérer les données d'enregistrement complètes pour chacun des ID, le code parcourt le tableau et effectue une requête SQL distincte pour chaque ID. Cela ressemblera souvent à ceci:
$data = []; foreach ($ids as $id) { $result = $connection->query('SELECT `x`, `y` FROM `values` WHERE `id` = ' . $id); $data[] = $result->fetch_row(); }
Mais la même chose peut être accomplie beaucoup plus efficacement dans un Célibataire Requête SQL comme suit:
$data = []; if (count($ids)) { $result = $connection->query('SELECT `x`, `y` FROM `values` WHERE `id` IN (' . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } }
Il est donc crucial de reconnaître quand des requêtes sont effectuées, directement ou indirectement, par votre code. Dans la mesure du possible, rassemblez les valeurs, puis exécutez une requête pour récupérer tous les résultats. Pourtant, il faut faire preuve de prudence là aussi, ce qui nous amène à notre prochaine erreur PHP commune ...
Bien que récupérer plusieurs enregistrements à la fois soit certainement plus efficace que d'exécuter une seule requête pour chaque ligne à extraire, une telle approche peut potentiellement conduire à une condition de «mémoire insuffisante» dans libmysqlclient
lors de l'utilisation de PHP mysql
extension.
Pour démontrer, jetons un œil à une boîte de test avec des ressources limitées (512 Mo de RAM), MySQL et php-cli
.
Nous allons amorcer une table de base de données comme celle-ci:
// connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); // create table of 400 columns $query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT'; for ($col = 0; $col query($query); // write 2 million rows for ($row = 0; $row <2000000; $row++) { $query = 'INSERT INTO `test` VALUES ($row'; for ($col = 0; $col query($query); }
OK, vérifions maintenant l'utilisation des ressources:
// connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); echo 'Before: ' . memory_get_peak_usage() . '
'; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1'); echo 'Limit 1: ' . memory_get_peak_usage() . '
'; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000'); echo 'Limit 10000: ' . memory_get_peak_usage() . '
';
Production:
Before: 224704 Limit 1: 224704 Limit 10000: 224704
Cool. Il semble que la requête soit gérée en toute sécurité en interne en termes de ressources.
Pour être sûr, cependant, augmentons encore une fois la limite et fixons-la à 100 000. Oh-oh. Lorsque nous faisons cela, nous obtenons:
PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11
Qu'est-il arrivé?
Le problème ici est la façon dont PHP mysql
le module fonctionne. C’est vraiment juste un proxy pour libmysqlclient
, qui fait le sale boulot. Lorsqu'une partie des données est sélectionnée, elle va directement en mémoire. Comme cette mémoire n’est pas gérée par le gestionnaire PHP, memory_get_peak_usage()
ne montrera aucune augmentation de l'utilisation des ressources car nous augmentons la limite dans notre requête. Cela conduit à des problèmes comme celui démontré ci-dessus où nous sommes amenés à la complaisance en pensant que notre gestion de la mémoire est bonne. Mais en réalité, notre gestion de la mémoire est sérieusement imparfaite et nous pouvons rencontrer des problèmes comme celui montré ci-dessus.
Vous pouvez au moins éviter le headfake ci-dessus (même s'il n'améliorera pas en soi votre utilisation de la mémoire) en utilisant à la place le mysqlnd
module. mysqlnd
est compilé comme une extension PHP native et il Est-ce que utilisez le gestionnaire de mémoire de PHP.
Par conséquent, si nous exécutons le test ci-dessus en utilisant mysqlnd
plutôt que mysql
, nous obtenons une image beaucoup plus réaliste de notre utilisation de la mémoire:
Before: 232048 Limit 1: 324952 Limit 10000: 32572912
Et c’est encore pire que ça, d’ailleurs. Selon la documentation PHP, mysql
utilise deux fois plus de ressources que mysqlnd
pour stocker des données, donc le script d'origine en utilisant mysql
vraiment utilisé encore plus de mémoire que montré ici (environ deux fois plus).
Pour éviter de tels problèmes, envisagez de limiter la taille de vos requêtes et d'utiliser une boucle avec un petit nombre d'itérations; par exemple.:
$totalNumberToFetch = 10000; $portionSize = 100; for ($i = 0; $i query( 'SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize'); }
Quand on considère à la fois cette erreur PHP et erreur n ° 4 ci-dessus, nous nous rendons compte qu'il y a un équilibre sain que votre code doit idéalement atteindre entre, d'une part, le fait que vos requêtes soient trop granulaires et répétitives, et que chacune de vos requêtes individuelles soit trop volumineuse. Comme c'est le cas pour la plupart des choses dans la vie, un équilibre est nécessaire; l'un ou l'autre extrême n'est pas bon et peut causer des problèmes avec PHP qui ne fonctionne pas correctement.
Dans un certain sens, c'est vraiment plus un problème en PHP lui-même que quelque chose que vous rencontreriez lors du débogage de PHP, mais cela n'a jamais été correctement résolu. Le cœur de PHP 6 devait être rendu compatible avec Unicode, mais cela a été suspendu lorsque le développement de PHP 6 a été suspendu en 2010.
Mais cela ne dispense en aucun cas le développeur de bien remettre l'UTF-8 et en évitant l'hypothèse erronée selon laquelle toutes les chaînes seront nécessairement du «simple ancien ASCII». Le code qui ne gère pas correctement les chaînes non ASCII est connu pour l'introduction de gnarly heisenbugs dans votre code. Même simple strlen($_POST['name'])
les appels pourraient causer des problèmes si quelqu'un avec un nom de famille comme «Schrödinger» essayait de s'inscrire à votre système.
Voici une petite liste de contrôle pour éviter de tels problèmes dans votre code:
mb_*
fonctions au lieu des anciennes fonctions de chaîne (assurez-vous que l'extension «multi-octets» est incluse dans votre build PHP).latin1
par défaut).json_encode()
convertit les symboles non ASCII (par exemple, «Schrödinger» devient «Schr u00f6dinger») mais serialize()
Est-ce que ne pas .Une ressource particulièrement précieuse à cet égard est le UTF-8 Primer pour PHP et MySQL poster par Francisco Claria sur ce blog.
$_POST
contiendra toujours vos données POSTMalgré son nom, le $_POST
array ne contient pas toujours vos données POST et peut être facilement trouvé vide. Pour comprendre cela, prenons un exemple. Supposons que nous fassions une requête serveur avec un jQuery.ajax()
appeler comme suit:
// js $.ajax({ url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json' });
(Soit dit en passant, notez le contentType: 'application/json'
ici. Nous envoyons les données au format JSON, ce qui est très populaire pour les API. C'est la valeur par défaut, par exemple, pour la publication dans le AngularJS $http
un service .)
Du côté serveur de notre exemple, nous vidons simplement le $_POST
tableau:
// php var_dump($_POST);
Étonnamment, le résultat sera:
array(0) { }
Pourquoi? Qu'est-il arrivé à notre chaîne JSON {a: 'a', b: 'b'}
?
La réponse est que PHP n'analyse automatiquement une charge utile POST que lorsqu'elle a un type de contenu de application/x-www-form-urlencoded
ou multipart/form-data
. Les raisons en sont historiques - ces deux types de contenu étaient essentiellement les seuls utilisés il y a des années lorsque PHP $_POST
a été mis en œuvre. Donc, avec tout autre type de contenu (même ceux qui sont assez populaires aujourd'hui, comme application/json
), PHP ne charge pas automatiquement la charge utile POST.
Depuis $_POST
est un superglobal, si on le remplace une fois que (de préférence au début de notre script), la valeur modifiée (c'est-à-dire, y compris la charge utile POST) sera alors référençable dans tout notre code. Ceci est important puisque $_POST
est couramment utilisé par les frameworks PHP et presque tous les scripts personnalisés pour extraire et transformer les données de demande.
Ainsi, par exemple, lors du traitement d'une charge utile POST avec un type de contenu de application/json
, nous devons analyser manuellement le contenu de la requête (c'est-à-dire décoder les données JSON) et remplacer le $_POST
variable, comme suit:
// php $_POST = json_decode(file_get_contents('php://input'), true);
Ensuite, lorsque nous vidons le $_POST
array, nous voyons qu'il inclut correctement la charge utile POST; par exemple.:
array(2) { ['a']=> string(1) 'a' ['b']=> string(1) 'b' }
Regardez cet exemple de code et essayez de deviner ce qu'il imprimera:
for ($c = 'a'; $c <= 'z'; $c++) { echo $c . '
'; }
Si vous avez répondu «a» à «z», vous serez peut-être surpris de savoir que vous vous êtes trompé.
Oui, il imprimera «a» à «z», mais ensuite il sera également imprimer «aa» à «yz». Voyons pourquoi.
En PHP, il n'y a pas de char
Type de données; seulement string
est disponible. Dans cet esprit, incrémenter le string
z
en PHP renvoie aa
:
php> $c = 'z'; echo ++$c . '
'; aa
Pourtant, pour compliquer davantage les choses, aa
est lexicographiquement Moins que z
:
php> var_export((boolean)('aa' <'z')) . '
'; true
C'est pourquoi l'exemple de code présenté ci-dessus imprime les lettres a
via z
, mais alors également impressions aa
à yz
. Il s'arrête lorsqu'il atteint za
, qui est la première valeur qu'il rencontre comme étant 'supérieur à' z
:
php> var_export((boolean)('za' <'z')) . '
'; false
Cela étant, voici une façon de correctement parcourez les valeurs «a» à «z» en PHP:
for ($i = ord('a'); $i <= ord('z'); $i++) { echo chr($i) . '
'; }
Ou bien:
$letters = range('a', 'z'); for ($i = 0; $i Erreur courante n ° 9: Ignorer les normes de codage
Bien que le fait d'ignorer les normes de codage n'entraîne pas directement le besoin de déboguer du code PHP, c'est probablement l'une des choses les plus importantes à discuter ici.
Ignorer les normes de codage peut entraîner toute une série de problèmes sur un projet. Au mieux, il en résulte un code incohérent (puisque chaque développeur «fait ses propres choses»). Mais au pire, il produit du code PHP qui ne fonctionne pas ou qui peut être difficile (parfois presque impossible) à naviguer, ce qui rend extrêmement difficile le débogage, l'amélioration, la maintenance. Et cela signifie une productivité réduite pour votre équipe, y compris beaucoup d'efforts inutiles (ou du moins inutiles).
Heureusement pour les développeurs PHP, il existe la recommandation sur les normes PHP (PSR), qui comprend les cinq normes suivantes:
- PSR-0 : Norme de chargement automatique
- PSR-1 : Norme de codage de base
- PSR-2 : Guide de style de codage
- PSR-3 : Interface de l'enregistreur
- PSR-4 : Chargeur automatique
PSR a été créé à l'origine sur la base des contributions des mainteneurs des plateformes les plus reconnues du marché. Zend, Drupal, Symfony, Joomla et autres ont contribué à ces normes et les respectent désormais. Même PEAR, qui a tenté d'être un standard pendant des années auparavant, participe maintenant au PSR.
Dans un certain sens, peu importe votre norme de codage, tant que vous vous entendez sur une norme et que vous vous y tenez, mais suivre le PSR est généralement une bonne idée, sauf si vous avez une raison impérieuse pour votre projet de faire autrement. . De plus en plus d'équipes et de projets se conforment au PSR. Il est définitivement reconnu à ce stade comme 'le' standard par la majorité des développeurs PHP, donc son utilisation aidera à garantir que les nouveaux développeurs connaissent et maîtrisent votre norme de codage lorsqu'ils rejoignent votre équipe.
Erreur commune n ° 10: mauvaise utilisation empty()
Certains développeurs PHP aiment utiliser empty()
for boolean vérifie à peu près tout. Il y a des cas, cependant, où cela peut prêter à confusion.
Tout d'abord, revenons aux tableaux et ArrayObject
instances (qui imitent des tableaux). Compte tenu de leur similitude, il est facile de supposer que les tableaux et ArrayObject
les instances se comporteront de la même manière. Cela s'avère cependant être une hypothèse dangereuse. Par exemple, en PHP 5.0:
// PHP 5.0 or later: $array = []; var_dump(empty($array)); // outputs bool(true) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false) // why don't these both produce the same output?
Et pour aggraver encore les choses, les résultats auraient été différents avant PHP 5.0:
// Prior to PHP 5.0: $array = []; var_dump(empty($array)); // outputs bool(false) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false)
Cette approche est malheureusement assez populaire. Par exemple, c'est la manière ZendDbTableGateway
de Zend Framework 2 renvoie des données lors de l'appel current()
sur TableGateway::select()
résultat comme le suggère le doc. Le développeur peut facilement être victime de cette erreur avec de telles données.
Pour éviter ces problèmes, la meilleure approche pour vérifier les structures de tableau vides consiste à utiliser count()
:
// Note that this work in ALL versions of PHP (both pre and post 5.0): $array = []; var_dump(count($array)); // outputs int(0) $array = new ArrayObject(); var_dump(count($array)); // outputs int(0)
Et d'ailleurs, puisque PHP lance 0
à false
, count()
peut également être utilisé dans if ()
conditions pour vérifier les tableaux vides. Il est également intéressant de noter qu'en PHP, count()
est une complexité constante (opération O(1)
) sur les tableaux, ce qui montre encore plus clairement qu’il s’agit du bon choix.
Un autre exemple quand empty()
peut être dangereux en le combinant avec la fonction de classe magique __get()
. Définissons deux classes et avons un test
propriété dans les deux.
Commençons par définir un Regular
classe qui comprend test
comme une propriété normale:
class Regular { public $test = 'value'; }
Définissons ensuite un Magic
classe qui utilise la magie __get()
opérateur pour accéder à son test
propriété:
class Magic { private $values = ['test' => 'value']; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } }
OK, voyons maintenant ce qui se passe lorsque nous tentons d’accéder à test
propriété de chacune de ces classes:
$regular = new Regular(); var_dump($regular->test); // outputs string(4) 'value' $magic = new Magic(); var_dump($magic->test); // outputs string(4) 'value'
Bien pour l'instant.
Mais voyons maintenant ce qui se passe lorsque nous appelons empty()
sur chacun de ceux-ci:
var_dump(empty($regular->test)); // outputs bool(false) var_dump(empty($magic->test)); // outputs bool(true)
Pouah. Donc, si nous nous appuyons sur empty()
, nous pouvons être induits en erreur en pensant que le test
propriété de $magic
est vide, alors qu'en réalité il vaut 'value'
.
Malheureusement, si une classe utilise la magie __get()
pour récupérer la valeur d’une propriété, il n’existe pas de moyen infaillible de vérifier si cette valeur de propriété est vide ou non. En dehors de la portée de la classe, vous ne pouvez vraiment vérifier que si un null
value sera renvoyée, et cela ne signifie pas nécessairement que la clé correspondante n'est pas définie, car elle pourrait a été ensemble à null
.
En revanche, si nous essayons de référencer une propriété inexistante d'un Regular
classe, nous recevrons un avis similaire à ce qui suit:
Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10 Call Stack: 0.0012 234704 1. {main}() /path/to/test.php:0
Le point principal ici est donc que le empty()
La méthode doit être utilisée avec précaution car elle peut se prêter à des résultats déroutants - voire potentiellement trompeurs - si l'on ne fait pas attention.
Emballer
La facilité d'utilisation de PHP peut endormir les développeurs dans un faux sentiment de confort, les laissant vulnérables à un débogage PHP prolongé en raison de certaines nuances et particularités du langage. Cela peut entraîner un dysfonctionnement de PHP et des problèmes tels que ceux décrits ici.
Le langage PHP a considérablement évolué au cours de ses 20 ans d'histoire. Se familiariser avec ses subtilités est une entreprise valable, car elle contribuera à logiciel que vous produisez est plus évolutif, robuste et maintenable.