deftcode
English
← Powrót do bloga

Podstawy PHP w pigułce: Operatory i sterowanie przepływem

Przeczytaj po angielsku
phpkurs phpoperatory

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 zwraca

Powyż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 kosza

Przypisanie 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 $a i $b to 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:

views/status.php
<?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ż tylko elseif, a else if to 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 2
foreach ($arr as $key => $value) { } // $key: 'a', $value: 1; potem $key: 'b', $value: 2

Warunek liczy się w każdym obiegu

// ❌ count() wywoływane przy każdym obiegu
for ($i = 0; $i < count($a); $i++) { }
// ✅ policzone raz
for ($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 5

To 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 istnieje

Po 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ści
foreach ($a as &$v) { } // w każdym obiegu: $v = &$a[$i]; — referencja do elementu

Kombinacja 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]
Diagram: po pętli po referencji zmienna $v wskazuje na trzeci element tablicy, więc druga pętla nadpisuje go w każdym obiegu i tablica kończy jako 1, 2, 2
Po pierwszej pętli $v to nadal referencja do $a[2]. Druga pętla przypisuje do $v, czyli pisze wprost do $a[2].

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 instrukcje
switch ($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 instrukcja
switch 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.

match nie zastępuje switch całkowicie: switch potrafi wykonać wiele instrukcji w jednej gałęzi, a match przyjmuje wyłącznie pojedyncze wyrażenie. Jeśli do uzyskania wyniku potrzebujesz kilku instrukcji, warto przenieść je do osobnej funkcji lub metody i w match tylko 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 roboczego

Plik może coś zwrócić

To popularny i czytelny sposób wczytywania konfiguracji w PHP — w ten sposób trzyma konfigurację np. Laravel:

config.php
<?php
return [
'db' => ['host' => 'localhost'],
];

Plik, który potrzebuje konfiguracji, po prostu przypisuje wynik require do zmiennej:

src/bootstrap.php
<?php
declare(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 bufora
include __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 buforowanie

Dzię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ą:

helpers.php
<?php
$user = 'admin';

i plik index.php, który wczytuje go, mając już własną zmienną $user:

index.php
<?php
$user = 'jan';
require __DIR__ . '/helpers.php';
echo $user; // admin — nic w index.php tego nie zapowiada

W 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.

Komentarze