Błędny kod PHP: 10 najczęstszych błędów popełnianych przez programistów PHP
Opublikowany: 2022-03-11PHP sprawia, że stosunkowo łatwo jest zbudować system internetowy, co jest główną przyczyną jego popularności. Jednak pomimo łatwości użycia, PHP przekształciło się w dość wyrafinowany język z wieloma frameworkami, niuansami i subtelnościami, które mogą ugryźć programistów, prowadząc do godzin debugowania, które ciągnie za włos. Ten artykuł przedstawia dziesięć najczęstszych błędów, których programiści PHP muszą się wystrzegać.
Powszechny błąd nr 1: pozostawienie nieaktualnych odwołań do tablicy po pętlach foreach
Nie wiesz, jak używać pętli foreach w PHP? Używanie referencji w pętlach foreach
może być przydatne, jeśli chcesz operować na każdym elemencie w tablicy, nad którym iterujesz. Na przykład:
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6, 8)
Problem polega na tym, że jeśli nie jesteś ostrożny, może to mieć również niepożądane skutki uboczne i konsekwencje. W szczególności w powyższym przykładzie, po wykonaniu kodu, $value
pozostanie w zakresie i będzie zawierać odwołanie do ostatniego elementu w tablicy. Kolejne operacje związane z $value
mogą zatem przypadkowo zakończyć modyfikację ostatniego elementu w tablicy.
Najważniejszą rzeczą do zapamiętania jest to, że foreach
nie tworzy zakresu. Zatem $value
w powyższym przykładzie jest referencją w górnym zakresie skryptu. W każdej iteracji foreach
ustawia referencję wskazującą na następny element $array
. Po zakończeniu pętli $value
nadal wskazuje ostatni element $array
i pozostaje w zasięgu.
Oto przykład błędów wymijających i mylących, do których może to prowadzić:
$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";
Powyższy kod wygeneruje następujące dane:
1,2,3 1,2,3 1,2,2
Nie, to nie literówka. Ostatnia wartość w ostatnim wierszu to rzeczywiście 2, a nie 3.
Czemu?
Po przejściu przez pierwszą pętlę foreach
, $array
pozostaje niezmieniona, ale, jak wyjaśniono powyżej, $value
pozostaje jako wiszące odwołanie do ostatniego elementu w $array
(ponieważ pętla foreach
uzyskała dostęp do $value
przez referencję ).
W rezultacie, gdy przechodzimy przez drugą pętlę foreach
, pojawiają się „dziwne rzeczy”. W szczególności, ponieważ dostęp do $value
jest teraz dostępny przez wartość (tj. przez copy ), foreach
kopiuje każdy kolejny element $array
do $value
w każdym kroku pętli. W rezultacie oto, co dzieje się na każdym etapie drugiej pętli foreach
:
- Przekaż 1: Kopiuje
$array[0]
(tj. „1”) do$value
(która jest odniesieniem do$array[2]
), więc$array[2]
teraz równa się 1. Zatem$array
zawiera teraz [1, 2, 1]. - Przekaż 2: Kopiuje
$array[1]
(tj. „2”) do$value
(która jest odniesieniem do$array[2]
), więc$array[2]
teraz równa się 2. Zatem$array
zawiera teraz [1, 2, 2]. - Przekaż 3: Kopiuje
$array[2]
(co teraz równa się „2”) do$value
(która jest odniesieniem do$array[2]
), więc$array[2]
nadal wynosi 2. Zatem$array
zawiera teraz [1 , 2, 2].
Aby nadal czerpać korzyści z używania referencji w pętlach foreach
bez narażania się na tego rodzaju problemy, wywołaj unset()
na zmiennej, bezpośrednio po pętli foreach
, aby usunąć referencję; np:
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value no longer references $arr[3]
Powszechny błąd nr 2: Niezrozumienie zachowania isset()
Pomimo swojej nazwy, isset()
nie tylko zwraca fałsz, jeśli element nie istnieje, ale także zwraca false
dla wartości null
.
Takie zachowanie jest bardziej problematyczne niż mogłoby się początkowo wydawać i jest częstym źródłem problemów.
Rozważ następujące:
$data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // do something here if 'keyShouldBeSet' is not set }
Przypuszczalnie autor tego kodu chciał sprawdzić, czy keyShouldBeSet
zostało ustawione w $data
. Ale, jak wspomniano, isset($data['keyShouldBeSet'])
również zwróci false, jeśli $data['keyShouldBeSet']
została ustawiona, ale została ustawiona na null
. Więc powyższa logika jest błędna.
Oto kolejny przykład:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; }
Powyższy kod zakłada, że jeśli $_POST['active']
zwróci true
, to postData
będzie koniecznie ustawione, a zatem isset($postData)
zwróci true
. Zatem odwrotnie, powyższy kod zakłada, że jedyny sposób, w jaki isset($postData)
zwróci false
, to jeśli $_POST['active']
również zwróci false
.
Nie.
Jak wyjaśniono, isset($postData)
również zwróci false
, jeśli $postData
została ustawiona na null
. Dlatego jest możliwe, że isset($postData)
zwróci false
, nawet jeśli $_POST['active']
zwróciło true
. Więc znowu, powyższa logika jest błędna.
A tak przy okazji, na marginesie, jeśli intencją w powyższym kodzie naprawdę było ponowne sprawdzenie, czy $_POST['active']
zwróciła prawdę, poleganie na isset()
w tym przypadku było kiepską decyzją o kodowaniu. Zamiast tego lepiej byłoby po prostu ponownie $_POST['active']
; tj:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; }
Jednak w przypadkach, w których ważne jest , aby sprawdzić, czy zmienna została naprawdę ustawiona (tj. aby odróżnić zmienną, która nie została ustawiona od zmiennej, która została ustawiona na null
), metoda array_key_exists()
jest znacznie bardziej niezawodna rozwiązanie.
Na przykład możemy przepisać pierwszy z dwóch powyższych przykładów w następujący sposób:
$data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // do this if 'keyShouldBeSet' isn't set }
Co więcej, łącząc array_key_exists()
z get_defined_vars()
, możemy wiarygodnie sprawdzić, czy zmienna w bieżącym zakresie została ustawiona, czy nie:
if (array_key_exists('varShouldBeSet', get_defined_vars())) { // variable $varShouldBeSet exists in current scope }
Powszechny błąd nr 3: Zamieszanie dotyczące zwracania przez odniesienie a zwracanie przez wartość
Rozważ ten fragment kodu:
class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
Jeśli uruchomisz powyższy kod, otrzymasz:
PHP Notice: Undefined index: test in /path/to/my/script.php on line 21
Co jest nie tak?
Problem polega na tym, że powyższy kod myli zwracanie tablic przez odniesienie z zwracaniem tablic przez wartość. O ile wyraźnie nie powiesz PHP, aby zwracał tablicę przez referencję (tj. używając &
), PHP domyślnie zwróci tablicę „według wartości”. Oznacza to, że zostanie zwrócona kopia tablicy, a zatem wywoływana funkcja i obiekt wywołujący nie będą uzyskiwać dostępu do tej samej instancji tablicy.
Zatem powyższe wywołanie getValues()
zwraca kopię tablicy $values
zamiast odniesienia do niej. Mając to na uwadze, przyjrzyjmy się dwóm kluczowym wierszom z powyższego przykładu:
// 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'];
Jednym z możliwych rozwiązań byłoby zapisanie pierwszej kopii tablicy $values
zwróconej przez getValues()
, a następnie wykonanie operacji na tej kopii; np:
$vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test'];
Ten kod będzie działał dobrze (tzn. wygeneruje test
bez generowania komunikatu „niezdefiniowany indeks”), ale w zależności od tego, co próbujesz osiągnąć, to podejście może być odpowiednie lub nie. W szczególności powyższy kod nie zmodyfikuje oryginalnej tablicy $values
. Jeśli więc chcesz, aby twoje modyfikacje (takie jak dodanie elementu „test”) wpłynęły na oryginalną tablicę, musisz zamiast tego zmodyfikować funkcję getValues()
, aby zwracała odwołanie do samej tablicy $values
. Odbywa się to poprzez dodanie &
przed nazwą funkcji, wskazując w ten sposób, że powinna ona zwrócić referencję; tj:
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'];
Wynikiem tego będzie test
, zgodnie z oczekiwaniami.
Ale żeby było bardziej zagmatwane, rozważ zamiast tego następujący fragment kodu:
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'];
Jeśli zgadłeś, że spowoduje to ten sam błąd „niezdefiniowanego indeksu”, co w naszym poprzednim przykładzie array
, to się myliłeś. W rzeczywistości ten kod będzie działał dobrze. Powodem jest to, że w przeciwieństwie do tablic PHP zawsze przekazuje obiekty przez referencję . ( ArrayObject
to obiekt SPL, który w pełni naśladuje użycie tablic, ale działa jako obiekt.)
Jak pokazują te przykłady, w PHP nie zawsze jest oczywiste, czy masz do czynienia z kopią, czy z referencją. Dlatego ważne jest, aby zrozumieć te domyślne zachowania (tj. zmienne i tablice są przekazywane przez wartość; obiekty są przekazywane przez referencję), a także dokładnie sprawdzić dokumentację API dla funkcji, którą wywołujesz, aby sprawdzić, czy zwraca ona wartość, kopia tablicy, odwołanie do tablicy lub odwołanie do obiektu.
Podsumowując, należy zauważyć, że praktyka zwracania referencji do tablicy lub ArrayObject
jest generalnie czymś, czego należy unikać, ponieważ daje ona wywołującemu możliwość modyfikowania prywatnych danych instancji. To „fruwa w twarz” enkapsulacji. Zamiast tego lepiej używać „getterów” i „setterów” w starym stylu, np.:
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'
Takie podejście daje wywołującemu możliwość ustawienia lub uzyskania dowolnej wartości w tablicy bez zapewniania publicznego dostępu do samej prywatnej tablicy $values
.
Częsty błąd nr 4: Wykonywanie zapytań w pętli
Często zdarza się, że napotkasz coś takiego, jeśli Twój PHP nie działa:
$models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); }
Chociaż może nie być tutaj absolutnie nic złego, ale jeśli będziesz postępować zgodnie z logiką w kodzie, może się okazać, że niewinnie wyglądające wywołanie $valueRepository->findByValue()
ostatecznie skutkuje jakimś zapytaniem, takim jak:
$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);
W rezultacie każda iteracja powyższej pętli skutkowałaby osobnym zapytaniem do bazy danych. Jeśli więc na przykład dostarczysz do pętli tablicę 1000 wartości, wygeneruje ona 1000 oddzielnych zapytań do zasobu! Jeśli taki skrypt zostanie wywołany w wielu wątkach, może potencjalnie doprowadzić system do zatrzymania się.
Dlatego ważne jest, aby rozpoznać, kiedy kod jest wykonywany i, jeśli to możliwe, zebrać wartości, a następnie uruchomić jedno zapytanie, aby pobrać wszystkie wyniki.
Jednym z przykładów dość powszechnego miejsca, w którym zapytania są wykonywane nieefektywnie (tj. w pętli), jest wysyłanie formularza z listą wartości (na przykład identyfikatory). Następnie, aby pobrać pełne dane rekordu dla każdego z identyfikatorów, kod przejdzie w pętli przez tablicę i wykona oddzielne zapytanie SQL dla każdego identyfikatora. Często będzie to wyglądać mniej więcej tak:
$data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); }
Ale to samo można osiągnąć znacznie wydajniej w pojedynczym zapytaniu SQL w następujący sposób:
$data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } }
Dlatego ważne jest, aby rozpoznać, kiedy zapytania są tworzone, bezpośrednio lub pośrednio, przez kod. Jeśli to możliwe, zbierz wartości, a następnie uruchom jedno zapytanie, aby pobrać wszystkie wyniki. Jednak i tam należy zachować ostrożność, co prowadzi nas do kolejnego powszechnego błędu PHP…
Powszechny błąd nr 5: fałszywe i nieefektywne wykorzystanie pamięci
Chociaż pobieranie wielu rekordów na raz jest zdecydowanie bardziej wydajne niż uruchamianie pojedynczego zapytania dla każdego wiersza do pobrania, takie podejście może potencjalnie prowadzić do stanu „braku pamięci” w libmysqlclient
podczas korzystania z rozszerzenia mysql
PHP.
Aby to zademonstrować, spójrzmy na pudełko testowe z ograniczonymi zasobami (512 MB RAM), MySQL i php-cli
.
Załadujemy tabelę bazy danych w następujący sposób:
// 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, teraz sprawdźmy wykorzystanie zasobów:
// 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";
Wyjście:

Before: 224704 Limit 1: 224704 Limit 10000: 224704
Fajny. Wygląda na to, że zapytanie jest bezpiecznie zarządzane wewnętrznie pod względem zasobów.
Jednak dla pewności podnieśmy limit jeszcze raz i ustawmy go na 100 000. O o. Kiedy to zrobimy, otrzymamy:
PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11
Co się stało?
Problemem jest sposób działania modułu mysql
PHP. To naprawdę tylko proxy dla libmysqlclient
, który wykonuje brudną robotę. Wybrana porcja danych trafia bezpośrednio do pamięci. Ponieważ ta pamięć nie jest zarządzana przez menedżera PHP, memory_get_peak_usage()
nie pokaże żadnego wzrostu wykorzystania zasobów, ponieważ zwiększamy limit w naszym zapytaniu. Prowadzi to do problemów, takich jak ten pokazany powyżej, gdzie wpadamy w samozadowolenie, myśląc, że nasze zarządzanie pamięcią jest w porządku. Ale w rzeczywistości nasze zarządzanie pamięcią jest poważnie wadliwe i możemy doświadczać problemów takich jak ten pokazany powyżej.
Możesz przynajmniej uniknąć powyższego headfake (chociaż samo to nie poprawi wykorzystania Twojej pamięci) używając zamiast tego modułu mysqlnd
. mysqlnd
jest skompilowany jako natywne rozszerzenie PHP i używa menedżera pamięci PHP.
Dlatego, jeśli przeprowadzimy powyższy test przy użyciu mysqlnd
zamiast mysql
, otrzymamy znacznie bardziej realistyczny obraz wykorzystania naszej pamięci:
Before: 232048 Limit 1: 324952 Limit 10000: 32572912
A tak przy okazji, jest jeszcze gorzej. Zgodnie z dokumentacją PHP, mysql
używa dwa razy więcej zasobów niż mysqlnd
do przechowywania danych, więc oryginalny skrypt używający mysql
naprawdę zużywał nawet więcej pamięci niż pokazano tutaj (w przybliżeniu dwa razy więcej).
Aby uniknąć takich problemów, rozważ ograniczenie rozmiaru zapytań i użycie pętli z małą liczbą iteracji; np:
$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"); }
Kiedy weźmiemy pod uwagę zarówno ten błąd PHP, jak i błąd nr 4 powyżej, zdajemy sobie sprawę, że istnieje zdrowa równowaga, którą Twój kod idealnie powinien osiągnąć między, z jednej strony, zbyt szczegółowymi i powtarzalnymi zapytaniami, a posiadaniem każdego z nich. poszczególne zapytania są za duże. Jak w przypadku większości rzeczy w życiu, potrzebna jest równowaga; albo skrajność nie jest dobra i może powodować problemy z niepoprawnym działaniem PHP.
Powszechny błąd nr 6: ignorowanie problemów z Unicode/UTF-8
W pewnym sensie jest to bardziej problem w samym PHP niż coś, na co można się natknąć podczas debugowania PHP, ale nigdy nie zostało to odpowiednio rozwiązane. Rdzeń PHP 6 miał być zgodny z Unicode, ale zostało to wstrzymane, gdy rozwój PHP 6 został zawieszony w 2010 roku.
Ale to w żadnym wypadku nie zwalnia programisty od prawidłowego posługiwania się kodem UTF-8 i unikania błędnego założenia, że wszystkie łańcuchy będą koniecznie „zwykłym starym ASCII”. Kod, który nie radzi sobie poprawnie z ciągami znaków spoza ASCII, jest znany z wprowadzania gnarly heisenbugs do twojego kodu. Nawet proste wywołania strlen($_POST['name'])
mogą powodować problemy, jeśli ktoś o nazwisku takim jak „Schrodinger” spróbuje zarejestrować się w twoim systemie.
Oto mała lista kontrolna, która pozwoli uniknąć takich problemów w kodzie:
- Jeśli nie wiesz zbyt wiele o Unicode i UTF-8, powinieneś przynajmniej nauczyć się podstaw. Tutaj jest świetny podkład.
- Pamiętaj, aby zawsze używać funkcji
mb_*
zamiast starych funkcji łańcuchowych (upewnij się, że rozszerzenie „wielobajtowe” jest zawarte w twojej kompilacji PHP). - Upewnij się, że Twoja baza danych i tabele są ustawione na używanie Unicode (wiele kompilacji MySQL nadal domyślnie używa
latin1
). - Pamiętaj, że
json_encode()
konwertuje symbole inne niż ASCII (np. „Schrodinger” staje się „Schr\u00f6dinger”), aleserialize()
nie . - Upewnij się, że pliki kodu PHP są również zakodowane w UTF-8, aby uniknąć kolizji podczas łączenia ciągów z zakodowanymi na stałe lub skonfigurowanymi stałymi ciągami.
Szczególnie cennym zasobem w tym zakresie jest post UTF-8 Primer dla PHP i MySQL autorstwa Francisco Claria na tym blogu.
Powszechny błąd nr 7: Zakładając, że $_POST
zawsze będzie zawierał Twoje dane POST
Pomimo swojej nazwy tablica $_POST
nie zawsze zawiera dane POST i można ją łatwo znaleźć jako pustą. Aby to zrozumieć, spójrzmy na przykład. Załóżmy, że wykonujemy żądanie serwera z jQuery.ajax()
w następujący sposób:
// js $.ajax({ url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json' });
(Nawiasem mówiąc, zwróć uwagę na contentType: 'application/json'
tutaj. Wysyłamy dane w formacie JSON, który jest dość popularny w przypadku interfejsów API. Jest to ustawienie domyślne, na przykład w przypadku publikowania w usłudze AngularJS $http
.)
Po stronie serwera w naszym przykładzie po prostu zrzucamy tablicę $_POST
:
// php var_dump($_POST);
Co zaskakujące, rezultatem będzie:
array(0) { }
Czemu? Co się stało z naszym ciągiem JSON {a: 'a', b: 'b'}
?
Odpowiedź jest taka, że PHP analizuje automatycznie ładunek POST tylko wtedy, gdy ma typ zawartości application/x-www-form-urlencoded
lub multipart/form-data
. Powody tego są historyczne — te dwa typy zawartości były zasadniczo jedynymi używanymi wiele lat temu, kiedy implementowano $_POST
w PHP. Tak więc w przypadku innych typów treści (nawet tych, które są obecnie dość popularne, jak application/json
), PHP nie ładuje automatycznie ładunku POST.
Ponieważ $_POST
jest superglobalną, jeśli nadpiszemy ją raz (najlepiej na początku naszego skryptu), zmodyfikowana wartość (tj. łącznie z ładunkiem POST) będzie możliwa do odniesienia w całym kodzie. Jest to ważne, ponieważ $_POST
jest powszechnie używany przez frameworki PHP i prawie wszystkie niestandardowe skrypty do wyodrębniania i przekształcania danych żądania.
Na przykład podczas przetwarzania ładunku POST z typem zawartości application/json
, musimy ręcznie przeanalizować zawartość żądania (tj. zdekodować dane JSON) i nadpisać zmienną $_POST
w następujący sposób:
// php $_POST = json_decode(file_get_contents('php://input'), true);
Następnie, gdy zrzucamy tablicę $_POST
, widzimy, że poprawnie zawiera ona ładunek POST; np:
array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }
Powszechny błąd nr 8: myślenie, że PHP obsługuje typ danych znakowych
Spójrz na ten przykładowy fragment kodu i spróbuj zgadnąć, co zostanie wydrukowane:
for ($c = 'a'; $c <= 'z'; $c++) { echo $c . "\n"; }
Jeśli odpowiedziałeś od „a” do „z”, możesz być zaskoczony tym, że się myliłeś.
Tak, wypisze „a” do „z”, ale potem wypisze również „aa” do „yz”. Zobaczmy dlaczego.
W PHP nie ma typu danych char
; dostępny jest tylko string
. Mając to na uwadze, zwiększenie string
z
PHP daje aa
:
php> $c = 'z'; echo ++$c . "\n"; aa
Aby jednak jeszcze bardziej zmylić sprawy, aa
jest leksykograficznie mniejsze niż z
:
php> var_export((boolean)('aa' < 'z')) . "\n"; true
Dlatego przykładowy kod przedstawiony powyżej drukuje litery od a
do z
, ale następnie drukuje również od aa
do yz
. Zatrzymuje się, gdy osiągnie za
, czyli pierwszą napotkaną wartość, która jest „większa niż” z
:
php> var_export((boolean)('za' < 'z')) . "\n"; false
W związku z tym, oto jeden ze sposobów poprawnej pętli przez wartości od „a” do „z” w PHP:
for ($i = ord('a'); $i <= ord('z'); $i++) { echo chr($i) . "\n"; }
Lub alternatywnie:
$letters = range('a', 'z'); for ($i = 0; $i < count($letters); $i++) { echo $letters[$i] . "\n"; }
Powszechny błąd nr 9: ignorowanie standardów kodowania
Chociaż ignorowanie standardów kodowania nie prowadzi bezpośrednio do konieczności debugowania kodu PHP, nadal jest to prawdopodobnie jedna z najważniejszych rzeczy do omówienia tutaj.
Ignorowanie standardów kodowania może spowodować masę problemów w projekcie. W najlepszym przypadku skutkuje to niespójnym kodem (ponieważ każdy programista „robi swoje”). Ale w najgorszym przypadku tworzy kod PHP, który nie działa lub może być trudny (czasem prawie niemożliwy) w nawigacji, co bardzo utrudnia debugowanie, ulepszanie, konserwację. A to oznacza zmniejszoną produktywność Twojego zespołu, w tym wiele zmarnowanego (lub przynajmniej niepotrzebnego) wysiłku.
Na szczęście dla programistów PHP istnieje Zalecenie dotyczące standardów PHP (PSR), składające się z następujących pięciu standardów:
- PSR-0: Standard automatycznego ładowania
- PSR-1: Podstawowy standard kodowania
- PSR-2: Przewodnik po stylach kodowania
- PSR-3: Interfejs rejestratora
- PSR-4: Autoloader
PSR został pierwotnie stworzony na podstawie danych wejściowych od opiekunów najbardziej uznanych platform na rynku. Zend, Drupal, Symfony, Joomla i inne przyczyniły się do powstania tych standardów i teraz je stosują. Nawet PEAR, który przez lata starał się być standardem, teraz uczestniczy w PSR.
W pewnym sensie prawie nie ma znaczenia, jaki jest twój standard kodowania, o ile zgadzasz się na standard i się go trzymasz, ale przestrzeganie PSR jest ogólnie dobrym pomysłem, chyba że masz w swoim projekcie ważny powód, aby zrobić inaczej . Coraz więcej zespołów i projektów jest zgodnych z PSR. Tt jest zdecydowanie uznawane w tym momencie za „standard” przez większość programistów PHP, więc korzystanie z niego pomoże upewnić się, że nowi programiści będą zaznajomieni z Twoim standardem kodowania, kiedy dołączą do Twojego zespołu.
Powszechny błąd nr 10: Niewłaściwe użycie empty()
Niektórzy programiści PHP lubią używać empty()
do sprawdzania wartości logicznych dla prawie wszystkiego. Są jednak przypadki, w których może to prowadzić do zamieszania.
Najpierw wróćmy do tablic i instancji ArrayObject
(które naśladują tablice). Biorąc pod uwagę ich podobieństwo, łatwo założyć, że tablice i instancje ArrayObject
będą zachowywać się identycznie. Okazuje się jednak, że jest to niebezpieczne założenie. Na przykład w 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?
Co gorsza, wyniki byłyby inne przed 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)
Takie podejście jest niestety dość popularne. Na przykład jest to sposób, w jaki Zend\Db\TableGateway
z Zend Framework 2 zwraca dane podczas wywoływania metody current()
na TableGateway::select()
, jak sugeruje dokument. Z takimi danymi programista może łatwo stać się ofiarą tego błędu.
Aby uniknąć tych problemów, lepszym podejściem do sprawdzania pustych struktur tablicowych jest użycie 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)
Nawiasem mówiąc, ponieważ PHP rzutuje 0
na false
, count()
może być również użyte w warunkach if ()
do sprawdzenia pustych tablic. Warto również zauważyć, że w PHP count()
jest stałą złożonością (operacja O(1)
) na tablicach, co jeszcze bardziej wyjaśnia, że jest to właściwy wybór.
Innym przykładem, kiedy empty()
może być niebezpieczne, jest połączenie go z funkcją magicznej klasy __get()
. Zdefiniujmy dwie klasy i miejmy w obu właściwość test
.
Najpierw zdefiniujmy klasę Regular
, która zawiera test
jako normalną właściwość:
class Regular { public $test = 'value'; }
Następnie zdefiniujmy klasę Magic
, która używa magicznego operatora __get()
, aby uzyskać dostęp do swojej właściwości test
:
class Magic { private $values = ['test' => 'value']; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } }
OK, teraz zobaczmy, co się stanie, gdy spróbujemy uzyskać dostęp do właściwości test
każdej z tych klas:
$regular = new Regular(); var_dump($regular->test); // outputs string(4) "value" $magic = new Magic(); var_dump($magic->test); // outputs string(4) "value"
Jak dotąd dobrze.
Ale teraz zobaczmy, co się stanie, gdy wywołamy metodę empty()
na każdym z nich:
var_dump(empty($regular->test)); // outputs bool(false) var_dump(empty($magic->test)); // outputs bool(true)
Uch. Jeśli więc polegamy na empty()
, możemy zostać zmyleni i uwierzyć, że właściwość test
$magic
jest pusta, podczas gdy w rzeczywistości jest ustawiona na 'value'
.
Niestety, jeśli klasa używa magicznej funkcji __get()
do pobrania wartości właściwości, nie ma niezawodnego sposobu sprawdzenia, czy ta wartość właściwości jest pusta, czy nie. Poza zakresem klasy można naprawdę tylko sprawdzić, czy zostanie zwrócona wartość null
, co nie musi oznaczać, że odpowiedni klucz nie jest ustawiony, ponieważ w rzeczywistości mógł być ustawiony na null
.
W przeciwieństwie do tego, jeśli spróbujemy odwołać się do nieistniejącej właściwości instancji klasy Regular
, otrzymamy komunikat podobny do następującego:
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
Tak więc głównym punktem tutaj jest to, że metoda empty()
powinna być używana z ostrożnością, ponieważ może prowadzić do mylących – lub nawet potencjalnie mylących – wyników, jeśli nie jest się ostrożnym.
Zakończyć
Łatwość użycia PHP może uśpić programistów do fałszywego poczucia komfortu, narażając ich na długotrwałe debugowanie PHP z powodu niektórych niuansów i dziwactw języka. Może to spowodować, że PHP nie będzie działać i problemy takie jak te opisane w niniejszym dokumencie.
Język PHP ewoluował znacząco w ciągu swojej 20-letniej historii. Zapoznanie się z jego subtelnościami jest wartościowym przedsięwzięciem, ponieważ pomoże zapewnić, że tworzone oprogramowanie będzie bardziej skalowalne, niezawodne i łatwe w utrzymaniu.