Podstawy PHP w pigułce: Typy danych i rzutowanie
O czym jest ta część
W poprzedniej części włączyliśmy declare(strict_types=1) i powiedziałem, że to najważniejsza linijka w pliku.
Teraz zobaczysz dlaczego — bo pod spodem PHP jest językiem, który bardzo chce zamienić jedną rzecz w drugą,
i robi to według reguł, których nie da się wywnioskować z intuicji.
Ta część to katalog tych reguł. Nie musisz umieć wszystkiego na pamięć — zwrócę uwagę na dwie rzeczy, które
naprawdę trzeba mieć w głowie: listę wartości falsy i zasady porównywania operatorem ==. Reszta to
materiał, do którego możesz wracać w razie potrzeby.
Jak PHP traktuje typy
PHP jest typowany dynamicznie — informację o typie niesie wartość przypisana do zmiennej, a nie zmienna sama w sobie. Ta sama zmienna może po kolei trzymać liczbę, string i tablicę, a interpreter w żaden sposób nie będzie protestował.
W praktyce znaczy to, że zmiennej nie da się otypować. Deklaracje typów istnieją w PHP tylko w trzech miejscach:
function f(int $x): string { } // ✅ parametr i typ zwracany
class A{ public int $count = 0; // ✅ właściwość klasy (od PHP 7.4)}
int $x = 2; // ❌ błąd składni — takiej konstrukcji po prostu nie ma$x = 2; // ✅ jedyna możliwa formaSam język tego braku nie nadrabia — nie ma składni, którą dałoby się przypilnować typu zwykłej zmiennej.
Nadrabiają go za to narzędzia analizy statycznej: PHPStan i Psalm czytają kod bez uruchamiania go i potrafią
wyłapać, że w zmiennej wylądowało coś innego, niż wynika z reszty programu. Rozumieją też adnotację @var, która
dla samego PHP jest zwykłym komentarzem, a dla nich deklaracją wiążącą:
/** @var int $count */$count = $repository->count();W praktyce to właśnie one zasypują tę lukę i w poważniejszych projektach PHP są dziś standardem — wrócimy do nich w serii o narzędziach.
PHP jest też typowany słabo — domyślnie zamienia typy w locie, żeby operacja mogła się wykonać. To drugie
wyłączasz przez declare(strict_types=1), ale tylko tam, gdzie wartość jest gdzieś przekazywana: przy
podawaniu argumentu do funkcji, przy zwracaniu z niej wyniku i przy przypisaniu do typowanej właściwości klasy.
W zwykłych wyrażeniach ta zamiana — nazywana żonglowaniem typami — działa dalej, niezależnie od
trybu ścisłego.
<?phpdeclare(strict_types=1);
function double(int $n): int{ return $n * 2;}
double('5'); // TypeError — TU pilnuje, bo wartość idzie do funkcji
$x = '5' + 3; // int(8) — TU nie pilnuje, to zwykłe wyrażenievar_dump('5' == 5); // true — porównanie też działa jak zawszeTo rozróżnienie jest ważne, bo łatwo wyjść z założenia, że jedna linijka „naprawia typowanie w PHP”. Ona pilnuje tych trzech miejsc. Cała reszta zostaje Twoją odpowiedzialnością.
Kategorie typów
| Kategoria | Typy |
|---|---|
| Skalarne | bool, int, float, string |
| Złożone | array, object, callable, iterable |
| Specjalne | null, resource |
resource to relikt — wskazanie na coś, czym zarządza sam PHP poza Twoim kodem: otwarty plik, połączenie z bazą,
strumień. Nowe rozszerzenia PHP konsekwentnie zamieniają go na normalne obiekty, więc spotkasz go głównie
w starszym API.
Sprawdzanie typu
var_dump($x); // typ + wartość — najlepsze do debugowania
gettype($x); // 'integer', 'double', 'boolean' — stare nazwy, NIE używajget_debug_type($x); // 'int', 'float', 'bool', 'App\User' — PHP 8, używaj TEGO
is_int($x); is_float($x); is_string($x); is_bool($x);is_array($x); is_null($x); is_object($x);is_callable($x); is_iterable($x); is_numeric($x);Tak wygląda wyjście var_dump() dla kilku typowych wartości — zwróć uwagę, że przy stringu podaje jeszcze jego
długość, a przy tablicy liczbę elementów:
var_dump(42); // int(42)var_dump(4.2); // float(4.2)var_dump('hi'); // string(2) "hi"var_dump(true); // bool(true)var_dump(null); // NULLvar_dump([1, 'a']); // array(2) { [0]=> int(1) [1]=> string(1) "a" }Różnica między gettype() a get_debug_type() nie jest kosmetyczna. gettype() zwraca 'double' dla floata
i 'integer' dla inta — nazwy, których nie da się wstawić do deklaracji typu ani porównać z nazwą klasy. Dla
obiektu zwraca po prostu 'object'. get_debug_type() zwraca dokładnie to, co napisałbyś w kodzie, łącznie
z pełną nazwą klasy. Do komunikatów błędów i logów jest jedynym sensownym wyborem.
is_numeric()sprawdza, czy string da się potraktować jak liczbę:is_numeric('1e3')totrue,is_numeric(' 12')też, bo spacja z przodu jest dozwolona. Za tois_numeric('12 '), ze spacją na końcu, zwracatruedopiero od PHP 8.0.
Rzutowanie
Rzutowanie jawne
(int) '12abc'; // 12 — parsuje wiodącą część liczbową i przestaje(int) 'abc'; // 0(int) ''; // 0(float) '1.5e3'; // 1500.0(bool) '0'; // false ← zaskakujące(bool) '0.0'; // true ← jeszcze bardziej(string) true; // '1'(string) false; // '' ← pusty string, NIE '0'(array) $obj; // publiczne właściwości stają się kluczamiDwie rzeczy z tej listy wracają najczęściej. Po pierwsze (int) na stringu nie jest walidacją — '12abc'
zamienia się w 12 bez cienia ostrzeżenia. Jeśli sprawdzasz dane wejściowe, użyj filter_var() albo
ctype_digit(), nigdy samego rzutowania.
Po drugie (string) false daje pusty string, a nie '0'. To regularnie psuje sklejanie stringów i zapisy do
bazy.
Funkcyjne odpowiedniki
intval('0x1A', 16); // 26 — drugi argument to podstawa systemufloatval('1.5abc'); // 1.5strval(3.0); // '3'boolval('0'); // false
settype($x, 'integer'); // zmienia typ samej zmiennej $x, zwraca boolintval() z podstawą to jedyna rzecz, której (int) nie umie. Poza tym wybieraj składnię z nawiasami, czyli
(int) '12abc' zamiast intval('12abc') — jest czytelniejsza i szybsza.
Rzutowanie na obiekt
(object) ['a' => 1]; // stdClass z właściwością ->a(object) [1, 2]; // klucze liczbowe stają się właściwościami "0" i "1"$o->{0}; // ← wymagana składnia z klamrami; $o->0 to błąd parsowania(object) 'hi'; // stdClass { scalar: 'hi' } — tak samo dla int/float/bool(object) null; // pusty stdClassW praktyce spotkasz to przy JSON-ie, bo json_decode() domyślnie zwraca stdClass, a nie tablicę:
json_decode($json); // stdClass — DOMYŚLNIEjson_decode($json, true); // tablica asocjacyjnaPrawie zawsze chcesz
truew drugim argumencie — ale nie dlatego, że tablica jest „bezpieczniejsza”. Ani tablica, anistdClassnie mają zadeklarowanej struktury: w obu przypadkach IDE nie podpowie Ci nazw pól, a sięgnięcie po nieistniejące skończy się ostrzeżeniem i wartościąnull. Przewaga tablicy jest czysto praktyczna — działa na niej??, destrukturyzacja i cały zestaw funkcjiarray_*, a zagnieżdżone dane nie mieszają Ci tablic z obiektami. Prawdziwym rozwiązaniem jest własna klasa i do tego wrócimy w części dziesiątej.
Wartości falsy
Falsy to wartości, które PHP uznaje za fałsz wszędzie tam, gdzie spodziewa się warunku. Po polsku mówi się o nich „wartości fałszywe”, ale w praktyce usłyszysz raczej angielskie falsy i truthy.
Ta lista jest krótka i warto ją znać na pamięć, bo decyduje o zachowaniu każdego if, while, empty()
i operatora ?: w Twoim kodzie.
false0-00.0-0.0'''0'[]nullWszystko inne jest truthy. W szczególności truthy są:
'0.0' // string, który wygląda jak zero' ' // spacja'false' // string 'false''[]' // string wyglądający jak pusta tablica-1 // każda liczba różna od zera, także ujemna0.00001[0] // tablica z jednym fałszywym elementem — sama nie jest pustanew stdClass() // każdy obiekt, nawet bez właściwościNajczęstsza wpadka to '0'. Jeżeli formularz przysyła numer strony, ilość albo kod pocztowy, wartość '0'
przejdzie przez if ($value) jak brak danych:
$qty = '0';
if ($qty) { } // ❌ nie wejdzie — '0' jest fałszyweif ($qty !== '') { } // ✅ jawne sprawdzenie tego, o co Ci chodziif (isset($qty)) { } // ✅ gdy pytasz o istnienie, nie o wartośćPorównania: == kontra ===
Obiecałem to na końcu poprzedniej części, więc zaczynamy od najważniejszej zmiany w PHP 8.
=== porównuje typ i wartość. == najpierw żongluje typami, potem porównuje wartości. I dokładnie te reguły
żonglerki zostały w PHP 8 przepisane.
// PHP 7 PHP 80 == 'foobar'; // true false0 == ''; // true false'abc' == 0; // true falseStara reguła brzmiała: przy porównaniu liczby ze stringiem string jest rzutowany na liczbę. Ponieważ 'abc'
rzutowane na int daje 0, wychodziło 0 == 'abc'. To było jedno z najczęstszych źródeł błędów bezpieczeństwa
w PHP: sprawdzenie w stylu $hasloZFormularza == 0 wychodziło prawdą dla dowolnego tekstu.
Nowa reguła jest odwrotna i o wiele rozsądniejsza: jeśli string nie jest liczbowy, to liczba jest rzutowana na string i porównanie idzie tekstowo. Jeśli string jest liczbowy — obie strony porównywane są jako liczby:
'1' == '01'; // true — oba stringi liczbowe, porównane jako liczby'10' == '1e1'; // true — notacja wykładnicza to też liczba100 == '1e2'; // true'abc' == 0; // false — 'abc' nie jest liczbowe, więc 0 staje się '0'null == false; // true[] == false; // true| Wyrażenie | Wynik | Dlaczego |
|---|---|---|
0 == 'abc' |
false |
string nieliczbowy → '0' == 'abc' |
0 == '0' |
true |
string liczbowy → porównanie liczbowe |
'1' == '01' |
true |
oba liczbowe → 1 == 1 |
'abc' == 'ABC' |
false |
== na stringach nie ignoruje wielkości liter |
null == 0 |
true |
oba fałszywe |
null === 0 |
false |
różne typy |
Reguła praktyczna: używaj
===, dopóki nie masz konkretnego powodu, żeby zrobić inaczej. Ta zasada oszczędzi Ci więcej czasu niż zapamiętanie całej tabeli powyżej.
Operator <=> — po angielsku spaceship operator, czyli statek kosmiczny, bo tak wygląda — porównuje dwie
wartości i mówi, która jest większa: zwraca -1, gdy lewa jest mniejsza, 0, gdy są równe, i 1, gdy lewa jest
większa. Dokładnie takiej odpowiedzi oczekują funkcje sortujące, którym podajesz własną regułę porównania:
1 <=> 2; // -1[1, 2, 3] <=> [1, 2, 3]; // 0
usort($users, fn($a, $b) => $a->age <=> $b->age);Pełną tabelę operatorów i pułapki priorytetów omówimy w części czwartej. Tutaj chodziło wyłącznie o to, co
== robi z typami.
Liczby całkowite
PHP_INT_MAX; // 9223372036854775807 — największy int na maszynie z 64-bitowym procesoremPHP_INT_MIN; // -9223372036854775808 — i najmniejszyPHP_INT_SIZE; // 8 — ile bajtów zajmuje jeden int (8 bajtów to właśnie 64 bity)Przepełnienie po cichu zmienia typ
W większości języków int, który wyjdzie poza swój zakres, przewija się na drugi koniec — w Javie czy C# dodanie jedynki do największego inta daje nagle liczbę ujemną:
// JavaInteger.MAX_VALUE // 2147483647Integer.MAX_VALUE + 1 // -2147483648 ← przewinęło się na drugi koniec zakresuPHP nie robi ani tego, ani nie rzuca wyjątkiem. Wynik po prostu zmienia typ na float:
var_dump(PHP_INT_MAX); // int(9223372036854775807)var_dump(PHP_INT_MAX + 1); // float(9.2233720368548E+18) ← to już nie jest intNie ma ostrzeżenia. Jeśli dalej w kodzie zakładasz int, dostaniesz TypeError w zupełnie innym miejscu niż
źródło problemu.
Zapis literałów
0b1010; // dwójkowo = 100o17; // ósemkowo = 15 (składnia z PHP 8.1)017; // ósemkowo = 15 (stara składnia — łatwa do przeoczenia)0x1A; // szesnastkowo = 261_000_000; // separator cyfr (7.4+) = 1000000Stara notacja ósemkowa to realna pułapka: 017 nie jest siedemnastką. Jeśli wklejasz uprawnienia plików albo
kody, wiodące zero zmienia znaczenie liczby.
Podkreślenia to funkcja parsera, nie formatu danych. W stringu nic nie znaczą:
(int) '2_00'daje2, a nie200— rzutowanie zatrzymuje się na pierwszym znaku niebędącym cyfrą.
Liczby zmiennoprzecinkowe
PHP_FLOAT_MAX; // 1.7976931348623157E+308 — największa liczba, jaką float udźwigniePHP_FLOAT_MIN; // 2.2250738585072E-308 — najmniejsza DODATNIA, nie najmniejsza w ogólePHP_FLOAT_DIG; // 15 — tyle cyfr znaczących jest jeszcze wiarygodnychPHP_FLOAT_EPSILON; // 2.220446049250313E-16 — najmniejsza różnica, jaką float w ogóle odróżniZwróć uwagę na PHP_FLOAT_MIN — w odróżnieniu od PHP_INT_MIN nie jest to najmniejsza możliwa liczba, tylko
najmniejsza dodatnia. Najmniejszą w ogóle jest -PHP_FLOAT_MAX.
Nigdy nie porównuj floatów przez ==
var_dump(0.1 + 0.2 == 0.3); // falseTo nie jest wada PHP — tak działa IEEE 754, czyli standard zapisu liczb zmiennoprzecinkowych, którego używa
praktycznie każdy język. Ułamków takich jak 0.1 nie da się zapisać dokładnie w systemie dwójkowym, więc pod
spodem siedzi liczba odrobinę inna od tej, którą wpisałeś. Najłatwiej to zobaczyć, wypisując wynik z pełną
dokładnością:
echo 0.1 + 0.2; // 0.3 ← tak to WYGLĄDAvar_dump(0.1 + 0.2); // float(0.30000000000000004)printf('%.20f', 0.1 + 0.2); // 0.30000000000000004441 ← tak jest NAPRAWDĘecho zaokrągla przy wypisywaniu do czternastu cyfr znaczących, więc pokazuje ładne 0.3 — i to właśnie dlatego
błąd jest tak trudny do wyśledzenia. Porównanie widzi prawdziwą wartość, nie tę wypisaną. Poprawnie sprawdza się
więc, czy różnica mieści się w granicy błędu:
var_dump(abs((0.1 + 0.2) - 0.3) < PHP_FLOAT_EPSILON); // trueGranicą błędu jest tu PHP_FLOAT_EPSILON ze stałych powyżej: różnica mniejsza niż ona oznacza, że dla typu
float to po prostu ta sama liczba.
Ten sam mechanizm psuje zaokrąglanie w dół:
floor((0.1 + 0.7) * 10); // 7, nie 8 — klasyczna pułapka reprezentacji binarnejPieniądze
Nigdy nie trzymaj kwot w typie float. Masz dwie sensowne opcje:
// 1. Liczby całkowite w najmniejszej jednostce (grosze) — najprostsze i najszybsze$price = 1999; // 19,99 zł
// 2. Rozszerzenie bcmath / gmp, gdy potrzebujesz arytmetyki dziesiętnejbcadd('0.1', '0.2', 2); // '0.30'W części dziesiątej zbudujemy z tego value object Money, który pilnuje jednostki i waluty za Ciebie.
Dzielenie, reszta i zaokrąglanie
intdiv(10, 3); // 3 — dzielenie całkowite: resztę ucina, nie zaokrąglaintdiv(10, 0); // DivisionByZeroError — dzielenie przez zero rzuca wyjątkiem
10 % 3; // 1 — reszta z dzielenia, działa TYLKO na liczbach całkowitych10.5 % 3; // 1 — 10.5 zostaje po cichu ucięte do 10, dopiero potem reszta-10 % 3; // -1 ← znak wyniku bierze się z LEWEJ strony, nie z prawejfmod(10.5, 3); // 1.5 — reszta dla floatów, zachowuje część ułamkową
fdiv(1, 0); // INF — jedyne dzielenie przez zero, które NIE rzuca wyjątkufdiv(-1, 0); // -INFfdiv(0, 0); // NAN — bo 0/0 nie ma żadnego sensownego wyniku
floor(7.9); // 7.0 — zaokrągla w dół, ale zwraca FLOAT, nie intceil(7.1); // 8.0 — zaokrągla w górę, też FLOATround(2.5); // 3.0 — połówki zaokrągla w górę, czyli w stronę od zeraround(-2.5); // -3.0 — stąd minus dwa i pół idzie do minus trzechround(2.345, 2); // 2.35 — drugi argument to liczba miejsc po przecinkuDwie rzeczy do zapamiętania: floor() i ceil() zwracają float, więc pod strict_types=1 nie wstawisz ich
wprost tam, gdzie wymagany jest int — potrzebujesz (int) floor($x) albo intdiv(). I % zawsze pracuje na
intach, więc 10.5 % 3 po cichu zamieni 10.5 na 10.
Zmiana w PHP 8:
1 / 0rzuca terazDivisionByZeroErrorzamiast wypisywać ostrzeżenie i zwracaćINF. Jeśli świadomie chceszINF, użyjfdiv().
NAN; // stała: „to nie jest liczba" (Not a Number) — wynik działania bez sensownej wartościINF; // stała: nieskończoność
log(-1); // NAN — logarytm z liczby ujemnej nie istniejesqrt(-1); // NAN — pierwiastek z ujemnej też niePHP_FLOAT_MAX * 2; // INF — wynik nie mieści się już w typie float
is_nan(sqrt(-1)); // true — jedyny sposób, żeby wykryć NAN (patrz niżej)is_infinite(PHP_FLOAT_MAX * 2); // true — czy to nieskończonośćis_finite(1.5); // true — zwykła liczba: ani NAN, ani nieskończonośćNAN ma jeszcze jedną cechę, która zaskakuje: nie równa się samemu sobie. NAN == NAN to false, więc do
sprawdzenia użyj is_nan().
Stringi
Apostrofy kontra cudzysłowy
To pierwsza rzecz, która różni PHP od większości języków — rodzaj cudzysłowu zmienia zachowanie.
$name = 'Paweł';
echo 'Cześć $name\n'; // dosłownie: Cześć $name\necho "Cześć $name\n"; // Cześć Paweł + znak nowej liniiApostrofy traktują tekst dosłownie: $name zostaje w wyniku jako $name, a \n jako ukośnik i litera n.
Rozumieją tylko dwa zapisy z ukośnikiem: \' wstawia apostrof w środku tekstu, a \\ sam ukośnik.
Cudzysłowy robią dwie rzeczy więcej. Po pierwsze interpolują, czyli w miejsce nazwy zmiennej wstawiają jej
wartość. Po drugie traktują ukośnik jako zapowiedź znaku, którego nie da się wpisać z klawiatury wprost: \n to
przejście do nowej linii, \t tabulator, \u{1F600} emoji. Takie zapisy nazywa się sekwencjami ucieczki
(escape sequences) i działają tylko w cudzysłowach.
Przy bardziej złożonych wyrażeniach potrzebne są klamry:
echo "Wartość: {$obj->prop['key']}"; // klamry wymaganeecho "Tablica: $arr[key]"; // klucz BEZ cudzysłowów — tylko w składni prostejecho "Tablica: {$arr['key']}"; // składnia z klamrami — z cudzysłowamiPraktyczna reguła: używaj apostrofów domyślnie, cudzysłowów gdy naprawdę interpolujesz. Nie chodzi o wydajność (różnica jest pomijalna), tylko o to, że apostrof jasno mówi „tu nie ma nic do podstawienia”.
Heredoc i nowdoc
Heredoc zachowuje się jak cudzysłów — interpoluje:
$html = <<<HTML <div class="card"> <h1>$title</h1> </div> HTML;Od PHP 7.3 znacznik zamykający może być wcięty, a to wcięcie jest usuwane z każdej linii treści. Znacznik nie może być wcięty bardziej niż treść — to błąd parsowania.
Nowdoc zachowuje się jak apostrof — nie interpoluje niczego:
$sql = <<<'SQL' SELECT * FROM users WHERE name = '$name' SQL;Nowdoc jest właściwym wyborem wszędzie tam, gdzie tekst zawiera znaki dolara, których nie chcesz podstawiać: zapytania SQL, szablony, fragmenty JavaScriptu, wyrażenia regularne.
Najczęściej używane funkcje
| Funkcja | Co robi | Przykład → wynik |
|---|---|---|
strlen($s) |
Długość w bajtach | strlen('żółw') → 7 |
mb_strlen($s) |
Długość w znakach | mb_strlen('żółw') → 4 |
str_contains($h, $n) |
Czy zawiera fragment (PHP 8) | str_contains('Hello', 'ell') → true |
str_starts_with() / str_ends_with() |
Test prefiksu / sufiksu (PHP 8) | str_starts_with('Hello', 'He') → true |
strpos($h, $n) |
Pozycja pierwszego wystąpienia albo false |
strpos('Hello', 'l') → 2 |
substr($s, $start, $len) |
Wycinek stringa | substr('Hello', 1, 3) → 'ell' |
str_replace($find, $rep, $subj) |
Podmienia każde wystąpienie | str_replace('l', 'L', 'Hello') → 'HeLLo' |
sprintf($fmt, ...) |
Buduje sformatowany string | sprintf('%05.2f', 3.5) → '03.50' |
number_format($n, $dec) |
Formatuje liczbę do wyświetlenia | number_format(1234.5, 2) → '1,234.50' |
trim() / ltrim() / rtrim() |
Obcina białe znaki lub podane znaki | trim(' hi ') → 'hi' |
explode($delim, $s, $limit) |
Dzieli string na tablicę | explode(',', 'a,b,c') → ['a','b','c'] |
implode($glue, $arr) |
Skleja tablicę w string | implode('-', ['a','b']) → 'a-b' |
str_pad($s, $len, $pad, $type) |
Dopełnia do stałej szerokości | str_pad('7', 3, '0', STR_PAD_LEFT) → '007' |
ucfirst() / ucwords() |
Wielka pierwsza litera / każdego słowa | ucwords('hello world') → 'Hello World' |
strtolower() / strtoupper() |
Zmiana wielkości liter | strtolower('HeLLo') → 'hello' |
hash_equals($known, $user) |
Porównanie odporne na atak czasowy | do tokenów i HMAC-ów, nigdy === |
Ostatni wiersz wymaga słowa komentarza. hash_equals() porównuje dwa stringi zawsze przez tyle samo czasu,
niezależnie od tego, na którym znaku się różnią. Zwykłe === kończy pracę na pierwszej różnicy — a mierząc, jak
długo trwała odpowiedź serwera, da się zgadywać sekret znak po znaku. Dlatego tokeny, podpisy i skróty haseł
porównuje się wyłącznie przez hash_equals().
Pułapki z tej tabeli
strpos('Hello', 'H'); // 0 — a to jest FAŁSZYWE. Zawsze porównuj !== false // albo użyj str_contains(), gdy chcesz tylko tak/nie
str_replace('l', 'L', $s, $n); // $n jest PRZEZ REFERENCJĘ — dostaje liczbę podmianexplode('', 'abc'); // ValueError — pusty separatorsprintf('%1$s %1$s', 'hi'); // 'hi hi' — %N$s używa argumentu numer N ponownietrim($s, 'abc'); // drugi argument to LISTA ZNAKÓW, nie fragment teksturtrim('file.php', 'hp.'); // 'file' — usuwa znaki h, p i kropkę, nie sufiks '.php'number_format($n, 2, ',', ' '); // 3. i 4. argument: separator dziesiętny i tysięcyPierwsza z nich jest klasykiem. strpos() zwraca 0, gdy szukany fragment jest na początku — a 0 jest
wartością falsy, więc if (strpos($s, $n)) przeoczy trafienie na pozycji zerowej. Od PHP 8 masz
str_contains() i w większości przypadków to jest odpowiedź.
Ostatni argument trim() też regularnie zaskakuje: to zbiór znaków do obcięcia, nie sufiks. rtrim('file.php', '.php')
zadziała przypadkiem, ale rtrim('graph.php', '.php') zwróci 'gra', bo obetnie też h z graph.
Bajty kontra znaki
Wszystkie funkcje bez przedrostka mb_ operują na bajtach, nie na znakach. Dla ASCII to bez różnicy. Dla
czegokolwiek po polsku różnica jest ogromna.
strlen('żółw'); // 7 — bajty w UTF-8mb_strlen('żółw'); // 4 — znakiKonsekwencje wychodzą wszędzie tam, gdzie tniesz albo mierzysz tekst użytkownika:
substr('żółw', 0, 3); // 'żó' + połowa znaku — uszkodzony UTF-8mb_substr('żółw', 0, 3); // 'żół'
strtoupper('żółw'); // 'żółw' — nie ruszy polskich litermb_strtoupper('żółw'); // 'ŻÓŁW'
str_pad('żółw', 6); // doda 0 spacji — bo strlen to już 7Funkcje
mb_*nie są częścią samego PHP — pochodzą z rozszerzenia mbstring, czyli dodatkowego modułu, który uczy PHP liczyć znaki zamiast bajtów. Bez niegomb_strlen()po prostu nie istnieje i dostaniesz błąd o nieznanej funkcji. Zwykle jest już włączone, a sprawdzisz to przezextension_loaded('mbstring').Gdy je masz, traktuj
mb_*jako domyślny wybór dla wszystkiego, co pochodzi od użytkownika albo do niego trafia. Funkcje bajtowe zostaw do danych binarnych.
Null
null to typ z dokładnie jedną wartością. W PHP dochodzi do tego zestaw konstrukcji, które różnią się subtelnie —
i to właśnie te różnice są źródłem błędów.
isset($x); // false dla null; NIE ostrzega o niezdefiniowanej zmiennejempty($x); // true dla każdej wartości falsy; też nie ostrzegais_null($x); // true tylko dla null; OSTRZEGA gdy $x nie istnieje
$x ?? 'domyślna'; // podstawia, gdy $x nie istnieje albo jest null; nie ostrzega$x ??= 'domyślna'; // to samo, ale od razu przypisuje wynik do $x (7.4)$obj?->method(); // operator nullsafe (8.0)| Sprawdzenie | Niezdefiniowana | null |
'' |
0 |
'0' |
[] |
|---|---|---|---|---|---|---|
isset() |
false |
false |
true |
true |
true |
true |
empty() |
true |
true |
true |
true |
true |
true |
is_null() |
⚠ ostrzeżenie | true |
false |
false |
false |
false |
?? podstawia |
tak | tak | nie | nie | nie | nie |
Ta tabela mówi wszystko: isset() pyta o istnienie, empty() o prawdziwość. Jeśli zależy Ci na tym, żeby
0 i '0' były poprawnymi wartościami — a przy ilościach, cenach i identyfikatorach zależy — empty() jest
złym narzędziem.
Operator nullsafe
$country = $user?->getAddress()?->country;Kluczowa właściwość: ?-> przerywa cały łańcuch. Jeśli $user jest null, to getAddress() w ogóle się nie
wykona, country nie zostanie odczytane, a całe wyrażenie da null. Nie musisz stawiać ?-> przy każdym ogniwie
„na wszelki wypadek” — wystarczy tam, gdzie faktycznie może pojawić się null.
?? kontra ?:
Oba skracają zapis „weź to, a jak nie ma, to tamto”. Różnią się tym, co dla nich znaczy „nie ma”.
$v = $a ? $a : $b; // pełny zapis: jeśli $a jest prawdziwe, weź $a, w przeciwnym razie $b$v = $a ?: $b; // to samo, tylko krócej — $a nie trzeba pisać dwa razy$v = $a ?? $b; // podobne, ale sprawdza COŚ INNEGO?: — po angielsku Elvis operator, bo obrócony przypomina fryzurę i oczy — patrzy na prawdziwość, czyli
sprawdza dokładnie to samo, co if ($a). Podstawi wartość zapasową dla każdej wartości falsy z listy
z początku tej części.
?? patrzy na istnienie. Podstawia tylko wtedy, gdy zmiennej albo klucza w ogóle nie ma, albo gdy siedzi
tam null.
Najlepiej widać to na trzech przypadkach tego samego parametru z adresu:
// 1. parametru nie ma w adresie$params = [];$params['page'] ?: 1; // 1 + Warning: Undefined array key "page"$params['page'] ?? 1; // 1 — bez ostrzeżenia
// 2. parametr jest, ale pusty: ?page=$params = ['page' => ''];$params['page'] ?: 1; // 1 — '' jest fałszywe, więc leci wartość zapasowa$params['page'] ?? 1; // '' — klucz istnieje, więc zwraca to, co w nim siedzi
// 3. parametr to zero: ?page=0$params = ['page' => '0'];$params['page'] ?: 1; // 1 ← PUŁAPKA: '0' jest fałszywe, strona zerowa znika$params['page'] ?? 1; // '0' — poprawniePrawie zawsze chodzi Ci o ??: dane wejściowe albo są, albo ich nie ma, a '' czy '0' bywają poprawnymi
wartościami. Po ?: sięgaj wtedy, gdy naprawdę interesuje Cię prawdziwość — na przykład $nazwa ?: 'Anonim',
gdzie pusty string faktycznie ma zostać zastąpiony.
Na koniec pułapka, która potrafi zaskoczyć przy przepisywaniu kodu z innego języka: zagnieżdżone operatory
trójargumentowe od PHP 8 wymagają nawiasów. Wcześniej działały, ale wiązały się od lewej — odwrotnie niż
w większości języków — więc a ? b : c ? d : e znaczyło (a ? b : c) ? d : e. Od 7.4 PHP ostrzegał, od 8.0 jest
to błąd składni:
// ❌ PHP 8: Unparenthesized `a ? b : c ? d : e` is not supported$label = $score > 90 ? 'świetnie' : $score > 50 ? 'nieźle' : 'słabo';
// ✅ nawiasy mówią wprost, co z czym$label = $score > 90 ? 'świetnie' : ($score > 50 ? 'nieźle' : 'słabo');
// ✅ zwykle czytelniej — o match w części czwartej$label = match (true) { $score > 90 => 'świetnie', $score > 50 => 'nieźle', default => 'słabo',};Co dalej
Wiesz już, co PHP trzyma w zmiennych i według jakich reguł zamienia jedno w drugie. Brakuje struktury, która w PHP obsługuje właściwie wszystko: tablicy.
W następnej części zobaczysz, dlaczego jedna konstrukcja pełni w PHP rolę listy, mapy, zbioru, stosu i kolejki
naraz, jak działają klucze (i dlaczego '1' oraz 1 to ten sam klucz), oraz które z pięćdziesięciu
funkcji array_* warto faktycznie znać.