Podstawy PHP w pigułce: Operatory i sterowanie przepływem
O czym jest ta część
Operatory w PHP wyglądają praktycznie tak samo jak w każdym innym języku programowania — co sprawia, że bywają zdradliwe. Większość działa dokładnie tak, jak się spodziewasz, ale trafiają się też takie, na debugowanie których przeznaczysz całe popołudnie.
Ta część jest o tych wyjątkach, o sterowaniu przepływem za pomocą instrukcji warunkowych, pętli, switch
i match, a także o wczytywaniu plików. Porównania == i === omówiliśmy już w drugiej części, więc
tutaj tylko się do nich odwołam.
Przegląd operatorów
| Grupa | Operatory |
|---|---|
| Arytmetyczne | + - * / % ** (potęgowanie, łączy się w prawo) |
| Przypisania | = += -= *= /= %= **= .= ??= |
| Porównania | == != <> === !== < > <= >= <=> |
| Logiczne | && || ! oraz and or xor |
| Tekstowe | . (konkatenacja) |
| Bitowe | & | ^ ~ << >> |
| Wyciszanie błędów | @ |
| Typu | instanceof |
Dwie uwagi już na starcie. Pierwsza dotyczy konkatenacji, za którą odpowiada kropka ., a nie plus + —
'a' + 'b' rzuci wyjątkiem TypeError, zamiast połączyć oba stringi. Druga dotyczy operatorów logicznych
and, or i xor, które nie są synonimami && i ||. Za chwilę zobaczysz dlaczego.
Operator @
$data = @file_get_contents($path); // wycisza ostrzeżenie, gdy pliku nie ma — $data będzie wtedy false@ tłumi diagnostykę wyrażenia, przed którym stoi. Mimo że może sprawiać wrażenie wygodnego rozwiązania,
prawie zawsze jest złą praktyką: ukrywa nie tylko ten błąd, którego się spodziewasz, ale też każdy inny w tym
samym wyrażeniu — literówkę w nazwie zmiennej, brak uprawnień, wyczerpaną pamięć. Sam błąd nie znika, tylko przestaje być widoczny: gdy pliku nie ma,
file_get_contents() zwraca false, więc $data jest false, a w logach nie zostaje po tym żaden ślad.
Zamiast tego sprawdź warunek jawnie albo złap wyjątek.
Inkrementacja i dekrementacja
$i++; // post-inkrementacja: zwraca STARĄ wartość, potem zwiększa++$i; // pre-inkrementacja: zwiększa, potem zwracaPowyższy kod działa tak, jak możesz się tego spodziewać, mając doświadczenie w innych językach programowania. Kod poniżej — już nie:
$n = null; $n++; // 1 — inkrementacja null działa$n = null; $n--; // null — dekrementacja null NIE ROBI NIC (Warning)
$s = 'abc'; $s++; // 'abd' — inkrementacja alfanumeryczna w stylu Perla (Deprecated)$s = 'abc'; $s--; // 'abc' — dekrementacja stringa NIE ROBI NIC (Deprecated)Ta asymetria nie ma dobrego uzasadnienia — jest historyczna. Inkrementacja stringów idzie dalej, niż się
spodziewasz: 'az' daje 'ba', a 'Zz' daje 'AAa', bo przeniesienie propaguje się w lewo jak w liczniku.
PHP stopniowo wygasza te zachowania. W PHP 8.5 dekrementacja null generuje ostrzeżenie, a ++ i -- na
nienumerycznym stringu są oznaczone jako przestarzałe. Jeśli naprawdę potrzebujesz „następnego” stringa, użyj
funkcji str_increment() (dostępnej od PHP 8.3) — str_increment('az') daje 'ba'.
Z pozostałymi typami jest podobnie. Na wartościach logicznych ++ i -- nic nie zmieniają i generują
ostrzeżenie, a na tablicach, obiektach i zasobach rzucają TypeError.
Praktyczny wniosek: nie polegaj na ++ na czymkolwiek innym niż liczba. Najczęściej trafisz na to przy
zliczaniu wystąpień w tablicy:
$tags = ['php', 'js', 'php'];$counts = []; // tag => liczba wystąpień
foreach ($tags as $tag) { $counts[$tag] ??= 0; // przy pierwszym wystąpieniu tagu ustaw 0 $counts[$tag]++;}
// $counts: ['php' => 2, 'js' => 1]Bez linii z ??= wynik byłby taki sam, ale przy każdym nowym tagu PHP wygeneruje ostrzeżenie
Undefined array key, bo ++ próbuje odczytać klucz, którego w tablicy jeszcze nie ma.
Pułapka priorytetów
Poniżej najważniejszy fragment całej tej sekcji.
$result = true and false;
var_dump($result); // true (!)Przyczyna: = ma wyższy priorytet niż and. Parser widzi więc:
($result = true) and false; // to jest w praktyce: przypisz true do $result, // a potem policz `true and false` i wyrzuć wynik do koszaPrzypisanie wykonuje się pierwsze, jego wynik (true) trafia do and, a rezultat całego wyrażenia jest
odrzucany. To, co chciałeś napisać, to:
$result = true && false; // false&& i || mają priorytet wyższy niż =, więc zachowują się intuicyjnie. and i or mają najniższy
priorytet w całym języku — niższy nawet od przypisania.
Tabela priorytetów
Od najwyższego do najniższego:
| Priorytet | Operatory |
|---|---|
| 1 | ** |
| 2 | ++ -- ~ rzutowania @ |
| 3 | instanceof |
| 4 | ! |
| 5 | * / % |
| 6 | + - |
| 7 | << >> |
| 8 | . (od PHP 8 niżej niż + i -) |
| 9 | < <= > >= |
| 10 | == != === !== <> <=> |
| 11–13 | &, potem ^, potem | |
| 14–15 | &&, potem || |
| 16 | ?? (łączy się w prawo) |
| 17 | ? : (od PHP 8 niełączny — zagnieżdżanie wymaga nawiasów) |
| 18 | = += -= … |
| 19–21 | and, xor, or |
Niełączny znaczy, że
$a ? 'x' : $b ? 'y' : 'z'bez nawiasów to błąd kompilacji. Trzeba napisać$a ? 'x' : ($b ? 'y' : 'z'). Dawniej PHP liczył to od lewej, odwrotnie niż pozostałe języki, dlatego tę składnię najpierw oznaczono jako przestarzałą (7.4), a potem zakazano (8.0).
Nie ucz się jej na pamięć. Zapamiętaj dwie rzeczy: przypisanie jest niżej niż &&, ale wyżej niż and,
oraz że w razie wątpliwości nawiasy nic nie kosztują.
Zmiana w PHP 8:
'suma: ' . $a + $b(gdzie$ai$bto liczby) dawniej parsowało się jako('suma: ' . $a) + $b— czyli próba dodania liczby do stringa. Teraz.ma niższy priorytet niż+, więc wyrażenie znaczy'suma: ' . ($a + $b). Rozsądniej, ale miej na uwadze, że nie jest to zmiana wstecznie kompatybilna i w starszych wersjach języka może to działać inaczej.
Jedyne sensowne zastosowanie and/or to właśnie ich niski priorytet — jak np. stary i już rzadko używany
idiom $f = fopen(…) or die(…).
W nowym kodzie po prostu ich nie używaj.
Instrukcje warunkowe
W PHP wyglądają tak samo jak w C, Javie czy JavaScripcie. Warto zwrócić uwagę na dwie rzeczy: elseif
zwykle pisze się jednym słowem, a w szablonach HTML jest do dyspozycji osobna składnia.
if ($a) { // …} elseif ($b) { // …} else { // …}W szablonach wygodniejsza jest składnia alternatywna, bo nie gubisz nawiasów klamrowych w gąszczu HTML-a:
<?php if ($isActive): ?> <p>Aktywny</p><?php elseif ($isPending): ?> <p>Oczekuje</p><?php else: ?> <p>Nieaktywny</p><?php endif; ?>Analogicznie działają endforeach;, endfor;, endwhile; i endswitch;.
W wersji z klamrami PHP przyjmie też
else if(dwa słowa) i zadziała ono identycznie, ale powszechnie stosuje sięelseif— zaleca go też standard PSR-12 i tego warto się trzymać. W składni alternatywnej działa już tylkoelseif, aelse ifto tam błąd parsowania.
Pętle
PHP ma cztery pętle znane z innych języków w stylu C: for, while, do-while oraz foreach do
przechodzenia po tablicach. Sama składnia niczym nie zaskakuje — ciekawiej robi się przy szczegółach, które
omawiam w dalszej części sekcji.
for ($i = 0; $i < 10; $i++) { }
while ($cond) { }
do { // ciało wykona się co najmniej raz} while ($cond);
$arr = ['a' => 1, 'b' => 2];
foreach ($arr as $value) { } // $value: 1, potem 2foreach ($arr as $key => $value) { } // $key: 'a', $value: 1; potem $key: 'b', $value: 2Warunek liczy się w każdym obiegu
// ❌ count() wywoływane przy każdym obiegufor ($i = 0; $i < count($a); $i++) { }
// ✅ policzone razfor ($i = 0, $n = count($a); $i < $n; $i++) { }Akurat przykład z count() nie jest do końca trafiony, bo na tablicy to tania operacja — PHP trzyma rozmiar
tablicy w jej strukturze i niczego nie przelicza. Gdyby jednak w warunku było np. zapytanie do bazy,
wykonywałoby się przy każdym obiegu i to już miałoby realne znaczenie.
break i continue w zagnieżdżonych pętlach
Po break i continue można podać liczbę, która mówi, ile poziomów zagnieżdżenia obejmuje instrukcja. 1
(wartość domyślna) oznacza bieżącą pętlę, 2 — pętlę, która ją otacza, i tak dalej.
$rows = [[1, null, 2], [3, 4], [5, 'stop', 6], [7]];
foreach ($rows as $row) { // pętla zewnętrzna foreach ($row as $cell) { // pętla wewnętrzna if ($cell === null) { continue 2; // następny obieg ZEWNĘTRZNEJ pętli, czyli następny $row } if ($cell === 'stop') { break 2; // wyjście z obu pętli naraz } echo $cell, ' '; }}// wypisze: 1 3 4 5To rozwiązuje problem, dla którego w innych językach używa się etykiet albo flagi.
Uwaga na switch wewnątrz pętli. Dla break i continue PHP traktuje switch jak kolejny poziom
zagnieżdżenia, więc samo continue w switch zadziała jak break: wyjdzie tylko ze switch, zamiast przejść do
następnego obiegu pętli. Od PHP 7.3 generuje to ostrzeżenie. Żeby przejść do następnego elementu, należy
skorzystać z continue 2:
foreach ($items as $item) { switch ($item['type']) { case 'skip': continue 2; // następny $item — pomija resztę ciała foreach case 'stop': break 2; // wyjście ze switch i z foreach }
process($item);}foreach przez referencję — klasyczny błąd
Ten błąd składa się z dwóch rzeczy, które osobno są zupełnie niewinne.
Po pierwsze: pętla w PHP nie ma własnego zasięgu. Zmienna z foreach nie znika po pętli. Żyje dalej
tam, gdzie była pętla (w funkcji albo w skrypcie), i ma wartość z ostatniego obiegu:
foreach ([1, 2, 3] as $v) { }
echo $v; // 3 — $v nadal istniejePo drugie: & robi z $v referencję. Bez & zmienna pętli to kopia elementu. Z & to ten sam element,
więc zmiana $v zmienia tablicę:
$prices = [10, 20, 30];
foreach ($prices as &$price) { $price *= 2;}unset($price); // ← ZAWSZE po pętli po referencji
// $prices: [20, 40, 60]Najprościej myśleć o foreach jak o przypisaniu powtarzanym w każdym obiegu:
foreach ($a as $v) { } // w każdym obiegu: $v = $a[$i]; — kopia wartościforeach ($a as &$v) { } // w każdym obiegu: $v = &$a[$i]; — referencja do elementuKombinacja obu daje błąd. Jeśli pominiesz unset(), $v po pętli nadal jest referencją do ostatniego
elementu. Wystarczy potem druga, zwykła pętla z tą samą nazwą zmiennej:
$a = [1, 2, 3];
foreach ($a as &$v) { } // po pętli $v to nadal REFERENCJA do $a[2]foreach ($a as $v) { } // każdy obieg przypisuje do $v, czyli zapisuje do $a[2]
var_dump($a); // [1, 2, 2] — nie [1, 2, 3]Druga pętla nie tworzy nowej zmiennej — wykonuje $v = $a[$i] na istniejącym $v, a ten wciąż jest
referencją do $a[2]. Każde takie przypisanie ląduje więc w $a[2]. Obieg po obiegu:
| Obieg drugiej pętli | Do $v (czyli do $a[2]) trafia |
$a po obiegu |
|---|---|---|
| 1 | $a[0] = 1 |
[1, 2, 1] |
| 2 | $a[1] = 2 |
[1, 2, 2] |
| 3 | $a[2] = 2 (kopiuje sam siebie) |
[1, 2, 2] |
Dlatego reguła jest bezwarunkowa: po każdej pętli po referencji wywołuj unset(). Alternatywnie unikaj
referencji i użyj array_map(), jak w części o tablicach.
switch kontra match
Obie konstrukcje wybierają jedną z gałęzi na podstawie wartości, którą im podasz — w przykładach poniżej jest
to $code. PHP porównuje ją po kolei z wartościami gałęzi i wybiera pierwszą pasującą. Różnią się tym, co
dzieje się dalej: switch wykonuje instrukcje z gałęzi (przypisania, wywołania funkcji, cokolwiek),
a match jest wyrażeniem — każda gałąź zwraca jedną wartość, którą od razu przypisujesz albo zwracasz.
// switch — porównanie LUŹNE (==), przechodzi dalej, wykonuje instrukcjeswitch ($code) { case 200: case 201: $msg = 'OK'; break; // zapomnisz — poleci dalej do default default: $msg = 'Nieznany';}
// match (PHP 8) — porównanie ŚCISŁE (===), wyrażenie, bez przechodzenia dalej$msg = match ($code) { 200, 201 => 'OK', 404 => 'Nie znaleziono', default => 'Nieznany',}; // średnik na końcu — to wyrażenie, nie instrukcjaswitch |
match |
|
|---|---|---|
Porównanie $code z wartością gałęzi |
== luźne |
=== ścisłe |
| Przechodzenie do kolejnego przypadku | Tak — wymaga break |
Nie |
| Zwraca wartość | Nie (instrukcja) | Tak (wyrażenie) |
Brak dopasowania bez default |
Po cichu nic nie robi | Rzuca \UnhandledMatchError |
| Ciało gałęzi | Wiele instrukcji | Tylko jedno wyrażenie |
Dwa wiersze z tej tabeli robią największą różnicę w praktyce. Porównanie ścisłe oznacza, że
match ('200') nie trafi w gałąź 200 — i bardzo dobrze, bo switch w tej sytuacji trafiłby po cichu.
Brak dopasowania rzuca wyjątkiem, więc dorzucenie nowej wartości do enuma bez obsłużenia jej w match
wyjdzie od razu, a nie po miesiącu.
match (true) zamiast łańcucha elseif
$tier = match (true) { $points >= 1000 => 'gold', $points >= 100 => 'silver', default => 'bronze',};Gałęzie sprawdzane są po kolei, pierwsza prawdziwa wygrywa. To czytelniejszy zapis tego, co inaczej byłoby
łańcuchem if/elseif z powtarzanym przypisaniem.
matchnie zastępujeswitchcałkowicie:switchpotrafi wykonać wiele instrukcji w jednej gałęzi, amatchprzyjmuje wyłącznie pojedyncze wyrażenie. Jeśli do uzyskania wyniku potrzebujesz kilku instrukcji, warto przenieść je do osobnej funkcji lub metody i wmatchtylko ją wywołać:
$price = match ($customerType) { 'b2b' => calculateB2bPrice($order), // cała logika rabatów siedzi w funkcji default => $order->basePrice,};Jest jeszcze jedna różnica, o której warto wiedzieć: switch i match obliczają swój argument raz,
a łańcuch if/elseif wylicza warunek osobno w każdej gałęzi. Ma to znaczenie, gdy argumentem jest kosztowne
wywołanie — np. funkcja, która odpytuje bazę.
include i require
Prawdziwy projekt nie mieści się w jednym pliku. Konfiguracja, funkcje pomocnicze i szablony HTML lądują
w osobnych plikach, więc potrzebny jest sposób, żeby w jednym pliku użyć kodu z innego. Do tego służą
include i require: wczytują wskazany plik PHP i wykonują go w miejscu wywołania, tak jakby jego treść była
wklejona właśnie tam. Obie instrukcje robią to samo, dopóki plik istnieje — różnią się tym, co się dzieje,
gdy go nie ma:
| Gdy pliku brak | Do czego | |
|---|---|---|
include |
Ostrzeżenie, skrypt leci dalej | Opcjonalne szablony |
require |
Rzuca Error — niezłapany zatrzymuje skrypt |
Wszystko niezbędne |
include_once / require_once |
Jak wyżej, ale pilnuje, żeby nie wczytać dwa razy | Stary kod; dziś zastąpione autoładowaniem |
Domyślnym wyborem jest require. include ma sens tylko wtedy, gdy brak pliku naprawdę jest dopuszczalny.
require __DIR__ . '/../config.php'; // __DIR__ uniezależnia ścieżkę od katalogu roboczegoPlik może coś zwrócić
To popularny i czytelny sposób wczytywania konfiguracji w PHP — w ten sposób trzyma konfigurację np. Laravel:
<?php
return [ 'db' => ['host' => 'localhost'],];Plik, który potrzebuje konfiguracji, po prostu przypisuje wynik require do zmiennej:
<?phpdeclare(strict_types=1);
$config = require __DIR__ . '/../config.php';return postawione bezpośrednio w pliku — poza jakąkolwiek funkcją czy klasą — kończy wykonywanie tego
pliku, a zwrócona wartość staje się wynikiem require. Z include działa to dokładnie tak samo. Co zwracają
obie instrukcje w zależności od sytuacji:
Plik z return |
Plik bez return |
Pliku brak | |
|---|---|---|---|
require |
zwróconą wartość | 1 |
rzuca Error — niezłapany zatrzymuje skrypt |
include |
zwróconą wartość | 1 |
ostrzeżenie i false, skrypt leci dalej |
Przechwycenie wyjścia szablonu
Szablon wczytany przez include od razu wypisuje HTML, który trafia prosto do przeglądarki. Czasem jednak
chcesz go mieć w zmiennej — żeby wysłać mailem, zapisać do pliku albo wstawić w większy layout:
ob_start(); // od teraz wszystko, co miałoby zostać wypisane, trafia do buforainclude __DIR__ . '/views/invoice.php'; // szablon wypisuje HTML — ale do bufora, nie do przeglądarki$html = ob_get_clean(); // pobierz zawartość bufora jako string i wyłącz buforowanieDzięki buforowaniu wyjścia HTML, który szablon normalnie wysłałby do przeglądarki, ląduje w zmiennej $html.
Na tym mechanizmie opiera się wiele prostych silników szablonów w PHP.
Uważaj na jedną rzecz: wczytywany plik widzi wszystkie zmienne z miejsca, w którym go wczytano, i może je zmieniać. Weźmy plik, który ustawia zmienną:
<?php$user = 'admin';i plik index.php, który wczytuje go, mając już własną zmienną $user:
<?php$user = 'jan';require __DIR__ . '/helpers.php';
echo $user; // admin — nic w index.php tego nie zapowiadaW małym skrypcie to wygoda, w większym projekcie pułapka: żeby zrozumieć, skąd wzięła się wartość zmiennej,
musisz przejrzeć każdy wczytany plik. Dlatego lepiej, żeby plik oddawał dane przez return (jak config.php
wyżej), a nie ustawiał zmiennych po cichu.
W nowoczesnym kodzie require piszesz właściwie raz — dla vendor/autoload.php. Resztą zajmuje się
autoładowanie PSR-4 Composera. Composerowi i autoładowaniu poświęcę osobną serię o zaawansowanym PHP.
Co dalej
Znasz już operatory, instrukcje warunkowe, pętle i wczytywanie plików. Czas na funkcje, dzięki którym ten sam kod można wykorzystać wiele razy.
W następnej części: definiowanie funkcji w PHP, typy parametrów i wartości zwracanych, argumenty nazwane
i wariadyczne, przekazywanie przez referencję, a potem closures — function () use ($x), funkcje strzałkowe,
składnia f(...) i to, czym w PHP jest callable.