Codice PHP buggy: i 10 errori più comuni commessi dagli sviluppatori PHP

Pubblicato: 2022-03-11

PHP rende relativamente facile costruire un sistema basato sul web, che è in gran parte il motivo della sua popolarità. Ma nonostante la sua facilità d'uso, PHP si è evoluto in un linguaggio piuttosto sofisticato con molti framework, sfumature e sottigliezze che possono mordere gli sviluppatori, portando a ore di debugging snervante. Questo articolo mette in evidenza dieci degli errori più comuni di cui gli sviluppatori PHP devono fare attenzione.

Errore comune n. 1: lasciare i riferimenti all'array penzolanti dopo i cicli foreach

Non sei sicuro di come utilizzare i cicli foreach in PHP? L'uso dei riferimenti nei cicli foreach può essere utile se si desidera operare su ciascun elemento dell'array su cui si sta eseguendo l'iterazione. Per esempio:

 $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6, 8)

Il problema è che, se non stai attento, questo può avere anche alcuni effetti collaterali e conseguenze indesiderati. In particolare, nell'esempio precedente, dopo l'esecuzione del codice, $value rimarrà nell'ambito e conterrà un riferimento all'ultimo elemento nell'array. Operazioni successive che coinvolgono $value potrebbero quindi finire per modificare involontariamente l'ultimo elemento nell'array.

La cosa principale da ricordare è che foreach non crea un ambito. Pertanto, $value nell'esempio precedente è un riferimento all'interno dell'ambito superiore dello script. Ad ogni iterazione foreach imposta il riferimento per puntare all'elemento successivo di $array . Dopo il completamento del ciclo, quindi, $value punta ancora all'ultimo elemento di $array e rimane nell'ambito.

Ecco un esempio del tipo di bug evasivi e confusi a cui ciò può portare:

 $array = [1, 2, 3]; echo implode(',', $array), "\n"; foreach ($array as &$value) {} // by reference echo implode(',', $array), "\n"; foreach ($array as $value) {} // by value (ie, copy) echo implode(',', $array), "\n";

Il codice sopra produrrà quanto segue:

 1,2,3 1,2,3 1,2,2

No, non è un errore di battitura. L'ultimo valore sull'ultima riga è infatti un 2, non un 3.

Come mai?

Dopo aver attraversato il primo ciclo foreach , $array rimane invariato ma, come spiegato sopra, $value viene lasciato come riferimento sospeso all'ultimo elemento in $array (poiché quel ciclo foreach ha avuto accesso $value per riferimento ).

Di conseguenza, quando attraversiamo il secondo ciclo foreach , sembra che succedano "cose ​​strane". In particolare, poiché l'accesso $value è ora in base al valore (cioè, tramite copy ), foreach copia ogni elemento sequenziale $array in $value in ogni passaggio del ciclo. Di conseguenza, ecco cosa succede durante ogni passaggio del secondo ciclo foreach :

  • Passaggio 1: copia $array[0] (cioè "1") in $value (che è un riferimento a $array[2] ), quindi $array[2] ora è uguale a 1. Quindi $array ora contiene [1, 2, 1].
  • Passaggio 2: copia $array[1] (cioè "2") in $value (che è un riferimento a $array[2] ), quindi $array[2] ora è uguale a 2. Quindi $array ora contiene [1, 2, 2].
  • Passaggio 3: copia $array[2] (che ora equivale a "2") in $value (che è un riferimento a $array[2] ), quindi $array[2] è ancora uguale a 2. Quindi $array ora contiene [1 , 2, 2].

Per ottenere comunque il vantaggio di utilizzare i riferimenti nei cicli foreach senza correre il rischio di questo tipo di problemi, chiamare unset() sulla variabile, subito dopo il ciclo foreach , per rimuovere il riferimento; per esempio:

 $arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value no longer references $arr[3]

Errore comune n. 2: incomprensione del comportamento isset()

Nonostante il nome, isset() non solo restituisce false se un elemento non esiste, ma restituisce anche false per valori null .

Questo comportamento è più problematico di quanto potrebbe sembrare all'inizio ed è una fonte comune di problemi.

Considera quanto segue:

 $data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // do something here if 'keyShouldBeSet' is not set }

L'autore di questo codice presumibilmente voleva verificare se keyShouldBeSet era impostato in $data . Ma, come discusso, anche isset($data['keyShouldBeSet']) restituirà false se $data['keyShouldBeSet'] è stato impostato, ma è stato impostato su null . Quindi la logica di cui sopra è viziata.

Ecco un altro esempio:

 if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; }

Il codice sopra presuppone che se $_POST['active'] restituisce true , allora postData sarà necessariamente impostato e quindi isset($postData) restituirà true . Quindi, al contrario, il codice sopra presuppone che l' unico modo in cui isset($postData) restituirà false è se $_POST['active'] restituito false .

Non.

Come spiegato, anche isset($postData) restituirà false se $postData è stato impostato su null . È quindi possibile che isset($postData) restituisca false anche se $_POST['active'] restituisce true . Quindi, ancora una volta, la logica di cui sopra è viziata.

E a proposito, come punto collaterale, se l'intento nel codice sopra era davvero quello di controllare di nuovo se $_POST['active'] restituisce true, fare affidamento su isset() per questo è stata comunque una decisione di codifica sbagliata. Invece, sarebbe stato meglio ricontrollare $_POST['active'] ; cioè:

 if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; }

Per i casi, tuttavia, in cui è importante verificare se una variabile è stata davvero impostata (ad esempio, per distinguere tra una variabile che non è stata impostata e una variabile che è stata impostata su null ), il metodo array_key_exists() è molto più robusto soluzione.

Ad esempio, potremmo riscrivere il primo dei due esempi precedenti come segue:

 $data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // do this if 'keyShouldBeSet' isn't set }

Inoltre, combinando array_key_exists() con get_defined_vars() , possiamo verificare in modo affidabile se una variabile all'interno dell'ambito corrente è stata impostata o meno:

 if (array_key_exists('varShouldBeSet', get_defined_vars())) { // variable $varShouldBeSet exists in current scope }

Errore comune n. 3: confusione sul ritorno per riferimento rispetto al valore

Considera questo frammento di codice:

 class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];

Se esegui il codice sopra, otterrai quanto segue:

 PHP Notice: Undefined index: test in /path/to/my/script.php on line 21

Cosa c'è che non va?

Il problema è che il codice precedente confonde gli array restituiti per riferimento con gli array restituiti per valore. A meno che tu non dica esplicitamente a PHP di restituire un array per riferimento (cioè, usando & ), PHP restituirà per impostazione predefinita l'array "per valore". Ciò significa che verrà restituita una copia dell'array e quindi la funzione chiamata e il chiamante non accederanno alla stessa istanza dell'array.

Quindi la chiamata precedente a getValues() restituisce una copia dell'array $values ​​anziché un riferimento ad esso. Con questo in mente, rivisitiamo le due linee chiave dell'esempio sopra:

 // 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'];

Una possibile soluzione sarebbe salvare la prima copia dell'array $values ​​restituito da getValues() e quindi operare su quella copia successivamente; per esempio:

 $vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test'];

Quel codice funzionerà correttamente (ovvero, produrrà test senza generare alcun messaggio di "indice non definito"), ma a seconda di ciò che stai cercando di ottenere, questo approccio potrebbe essere adeguato o meno. In particolare, il codice precedente non modificherà l'array $values ​​originale. Quindi, se vuoi che le tue modifiche (come l'aggiunta di un elemento 'test') influiscano sull'array originale, dovresti invece modificare la funzione getValues() per restituire un riferimento all'array $values ​​stesso. Questo viene fatto aggiungendo un & prima del nome della funzione, indicando così che dovrebbe restituire un riferimento; cioè:

 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'];

L'output di questo sarà test , come previsto.

Ma per rendere le cose più confuse, considera invece il seguente frammento di codice:

 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'];

Se hai intuito che ciò avrebbe comportato lo stesso errore "indice non definito" del nostro esempio di array precedente, ti sei sbagliato. In effetti, questo codice funzionerà perfettamente. Il motivo è che, a differenza degli array, PHP passa sempre gli oggetti per riferimento . ( ArrayObject è un oggetto SPL, che imita completamente l'utilizzo degli array, ma funziona come un oggetto.)

Come dimostrano questi esempi, in PHP non è sempre del tutto ovvio se si tratta di una copia o di un riferimento. È quindi essenziale comprendere questi comportamenti predefiniti (ovvero, le variabili e gli array vengono passati per valore; gli oggetti vengono passati per riferimento) e anche controllare attentamente la documentazione API per la funzione che stai chiamando per vedere se sta restituendo un valore, un copia di un array, un riferimento a un array o un riferimento a un oggetto.

Detto questo, è importante notare che la pratica di restituire un riferimento a un array o un ArrayObject è generalmente qualcosa che dovrebbe essere evitata, poiché fornisce al chiamante la possibilità di modificare i dati privati ​​dell'istanza. Questo "vola in faccia" dell'incapsulamento. Invece, è meglio usare "getter" e "setter" vecchio stile, ad esempio:

 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'

Questo approccio offre al chiamante la possibilità di impostare o ottenere qualsiasi valore nell'array senza fornire l'accesso pubblico all'array $values , altrimenti privato.

Errore comune n. 4: eseguire query in un ciclo

Non è raro imbattersi in qualcosa del genere se il tuo PHP non funziona:

 $models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); }

Anche se qui potrebbe non esserci assolutamente nulla di sbagliato, ma se segui la logica nel codice, potresti scoprire che la chiamata dall'aspetto innocente sopra a $valueRepository->findByValue() alla fine si traduce in una query di qualche tipo, come ad esempio:

 $result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);

Di conseguenza, ogni iterazione del ciclo precedente risulterebbe in una query separata al database. Quindi se, ad esempio, fornissi un array di 1.000 valori al ciclo, genererebbe 1.000 query separate alla risorsa! Se un tale script viene chiamato in più thread, potrebbe potenzialmente portare il sistema a una brusca battuta d'arresto.

È quindi fondamentale riconoscere quando le query vengono eseguite dal codice e, quando possibile, raccogliere i valori e quindi eseguire una query per recuperare tutti i risultati.

Un esempio di un luogo abbastanza comune in cui le query vengono eseguite in modo inefficiente (ad esempio, in un ciclo) è quando un modulo viene inviato con un elenco di valori (ID, ad esempio). Quindi, per recuperare i dati del record completo per ciascuno degli ID, il codice scorrerà l'array ed eseguirà una query SQL separata per ogni ID. Questo sarà spesso simile a questo:

 $data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); }

Ma la stessa cosa può essere eseguita in modo molto più efficiente in una singola query SQL come segue:

 $data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } }

È quindi fondamentale riconoscere quando vengono effettuate query, direttamente o indirettamente, dal codice. Quando possibile, raccogli i valori e quindi esegui una query per recuperare tutti i risultati. Tuttavia, anche lì è necessario prestare attenzione, il che ci porta al nostro prossimo errore PHP comune...

Errore comune n. 5: errori e inefficienze nell'utilizzo della memoria

Sebbene il recupero di più record contemporaneamente sia decisamente più efficiente dell'esecuzione di una singola query per ogni riga da recuperare, un tale approccio può potenzialmente portare a una condizione di "memoria insufficiente" in libmysqlclient quando si utilizza l'estensione mysql di PHP.

Per dimostrare, diamo un'occhiata a un test box con risorse limitate (512 MB di RAM), MySQL e php-cli .

Faremo il bootstrap di una tabella di database come questa:

 // 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 < 400; $col++) { $query .= ", `col$col` CHAR(10) NOT NULL"; } $query .= ');'; $connection->query($query); // write 2 million rows for ($row = 0; $row < 2000000; $row++) { $query = "INSERT INTO `test` VALUES ($row"; for ($col = 0; $col < 400; $col++) { $query .= ', ' . mt_rand(1000000000, 9999999999); } $query .= ')'; $connection->query($query); }

OK, ora controlliamo l'utilizzo delle risorse:

 // connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); echo "Before: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1'); echo "Limit 1: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000'); echo "Limit 10000: " . memory_get_peak_usage() . "\n";

Produzione:

 Before: 224704 Limit 1: 224704 Limit 10000: 224704

Freddo. Sembra che la query sia gestita internamente in modo sicuro in termini di risorse.

Per sicurezza, però, aumentiamo il limite ancora una volta e impostiamolo a 100.000. Uh Oh. Quando lo facciamo, otteniamo:

 PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11

Quello che è successo?

Il problema qui è il modo in cui funziona il modulo mysql di PHP. In realtà è solo un proxy per libmysqlclient , che fa il lavoro sporco. Quando una parte di dati è selezionata, va direttamente in memoria. Poiché questa memoria non è gestita dal gestore di PHP, memory_get_peak_usage() non mostrerà alcun aumento nell'utilizzo delle risorse mentre aumenteremo il limite nella nostra query. Questo porta a problemi come quello dimostrato sopra, in cui veniamo indotti a pensare all'autocompiacimento che la nostra gestione della memoria vada bene. Ma in realtà, la nostra gestione della memoria è seriamente difettosa e possiamo riscontrare problemi come quello mostrato sopra.

Puoi almeno evitare l'headfake di cui sopra (sebbene non migliorerà di per sé l'utilizzo della memoria) utilizzando invece il modulo mysqlnd . mysqlnd è compilato come estensione PHP nativa e utilizza il gestore della memoria di PHP.

Pertanto, se eseguiamo il test precedente utilizzando mysqlnd anziché mysql , otteniamo un'immagine molto più realistica del nostro utilizzo della memoria:

 Before: 232048 Limit 1: 324952 Limit 10000: 32572912

Ed è anche peggio di così, tra l'altro. Secondo la documentazione PHP, mysql utilizza il doppio delle risorse di mysqlnd per archiviare i dati, quindi lo script originale che utilizza mysql utilizzava davvero ancora più memoria di quella mostrata qui (circa il doppio).

Per evitare tali problemi, considera di limitare la dimensione delle tue query e di utilizzare un ciclo con un numero ridotto di iterazioni; per esempio:

 $totalNumberToFetch = 10000; $portionSize = 100; for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) { $limitFrom = $portionSize * $i; $res = $connection->query( "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize"); }

Quando consideriamo sia questo errore PHP che l'errore n. 4 sopra, ci rendiamo conto che c'è un sano equilibrio che il tuo codice deve idealmente raggiungere tra, da un lato, avere le tue query troppo granulari e ripetitive, rispetto a ciascuna delle tue le singole query sono troppo grandi. Come è vero per la maggior parte delle cose nella vita, è necessario equilibrio; entrambi gli estremi non vanno bene e possono causare problemi con PHP che non funziona correttamente.

Errore comune n. 6: ignorare i problemi di Unicode/UTF-8

In un certo senso, questo è davvero più un problema in PHP stesso che qualcosa in cui ti imbatteresti durante il debug di PHP, ma non è mai stato affrontato adeguatamente. Il core di PHP 6 doveva essere reso compatibile con Unicode, ma è stato sospeso quando lo sviluppo di PHP 6 è stato sospeso nel 2010.

Ma ciò non esonera in alcun modo lo sviluppatore dal consegnare correttamente UTF-8 ed evitare l'assunto erroneo che tutte le stringhe saranno necessariamente "semplice vecchio ASCII". Il codice che non riesce a gestire correttamente le stringhe non ASCII è noto per aver introdotto gnarly heisenbugs nel tuo codice. Anche semplici chiamate strlen($_POST['name']) potrebbero causare problemi se qualcuno con un cognome come "Schrodinger" tentasse di registrarsi nel tuo sistema.

Ecco una piccola lista di controllo per evitare tali problemi nel codice:

  • Se non sai molto su Unicode e UTF-8, dovresti almeno imparare le basi. C'è un ottimo primer qui.
  • Assicurati di usare sempre le funzioni mb_* invece delle vecchie funzioni di stringa (assicurati che l'estensione "multibyte" sia inclusa nella tua build PHP).
  • Assicurati che il database e le tabelle siano impostati per utilizzare Unicode (molte build di MySQL usano ancora latin1 per impostazione predefinita).
  • Ricorda che json_encode() converte i simboli non ASCII (ad esempio, "Schrodinger" diventa "Schr\u00f6dinger") ma serialize() non lo fa.
  • Assicurati che i tuoi file di codice PHP siano anche codificati in UTF-8 per evitare collisioni durante la concatenazione di stringhe con costanti di stringa codificate o configurate.

Una risorsa particolarmente preziosa a questo proposito è il post UTF-8 Primer per PHP e MySQL di Francisco Claria su questo blog.

Errore comune n. 7: supponendo $_POST conterrà sempre i tuoi dati POST

Nonostante il nome, l'array $_POST non conterrà sempre i tuoi dati POST e può essere facilmente trovato vuoto. Per capirlo, diamo un'occhiata a un esempio. Supponiamo di fare una richiesta al server con una chiamata jQuery.ajax() come segue:

 // js $.ajax({ url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json' });

(Per inciso, nota il contentType: 'application/json' qui. Inviamo i dati come JSON, che è abbastanza popolare per le API. È l'impostazione predefinita, ad esempio, per la pubblicazione nel servizio AngularJS $http .)

Sul lato server del nostro esempio, scarichiamo semplicemente l'array $_POST :

 // php var_dump($_POST);

Sorprendentemente, il risultato sarà:

 array(0) { }

Come mai? Cosa è successo alla nostra stringa JSON {a: 'a', b: 'b'} ?

La risposta è che PHP analizza automaticamente un payload POST solo quando ha un tipo di contenuto application/x-www-form-urlencoded o multipart/form-data . Le ragioni di ciò sono storiche: questi due tipi di contenuto erano essenzialmente gli unici utilizzati anni fa quando è stato implementato $_POST di PHP. Quindi, con qualsiasi altro tipo di contenuto (anche quelli che sono abbastanza popolari oggi, come application/json ), PHP non carica automaticamente il payload POST.

Poiché $_POST è un superglobale, se lo sovrascriviamo una volta (preferibilmente all'inizio del nostro script), il valore modificato (cioè, incluso il payload POST) sarà quindi referenziabile in tutto il nostro codice. Questo è importante poiché $_POST è comunemente usato dai framework PHP e quasi tutti gli script personalizzati per estrarre e trasformare i dati delle richieste.

Quindi, ad esempio, quando si elabora un payload POST con un tipo di contenuto application/json , è necessario analizzare manualmente il contenuto della richiesta (ovvero decodificare i dati JSON) e sovrascrivere la variabile $_POST , come segue:

 // php $_POST = json_decode(file_get_contents('php://input'), true);

Quindi, quando scarichiamo l'array $_POST , vediamo che include correttamente il payload POST; per esempio:

 array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }

Errore comune n. 8: pensare che PHP supporti un tipo di dati di carattere

Guarda questo esempio di codice e prova a indovinare cosa stamperà:

 for ($c = 'a'; $c <= 'z'; $c++) { echo $c . "\n"; }

Se hai risposto da "a" a "z", potresti essere sorpreso di sapere che avevi torto.

Sì, stamperà da 'a' a 'z', ma poi stamperà anche da 'aa' a 'yz'. Vediamo perché.

In PHP non esiste un tipo di dati char ; è disponibile solo string . Con questo in mente, incrementando la string z in PHP si ottiene aa :

 php> $c = 'z'; echo ++$c . "\n"; aa

Tuttavia, per confondere ulteriormente le cose, aa è lessicograficamente inferiore a z :

 php> var_export((boolean)('aa' < 'z')) . "\n"; true

Ecco perché il codice di esempio presentato sopra stampa le lettere a a z , ma poi stampa anche da aa a yz . Si ferma quando raggiunge za , che è il primo valore che incontra che è “maggiore di” z :

 php> var_export((boolean)('za' < 'z')) . "\n"; false

Stando così le cose, ecco un modo per scorrere correttamente i valori da "a" a "z" in PHP:

 for ($i = ord('a'); $i <= ord('z'); $i++) { echo chr($i) . "\n"; }

O in alternativa:

 $letters = range('a', 'z'); for ($i = 0; $i < count($letters); $i++) { echo $letters[$i] . "\n"; }

Errore comune n. 9: ignorare gli standard di codifica

Sebbene ignorare gli standard di codifica non comporti direttamente la necessità di eseguire il debug del codice PHP, è probabilmente ancora una delle cose più importanti da discutere qui.

Ignorare gli standard di codifica può causare tutta una serie di problemi in un progetto. Nella migliore delle ipotesi, si traduce in un codice incoerente (dal momento che ogni sviluppatore sta "facendo le proprie cose"). Ma nel peggiore dei casi, produce codice PHP che non funziona o può essere difficile (a volte quasi impossibile) da navigare, rendendo estremamente difficile il debug, il miglioramento, la manutenzione. E ciò significa una produttività ridotta per il tuo team, inclusi molti sforzi sprecati (o almeno inutili).

Fortunatamente per gli sviluppatori PHP, esiste la PHP Standards Recommendation (PSR), composta dai seguenti cinque standard:

  • PSR-0: caricamento automatico standard
  • PSR-1: standard di codifica di base
  • PSR-2: Guida allo stile di codifica
  • PSR-3: interfaccia logger
  • PSR-4: caricatore automatico

PSR è stato originariamente creato sulla base degli input dei manutentori delle piattaforme più riconosciute sul mercato. Zend, Drupal, Symfony, Joomla e altri hanno contribuito a questi standard e ora li stanno seguendo. Anche PEAR, che ha tentato di essere uno standard per anni prima, partecipa ora al PSR.

In un certo senso, non importa quasi quale sia il tuo standard di codifica, purché tu sia d'accordo su uno standard e ti attieni ad esso, ma seguire il PSR è generalmente una buona idea a meno che tu non abbia qualche ragione convincente sul tuo progetto per fare diversamente . Sempre più team e progetti sono conformi al PSR. Tt è sicuramente riconosciuto a questo punto come "lo" standard dalla maggior parte degli sviluppatori PHP, quindi il suo utilizzo aiuterà a garantire che i nuovi sviluppatori abbiano familiarità e si sentano a proprio agio con il tuo standard di codifica quando si uniscono al tuo team.

Errore comune n. 10: uso improprio di empty()

Ad alcuni sviluppatori PHP piace usare empty() per i controlli booleani per quasi tutto. Ci sono casi, tuttavia, in cui ciò può creare confusione.

Innanzitutto, torniamo agli array e alle istanze ArrayObject (che imitano gli array). Data la loro somiglianza, è facile presumere che gli array e le istanze di ArrayObject si comporteranno in modo identico. Questo si rivela, tuttavia, un presupposto pericoloso. Ad esempio, in 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?

E come se non bastasse, i risultati sarebbero stati diversi prima di 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)

Questo approccio è purtroppo abbastanza popolare. Ad esempio, questo è il modo in cui Zend\Db\TableGateway di Zend Framework 2 restituisce i dati quando si chiama current() sul TableGateway::select() come suggerisce il documento. Lo sviluppatore può facilmente diventare vittima di questo errore con tali dati.

Per evitare questi problemi, l'approccio migliore per verificare la presenza di strutture di array vuote consiste nell'usare 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)

E per inciso, poiché PHP esegue il cast da 0 a false , count() può essere utilizzato anche all'interno delle condizioni if () per verificare la presenza di array vuoti. Vale anche la pena notare che, in PHP, count() è una complessità costante (operazione O(1) ) sugli array, il che rende ancora più chiaro che è la scelta giusta.

Un altro esempio in cui empty() può essere pericoloso è quando lo si combina con la funzione di classe magica __get() . Definiamo due classi e disponiamo di una proprietà test in entrambe.

Per prima cosa definiamo una classe Regular che include test come proprietà normale:

 class Regular { public $test = 'value'; }

Quindi definiamo una classe Magic che utilizza l'operatore magic __get() per accedere alla sua proprietà test :

 class Magic { private $values = ['test' => 'value']; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } }

OK, ora vediamo cosa succede quando tentiamo di accedere alla proprietà test di ciascuna di queste classi:

 $regular = new Regular(); var_dump($regular->test); // outputs string(4) "value" $magic = new Magic(); var_dump($magic->test); // outputs string(4) "value"

Finora bene.

Ma ora vediamo cosa succede quando chiamiamo empty() su ciascuno di questi:

 var_dump(empty($regular->test)); // outputs bool(false) var_dump(empty($magic->test)); // outputs bool(true)

Uffa. Quindi, se ci affidiamo a empty() , possiamo essere indotti in errore nel credere che la proprietà test di $magic sia vuota, mentre in realtà è impostata su 'value' .

Sfortunatamente, se una classe utilizza la funzione magica __get() per recuperare il valore di una proprietà, non esiste un modo infallibile per verificare se il valore della proprietà è vuoto o meno. Al di fuori dell'ambito della classe, puoi davvero solo verificare se verrà restituito un valore null , e ciò non significa necessariamente che la chiave corrispondente non sia impostata, poiché in realtà potrebbe essere stata impostata su null .

Al contrario, se proviamo a fare riferimento a una proprietà inesistente di un'istanza di classe Regular , otterremo un avviso simile al seguente:

 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

Quindi il punto principale qui è che il metodo empty() dovrebbe essere usato con attenzione in quanto può prestarsi a risultati confusi o addirittura potenzialmente fuorvianti, se non si sta attenti.

Incartare

La facilità d'uso di PHP può cullare gli sviluppatori in un falso senso di comfort, lasciandosi vulnerabili a un lungo debug di PHP a causa di alcune delle sfumature e delle idiosincrasie del linguaggio. Ciò può comportare il mancato funzionamento di PHP e problemi come quelli descritti qui.

Il linguaggio PHP si è evoluto in modo significativo nel corso dei suoi 20 anni di storia. Familiarizzare con le sue sottigliezze è uno sforzo utile, in quanto aiuterà a garantire che il software che produci sia più scalabile, robusto e manutenibile.